最近开始看一些传统前端框架和库的源码,包括Bootstrap underscore等,准备在2017年刚步入工作的这半年里面能够有时间阅读完,同时能够将阅读过程中的收获记录下来。
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?
最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,
非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0
代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&
函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及
API 设计的原理(向后兼容)。
上面这段话引自hanzichi,以后关于underscore.js的源码阅读的过程应该会借鉴他的文章。当然也不会仅仅局限于他的文章啦(#^.^#)。
这篇文章首先来说说underscore.js的整体架构,然后再来说说undefined为何被void 0替代,最后再来说说underscore.js中的_.keys函数中的对于for in在IE9以下浏览器中的特殊处理。
underscore.js的整体架构
首先underscore.js用一个立即执行函数将所有的代码包裹起来,形成一个独立的作用域,来防止其他代码对于underscore代码的影响,同时避免全局变量的污染。1
2
3
4
5
6
7
8
9
10// Underscore.js 1.8.3
// http://underscorejs.org
// (c) 2009-2017 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Underscore may be freely distributed under the MIT license.
(function() {
//all codes here
}());
核心代码的第一部分,就是一些基本的配置。
首先建立root对象,在客户端(浏览器)中建立window(self),在服务端建立(node环境下)global,或者一些虚拟机中建立this。为了支持webWorker,作者用self代替window进行配置。
1 | // Baseline setup |
之后将全局环境中的_变量赋值给previousUnderscore,用于后面的noConflict函数来解决冲突.
1 | // Save the previous value of the `_` variable. |
接下来做了一些变量缓存,为的是减少代码压缩的体积,当然这里的代码压缩不是指的的gzip压缩,当然,代码缓存还包含了快速引用,减少js引擎在原型链中查找的长度,提高代码效率。
1 | // Save bytes in the minified (but not gzipped) version: |
接下里定义了一些变量去引用ES5中的一些方法,如果环境允许使用,则underscore会优先使用它们。
1 | // All **ECMAScript 5** native function implementations that we hope to use |
之后声明一个Ctor变量,用于后面baseCreate函数来兼容老版本 JavaScript 的继承,即用来实现 Object.create 函数。
1 | // Naked function reference for surrogate-prototype-swapping. |
接下来声明_构造函数,函数内部做了一步优化处理,用于检测用户是否使用了new关键字调用,若果没有,则返回一个new实例。
1 | // Create a safe reference to the Underscore object for use below. |
接下来就是,在node.js环境下将underscore作为一个模块使用,并向后兼容旧版的模块API,即require。如果是浏览器环境下则暴露在全局环境下。
1 | // Export the Underscore object for **Node.js**, with |
关于underscore的架构先写这么多,之后有坑再来填吧。
undefined为何被void 0替代
注意到underscore函数中有一个函数isUndefined是用来判断undefined的,代码如下:1
2
3
4// Is a given variable undefined?
_.isUndefined = function(obj) {
return obj === void 0;
};
为什么需要用void 0来代替undefined呢?查阅了相关的资料,最终在《You-Dont-Know-JS》中找到答案。
undefined是一个内置的标识符,不过它是可以被赋值的,可以从以下代码中看出来。
1 | function foo() { |
1 | function foo() { |
在严格模式非严格模式下,我们都可以声明一个名为undefined的局部变量。正是因为如此,underscore才会定义isUndefined来特殊处理undefined。
1 | function foo() { |
表达式void _没有返回值,因此返回值为undefined。同时,void并不会改变表达式的返回值,只是让函数不返回值。按照惯例我们使用void 0来获取undefined(这种习惯来自于C语言)。
1 | var a = 42; |
_.keys和_.allKeys函数
1 | // Retrieve the names of an object's own properties. |
注意到_.keys和_.allKeys对于对于IE9 以下的环境做了一个BUG修复。我们就来看下hasEnumBug和collectNonEnumProps函数是怎样实现的。
1 | // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. |
IE < 9下不能用for key in ...来枚举对象的某些 key比如重写了对象的 toString 方法,这个 key 值就不能在 IE < 9 下用 for in 枚举到IE < 9,{toString: null}.propertyIsEnumerable('toString')返回 false,所以IE < 9,重写的 toString 属性被认为不可枚举,所以在underscore中定义了hasEnumBug函数利用propertyIsEnumerable方法来判断是否有这个bug。
IE < 9 下不能用 for in 来枚举的 key 值集合有['valueOf', 'isPrototypeOf', 'toString', 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']。
proto 变量保存了原型,一个对象的原型可以通过 obj.constructor.prototype 获取,但是如果重写了 constructor 很显然就无法这样获取了,则用 Object.prototype 替换。这样比如说重写了 toString,我们只需要比较 obj.toString 是否和 proto.toString引用相同即可。
至于if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop))中对于prop in obj的判断,我觉得是有必要的,有一种情况如果不加结果就会不同:
1 | var o = {} |
起始今天有点晕晕的,文章写到这也差不多写完了,后面有时间应该也会来重新看看,如果有错的地方也会自行修改。