【Javascript】Sizzle引擎--原理与实践(一)

简述

Sizzle是jQuery的御用选择器引擎,是jQuery作者John Resig写的DOM选择器引擎,速度号称业界第一。
另外,Sizzle是独立的一部分,不依赖任何库,如果你不想用jQuery,可 以只用Sizzle。
所以单独拿出来特别对待。在Prototype1.7中,选择器也采用了Sizzle,不过版本有点老,所以我去Sizzle网站搞了一份新的 下来,于是下面分析的时候使用的是最新版的Sizzle.js

预先说明

在分析初期为了保证各个浏览器的结果一致,不考虑原生getElementsByClass以及querySelectorAll的影响,同时忽略XML类型,因此作一下处理:

源码1292行,

1
2
3
(function(){
...
})()

改为:

1
2
3
4
(function(){
return false;
...
})()

源码1140行

1
2
3
if ( document.querySelectorAll ) {
...
}

改为:

1
2
3
if ( document.querySelectorAll && false) {
...
}

至于其他的浏览器兼容处理部分,会在初步分析中一并涉及。

预备知识

CSS选择器,jQuery选择器。由于jQuery选择器的形式来自CSS,但是在CSS的基础上又增加了很多新的选择表达式,因此,一切以jQuery选择器为基础。

实例开题

对于一个选择表达式:

div#outer > ul , div p:nth(1),form input[type="text"]

关于分块的讨论

这里面包含三个并联的选择符,我们怎么处理?

解决方案:

1. 可以用split(',')来处理,但是这样只是单纯的分割出来了,并不能获得更多的信息。
2. 所以我们采用正则来分块,缺点是可读性以及效率的问题,优点是可以提取一些必要的信息,进行预处理。

所以jquery采取的是正则,但是并没有完全分块,而是一部分一部分的取。对于上面的例子我们看看怎么分块。

div#outer > ul , div p:nth(1),form input[type=”text”]

先分离出来 div#outer > ul,处理完毕后再分离出来 div p:nth(1),处理完毕后再分离出来 form input[type=”text”],最后合并三部分的结果

关于选择顺序的讨论

这里需要记得jQuery选择器的作用,一个典型的例子:

body div p:first-child
body div p:first

这两个的含义完全不一样,如果可以用括号这么写的话,可以改成:

body div p:first-child --> body div (p:first-child)
body div p:first --> (body div p):first

first-child 是用来限定 p 的,可以算是 p 的一个属性,

body div p:first 是用来限定 body div p 结果集的,可以算是 body div p 结果集的一个方法。

body div p:first == (body div p).eq(0)

类似的情况还有jQuery自定义的几个位置查询表达式,所以

情况一、body div p:first

  自左向右的查询。先找到body,获得集合A,然后在集合A中查找div获得集合B,在集合B中查找p获得集合C,最后取集合C的第一个元素,得最终结果XX

情况二、body div p:first-child  

  自右向左的查询,先找到p:first-child获得集合A1,然后判断祖先元素里面是否有div获得集合B1,然后判断祖先元素里面是否有body获得集合C1,得最终结果C1

  对比上面的两个过程,相较于过程一,过程二更像是一个过滤的过程,因此,Sizzle最大的亮点是自右向左过滤。

  另外,为了提高查询效率,最重要的就是缩小查找范围和减少遍历次数。

一个典型的例子是:

div p

情况一、先找到p,获得集合A,然后判断祖先元素里面是否有div获得集合B,得最终结果B

div#a p

#a 一般只会有一个,所以情况一里面p的范围太大了,所以如果第一个选择表达式里面含有id,Sizzle总是先查找第一个,从而缩小查找范围

情况二、先找到div#a,获得单个元素A1,然后再A1的环境中查找p,得最终结果B1

因此,最终的过程就变成

  1. 分割表达式
  2. 查找元素
  3. 过滤元素(过滤分两种,1、通过元素自身属性过滤 2、通过元素之间关系过滤。)

因此可以获得Sizzle的大致代码结构

Sizzle引擎基本结构

主要流程:window.Sizzle = function(){};
查找元素:Sizzle.find = function(){};
过滤元素:Sizzle.filter = function(){};

定义用到的一些查找方式和过滤条件:Sizzle.selectors = {};

Sizzle.selectors 经常要用到,于是给它一个简短的名字,就叫做 Expr

于是Sizzle的代码框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
window.Sizzle = function(){
//主要负责分块和主线流程,通过元素之间关系过滤也被放在这个部分
};
Sizzle.find = function(){
//查找元素
};
Sizzle.filter = function(){
//过滤元素,主要处理通过元素自身属性过滤
};
Sizzle.selectors = {
//命名空间,以及定义一些变量和特异方法
};
var Expr = Sizzle.selectors;//别称

剩下的问题就是怎么获得我们需要的元素集合

关于查找元素的初步讨论

我们先看,怎么找元素,浏览器原生只有三种查找元素的方式(文章开头我们已经假设初步所有的浏览器原生都不支持getElementsByClassName,虽然大部分都支持):

1
2
3
getElementById
getElementsByName
getElementsByTagName

问题一

当我们遇到一个选择表达式的时候,怎么判断这个选择表达式是什么类型的。

解决方案:

依次检测这个表达式的类型,获得匹配的类型,注意,一旦获得了匹配的类型,就不会继续匹配了。至于先匹配哪个类型,查找原则就是尽可能的缩小检索范围(特异性越高,检索范围就越小)。一般情况下,ID数量小于NAME数量,NAME数量又小于TAG数量。因此判断顺序就是[‘ID’,’NAME’,’TAG’]

实例说明:

  1. input[name=”test”],先查找[name=”test”],此时虽然还可以继续查找input,但是没有那个必要了,因为如果查询input之后再去取交集,会破坏程序的结构。

