源码版本 1.4.4
创建 Underscore 对象的引用
1 | var _ = function(obj) { |
上面的代码创建了 undersore 的引用,然而我对执行顺序却理得不是很清楚,尤其是上面的第三行,于是我尝试在 chrome 中 debug 了一下。不过在理解之前我们先复习一下使用 new 调用构造函数的具体流程,以及 instanceof 操作符的用法。
new 调用构造函数的实际操作
我们都知道,用 new 调用构造函数时,操作符会创建一个空对象,然后让被调用函数的 this 指向这个空对象。这样,在构造函数内部使用的 this.property 就可以为这个空对象添加属性了。同时,new 会为空对象添加一个属性 __proto__,让此属性指向构造函数的原型对象。调用构造函数之后,new 会将这个空对象返回,这个空对象就是构造函数的实例对象了。
为了理解,我们来看下面的这段代码,它调用构造函数生成了 Color 的实例对象:
1 | function Color () { |
在上面的代码中,我们用 new 调用构造函数时,它实际执行的操作如下:
1 | var obj = {}; // 创建一个空对象 obj |
从上面可以清晰地看到, new 创建了一个空对象,并为其添加了 __proto__ 属性,然后将函数的 this 绑定到此对象上执行函数,最后返回了此空对象。
instanceof 操作符的作用
instanceof 运算符用来判断一个构造函数的 prototype 属性所指向的对象是否存在另外一个要检测对象的原型链上。
这句话有点抽象,但是我们知道通常我们可以用 instanceof 操作符来判断一个对象的类型。在上面出现的代码中,如果我们执行这一行代码 foo instanceof Color ,会发现结果为 true,因为 foo 的类型即为 Color。
现在我们再考虑这句话,发现实例对象 foo 上拥有一个指向 Color 的原型对象的 __proto__ 属性,因此 instanceof 操作符的结果为 true。这就是 instanceof 操作符能够判断对象类型的原因。
回顾了 new 与 instanceof 的用法,接下来我们来看在创建一个具体的 Underscore 对象时,构造函数的执行过程。
构造函数的执行过程
由于构造函数本身也是一个函数,因此我们有如下两种调用方法来创建对象:
1 | var _ = function(obj) { |
当我们使用方法一创建 _ 的实例对象时,new 操作符会像上面提到的那样创建一个空对象,空对象的 __proto__ 属性指向 _.prototype,然后执行构造函数。
在构造函数的第二行,由于 obj 实际为 {},不是 _ 的实例,结果为 false,接着执行第三行。在第三行中,由于 new 创建的空对象已经包含了指向 _.prototype 的 __proto__ 属性,且 此函数的 this 就指向这个空对象,于是 this instanceof _ 结果为 true,再取反后为 false,于是跳过执行第四行。最终在第四行把 obj 交给 _ 的 _wrapped 属性。
从上面可以看出,使用第一种方法创建 _ 的实例时,第三行貌似可以删除,因为总会跳过。但是,当我们用第二种方法生成 _ 的实例是,第三行就比不可少了。
使用方法二直接在全局中调用构造函数时,构造函数的 this 指向外部的全局对象 window。此时当代码执行到第三行后,由于 window 不包含指向 _.prototype 的属性,!(this instanceof _) 会返回 true,于是执行 return new _(obj)。这样就会像第一种方法一样,会把 obj 交给 _wrapped,生成一个 _ 的实例对象。这个实例对象最终由 return 返回给 foo。
简言之,第三行是为了让我们能够用第二种方式创建 _ 对象实例而存在的。
Array.prototype.forEach() 方法
语法:
1 | array.forEach(callback(currentValue, index, array){ |
callback 为数组中每个元素执行的函数,该函数接收三个参数:currentValue(当前值)数组中正在处理的当前元素。index(索引)数组中正在处理的当前元素的索引。array为 forEach() 方法正在操作的数组。thisArg可选可选参数。当执行回调 函数时用作 this 的值(参考对象)。
(一直以来都还不知道thisArg这个参数呢 = =)
_.each(obj, iterator, [context])
_.each() 方法用于遍历一个数组或者对象,类似于 ECMAScript 5 为 Array 提供的原生的 forEach() 方法,只不过同样适用于对象而已。如果存在原生的 forEach 方法,Underscore 会优先调用原生的方法。
1 | // 一个特殊的对象,这个对象用于控制在循环中跳出 |
在代码的第五行有一句 obj.length === +obj.length,它的作用是判断对象 obj 是否拥有 length 属性(判断 obj 是不是数组或类数组对象)。在这里,会先执行 +obj.length,+ 的作用是把 obj.length 转化成数字类型;然后 === 将转化的结果与 obj.length 相比较,看两者的类型和值是否相同。
一般来说, Object 类型是没有 length 属性的,因此 obj.length 是 undefined,+obj.length 是 NaN,比较结果为 false。而对于 Array、String 类型以及其他类数组对象来说,它们都是拥有 length 属性的,而且可以用方括号来访问对应位置的值,因此可以在后面用 for 循环来遍历。
在第十一行有一个 breaker,它是在前面定义的一个变量,其值为 {}。关于 breaker 的作用,正如注释里所提到的那样,是用来跳出 each 的循环的。在 Underscore.js 的其他地方,会有另外的一些函数调用 each 函数。当这些函数想要从 each 循环中跳出时,它们就会返回一个 breaker。这样第 19 行的结果会为 true,each 函数就停止执行了。
如果还没有理解的话可以看看下面的代码,它是 Underscore.js 里的 some() 函数的简化版。some() 函数的作用是判断数组中是否有元素满足迭代器的检测,只要任意一个元素满足了检测就返回 true,并中断对数组的遍历。
1 | _.some = function (obj, iterator) { |
类数组对象
在 js 的 DOM 操作中,经常遇到一个 NodeList 属性,它就是一个类数组对象。对于类数组对象,我们可以用 length 来访问它的元素个数,还可以用方括号语法来访问对应下标的值。
其实,在上面第 9 行代码处就是借用了类数组对象。就算 obj 本身不是数组,像 String 类型也可以用方括号和下标访问对应地方的值,因此可以用 for 循环来遍历。
了解更多关于类数组对象的内容可以看看这里。
_.bind(function, object, *arguments)
函数 bind 作用与 ES5 提供的原生的 bind 函数作用相同:将函数 function 绑定到参数 object 上,即函数无论在何处被调用,内部的 this 始终指向绑定的 object。bind 的方法的第三个可选参数 arguments 是提前传给 function 的任意数量的参数,这样,函数在调用时只接收剩余未绑定的参数。bind 方法返回原函数的拷贝函数,这个函数的 this 及部分参数已被指定。
下面 Underscore.js 中对这个方法的实现:
1 | // 用于原型设置的可重用构造函数 |
初次看这段源码时对第13行内的 if 判断及相应的代码很困惑,在网上查找一番后终于在 segmentfault 上找到了答案。
原来,当一个函数以普通方式调用时,即 f(),this 会指向 window 对象。此时的 !(this instanceof bound) 自然为 true,进入 if 内部执行。但是,如果函数被用 new 调用时,即 new f(),此时 !(this instanceof bound) 语句中的 this 有一个指向 bound 原型对象的指针 __proto__(详见前面提到的new与instanceof操作符的实际操作),因此这句的结果为 false,执行 if 后面的内容。
if 后面的代码就是针对使用 new 调用函数的情况。我们知道,使用 new 调用函数时,函数的 this 指向一个空对象。如果上面的代码没有针对 new 调用函数的特殊情况做处理,那么函数的 this 会指向绑定的 context,就出错了。