浅析这句经常在框架中出现的JS代码加深对bind的理解

  call、bind这类方法我们虽然在平时开发中用到的不多,但是在看框架源码时,我们会经常看到。比如我们经常在框架级的源码中看到类似如下的一句代码:

var toStr1 = Function.prototype.call.bind(Object.prototype.toString);

  在这一句代码中既使用call方法,同时也使用bind方法,乍看之下,有点晕!这到底是想干嘛?

  无妨,我们调用看看,传入不同的类型试试,效果如下:

var toStr1 = Function.prototype.call.bind(Object.prototype.toString);
console.log(toStr1({}));      // "[object Object]"
console.log(toStr1([]));      // "[object Array]"
console.log(toStr1(123));     // "[object Number]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1(new Date));// "[object Date]"

  从结果中可以看到该方法的主要功能是用于检测对象的类型。但通常类型检测,我们可能更多地看到如下代码实现:

var toStr2 = obj => Object.prototype.toString.call(obj);
console.log(toStr2({}));      // "[object Object]"
console.log(toStr2([]));      // "[object Array]"
console.log(toStr2(123));     // "[object Number]"
console.log(toStr2("abc"));   // "[object String]"
console.log(toStr2("abc"));   // "[object String]"
console.log(toStr2(new Date));// "[object Date]"

  第二种方法更简洁,仅仅使用一次call就能获得我们想要的功能,且代码逻辑清晰,理解起来更加容易,可在众多框架中为何更多使用第一种呢?

  其实主要的原因是防止原型污染。

  比如我们在业务代码中覆写了Object.prototype.toString方法,第二种写法将得不到正确的结果,而第一种写法仍然可以。我们用代码来来试试:

var toStr1 = Function.prototype.call.bind(Object.prototype.toString);

var toStr2 = obj => Object.prototype.toString.call(obj);

Object.prototype.toString = function(){
 return'toString方法被覆盖!';
}
// 接着我们再调用上述方法

// toStr1调用结果如下:
console.log(toStr1({}));      // "[object Object]"
console.log(toStr1([]));      // "[object Array]"
console.log(toStr1(123));     // "[object Number]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1(new Date));// "[object Date]"

// toStr2调用结果如下:
console.log(toStr2({}));      // "toString方法被覆盖!"
console.log(toStr2([]));      // "toString方法被覆盖!"
console.log(toStr2(123));     // "toString方法被覆盖!"
console.log(toStr2("abc"));   // "toString方法被覆盖!"
console.log(toStr2("abc"));   // "toString方法被覆盖!"
console.log(toStr2(new Date));// "toString方法被覆盖!"

  结果很明显。第一种方法仍然能正确得到结果,而第二种则不行!那么为什么会这样呢?

  我们知道bind函数返回结果是一个函数,这个函数是函数内部的函数,会被延迟执行,那么很自然联想到这里可能存在闭包!不过在现代版浏览器中call和bind都已经被js引擎内部实现了,我们没有办法调试!但是我们可以通过polly-fill提供的近似实现的源码来理解引擎内部的逻辑,下面是个简单的demo:

// 模拟实现call
// ES6实现
Function.prototype.mycall = function (context) {
 context = context ? Object(context) : window;
 var fn = Symbol();
 context[fn] = this;

 let args = [...arguments].slice(1);
 let result = context[fn](...args);

 delete context[fn]
 return result;
}
// 模拟实现bind
Function.prototype.mybind = function (context) {
 if (typeof this !== "function") {
   throw new Error("请使用函数对象调用我,谢谢!");
}

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

 var fNOP = function () { }; // 注意这个是用来处理new的特性

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

 fNOP.prototype = this.prototype;
 fBound.prototype = new fNOP();
 return fBound;
}
// 模拟实现apply
// ES6实现
Function.prototype.myapply = function (context, arr) {
 context = context ? Object(context) : window;
 context.fn = this;
 let result;
 if (!arr) {
   result = context.fn();
} else {
   result = context.fn(...arr);
}

 delete context.fn
 return result;
}
var toStr1 = Function.prototype.mycall.mybind(Object.prototype.toString);

console.log(toStr1({}));      // "[object Object]"
console.log(toStr1([]));      // "[object Array]"
console.log(toStr1(123));     // "[object Number]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1(new Date));// "[object Date]"

  上述的实现略去一些健壮性的代码,仅保留核心逻辑,具体的实现细节这里不做解释,有兴趣的可以自己研究,从devtools我们看到mybind形成的闭包确实在函数对象toStr1的作用域上!当然如果你对原型链有深刻理解的话,其实这句有趣的代码还可以写成如下方式:

var toStr3 = Function.call.bind(Object.prototype.toString);
var toStr4 = Function.call.call.bind(Object.prototype.toString);
var toStr5 = Function.call.call.call.bind(Object.prototype.toString);

  其实这个代码拆分出来看就好理解了:

1、Function.prototype.call表示调用函数原型上的call函数,跟调用Array.prototype.slice的方式没什么区别,都是调的原型上的方法;

2、(Function.prototype.call).bind(Object.prototype.toString); 这样看只是,给call方法做了个bind而已,改变了call函数的this指向为toString;

  作个假设,call函数内部通过调用this取得调用的函数,比如Array.prototype.slice.call则call内部this可以取得slice,

3、同理,Function.prototype.call 内部通过bind绑定,已经将this上下文绑定了成了toString,文中toStr1({}) 只不过在使用call({}),并且是提前绑定了this上下文为Object.prototype.toString,等价于Object.prototype.toString.call({})。

  可能你会疑惑为什么不写成Function.prototype.call(Object.prototype.toString, {}),别忘了call函数本身第一个参数就是作为参数使用,就像Array.prototype.slice.call({0:'1',1:'2',length:2})这个一样,根本无法改变到call的this,而是改变的slice的this,所以才会出现(Function.prototype.call).bind(Object.prototype.toString)这种写法,通过bind去改变call的this为Object.prototype.toString,这样,就等同于使用(Object.prototype.toString).call了,因为此时call的this指向了Object.prototype.toString。这样用法实际都是设计模式种鸭子模式的使用,只要长的像鸭子,叫声像鸭子,那么就可以当成鸭子用,这也是为什么Array.prototype.slice.call({0:'1',1:'2',length:2}),能够转化类数组的原因,因为有索引,有length,slice内部的this就能像操作数组的索引和length一样去取属性,自然也就能正常运行下去。

  其实就一句话:bind形参刚开始 和 object.prototype.tostring 指向同一块堆,后来改变了object.prototype.tostring指向,但是bind形参没有改变。

1、call,apply是动态的改变this的指向,即换个对象执行原对象方法的方法,并立即执行;

2、bind是静态改变this的指向,并返回一个修改后的函数。静态指向之后就不会改变,除非new。

  关于call、apply、bind的一些相关知识,可以详见我之前总结的博客:

  原生JS实现bind()函数

  深入理解this和call、bind、apply对this的影响及用法

  JavaScript中的bind方法及其常见应用

 

posted @ 2020-11-25 12:34  古兰精  阅读(169)  评论(0编辑  收藏  举报