// Establish the root object, `window` (`self`) in the browser, `global` // on the server, or `this` in some virtual machines. We use `self` // instead of `window` for `WebWorker` support. var root = typeof self == 'object' && self.self === self && self || typeof global == 'object' && global.global === global && global || this;
root 变量的作用是用来捕捉外部环境. 由于在自执行函数中,this 变量会被设置成
Window (浏览器中),所以我们可能通过为 this (即此处的root) 添加相应的变量来导出函数. 如:
(function() { this.exported_var = 10 }())
console.log(this.exported_var); // => 10
// Save bytes in the minified (but not gzipped) version: var ArrayProto = Array.prototype, ObjProto = Object.prototype;
// Create quick reference variables for speed access to core prototypes. var push = ArrayProto.push, slice = ArrayProto.slice, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty;
// All **ECMAScript 5** native function implementations that we hope to use // are declared here. var nativeIsArray = Array.isArray, nativeKeys = Object.keys, nativeCreate = Object.create;
以上同理.
// Naked function reference for surrogate-prototype-swapping. var Ctor = function(){};
// Create a safe reference to the Underscore object for use below. var _ = function(obj) { if (obj instanceof _) return obj; if (!(this instanceof _)) return new _(obj); this._wrapped = obj; };
// Add a "chain" function. Start chaining a wrapped Underscore object. _.chain = function(obj) { var instance = _(obj); instance._chain = true; return instance; };
// Export the Underscore object for **Node.js**, with // backwards-compatibility for their old module API. If we're in // the browser, add `_` as a global object. // (`nodeType` is checked to ensure that `module` // and `exports` are not HTML elements.) if (typeof exports != 'undefined' && !exports.nodeType) { if (typeof module != 'undefined' && !module.nodeType && module.exports) { exports = module.exports = _; } exports._ = _; } else { root._ = _; }
// Current version. _.VERSION = '1.8.3';
上文较好理解,判断不同的平台,导出 _ 变量。
// Internal function that returns an efficient (for current engines) version // of the passed-in callback, to be repeatedly applied in other Underscore // functions. 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); }; // The 2-parameter case has been omitted only because no current consumers // made use of it. case 3: return function(value, index, collection) { return func.call(context, value, index, collection); }; case 4: return function(accumulator, value, index, collection) { return func.call(context, accumulator, value, index, collection); }; } return function() { return func.apply(context, arguments); }; };
它接收额外的参数 context。而它的作用是在 iteratee 函数中将 this 指向
context。下面的是一个
StackOverflow
的例子:
var someOtherArray = ["name","patrick","d","w"];
_.each([1, 2, 3], function(num) { // 函数内, this “等于” someOtherArray
alert( this[num] ); // num is the value from the array being iterated // so this[num] gets the item at the "num" index of // someOtherArray. }, someOtherArray);
// A mostly-internal function to generate callbacks that can be applied // to each element in a collection, returning the desired result — either // `identity`, an arbitrary callback, a property matcher, or a property accessor. var cb = function(value, context, argCount) { if (value == null) return _.identity; if (_.isFunction(value)) return optimizeCb(value, context, argCount); if (_.isObject(value)) return _.matcher(value); return _.property(value); };
// An internal function for creating a new object that inherits from another. var baseCreate = function(prototype) { if (!_.isObject(prototype)) return {}; if (nativeCreate) return nativeCreate(prototype); Ctor.prototype = prototype; var result = new Ctor; Ctor.prototype = null; return result; };
// Helper for collection methods to determine whether a collection // should be iterated as an array or as an object. // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; var getLength = property('length'); var isArrayLike = function(collection) { var length = getLength(collection); return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; };
// The cornerstone, an `each` implementation, aka `forEach`. // Handles raw objects in addition to array-likes. Treats all // sparse array-likes as if they were dense. _.each = _.forEach = function(obj, iteratee, context) { iteratee = optimizeCb(iteratee, context); var i, length; if (isArrayLike(obj)) { for (i = 0, length = obj.length; i < length; i++) { iteratee(obj[i], i, obj); } } else { var keys = _.keys(obj); for (i = 0, length = keys.length; i < length; i++) { iteratee(obj[keys[i]], keys[i], obj); } } return obj; };
// Return the results of applying the iteratee to each element. _.map = _.collect = function(obj, iteratee, context) { iteratee = cb(iteratee, context); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, results = Array(length); for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; results[index] = iteratee(obj[currentKey], currentKey, obj); } return results; };
// 获取键值 var currentKey = keys ? keys[index] : index;
相应的,如果涉及值运算时,underscore.js 通常会先取得它的值数组:
obj = isArrayLike(obj) ? obj : _.values(obj);
这个模式中 underscore.js 中被多次运用。
// Create a reducing function iterating left or right. var createReduce = function(dir) { // Optimized iterator function as using arguments.length // in the main function will deoptimize the, see #1991. var reducer = function(obj, iteratee, memo, initial) { var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, index = dir > 0 ? 0 : length - 1; if (!initial) { memo = obj[keys ? keys[index] : index]; index += dir; } for (; index >= 0 && index < length; index += dir) { var currentKey = keys ? keys[index] : index; memo = iteratee(memo, obj[currentKey], currentKey, obj); } return memo; };
// Groups the object's values by a criterion. Pass either a string attribute // to group by, or a function that returns the criterion. _.groupBy = group(function(result, value, key) { if (_.has(result, key)) result[key].push(value); else result[key] = [value]; });
它的 behavior 函数就是将 iteratee 调用后的结果 value 按 key 进行分组。
_.partition = function(obj, iteratee) { var result = _.groupBy(obj, iteratee); return [result[true], result[false]]; }
// Generator function to create the findIndex and findLastIndex functions var createPredicateIndexFinder = function(dir) { return function(array, predicate, context) { predicate = cb(predicate, context); var length = getLength(array); var index = dir > 0 ? 0 : length - 1; for (; index >= 0 && index < length; index += dir) { if (predicate(array[index], index, array)) return index; } return -1; }; };
// Returns the first index on an array-like that passes a predicate test _.findIndex = createPredicateIndexFinder(1); _.findLastIndex = createPredicateIndexFinder(-1);
createPredicateIndexFinder 根据指定的步长 dir 创建遍历的函数。而实际上它在被用来创建 _.findIndex 和 _.findLastIndex,但无疑,这增加了许多阅读上的复杂度。当一个逻辑没有被很多使用时,是否需要独立成一个单独的模块,值得思考与讨论。
函数相关的函数
// Determines whether to execute a function as a constructor // or a normal function with the provided arguments var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) { if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); var self = baseCreate(sourceFunc.prototype); var result = sourceFunc.apply(self, args); if (_.isObject(result)) return result; return self; };
// Create a function bound to a given object (assigning `this`, and arguments, // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if // available. _.bind = restArgs(function(func, context, args) { if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); var bound = restArgs(function(callArgs) { return executeBound(func, bound, context, this, args.concat(callArgs)); }); return bound; });
var retrieveX = module.getX; retrieveX(); // 9, because in this case, "this" refers to the global object
// Create a new function with 'this' bound to module //New programmers (like myself) might confuse the global var getX with module's property getX var boundGetX = retrieveX.bind(module); // var boundGetX = _.bind(retrieveX, module); // underscore.js 相应的替代方法。 boundGetX(); // 81
// Partially apply a function by creating a version that has had some of its // arguments pre-filled, without changing its dynamic `this` context. _ acts // as a placeholder by default, allowing any combination of arguments to be // pre-filled. Set `_.partial.placeholder` for a custom placeholder argument. _.partial = restArgs(function(func, boundArgs) { var placeholder = _.partial.placeholder; var bound = function() { var position = 0, length = boundArgs.length; var args = Array(length); for (var i = 0; i < length; i++) { args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i]; } while (position < arguments.length) args.push(arguments[position++]); return executeBound(func, bound, this, this, args); }; return bound; });
_.partial.placeholder = _;
partial 函数类似于科里化(curry),但功能更加强大。关键在于支持占位符。如:
var subtract = function(a, b) { return b - a; }; subFrom20 = _.partial(subtract, _, 20); subFrom20(5); // => 15
// Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
var collectNonEnumProps = function(obj, keys) { var nonEnumIdx = nonEnumerableProps.length; var constructor = obj.constructor; var proto = _.isFunction(constructor) && constructor.prototype || ObjProto;
// Constructor is a special case. var prop = 'constructor'; if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
while (nonEnumIdx--) { prop = nonEnumerableProps[nonEnumIdx]; if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { keys.push(prop); } } };
In IE < 9, JScript will skip over any property in any object where there is a same-named property in the object’s prototype chain that has the DontEnum attribute.
在 IE < 9 中,若 object 中的某个属性在它的原形链 (prototype chain)
上有一个同名的,具有 DontEnum 特性的属性,则在 for key in object
枚举时将被忽略。
上述代码就是用来处理这个情形。注意代码中是如何手工判断 obj 是否含有键 prop :
if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { keys.push(prop); }
// An internal function for creating assigner functions. var createAssigner = function(keysFunc, defaults) { return function(obj) { var length = arguments.length; if (defaults) obj = Object(obj); if (length < 2 || obj == null) return obj; for (var index = 1; index < length; index++) { var source = arguments[index], keys = keysFunc(source), l = keys.length; for (var i = 0; i < l; i++) { var key = keys[i]; if (!defaults || obj[key] === void 0) obj[key] = source[key]; } } return obj; }; };
// Extend a given object with all the properties in passed-in object(s). _.extend = createAssigner(_.allKeys);
if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]);
就是起这个作用的。
// Invokes interceptor with the obj, and then returns obj. // The primary purpose of this method is to "tap into" a method chain, in // order to perform operations on intermediate results within the chain. _.tap = function(obj, interceptor) { interceptor(obj); return obj; };
eq = function(a, b, aStack, bStack) { // Identical objects are equal. `0 === -0`, but they aren't identical. // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). if (a === b) return a !== 0 || 1 / a === 1 / b; // A strict comparison is necessary because `null == undefined`. if (a == null || b == null) return a === b; // `NaN`s are equivalent, but non-reflexive. if (a !== a) return b !== b; // Exhaust primitive checks var type = typeof a; if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; return deepEq(a, b, aStack, bStack); };
// Internal recursive comparison function for `isEqual`. deepEq = function(a, b, aStack, bStack) { // Unwrap any wrapped objects. if (a instanceof _) a = a._wrapped; if (b instanceof _) b = b._wrapped; // Compare `[[Class]]` names. var className = toString.call(a); if (className !== toString.call(b)) return false; switch (className) { // Strings, numbers, regular expressions, dates, and booleans are compared by value. case '[object RegExp]': // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. return '' + a === '' + b; case '[object Number]': // `NaN`s are equivalent, but non-reflexive. // Object(NaN) is equivalent to NaN if (+a !== +a) return +b !== +b; // An `egal` comparison is performed for other numeric values. return +a === 0 ? 1 / +a === 1 / b : +a === +b; case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. return +a === +b; } ... };
if (!areArrays) { if (typeof a != 'object' || typeof b != 'object') return false; // 1
// Objects with different constructors are not equivalent, but `Object`s or `Array`s // from different frames are. var aCtor = a.constructor, bCtor = b.constructor; if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && _.isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) { return false; } }
上文代码应是有些 Bug,因为函数并非数组,所以会进入该 if 语句,但由于它们的类型并非 object 所以直接返回 false,即所有函数都不相等。考虑下面的测试用例:
// Assume equality for cyclic structures. The algorithm for detecting cyclic // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
// Initializing stack of traversed objects. // It's done here since we only need them for objects and arrays comparison. aStack = aStack || []; bStack = bStack || []; var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (aStack[length] === a) return bStack[length] === b; }
var template = function(data) { return render.call(this, data, _); };
// Provide the compiled source as a convenience for precompilation. var argument = settings.variable || 'obj'; template.source = 'function(' + argument + '){\n' + source + '}';