无垢清净光,慧日破诸暗,能伏灾风火,普明照世间。 --法华经
介绍
在function的原型上有三个方法 call apply bind,所有函数都是Function的实例,所以所有的函数都可以调用这三个方法,而这三个方法都是用来改变this指向的
三者特征
call、apply、bind三者都是用来重定向this指向问题的,但是又有一定的不同。
call
- 功能: 调用函数 + 改变this指向
- 参数:
第一个参数 :用来 设置this的指向,如果指定了 null 或者 undefined 则内部 this 指向 window
剩余参数 : 对应函数的参数 - call的返回值就是函数的返回值
apply
- 功能: 调用函数 + 改变this指向
- 第一个参数 : 设置函数内部this的指向
第二个参数 : 是数组
- call的返回值就是函数的返回值
bind
- bind函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。
- 当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。
- 一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
调用举例:
toString.call([],1,2,3);
toString.apply([],[1,2,3]);
哪个性能会好一点
参数在三个以内,call 和 apply 的性能差不多,
参数超过三个,call 的性能要比 apply 的好一点
call
我们先来梳理call做了什么
var foo = {
value: 1
};
function bar() {
console.log(this);
}
bar.call(foo); // 1
输出结果是:
我们发现,这里call方法做了两件事
- this指向了对象foo
- 执行力bar函数
这里是call方法最核心的两个功能
可以理解给,把bar方法绑定到了foo上,由foo调用了一次,然后再删除这个对象,以免对foo产生干扰。根据这个思路,我们来写一下实现
V1-基本实现
Function.prototype.call1 = function(context) {
context.fn = this;
context.fn();
delete context.fn;
};
const foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call1(foo); // 1
V2-多种情况考虑
发现我们确实实现了这个功能,打印的结果和call一样,那么我们给call方法不仅传递了要绑定的对象,还传递了参数。这里汇总一下可能的情况
- 函数有参数或者参数为空
- 函数有返回值
最终版本如下
Function.prototype.call2 = function(context) {
// 这里是为了考虑到context为空的情况,为空则设置为Window对象
context = context || window;
// 这一步是取出arguments类数组对象除要绑定的对象外的参数,在本🌰中为 "努力", 18
let arg = [...arguments].slice(1);
// 这里的this是调用call2的对象,也就是bar
context.fn = this;
// 执行bar函数
let res = context.fn(...arg);
// 删除bar,避免对foo产生影响
delete context.fn;
// 返回函数的返回值
return res;
};
const foo = {
value: 1
};
function bar(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
bar.call2(foo, "努力", 18);
apply
apply的实现方式和call几乎一模一样,唯一不同的是前者的参数是数组
由于思路一样,就不再赘述,实现方式如下
Function.prototype.apply1 = function (context, arg) {
context = Object(context) || window;
context.fn = this;
let result;
if (arg) {
result = context.fn(...arg);
} else {
result = context.fn();
}
delete context.fn;
return result;
};
bind
bind的实现是三者里面最难的,因为他不仅要绑定this,他的返回值还能作为构造函数,来使用new关键字创建实例
所以,我们来梳理一下bind
- 绑定this
- 和call、apply相比,不执行函数
- 返回值可以用做构造函数
根据这些要求,我们来做一下简单的实现
Function.prototype.bind2 = function(context) {
const self = this; //this 代表的是调用bind的对象
return function() {
return self.apply(self);
};
};
测试一下:
const value = 2;
const foo = {
value: 1
};
function bar(name, age) {
this.habit = "shopping";
console.log(this);
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = "kevin";
const bindFoo = bar.bind2(foo, "daisy");
// bindFoo('18');
const obj = new bindFoo("18");
结果OK
加点难度
再把构造函数等需求加进来
Function.prototype.bind2 = function(context) {
const self = this; //this代表的是调用bind的对象
const args = Array.prototype.slice.call(arguments, 1);
const fNOP = function() {
};
const fBound = function() {
const bindArgs = Array.prototype.slice.call(arguments);
// 这里使用this instanceof fNOP会为true是因为,fBound.prototype = new fNOP();这句代码,相当于fNOP位于fBound的原型链上,
// 原直接将 fBound. prototype = this.prototype,我们直接修改 fBound.prototype 的时候,
// 也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
引用
JavaScript深入之call和apply的模拟实现 · Issue #11 · mqyqingfeng/Blog