Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript深入之bind的模拟实现 #12

Open
mqyqingfeng opened this issue May 3, 2017 · 200 comments
Open

JavaScript深入之bind的模拟实现 #12

mqyqingfeng opened this issue May 3, 2017 · 200 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented May 3, 2017

bind

一句话介绍 bind:

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

由此我们可以首先得出 bind 函数的两个特点:

  1. 返回一个函数
  2. 可以传入参数

返回函数的模拟实现

从第一个特点开始,我们举个例子:

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

// 返回了一个函数
var bindFoo = bar.bind(foo); 

bindFoo(); // 1

关于指定 this 的指向,我们可以使用 call 或者 apply 实现,关于 call 和 apply 的模拟实现,可以查看《JavaScript深入之call和apply的模拟实现》。我们来写第一版的代码:

// 第一版
Function.prototype.bind2 = function (context) {
    var self = this;
    return function () {
        return self.apply(context);
    }

}

此外,之所以 return self.apply(context),是考虑到绑定函数可能是有返回值的,依然是这个例子:

var foo = {
    value: 1
};

function bar() {
	return this.value;
}

var bindFoo = bar.bind(foo);

console.log(bindFoo()); // 1

传参的模拟实现

接下来看第二点,可以传入参数。这个就有点让人费解了,我在 bind 的时候,是否可以传参呢?我在执行 bind 返回的函数的时候,可不可以传参呢?让我们看个例子:

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);

}

var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18

函数需要传 name 和 age 两个参数,竟然还可以在 bind 的时候,只传一个 name,在执行返回的函数的时候,再传另一个参数 age!

这可咋办?不急,我们用 arguments 进行处理:

// 第二版
Function.prototype.bind2 = function (context) {

    var self = this;
    // 获取bind2函数从第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);

    return function () {
        // 这个时候的arguments是指bind返回的函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context, args.concat(bindArgs));
    }

}

构造函数效果的模拟实现

完成了这两点,最难的部分到啦!因为 bind 还有一个特点,就是

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

也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。举个例子:

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'daisy');

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

注意:尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的 this 失效了,如果大家了解 new 的模拟实现,就会知道这个时候的 this 已经指向了 obj。

(哈哈,我这是为我的下一篇文章《JavaScript深入系列之new的模拟实现》打广告)。

所以我们可以通过修改返回的函数的原型来实现,让我们写一下:

// 第三版
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
        // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    fBound.prototype = this.prototype;
    return fBound;
}

如果对原型链稍有困惑,可以查看《JavaScript深入之从原型到原型链》

构造函数效果的优化实现

但是在这个写法中,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转:

