面试之道——手写call、apply和bind
嗨嗨嗨~这里是哆啦美玲分享的知识点,一起来学呀!
这次的文章基于我之前写的this的显式绑定
的文章,有不懂的可以倒回去看看哦——搞懂this,如此简单 - 掘金
call
、apply
和 bind
都是 JavaScript 中函数的调用方法,它们的作用是改变函数的上下文 (this
) 和传递参数,但它们之间有一些不同点。
首先我们看下面这段代码:
let obj = {
a: 1
}
function foo(x, y) {
console.log(this.a, x + y);
return 'hello'
}
console.log(foo(1, 2)); // undefined 3 hello
我们声明了一个对象obj和函数foo,在独立调用foo时,this
指向的是全局,所以this.a会返回undefiend
。那我们如何实现this指向obj呢?
一、call
1. call的特点
- call 方法立即调用一个函数,并且可以指定
this
的值,同时传入参数。 - 参数是按顺序传递的,多个参数使用逗号分隔。
const res = foo.call(obj, 3, 4) // call的this指向foo,foo的this指向obj
console.log(res); // 1 7 hello
2. 手写myCall(context, ...args)
在手写myCall之前我们需要分析:myCall写哪里?
如图的代码结果,foo既是函数也是对象,但是使用对象obj调用call方法会报错:
call is not a function
,所以myCall方法写在构造函数Function的原型(Function.prototype
)。
因为函数也是对象,所以foo.call()会导致call的this指向foo;call的执行会让obj调用foo,让foo的this指向obj;
foo在声明前需要多少参数call并不清楚,所以call可以使用...args
的形式接收剩余参数;另外,我们写的foo是有返回值的,在call的调用后会返回foo的结果,所以我们手写call的时候也要写返回值。
我们再看,如果call传入的参数中第一位不是对象,会得到什么?
从图上代码的输出结果可以看出来:call会把传入的参数除去第一位后按顺序交给foo,且第一位必须是对象才能改变this的指向。
根据分析,手写代码如下:
// 手写call
Function.prototype.myCall = function (context, ...args) { // foo.__proto__ == Function.prototype
if (typeof (this) !== 'function') { // 判断this必须是函数
throw new TypeError('Error')
}
context = context || window
const key = Symbol('fn') // 唯一的key
context[key] = this // this = foo
const res = context[key](...args) // 隐式绑定 this = context
delete context[key]
return res
}
console.log(foo.myCall(obj, 2, 3)); // 1 5 hello
代码第6行 context = context || window
的意思是:如果 context 变量已经有值(即不是 null 或 undefined),那么就使用 context 的值;如果 context 没有值(即为 null 或 undefined),就使用 window 作为默认值。
代码第8-9行是给foo函数创建一个唯一的key值,确保不会修改掉函数内部原本的属性值。
最后为了不修改原对象,要记得把新增的属性删除!
二、apply
1. apply的特点
- apply方法也立即调用一个函数,并指定
this
的值,同时传入参数。 - 参数传递方式是将一个数组或类数组对象作为参数列表传入。
const res1 = foo.apply(obj, [3, 4]) // apply的this指向foo, foo的this指向obj
console.log(res1); // 1 7 hello
2. 手写myApply
apply与call的用法是一样的,唯一不同的就是接收的参数不一样,所以只需要修改一点点就可以了,代码如下:
// 手写apply
Function.prototype.myApply = function (context, args) { // foo.__proto__ == Function.prototype
if (typeof (this) !== 'function') { // 判断this必须是函数
throw new TypeError('Error')
}
context = context || window
const key = Symbol('fn')
context[key] = this // this = foo
const res = context[key](...args) // 隐式绑定 this = context
delete context[key]
return res
}
console.log(foo.myApply(obj, [2, 4])); // 1 6 hello
三、bind
1. bind的特点
- bind方法并不立即调用函数,而是
返回一个新的函数
,新的函数会绑定指定的this
和参数。 - 这个返回的新函数可以在之后的某个时刻被调用,且新函数也可以接收零散参数。
- 当新函数被
new
调用时, 返回的是调用 bind 的那个函数的实例对象
const fn = foo.bind(obj, 4)
const res2 = fn(4)
console.log(res2); // 1 8 hello
const f = new fn(4)
console.log(f); // undefined 8 foo {}
从代码中可以看出:bind函数调用时,foo函数在接收参数时会先在bind传入的参数里面按顺序找,如果不够再去找bind返回的新函数f传入的参数
找。
另外,在代码的5-6行我们会发现,在new fn()
时,本来应该返回一个fn的实例对象fn{}
,但实际返回的却是foo的实例对象foo{}
,并且foo中的this指向了全局,所以在new的过程会导致this指向全局,fn()执行返回foo的实例对象。
2. 手写myBind
bind与前面两个方法很不一样,第一需要注意的点就是调用bind后会返回一个新的函数体。
接下来我们看看:如果bind传入的第一个不是参数,新函数会是什么?
从图中的结果可知:foo.bind(123)返回的是一个foo的实例对象,所以this指向的是全局。
另外,在前面我们已经说了如果我们new
foo.bind(obj)得到的新函数,也会得到一个foo的实例对象。所以bind返回的函数体不能是箭头函数,因为箭头函数里面没有this,不能被new。
这就需要我们区分new fn()
和fn()
:因为new会使函数的this直接指向得到的实例对象,且让实例对象的隐式原型等于构造函数的显式原型,所以我们采用判断this.__proto__ === F.prototype
来判断是否被new。
代码如下:
Function.prototype.myBind = function (context, ...args) {
if (typeof (this) !== 'function') { // 判断this必须是函数
throw new TypeError('Error')
}
context = context || window
const self = this // 存储this的值 foo
return function F(...args2) {
if(this.__proto__ === F.prototype){ // 被 new 直接返回 foo 实例,所以这里不能是箭头函数
return new self(...args, ...args2) // 返回foo 的实例对象
}else{
return self.apply(context, [...args, ...args2]) // foo指向obj,foo执行且返回值接收并返回
}
}
}
const fo = foo.myBind(obj, 1, 2)
console.log(fo(),'//////'); // 1 3 hello //////
const fun = new fo()
console.log( 'new fo() 得到的结果:', fun); // undefined 3 new fo() 得到的结果:foo {}
好啦,本次知识点分享完毕,家人们下次见!!!
喜欢这次的文章就麻烦点个赞赞啦~谢谢大家!