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

查找的入口对应的是Sizzle.find方法:

1
Sizzle.find = function( expr, context) {}
expr      :查找的表达式
context :查找的范围

find的步骤

  1. 判断主要集合,方法说过了,依次匹配,顺序就是ID –> NAME –> TAG
  2. (1)当有类型被匹配时,调用相应的方法,获取集合set。(2)当ID,NAME,TAG全部不匹配时,获取context范围内的全部元素集合set
  3. 去除expr中已经匹配的部分,返回结果{expr : expr,set : set}

因此,Sizzle.find的大致代码流程是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Sizzle.find = function( expr, context, isXML ) {
for ( i = 0, len = Expr.order.length; i < len; i++ ) {
var type = Expr.order[i];
if((match = Expr.leftMatch[ type ].exec( expr ))){ // 对应第一步
if((set = Expr.find[ type ]( match, context ))!=null){ //对应第二步(1)
expr = expr.replace( Expr.match[ 'ID' ], "" ); //对应第三步
break;
}
}
}
if(!set){//对应第二步(2)
set = context.getElementsByTagName( "*" );
}
return {expr : expr,set : set}
}

实例说明:

1
2
3
<input type="radio" id="a" name="gender" value="man" class="default" /><label for="a" ></label>
<input type="radio" id="b" name="gender" value="man" class="default" /><label for="b"></label>
<input type="checkbox" id="c" name="gender" value="man" /><label for="c">人妖</label>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var set;
var expr = 'input[class*="default"]';

var match = Expr.leftMatch[ 'ID' ].exec( expr )
var left = match[1];
match.splice( 1, 1 );

set = Expr.find[ 'ID' ]( match, document);
if ( set != null ) {
expr = expr.replace( Expr.match[ 'ID' ], "" );
}
if ( !set ) {
set = typeof document.getElementsByTagName !== "undefined" ?document.getElementsByTagName( "*" ) :[];
}
Expr.find['ID'] = function( match, context ) {
if ( typeof context.getElementById !== "undefined") {
var m = context.getElementById(match[1]);
return m && m.parentNode ? [m] : [];
}
}
console.log({ set: set, expr: expr });

因此从上面的实例来看,Sizzle.find并不是执行查找功能的部分,而是主要起了一个分发器的作用:

将不同的选择表达式分发到不同的更专一的查找器上面。上面的例子中就是将具体查找分发给 Expr.find[ ‘ID’ ]

具体代码分析:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Sizzle.find = function( expr, context, isXML ) {
var set, i, len, match, type, left;

if ( !expr ) { //如果没有选择表达式,直接返回空集合
return [];
}

for ( i = 0, len = Expr.order.length; i < len; i++ ) {//用来判断应该选用哪个查找器,对应的顺序是[ "ID", "NAME", "TAG" ];
type = Expr.order[i];

if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { //碰到符合条件的匹配
left = match[1];
match.splice( 1, 1 ); //因为leftMatch在match的头部添加了一个新的分组,所以现在提取第一个分组到left里面,然后删除这个分组

if ( left.substr( left.length - 1 ) !== "\\" ) { //参见讨论部分
match[1] = (match[1] || "").replace( rBackslash, "" ); //检测,替换回车而已
set = Expr.find[ type ]( match, context, isXML ); //转到相应的查找器执行查找程序

if ( set != null ) { //找到相应的结果,修剪expr
expr = expr.replace( Expr.match[ type ], "" );
break;
}
}
}
}

if ( !set ) { //[ "ID", "NAME", "TAG" ]中没有匹配的类型时候,直接返回context范围内的所有标签。
set = typeof context.getElementsByTagName !== "undefined" ? context.getElementsByTagName( "*" ) :[];
}

return { set: set, expr: expr };
};

Expr.order = [ "ID", "NAME", "TAG" ];
Expr.find = {
ID: function( match, context, isXML ) {
if ( typeof context.getElementById !== "undefined" && !isXML ) {
var m = context.getElementById(match[1]);
return m && m.parentNode ? [m] : []; //这里注意优先级的问题。&&的优先级高于?:的优先级
}
},

NAME: function( match, context ) {
if ( typeof context.getElementsByName !== "undefined" ) {
var ret = [],
results = context.getElementsByName( match[1] );

for ( var i = 0, l = results.length; i < l; i++ ) {
if ( results[i].getAttribute("name") === match[1] ) {
ret.push( results[i] );
}
}
return ret.length === 0 ? null : ret;
}
},

TAG: function( match, context ) {
if ( typeof context.getElementsByTagName !== "undefined" ) {
return context.getElementsByTagName( match[1] );
}
}
};

