解析 Bind 原理,并手写 Bind 实现

时间:2021-12-02 16:01:18

解析 Bind 原理,并手写 Bind 实现

bind()

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

— MDN

bind 方法与 call / apply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。

来个例子说明下:

  1. let value = 2;
  2. let foo = {
  3. value: 1
  4. };
  5. function bar(name, age) {
  6. return {
  7. value: this.value,
  8. name: name,
  9. age: age
  10. }
  11. };
  12. bar.call(foo, "Jack", 20); // 直接执行了函数
  13. // {value: 1, name: "Jack", age: 20}
  14. let bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数
  15. bindFoo1();
  16. // {value: 1, name: "Jack", age: 20}
  17. let bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数
  18. bindFoo2(20);
  19. // {value: 1, name: "Jack", age: 20}

通过上述代码可以看出 bind 有如下特性:

1、指定 this

2、传入参数

3、返回一个函数

4、柯里化

模拟实现:

  1. Function.prototype.bind = function (context) {
  2. // 调用 bind 的不是函数,需要抛出异常
  3. if (typeof this !== "function") {
  4. throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
  5. }
  6. // this 指向调用者
  7. var self = this;
  8. // 实现第2点,因为第1个参数是指定的this,所以只截取第1个之后的参数
  9. var args = Array.prototype.slice.call(arguments, 1);
  10. // 实现第3点,返回一个函数
  11. return function () {
  12. // 实现第4点,这时的arguments是指bind返回的函数传入的参数
  13. // 即 return function 的参数
  14. var bindArgs = Array.prototype.slice.call(arguments);
  15. // 实现第1点
  16. return self.apply( context, args.concat(bindArgs) );
  17. }
  18. }

但还有一个问题,bind 有以下一个特性:

一个绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

来个例子说明下:

  1. let value = 2;
  2. let foo = {
  3. value: 1
  4. };
  5. function bar(name, age) {
  6. this.habit = 'shopping';
  7. console.log(this.value);
  8. console.log(name);
  9. console.log(age);
  10. }
  11. bar.prototype.friend = 'kevin';
  12. let bindFoo = bar.bind(foo, 'Jack');
  13. let obj = new bindFoo(20);
  14. // undefined
  15. // Jack
  16. // 20
  17. obj.habit;
  18. // shopping
  19. obj.friend;
  20. // kevin

上面例子中,运行结果 this.value 输出为 undefined ,这不是全局 value 也不是 foo 对象中的 value ,这说明 bind 的 this 对象失效了,new 的实现中生成一个新的对象,这个时候的 this 指向的是 obj 。

这个可以通过修改返回函数的原型来实现,代码如下:

  1. Function.prototype.bind = function (context) {
  2. // 调用 bind 的不是函数,需要抛出异常
  3. if (typeof this !== "function") {
  4. throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
  5. }
  6. // this 指向调用者
  7. var self = this;
  8. // 实现第2点,因为第1个参数是指定的this,所以只截取第1个之后的参数
  9. var args = Array.prototype.slice.call(arguments, 1);
  10. // 创建一个空对象
  11. var fNOP = function () {};
  12. // 实现第3点,返回一个函数
  13. var fBound = function () {
  14. // 实现第4点,获取 bind 返回函数的参数
  15. var bindArgs = Array.prototype.slice.call(arguments);
  16. // 然后同传入参数合并成一个参数数组,并作为 self.apply() 的第二个参数
  17. return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
  18. // 注释1
  19. }
  20. // 注释2
  21. // 空对象的原型指向绑定函数的原型
  22. fNOP.prototype = this.prototype;
  23. // 空对象的实例赋值给 fBound.prototype
  24. fBound.prototype = new fNOP();
  25. return fBound;
  26. }

注释1 :

  • 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true ,可以让实例获得来自绑定函数的值,即上例中实例会具有 habit 属性。
  • 当作为普通函数时,this 指向 window ,此时结果为 false ,将绑定函数的 this 指向 context

注释2 :

  • 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值,即上例中 obj 可以获取到 bar 原型上的 friend
  • 至于为什么使用一个空对象 fNOP 作为中介,把 fBound.prototype 赋值为空对象的实例(原型式继承),这是因为直接 fBound.prototype = this.prototype 有一个缺点,修改 fBound.prototype 的时候,也会直接修改 this.prototype ;其实也可以直接使用ES5的 Object.create() 方法生成一个新对象,但 bind 和 Object.create() 都是ES5方法,部分IE浏览器(IE < 9)并不支

注意: bind() 函数在 ES5 才被加入,所以并不是所有浏览器都支持,IE8 及以下的版本中不被支持,如果需要兼容可以使用 Polyfill 来实现

详情可前往 深度解析bind原理、使用场景及模拟实现 查看

补充:柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

  1. var add = function(x) {
  2. return function(y) {
  3. return x + y;
  4. };
  5. };
  6. var increment = add(1);
  7. var addTen = add(10);
  8. increment(2);
  9. // 3
  10. addTen(2);
  11. // 12
  12. add(1)(2);
  13. // 3

这里定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 之后,返回的函数就通过闭包的方式记住了 add 的第一个参数。所以说 bind 本身也是闭包的一种使用场景。

柯里化是将 f(a,b,c) 可以被以 f(a)(b)(c) 的形式被调用的转化。JavaScript 实现版本通常保留函数被正常调用和在参数数量不够的情况下返回偏函数这两个特性。

原文链接:https://mp.weixin.qq.com/s/mfPcwNR8OJaQ0Mvztdaqdw