最近开始看一些传统前端框架和库的源码,包括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 = {} |
起始今天有点晕晕的,文章写到这也差不多写完了,后面有时间应该也会来重新看看,如果有错的地方也会自行修改。