讨论

关于检测’'的讨论【这个不知道对不对】

实例表达式expr = ‘.className’,CLASS匹配结果[‘.className’,’.className’], 此时原本的意图是’.’被转义,因此不应该作为class匹配,中止。

关于getElementById方法bug的讨论(源码1056行),

在某些浏览器里面,getElementById(‘test’)会返回name值为test的a节点,因此会对查询结果产生干扰。比如:

此时a节点在div前面,getElementById(‘test’)回返回而非预期中的

Sizzle也对此做了检测:

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
(function(){
// 先创建一个测试环境<div><a name="script20120215"></a></div>
var form = document.createElement("div"),
id = "script" + (new Date()).getTime(),
root = document.documentElement;

form.innerHTML = "<a name='" + id + "'/>";
root.insertBefore( form, root.firstChild );

//检测getElementById的返回值,现获取了相应的节点之后,添加一步检测id属性,如果吻合就保存,不吻合就丢弃
if ( document.getElementById( id ) ) {
Expr.find.ID = function( match, context, isXML ) {
if ( typeof context.getElementById !== "undefined" && !isXML ) {
var m = context.getElementById(match[1]);

return m ?
m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ?
[m] :
undefined :
[];
}
};
Expr.filter.ID = function( elem, match ) {
var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");

return elem.nodeType === 1 && node && node.nodeValue === match;
};
}

root.removeChild( form );

// 对于IE,需要释放刚才添加的DIV过程中各变量的缓存,便于垃圾回收
root = form = null;
})();

接下来是Sizzle.filter流程分析:《Sizzle引擎–原理与实践(四)》

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

主要流程与正则

表达式分块

1
var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g;

这个正则比较长,主要是用来分块和一步预处理。

1、
1

2、
1

3、
1

4、
1

'div#test + p > a.tab'    --> ['div#test','+','p','>','a.tab']

从表达式提取出相应的类型:

这个需要对应jQuery的选择器来看,共7种

ID选择器
CLASS选择器
TAG选择器
ATTR属性选择器
CHILD子元素选择器
PSEUDO伪类选择器
POS位置选择器

判断的方法还是正则,具体正则如下:

ID    : /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
CLASS : /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
NAME  : /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,
ATTR  : /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,
TAG   : /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,
CHILD : /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,
POS   : /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,
PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/

ID:

1

CLASS:

1

NAME:

1

TAG:

1

ATTR:

1

POS:

1

PSEUDO:

1

正则小提示:

? 非贪婪量词
\3 匹配分
?= 正向预查

这些正则可能一开始不好看,但是对应到具体的jQuery选择器就比较好理解了:

POS   :first :nth() :last :gt :lt :even :odd  

这些是Sizzle新加的,跟CSS无关

其他的倒是跟CSS基本无异,需要注意的是,由于PSEUDO的存在,同一个表达式可能同时匹配多个类型,这个后面的filter部分会提到。

上面的正则字符串保存在Expr的match属性中,

1
2
3
4
5
Expr = {
match:{
//ID:....
}
}

这部分正则并没有直接使用,进行了进一步的处理

第一、每个字符串后面都增加了一个判断,用来确保匹配结果,末尾不包含)或者}

/#((?:[\w\u00c0-\uFFFF\-]|\\.)+))/变成/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?![^\[]*\])(?![^\(]*\)/

第二、同时Sizzle会检测转义字符,因此各部分头部都增加了一个捕获组用来保存目标字符串前面的部分,
在这一步的时候,由于在头部增加了一分组,因此原正则字符串中的\3等符号必须顺次后移。

/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?![^\[]*\])(?![^\(]*\)/

变成

/(^(?:.|\r|\n)*?)#((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?![^\[]*\])(?![^\(]*\)/


/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/

变成

/(^(?:.|\r|\n)*?):((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\3\))?(?![^\[]*\])(?![^\(]*\)/

对应到源码中就是:

1
2
3
4
5
6
7
8
var fescape = function(all, num){
return "\\" + (num - 0 + 1);
};

for ( var type in Expr.match ) {
Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) );
Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) );
}

Expr.leftMatch 中保存的是处理过后的正则部分,这么做的另一个好处就是避免每次匹配都去创建一个新的RegExp对象

回到主流程

函数介绍:

1
var Sizzle = function( selector, context, results, seed ){}