// 第四版
Function.prototype.bind2 = function (context) {

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

到此为止,大的问题都已经解决,给自己一个赞!o( ̄▽ ̄)d

三个小问题

接下来处理些小问题:

1.apply 这段代码跟 MDN 上的稍有不同

在 MDN 中文版讲 bind 的模拟实现时,apply 这里的代码是:

self.apply(this instanceof self ? this : context || this, args.concat(bindArgs))

多了一个关于 context 是否存在的判断,然而这个是错误的!

举个例子:

var value = 2;
var foo = {
    value: 1,
    bar: bar.bind(null)
};

function bar() {
    console.log(this.value);
}

foo.bar() // 2

以上代码正常情况下会打印 2,如果换成了 context || this,这段代码就会打印 1!

所以这里不应该进行 context 的判断,大家查看 MDN 同样内容的英文版,就不存在这个判断!

(2018年3月27日更新,中文版已经改了😀)

2.调用 bind 的不是函数咋办?

不行,我们要报错!

if (typeof this !== "function") {
  throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}

3.我要在线上用

那别忘了做个兼容:

Function.prototype.bind = Function.prototype.bind || function () {
    ……
};

当然最好是用 es5-shim 啦。

最终代码

所以最最后的代码就是:

Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

下一篇文章

《JavaScript深入系列之new的模拟实现》

相关链接

《JavaScript深入之从原型到原型链》

《JavaScript深入之call和apply的模拟实现》

《JavaScript深入系列之new的模拟实现》

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

@jawil
Copy link

jawil commented May 3, 2017

先来个沙发,等会有时间看

@mqyqingfeng
Copy link
Owner Author

哈哈,欢迎光临。@jawil

@JuniorTour
Copy link

前辈,好像有一个typo。
模拟构造函数效果里的代码,有一个hobbit-霍比特人属性,应该是habit-习惯吧?#笑哭

@mqyqingfeng
Copy link
Owner Author

哈哈,确实是写错了,本来是想写habit,没有想到hobbit写的太顺手,我竟然没有任何违和的感觉……感谢指出哈~

@enjkvbej
Copy link

我把最后的实现代码跑了一下构造函数的例子 发现this依然失效了啊 是什么问题呢

@mqyqingfeng
Copy link
Owner Author

@enjkvbej 作为构造函数时,this 就是会失效呐

@jawil
Copy link

jawil commented May 22, 2017

😄

@mqyqingfeng
Copy link
Owner Author

@jawil 说起来,博主的 V8 源码系列写得怎么样了?很好奇第一篇会讲什么?

@jawil
Copy link

jawil commented May 22, 2017

V8 源码系列从入门到放弃,卒

@fbsstar
Copy link

fbsstar commented May 27, 2017

fNOP.prototype = this.prototype;
fbound.prototype = new fNOP();

是不是就等于fbound.prototype = Object.create(this.prototype);

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented May 27, 2017

@fbsstar 是的,Object.create 的模拟实现就是:

Object.create = function( o ) {
    function f(){}
    f.prototype = o;
    return new f;
};

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented Jun 15, 2017

对第三版模拟实现代码进行了优化。以前是

this instanceof self ? this : context

现在改成了

this instanceof fBound ? this : context

因为 fNOP.prototype = this.prototype的缘故,两段代码在效果上并没有区别,但是个人觉得改成 fBound 会更好理解, 而且 MDN 也是采用的 fBound 。

@caiyongmin
Copy link

caiyongmin commented Jun 20, 2017

为什么要设置 fBound.prototype = this.prototype,只是为了继承一下绑定函数的原型对象中的属性吗?

@mqyqingfeng
Copy link
Owner Author

@caiyongmin 为了让 fBound 构造的实例能够继承绑定函数的原型中的值

@caiyongmin
Copy link

我的意思是,为什么要继承?

@mqyqingfeng
Copy link
Owner Author

@caiyongmin 因为原生的 bind 的效果就是这样呐

@liuxinqiong
Copy link

liuxinqiong commented Jun 29, 2017

您好,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这里有点不太懂诶,能多讲解一下吗,谢谢

@mqyqingfeng
Copy link
Owner Author

@liuxinqiong 我们来写个 demo 哈:

Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    fBound.prototype = this.prototype;
    return fBound;
}


function bar() {}

var bindFoo = bar.bind2(null);

bindFoo.prototype.value = 1;

console.log(bar.prototype.value) // 1

你会发现我们明明修改的是 bindFoo.prototype ,但是 bar.prototype 的值也被修改了,这就是因为 fBound.prototype = this.prototype导致的。

@liuxinqiong
Copy link

@mqyqingfeng 万分感谢,点破之后,对之前的知识都有了新的认识!已经第二次看了,每次都有收获!

@mqyqingfeng
Copy link
Owner Author

@liuxinqiong 哈哈,感谢肯定~ 加油哈~

@youzaiyouzai666
Copy link

建议第三版中
fBound.prototype = this.prototype;
修改为:
fBound.prototype = self.prototype;
因为构造函数版本中,个人认为 核心是两个this的理解,如果理解了两个this,那么基本上就没太大的坑了。
再者用es6语法 写demo可读性更强

@mqyqingfeng
Copy link
Owner Author

@youzaiyouzai666 感谢指出,改成 self 能避免理解混乱,确实更好一些~ 关于 es6 的写法,我给自己的要求是在没写 ES6 系列之前,尽量保持 ES5 的写法,这是希望看这个系列的初学者们不要有额外的学习成本

@baixiaoji
Copy link

看到写bind最少的代码

// The .bind method from Prototype.js
Function.prototype.bind = function(){
  var fn = this, args = Array.prototype.slice.call(arguments), object = args.shift();
  return function(){
    return fn.apply(object,
      args.concat(Array.prototype.slice.call(arguments)));
  };
};

分享一下

@mqyqingfeng
Copy link
Owner Author

@baixiaoji 感谢分享哈~

不过这段代码并没有完整的实现 bind 的特性,比如 "当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效"

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(this.value);
}

var bindFoo = bar.bind(foo);

var obj = new bindFoo('18');

