十、JavaScript之call、apply和bind的模拟实现

call

简单来说

call()方法在将函数的第一个参数值指定到 this 和剩余参数指定的情况下调用某个函数或方法。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var foo = {
name: "lilei",
};
function bar(a, b) {
console.log(this, a, b);
}
// this指向第一个参数foo;
bar.call(foo, 1, 2); // {name: "lilei"} 1 2
// a,b未传参数,默认是undefined
bar.call(foo); // {name: "lilei"} undefined undefined
// call方法不传参数或传递null/undefined,this都指向window(非严格模式)
bar.call(); // window undefined undefined
bar.call(null); // window undefined undefined
bar.call(undefined); // window undefined undefined

模拟实现

call 方法的模拟实现需要明白 call 的原理:

  1. this 指向的改变
  2. call 函数中执行调用的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Function.prototype.myCall = function (context, ...args) {
if (context === null || context === undefined) {
context = window;
} else {
context = Object(context);
}
context.fn = this;
delete context.fn(...args);
return;
};
var foo = {
name: "lilei",
};
function bar(a, b) {
console.log(this, a, b);
}
bar.myCall(foo, 1, 2); // {name: "lilei", fn: ƒ} 1 2
bar.myCall(foo); // {name: "lilei", fn: ƒ} undefined undefined
bar.myCall(); // Window undefined undefined
bar.myCall(null); // Window undefined undefined
bar.myCall(undefined); // Window undefined undefined
bar.myCall(23, 24); // Number {23, fn: ƒ} undefined undefined

apply

apply 的使用与 apply 基本一致,两者之间的区别:apply 的第二个参数是数组[arg1,arg2,arg3,…],call 的第二个参数是列表(arg1,arg2,arg3,…);
使用示例:

1
2
3
4
5
6
7
8
9
10
11
var foo = {
name: "lilei",
};
function bar(a, b) {
console.log(this, a, b);
}
bar.apply(foo, [1, 2]); // {name: "lilei"} 1 2
bar.apply(foo); // {name: "lilei"} undefined undefined
bar.apply(); // window undefined undefined
bar.apply(null); // window undefined undefined
bar.apply(undefined); // window undefined undefined

模拟实现

1
2
3
4
5
6
7
8
9
10
Function.prototype.myApply = function (context, args) {
if (context === null || context === undefined) {
context = window;
} else {
context = Object(context);
}
context.fn = this;
delete context.fn(args);
return;
};

bind

引用 MDN 中的介绍

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

由此看出 bind 的特点:

  • bind 返回一个新的函数。
  • 这个新的函数可以预置参数。

举个例子

1
2
3
4
5
6
7
8
var foo = {
name: "lilei",
};
function bar(a, b) {
console.log(this, a, b);
}
var fn = bar.bind(foo, 1, 2);
fn(); //// {name: "lilei"} 1 2

模拟实现

1.实现返回一个函数,并可以传递参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.myBind = function () {
// 保存要绑定的this
var _this = arguments[0];
// 获取预置参数
var args = [].slice.call(arguments, 1);
var self = this;
return function () {
// 获取执行时传入的参数
var restArgs = [].slice.call(arguments);
// 合并参数
var allArgs = args.concat(restArgs);
return self.apply(_this, allArgs);
};
};
  1. 接下来最重要的一点,bind 返回的函数支持 new 调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Function.prototype.myBind = function () {
var self = this;
if (typeof self !== "function") {
throw new Error("绑定的目标必须是函数");
}
// 保存要绑定的this
var _this = arguments[0];
// 获取预置参数
var args = [].slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
// 获取执行时传入的参数
var restArgs = [].slice.call(arguments);
// 合并参数
var allArgs = args.concat(restArgs);
// instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
// 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
return self.apply(this instanceof fBound ? this : _this, allArgs);
};
// 原型链关系处理
// fBound.prototype = Object.create(self.prototype || Function.prototype);
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};

参考文献

https://github.com/mqyqingfeng/Blog/issues/11