Sizzle有四个参数:

  selector :选择表达式
  context :上下文
  results :结果集
  seed :候选集

实例说明:

1
Sizzle('div',#test,[#a,#b],[#c,#d,#e])

就是在集合 [#c,#d,#e] 中查找满足条件(在 #test 范围中并标签名为div)的元素,
然后将满足条件的结果存入[#a,#b]中,假设满足条件的有#d,#e,最后获得就是[#a,#b,#d,#e]。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    var Sizzle = function( selector, context, results, seed ){
var soFar = selector,
extra ,//extra用来保存并联选择的其他部分,一次只处理一个表达式
parts = [],
m;
do {
chunker.exec( "" ); //这一步主要是将chunker的lastIndex重置,当然直接设置chunker.lastIndex效果也一样
m = chunker.exec( soFar );
if ( m ) {
soFar = m[3];
parts.push( m[1] );
if ( m[2] ) { //如果存在并联选择器,就中断,保存其他的选择器部分。
extra = m[3];
break;
}
}
} while ( m );
}

对于’div#test + p > a.tab’

parts结果就是 [‘div#test’,’+’,’p’,’>’,’a.tab’]

分块之后,下一步就是决定的选择器的顺序,可以对照(一)的说明,构建两个分支:

1
2
3
4
5
if ( parts.length > 1 && origPOS.exec( selector ) ) {
//自左向右,判断标准就是存在关系选择符同时有位置选择符,因为如果只是类似div#test的选择表达式,就不存在顺序的问题。
}else{
//其他,自右向左
}

【说明:origPOS保存的是Expr.match.POS,源码901行】

先看普通的(自右向左)情况

然后就是ID的问题,第一个选择表达式含有id就重设context,

当存在context的时候【没有的话就不用找了,因为肯定没结果】,

在重设了contexr之后,既然是自右向左,第一步就是获取等待过滤的集合,

1
2
ret = seed ?{ expr: parts.pop(), set: makeArray(seed) } :Sizzle.find( parts.pop(), context);//Sizzle.find负责查找
set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set;//Sizzle.filter负责过滤

有候选集seed的时候直接获得候选集,没有的时候获取最右边一个选择符的结果集。

后面的过程就是依次取出parts中的选择符,在set中查找,过滤,直到全部查完

1
2
3
while ( parts.length ) {
Expr.relative[ cur ]( checkSet, context, contextXML );//context代表上下文,并非源码中的参数
}

实例说明:

['div#test','+','p','>','a.tab'] 

处理流程

第一步、没有候选集seed,第一项'div#test'中含有id信息,最后一项'a.tab'中不含有id信息,因此重设content=Sizzle.find('div#test',document)
第二步、剩余部分为['+','p','>','a.tab'],没有候选集seed,先获取等待过滤[标签名为a]的集合A,在集合A中过滤类名为tab的集合B
第三步、剩余部分为['+','p','>'],进行基于关系的过滤,这是一个逆向过程,假设第二步中的B=[#a,#b,#c,#d],先在查找直接父节点是p的元素,获得集合
    C = [#a,#b,false,false],然后获取紧挨第一步中content的元素,获得集合D = [#a,false,false,false]
第四步、取得D中不为false的部分,获得此次选择的集合E=[#a];并入结果集result中。
第五步、按照上面的规则,处理并联选择表达式的第二部分。

讨论

关于context的选择

在没有候选集,需要重设ID情况有那些呢?
  1、div#id_1 a#id_2
  2、div#id_1 a
  3、div a#id_2
Sizzle中,只有情况(2)下才去设置context

第二步中、关系选择符“+”和“~”表示的是同层级的关系,因此,context【查找范围】会被设置成context.parentNode

实例说明

1
2
3
4
5
6
7
8
9
10
11
12
<body>
<div id="test_a">
<p class="tab" id="a1">a1</p>
<p class="tab" id="a1">a2</p>
<p class="tab" id="a1">a3</p>
</div>
<div id="test_b">
<p class="tab" id="b1">b1</p>
<p class="tab" id="b1">b2</p>
<p class="tab" id="b1">b3</p>
</div>
</body>

选择表达式

'div#test_a ~ div'

第一步重设context为div#test_a

第二步中如果直接执行(div#test_a).getElemnetsByTagName(‘div’)显然是没有结果的,此时操作根本就是错误的。
因此,应该执行的是(div#test_a).parentNode.getElemnetsByTagName(‘div’).然后再进行第三步。

【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对象,颇为重要。