深入理解JavaScript系列(9):bind

bind 函数与 call,apply 的区别是什么呢?

  • bind 函数并不是直接调用的,且返回函数可以参数
  • 参数一 与 call、apply 同样,指定的对象(该函数的执行上下文
  • 其他参数与 call 相同,都作为参数传入,但是 apply 只传入一个数组参数

具体对照,可以看我上面的一篇文章,《深入理解 JavaScript 系列(8):call/apply》

我们来看 bind 的特点

  • 返回函数
  • 返回函数可以传参数

第一版 - 返回函数

我们来看一下 bind 执行的返回效果吧:

1
2
3
4
5
6
7
8
9
10
let obj = {
name: 'hzzzzzzzq',
age: 18,
};
function log() {
console.log(this.name, this.age);
}
let print = log.bind(obj);
print();
// hzzzzzzzq 18

由此,我们来写文吗的第一版代码。

1
2
3
4
5
6
Function.prototype.myBind = function (context) {
let self = this;
return function () {
return self.apply(context);
};
};

我们来看看结果,是不是一样了呢?

1
2
3
4
5
6
7
8
9
10
let obj = {
name: 'hzzzzzzzq',
age: 18,
};
function log() {
console.log(this.name, this.age);
}
let print = log.myBind(obj);
print();
// hzzzzzzzq 18

这时候,我们得到了一个返回值,并且绑定。

第二版 - 传入参数

我们来看看,不仅在 bind 函数中可以传入参数,其返回的函数也可以传入参数。
我们看下面的例子:

1
2
3
4
5
6
7
8
9
let obj = {
value: 1,
};
function log(name, age) {
console.log(this.value, name, age);
}
let print = log.bind(obj, 'hzzzzzzzq');
print(18);
// 1 hzzzzzzzq 18

我们怎么加入参数呢?

  1. 我们要获取调用 bind 函数时除了第一个绑定对象以外的参数
  2. 我们要获取内部返回函数的参数
1
2
3
4
5
6
7
8
Function.prototype.myBind = function (context) {
let self = this;
const args = [].slice.call(arguments, 1); // 获取从第二个开始的全部参数
return function () {
const subArgs = [].slice.call(arguments); // 获取 bind 返回函数的内部参数
return self.apply(context, args.concat(subArgs));
};
};

第二版写完了。

第三版 - 作为构造函数使用的绑定函数

接下来就是构造函数啦,最难的部分了。

绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略

我们来举个例子,看一下。

1
2
3
4
5
6
7
8
9
10
11
12
var value = 2;
let obj = {
value: 1,
};
function log(name, age) {
this.sport = 'playing';
console.log(this.value, name, age);
}
let print = log.bind(obj, 'hzzzzzzzq');
let obj2 = new print('18');
// undefined hzzzzzzzq 18
console.log(obj2.sport); // playing

从上面结果,我们可以看出,使用 new 进行创建新实例时,this 指向绑定的 obj 已经失效了,返回 undefined

但是为什么不会指向全局变量呢?其实就是 new 操作,让现在的 this 指向了 obj2

可以来看看 new 操作是怎么实现的,参考我的这篇文章 - 《深入理解 JavaScript 系列(10):new》

1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.myBind = function (context) {
let self = this;
const args = [].slice.call(arguments, 1); // 获取从第二个开始的全部参数
const resultFn = function () {
const subArgs = [].slice.call(arguments); // 获取调用时传入的参数
return self.apply(
this instanceof resultFn ? this : context,
args.concat(subArgs) // 拼接参数
);
};
resultFn.prototype = this.prototype;
return resultFn;
};

肯定有人会对这句代码有疑问 this instanceof resultFn ? this : context,我们来解释一下。

这里我们根据 thisresultFn 进行 instanceof 来判断是构造函数还是普通函数调用。

  • 构造函数,this 指向实例,判断结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值。
  • 普通函数,this 指向 window,结果为 false,绑定函数的 this 指向 context

上面的代码真的实现了吗?

好像还不够,存在问题,那就是返回函数的值修改时,也会导致原函数值的修改。

1
2
3
let bindObj = log.myBind(null);
bindObj.prototype.name = 'hzzzzzzzq';
console.log(obj.prototype.name); // hzzzzzzzq

第四版 - 汇总

我们来汇总一下,我们实现 bind 函数的步骤。

我们还会对上面的问题进行优化 - 就是通过一个额外的函数进行中转。

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况
  2. 保存当前函数的引用,获取其余传入参数值
  3. 创建一个函数返回
  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 thisapply 调用,其余情况都传入指定的上下文对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new Error('只有函数可以调用 myBind');
}
const self = this;
const args = [].slice.call(arguments, 1);
const fn = function () {}; // 中转函数
const resultFn = function () {
const subArgs = [].slice.call(arguments); // 获取调用时传入的参数
return self.apply(
this instanceof fn ? this : context,
args.concat(subArgs) // 拼接参数
);
};
fn.prototype = self.prototype;
resultFn.prototype = new fn();
return resultFn;
};
-------------------- 本文结束 感谢阅读 --------------------