在一个多月的毕业设计以后,我再次开始了Underscore的源码阅读学习,断断续续也写了好些篇文章了,基本把一些比较重要的或者我的认为有养分的函数都解读了一遍,因此如今学习一下Underscore的总体架构。我相信不少程序员都会有一个梦想,那就是能够写一个本身的模块或者工具库,那么咱们如今就来学习一下若是咱们要写一个本身的Underscore,咱们该怎么写?css
大体的阅读了一下Underscore源码,能够发现其基本架构以下:html
在ES6以前,JavaScript开发者是没法经过let、const关键字模拟块做用域的,只有函数内部的变量会被认为是私有变量,在外部没法访问,因此大部分框架或者工具库的模式都是在当即执行函数里面定义一系列的变量,完成框架或者工具库的构建,这样作的好处就是代码不会污染全局做用域。Underscore也不例外,它也使用了经典的当即执行函数的模式:node
(function() {
// ...
}())
复制代码
此外,Underscore采用了经典的构造器模式,这使得用户能够经过_(obj).function()
的方式使用Underscore的接口,由于任意建立的Underscore对象都具备原型上的全部方法。那么代码形式以下:git
(function() {
var _ = function() {
// ...
};
}())
复制代码
_是一个函数,可是在JavaScript中,函数也是一个对象,因此咱们能够给_添加一系列属性,即Underscore中的一系列公开的接口,以即可以经过_.function()
的形式调用这些接口。代码形式以下:程序员
(function() {
var _ = function() {
// ...
};
_.each = function() {
// ...
};
// ...
}())
复制代码
_变量能够当作构造器构造一个Underscore对象,这个对象是标准化的,它具备规定的属性,好比:_chain
、_wrapped
以及全部Underscore的接口方法。Underscore把须要处理的参数传递给_构造函数,构造函数会把这个值赋给所构造对象的_wrapped
属性,这样作的好处就是在以后以_(obj).function()
形式调用接口时,能够直接到_wrapped
属性中寻找要处理的值。这就使得在定义_构造函数的时候,须要对传入的参数进行包裹,此外还要防止多层包裹,以及为了防止增长new操做符,须要在内部进行对象构建,代码形式以下:es6
(function() {
var _ = function(obj) {
// 防止重复包裹的处理,若是obj已是_的实例,那么直接返回obj。
if(obj instanceof _) {
return obj;
}
// 判断函数中this的指向,若是this不是_的实例,那么返回构造的_实例。
// 这里是为了避免使用new操做符构造新对象,很巧妙,由于在经过new使用构造函数时,函数中的this会指向新构造的实例。
if(!(this instanceof _)) {
return new _();
}
//
this._wrapped = obj;
};
_.each = function() {
// ...
};
// ...
}())
复制代码
这一段的处理很关键也很巧妙。github
既然咱们是在当即执行函数内定义的变量,那么_的生命周期也只存在于匿名函数的执行阶段,一旦函数执行完毕,这个变量所存储的数据也就被释放掉了,因此不导出变量的话实际上这段代码至关于什么都没作。那么该如何导出变量呢?咱们知道函数内部能够访问到外部的变量,因此只要把变量赋值给外部做用域或者外部做用域变量就好了。一般为了方便实用,把变量赋值给全局做用域,不一样的环境全局做用域名称不一样,浏览器环境下一般为window,服务器环境下一般为global,根据不一样的使用环境须要作不一样的处理,好比浏览器环境下代码形式以下:npm
(function() {
var _ = function() {
// ...
};
_.each = function() {
// ...
};
// ...
window._ = _;
}())
复制代码
这样处理以后,在全局做用域就能够直接经过_使用Underscore的接口了。小程序
可是仅仅这样处理还不够,由于Underscore面向环境不少,针对不一样的环境要作不一样的处理。接下来看Underscore源码。微信小程序
首先,Underscore经过如下代码根据不一样的环境获取不一样的全局做用域:
//获取全局对象,在浏览器中是self或者window,在服务器端(Node)中是global。
//在浏览器控制台中输入self或者self.self,结果都是window。
var root = typeof self == 'object' && self.self === self && self || typeof global == 'object' && global.global === global && global || this || {};
root._ = _;
复制代码
注释写在了代码中,若是既不是浏览器环境也不是Node环境的话,就获取值为this,经过this获取全局做用域,若是this仍然为空,就赋值给一个空的对象。感谢大神@冴羽的指教,赋值给空对象的做用是防止在开发微信小程序时报错,由于在微信小程序这种特殊环境下,window和global都是undefined,而且强制开启了strict模式,这时候this也是undefined(严格模式下禁止this指向全局变量),因此指定一个空对象给root,防止报错,具体参考:`this` is undefined in strict mode。
这里值得学习的地方还有做者关于赋值的写法,十分简洁,尝试了一下,对于下面的写法:
const flag = val1 && val2 && val3 || val4 && val5;
复制代码
程序会从左到右依次判断val一、val二、val3的值,假设||
把与运算分为许多组,那么:
好比:
const a = 1 && 2 && 3 || 2 && 3;
// a === 3
const b = 1 && false && 2 || 2 && 3;
// b === 3
const c = 1 && false && 2 || false && 2
// c === false
const d = 1 && false && 2 || 0 && 2
// d === 0
const e = 1 && false && 2 || 1 && 2
// e === 2
复制代码
除了要考虑给全局做用域赋值的差别之外,还要考虑JavaScript模块化规范的差别,JavaScript模块化规范包括AMD、CMD等。
经过如下代码兼容AMD规范:
//兼容AMD规范的模块化工具,好比RequireJS。
if (typeof define == 'function' && define.amd) {
define('underscore', [], function () {
return _;
});
}
复制代码
若是define是一个函数而且define.amd不为null或者undefined,那就说明是在AMD规范的工做环境下,使用define函数导出变量。
经过如下代码兼容CommonJS规范:
//为Node环境导出underscore,若是存在exports对象或者module.exports对象而且这两个对象不是HTML DOM,那么即为Node环境。
//若是不存在以上对象,把_变量赋值给全局环境(浏览器环境下为window)。
if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
复制代码
此外,经过以上代码能够支持ES6模块的import语法。具体原理参考阮一峰老师的教程:ES6 模块加载 CommonJS 模块。若是既不是AMD规范也不是CommonJS规范,那么直接将_赋值给全局变量。这一点能够经过将Underscore源码复制到浏览器的控制台回车后再查看_
和_.prototype
的值获得结论。
导出变量以后,在外部就可使用咱们定义的接口了。
许多出名的工具库都会提供链式调用功能,好比jQuery的链式调用:$('...').css().click();
,Underscore也提供了链式调用功能:_.chain(...).each().unzip();
。
链式调用基本都是经过返回原对象实现的,好比返回this,在Underscore中,能够经过_.chain
函数开始链式调用,实现原理以下:
// Add a "chain" function. Start chaining a wrapped Underscore object.
//将传入的对象包装为链式调用的对象,将其标志位置位true。
_.chain = function (obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};
复制代码
它构造一个_实例,而后将其_chain
链式标志位属性值为true表明链式调用,而后返回这个实例。这样作就是为了强制经过_().function()
的方式调用接口,由于在_的原型上,全部接口方法与_的属性方法有差别,_原型上的方法多了一个步骤,它会对其父对象的_chain
属性进行判断,若是为true,那么就继续使用_.chain
方法进行链式调用的包装,在一部分在后续会继续讨论。
在许多出名的工具库中,均可以实现用户扩展接口,好比jQuery的$.extend
和$.fn.extend
方法,Underscore也不例外,其_.mixin
方法容许用户扩展接口。
这里涉及到的一个概念就是mixin设计模式,mixin设计模式是JavaScript中最多见的设计模式,能够理解为把一个对象的属性拷贝到另一个对象上,具体能够参考:掺杂模式(mixin)。
先看Underscore中_.mixin
方法的源代码:
_.mixin = function (obj) {
// _.functions函数用于返回一个排序后的数组,包含全部的obj中的函数名。
_.each(_.functions(obj), function (name) {
// 先为_对象赋值。
var func = _[name] = obj[name];
// 为_的原型添加函数,以增长_(obj).mixin形式的函数调用方法。
_.prototype[name] = function () {
// this._wrapped做为第一个参数传递,其余用户传递的参数放在后面。
var args = [this._wrapped];
push.apply(args, arguments);
// 使用chainResult对运算结果进行链式调用处理,若是是链式调用就返回处理后的结果,
// 若是不是就直接返回运算后的结果。
return chainResult(this, func.apply(_, args));
};
});
return _;
};
复制代码
这段代码很好理解,就是对于传入的obj对象参数,将对象中的每个函数拷贝到_对象上,同名会被覆盖。与此同时,还会把obj参数对象中的函数映射到_对象的原型上,为何说是映射,由于并非直接拷贝的,还进行了链式调用的处理,经过chainResult方法,实现了了链式调用,因此第三节中说_对象原型上的方法与_对象中的对应方法有差别,原型上的方法多了一个步骤,就是判断是否链式调用,若是是链式调用,那么继续经过_.chain
函数进行包装。chainResult函数代码以下:
// Helper function to continue chaining intermediate results.
//返回一个链式调用的对象,经过判断instance._chain属性是否为true来决定是否返回链式对象。
var chainResult = function (instance, obj) {
return instance._chain ? _(obj).chain() : obj;
};
复制代码
实现mixin函数以后,Underscore的设计者很是机智的运用了这个函数,代码中只能够看到为_自身定义的一系列函数,好比_.each
、_.map
等,但看不到为_.prototype
所定义的函数,为何还能够经过_().function()
的形式调用接口呢?这里就是由于做者经过_.mixin
函数直接将全部_上的函数映射到了_.prototype
上,在_.mixin
函数定义的下方,有一句代码:
// Add all of the Underscore functions to the wrapper object.
_.mixin(_);
复制代码
这句代码就将全部的_上的函数映射到了_.prototype
上,有点令我叹为观止。
经过_.mixin
函数,用户能够为_扩展自定义的接口,下面的例子来源于中文手册:
_.mixin({
capitalize: function(string) {
return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase();
}
});
_("fabio").capitalize();
=> "Fabio"
复制代码
在许多工具库中,都有实现noConflict,由于在全局做用域,变量名是独一无二的,可是用户可能引入多个类库,多个类库可能有同一个标识符,这时就要使用noConflict实现无冲突处理。
具体作法就是先保存原来做用域中该标志位的数据,而后在调用noConflict函数时,为全局做用域该标志位赋值为原来的值。代码以下:
// Save the previous value of the `_` variable.
//保存以前全局对象中_属性的值。
var previousUnderscore = root._;
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
// previous owner. Returns a reference to the Underscore object.
_.noConflict = function () {
root._ = previousUnderscore;
return this;
};
复制代码
在函数的最后,返回了Underscore对象,容许用户使用另外的变量存储。
做为一个对象,应该有一些基本属性,好比toString、value等等,须要重写这些属性或者函数,以便使用时返回合适的信息。此外还须要添加一些版本号啊什么的属性。
作完以上全部的工做以后,一个基本的工具库基本就搭建完成了,完成好测试、压缩等工做以后,就能够发布在npm上供你们下载了。想要写一个本身的工具库的同窗能够尝试一下。
另外若是有错误之处或者有补充之处的话,欢迎你们不吝赐教,一块儿学习,一块儿进步! 更多Underscore源码解析:GitHub