最近开始读源码了,遵循大多数过来人的建议,underscore.js是最适合新手阅读的一个js库,所以我就迫不及待地下载了underscore.js,1.8.3版本。
IIFE(立即执行函数)
与其他第三方库一样,underscore.js也是通过IIFE
来包裹自己的业务逻辑。
(function(){
...
}.call(this))
通过传入this
(浏览器环境中其实就是window
对象)来改变函数的作用域。(function(){}.call(this))
等同于(function(){}())
,就变成熟知的IIFE的写法了。
关于IIFE
和this
,附上两篇文章,有助于更好地认识相关问题。
详解javascript立即执行函数表达式(IIFE)
如何理解JavaScript中的函数调用和“this”
暴露一个全局对象_
(function(){
var root = this;
// 定义underscore对象
var _ = function (obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};
// 把underscore对象暴露到全局作用域
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
}.call(this));
可以看到代码中的_
被定义成了一个函数(暂时先不管函数里面的代码是什么意思),函数也是对象,所以可以在_
对象上添加一些方法。比如:
(function(){
...
_.sayHello = function(name) {
console.log('Hello, ' + name + '!');
};
}.call(this));
// 此时就可以在我们自己的js代码中调用这个方法了
_.sayHello('underscore'); // 'Hello, underscore!'
这也是underscore.js大部分情况下的调用方式,_.funcName(arg1, arg2)
,文档中也是以这种方式调用的。
看,我们好像也写出了一个类underscore.js库呢,好开森啊,哈哈~~~
面向对象编程(OOP)风格
在函数式编程(FP)范式中,函数是拿来就用的,即便是对象,也只是函数的一个参数:
var arr = [1, 2, 3];
_.map(arr, function(item) {
return item * 2;
});
而在OOP中,函数一般是隶属于某个对象的方法:
var arr = [1, 2, 3];
arr.map(function(item) {
return item * 2;
});
underscore.js是推崇函数式编程的,但是也提供了OOP风格的函数调用方式,仅需要通过_()
来包裹一下对象即可:
var arr = [1, 2, 3];
// FP风格
_.map(arr, function(item) {
return itme * 2;
});
// OOP风格
_(arr).map(function(item) {
return item * 2;
});
通过_()
构造出一个实例对象,下面来分析一下这个构造过程:
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};
_([1, 2]);
// 当执行这句代码时,函数中的this指向window
// obj instanceof _ === false, this instanceof _ === false
// 所以执行 return new _(obj)
// 在new的时候,this指向了构造函数_的实例,所以 this instanceof _ === true
// this._wrapped = obj; 表示构造实例有一个属性_wrapped,值为obj(供后面_.mixin方法调用)
// 当obj已经是一个实例的时候,_(obj)直接返回这个实例: _(_([1,2])) == _([1,2])
// 自此,构造过程就结束了
// 这也就是所谓的 无new调用的构造函数
_
的构造实例怎么调用_.func
这些方法呢?构造实例本身没有这些方法,那么这些方法应该存在原型链上,也就是_.prototype
上。_
又是如何把自身的方法挂载到_.prototype
上面呢?
思路:遍历_
的属性,如果某个属性的类型是function
,就把该函数挂载到_.prototype
上:
_.mixin = function(obj) {
// _.functions(obj) 返回obj中所有的方法
_.each(_.functions(obj), function(name) {
_.prototype[name] = function() {
var args = [this._wrapped];
// _(args1).func(args2) == _.func(args1, args2)
// 右侧func的参数比左侧func的参数多一个,也就是this._wrapped
push.apply(args, arguments);
return obj[name].apply(_, args); // 方法调用结果
};
});
};
_.mixin(_); // 把 _ 对象上的方法都挂载到其原型上
链式调用
想要实现链式调用,通常我们会在支持链式调用的函数中返回对象本身:
var car = {
run: function() {
console.log('begin run');
return this;
},
stop: function() {
console.log('stopped');
}
};
car.run().stop();
// => begin run
// => stopped
underscore.js中链式调用的实现:
// 开启一个链式调用
_.chain = function(obj) {
var instance = _(obj); // 获得一个underscore实例
instance._chain = true; // 标识当前实例支持链式调用
return instance;
};
// Helper函数,用来判断 实例调用某一方法之后的返回结果是否支持继续链式调用
var result = function(instance, obj) {
return instance._chain ? _(obj).chain() : obj;
};
// 此时,我们要把上面的_.mixin函数更新一下了
_.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return result(this, obj[name].apply(_, args)); // this指向underscore实例
};
});
};
// 由于链式调用的结果被转化为一个带有_chain属性的underscore实例对象,
// {_wrapped: obj, _chain: true}
// 所以想要获取链式调用的结果时,需要有个取值过程
_.prototype.value = function() {
return this._wrapped; // 返回被包裹的对象
};
// 链式调用demo
var testArr = [1, 2, 3];
_.chain(testArr)
.map(function(item) {
return item * 5;
})
.each(function(item) {
console.log(item);
})
.first()
.value(); // 5
// _(testArr).chain().map().each()... 这种方式也可以
Mixin
mixin(混入)模式是增加代码复用度的一个广泛使用的设计模式:向一个对象混入一系列方法,使之具备更强大的能力,这一系列方法又包裹在一个称之为mixin的对象中,这样,其他对象也能够通过该mixin进行扩展。
underscore.js也允许用户扩充_
的功能,只需要在_.mixin函数上加上一行代码:
// 完整版
_.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
// 当obj是自定义对象时,
// obj的方法被扩充到underscore的方法集合中以及_.prototype上
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return result(this, func.apply(_, args));
};
});
};
// demo
_.mixin({
add5: function(num) {
return num + 5;
}
});
_.add5(5); // 10
_([1,2,3]).chain().map(function(item) {
return item * 10;
}).first().add5().value(); // 15
理解了以上所说的内容,underscore.js的整体代码架构就掌握的差不多了,接下去就可以一个一个方法的去看具体代码细节。才刚读了这些内容,我就感觉收货颇多,特别是读到Mixin模式的时候,感觉像发现了新大陆似的,接下来会去再看一些其他类型的设计模式。
第一次读源码,认识不足或分析不到位的地方,还请路过的大大给以指正,谢谢!
以下两个链接的内容对于我理清underscore.js的代码结构起了很大帮助,非常感谢原作者。
underscore源码分析 - 这是本gitbook,写得特别好
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。