解析:这里我给大家画了一张思维导图:
结合这张图来说明,会清楚得多:
call、apply 和 bind,都是用来改变函数的 this 指向的。
call、apply 和 bind 之间的区别比较大,前者在改变 this 指向的同时,也会把目标函数给执行掉;后者则只负责改造 this,不作任何执行操作。
call 和 apply 之间的区别,则体现在对入参的要求上。前者只需要将目标函数的入参逐个传入即可,后者则希望入参以数组形式被传入。
进阶编码题:模拟实现一个 call/apply/bind 方法
在实现 call 方法之前,我们先来看一个 call 的调用示范:
var me = {
name: 'xiuyan'
}
function showName() {
console.log(this.name)
}
showName.call(me) // xiuyan
结合 call 表现出的特性,我们首先至少能想到以下两点:
-
call 是可以被所有的函数继承的,所以 call 方法应该被定义在 Function.prototype 上
-
call 方法做了两件事:
- 改变 this 的指向,将 this 绑到第一个入参指定的的对象上去;
- 根据输入的参数,执行函数。
结合这两点,我们一步一步来实现 call 方法。首先,改变 this 的指向:
showName 在 call 方法调用后,表现得就像是 me 这个对象的一个方法一样。
所以我们最直接的一个联想是,如果能把 showName 直接塞进 me 对象里就好了,像这样:
var me = {
name: 'xiuyan',
showName: function() {
console.log(this.name)
}
}
me.showName()
但是这样做有一个问题,因为在 call 方法里,me 是一个入参:
showName.call(me) // xiuyan
用户在传入 me 这个对象的时候, 想做的仅仅是让 call 把 showName 里的 this 给改掉,而不想给 me 对象新增一个 showName 方法。所以说我们在执行完 me.showName 之后,还要记得把它给删掉。遵循这个思路,我们来模拟一下 call 方法(注意看注释):
Function.prototype.myCall = function(context) {
// step1: 把函数挂到目标对象上(这里的 this 就是我们要改造的的那个函数)
context.func = this
// step2: 执行函数
context.func()
// step3: 删除 step1 中挂到目标对象上的函数,把目标对象”完璧归赵”
delete context.func
}
有兴趣的同学,可以测试一下我们的 myCall:
showName.myCall(me) // xiuyan
在我们这个例子里,myCall 的执行结果结果与 call 无差,撒花~~
到这里,我们已经实现了 “改变 this 的指向” 这个功能点。现在我们的 myCall 还需要具备读取函数入参的能力,类比于 call 的这种调用形式:
var me = {
name: 'Chris'
}
function showFullName(surName) {
console.log(`${this.name} ${surName}`)
}
showFullName.call(me, 'Lee') // Chris Lee
读取函数入参,具体来说其实是读取 call 方法的第二个到最后一个入参。要做到这一点,我们可以借助数组的扩展符
// '...'这个扩展运算符可以帮助我们把一系列的入参变为数组
function readArr(...args) {
console.log(args)
}
readArr(1,2,3) // [1,2,3]
我们把这个逻辑用到我们的 myCall 方法里:
Function.prototype.myCall = function(context, ...args) {
...
console.log('入参是', args)
}
就能通过 args 这个数组拿到我们想要的入参了。把 args 数组代表的目标入参重新展开,传入目标方法里,就大功告成了:
Function.prototype.myCall = function(context, ...args) {
// step1: 把函数挂到目标对象上(这里的 this 就是我们要改造的的那个函数)
context.func = this
// step2: 执行函数,利用扩展运算符将数组展开
context.func(...args)
// step3: 删除 step1 中挂到目标对象上的函数,把目标对象”完璧归赵”
delete context.func
}
现在我们来测试一下功能完备的 myCall 方法:
Function.prototype.myCall = function(context, ...args) {
// step1: 把函数挂到目标对象上(这里的 this 就是我们要改造的的那个函数)
context.func = this
// step2: 执行函数,利用扩展运算符将数组展开
context.func(...args)
// step3: 删除 step1 中挂到目标对象上的函数,把目标对象”完璧归赵”
delete context.func
}
var me = {
name: 'Chris'
}
function showFullName(surName) {
console.log(`${this.name} ${surName}`)
}
showFullName.myCall(me, 'Lee') // Chris Lee
结果与 call 方法无差!
以上,我们就成功模拟了一个 call 方法出来。
基于这个最基本的 call 思路,大家还可以为这个方法作能力扩充:
比如如果我们第一个参数传了 null 怎么办?是不是可以默认给它指到 window 去?函数如果是有返回值的话怎么办?是不是新开一个 result 变量存储一下这个值,最后 return 出来就可以了?等等等等 —— 这些都是小事儿。当面试官问你 “如何模拟 call 方法的实现的时候”,他最想听的其实就楼上这两个核心功能点的实现思路,其它的,都是锦上添花~
基于对 call 方法的理解,写出一个 apply 方法(更改读取参数的形式) 和 bind 方法(延迟目标函数执行的时机)不是什么难事,只需要大家在上面这段代码的基础上作改造即可。(前提是你对 apply 方法和 bind 方法的特性和用法要心知肚明~)。
Function.prototype.myCall = function (context, ...args) {
context.func = this;
context.func(...args);
delete context.func;
}
Function.prototype.myApply = function (context, arr) {
context.func = this;
context.func(...arr);
delete context.func;
}
Function.prototype.myBind = function (context, ...args) {
let self = this;
return function () {
self.call(context, ...args)
}
}
Function.prototype.myApply(context, arr){
var context = context | window
context.func = this
var result
if (!arr) {
result = context.func()
} else {
context.func(arr)
}
delete context.func
return result
}