使用原生的 bind 就会返回 undefined,使用这段代码的话,就会返回 1

@stickmy
Copy link

stickmy commented Aug 19, 2017

最终版代码应该是

  this instanceof fBound

而不是

  this instanceof fNOP

@1sm23
Copy link

1sm23 commented Nov 16, 2021

Array.prototype.slice.call(arguments) ,这里调用 slice 不传参数是什么作用?

@mqyqingfeng
Copy link
Owner Author

@1sm23 Array.prototype.slice.call(arguments) 的作用是将 arguments 转为数组形式

@Longgererer
Copy link

Longgererer commented Dec 5, 2021

贴下我的写法:

Function.prototype.customBind = function (thisArg, ...bindArgs) {
  const self = this
  function tempFunc() {}
  tempFunc.prototype = self.prototype
  bindFunc.prototype = new tempFunc()
  function bindFunc(...argsArray) {
    return self.apply(new.target ? this : thisArg, [...bindArgs, ...argsArray])
  }
  return bindFunc
}

可以使用 new.target 代替 instanceOf 判断是否被 new 所调用。

@woow-wu7
Copy link

woow-wu7 commented Dec 6, 2021 via email

@lsc9
Copy link

lsc9 commented Dec 16, 2021

@liuxinqiong 我们来写个 demo 哈:

Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    fBound.prototype = this.prototype;
    return fBound;
}


function bar() {}

var bindFoo = bar.bind2(null);

bindFoo.prototype.value = 1;

console.log(bar.prototype.value) // 1

你会发现我们明明修改的是 bindFoo.prototype ,但是 bar.prototype 的值也被修改了,这就是因为 fBound.prototype = this.prototype导致的。
@mqyqingfeng 实际上绑定函数的 prototype 属性值是 undefined。

function bar() {}
var bar2 = bar.bind(null);
console.log(bar2.prototype);    // undefined

@fangyinghua
Copy link

fangyinghua commented Dec 16, 2021 via email

@MarvinXu
Copy link

有一个小小的问题,原生的 bind 方法 生成的 boundFn,用 new 调用得到的实例并不会继承 boundFn

function foo(name, age) {
	console.log(this.a)
  this.name = name
  this.age = age
}

let boundFoo = foo.bind({a: 1}, 'Bob')
boundFoo.prototype // undefined
boundFoo.prototype = Object.create(null)
boundFoo.prototype.say = function() {
	console.log(this.name)
}
let ins = new boundFoo(24)
ins.say // undefined

看了下 code-js 的实现,它是直接返回 new fn(),也不会去继承 boundFn:

var construct = function (C, argsLength, args) {
  if (!(argsLength in factories)) {
    for (var list = [], i = 0; i < argsLength; i++) list[i] = 'a[' + i + ']';
    factories[argsLength] = Function('C,a', 'return new C(' + list.join(',') + ')');
  } 
  return factories[argsLength](C, args);
};
module.exports = function bind(that /* , ...args */) {
  var fn = aFunction(this);
  var partArgs = slice.call(arguments, 1);
  var boundFunction = function bound(/* args... */) {
    var args = partArgs.concat(slice.call(arguments));
    return this instanceof boundFunction ? construct(fn, args.length, args) : fn.apply(that, args);
  };
  // to make instanceof to work for boundFunction
  if (isObject(fn.prototype)) boundFunction.prototype = fn.prototype;
  return boundFunction;
};

@Longgererer
Copy link

@liuxinqiong 我们来写个 demo 哈:

Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    fBound.prototype = this.prototype;
    return fBound;
}


function bar() {}

var bindFoo = bar.bind2(null);

bindFoo.prototype.value = 1;

console.log(bar.prototype.value) // 1

你会发现我们明明修改的是 bindFoo.prototype ,但是 bar.prototype 的值也被修改了,这就是因为 fBound.prototype = this.prototype导致的。

对于这个demo我有疑问:

const module = {
  x: 42,
  getX: function() {
    return this.x
  }
};

const unboundGetX = module.getX;

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX.prototype); // undefined
console.log(Function.prototype.bind.prototype) // undefined

实际上无论是 bind 还是 bind 所返回的函数副本的 prototype 都是 undefined,因为它在ECMAScript2015规范中被认为是 exotic objects,因此只要把它们的 prototype 设置为 undefined 就可以避免外部修改 prototype 所造成的影响了,但这样的话就不可以使用 new 来实例化 bind 所返回的函数副本了,有什么方法可以实现这个机制么?

