(这是两年前的笔记,现在把它搬到博客上,Underscore 版本:1.8.3)
受 这篇文章 的启发,萌生阅读 underscore.js 源码的念头,其中有许多不理解的地方,也是读了上述文章后才明白的.为了保持本文的完整性,也尽量按自己的理解进行注释. 不再提及上述引用文章.
全局定义
(function(){ |
underscore.js 中通过自执行函数来防止打乱已有的命令空间中的变量.这样函数中定义的所有变量在外部都是不可见的.但是仍旧需要以某种方式来导出其中定义的变量.
// Establish the root object, `window` (`self`) in the browser, `global` |
root
变量的作用是用来捕捉外部环境. 由于在自执行函数中,this
变量会被设置成
Window
(浏览器中),所以我们可能通过为 this
(即此处的root
) 添加相应的变量来导出函数. 如:
(function() { |
// Save bytes in the minified (but not gzipped) version: |
为了减少 JS 代码在网络传输中占用的流量,通常要对其进行压缩,以减少源代码的大小.方法之一是替换现有的变量名.将 Array.prototype
赋值给新变量,就允许我们对该变量进行重命名.例如: ArrayProto.toString => a.toString
而若使用诸如
Array.prototype.toString => a.prototype.toString
则找不到该函数.
// Create quick reference variables for speed access to core prototypes. |
以上同理.
// Naked function reference for surrogate-prototype-swapping. |
Ctor
函数只有一个用途,就是为了兼容老版本 JavaScript 的继承,即用来实现
Object.create
函数.
SubClass.prototype = Object.create(SuperClass.prototype) |
Ctor
在之后的 baseCreate
函数中使用.
链式调用
因为涉及的内容较多,所以归成一节.
首先,我们要明白什么是链式调用.简单地说,链式调用是方便我们写代码的一个手段,看下面的例子:
var x = obj.method_O(); |
上述写法需要许多中间变量,由于对象 obj
的 method_O
方法正好返回一个类 X
的对象(这里指的是返回的变量 x
需要有 method_X
方法),所以可以直接调用 X
的方法 method_X()
. 以此类推.因此我们可以省略其中的中间变量,写成:
obj.method_O().method_X().method_Y().method_Z(); |
要达到上述效果,我们便需要让 method_O()
方法在结束时返回一个类 X
的对象:
function method_O() { |
以此类推.上述方法是 Javascript 原生支持的.现在的问题在于,例如调用 method_X
方法返回了 Y
的对象,就再也无法使用类 X
中的方法了.例如:
var flattened_obj = _([[1,2]]).flatten(); |
上述代码中我们首先创建了一个 underscore.js 的对象 _([[1,2]])
目的是使用
underscore.js 为我们提供的丰富辅助函数.之后我们调用 underscore.js 中的
flatten
函数得到一个扁平化的数组: [1,2]
. 之后我们想在其中调用
underscore.js 的 each
函数. 此时报错,提示没有该函数.故此时我们无法使用链式调用:
_([[1,2]]).flatten().each(...) // 报错 |
故而 underscore.js 需要提供一些机制来包裹返回的对象,使之能访问 underscore.js 中的函数.
underscore.js 中通过 _.chain(obj)
来返回一个包裹的 _
对象;再对
underscore.js 中提供的所有函数做特殊的处理,使得:当调用函数的是包裹的对象时,返回的结果也是一个 _
的对象,而由于 underscore.js 中的所有函数都存放在 _
中,所以调用链中的每一步都可以访问 underscore.js 中的函数.
例如:
_.chain([[1,2]]) instanceof _; // => true |
链式调用的实现
// Create a safe reference to the Underscore object for use below. |
从上面的函数可以看到 _
函数生成一个新的 _
对象,并将输入的 obj
置于
this._wrapped
中. 而 _.chain
函数则再设置 this._chain = true
的标志.
单凭上述两个函数并没有实际用途,因此需要一个辅助函数:
// Helper function to continue chaining intermediate results. |
该函数检查 instacne
本身是否设置了 _chain
标志,若是则将 obj
用 chain()
包裹,它的作用就是对调用链上函数返回的结果进行处理,如 x.method()
中,若设置了 _chain
标志,则将 x.method()
的返回结果再用 chain()
包裹.这样调用链中的每个函数返回的都是一个 _
的对象,因此也就能继续访问类 _
的方法了.
还有一个问题是,即使有以上函数, underscore.js 在定义新的函数时仍需手工调用
chainResult
函数,十分麻烦. 所以 underscore.js 又提供了另一个辅助函数,将所有已有的函数进行包裹:
// Add your own custom functions to the Underscore object. |
该函数将 obj
中的所有函数替换成包裹后的函数.首先取出 _
对象中包裹的实际值, push.apply(args, arguments)
将该值与现有的函数参数结合,最后对原函数的返回值进行处理: chainResult(this, func.apply(_, args))
.
还有一些函数单独作了处理,如 pop
, push
, reverse
, 等等,此处不再详谈.
接前文
// Export the Underscore object for **Node.js**, with |
上文较好理解,判断不同的平台,导出 _
变量。
// Internal function that returns an efficient (for current engines) version |
要理解 optimizeCb
的作用,需要先理解 underscore.js 提供的 context 切换的功能。我们首先查看 _.each
的文档:
each: _.each(list, iteratee, [context]) Alias: forEach |
它接收额外的参数 context
。而它的作用是在 iteratee
函数中将 this
指向
context
。下面的是一个
StackOverflow
的例子:
var someOtherArray = ["name","patrick","d","w"]; |
关于 context 的具体应用可以参考 这篇文章
为了切换 this
的实际值,我们需要做如下的工作:
var origin = function(arg ...) { |
即通过 function.call(...)
的方式来调用函数,以传入新的 this
值。而
optimizeCb
函数便是 underscore.js 内部用于完成这个转换的辅助函数。
optimizeCb
函数中判断了目标函数 func
的参数个数,返回不同的函数,如果参数的个数不是 1~4,则采用通用的逻辑 func.apply
代替 func.call
。似乎对当前的引擎而言,func.call
要稍快于 func.apply
。 这个网页 用于测试各种调用方式的效率,在我本机测试下 call
要稍快于(7%) apply
// A mostly-internal function to generate callbacks that can be applied |
cb
几乎只被内部函数使用,用途是根据 value
的类型生成回调函数。
// Similar to ES6's rest param (http://ariya.ofilabs.com/2013/03/es6-and-rest-parameter.html) |
restArgs
也只在内部使用,它用来实现类似其它语言(及ES6)的 rest
参数。rest
参数的作用是将多余的参数以数组(Array)的方式保存为最后一个参数。
function test(a, b, rest) { |
当然,JavaScript 并不直接支持(ES6 前)这样的语法,所以 underscore.js 自己实现了一个(JavaScript 真强大啊!)。有了 restArgs
我的就能写成:
function orig(a, b, rest) { |
// An internal function for creating a new object that inherits from another. |
baseCreate
与 Object.create(...)
等价,只是老版本的 JavaScript 没有
Object.create
函数,因此用它来做兼容。
var property = function(key) { |
isArrayLike
用来判断一个 collection 是否是“类数组”的,那什么是“类数组”呢?需要满足两个条件:
- 元素可以通过编号访问
- 元素个数通过
length
属性得到。
“类数组” 不要求有数组(Array)提供的函数,如 push
, forEach
及 indexOf
. 例如:
var arrayLikeCollection = {} |
所以,underscore.js 中定义的 isArrayLike
并没有真正检查条件1。条件 2 在先前的版本中是通过 obj.length === +obj.length
完成的,但似乎在某些情况下有 BUG,于是改成了当前的版本。
Collection 函数
本节中讲的是一些 collection 的辅助函数,如 map
, each
, reduce
等等。这些函数常用于函数式编程语言(如 Haskell)中,它们能更好地描述 collection 的一些操作。在编程中,我们要学习利用这些函数,学会从 collection 的整体角度进行思考,而不以 collection 中的元素作为处理对象。
// The cornerstone, an `each` implementation, aka `forEach`. |
_.each
函数是 collection 相关函数的基石,它的作用是将函数 iteratee
应用于
collection 中的每个元素,而 map
函数将 iteratee
每次调用的结果收集,以一个数组返回。
注意的是 _.each
与 _.map
同时支持以 “类数组”及 collection。在
underscore.js 中,通常将 object 抽象成 “广义的数组”。广义的数组包含一个键数组 keys
和一个值数组 values
,它们一一对应,而由于它们是数组,也因此可以通过索引进行访问。对于普通的“类数组”,键数组中包含的就是对应值的索引。
所以,在涉及到索引相关的运算时,underscore.js 通常会先获取键数组,如 _.map
函数中的:
// 获取键数组 |
相应的,如果涉及值运算时,underscore.js 通常会先取得它的值数组:
obj = isArrayLike(obj) ? obj : _.values(obj); |
这个模式中 underscore.js 中被多次运用。
// Create a reducing function iterating left or right. |
与 _.map
一样,_.reduce
也是函数式编程语言中常用的辅助函数,上面的代码较乱,下面是一个更为简单的实现,用以演示核心的逻辑。
function reduce(coll, func, init_val) { |
这里的实现使用了两个闭包,这篇文章 认为这里闭包的作用是持久化变量。但我认为,这里将逻辑分成两个函数的目的,如注释所说的,是为了提高执行的效率。即使主逻辑中不包含对arguments.length
的使用,但具体为何能提高效率,还有待学习。
_.find
, _.filter
, _.reject
, _.every
, _.some
等函数中规中矩,唯一要注意的是它们是如何同时处理 collection 和“类数组”的情况。
var group = function(behavior, partition) { |
group
函数稍微难理解一些,它只在 underscore 内部使用。函数的主要复杂性来源于参数 partition
,它用来标记 group
返回的函数返回结果的类型。我认为这是一个不恰当的抽象,一个更直观的抽象应该是(这里不考虑 context 切换的问题):
var simpleGroup = function(behavior) { |
即对于 obj
中的每个元素,通过调用 iteratee
函数得到一个分组的依据 key
,再调用 behavior
对返回的结果进行组装。如 _.groupBy
函数:
// Groups the object's values by a criterion. Pass either a string attribute |
它的 behavior
函数就是将 iteratee
调用后的结果 value
按 key
进行分组。
上面提到,group
由于支持 partition
带来了额外的复杂性,具体的调用如下:
_.partition = group(function(result, value, pass) { |
而其实该函数可以由 _.groupBy
实现:
_.partition = function(obj, iteratee) { |
// Generator function to create the findIndex and findLastIndex functions |
createPredicateIndexFinder
根据指定的步长 dir
创建遍历的函数。而实际上它在被用来创建 _.findIndex
和 _.findLastIndex
,但无疑,这增加了许多阅读上的复杂度。当一个逻辑没有被很多使用时,是否需要独立成一个单独的模块,值得思考与讨论。
函数相关的函数
// Determines whether to execute a function as a constructor |
这里,我们首先回顾一下 restArgs(func, startIndex)
默认的使用方法。当参数
startIndex
为空时,它默认为 func
参数个数减一。所以有:
function orig(a, b, rest) { |
知道了这点就不难看懂 _.bind
与 executeBound
函数。还有一点需要深追的是条件判断:(!(callingContext instanceof boundFunc))
,它的作用是什么呢?
其实 _.bind
是要实现 ECMA5 中的 Function.bind
类似的功能,我们从 MDN 上截取 bind
函数的一个使用实例:
this.x = 9; |
上述例子在执行时,callingContext
指向的是全局的 Window
(浏览器中)。而只有当我们创建一个新的该函数的对象时,才会出现 callingContent instanceof boundFunc
的情形:
var instance = new boundGetX(); // 或者 |
这是由 new
操作符的特性导致的。一般来说,获取一个函数(类)的一个实例“只能”通过 new
操作符来完成。new func(...)
执行了三个步骤:
- 创建一个新的对象,该对象继承了
func.prototype
; - 以新创建的对象为
this
调用构造函数func
; - 如果
func
有返回值则返回它,若没有,则返回第1步创建的对象。
以代码来说就是:
var newObj = Object.create(func.prototype); |
因此,在上述例子中 func.apply
的过程中,this
指针必须要指向 newObj
而不能指向先前绑定的 context
值。所以 executeBound
判断了这一情况,并实现了类似 new
操作符的逻辑。
// Partially apply a function by creating a version that has had some of its |
partial
函数类似于科里化(curry),但功能更加强大。关键在于支持占位符。如:
var subtract = function(a, b) { return b - a; }; |
并且,由于要支持占位符,所以每次执行 _.partial
返回的函数,它的内部都要访问
_.partial
定义时的参数,无形中降低了一些效率。即:
args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i]; |
_.throttle
和 _.debounce
函数都比较有意思。其中 _.throttle
将对一个函数进行包裹,返回一个函数,当我们迅速调用该函数时,在一个的时间范围内,至多被调用一次。可以实验以下代码:
var inc = (function() { |
可以看到在 3s 内它只会被调用一次,且在这个时间范围内,调用直接返回前一次调用得到的结果,而不实际执行函数。
_.debound(func, wait)
正好相反,如果执行了某个函数后,在 wait
时间内,若再调用该函数,则不执行它,并且将等待时间置零,直到 wait
时间后才继续执行。
Object 相关函数
// Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. |
In IE < 9, JScript will skip over any property in any object where there is a same-named property in the object’s prototype chain that has the DontEnum attribute.
在 IE < 9 中,若 object 中的某个属性在它的原形链 (prototype chain)
上有一个同名的,具有 DontEnum 特性的属性,则在 for key in object
枚举时将被忽略。
上述代码就是用来处理这个情形。注意代码中是如何手工判断 obj
是否含有键 prop
:
if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { |
// An internal function for creating assigner functions. |
createAssigner
看似较为复杂,但只要了解了它如何使用,那其中的逻辑也不难理解了。我们看 _.extend
的使用例子:
_.extend({name: 'moe'}, {age: 50}); |
即,它以接收多个 object 作为参数,将第2个及之后的 object 的属性不断加入/覆盖到第一个 object 中并返回。因此 createAssigner
的核心就是两层循环,外层对参数进行迭代,内层对该参数的所有属性进行迭代。
// Internal pick helper function to determine if `obj` has key `key`. |
_.pick
的复杂性也是由于额外的支持引起的。由于它可以接受一个函数作为参数,用作判断一个键是否选取的依据,因此它的代码中就要对参数 keys
是函数的情况进行判断。如果 keys
只是普通的键名,则 iteratee
退化为 keyInObj
。额外的一点是,_.pick
除了接收函数作参数,同时还支持改变该函数的 context
,函数中的
if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]); |
就是起这个作用的。
// Invokes interceptor with the obj, and then returns obj. |
前面提到了链式调用,_.tap(func)
的作用是将 func
应用到链式调用的中间结果。看 underscore.js 官方的例子:
_.chain([1,2,3,200]) |
从 _.tap
的实现中我们注意到,几乎所有 underscore.js 内置的函数的第一个参数都是 obj
。而这样定义的函数我们又能以两种方式调用,如 _.each
函数:
_.each([1,2,3], function (x) { console.log(x);}); // 1 |
而 _.each = function(obj, iteratee, context) {...}
包含三个参数,为什么第 2
种调用可行,而第三种调用则出错呢?
原因是:1、2 两种调用的根本就不是一个函数!
首先要注意的是 _
变量本身是一个函数,而在 Javascript 中,函数同时承载着“类”的功能。因此要区分两种赋值方式:_.attr = ...
及 _.prototype[attr] = ...
第一种是为变量(对象)本身添加属性,第二种是为原型(类)添加属性。区分以下例子:
var underscore = function () {} |
所以,当我们试图访问变量(对象)的某个属性时,它会首先寻找变量本身的属性,若不存在,则通过原型链(prototype chain)进行查找。
回到 _.each
的例子上,_.each([1,2,3], func...)
的调用的方法是变量(对象)
_
的属性,而 _([1,2,3]).each(...)
调用的是变量(对象)_([1,2,3])
的属性,而由于该变量并没有 each
属性,所以是调用的是 _.prototype.each
函数。
最后一个问题是 _.prototype.each
是在哪里设置的呢?答案是 _.mixin
函数中,上文已有讨论。
相等判断
(不知道 Equality 怎么翻译)
这部分是用来学习 Javascript 内部判等机制的好材料。这里只涉及一个函数 eq
用来深度判等,举例来说,两个数组相等,当且仅当包含同样个数,且每个元素都相等,由于元素可能还是数组,所以要递归(深度)地进行判断。
先来个链接:判等表格
eq = function(a, b, aStack, bStack) { |
可见,几乎所有的原子型数据都可以通过 ===
进行判断。具体的判断方法参见 ECMA6
Strict Equality
Comparison
个人觉得使用 x === y
有几点值得一说:
===
会首先判断x
与y
的类型,若不相同,则返回false
。===
会判断x
与y
的值(原子类型),若相等,则返回true
,反之false
.NaN
不等于任意数字,另-0 === +0
。- 对于非原子类型,当且仅当它们是指向同一个 object 时才
===
。
接下去的 deepEq
函数很长,我们逐步分析。
|
虽然在 eq
函数中判断了原子型数据,但由于我们可能创建了 underscore.js 的对象,如 _(1)
或 _("abc")
,它们并不是原子型数据,所以上面的代码相当于自己实现了 ===
的逻辑。根据 a
b
的类型进行相应的判断。
var areArrays = className === '[object Array]'; |
判断一个对象是否是 ‘Array’ 的正确方法。
if (!areArrays) { |
上文代码应是有些 Bug,因为函数并非数组,所以会进入该 if
语句,但由于它们的类型并非 object
所以直接返回 false
,即所有函数都不相等。考虑下面的测试用例:
var tmp = function () {} |
所以结果是所有的函数都不相等。
接下来重要的是下面这个代码:
// Assume equality for cyclic structures. The algorithm for detecting cyclic |
上面的代码是用来检测环形数据结构的,什么意思呢?就是对象中直接或间接地引用了自己本身,如:
// 直接引用 |
上述检测环形数据的原理是:只要是环形数据,意味着在递归获取子结构时,在某个时候,得到的子结构会与之前访问过的某一结构完全一致。如:
1 -> 2 -> 3 -> 4 -> 5 |
在第一次访问 3 时,aStack
中保存了整个环(3 -> 4 -> 5 -> 6 -> 7 -> 8 ->
…),所以第二次访问 3 时,仍然得到这个环(3 -> 4 -> 5 -> 6 -> 7 -> 8 ->
…),此时,条件 if(aStack[length] === a)
就会通过,从而检测出该环。
类型判断
接下去是一些类型判断的函数如 isArray
,isObject
等。主要的判断依据是
toString
函数。
根据 ELS6,
Object.prototype.toString
会输出调用时 this
所指对象的内部 [[Class]]
属性。输出 "[object" + [[Class]] + "]"
。
例如,在创建数组对象时,对象的 [[Class]]
属性会被设置为 Array
,故对数组调用 toString
时将输出 [object Array]
。
这里要注意的是 toString
的调用方法:toString.call(obj)
而非
toString(obj)
。这就涉及到函数调用 toString(obj)
时 this
的值究竟是什么?它的规则如下:
函数是否由 new
调用?
- 是 ->
this
指向新建的对象 - 否 -> 函数是否由
dot(.)
进行调用?- 是 ->
this
指向 dot 之前的对象 - 否 ->
this
指向全局对象 window
- 是 ->
测试下面代码的结果:
toString("abc"); // => "[object Undefined]" |
template
模板函数是 underscore.js 中个人觉得最有趣的函数。
// JavaScript micro-templating, similar to John Resig's implementation. |
当然在使用之前要明白它的 使用方法。简单来说就是预先定义好模板,之后就可以用它来生成字符串。
模板中支持三种替换类型:值替换(interpolate)<%= ... %>
;执行替换(evaluate) <% ... %>
及转义替换(escape) <%- ... %>
。
下面的例子取自官网:
var compiled = _.template("hello: <%= name %>"); |
在试图看懂这段代码之前,我们先来了解 Javascript 中的 eval 函数。它的作用是将输入的字符串作为代码执行。举个“看似”有用的例子:
function gen_getter_setter(obj, field) { |
例子中 gen_getter_setter('object', 'a')
生成的字符串如下:
"function get_a() { |
也即我们生成了一个字符串,但字符串的内容完全符合 Javascript 的语法,因此
eval
可以根据 Javascript 的语法来解析该字符串。可以参考 Lisp 中的 Macro
(宏)。
说了这么多,可是代码里根本没有 eval
啊?好吧,是的,只是代码里通过 new Function(...)
创建新的函数对象时,也是传递字符串作为函数的函数体(函数的正文)。所以要明确的就是我们可以构建字符串,将字符串作为代码来执行。
因此,_.template
函数的大部分功能就是在构造 render
函数的函数体。我们先撇开对 source
的构建,先看 render
的框架部分(重新调整了格式):
function (obj, _) { |
从上面的代码可以看出,生成的代码根据参数 obj
进行操作(具体操作未知),最终将代码存放在变量 __p
中返回。
所以具体的操作就要看 source
中的内容,而它又是根据模板字符串 text
生成的。下面再贴出主要逻辑的代码,以便于查看:
var source = "__p+='"; |
这段代码将匹配 _.template
支持的三种模式,即 <%= ... %>
、<% .. %>
及<%- .. %>
并将其替换成相应的代码。
例如:对于模板字符串 "<b><%- value %></b>"
,则会进入 escape
分支(注意会调用不止一次),生成相应的代码放在之前的上下文中如下:
function (obj, _) { |
理解 _.template
代码要注意区分生成字符串的代码与生成的字符串。相信跟着例子调试几次就能够完全理解它了。
写在后面
阅读 underscore.js 的代码花费了许多时间,但受益颇丰。通过深究其中的许多细节,让我对 Javascript 的原理有了更深的理解和掌握。相信只要读者静下心来,仔细钻研其中的细节,定有收获。
文章写得匆忙,也只是作为个人的笔记,若有错误不足之处,敬请批评指正。