Node-JS专精08手写bind02

手写bind02

上次实现的代码

但它有个问题

  • 不支持 new

自带的bind 演示

  • 理解 new fn() 后的四个关键步骤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var fn = function(a){
this.a = a;
}


new fn('x'); // 返回一个对象
// 当你这样之后
// 会有四个步骤

// step01 产生临时对象 temp
var temp = {};

// step02 挂载 fn.prototype 到 temp对象的 prototype上
temp.__proto__ = fn.prototype

// step03 调用 call 绑定this 把生成的临时对象 temp作为 this
fn.call(temp,'x')

// step04 new fn('a')后的返回值 this 返回 this 就是 temp
return this;


var fn2 = fn.bind(undefined,'y')
new fn2(); // 此时bind通过参数帮我们 绑定身上的 a = 'y'

这就是原版bind支持 new操作的

我们先测试原版bind

  • 测试后可以通过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function test5(message){
console.log(message)
Function.prototype.bind2 = bind;
const fn = function(p1, p2){
this.p1 = p1;
this.p2 = p2;
}
const fn2 = fn.bind(undefined,'x','y');
const object = new fn2();
console.log(object) // 此时 object = {p1:'x',p2:'y'}
console.assert(object.p1 === 'x','p1');
console.assert(object.p2 === 'y','p2');
}

// 这是可以通过的
  • 测试我们的bind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var slice = Array.prototype.slice;
function bind(asThis){
var args = slice.call(arguments,1);
const fn = this
if(typeof fn !== 'function'){
throw new Error("bind 必须调用在函数身上");
}
return function(){
var args2 = slice.call(arguments,0);
return fn.apply(asThis, args.concat(args2))
}
}

// 测试不通过
Function.prototype.bind2 = bind;
const fn = function(p1, p2){
this.p1 = p1;
this.p2 = p2;
}
const fn2 = fn.bind2(undefined,'x','y');
const object = new fn2();
console.log(object) // 此时 object = {}
console.assert(object.p1 === 'x','p1');
console.assert(object.p2 === 'y','p2');

分析原因

  • new fn2()的时候
    • 产生临时对象 temp
      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
      27
      function bind(asThis){
      var args = slice.call(arguments,1);
      const fn = this
      return function(){
      /*
      // step01
      产生临时对象 temp
      // step02
      temp.p1 = 'x';
      temp.p2 = 'y'
      // step03
      return this
      // 问题来了 ? 此时你主动写了 return 就不会把 temp正常返回
      // 返回的是什么? 就是 global 对象
      // 实际效果是 global.p1 = 'x' ; global.p2= 'y';
      */

      var args2 = slice.call(arguments,0);
      return fn.apply(asThis, args.concat(args2))
      }
      }

      // 所以我们最后不能使用 return fn.apply(asThis, args.concat(args2) 里的 asThis
      而是使用 new 产生的临时对象 this

      // 意思就是 可能是 asThis 可能是 temp临时对象 (this)
      // 那什么时候用这个 this呢?

如何知道一个函数调用的时候用没用 new

1
2
3
4
5
6
7
8
9
10
11
12
13
var fn = function(){console.log(this); console.log(this.__proto__ === fn.prototype);}

fn()
// window
// false

fn.call({a:11})
// {a:11}
// false

new fn();
// fn{}
// true

所以答案非常明显我们只要这样判断 this就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var slice = Array.prototype.slice;
function bind(asThis){
var args = slice.call(arguments,1);
const fn = this
if(typeof fn !== 'function'){
throw new Error("bind 必须调用在函数身上");
}
return function resultFn(){
var args2 = slice.call(arguments,0);
return fn.apply(
this.__proto__ === resultFn.prototype ? this : asThis,
args.concat(args2)
)
}
}

// 而且测试也能通过

其实此时代码是有bug的

  • 继续添加测试用例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function test6(message){
console.log(message)
Function.prototype.bind2 = bind;
const fn = function(p1, p2){
this.p1 = p1;
this.p2 = p2;
}
fn.prototype.sayHi = function(){};
const fn2 = fn.bind2(undefined,'x','y');
const object = new fn2();
console.assert(object.p1 === 'x','p1');
console.assert(object.p2 === 'y','p2');
console.assert(typeof object.sayHi === 'function');
}

// 测试没有通过
// 而用原版 bind 是可以通过的
// 证明我们代码有问题
  • 重新指向它的 prototype
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var slice = Array.prototype.slice;
// ES5版
function bind(asThis){
var args = slice.call(arguments,1);
const fn = this
if(typeof fn !== 'function'){
throw new Error("bind 必须调用在函数身上");
}
function resultFn(){
var args2 = slice.call(arguments,0);
return fn.apply(
this.__proto__ === resultFn.prototype ? this : asThis,
args.concat(args2)
)
}
resultFn.prototype = fn.prototype;
return resultFn;
}

// 此时测试可以通过

// 但是代码还是有问题