Sizzle的处理方式就是:先查找[name=”test”]获得集合A,然后在A中过滤input,input被作为一个过滤条件丢到过滤部分去处理。

  1. input#demo[name=”test”] 由于ID的优先级比NAME高,所以先查找#demo获得集合A,然后在A中过滤input[name=”test”],input[name=”test”]被作为一个过滤条件丢到过滤部分去处理。

关于过滤元素的初步讨论

过滤是一个条件一个条件来进行的,对于一个集合A以及过滤表达式

1
expr='[class="test"][rel=1]:first-child'

在A中过滤[class=”test”]得到集合B,此时过滤表达式expr=’[rel=1]:first-child’

在B中过滤[rel=1]得到集合C,此时过滤表达式expr=’:first-child’

在C中过滤:first-child得到集合D,此时过滤表达式expr=’’,过滤完毕

【说明,上面的顺序不代表真实的顺序,是指为了说明“过滤是一个条件一个条件来进行的”这句活而已】

过滤分两步:

  1. 预过滤Expr.preFilter,处理兼容问题以及格式转换,决定下一步的走向
  2. 最终过滤Expr.filter,这一步的形式都是一样的,最终返回的都是一个布尔值。

过滤方式:

假设第一轮的筛选集合:A = ['#1','#2','#3','#4','#5','#6']
过滤条件为isMatch
满足条件的集合为C = ['#1','#2','#3']

第一、

