开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
通过上一篇文章我们知道了this的指向,以及call、apply、bind的作用,简单回顾一下
//定义一个对象,对象中挂载一些属性
let obj = {
name: "夹心啊",
age: 18,
type: "美女",
};
var name = "Alice";
function foo(num1, num2) {
console.log(this.name);
console.log(this.age);
console.log(this.type);
console.log(num1 + num2);
}
foo.call(obj,1,2)
foo.apply(obj,[1,2])
var newFoo=foo.bind(obj)
newFoo(1,2)
看看结果
call、apply、bind的区别
-
执行方法:从上面可以看出call和apply都是使用后直接执行,但是唯独bind不会立即给你执行,而是返回一个新的函数,需要你去手动调用
-
传参方式:call传参采用一个一个参数列举,apply则采用数组传参方式,将使用的参数包裹在一个数组内。bind有两种方式,你可以和call一样在调用bind的时候直接传参,也可以等call返回一个新函数后,调用新函数时向新函数内传参。
call实现原理
前期铺垫做完了,我们已然发现了call、apply、bind的作用和区别,那我们先来实现一下call吧
-
首先我们知道,我们的函数身上并没有call、apply、bind这三种方法,所以他们必定是挂载在函数原型上的。Function.prototype.myCall
-
同时我们知道他们传进去的第一个必定是一个对象,剩下几个都是参数,但是我们并不知道它会传多少个参数,所以需要将参数做一个切割,将对象和其他参数分别切出来,没有传参数的情况也要考虑到var args = [...arguments].slice(1)||"";
-
最重要的一点来了,实际上,我们为了能让foo中的this指向obj,我们还是采用了隐式绑定规则,即让foo挂载在obj内部,让obj进行调用!这就是call能改变this绑定的最终奥义!
call的代码
Function.prototype.myCall = function (context) {
//参数用slice切割,将对象后面传入的参数拿出来
var args = [...arguments].slice(1);
//注意必须将arguments结构一下,它是类数组,不解构无法使用数组身上的api哦
……
};
接下来我们要思考了,也是最大的难题,我们要如何拿到foo,让它挂载在obj上呢?仔细想想,我们上一篇文章里面讲到了什么? this!!我们要使用call是不是需要用函数去调用?那么函数中的this自然会指向foo上下文咯!!所以this就相当于我们的foo函数了
//context是传入的obj
Function.prototype.myCall = function (context) {
//参数用slice切割,将obj拿出来
var args = [...arguments].slice(1);
//注意必须将arguments结构一下,它是类数组,不解构无法使用数组身上的api哦
context['fn'] = this;//this指向调用mycall的函数,将这个函数挂载到对象
};
到这一步,其实call的原理已经差不多了,但call还帮我们多做了一步,就是调用改变this指向后的函数,返回它的返回结果。
Function.prototype.myCall = function (context) {
var args = [...arguments].slice(1); //参数用slice切割,将obj拿出来
context['fn'] = this;//this指向调用mycall的函数,将这个函数挂载到对象
const res = context['fn'](...args);//调用改变this指向后的函数
return res;//返回它的返回结果。
};
这样一个call方法基本就完成了,让我来试试好不好用
Yes!!我们成功啦!!
代码完善
-
delete删除obj上多出来的函数
但这样的代码并不完美,为什么呢?让我们来看看obj上多了点什么
你会看到,我们的obj上多了一个名叫foo的函数,是我们在myCall内部给obj挂载上去的,但是我们原本的call是不会这样的,所以我们还需要多做一步,就是在函数调用完毕后,将刚刚挂载到obj的函数删除 delete context['fn']
Function.prototype.myCall = function (context) {
var args = [...arguments].slice(1); //参数用slice切割,将obj拿出来
context['fn'] = this;//this指向调用mycall的函数,将这个函数挂载到对象
const res = context['fn'](...args);//调用改变this指向后的函数
delete context['fn']//将刚刚挂载到obj的函数删除
return res;//返回它的返回结果。
};
-
不传参会发生什么
我们发现不传参this就指回全局,但是我们的myCall并没有处理这一步,会报错
所以我们需要在使用传入context(obj)前处理一下,如果没传入obj就让它指向window var context = context || "window";
Function.prototype.myCall = function (context) {
var context = context || "window";//如果没传入obj就让它指向window
……
};
-
挂载一个独一无二的"fn"
思考一下万一obj里面存在一个与fn重名了的函数怎么办呢,我们myCall应该去调用执行哪个呢?我们想一想ES6里面新增了一个什么数据类型可以帮我们解决这个问题?是不是Symbol。可以声明一个不被改变的独一无二的fn。const fn = Symbol("fn") 注意这样的话context["fn"]里面的引号就要去掉了,因为此时的fn是声明的一个变量了,而不是一个名称。
想到这三点处理,我们的没有myCall就完美做成了
以下是myCall完整代码
Function.prototype.myCall = function (context) {
var context = context || "window";
var args = [...arguments].slice(1)||"";
const fn = Symbol("fn");
context[fn] = this;
const res = context[fn](...args);
delete context[fn];
return res;
};
试一试效果
不错不错,真完美~~