1. 首页

这是一句有趣的JS代码

难度:中等

我们经常在框架级的源码中看到类似如下的一句代码,比如:

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */

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

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

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

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */

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]"

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

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */

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(new Date));// "[object Date]"

熟悉bind和call的同学应该知道,两种方法本质上是相同的,而第二种方法更简洁,仅仅使用一次call就能获得我们想要的功能,且代码逻辑清晰,理解起来更加容易,可在众多框架中为何更多使用第一种呢?

其实主要的原因是防止原型污染,比如我们在业务代码中覆写了Object.prototype.toString方法,第二种写法将得不到正确的结果,而第一种写法仍然可以。我们用代码来来试试:

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */

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(new Date));// "toString方法被覆盖!"

image

image

结果很明显。第一种方法仍然能正确得到结果,而第二种则不行!那么为什么会这样呢?我们知道bind函数返回结果是一个函数,这个函数是函数内部的函数,会被延迟执行,那么很自然联想到这里可能存在闭包!因为闭包可以保持内部函数执行时的上下文状态。不过在现代版浏览器中call和bind都已经被js引擎内部实现了,我们没有办法调试!但是我们可以通过polly-fill提供的近似实现的源码来理解引擎内部的逻辑,下面是本文调试的demo,大家可以尝试下:

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */

// 模拟实现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 () { };
​
 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;
 var fn = Symbol();
 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]"

image

上述的实现略去一些健壮性的代码,仅保留核心逻辑,具体的实现细节这里不做解释,有兴趣的同学可以自己研究,从devtools我们看到mybind形成的闭包确实在函数对象toStr1的作用域上!

当然如果你对原型链有深刻理解的话,其实这句有趣的代码还可以写成如下方式:

 /* Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */

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);
​
// 甚至可以这么写。。。
var toStr6 = (()=>{}).call.bind(Object.prototype.toString);

-END-

作者:风清洋
链接:https://segmentfault.com/a/1190000038042300

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:这是一句有趣的JS代码

链接:https://www.javascriptc.com/4791.html

« 为开源项目做贡献的10个步骤
Nodejs V12»
Flutter 中文教程资源

相关推荐

QR code