开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情
通过前两篇文章我们知道了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返回一个新函数后,调用新函数时向新函数内传参。
apply的原理
上一篇中我们实现了call方法,那接下来我们再来实现一下apply,其实和call没什么太大的区别,主要区别就在于传参方式,apply的参数,除了第一个对象,就是数组形式。
-
首先我们知道,我们的函数身上并没有call、apply、bind这三种方法,所以他们必定是挂载在函数原型上的。Function.prototype.myApply
-
同时我们知道他们传进去的第一个必定是一个对象,剩下几个都是参数,并且传入的参数都是以数组形式包裹的,所以 arguments自然只有两个参数,下标为0的是对象,为1的是数组参数,我们只需要按下标把参数拿出来就可以了,没有传参数的情况也要考虑到。var args = arguments[1]||"";
-
最重要的一点来了,实际上,我们为了能让foo中的this指向obj,我们还是采用了隐式绑定规则,即让foo挂载在obj内部,让obj进行调用!这就是call能改变this绑定的最终奥义!
apply的代码
Function.prototype.myApply= function (context) {
//将对象后面传入的参数拿出来
var args = arguments[1]||"";
……
};
同样,通过上一篇我们知道了this其实就是指向调用myApply函数的上下文,那么函数中的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的函数,将这个函数挂载到对象
};
到这一步,其实apply的原理已经差不多了,但apply还帮我们多做了一步,就是调用改变this指向后的函数,返回它的执行结果。
Function.prototype.myCall = function (context)
{
var args = arguments[1]||""; //参数用slice切割,将obj拿出来
context['fn'] = this;//this指向调用mycall的函数,将这个函数挂载到对象
const res = context['fn'](...args);//调用改变this指向后的函数
return res;//返回它的返回结果。
};
这样一个call方法基本就完成了,让我来试试好不好用
可以!差不多实现了
代码完善
这一步和我上一篇实现call是一样的,看过的小伙伴可以不要再看了。
-
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就指回全局,但是我们的myApply并没有处理这一步,会报错
所以我们需要在使用传入context(obj)前处理一下,如果没传入obj就让它指向window var context = context || "window";
Function.prototype.myCall = function (context) {
var context = context || "window";//如果没传入obj就让它指向window
……
};
-
挂载一个独一无二的"fn"
思考一下万一obj里面存在一个与fn重名了的函数怎么办呢,我们myApply应该去调用执行哪个呢?我们想一想ES6里面新增了一个什么数据类型可以帮我们解决这个问题?是不是Symbol。可以声明一个不被改变的独一无二的fn。const fn = Symbol("fn") 注意这样的话context["fn"]里面的引号就要去掉了,因为此时的fn是声明的一个变量了,而不是一个名称。
想到这三点处理,我们的没有myCall就完美做成了
以下是myApply完整代码
Function.prototype.myApply = function (context) {
var context = context || "window";
var args = arguments[1] || "";
const fn = Symbol("fn");
context[fn] = this;
const res = context[fn](...args);
delete context[fn];
return res;
};
试一试效果
完美结束