学习新知识有时候很难决定是做深度遍历还是广度遍历,比如那么多javascript框架,是先都走马观花看一遍呢还是逮着一个看下去,完了再看另一个呢?我觉得主要是看兴趣,至少目前我是这种状态。
几个月前老吴同学在构建自己的一个应用时用到underscore,当时他给我演示了一下,但是没啥概念,只知道是个类库。最近看backbone.js时很多人提到喜欢backbone.js其实是喜欢underscore.js,我就有些好奇,underscore.js到底是啥玩意捏?(女儿2岁多,说话大舌头,“呢”总是发成“捏”,挺好玩)
几年前就有人说javascript是最被低估一种编程语言,自从nodejs出来后,全端(All Stack/Full Stack)概念日渐兴起,现在恐怕没人再敢低估它了。javascrip是一种类C的语言,有C语言基础就能大体理解javascript的代码,但是作为一种脚本语言,javascript的灵活性是C所远远不及的,这也会造成学习上的一些困难,本文主要记录我在学习underscore.js过程中的发现一些知识点以及对部分源码的剖析。
现在可以用nodejs很方便地运行和调试javascript,nodejs调试javascript的命令是node debug filename。更多请参考http://www.cnblogs.com/moonz-wu/archive/2012/01/15/2322120.html
登录underscore的官方网站可以看到类库分为几大类:Collections,Arrays,Functions,Objects,Utility和Chaining,前几类比较明显的是按照函数适用的对象类型来分类的。大概过一遍提供的函数后就发现underscore跟C++的STL非常类似,C++因为是强类型,所以STL有对vector,list,set,map等集合对象的操作函数,以及一些通用的algorithm函数,这些在underscore中都能找到影子,而且underscore的功能似乎更强大一些。
数据类型
STL需要对vector、list等进行区分是因为不同的数据结构需要或者可以进行不同的实现,但underscore里面Collections和Arrays分开是什么道理呢?这也要从javascript的数据类型说起,看下图。
这里不对所有类型进行详细解释,有兴趣可以读一下
分类知识点
Collections
Collections是集合,指那些由单个元素组成,能够使用下标操作的数据类型的统称,比如Array,Object,String等,从underscore的源码来看,这一类函数只用到最基本的[]运算符和for循环,以及对由此构成的新方法的组合运用。这一类方法有:each、map、reduce、find等。
Arrays
相对Collections的鸟枪而言,Arrays有了小钢炮,能够使用Array内置的方法,比如slice。underscore的Arrays方法不仅适用于Array类型,也适用于String和Array-like类型的对象。这一类方法有:union、intersection、difference、indexOf等。
简单介绍一下Array-like,顾名思义,就是像Array而不是Array的一种数据类型,它的特点是能够通过数字下标(0、1、2 ...)访问,有length属性,但是不能使用Array的内置方法。这类里面比较常见的是arguments,就是函数的参数列表,Object.getOwnPropertyNames(arguments)的返回值除了参数列表外,还有length和callee两个属性。那么对这种类型如果要想用Array的内置方法怎么办呢?可以通过数组泛化来调用,有两种方式:
1. Array.method(obj);
2. Array.prototype.method.call(obj);
在underscore里面主要用了call的方式。既然提到了call,就再对call进行一下解释。
在javascript中call和apply常用于实现继承机制,二者很类似,只有第二个参数略有差异。调用 call() 方法时,调用者是需要执行的函数对象,第一个参数就是要执行函数中的 this变量,后面的参数都会作为参数传递给要执行函数。举例来说:
var me = {
name: "Alex",
City: "Beijing",
};
function sayHello(comments) {console.log("Hi, " + this.name + comments)};
sayHello.call(me, ", you are great!");
执行后会打印出“Hi, Alex, you are great!”。再比如,a.func().call(b), 就相当于b继承了a, 结果就是b调用了a的func()方法。
在Collections部分的源码中经常要对Array或Array-like类型与Object类型分开来处理,用到了一个技巧
if (obj.length === +obj.length) {}
对于前者而言,返回为true,而Object没有length属性,obj.length返回的是undefined,"+"是将其他类型转化为数字或者NaN,等同于Number(obj.length),+undefined的结果是NaN,因此整个表达式返回false。这里有一个知识点是null, false, undefined, NaN的关系,参考
http://www.mapbender.org/JavaScript_pitfalls:_null,_false,_undefined,_NaN
Functions
underscore中Functions部分提供了一些与Function相关的函数,在javascript里,函数本身也是一种变量类型,函数名只是指向函数对象的引用值,跟其他变量有一样的行为。Objects
Objects部分最复杂的一个函数是内部用的equal,可能也是整个underscore最复杂的一个,这里最重要的知识点就是javascript的判等,注意引用类型不能直接使用“==”或“===”,需要使用迭代函数转化成原始类型进行比较。underscore的isEqual函数与javascript的完全等同(===)或相等(==)不太一样,更符合人的直觉。根据源码简单总结一下规则,有顺序,前面的规则没有匹配才匹配后面的:
- 如果两个变量完全等同(===),且不为0,则返回true。如果是0的话,要保证符号一样(0和-0认为不相等),来源于egal:http://wiki.ecmascript.org/doku.php?id=harmony:egal
- 如果两个变量都是null或都是undefined,返回true
- 如果两个变量的类型不一样,返回false,类型使用Object.prototype.toString.call()来获得
- 对于String类型,原始类型和包装类型,只要值一样,认为相等“5” isEqual new String("5")
- 两个变量都是NaN返回true,判断是否是NaN依据Object.prototype.toString.call()为[object Number],且a!=+a;其他Number类型依据egal原则判断
- 对于Date和Boolean类型转化为原始的数字类型进行比较
- RegExp类型要比较source、global、multiline和ignoreCase几个属性
- 函数变量认为不相等(引用指向同一个函数的情况已经在第一条规则排除了)
- 对于数组和普通对象类型,基于上面的规则递归判断,所有子对象都相等才返回true
Objects部分的函数包括:keys、values、has、isEqual以及一堆isType用于判断类型的函数。
Utility
Utility部分感觉比较有用的是生成随机数和生成ID,还包括增加自定义函数的mixin,转义html的escape,以及一个简单的html模板函数。
OO方式
除了使用函数风格的underscore外,还可以使用面向对象的方式,在这种方式下,underscore支持链式调用。通过_.chain(obj) 将变量用underscore包装,然后可以调用所有函数的OO版本,且可以一直“点”下去,类似于_.chain(obj).func1().func2().func3().value(),注意最后要用value获取返回值。
部分源码注释
each
//_.each是underscore.js的基础方法,基于集合的方法很多有需要用到each
_.each = _.forEach = function(obj, iterator, context) { //别名是forEach,context是可选参数,如果要修改iterator的调用对象为context,即函数中this为context,就传递这个参数,否则context为undefined
if (obj == null) return obj;
if (obj.length === +obj.length) { //判断是否是数组或字符串,实际上function类型的变量也有length属性,但是function的[i]返回undefined,所以后面相当于对undefined进行变换
for (var i = 0, length = obj.length; i < length; i++) {
if (iterator.call(context, obj[i], i, obj) === breaker) return; //breaker是undercore定义的一个空对象,用于跳出循环,注意{}==={}返回false
}
} else { //其余类型,包括很多标准的内置对象
var keys = _.keys(obj); //只有Object才有key-value对,才进行处理,其他类型的对象不处理
for (var i = 0, length = keys.length; i < length; i++) {
if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
}
}
return obj; //返回obj自身,未经任何处理
};
map
_.map = _.collect = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
_.each(obj, function(value, index, list) { //这里函数的三个传入参数分别是obj的一个成员的值,key或者index,和obj自己,由each函数决定
results.push(iterator.call(context, value, index, list)); //保存处理结果
});
return results;
};
some
_.some = _.any = function(obj, predicate, context) { //如果obj是Object类型的话,会判断其每一个属性的值是否满足prediacte,这是由each方法决定的
predicate || (predicate = _.identity); //如果predicate为空,则将predicate赋值为空函数[Function]
var result = false;
if (obj == null) return result;
_.each(obj, function(value, index, list) {
if (result || (result = predicate.call(context, value, index, list))) return breaker;//这里发现一个问题,就是each循环无法break,必须将全部的对象都遍历一遍,应该是一个可改进的地方
});
return !!result; //两次取反,保证返回的是boolean类型,0/null/undefined进行两次取反都会返回false
};
every
_.every = _.all = function(obj, predicate, context) {
predicate || (predicate = _.identity);
var result = true;
if (obj == null) return result;
_.each(obj, function(value, index, list) {
if (!(result = result && predicate.call(context, value, index, list))) return breaker; //开始没看明白,“result=result”是什么意思?后来想到运算符优先级才看明白,“=”的优先级比“&&”要低,javascript为了减小文件大小,多余的运算符一概不要,能不能在开发版不要这么省啊
});
return !!result;
};
indexOf
_.indexOf = function(array, item, isSorted) {
if (array == null) return -1;
var i = 0, length = array.length;
if (isSorted) {
if (typeof isSorted == 'number') { //如果是数字,则从数字开始,逐个元素与item对比,直到找到
i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted);
} else { //如果isSorted是true,使用二分查找,由sortedIndex实现。实际上没有对isSorted进行过多校验,由开发者自己保证正确性
i = _.sortedIndex(array, item);
return array[i] === item ? i : -1;
}
}
for (; i < length; i++) if (array[i] === item) return i;
return -1;
};
invoke
_.invoke = function(obj, method) {
var args = slice.call(arguments, 2); //invoke方法接受多于两个参数作为函数参数,从第3个参数开始将作为被调用函数的参数
var isFunc = _.isFunction(method);
return _.map(obj, function(value) {
return (isFunc ? method : value[method]).apply(value, args); //method可以是对象的属性名,这种情况就是调用对象自己属性名为method的方法
});
};
toArray
_.toArray = function(obj) {
if (!obj) return [];
if (_.isArray(obj)) return slice.call(obj); //slice.call会将Array-like对象转化为Array,难道isArray判断有问题?
if (obj.length === +obj.length) return _.map(obj, _.identity); //其他有length属性的对象,_identity返回参数自己,组成新数组。但是不清楚什么类型的对象会走到这里?
return _.values(obj); //Object,返回属性的value组成数组
};
memoize(不知道是不是拼写错误,也改不了了)
_.memoize = function(func, hasher) {
var memo = {};
hasher || (hasher = _.identity);
return function() {
var key = hasher.apply(this, arguments);
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); //利用闭包来保存结果,好处在大量运算时避免了对func函数的相同参数情况下的重复调用,只要执行过一次,以后直接取结果
};
};
isType
_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
//定义了一批isType函数,each没有返回值,但是相当于循环展开
_['is' + name] = function(obj) {
return toString.call(obj) == '[object ' + name + ']';
};
});
isUndefined
_.isUndefined = function(obj) {
return obj === void 0; // void 0就是undefined,但是为什么不直接用undefined呢?没搞清楚
};
推荐的创建对象方式
跟underscore关系不大,来源于w3school,解决了一些问题,建议任选一种方式创建对象混合的构造函数/原型方式
function Car(sColor,iDoors,iMpg) {
this.color = sColor;
this.doors = iDoors;
this.mpg = iMpg;
this.drivers = new Array("Mike","John");
}
Car.prototype.showColor = function() {
alert(this.color);
};
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);
oCar1.drivers.push("Bill");
alert(oCar1.drivers); //输出 "Mike,John,Bill"
alert(oCar2.drivers); //输出 "Mike,John"
动态原型方法
function Car(sColor,iDoors,iMpg) {
this.color = sColor;
this.doors = iDoors;
this.mpg = iMpg;
this.drivers = new Array("Mike","John");
if (typeof Car._initialized == "undefined") {
Car.prototype.showColor = function() {
alert(this.color);
};
Car._initialized = true;
}
}
这两种方式都解决了经典方式中成员方法会生成多份的问题或者新建对象的属性成员指向同一个引用对象的问题。而问题的根因在于function和object是引用类型,作为属性的object需要创建多份,而方法为节省空间在内存中应该只有一份。