前言
无论在面试时还是使用中,难免会遇到改变this指向的问题,这时我们便会想到call、apply、bind,可对于他们的底层是如何实现,大多数人不太清楚,如果你对他们还不了解,先看看mdn的call、apply、bind。
本文尽量用简洁的语言讲解他们的用法,底层实现思路,模拟实现 call、apply、bind
模拟call
使用一个指定的 this 值和单独给出一个或多个参数来调用一个函数。
function.call(this, arg1, arg2, arg3, ...)
根据定义得知,call()方法有两个作用,一是改变 this 指向,另外一个传递参数,如下:
const obj = { value: '我是obj.value', } function fn(arg) { console.log(this.value, arg); // '我是obj.value', 2 } fn.call(obj, 2);
上面的例子,使用 call()
方法使函数fn
的 this
指向了obj
,所以 this.value 的值为 我是obj.value
。
那么如果不使用 call()
方法,该如何实现呢?
call 实现思路
不考虑使用 call、apply、bind 方法,上面例子 fn 函数如何能拿到 obj 里面的 value 值呢?
改造一下上面的例子
const obj = { value: '我是obj.value', fn: function() { console.log(this.value); // '我是obj.value' } } obj.fn();
这样一改,this
就指向了 obj
,根据这个思路,封装一个将传入的 this转换的方法,那么当前 this
的指向就是我们想要的结果。
需要注意fn函数不能写成箭头函数,因为箭头函数没有this。
所以模拟的步骤为:
- 将函数设置为传入对象的属性;
- 执行该函数;
- 删除该属性; 上面的例子就可以改写为:
// 给obj添加属性func obj.func = fn; // 执行函数 obj.func(); // 删除添加的属性 delete obj.func;
开始模拟
根据上面的思路,来模拟实现一版 call()方法。
Function.prototype.myCall = function (obj) { obj.func = this; obj.func(); delete obj.func; }
看下这个极简版 call
方法,能否能正确改变 this。
const obj = { value: '我是obj.value', } function fn(arg) { console.log(this.value, arg); // '我是obj.value', undefined } fn.myCall(obj, 2);
实现了!!! 根据上面例子,已经能正确改变 this 的指向!!!
但是传入的值却没有拿到,考虑的传入的值是不确定的,只能借助Arguments 对象(通过它可以拿到所有传入的参数)
现在还有两个问题:
- 传入的值却没有拿到,我们可以借助Arguments 对象,再加上es6的'...'操作符将事半功倍。
- 自带的
call()
是有返回值的,我们目前还没有,解决这个问题很好办,在封装的 call 方法里面,将执行的函数结果存下来,return 出来即可
模拟call最终版
使用到了es6的语法糖。
Function.prototype.myCall = function () { const [context, ...args] = [...arguments]; // 在传入的对象上设置属性为待执行函数 context.fn = this; // 执行函数 并获取其返回值 const res = context.fn(...args); // 删除属性 delete context.fn; // 返回执行结果 return res; } const obj = { value: '我是obj.value', } function fn(canshu1,canshu2,canshu3) { console.log(this.value); // 我是obj.value return [canshu1,canshu2,canshu3] } const xxx = fn.myCall(obj, 2,3,4); console.log(xxx) // [2,3,4]
到此,模拟实现了 call 方法。
模拟apply
定义: 调用一个具有给定 this 值的函数,及以一个数组的形式提供的参数。
func.apply(thisArg, [argsArray]); 复制代码
apply
与call
的区别在于传参方式
call
的参数是分开传递,而 apply
则用数组传,如下:
const obj = { value: '我是obj.value', }; function fn() { console.log(this.value); // '我是obj.value' return [...arguments] } console.log(fn.apply(obj, [1, 2])); // [1, 2]
思路
apply 的实现思路和 call 一样,需要考虑的是 apply 只有两个参数,因此,根据 call 的思路实现如下:
Function.prototype.apply1 = function (context, args) { // 给传入的对象添加属性,值为当前函数 context.fn = this; // 判断第二个参数是否存在,不存在直接执行,否则拼接参数执行,并存储函数执行结果 let res = !args ? context.fn() : context.fn(...args) // 删除新增属性 delete context.fn; // 返回函数执行结果 return res; } const obj = { value: '我是obj.value', }; function fn() { console.log(this.value); // '我是obj.value' return [...arguments] } console.log(fn.apply(obj, [1, 2])); // [1, 2]
模拟bind
bind
与 call
、apply
的区别 ,在于call
和apply
是直接执行,而bind
方法会创建一个新的函数,返回这个函数,并允许传入参数。
首先看一个例子。
const obj = { value: '我是obj.value', fn: function () { return this.value; }, }; const func = obj.fn; console.log(func()); // undefined
为什么会输出 undefined
呢?这涉及到了 this
的问题,不清楚的可以看这里
简单来讲,函数的this
取决于谁调用它,直接运行func()
,相当于是window.func()
。
window.func()
的this自然是window
,func
函数中想要输出window.value
,而在window
上没有定义value
,那输出的值自然就是undefined
了
此时就发现,this
的值总是乱变,那如何让函数中的锁定this为某对象呢?bind就可以。
const obj = { value: '我是obj.value', fn: function () { return this.value; }, }; const func = obj.fn; const bindFunc = func.bind(obj); console.log(bindFunc()); // 我是obj.value
开始写自己的bind
首先解决 bind 的第一个问题,返回一个函数。
其中改变 this 指向问题,可以使用 call 和 apply 方法,可参照上面的实现方式。
Function.prototype.myBind = function (context) { // 将当前函数的this存放起来 const _self = this; return function () { // 改变this return _self.apply(context); }; };
this 改变了后,需要考虑传参问题,参数是不定的,所以我们使用arguments。
// 关于对此行代码的解释,我写在了本文的最下方 // 此刻可以理解为 arguments.toArray().slice(1) const args = Array.prototype.slice.call(arguments, 1);
参数取到后,将参数传入即可,最终代码如下:
Function.prototype.myBind = function (context) { // 将当前函数的this存放起来 const _self = this; // 绑定bind传入的参数,从第二个开始 const args = Array.prototype.slice.call(arguments, 1); return function () { // 绑定bind返回新的函数,执行所带的参数 const bindArgs = Array.prototype.slice.apply(arguments); // 改变this return _self.apply(context, [...args, ...bindArgs]); }; }; const obj = { value: "我是obj.value", fn: function (value1, value2, value3, value4) { console.log("value1:", value1); // 我是传给fn方法的第一个参数 console.log("value2:", value2); // 俺是第二个 console.log("value3:", value3); // 呼呼呼,我是第三个 console.log("value4:", value4); // 也可以传第四个 return this.value; }, }; const func = obj.fn; const fn = func.myBind(obj, "我是传给fn方法的第一个参数", "俺是第二个"); console.log(fn("呼呼呼,我是第三个", "也可以传第四个"));
这里模拟的 bind
函数不是最终版,在 CDN 上有bind 实现;
对Array.prototype.slice.call(arguments,1) 的简要理解
首先对其进行简要拆分:
Array.prototype
属性表示 Array
构造函数的原型,并允许您向所有Array对象添加新的属性和方法。 slice(start,end) 是Array原型上的方法,可以对一个数组进行截取(从start开始,不包含end;如果只有 start 则截取从 start 到数组结束的所有元素。)并返回一个新的数组(浅拷贝)。
Array.prototype.slice.call(arguments,1)
可以理解为:改变数组slice
方法的作用域,使 this
指向arguments
对象,call()
方法的第二个参数表示传递给slice
的参数即截取数组的起始位置。这样 arguments
类数组就可以使用数组的方法 slice()
了,否则由于 arguments
是类数组并不是真正的数组,他是不可以使用 Array
的相关方法的。
简单理解为: Array.prototype.slice.call(arguments,1)
能够将具有length
属性(这一点需要注意,必须包含length属性)的对象转换为数组,简化记忆为:arguments.toArray().slice(1)
; 但有一个例外,IE下的节点集合它不能转换(因为IE下的dom对象是以com对象的形式实现,js对象和com对象不能进行转换)。