1. 作用域包裹,通过立即执行函数来包裹自己的业务逻辑
作用:(不受外部影响,也不给外部添麻烦)
- 避免全局污染:所有库的逻辑,库所定义和使用的变量全部被封装到了该函数的作用域中
- 隐私保护:但凡在立即执行函数中声明的函数、变量等,除非是自己想暴露。否则不可能在外部获得
(function() {
// ...执行逻辑
})(this)
2. 对象 _,函数对象,之后所有的API会被挂载在这个对象上,例如: _.each _.map
var _ = function (obj) {
// 以下均针对 OOP 形式的调用
// 如果是非 OOP 形式的调用,不会进入该函数内部
// 如果 obj 已经是 `_` 函数的实例,则直接返回 obj
if (obj instanceof _) { return obj; }
// 如果不是 `_` 函数的实例
// 则调用 new 运算符,返回实例化的对象
if (!(this instanceof _)) { return new _(obj); }
// 将 obj 赋值给 this._wrapped 属性
this._wrapped = obj;
};
3.执行环境的判断
如何使你的库既能够服务于浏览器,又能服务于诸如nodejs所搭建的服务器端,underscore对象_会将依托当前所处的环境,挂载到不同的全局空间中,浏览器的全局对象是windowsWindows,node的全局对象是global。
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this;
Tips:在这里写self是为了一些不具有窗口的上下文环境中,例如WebWorker
其次,如果处于node环境,_还将会被作为模块导出
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
4.松弛绑定
默认情况下,underscore对象会覆盖对象同名的属性,但是他会保存之前存在的属性,因为想lodash这样的一些库也喜欢将自己的对象命名为
var previousUnderscore = root._;
当当前用户已经在全局对象上绑定了对象的时候,可以通过underscore提供的noConflict函数来重命名underscore对象,或者说是手动获得underscore对象,避免与之前的冲突
var underscore = _.noConflict();
noConflict源码,在其内部,将会恢复原来对象上的_:
/**
* 返回一个 underscore 对象,把_所有权交还给原来的拥有者(比如 lodash)
*/
_.noConflict = function () {
// 回复原来的_指代的对象
root._ = previousUnderscore;
// 返回 underscore 对象
return this;
};
5.局部变量的妙用
- underscore本身也一来了不少js原生方法,他会通过局部变量来保存一些他经常用到的方法或者属性,避免了冗长代码的书写
- 减少了对象成员的访问深度,,(Array.prototype.push --> push), 这样做能带来一定的性能提升
// 缓存变量便于压缩代码,值得是压缩到min.js的版本
var ArrProto = Array.prototype;
var ObjProto = Object.prototype;
var FuncProto = Function.prototype;
// 缓存变量便于压缩代码,值得是压缩到min.js的版本
// 同时可以减少在原型链中的查找次数(提高代码效率)
var push = ArrProto.push;
var slice = ArrProto.slice;
var toString = ObjProto.toString;
var hasOwnProperty = ObjProto.hasOwnProperty;
//ES5的原生的方法,如果浏览器支持,则 underscore 中会优先使用
var nativeArray = Array.isArray;
var nativeKeys = Object.keys;
var nativeBind = FuncProto.bind;
var nativeCreate = Object.create;
6.undefined的处理
因为JS中undefined并不是可靠的,因为这个标识符可能会被改写
例如:
function test(a) {
var undefined = 1;
console.log(undefined); // => 1
if (a === undefined) {
// ...
}
}
// 在 ES5 之前,全局的 undefined 也是可以被修改的,而在 ES5 中,该标识符被设计为了只读标识符, 假如你现在的浏览器不是太老,你可以在控制台中输入以下语句测试一下:
undefined = 1;
console.log(undefined); // => undefined
可见,标识符undefined并不能真正的反应‘未定义’,所以我们需要通过其他手段。JS提供了void运算符,这个运算符会被指定的表达式求职,并返回受信任的undefined:
void expression
// 最常见的用法是通过以下运算来获得 undefined,表达式为 0 时的运算开销最小:
void 0; or void(0);
// 所以在underscore 中,所有需要获得 undefined 地方,都通过void 0进行了替代
当然,曲线救国的方式不只一种,我们看到包裹jquery的立即执行函数:
// 在这个函数中,我们没有向其传递第二参数(形参名叫 undefined),那么第二个参数的值就会被传递上 “未定义”,因此,通过这种方式,在该函数的作用域中所有的 undefined 都为受信的 undefined。
(function(window, undefined) {
// ...
})(window)
7.使用迭代,而不是循环
// 迭代
var results = _.map([1,2,3], function(elem) {
return elem * 2;
}); // => [2, 4, 6]
// 循环
var results = [];
var elems = [1, 2, 3];
for (var i = 0, length = elems.length; i < length; i++) {
result.push(elems[i]*2);
} // => [2, 4, 6]
迭代 iteratee
对于一个迭代来说,他至少由2部分组成,
- 被迭代的集合
- 当前迭代过程:在 underscore 中,当前迭代过程是一个函数,他被称为 iteratee(直译为被迭代者),他将对当前的迭代元素进行处理。
- 我们看到 _.map 的实现
// Math.pow(2, 53) - 1 是 JavaScript 中能精确表示的最大数字
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
// getLength 函数
// 该函数传入一个参数,返回参数的 length 属性值
// 用来获取 array 以及 arrayLike 元素的 length 属性值
var getLength = property('length');
// 判断是否类数组,即拥有 length 属性并且 length 属性值为 Number 类型的元素
// 包括数组、arguments、HTML Collection 以及 NodeList 等等
// 包括类似 {length: 10} 这样的对象
// 包括字符串、函数等
var isArrayLike = function (collection) {
// 返回参数 collection 的 length 属性值
var length = getLength(collection);
return typeof length === 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
// 返回一个对象的 keys 组成的数组
// 仅返回 own enumerable properties 组成的数组
_.keys = function (obj) {
// 容错
// 如果传入的参数不是对象,则返回空数组
if (!_.isObject(obj)) return [];
// 如果浏览器支持 ES5 Object.key() 方法
// 则优先使用该方法
if (nativeKeys) return nativeKeys(obj);
var keys = [];
// own enumerable properties
for (var key in obj)
// hasOwnProperty
{ if (_.has(obj, key)) keys.push(key); }
// Ahem, IE < 9.
// IE < 9 下不能用 for in 来枚举某些 key 值
// 传入 keys 数组为参数
// 因为 JavaScript 下函数参数按值传递
// 所以 keys 当做参数传入后会在 `collectNonEnumProps` 方法中改变值
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};
// 遍历数组(每个元素)或者对象的每个元素(value)
// 对每个元素执行 iteratee 迭代方法
// 将结果保存到新的数组中,并返回
_.map = _.collect = function (obj, iteratee, context) {
// 根据 context 确定不同的迭代函数
iteratee = cb(iteratee, context);
// 如果传参是对象,则获取它的 keys 值数组(短路表达式)
var keys = !isArrayLike(obj) && _.keys(obj);
// 如果 obj 为对象,则 length 为 key.length
// 如果 obj 为数组,则 length 为 obj.length
var length = (keys || obj).length;
var results = Array(length); // 结果数组, 定长初始化数组
// 遍历
for (var index = 0; index < length; index++) {
// 如果 obj 为对象,则 currentKey 为对象键值 key
// 如果 obj 为数组,则 currentKey 为 index 值
var currentKey = keys ? keys[index] : index;
results[index] = iteratee(obj[currentKey], currentKey, obj);
}
// 返回新的结果数组
return results;
};
我们传递给的 _.map 的第二个参数就是一个 iteratee,他可能是函数,对象,甚至是字符串,underscore 会将其统一处理为一个函数。这个处理由 underscore 的内置函数 cb 来完成。
cb 的实现如下:
var cb = function(value, context, argCount) {
// 是否用自定义的iteratee;重置默认的.iteratee改变迭代过程中的行为只在underscore最新的master分支支持, 发布版的1.8.3并不支持
if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
// 针对不同的情况
if (value == null) return _.identity;
if (_.isFunction(value)) return optimizeCb(value, context, argCount);
if (_.isObject(value)) return _.matcher(value);
return _.property(value);
};
//返回传入的参数,统一将value做函数处理, _.identity 在 undescore 内大量作为迭代函数出现
// 能简化很多迭代函数的书写
_.identity = function (value) {
return value;
};
// 判断一个给定的对象是否有某些键值对
_.matcher = _.matches = function (attrs) {
attrs = _.extendOwn({}, attrs);
return function (obj) {
return _.isMatch(obj, attrs);
};
};
// 闭包
var property = function (key) {
return function (obj) {
return obj == null ? void 0 : obj[key];
};
};
_.property = property;
cb 将根据不同情况来为我们的迭代创建一个迭代过程 iteratee,服务于每轮迭代:
- value 为 null
如果传入的 value 为 null,亦即没有传入 iteratee,则 iteratee 的行为只是返回当前迭代元素自身,比如:
var results = _.map([1, 2, 3]); // => results: [1, 2, 3]
- value 为一个函数
如果传入 value 是一个函数,那么通过内置函数 optimizeCb 对其进行优化,optimizeCb 的作用放到之后讲,先来看个传入函数的例子
// => results: [
// "[1,2,3]'s 0 position is 1",
// "[1,2,3]'s 1 position is 2",
// "[1,2,3]'s 2 position is 3"
// ]
- value 为一个对象
如果 value 传入的是一个对象,那么返回的 iteratee(_.matcher)的目的是想要知道当前被迭代元素是否匹配给定的这个对象:
var results = _.map([{name: 'yoyoyohamapi'}, {name: 'wxj', age: 13}], {name: 'wxj'});
// => results: [false, true]
- value 是字面量,如数字,字符串等
var results = _.map([{name: 'yoyoyohamapi'}, {name: 'wxj', age: 13}], {name: 'wxj'});
// => results: [false, true]
var results = _.map([{name: 'yoyoyohamapi'}, {name: 'wxj'}], 'name');
// results: ['yoyoyohamapi', 'wxj'];
#######自定义的 iteratee
在 cb 函数的代码中,我们也发现了 underscore 支持通过覆盖其提供的 _.iteratee 函数来自定义 iteratee,更确切的说,来自己决定如何产生一个 iteratee:
var cb = function (value, context, argCount) {
// ...
if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
// ...
}
我们看一下 iteratee 函数的实现:
_.iteratee = builtinIteratee = function (value, context) {
return cb(value, context, Infinity);
};
默认的 _.iteratee 函数仍然是把生产 iteratee 的工作交给 cb 完成,并且通过变量 buildIteratee 保存了默认产生器的引用,方便之后我们覆盖了 _.iteratee 后,underscore 能够通过比较 _.iteratee 与 buildIteratee 来知悉这次覆盖(也就知悉了用户想要自定义 iteratee 的生产过程)。
比如当传入的 value 是对象时,我们不想返回一个 _.matcher 来判断当前对象是否满足条件,而是返回当前元素自身(虽然这么做很无聊),就可以这么做:
_.iteratee = function(value, context) {
// 现在,value为对象时,也是返回自身
if (value == null || _.isObject(value)) return _.identity;
if (_.isFunction(value)) return optimizeCb(value, context, argCount);
return _.property(value);
};
现在运行之前的例子,看一下有什么不同:
var results = _.map([{name: 'yoyoyohamapi'}, {name: 'wxj', age: 13}], {name: 'wxj'});
// => results: [{name: 'yoyoyohamapi'}, {name: 'wxj', age: 13}];
8.optimizeCb(优化回调)
当传入的 value 是一个函数时,value 还要经过一个叫 optimizeCb 的内置函数才能获得最终的 iteratee
/** 优化回调(特指函数中传入的回调)
*
* @param func 待优化回调函数
* @param context 执行上下文
* @param argCount 参数个数
* @returns {function}
*/
var optimizeCb = function(func, context, argCount) {
// 一定要保证回调的执行上下文存在
if (context === void 0) return func;
switch (argCount == null ? 3 : argCount) {
case 1: return function(value) {
return func.call(context, value);
};
case 2: return function(value, other) {
return func.call(context, value, other);
};
case 3: return function() {
return func.call(context, value, index, collection);
};
case 4: return function() {
return func.call(context, accumlator, value, index, collection);
};
}
return function() {
return func.apply(context, arguments);
};
};
optimizeCb 的总体思路就是:传入待优化的回调函数 func,以及迭代回调需要的参数个数 argCount,根据参数个数分情况进行优化。
- argCount == 1,即 iteratee 只需要 1 个参数
在 underscore 的 .times 函数的实现中,.times 的作用是执行一个传入的 iteratee 函数 n 次,并返回由每次执行结果组成的数组。它的迭代过程 iteratee 只需要 1 个参数 -- 当前迭代的索引:
// 执行iteratee函数n次,返回每次执行结果构成的数组
_.times = function(n, iteratee, context) {
var accum = Array(Math.max(0, n));
iteratee = optimizeCb(iteratee, context, 1);
for (var i = 0; i < n; i++) accum[i] = iteratee(i);
return accum;
};
// 看一个 _.times 的使用例子:
function getIndex(index) {
return index;
}
var results = _.times(3, getIndex); // => [0, 1, 2]
- argCount == 2,即 iteratee 需要 2 个参数
该情况在 underscore 没用使用,所以最新的 master 分支已经不再考虑这个参数个数为 2 的情况。 - argCount == 3(默认),即 iteratee 需要 3 个参数
这 3 个参数是:
value:当前迭代元素的值
index:迭代索引
collection:被迭代集合
在 _.map, _.each, _.filter 等函数中,都是给 argCount 赋值了 3:
_.each([1, 2, 3], function() {
console.log("被迭代的集合:"+collection+"; 迭代索引:"+index+"; 当前迭代的元素值"+value);
});
// =>
// 被迭代的集合:1,2,3; 迭代索引:0; 当前迭代的元素值:1
// 被迭代的集合:1,2,3; 迭代索引:1; 当前迭代的元素值:2
// 被迭代的集合:1,2,3; 迭代索引:2; 当前迭代的元素值:3
- argCount == 4,即 iteratee 需要 4 个参数
这 4 个参数分别是:
accumulator:累加器
value:迭代元素
index:迭代索引
collection:当前迭代集合
那么这个累加器是什么意思呢?在 underscore 中的内部函数 createReducer 中,就涉及到了 4 个参数的情况。该函数用来生成 reduce 函数的工厂,underscore 中的 _.reduce 及 _.reduceRight 都是由它创建的:
/**
* reduce 函数的工厂函数,用于生成一个reducer,通过参数决定reduce的方向
* @param dir 方向 left or right
* @returns {function}
*/
var createReduce = function (dir) {
var reducer = function (obj, iteratee, memo, initial) {
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length,
index = dir > 0 ? 0 : length - 1;
// memo用来记录最新的reduce结果
// 如果reduce没有初始化 memo, 则默认为首个元素 (从左开始则为第一个元素, 从右则为最后一个元素)
if (!initial) {
memo = obj[keys ? keys[index] : index];
index += dir;
}
for (; index >= 0 && index < length; index += dir) {
var currentKey = keys ? keys[index] : index;
// 执行 reduce 回调, 刷新当前值
memo = iteratee(memo, obj[currentKey], currentKey, obj);
}
return memo;
};
return function () {
// 如果参数正常, 则代表已经初始化了 memo
var initial = arguments.length >= 3;
// reducer 因为引入了累加器, 所以优化函数的第三个参数传入了 4,
// 这样, 新的迭代回调第一个参数就是当前的累加结果
return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
};
};
我们可以看到,createReduce 最终创建的 reducer 就是需要一个累加器,该累加器需要被初始化,看一个利用 _.reduce 函数求和的例子:
var sum = _.reduce([1,2,3,4,5], function(accumlator, value, index, collection) {
return accumlator + value;
}, 0); // => 15;
9.ubderscore是如何创建对象的
/**
* 创建一个对象,该对象继承自prototype
* 并且保证该对象在其原型上挂载属性不会影响所继承的prototype
* @param {object} prototype
*/
var baseCreate = function (prototype) {
if (!_.isObject(prototype)) return {};
// 如果存在原生的创建方法(Object.create),则用原生的进行创建
if (nativeCreate) return nativeCreate(prototype);
// 利用Ctor这个空函数,临时设置对象原型
Ctor.prototype = prototype;
// 创建对象,result.__proto__ === prototype
var result = new Ctor;
// 还原Ctor原型
Ctor.prototype = null;
return result;
};
我们可以看到,underscore 利用 baseCreate 创建对象的时候会先检查当前环境是否已经支持了 Object.create,如果不支持,会创建一个简易的 polyfill:
// 利用Ctor这个空函数,临时设置对象原型
Ctor.prototype = prototype;
// 创建对象,result.__proto__ === prototype
var result = new Ctor;
// 防止内存泄漏,因为闭包的原因,Ctor常驻内存
Ctor.prototype = null;
而之所以叫 baseCreate,也是因为其只做了原型继承,而不像 Object.create 那样还支持传递属性列表。
网友评论