面试时,为了能够进一步试探你对 this 相关概念理解和掌握的深度, 面试官会考察你 call、apply 和 bind 的实现机制,甚至可能会要求你手写代码。但很多时候,如果你以前并不了解这三个函数的实现机制,光靠死记硬背那是不行的,毕竟人是容易健忘的动物。
因此,针对 call、 apply 和 bind,我们不仅要会用、会辨析,更要对其原理知根知底。接下来,我们将这三个方法的实现一步步带领大家解析,由浅入深,这样方能信手拈来、百战不殆。
在这之前,我们先要知道call、apply 和 bind 是干嘛的?如何使用?它们之间有哪些区别?
这里我给大家画了一张思维导图:
结合这张图来说明,相信大伙会清楚得多:
call、apply 和 bind,都是用来改变函数的 this 指向的。
call、apply 和 bind 之间的区别比较大,前者在改变 this 指向的同时,也会把目标函数给执行掉;后者则只负责改造 this,不作任何执行操作。
call、bind 和 apply 之间的区别,则体现在对入参的要求上。前者只需要将目标函数的入参逐个传入即可,后者则希望入参以数组形式被传入。
好了,在了解三者的作用和区别后,让我们来模拟实现一个 call/apply/bind 方法,三者在实现层面上非常相似,我们以 call 方法的实现为例,带大家深入理解一下这类方法的模拟思路:
call 方法的模拟
实现之前,先来看一个 call 的调用示范:
var student = {
name: 'xiaoming'
}
function showName() {
console.log(this.name);
}
showName.call(student); // xiaoming
结合 call 方法的特性,我们首先至少能想到以下两点:
结合这两点,我们一步一步来实现 call 方法。首先,改变 this 的指向:
showName 在调用 call 方法后,表现得就像是 student 这个对象的一个方法一样。
所以我们最直接的一个联想是,如果能把 showName 直接塞进 student 对象里就好了,像这样:
var student = {
name: 'xiaoming',
showName: function() {
console.log(this.name);
}
}
student.showName(); // xiaoming
遵循以上思路,我们来模拟一下 call 方法(注意看注释):
Function.prototype.myCall = function(context) {
// step1: 把函数挂到目标对象上(这里的 this 就是我们要改造的的那个函数)
context.fn = this;
// step2: 执行 fn 函数
context.fn();
}
这样我们基本实现了刚才的联想,简单测试下我们的 myCall 函数:
showName.myCall(student); // xiaoming
但是这样做有一个问题,用户在传入 student 这个对象的时候,想做的仅仅是让 call 把 showName 里的 this 指向 student,而不是想给 student 对象新增一个额外的 fn 方法。所以我们在执行完 fn 之后,还要记得把它给删掉:
Function.prototype.myCall = function(context) {
// step1: 把函数挂到目标对象上(这里的 this 就是我们要改造的的那个函数)
context.fn = this;
// step2: 执行 fn 函数
context.fn();
// step3: 删除 step1 中挂到目标对象上的 fn 函数,此举非“过河拆桥”,而是把目标对象”完璧归赵”
delete context.fn;
}
在这个例子中,myCall 的执行结果结果与 call 无差,我们已经实现了 “改变 this 的指向” 这个功能点。现在我们的 myCall 函数还需要具备读取函数入参的能力,类比于 call 的这种调用形式:
var student = {
name: 'xiaoming'
}
function showAge(age) {
console.log(`My name is ${
this.name}, I am ${
age} years old.`)
}
showAge.call(student, 18) // My name is xiaoming, I am 18 years old.
读取函数入参,具体来说其实是读取 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)
}
就能通过 args 这个数组拿到我们想要的入参了。把 args 数组代表的目标入参重新展开,传入目标方法里,就大功告成了:
Function.prototype.myCall = function(context, ...args) {
// step1: 把函数挂到目标对象上(这里的 this 就是我们要改造的的那个函数)
context.fn = this;
// step2: 执行 fn 函数
context.fn(...args);
// step3: 删除 step1 中挂到目标对象上的 fn 函数,此举非“过河拆桥”,而是把目标对象”完璧归赵”
delete context.fn;
}
接下来测试下我们的 myCall 函数:
var student = {
name: 'xiaoming'
}
function showAge(age) {
console.log(`My name is ${
this.name}, I am ${
age} years old.`)
}
showAge.myCall(student, 18) // My name is xiaoming, I am 18 years old.
以上,我们就成功模拟了一个 call 方法出来。
但还没完,考虑到函数的健壮性,我们还需要处理一些 边界 问题:
比如第一个参数传了 null 怎么办?是不是可以默认给它指到 window 去?函数如果是有返回值的话怎么办?是不是新开一个 result 变量存储一下这个值,最后 return 出来就可以了?等等…
让我们来完善一下这个 myCall 函数:
Function.proptotype.myCall = function(context, ...args){
if(typeof this !== 'function'){
throw new TypeError(`${
this} is not a function`);
}
context = context || window;
context.fn = this;
const result = context.fn(...args);
delete context.fn;
return result;
}
最后总结一下:
...
’ 扩展运算符可以帮助我们把一系列的入参变为数组;基于对 call 方法的理解,写出一个 apply 方法(更改读取参数的形式) 和 bind 方法(延迟目标函数执行的时机)不是什么难事,只需要大家在上面这段代码的基础上作改造即可。(前提是你对 apply 方法和 bind 方法的特性和用法要心知肚明~)。
apply 方法的模拟
apply 的实现与 call 非常类似,区别在于对参数的处理,在这就不做一一分析了。
Function.prototype.myApply = function(context, args){
if(typeof this !== 'function'){
throw new TypeError(`${
this} is not a function`);
}
if(args && !(args instanceof Array)){
throw new TypeError('TypeError: CreateListFromArrayLike called on non-object');
}
context = context || window;
context.fn = this;
const result = context.fn(args ? [...args] : '');
delete context.fn;
return result;
}
bind 方法的模拟
bind 的实现对比其他两个函数略微地复杂了一点,因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现:
Function.prototype.myBind = function (context, ...args) {
if (typeof this !== 'function') {
throw new TypeError(`${
this} is not a function`);
}
const _this = this;
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments);
}
return _this.apply(context, args.concat(...arguments));
}
}
分析:
面试中,要是你除了能讲解出以上的实现机制并手写出来,那面试官基本就露出认可的笑容了。