资讯专栏INFORMATION COLUMN

从一道面试题,到“我可能看了假源码[2]

chanthuang / 2715人阅读

摘要:函数是这样子声明的使用了系统自己的构造函数来声明,第一个参数是,函数体内又。构造函数调用情况正常方式调用无穷无尽当然,里还归纳了几项比较简单,我就不再翻译了。

上一篇从一道面试题,到“我可能看了假源码”中,由浅入深介绍了关于一篇经典面试题的解法。
最后在皆大欢喜的结尾中,突生变化,悬念又起。这一篇,就是为了解开这个悬念。

如果你还没有看过前传,可以参看前情回顾:

回顾1. 题目是模拟实现ES5中原生bind函数;
回顾2. 我们通过4种递进实现达到了完美状态;
回顾3. 可是ES5-shim中的实现,又让我们大跌眼镜...

ES5-shim的悬念

ES5-shim实现方式源码贴在了最后,我们看看他做了什么奇怪的事情:
1)从结果上看,返回了bound函数。
2)bound函数是这样子声明的:

bound = Function("binder", "return function (" + boundArgs.join(",") + "){ return binder.apply(this, arguments); }")(binder);

3)bound使用了系统自己的构造函数Function来声明,第一个参数是binder,函数体内又binder.apply(this, arguments)。
我们知道这种动态创建函数的方式,类似eval。最好不要使用它,因为用它定义函数比用传统方式要慢得多。
4)那么ES5-shim抽风了吗?

追根问底

答案肯定是没抽风,他这样做是有理由的。

神秘的函数的length属性

你可能不知道,每个函数都有length属性。对,就像数组和字符串那样。函数的length属性,用于表示函数的形参个数。更重要的是函数的length属性值是不可重写的。我写了个测试代码来证明:

function test (){}
test.length  // 输出0
test.hasOwnProperty("length")  // 输出true
Object.getOwnPropertyDescriptor("test", "length") 
// 输出:
// configurable: false, 
// enumerable: false,
// value: 4, 
// writable: false 
拨云见日

说到这里,那就好解释了。
ES5-shim是为了最大限度的进行兼容,包括对返回函数length属性的还原。如果按照我们之前实现的那种方式,length值始终为零。
所以:既然不能修改length的属性值,那么在初始化时赋值总可以吧!
于是我们可通过eval和new Function的方式动态定义函数来。
同时,很有意思的是,源码里有这样的注释:

// XXX Build a dynamic function with desired amount of arguments is the only
// way to set the length property of a function.
// In environments where Content Security Policies enabled (Chrome extensions,
// for ex.) all use of eval or Function costructor throws an exception.
// However in all of these environments Function.prototype.bind exists
// and so this code will never be executed.

他解释了为什么要使用动态函数,就如同我们上边所讲的那样,是为了保证length属性的合理值。但是在一些浏览器中出于安全考虑,使用eval或者Function构造器都会被抛出异常。但是,巧合也就是这些浏览器基本上都实现了bind函数,这些异常又不会被触发。

So, What a coincidence!

叹为观止

我们明白了这些,再看他的进一步实现:

if (!isCallable(target)) {
    throw new TypeError("Function.prototype.bind called on incompatible " + target);
}

这是为了保证调用的正确性,他使用了isCallable做判断,isCallable很好实现:

isCallable = function isCallable(value) { 
    if (typeof value !== "function") { 
        return false; 
    }
}

重设绑定函数的length属性:

var boundLength = max(0, target.length - args.length);

构造函数调用情况,在binder中也有效兼容。如果你不明白什么是构造函数调用情况,可以参考上一篇。

if (this instanceof bound) { 
    ... // 构造函数调用情况
} else {
    ... // 正常方式调用
}

if (target.prototype) {
    Empty.prototype = target.prototype;
    bound.prototype = new Empty();
    // Clean up dangling references.
    Empty.prototype = null;
}
无穷无尽

当然,ES5-shim里还归纳了几项todo...

// TODO
// 18. Set the [[Extensible]] internal property of F to true.
// 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3).
// 20. Call the [[DefineOwnProperty]] internal method of F with
//   arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]:
//   thrower, [[Enumerable]]: false, [[Configurable]]: false}, and
//   false.
// 21. Call the [[DefineOwnProperty]] internal method of F with
//   arguments "arguments", PropertyDescriptor {[[Get]]: thrower,
//   [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false},
//   and false.
// 22. Return F.

比较简单,我就不再翻译了。