@lockontan
Copy link

lockontan commented Mar 8, 2022

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'daisy');

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

obj.__proto === bar.prototype,按照文章实现的bind方法 ,两者并不会相等,为什么不用以下的实现办法呢

Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);


    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        if (this instanceof fBound) {
            return new self(...(args.concat(bindArgs)))
        } else {
            return self.apply(context, args.concat(bindArgs));
        }
    }
    return fBound;
}

@ZHL960663295
Copy link

受益匪浅,谢谢羽哥

@SpiritQAQ
Copy link

image

请问这是为什么?看起来原生bind在内部没有fNOP这一步?或者说是用了别的方法做了第三步?

还有个问题:bind()产生的函数没有prototype也好怪,原文中"我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype"指的是这个场景吗?

有没有可能是作者写文章的时候和2022年相比bind的机制有变化...

@wangzhiwei1888
Copy link

bind,apply,call的区别和实现

bind,apply,call都是用来改变this指向,而不同点如下:

  1. bind、apply和call函数的入参第一个都是需要改变this的指向,后面是需要传入的其他参数,但是apply需要传入的其他参数需要放在[数组]里
    fn.bind(null, 1,2,3)
    fn.call(null, 1,2,3)
    fn.apply(null, [1,2,3])

  2. apply,call会立即执行,bind不会立即执行,而是返回一个改变了this指向的函数

  3. call,apply在第一个参数为undefined或null时,this会指向window

下面是bind,apply,call的实现:
参考
https://blog.csdn.net/qq_30340185/article/details/105677065

@qiugu
Copy link

qiugu commented Feb 6, 2023

现在最新的Chrome中,bind执行返回以后的函数上没有原型,修改bind函数的原型,或者给bind返回函数添加原型,都不会相互影响了,所以这是v8源码改了?

@fangyinghua
Copy link

fangyinghua commented Feb 6, 2023 via email

@ccBreeze
Copy link

Function.prototype.bind2 = function (context, ...args) {
  if (typeof this !== 'function') {
    throw new Error()
  }

  context ??= window
  if (typeof context !== 'object') context = Object(context)

  // 在原型链上的方法,this 指向的是调用该方法的对象
  const self = this
  return function F() {
    // 作为构造函数调用
    // this 绑定失效
    if (this instanceof F) { // 可以使用 new.target 判断是否是 new 调用
      // this  ->  new的新实例
      return new self(...args, ...arguments)
    }
    // 有返回值
    return self.apply(context, [...args, ...arguments])
  }
}

@fangyinghua
Copy link

fangyinghua commented Feb 21, 2023 via email

@erdong-fe
Copy link

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'daisy');

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

obj.__proto === bar.prototype,按照文章实现的bind方法 ,两者并不会相等,为什么不用以下的实现办法呢

Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);


    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        if (this instanceof fBound) {
            return new self(...(args.concat(bindArgs)))
        } else {
            return self.apply(context, args.concat(bindArgs));
        }
    }
    return fBound;
}

我觉得你这样实现没问题

@fangyinghua
Copy link

fangyinghua commented May 8, 2023 via email

@jingaier
Copy link

jingaier commented May 8, 2023 via email

@lwpersonal
Copy link

Function.prototype.bind = Function.prototype.bind = function(context, ...args) {
	if (typeof this !== 'function') {
    throw new TypeError('Type Error');
  }
  const _this = this;
  function Fn (...args2) {
    // 当作为构造函数时,this 指向实例,此时结果为 true
    return _this.call(this instanceof Fn ? this : context, ...args, ...args2); 
  }
  // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
  if (this.prototype) {
  	Fn.prototype = Object.create(this.prototype);
  }
  return Fn;
}

@fangyinghua
Copy link

fangyinghua commented Jun 16, 2023 via email

@bosens-China
Copy link

Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

觉得比较难理解的,可以换成下面的来进行思考

Function.prototype.bind2 = function (context, ...args) {

  if (typeof this !== "function") {
    throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
  }

  var self = this;




  var fBound = function (...bindArgs) {

      return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
  }


  fBound.prototype = Object.create(this.prototype)
  return fBound;
}

@jingaier
Copy link

jingaier commented Oct 6, 2023 via email

@SherlockHomer
Copy link

https://github.com/zloirock/core-js/blob/master/packages/core-js/internals/function-bind.js#L3

