目录
说明
underscore是一个很有用的javaScript工具库,对函数式编程提供很多方法,所以读源码很适合了解函数式编程,笔者读的是1.8.3版,代码和注释都为手写,又不懂得也参考了网上资料,然后用了es6的let代替了var,等读完会将封装过的js文件放到github上:
_.VERSION = '1.8.3';
立即执行函数
underscore的最外层是一个立即执行函数,所有内容都放在函数内部,利用的思想是闭包。
(function ( ) {...}) ()
形成一个独立的作用域,这样的好处是可以不污染全局,也可以防止其他因素对内部函数的影响
全局变量声明
let root = (
(typeof self == 'object') &&
// self表示window窗口自身,这是浏览器环境下的全局命名空间
(self.self === self) &&
// 如果存在self,判断self是否是自身引用,即window这一对象
(self)
// 如果以上都满足,说明全局对象是window,并返回window作为root,这里self即window
) ||
(
(typeof global == 'object') &&
// global表示node环境下全局的命名空间
(global.global === global) &&
// 如果存在gloabl,判断global是否是自身引用
(global)
// 如果以上都满足,说明全局对象是global,并返回global作为root
) ||
(this);
// 如果以上两者都不是,直接返回this,这里应该处理既不是window这一浏览器环境,也不是global这一node环境的
该段代码主要用于确认环境的全局命名空间,并赋值给变量root。
值得注意的是,网上很多源码分析给出的是
var root = this;
但在1.8.3版本中查看了github上的源码,发现它是最上面的封装方式,这样的好处有:
1、向前兼容严格模式,在严格模式下直接使用this会得到undefined。这是因为ecma262第5版中,为了防止人们误将非new出来的对象的this指向全局命名空间,特地将之设置为undefined。
2、用来支持WebWorker,在WebWorker里可以使用self但不能使用window.
另一个值得注意的地方是:
(typeof self == 'object')
这里用的是==,而不是===。==和===的区别是一个不完全等,一个是全等,用==的好处是可以进行转义,因为全局命名空间不一定完全等价为对象,这跟浏览器的实现有关。
避免命名冲突
let previousUnderscore = root._; // 将全局变量中的变量
_赋值给变量previousUnderscore进行缓存
previousUnderscore,从字面上理解就是“以前的 underscore”,最开始不明白这条语句的用意,但是从头到尾只有一个地方用到了 previousUnderscore,即(1352行):
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};
// 使用noConflict方法返回自身
如果发生冲突,可以用例如
var underscore_cache = _.noConflict();
用来重新定义 underscore 命名.
_(初始化)
let _ = function (obj) {
if (obj instanceof _){
return obj;
}
// 如果obj已经是·_·函数的实例,则直接返回obj
if (!(this instanceof _)) {
return new _(obj);
}
// 如果不是`_`函数的实例
// 则调用new运算符,返回实例化对象
this._wrapped = obj;
// 将obj赋值给this._wrapped属性
};
其实核心函数——
_
其实是一个构造函数,支持无new调用的构造函数。当使用函数式风格的代码时并不会有太大影响,但使用面向对象风格的代码时,这里省去了使用者调用new的麻烦。
node服务端中:
if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
这是 Node.js 中对通用模块的封装方法,通过对判断 exports 是否存在来决定将局部变量 _ 赋值给exports,顺便说一下 AMD 规范、CMD规范和 UMD规范,Underscore.js 是支持 AMD 的,在源码尾部有定义,这里简单叙述一下:
amd:AMDJS
define(['underscore'], function (_) {
//todo
});
var _ = require('underscore');
module.exports = _;
值得一提的是使用nodeType来确保exports和module并不是HTML的元素。
保存原型
let ArrayProto = Array.prototype,
ObjProto = Object.prototype,
SymbolProto = ((typeof(Symbol)) !== ("undefined")) ? (Symbol.prototype) : (null);
// 缓存变量,减少代码量,便于压缩代码
let push = ArrayProto.push,
slice = ArrayProto.slice,
// slice方法,可以从数组中返回选定的元素
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// 缓存变量,减少代码量,便于压缩代码
// 同时可减少在原型链中的查找次数(提高代码效率)
let nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
// 返回一个给定对象的所有可枚举属性的字符串数组
nativeCreate = Object.create;
// 可以调用create方法来创建对象,对象的原型就是create方法的第一个参数
//ES5原生方法,如果浏览器支持,则underscore中会优先使用
值得一提的是:
var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;
2009年的 ES5 规定了六种语言类型:Null Undefined Number Boolean String Object,详见ES5/类型 和 ES5/类型转换与测试。新出台的 ES6 则规定,包括六种原始类型:Null Undefined Number Boolean String 和 Symbol,还有一种 Object,详见JavaScript 数据类型和数据结构。新增加的 Symbol 很早就已经提出,其具体概念这里不再复述请移步参考 Symbol ,得益于 ES6 的渐渐普及,客户端浏览器也有很多已经支持 Symbol,比如 Firefox v36+ 和 Chrome v38+ 等,具体参考 ES6 支持情况,如果大家对 ES6 想要深入了解可以看 ES6 In Depth 这篇文章和 ES6草案。
对象创建的特殊处理
let Ctor = function () {}; //用于代理转换的空函数
let baseCraate = function (prototype) {
if (!_.isObject(prototype)) {
return {};
}
// 如果参数不是对象,直接返回空对象
if (nativeCreate) {
return nativeCreate(prototype);
}
//如果原生的对象创建可以使用,返回该方法根据原型创建的对象。
//处理没有原生对象创建的情况
Ctor.prototype = prototype;
//将空函数的原型指向要使用的原型
let result = new Ctor();
//创建一个实例
Ctor.prototype = null;
// 恢复Ctor的原型给下次使用
return result;
//返回该实例
};
为了处理Object.create的跨浏览器的兼容性,underscore进行了特殊的处理。原型是无法直接实例化的,因此我们先创建一个空对象,然后将其原型指向这个我们想要实例化的原型,最后返回该对象其一个实例。
回调处理
在看这个之前,先解释一下void 0是什么含义? 引文来源
在 JavaScript 中,假设我们想判断一个是否是 undefined,那么我们通常会这样写:
if(a === undefined){}
但是,JavaScript 中的 undefined 并不可靠,我们试着写这样一个函数:
function test(a) {
var undefined = 1;
console.log(undefined); // => 1
if(a===undefined) {
// ...
}
}
现在我们能够明确的,标识符 undefined 并不能真正反映 “未定义”,所以我们得通过其他手段获得这一语义。幸好 JavaScript 还提供了 void 运算符,该运算符会对指定的表达式求值,并返回受信的 undefined:
void expression
最常见的用法是通过以下运算来获得 undefined,表达式为 0 时的运算开销最小:
void 0;
// or
void(0);
在 underscore 中,所有需要获得 undefined 地方,都通过 void 0 进行了替代。
当然,曲线救国的方式不只一种,我们看到包裹 jquery 的立即执行函数:
(function(window,undefined) {
// ...
})(window)
在这个函数中,我们没有向其传递第二参数(形参名叫 undefined),那么第二个参数的值就会被传递上 “未定义”,因此,通过这种方式,在该函数的作用域中所有的 undefined 都为受信的 undefined。
// underscore内部方法
// 根据this指向(context参数)以及argCount参数二次操作返回一些回调、迭代方法
let optimizeCb = function (func, context, argCount) {
if (context === void 0) {
return func;
}
// void 0返回undefined,即未传入上下文信息时直接返回相应的函数
switch (argCount) {
// 如果传入了argCount,那么参数数量为argCount,如果传入等价为null,则为3,包括未传值得情况
// 1个参数的时候,只需要传递当前值
case 1: return function (value) {
return func.call(context, value);
};
// 并没有2个参数的时候,因为目前并没有用到2个参数的时候
// 3个参数的时候,分别是当前值、当前索引以及整个集合
// _.each、_.map
case 3:return function (value, index, collection) {
return func.call(context, value, index, collection)
};
// 4个参数的时候,分别是累计值、当前值、当前索引以及整个集合
//_.reduce、_reduceRight
case 4:return function (accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
// 如果都不符合上述的任一条件,直接使用apply调用相关函数
return function () {
return func.apply(context, arguments)
}
其实不用上面的switch-case语句
直接执行下面的return语句,一样的效果
不这样做的原因call比apply快很多
apply在运行前要对作为参数的数组进行一系列的检验和深拷贝,.call则没有这些步骤
https://segmentfault.com/q/1010000007894513
http://www.ecma-international.org/ecma-262/5.1/#sec-15.3.4.3
http://www.ecma-international.org/ecma-262/5.1/#sec-15.3.4.4
接下来,是针对集合迭代的回调处理
var builtinIteratee;
// 设置变量保存内置迭代
var cb = function(value, context, argCount) {
if (_.iteratee !== builtinIteratee) {return _.iteratee(value, context);}
// 如果用户修改了迭代器,则使用新的迭代器
if (value == null) {return _.identity;}
// 如果不传value,表示返回等价的自身
if (_.isFunction(value)) {return optimizeCb(value, context, argCount);}
// 如果传入函数,返回该函数的回调
if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
// 如果传入对象,寻找匹配的属性值
return _.property(value);
// 如果都不是,返回相应的属性访问器
};
// 默认的迭代器,是以无穷argCount为参数调用cb函数。用户可以自行修改。
_.iteratee = builtinIteratee = function(value, context) {
return cb(value, context, Infinity);
};
剩余的处理
// 属性访问器生成。
// 通过传入键名,返回可以访问以传入对象为参数,并可以获取该对象相应键值对的函数
let shallowProperty = function(key) {
return function(obj) {
return obj == null ? void 0 : obj[key];
};
};
// 额外参数。
// 等价于ES6的rest参数。它将起始索引后的参数放入一个数组中。
let restArgs = function(func, startIndex) {
startIndex = startIndex == null ? func.length - 1 : +startIndex;
// startIndex为null时,为函数声明时的参数数量减1,即除了第一个参数后的其他参数,否则为传入的startIndex
// 返回一个支持rest参数的函数
return function() {
// 校正参数, 以免出现负值情况
let length = Math.max(arguments.length - startIndex, 0),
// 剩余参数是一个length长度的数组
rest = Array(length),
index = 0;
// 假设参数从2个开始: func(a,b,*rest)
// 调用: func(1,2,3,4,5); 实际的调用是:func.call(this, 1,2, [3,4,5]);
for (; index < length; index++) {
rest[index] = arguments[index + startIndex];
}
// 根据rest参数不同, 分情况调用函数, 需要注意的是, rest参数总是最后一个参数, 否则会有歧义
switch (startIndex) {
case 0: return func.call(this, rest);
// 起始索引为0,表示全部参数作为一个数组调用
case 1: return func.call(this, arguments[0], rest);
// 表示将第一个参数以及后续参数的数组,作为两个参数进行调用
case 2: return func.call(this, arguments[0], arguments[1], rest);
// 将第一个参数、第二个参数以及后续参数的数组作为三个参数进行调用
}
// 如果不是上面三种情况, 而是更通用的apply
let args = Array(startIndex + 1);
// 先拿到前面参数
for (index = 0; index < startIndex; index++) {
args[index] = arguments[index];
}
// 拼接上剩余参数
args[startIndex] = rest;
return func.apply(this, args);
};
};
// 处理类数组对象。
// 类数组对象的长度应当是一个非负整数
let MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
// 表示正无穷+∞
let getLength = shallowProperty('length');
// 获取length属性访问器
let isArrayLike = function(collection) {
let length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
网友评论