解决代码的问题

  • bind传递的this 是 fn是 new fn() 就会误判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 虽然没造成影响,但是有 误判的情况
    function test7(message){
    console.log(message)
    Function.prototype.bind2 = bind;
    const fn = function(p1, p2){
    this.p1 = p1;
    this.p2 = p2;
    }
    fn.prototype.sayHi = function(){};
    const object1 = new fn();
    const fn2 = fn.bind2(object1,'x','y');
    const object = fn2();
    console.assert(object === undefined, 'object为空');
    console.assert(object1.p1 === 'x','p1');
    console.assert(object1.p2 === 'y','p2');
    }
  • this.__proto__ === resultFn.prototype 中的 this.__proto__ 是不能在生产代码里用的

    • this instanceof resultFn
    • resultFn.prototype.isPrototypeOf(this)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      var slice = Array.prototype.slice;
      // ES5版
      function bind(asThis){
      var args = slice.call(arguments,1);
      const fn = this
      if(typeof fn !== 'function'){
      throw new Error("bind 必须调用在函数身上");
      }
      function resultFn(){
      var args2 = slice.call(arguments,0);
      return fn.apply(
      resultFn.prototype.isPrototypeOf(this) ? this : asThis,
      args.concat(args2)
      )
      }
      resultFn.prototype = fn.prototype;
      return resultFn;
      }

手写bind完整版

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
27
28
29
30
31
32
33
34
35
36
37
var slice = Array.prototype.slice;
// ES5版
function bind(asThis){
var args = slice.call(arguments,1);
const fn = this
if(typeof fn !== 'function'){
throw new Error("bind 必须调用在函数身上");
}
function resultFn(){
var args2 = slice.call(arguments,0);
return fn.apply(
resultFn.prototype.isPrototypeOf(this) ? this : asThis,
args.concat(args2)
)
}
resultFn.prototype = fn.prototype;
return resultFn;
}
module.exports = bind;

// ES6版
function _bind(asThis, ...args){
const fn = this
function resultFn(...args2){
return fn.call(
resultFn.prototype.isPrototypeOf(this) ? this : asThis,
...args,
...args2
);
}
resultFn.prototype = fn.prototype;
return resultFn;
}

if(!Function.prototype.bind){
Function.prototype.bind = bind;
}

MDN的bind实现是有缺陷的

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
27
28
29
30
31
// Yes, it does work with `new funcA.bind(thisArg, args)`
if (!Function.prototype.bind) (function(){
var ArrayPrototypeSlice = Array.prototype.slice;
Function.prototype.bind = function(otherThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}

var baseArgs= ArrayPrototypeSlice .call(arguments, 1),
baseArgsLength = baseArgs.length,
fToBind = this,
fNOP = function() {},
fBound = function() {
baseArgs.length = baseArgsLength; // reset to default base arguments
baseArgs.push.apply(baseArgs, arguments);
return fToBind.apply(
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
);
};

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

return fBound;
};
})();

它的代码和我们不同的地方在这

1
2
3
4
if (this.prototype) {
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();

先不管它代码什么意思 我们先测试一下

  • step01拷贝到浏览器控制台
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
27
28
29
30
31

var ArrayPrototypeSlice = Array.prototype.slice;
// 注意不能直接 Function.prototype.bind = xxx
// 因为不能改写不了 要 Function.prototype.bind2 = xxx
Function.prototype.bind2 = function(otherThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}

var baseArgs= ArrayPrototypeSlice .call(arguments, 1),
baseArgsLength = baseArgs.length,
fToBind = this,
fNOP = function() {},
fBound = function() {
baseArgs.length = baseArgsLength; // reset to default base arguments
baseArgs.push.apply(baseArgs, arguments);
return fToBind.apply(
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
);
};

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

return fBound;
};
  • step02继续拷贝代码
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
27
28
29
30
31
32
33
34
35
36
37
var fn = function(a){this.a = a;}
fn.prototype.sayHi = function(){};

// 此时来测试
new fn('x')
// 打印 fn{a:'x'}
// 点开它只有一层 fn.__proto__

// 继续测试bind
fn2 = fn.bind(undefined,'y');
new fn2();
// 打印 fn{a:'y'}
// 点开它也只有一层 fn.__proto__

// 测试 mdn 的 bind
fn3 = fn.bind2(undefined,'y')
new fn3();
// 打印 fn{a:'y'}
// 点开它会多了一层 fn.__proto__.__proto__
/*
fn
-__proto__
-__proto__
-sayHi
*/


// MDN的代码问题 它的 prototype和原始的是不一样的
const o1 = new fn('x')
o1.__proto__ === fn.prototype // true

const o2 = new fn2('y')
o2.__proto__ === fn.prototype // true

const o3 = new fn3('x')
o3.__proto__ === fn.prototype // false
o3.__proto__.__proto__ === fn.prototype // true
  • MDN bind支持 new操作的实现的__proto__会多了一层
  • 而且它的prorotype不是跟原始 bind的不一样

代码链接

  • https://github.com/slTrust/source-bind
  • 启发
  • 先实现一个最容易的
  • 找到自己的问题(需要经验)
  • 不停迭代
  • 直到找不到问题(需要经验)
  • 留一个问题给面试官
    • 先甩出简陋版 ES6版
    • TA说给我改成ES5的/支持分阶段绑定参数/支持new的
    • 不要一开始就放大 不然一下提出一个高出你最终版的你没碰到的问题你就惨了