1
2
3
4
5
6
7
8
9
var B = [];
for( i = 0; (item = A[i]) != null; i++ ){
A[i] = isMatch(item);
}
for( i = 0; i < A.length; i++ ){
if(A[i]){
B.push(A[i]);
}
}
  1. [‘#1’,’#2’,’#3’,’#4’,’#5’,’#6’]
  2. [‘#1’,’#2’,’#3’,false,false,false]
  3. [‘#1’,’#2’,’#3’]

第二、

1
2
3
4
5
6
var B = [];
for( i = 0; (item = A[i]) != null; i++ ){
if(isMatch(item)){
B.push(A[i]);
}
}

【Javascript】Prototype源码浅析—元素选择器部分(一)之$

简述

$ 方法是 Prototype 的基础,和jquery中的$作用差不多,不过功能却弱了很多。
因为Prototype中还有一个$$方法,看名字就知道,和$相比,$$加了一倍的钱,功能肯定就丰富撒。本文主要是剖析$方法,1.7版本的$$方法使用的是Sizzle引擎,比较复杂,是后面的事情。

基本原理

$ 方法其实比较简单,平时在个人的代码中见得也比较多。
基本原理就是如果传入的是一个字符串,就执行document.getElementById方法,如果是一个DOM元素,就直接返回传入的元素,代码实现:

1
2
3
4
5
6
function _$(element){
if(typeof element == 'string'){
element = document.getElementById(element);
}
return element;
}

改进版本,如果传入的是多个字符串id:

1
2
3
4
5
6
7
8
9
10
11
12
//如果传入的是多个id,保留上面一个函数
function $(element){
if(arguments.length > 1){
for(var i = 0,elements = [],len = arguments.length; i < len; i++){
if(typeof element == 'string'){
element = _$(element);
}
elements.push(element);
}
}
return elements;
}

由于_$中已经做了类型判断,所以_$1中的还可以换种形式,上面的函数还可以换种形式:

1
2
3
4
5
6
7
8
function $(element){
if(arguments.length > 1){
for(var i = 0,elements = [],len = arguments.length; i < len; i++){
elements.push(_$(element));
}
}
return elements;
}

可以组合到一个函数里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
function $(element){
if(arguments.length > 1){
for(var i = 0,elements = [],len = arguments.length; i < len; i++){
elements.push($(element));
}
return elements;
}
if(typeof element == 'string'){
element = document.getElementById(element);
}
return element;
//最后一步,这里返回的是扩展了的Element元素,于是return Element.extend(element);
}

需要说明的一点是,上面只是获取了目标元素,而且返回的结果里面包含的都是原生的DOM元素,相比Prototype的实现,少了一步元素的扩展,这个留到Element部分。

【Javascript】Prototype源码浅析—Date

Date

Date比较好理解,理解清楚了 Number 对象的 toPaddedString 方法就可以了。

只有两个方法(toJSON和toISOString),而且这两个方法还一样,就是将日期转换为 JSON 字符串(遵循 ISO 格式)。

代码很短,我直接贴上来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function(proto) {
function toISOString() {
return this.getUTCFullYear() + '-' +
(this.getUTCMonth() + 1).toPaddedString(2) + '-' +
this.getUTCDate().toPaddedString(2) + 'T' +
this.getUTCHours().toPaddedString(2) + ':' +
this.getUTCMinutes().toPaddedString(2) + ':' +
this.getUTCSeconds().toPaddedString(2) + 'Z';
}
function toJSON() {
return this.toISOString();
}
if (!proto.toISOString){
proto.toISOString = toISOString;
}
if (!proto.toJSON){
proto.toJSON = toJSON;
}

})(Date.prototype);

补充

ISO8601提供了一种标准的交叉国家方法:一种由全面到具体的表达方法形成了一个日期的表达式,这种方法表示的日期非常容易推导,首先是年,接着是月然后是天,每个部分用连字符“-”分割。加上零,数字均是小于10的,将年份1之前的年用“0”表示,而0年以后的年份就用“-1”表示。
因此,1998年3月30日就可以表示成:1998-03-30。

W3C参考:http://www.w3.org/QA/Tips/iso-date

小结

到此为止,Prototype关于Javascript的语言核心扩展就算完了,Function,Object,Array,Hash,String,Number,Date,RegExp外加一个辅助对象Enumerable九个部分,这些部分内容(除了setTimeout以及setInterval外)不涉及任何DOM或者BOM的内容。

后面的分析部分开始涉及到DOM处理和BOM部分,但是前面的语言核心是关键,也是Prototype最典型的特点,需注意。

【Javascript】关于Chrome的sort()优化

今天看Sizzle代码的时候,里面有这么一段:

1
2
3
4
5
6
7
8
// Here we check if the JavaScript engine is using some sort of
// optimization where it does not always call our comparision
// function. If that is the case, discard the hasDuplicate value.
// Thus far that includes Google Chrome.
[0, 0].sort(function() {
baseHasDuplicate = false;
return 0;
});

然后google一番,发现这篇文章:《Chrome V8 引擎对 sort 的优化》,简单的拷贝过来:

1
2
3
4
5
6
7
8
9
10
11
var a = 0, b = 0;
[0, 0].sort(function() {
a = 1;
return 0;
});
[0, 1].sort(function() {
b = 1;
return 0;
});

alert(a === b); // true or false ?

上面的代码,除了 Chrome 输出 false, 其它浏览器皆为 true.

原因是 Chrome 对数组的 sort 方法进行了优化:

1
2
3
4
5
6
7
8
9
function sort(comparefn) {
var custom_compare = (typeof(comparefn) === 'function');
function Compare(x,y) {
if (x === y) return 0;
if (custom_compare) {
return comparefn.call(null, x, y);
}
...
}

不过测试了一下,发现这个并不成立,在Chrome 16.0.912.77 m中,并没有执行什么优化,alert出来的同样是true,与FF等浏览器无异。

因此,这个应该不是存在所有的Chrome版本中,因此并不能作为定论。

测试结果没有覆盖所有Chrome版本,错误之处请指出。

【Javascript】Prototype源码浅析—Hash部分(一)

Hash

Hash是Prototype作者扩展出来的一个数据类型。
本质上他就是一个普通的javascript对象(注:不要纠结什么javascript变量都是对象,这里说new Object()那种),然后在这个对象上面扩展出来一些其他的方法。

基本原理

基本的原理的代码说明就是:

1
2
3
4
5
6
7
8
9
10
function Hash(object){
this._object = object;
}
Hash.prototype = {
constructor : Hash,
method_1 : function(){//this._object},
method_2 : function(){//this._object}
//...
}
var hash_1 = new Hash({name : 'xesam'});

不过在源码中肯定不是这样的咯,创建类的形式就不是这样,因此我们换成Paototype中创建类的形式(注:Prototype源码浅析——Class部分(一)之类):

1
2
3
4
5
6
7
8
var Hash = Class.create(Enumerable, (function() {
function initialize() {
//this._object
}
return {
initialize : initialize
}
})())

Hash里面也混入了Enumerable对象,借此给Hash增加更多的方法。

(在这里我们有个区别可以留意一下:尽管Hash和Array都混入了Enumerable,但是与Array不同的是,Array是js原生的一个数据类型,只能用extend的方式,Hash是作者自己创建的一个数据类型,因此,Hash采用的方式是Class.create的方式。)

既然混入了Enumerable对象,那么肯定有一个_each方法,对于Hash对象,_each方法的iterator参数的参数是一个pair,这个pair比较特别一点
既是一个数组[key,pair],也是一个对象:

1
2
3
4
{
key : key,
value : value
}

因此pair[0]和pair.key的结果是一致的。

基本操作:

看下创建类时的initialize方法,每一个hash对象内部都维护着一个内部的对象,变量名叫做_object,因此原则上不能通过直接操作实例的属性来读取或者设置实例的属性。所以作者提供了单独的方法,现在有三个基本的方法:get,set,unset。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function set(key, value) {
return this._object[key] = value;
}

function get(key) {
if (this._object[key] !== Object.prototype[key])
return this._object[key];
}

function unset(key) {
var value = this._object[key];
delete this._object[key];
return value;
}

(注:Prototype的类很基础,封装基本没有,因此虽然内部维护了一个_object变量,但是并不是真正意义上的私有变量,访问hash的某个属性可以用上面的get,set,unset,也可以直接修改_object对象:

1
2
3
hash.set('name','new name')

hash._object.name = 'new name'

上面操作没区别,靠自觉而已。)

复制hash:

第一:
克隆当前hash对象(hash)——clone。克隆内部的对象(_object)——toObject.

toObject调用的是Object.clone方法,现在回顾一个下Object.clone方法

1
2
3
function clone(object) {
return extend({ }, object);
}

就是把this._object复制到一个空对象里面,然后返回。

1
2
3
function clone() {
return new Hash(this);
}

现在看一看创建Hash类的时候的initialize函数,先前忽略了this._object的初始化过程:

1
2
3
function initialize(object) {
this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
}

看看实现:如果object是一个普通对象,那么直接调用Object.clone方法。如果object是一个Hash实例,那么调用object.toObject,注意object.toObject的本质。间接调用的还是Object.clone方法,不过最终返回的类型是Hash类型,享有Hash的各种方法。

取得Hash对象的keys和values数组

方法名就是keys和values。这里使用的是父对象的一个方法——plunk(注:plunk是Enumerable中定义的一个方法Prototype源码浅析——Enumerable部分(三)):

1
2
3
4
5
6
7
function keys() {
return this.pluck('key');
}

function values() {
return this.pluck('value');
}

与plunk关系最密切的一个方法是each,回去看下each,each函数的iterator的参数是pair,是一个对象。

更新(合并)一个Hash实例:

这样的操作有两种:

一种是破坏性的update(会修改原始对象)
一种是非破坏性的merge(不会修改原始对象的)

update使用的是inject方法(注:Prototype源码浅析——Enumerable部分(三)),第一个参数是this(当前Hash实例),因此当前对象会被修改。

merge使用的也是update方法,不过先执行了一次clone操作,因此原始的Hash实例被保留下来了。

1
2
3
4
5
6
7
8
9
10
function merge(object) {
return this.clone().update(object);//先拷贝了内部变量_object
}

function update(object) {
return new Hash(object).inject(this, function(result, pair) {
result.set(pair.key, pair.value);
return result;
});
}

格式转换:

toQueryPair 和 toQueryString

toQueryPair是一个内部使用的方法,主要作用是检测value的值。如果value没为undefined,那么只保留key值,如果value值为null,那么保留“key=”形式否则转换为key=encodeURIComponent(value)的形式。

1
2
3
4
function toQueryPair(key, value) {
if (Object.isUndefined(value)) return key;
return key + '=' + encodeURIComponent(String.interpret(value));
}

toQueryString主要涉及到一个处理values是数组的情况,values为数组就使用concat展开,values为字符串就直接push,然后再展开。

这里和Object.toQueryString方法是一样的,应该是可以相互代替的。
这里回头去看Object里面的Str方法,那个清楚了,这个就不在话下了(注:Prototype源码浅析——Object部分(三)之有关JSON)。

inspect就没什么好说的了。很常见了。

【Javascript】Prototype源码浅析—Enumerable部分(三)

现在来看Enumerable剩下的方法

toArray | size | inspect
inject | invoke | sortBy | eachSlice | inGroupsOf | plunk | zip

前面说过map的原理,不管原来的集合是什么,调用map之后返回的结果就是一个数组,其中数组的每一项都是经过interator处理了的,如果不提供interator那么默认使用Prototype.K,此时的作用很明显,返回的结果就是原来集合的数组形式。原来的集合中length属性为多少,返回结果数组的length就是多少。

这个特殊情况被作为一个方法独立出来,叫做toArray:

1
2
3
function toArray() {
return this.map();
}

另一个size方法是返回上面那个数组的长度:

1
2
3
function size() {
return this.length;
}

至于inspect,前面在Object和Strng部分见过好几次了,这里调用Array的inspect方法[Array部分后面分析,不过inspect这个方法太熟悉了,也没必要说了]。

1
2
3
function inspect() {
return '[' + this.map(Object.inspect).join(', ') + ']';
}

另外,对于集合来说,另一个操作是分组。Enumerable中对应的方法是eachSlice 和 inGroupsOf。

先抛开源码,单独来实现这个方法,需要的一个参数是每一个分组的长度(叫做number),如果最后一组长度不够,就保留最后一组的实际长度。

具体实现步骤:

  1. 检测number的值,如果number小于1,那么肯定是非法字符,直接返回原来的集合。
  2. 将所操作集合转变为数组A,转变方法就是在原来的集合上面调toArray方法。
  3. 数组A分组有原生的方法slice,循环调用即可。

按照上面的步骤,可得到下面的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
Enumerable.eachSlice = function(number){
if(number < 1){ //第一步
return this;
}
var array = this.toArray();//第二步
var index = 0;
var results = [];
while(index < array.length){//第三步
results.push(array.slice(index,index + number));
index += number;
}
return results;
};

对应到具体的源码实现中,index += number;这一步被挪到while的条件中,因此,为了保证循环从0开始,index的初始值被设为-number;另外,和其他的方法一样,eachSlice也提供了iterator, context两个参数,作用依旧不变,所以最后的结果变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
  function eachSlice(number, iterator, context) {
var index = -number, slices = [], array = this.toArray();
if (number < 1) return array;
while ((index += number) < array.length)
slices.push(array.slice(index, index+number));
return slices.collect(iterator, context); //collect就是map
}
````

例子:

```javascript
console.log([1,2,3,4,5].eachSlice(2));//[[1,2],[3,4],[5,6]]

至于inGroupsOf方法,则是对eachSlice的一个补充而已。eachSlice最后一组长度不够,就保留最后一组的实际长度,在inGroupsOf最后一组长度不够,会用指定的填充符填充(默认填充为null)。因此只要提供iterator函数就可以了:

1
2
3
4
5
6
7
8
9
Enumerable.inGroupsOf = function(number, fillWith) {
fillWith = typeof fillWith == 'undefined' ? null : fillWith;//源码中这里使用的是Object.isUndefined
return this.eachSlice(number, function(slice) {
while(slice.length < number){
slice.push(fillWith);
}
return slice;
});
}

下面看inject,先看手册说明:

1
inject(accumulator, iterator[, context]) -> accumulatedValue

根据参数 iterator 中定义的规则来累计值。首次迭代时,参数 accumulator 为初始值,迭代过程中,iterator 将处理过的值存放在 accumulator 中,并作为下次迭代的起始值,迭代完成后,返回处理过的 accumulator。

这个操作其实前面也遇到过,可以单独用each来实现,看一个数组求和的例子:

1
2
3
console.dir([1,2,3,4].inject(0,function(sum,n){
return sum + n;
}));//10

如果换做each实现,就是:

1
2
3
4
5
var sum = 0;
[1,2,3,4].each(function(value){
sum += value;
})
console.dir(sum);//10

对比上面的实现和源码中的实现,上面的实现中有一个缺陷:sum全局变量,这是不合理的。

所以变形上面的形式,抛弃那个全局变量sum,由于each方法是固定死的,没有办法再改变,所以我们在外层再包装一个方法,并将叠加部分填进去:

1
2
3
4
5
6
7
8
9
10
function fn(accumulator,interator,context){
[1,2,3,4].each(function(value,index){
accumulator = interator.call(context,accumulator,value,index);
})
return accumulator;
}
console.dir(fn(0,function(accumulator,value,index){
accumulator += value;
return accumulator;
}));//10

看上面的实现,需要注意的一点是,fn中第一个传入的是一个引用类型的变量,由于这一个实现方式:

1
accumulator = interator.call(context,accumulator,value,index);

那么最后返回的结果是同一个变量,是对最初传入变量的一个引用,这一点是出于性能和效率的考虑,不过有时候可能导致问题,需警惕。

接下来是invoke方法,这个方法和each(map)的作用基本一致,唯一的区别是each(map)执行的是外部提供的一个方法,而invoke执行的是集合对象自身本来就存在的方法。因此,invoke的参数有且只需要有一个,就是集合对象的方法名。举个例子,我们将一个数字数组的每一项都转化为字符串:

对比两种实现:

1
2
3
4
5
6
var array_1 = [1,2,3,4].map(function(value){
return value.toString();
});
console.log('array_1:',array_1);//["1", "2", "3", "4"]
var array_2 = [1,2,3,4].invoke('toString');
console.log('array_2:',array_2);//["1", "2", "3", "4"]

由于少了一次闭包的消耗,因此invoke在效率上稍高,而且形式也简洁不少。

具体实现:

1
2
3
4
5
6
7
function invoke(method) {
//var args = $A(arguments).slice(1); //源码实现
var args = Array.prototype.slice.call(arguments,1);
return this.map(function(value) {
return value[method].apply(value, args);
});
}

plunk方法比较简单,获取所有元素的同一个属性的值,并返回相应的数组。这个方法显然是针对普通对象来的,数组的话没有什么属性好取的:

1
2
3
4
5
6
7
function pluck(property) {
var results = [];
this.each(function(value) {
results.push(value[property]);
});
return results;
}

源码好理解,给个例子就行:

1
['hello', 'world', 'my', 'is', 'xesam'].pluck('length')// [5, 5, 2, 3, 5]

另一个在其他脚本里面常见的方法是zip,这个方法不是很好理解:

1
zip(Sequence...[, iterator = Prototype.K]) -> Array

将多个(两个及以上)序列按照顺序配对合并(想像一下拉链拉上的情形)为一个包含一序列元组的数组。
元组由每个原始序列的具有相同索引的元素组合而成。如果指定了可选的 iterator 参数,则元组由 iterator 指定的函数生成。

我们先不考虑iterator ,来第一个实现:

1
2
3
4
5
6
7
8
9
10
function zip(){
var array_1 = [1,2,3];
var array_2 = ['a','b','c'];
var array_3 = ['x','y','z'];
var result = [array_1].concat([array_2,array_3]);//[[1,2,3],['a','b','c'],['x','y','z']]
return array_1.map(function(value,index){
return result.pluck(index);
})
}
console.log(zip());//[[1,'a','x'],[2,'b','y'],[3,'c','z']]

改为Enumerable方法的形式就是:

1
2
3
4
5
6
7
function zip(){
var result = [this].concat(Array.prototype.slice.call(arguments,1));
return this.map(function(value,index){
return result.pluck(index);
})
}
console.log([1,2,3].zip(['a','b','c'],['x','y','z']));////[[1,'a','x'],[2,'b','y'],[3,'c','z']]

加上interator处理之后就是:

1
2
3
4
5
6
7
8
9
10
11
function zip(){
var args = Array.prototype.slice.call(arguments,0);
var interator = function(x){ return x;} //Prototype.K
if(args[args.length - 1].constructor == Function){
interator = args.pop();
}
var result = [this].concat(Array.prototype.slice.call(arguments,1));
return this.map(function(value,index){
return interator.call(result.pluck(index));
})
}

【Javascript】Prototype源码浅析—Enumerable部分(二)

剩下的方法太多,于是分作两部分。

亮点就是$break和$continue,以及grep方法的思想。

前面each方法中掉了一个方面没有说,就是源码中的$break和$continue。
这两个变量是预定义的,其作用相当于普通循环里面的break和continue语句的作用。
出于效率的考虑,在某些操作中并不需要完全遍历一个集合(不局限于一个数组),所以break和continue还是很必要的。

对于一个循环来说,对比下面几种退出循环的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var array_1 = [1,2,3];
var array_2 = ['a','b','c'];
(function(){
for(var i = 0, len = array_1.length; i < len; i++){
for(var j = 0, len_j = array_1.length; i < len_j; j++){
if('c' === array_2[j]){
break;
}
console.log(array_2[j]);
}
}
})();//a,b,a,b,a,b
(function(){
for(var i = 0, len = array_1.length; i < len; i++){
try{
for(var j = 0, len_j = array_1.length; i < len_j; j++){
if('c' === array_2[j]){
throw new Error();
}
console.log(array_2[j]);
}
}catch(e){
console.log('退出一层循环');
}
}
})();//a,b,'退出一层循环',a,b,'退出一层循环',a,b,'退出一层循环'
(function(){
try{
for(var i = 0, len = array_1.length; i < len; i++){
for(var j = 0, len_j = array_1.length; i < len_j; j++){
if('c' === array_2[j]){
throw new Error();
}
console.log(array_2[j]);
}
}
}catch(e){
console.log('退出一层循环');
}
})();//a,b,'退出一层循环'

当我们把错误捕获放在相应的循环层面时,就可以中断相应的循环。
可以实现break和break label的作用(goto)。这样的一个应用需求就是可以把中断挪到外部去,恰好符合Enumerable处的需求。

回到Enumerable上来,由于each(each = function(iterator, context){})方法的本质就是一个循环,对于其第一个参数iterator,并不包含循环,
因此直接调用break语句会报语法错误,于是Prototype源码中采用上面的第二种方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
Enumerable.each = function(iterator, context) {
var index = 0;
try{
this._each(function(value){
iterator.call(context, value, index++);
});
}catch(e){
if(e != $break){
throw e;
}
}
return this;
};

一旦iterator执行中抛出一个$break,那么循环就中断。如果不是$break,那么就抛出相应错误,程序也稳定点。
这里的$break的定义并没有特殊要求,可以按照自己的喜好随便更改,不过意义不大。

Enumerable中的某些方法在一些现代浏览器里面已经实现了(参见chrome原生方法之数组),下面是一张对比图:

1

在实现这些方法时,可以借用原生方法,从而提高效率。不过源码中并没有借用原生的部分,大概是因为Enumerable除了混入Array部分外,还需要混入其他的对象中。

看上面的图示明显可以看得出,each和map 的重要性,map其实本质还是each,只不过each是依次处理集合的每一项,map是在each的基础上,还把处理后的结果返回来。在Enumerable内部,map是collect方法的一个别名,另一个别名是select,其内部全部使用的是collect这个名字。

检测:all | any | include

这三个方法不涉及对原集合的处理,返回值均是boolean类型。

all : 若 Enumerable 中的元素全部等价于 true,则返回 true,否则返回 false

1
2
3
4
5
6
7
function all(iterator, context) {
var result = true;
this.each(function(value, index) {
result = result && !!iterator.call(context, value, index);
});
return result;
}

  对于all方法来说,里面的两个参数都不是必须的,所以,内部提供了一个函数,以代替没有实参时的iterator,直接返回原值,名字叫做Prototype.K。Prototype.K的定义在库的开头,是一个返回参数值的函数Prototype.K = function(x){return x;}。另外,在all方法中,只要有一个项的处理结果为false,整个过程就可以放弃(break)了,于是用到了本文开头的中断循环的方法。最后的形式就是:

1
2
3
4
5
6
7
8
9
10
Prototype.K = function(){};
Enumerable.all = function(iterator, context) {
iterator = iterator || Prototype.K;
var result = true;
this.each(function(value, index) {
result = result && !!iterator.call(context, value, index);
if (!result) throw $break;
});
return result;
}

最后返回的result是一个boolean型,偏离一下all,我们改一下result:

1
2
3
4
5
6
7
8
function collect(iterator, context) {
iterator = iterator || Prototype.K;
var results = [];
this.each(function(value, index) {
results.push(iterator.call(context, value, index));
});
return results;
}

此时results是一个数组,我们不中断处理过程,保存所有的结果并返回,恩,这就是collect方法,或者叫做map方法。

any:若 Enumerable 中的元素有一个或多个等价于 true,则返回 true,否则返回 false,其原理和all差不多,all是发现false就收工,any是发现true就收工。

1
2
3
4
5
6
7
8
9
function any(iterator, context) {
iterator = iterator || Prototype.K;
var result = false;
this.each(function(value, index) {
if (result = !!iterator.call(context, value, index))
throw $break;
});
return result;
}

include:判断 Enumerable 中是否存在指定的对象,基于 == 操作符进行比较

这个方法有一步优化,就是调用了indexOf方法,对于数组来说,indexOf返回-1就不可以知道相应元素不存在了,如果集合没有indexOf方法,就只能查找比对了。这里的查找和没有任何算法,一个个遍历而已,如果要改写也容易,不过平时应用不多,因此估计也没有花这个精力去优化这个。所以如果结果为true的时候效率比结果为false的时候要高一些,看运气了。

1
2
3
4
5
6
7
8
9
10
11
12
13
function include(object) {
if (Object.isFunction(this.indexOf))//这个判定函数应该很熟悉了
if (this.indexOf(object) != -1) return true;//有indexOf就直接调用

var found = false;
this.each(function(value) {//这里的效率问题
if (value == object) {
found = true;
throw $break;
}
});
return found;
}

下面是一组过滤数据的方法:

返回单个元素:max | min | detect

返回一个数组:grep | findAll | reject | partition

其中max和min并不局限于数字的比较,字符的比较一样可以。

max(iterator, context)依旧可以带有两个参数,可以先用iterator处理之后再来比较值,这样的好处就是不必局限于特定的数据类型,比如,对象数组按照一定规则取最大值:

1
2
3
console.dir([{value : 3},{value : 1},{value : 2}].max(function(item){
return item.value;
}));//3

因此源码的实现方式可以想象,直接比较的时候,实现方式可以如下:

1
2
3
4
5
6
7
8
function max() {
var result;
this.each(function(value) {
if (result == null || value >= result) //result==null是第一次比较
result = value;
});
return result;
}

扩展之后,value要进一步变为value = (iterator处理后的返回值):

1
2
3
4
5
6
7
8
9
10
function max(iterator, context) {
iterator = iterator || Prototype.K;
var result;
this.each(function(value, index) {
value = iterator.call(context, value, index);
if (result == null || value >= result)
result = value;
});
return result;
}

min的原理也一样。

detect和any的原理和接近,any是找到一个true就返回true,detect是找到一个true就返回满足true条件的那个值。源码就不贴了。

grep 这个很眼熟啊,一个unix/linux工具,其作用也很眼熟——就是返回所有和指定的正则表达式匹配的元素。

只不过unix/linux只能处理字符串,这里扩展了范围,但是基本形式还是没有变。如果集合的每一项都是字符串,那么实现起来回事这样:

1
2
3
4
5
6
7
8
9
10
11
12
Enumerable.grep = function(filter) {
if(typeof filter == 'string'){
filter = new RegExp(filter);
}
var results = [];
this.each(function(value,index){
if(value.match(filter)){
results.push(value);
}
})
return results;
};

但是有一现在要处理的集合可能并都是字符串,为了达到更广泛的应用,首先要考虑的就是调用形式。看上面的实现,注意这么一句:

1
if(value.match(filter))

其中value是个字符串,match是String的方法,现在要扩展所支持的类型,要么给每一个value都加上match方法,要么转换形式。显然第一种巨响太大,作者转换了思路:

1
if (filter.match(value))

这么一来,不论value为何值,只要filter有对应的match方法即可,上面对于RegExp对象,是没有match方法的,于是在源码中,作者扩展了RegExp对象:

1
RegExp.prototype.match = RegExp.prototype.test;

注意上面的match和String的match有本质区别。

这么一来,如果value是对象,我们的filter只需要提供相应的检测对象的match方法即可。于是就有:

1
2
3
4
5
6
7
8
9
10
11
12
13
function grep(filter, iterator, context) {
iterator = iterator || Prototype.K;
var results = [];

if (Object.isString(filter))
filter = new RegExp(RegExp.escape(filter));

this.each(function(value, index) {
if (filter.match(value))//原生filter是没有match方法的。
results.push(iterator.call(context, value, index));
});
return results;
}

对于匹配的结果,可以处理之后再返回,这就是iterator参数的作用。
不同于max方法,grep是进行主要操作时候再用iterator来处理结果,max是用iterator处理源数据之后再来进行主要操作。因为grep中的filter代替了max中iterator的作用。

至于findAll,是grep的加强版,看过grep,findAll就很简单了。reject就是findAll的双子版本,作用正好相反。partition就是findAll + reject,组合亲子版本。

【Javascript】Prototype源码浅析—Enumerable部分(一)

在javascript中,根本找不到 Enumerable 的影子,因为这一块是Prototype作者从Ruby中借鉴过来的。
并且Enumerable在实际中根本没有直接应用的机会,都是混入到其他的对象中,可以说是其他对象的一个“父类”(不过只是调用了Object的extend方法,进行了方法的直接拷贝而已)。

我并不熟悉Ruby,不过看Enumerable中的一些方法,倒是跟Python中的有几分相似。

Enumerable其中一个最重要的方法是each,each这个方法应该都比较熟悉,其作用便是遍历一个序列的所有元素,并进行相应的处理。不过多数是应用在数组上,比如原生数组的forEach方法,以及jQuery中的链式调用,都依赖于each方法。因为jQuery选择器返回的是一个DOM对象数组,然后再在返回的数组上来调用each,从而分别处理每一个元素。

一般each都有两个参数:一个是迭代处理的函数和方法对应的上下文。

1
2
3
4
5
var each = Array.prototype.forEach || function(iterator,context){
for(var i = 0,len = this.length ; i < len ; i++){
iterator.call(context,this[i],this);
}
};

按照上面的方法,我们给Array对象扩展一个打印当前所有元素的print方法。

1
2
3
4
5
6
7
8
9
10
11
Array.prototype.each = Array.prototype.forEach || function(iterator,context){
for(var i = 0,len = this.length ; i < len ; i++){
iterator.call(context,this[i],i,this);
}
};
Array.prototype.print = function(){
this.each(function(item){
console.log(item);
});
}
console.log([1,2,3,4].print());//1,2,3,4

在Enumerable中,each并没有对应到具体的方法,前面说过Enumerable并不之际应用,而是作为一个“父类”应用到其他的对象,因此它的each方法是调用“子类”_each方法,因此任何混入Enumerable模块的对象,都必须提供一个_each方法,作为作用于实际循环的迭代代码。

现在Array.prototype上实现一个_each方法和一个each方法,实现一:

1
2
3
4
5
6
7
8
9
Array.prototype.each = function(iterator,context){
this._each(iterator,context)
}
Array.prototype._each = function(iterator,context){
for(var i = 0,len = this.length ; i < len ; i++){
iterator.call(context,this[i],i,this);
}
};

按照先前说的,_each只需要提供一个iterator参数就可以了,不过由于_each也被扩展到Array.prototype上面,于是实现的时候也附带了context参数。因此在Enumerable中,并没有使用_each的第二个context参数,是否实现对each没有影响。因此上面的实现一 不应该依赖_each的context,于是修改each如下:

1
2
3
4
5
6
Array.prototype.each = function(iterator,context){
var index = 0;
this._each(function(value){
iterator.call(context,value,index++);
})
}

  
这样一来,each方法的独立性提高了,在后续的Hash中也可以使用这个Enumerable了。任何看遍历的对象,只要提供了_each方法,就可以从Enumerable这里获得相应的方法。

因此,将上面的print例子用Enumerable的形式来实现,便得到如下的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var Enumerable = {};
Enumerable.each = function(iterator, context) {
var index = 0;
this._each(function(value){
iterator.call(context, value, index++);
});
return this;
};
Enumerable.print = function(){
this.each(function(item){
console.log(item);
})
};
Array.prototype._each = function(iterator,context){
for(var i = 0,len = this.length ; i < len ; i++){
iterator.call(context,this[i],i,this);
}
};
//下面的实现源码中是用的extend方法
for(var key in Enumerable){
Array.prototype[key] = Enumerable[key];
};
[1,2,3,4].print();//1,2,3,4

理解each的实现是理解Enumerable对象的关键,后面的Array和Hash都混入Enumerable对象,颇为重要。

【Javascript】Prototype源码浅析—Number部分

Prototype在原生对象的基础上扩展,分别是 Object,Function,String,Number,Array,Date,前面分析了 Object,Function,String,还剩下Number,Array,Date。

Number部分方法比较少,一共有8个:

toColorPart: 将 Number 对象转换为具有两位数字的十六进制形式
succ:  返回当前 Number 对象的下一个值,即当前值加一         
times: 采用 Ruby 的风格来封装一个标准的 [0...n] 循环
toPaddedString:将当前 Number 对象转换为字符串,如果转换后的字符串长度小于 length 指定的值,则用 0 在左边补足其余的位数 
abs:   返回当前 Number 对象的绝对值。        
round: 返回当前 Number 对象四舍五入后的整数值。
ceil:  返回大于等于当前 Number 对象的最小整数值。
floor: 返回小于等于当前 Number 对象的最大整数值。

其中一个重要的方法是toPaddedString。Number对象重写了toString方法:

1
NumberObject.toString(radix)

radix 可选。规定表示数字的基数,使 2 ~ 36 之间的整数。若省略该参数,则使用基数 10。但是要注意,如果该参数是 10 以外的其他值,则 ECMAScript 标准允许实现返回任意值。

1
2
3
4
function toPaddedString(length,radix){
var string = this.toString(radix || 10);//先将数字转换成相应的进制
return '0'.times(length - string.length) + string;//times方法在String中扩展的,将一个字符重复n遍
}

有了这个方法,就有一个比较有用的延伸就是toColorPart,可用于CSS中的颜色转换:

1
2
3
function toColorPart() {
return this.toPaddedString(2, 16);
}

既然是CSS颜色转换,因此数字就要求在[0-255]范围内。

1
console.log((10).toColorPart());//0a

有一个和String中同名的方法succ,作用也差不多,String中是按照字符表来递加的,Number中是按照自然数的顺序来的。

1
2
3
4
function succ() {
return this + 1;
}
console.log((10).succ());//11

从这个方法出发,来一个简单的0-n的数组

1
2
3
4
5
6
7
8
function range(){
var ret = [0];
for(var i = 0; i < this - 1; i++){
ret.push(i.succ());
}
return ret;
}
console.log((10).range());//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

暂时用这个range函数来得到times函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    function times(iterator, context){
this.range().forEach(iterator, context);//源码中使用的是R()方法
return this;
}
var s = '';
(5).times(function(item){
s += item;
});
console.log(s);//01234
```

除去上面几个方法,其他的方法就是将Math的静态方法扩展到Number对象上【说法不准确,意会··=。=】

```javascript
function abs() {
return Math.abs(this);
}
function round() {
return Math.round(this);
}
function ceil() {
return Math.ceil(this);
}
function floor() {
return Math.floor(this);
}

【Javascript】改造alert的引发的争论

今天在群里讨论alert的问题,说到了alert的改造,虽然说改造原生方法不是好做法,但是既然提到了就可以讨论下,所以我按照他们的讨论给出了下面的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
var _alert = window.alert;
window.alert = function(param,isDebug){
if(isDebug){
console.log(param);
}else{
_alert(param);
}
}
alert('x');
alert('x',true);
alert('x');
alert('x',true);

于是开始反驳我,大致的论点集中在下面的一段上面:

1
2
_alert = window.alert;
window.alert = function(){}

反方论点:这么写之后,_alert与window.alert应该还是一致的,因为大家都知道函数名称保存的只是引用而已。

当时被弄糊涂了,难道价值观被颠覆了?后来仔细想了一会儿,发现貌似被忽悠了。函数变量名保存的是引用没错,可是这个例子和引用的关系不是这么讲的吧···

要说明函数的引用关系,应该是这个例子:

1
2
3
4
var a = [1,2,3];
var b = a;
a.push(4);
console.log(a,b);//[1,2,3,4],[1,2,3,4]

后来回想一下,问题的最终原因还是值类型和引用类型被忽悠了,于是分析一下。先看下面的例子:

1
2
3
var a = 1;
var b = a;
console.log(a,b);

在这个例子中a,b完全只是一个名字而已,没有其他的作用。不论要不要,那个1都是真实存在的(假如可以无端产生一个变量1,那么1在被回收之前,无论有没有名字,都是存在的),此时的情况是:

1

继续上面的例子:

1
2
3
4
var a = 1;
var b = a;
a = 2;
console.log(a,b);

1

上面是Number类型的情况,下面换做一个引用类型的,比如Array。

1
2
3
var a = [1,2,3];
var b = a;
console.log(a,b);

1
继续:

1
2
3
4
var a = [1,2,3];
var b = a;
a.push(4);
console.log(a,b);

1

这就是引用传递,在整个过程中,变量名本身还是a,b没有变化,但是他们引用的东西发生了变化。

此时a与b之间没有半毛钱的关系,他们是通过[1,2,3,4]存在一丝形式上的相同性,而不是b与[1,2,3,4]通过a有联系,这一点才是引用传递要注意的。此时a===b为true。

下面换一个操作:

1
2
3
4
var a = [1,2,3];
var b = a;
a = [1,2,3];
console.log(a,b);

此时,虽然a,b各自保存的还是引用类型数据,但是a与b本来就半毛钱的关系都没有,所以此时a换个内容,与b的关系就完全没有了,连通过最初[1,2,3]获得的相同性都没有了,此时a===b为false。对应下面的图:

1

继续操作a:

1
2
3
4
5
var a = [1,2,3];
var b = a;
a = [1,2,3];
a.push(4);
console.log(a,b);

1

所以,“引用”引用的内存中的值,而非那个变量名。

回到开头的例子中:

1
2
3
4
5
6
7
8
var _alert = window.alert;
window.alert = function(param,isDebug){
if(isDebug){
console.log(param);
}else{
_alert(param);
}
}

在执行var _alert = window.alert;的时候,三者的关系是:

1

执行:

1
2
3
window.alert = function(param,isDebug){
//
}

1

所以最初讨论的时候,一开始就错了,因为_alert引用的不是window.alert,而是window.alert所引用的那个函数,不论是_alert还是window.alert,都只是一个名字而已,两者没有半毛钱的关系,最终的枢纽还是真实存在的内存中的那个变量。

然后另外一个问题就是关于才浏览器控制台测试代码的问题。

我们讨论的时候,先运行了一段测试代码:

1
2
3
var _alert = window.alert;
window.alert = function(){};
console.log(_alert === window.alert);

然后我们又在相同的控制台里面运行了我在开头给出的那一段代码,所以在同一个窗口里面,控制台的环境并没有刷新,测试代码的效果还在,所以alert已经混乱了。

要完全测试的话,最好还是每次都新开一个窗口测试。

错误之处请大牛指教~