源码回放
bind: function bind(that) {
    var target = this;
    if (!isCallable(target)) {
        throw new TypeError("Function.prototype.bind called on incompatible " + target);
    }
    var args = array_slice.call(arguments, 1);
    var bound;
    var binder = function () {
        if (this instanceof bound) {
            var result = target.apply(
                this,
                array_concat.call(args, array_slice.call(arguments))
            );
            if ($Object(result) === result) {
                return result;
            }
            return this;
        } else {
            return target.apply(
                that,
                array_concat.call(args, array_slice.call(arguments))
            );
        }
    };
    var boundLength = max(0, target.length - args.length);
    var boundArgs = [];
    for (var i = 0; i < boundLength; i++) {
        array_push.call(boundArgs, "$" + i);
    }
    bound = Function("binder", "return function (" + boundArgs.join(",") + "){ return binder.apply(this, arguments); }")(binder);

    if (target.prototype) {
        Empty.prototype = target.prototype;
        bound.prototype = new Empty();
        Empty.prototype = null;
    }
    return bound;
}
总结

通过学习ES5-shim的源码实现bind方法,结合前一篇,希望读者能对bind和JS包括闭包,原型原型链,this等一系列知识点能有更深刻的理解。
同时在程序设计上,尤其是逻辑的严密性上,有所积累。

PS:百度知识搜索部大前端继续招兵买马,有意向者火速联系。。。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81630.html

相关文章

  • 深入理解js

    摘要:详解十大常用设计模式力荐深度好文深入理解大设计模式收集各种疑难杂症的问题集锦关于,工作和学习过程中遇到过许多问题,也解答过许多别人的问题。介绍了的内存管理。 延迟加载 (Lazyload) 三种实现方式 延迟加载也称为惰性加载,即在长网页中延迟加载图像。用户滚动到它们之前,视口外的图像不会加载。本文详细介绍了三种延迟加载的实现方式。 详解 Javascript十大常用设计模式 力荐~ ...

    caikeal 评论0 收藏0
  • 一道面试引发的思考 --- Event Loop

    摘要:想必面试题刷的多的同学对下面这道题目不陌生,能够立即回答出输出个,可是你真的懂为什么吗为什么是输出为什么是输出个这两个问题在我脑边萦绕。同步任务都好理解,一个执行完执行下一个。本文只是我对这道面试题的一点思考,有误的地方望批评指正。 想必面试题刷的多的同学对下面这道题目不陌生,能够立即回答出输出10个10,可是你真的懂为什么吗?为什么是输出10?为什么是输出10个10?这两个问题在我脑...

    betacat 评论0 收藏0
  • 一道面试可能了假源码

    摘要:返回的绑定函数也能使用操作符创建对象这种行为就像把原函数当成构造器。同时,将第一个参数以外的其他参数,作为提供给原函数的预设参数,这也是基本的颗粒化基础。 今天想谈谈一道前端面试题,我做面试官的时候经常喜欢用它来考察面试者的基础是否扎实,以及逻辑、思维能力和临场表现,题目是:模拟实现ES5中原生bind函数。也许这道题目已经不再新鲜,部分读者也会有思路来解答。社区上关于原生bind的研...

    Carson 评论0 收藏0
  • 一道面试可能了假源码

    摘要:返回的绑定函数也能使用操作符创建对象这种行为就像把原函数当成构造器。同时,将第一个参数以外的其他参数,作为提供给原函数的预设参数,这也是基本的颗粒化基础。 今天想谈谈一道前端面试题,我做面试官的时候经常喜欢用它来考察面试者的基础是否扎实,以及逻辑、思维能力和临场表现,题目是:模拟实现ES5中原生bind函数。也许这道题目已经不再新鲜,部分读者也会有思路来解答。社区上关于原生bind的研...

    rockswang 评论0 收藏0
  • 一道面试可能了假源码

    摘要:返回的绑定函数也能使用操作符创建对象这种行为就像把原函数当成构造器。同时,将第一个参数以外的其他参数,作为提供给原函数的预设参数,这也是基本的颗粒化基础。 今天想谈谈一道前端面试题,我做面试官的时候经常喜欢用它来考察面试者的基础是否扎实,以及逻辑、思维能力和临场表现,题目是:模拟实现ES5中原生bind函数。也许这道题目已经不再新鲜,部分读者也会有思路来解答。社区上关于原生bind的研...

    jlanglang 评论0 收藏0

发表评论

0条评论

chanthuang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<