前言
在“多数情况下”,this 遵循的指向机制。在另外一些情况下 this 是不遵循这个机制的。改变 this 的指向,我们主要有两条路:
- 通过改变书写代码的方式做到(比如箭头函数)。
- 显式地调用一些方法来帮忙(比如call/apply/bind)。
改变书写代码的方式,进而改变 this 的指向
唱反调的箭头函数
var n = 1
var obj = {
n: 2,
// 声明位置
sayN: () => {
console.log(this.n)
}
}
// 调用位置
obj.sayN() // 1
当我们将普通函数改写为箭头函数时,箭头函数的 this 会在书写阶段(即声明位置)就绑定到它父作用域的 this 上。无论后续我们如何调用它,都无法再为它指定目标对象 —— 因为箭头函数的 this 指向是静态的,“一次便是一生”。
构造函数里的 this
function Person(name) {
this.name = name
console.log(this)
}
var person = new Person('icon') // Person {name: "icon"}
构造函数里面的 this 会绑定到我们 new 出来的这个对象上
显式地调用一些方法来帮忙
考虑到实际开发中我们改变 this 指向的场景非常多,所以这三种方法的使用在面试中考察的频率也比较高。最常见的考法,是问询三种方法的使用及区别。但很多时候,为了能够进一步试探你对 this 相关概念理解和掌握的深度, 面试官会考察你 call、apply 和 bind 的实现机制,甚至可能会要求你手写代码。
因此,针对 call、 apply 和 bind,我们不仅要会用、会辨析,更要对其原理知根知底。接下来,我们将这三种方法的考察方式汇聚到两道题里面,大家若能掌握这两个问题,就可以做到举一反三,知一解百。
基本问答题:call、apply 和 bind 是干嘛的?如何使用?它们之间有哪些区别?
结合这张图来说明,会清楚得多
call、apply 和 bind,都是用来改变函数的 this 指向的。
call、apply 和 bind 之间的区别比较大,前两者在改变 this 指向的同时,也会把目标函数给执行掉;后者则只负责改造 this,不作任何执行操作。
call 和 apply 之间的区别,则体现在对入参的要求上。前者只需要将目标函数的入参逐个传入即可,后者则希望入参以数组形式被传入。
进阶编码题:模拟实现一个 call/apply/bind 方法
call 方法的模拟
在实现 call 方法之前,我们先来看一个 call 的调用示范:
var me = {
name: 'icon'
}
function showName() {
console.log(this.name)
}
showName.call(me) // icon
结合 call 表现出的特性,我们首先至少能想到以下两点:
- call 是可以被所有的函数继承的,所以 call 方法应该被定义在 Function.prototype 上
- call 方法做了两件事:
- 改变 this 的指向,将 this 绑到第一个入参指定的的对象上去;
- 根据输入的参数,执行函数。
结合这两点,我们一步一步来实现 call 方法。首先,改变 this 的指向
showName 在 call 方法调用后,表现得就像是 me 这个对象的一个方法一样。
所以我们最直接的一个联想是,如果能把 showName 直接塞进 me 对象里就好了,像这样:
var me = {
name: 'icon',
showName: function() {
console.log(this.name)
}
}
me.showName()
但是这样做有一个问题,因为在 call 方法里,me 是一个入参:
showName.call(me) // icon
用户在传入 me 这个对象的时候, 想做的仅仅是让 call 把 showName 里的 this 给改掉,而不想给 me 对象新增一个 showName 方法。所以说我们在执行完 me.showName 之后,还要记得把它给删掉。遵循这个思路,我们来模拟一下 call 方法(注意看注释):
Function.prototype.myCall = function(context) {
// 1: 把函数挂到目标对象上(这里的 this 就是我们要改造的的那个函数)
context.func = this
// 2: 执行函数
context.func()
// 3: 删除 1 中挂到目标对象上的函数,把目标对象”完璧归赵”
delete context.func
}
试试
var me = {
name: 'icon'
}
function showName() {
console.log(this.name)
}
showName.myCall(me) // icon
到这里,我们已经实现了 改变 this 的指向 这个功能点。现在我们的 myCall 还需要具备读取函数入参的能力,类比于 call 的这种调用形式:
var me = {
name: 'icon'
}
function showFullName(surName) {
console.log(`${this.name} ${surName}`)
}
showFullName.call(me, 'lee') // icon lee
读取函数入参,具体来说其实是读取 call 方法的第二个到最后一个入参。要做到这一点,我们可以借助ES6数组的扩展符
// '...'这个扩展运算符可以帮助我们把一系列的入参变为数组
function readArr(...args) {
console.log(args)
}
readArr(1,2,3) // [1,2,3]
我们把这个逻辑用到我们的 myCall 方法里:
Function.prototype.myCall = function(context, ...args) {
console.log('入参是', args)
context.func = this
context.func()
delete context.func
}
就能通过 args 这个数组拿到我们想要的入参了。把 args 数组代表的目标入参重新展开,传入目标方法里,就大功告成了:
Function.prototype.myCall = function(context, ...args) {
context.func = this
// 执行函数,利用扩展运算符将数组展开
context.func(...args)
delete context.func
}
现在我们来测试一下功能完备的 myCall 方法:
Function.prototype.myCall = function(context, ...args) {
context.func = this
context.func(...args)
delete context.func
}
var me = {
name: 'icon'
}
function showFullName(surName) {
console.log(`${this.name} ${surName}`)
}
showFullName.myCall(me, 'lee') // icon lee
以上,我们就成功模拟了一个 call 方法出来。
基于这个最基本的 call 思路,大家还可以为这个方法作能力扩充:
比如如果我们第一个参数传了 null 怎么办?是不是可以默认给它指到 window 去?函数如果是有返回值的话怎么办?是不是新开一个 result 变量存储一下这个值,最后 return 出来就可以了?等等—— 这些都是小事儿。
当面试官问你 “如何模拟 call 方法的实现的时候”,他最想听的其实就楼上这两个核心功能点的实现思路,其它的,都是锦上添花
基于对 call 方法的理解,写出一个 apply 方法(更改读取参数的形式) 和 bind 方法(延迟目标函数执行的时机)不是什么难事,只需要大家在上面这段代码的基础上作改造即可。
apply方法的模拟
apply的实现和call非常相似,区别只是在参数的处理上。这里就不说多了,直接上代码(注意看注释):
Function.prototype.myApply = function(context, args){
// 1: 判断当前传参是否是数组
if(args && !(args instanceof Array)){
throw new TypeError('呀呀呀,参数必须是数组哦')
}
// 2: 上面说的 如果是null默认指向window
context = context || window
// 3: 把函数挂到目标对象上(这里的 this 就是我们要改造的的那个函数)
context.func = this
// 4: 执行函数并且存储上面说的 返回值
const result = context.func(args ? [...args] : '')
// 5: 删除 1 中挂到目标对象上的函数,把目标对象”完璧归赵”
delete context.func;
// 6: 返回结果值
return result;
}
bind方法的模拟
bind的实现稍微麻烦点,因为需要返回一个函数,需要判断些边界条件。这里就不说多了,直接上代码(注意看注释):
Function.prototype.myBind = function (context, ...args) {
// 1: 保存下当前 this(这里的 this 就是我们要改造的的那个函数)
const _this = this;
// 2: 返回一个函数
return function F() {
// 3: 因为返回了一个函数,除了直接调用还可以 new F(),所以需要判断分开走
// 4: new 的方式
if (_this instanceof F) {
return new _this(...args, ...arguments);
}
// 5: 直接调用,这里选择了 apply 的方式实现但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(…arguments);
return _this.apply(context, args.concat(...arguments));
}
}
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!