module.exports = NATIVE_BIND ? $Function.bind : function bind(that /* , ...args */) {
  var F = aCallable(this);
  var Prototype = F.prototype;
  var partArgs = arraySlice(arguments, 1);
  var boundFunction = function bound(/* args... */) {
    var args = concat(partArgs, arraySlice(arguments));
    return this instanceof boundFunction ? construct(F, args.length, args) : F.apply(that, args);
  };
  if (isObject(Prototype)) boundFunction.prototype = Prototype; // here
  return boundFunction;
};

并不需要 多包一层 fBound.prototype = new fNOP(); 从测试对象结构可以看出多包了一层
image

@yhb-flydream
Copy link

最终代码

Function.prototype.bind2 = function (context) {
  if (typeof this !== 'function') {
    throw new Error('Function.prototype.bind - what is trying to be bound is not callable')
  }

  var self = this
  var args = Array.prototype.slice.call(arguments, 1)

  var fNOP = function () {}

  var fBound = function () {
    var bindArgs = Array.prototype.slice.call(arguments)
    return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs))
  }

  fNOP.prototype = this.prototype
  fBound.prototype = new fNOP()
  return fBound
}
  • 1

return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));

这里的 fNOP 要么改成 fBound

  • 2

要么 fNOP.prototype = this.prototype; 这里改成

if (this.prototype) {
  // Function.prototype doesn't have a prototype property
  fNOP.prototype = this.prototype
}

测试时当直接声明一个函数时:

let obj = {
  name: 'obj',
}

function getName() {
  console.log('getName this.name:>> ', this.name)
}

let getNameBindObj = getName.bind2(obj)

getNameBindObj()

输出:

// getName this.name:>>  obj

这时候 this instanceof fNOP 判断不会出错,因为:

console.log('getName :>> ', getName.prototype)
// getName :>>  {}

也就是:

fNOP.prototype = this.prototype = {}

当函数声明在一个对象里面时:

let obj = {
  name: 'obj',
}

let obj2 = {
  name: 'obj2',
  getName() {
    console.log('obj2 getName this.name:>> ', this.name)
  },
}

let obj2GetName = obj2.getName
let obj2GetNameBindObj = obj2GetName.bind2(obj)

obj2GetNameBindObj()

输出报错:

TypeError: Function has non-object prototype 'undefined' in instanceof check(在 instanceof 校验中 Function 有非对象的 prototype 属性 'undefined')

这时候 this instanceof fNOP 判断会出错,因为:

console.log('obj2GetName :>> ', obj2GetName.prototype)
// obj2GetName :>>  undefined

也就是:

fNOP.prototype = this.prototype = undefined

如果把上面提到的第 1 处可以修改的 fNOP 改成 fBound,那么

fBound.prototype = new fNOP() = {}

this instanceof fBound 判断就不会出错

或者第 2 处 fNOP.prototype = this.prototype; 这里改成

if (this.prototype) {
  // Function.prototype doesn't have a prototype property
  fNOP.prototype = this.prototype
}

那么

var fNOP = function () {}

fNOP.prototype = {}

this instanceof fNOP 判断也不会出错


最后说一下在输出报错那里,为什么会有

fNOP.prototype = this.prototype = undefined

不知道在看上面代码的时候有没有注意到 obj2 的 getName 的写法:

let obj2 = {
  name: 'obj2',
  getName() {
    console.log('obj2 getName this.name:>> ', this.name)
  },
}

这里 getName 用了简写的形式,相当于:

let obj2 = {
  name: 'obj2',
  getName: () => {
    console.log('obj2 getName this.name:>> ', this.name)
  },
}

原来 getName 是一个箭头函数,箭头函数是没有 prototype 属性的,所以会出现

console.log('obj2GetName :>> ', obj2.getName.prototype)
// obj2GetName :>>  undefined

如果改成:

let obj2 = {
  name: 'obj2',
  getName: function () {
    console.log('obj2 getName this.name:>> ', this.name)
  },
}

就没有问题了

console.log('obj2GetName :>> ', obj2.getName.prototype)
// obj2GetName :>>  {}

所以这里的判断就是为了预防出现箭头函数的形式吧

if (this.prototype) {
  // Function.prototype doesn't have a prototype property
  fNOP.prototype = this.prototype
}

@fangyinghua
Copy link

fangyinghua commented Dec 4, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests