前面已引见过了,关于 _
在内部是一个什么样的状况,实在就是定义了一个名字叫做 _
的函数,函数自身就是对象呀,就在 _
上扩大了 100 多种要领。
起个头
接着上一篇文章的内容往下讲,第一个扩大的函数是 each
函数,这和数组的 forEach
函数很像,纵然不是数组,是伪数组,也能够经由过程 call 的体式格局来处置惩罚轮回遍历,forEach
吸收三个参数,且没有返回值,不对原数组发生转变。来看看 each
函数:
_.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;
};
each
函数吸收三个参数,分别是 obj 实行体,回调函数和回调函数的上下文,回调函数会经由过程 optimizeCb 来优化,optimizeCb 没有传入第三个参数 argCount,表明默许是三个,然则假如上下文 context 为空的状况下,就直接返回 iteratee 函数。
isArrayLike
前面已引见过了,差别于数组的 forEach
要领,_
的 each 要领能够处置惩罚对象,只不过要先挪用 _.keys
要领猎取对象的 keys 鸠合。返回值也算是一个特性吧,each 函数返回 obj,而数组的要领,是没有返回值的。
第二个是 map
函数:
_.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;
};
套路都是一样的,既能够处置惩罚数组,又能够处置惩罚对象,然则 map 函数要有一个返回值,无论是数组,照样对象,返回值是一个数组,而且从代码能够看到,重生成了数组,不会对原数组发生影响。
然后就是 reduce
函数,认为引见完这三个就能够招呼神龙了,个中 reduce 分为左和右,以下:
_.reduce = _.foldl = _.inject = createReduce(1);
_.reduceRight = _.foldr = createReduce(-1);
为了削减代码量,就用 createReduce
函数,吸收 1 和 -1 参数:
function createReduce(dir) {
// iterator 函数是实行,在终究效果内里
function iterator(obj, iteratee, memo, keys, index, length) {
for (; index >= 0 && index < length; index += dir) {
var currentKey = keys ? keys[index] : index;
memo = iteratee(memo, obj[currentKey], currentKey, obj);
}
return memo;
}
return function(obj, iteratee, memo, context) {
// 依旧是回调函数,优化
iteratee = optimizeCb(iteratee, context, 4);
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length,
index = dir > 0 ? 0 : length - 1;
// 参数能够疏忽第一个值,但用数组的第一个元素替换
if (arguments.length < 3) {
memo = obj[keys ? keys[index] : index];
index += dir;
}
return iterator(obj, iteratee, memo, keys, index, length);
};
}
createReduce
用闭包返回了一个函数,该函数吸收四个参数,分别是实行数组或对象、回调函数、初始值和上下文,个人认为这里的逻辑有点换杂沓,比方我只需三个参数,有初始值没有上下文,这个好办,然则假如同样是三个参数,我是有上下文,然则没有初始值,就会致使流程出现问题。不过我也没有比较好的处置惩罚办法。
当参数为两个的时刻,初始值没有,就会挪用数组或对象的第一个参数作为初始值,并把指针后移一名(这里用指针,现实上是数组的索引),传入的函数,它有四个参数,这和数组 reduce 要领是一样的。
个人认为,reduce 用来处置惩罚对象,照样有点问题的,比方猎取对象的 keys 值,假如每次猎取的递次都不一样,致使处置惩罚的递次也不一样,那终究的效果还会一样吗?所以我决议处置惩罚对象照样要郑重点好。
_.findKey = function(obj, predicate, context) {
predicate = cb(predicate, context);
var keys = _.keys(obj), key;
for (var i = 0, length = keys.length; i < length; i++) {
key = keys[i];
if (predicate(obj[key], key, obj)) return key;
}
};
_.find = _.detect = function(obj, predicate, context) {
var key;
if (isArrayLike(obj)) {
key = _.findIndex(obj, predicate, context);
} else {
key = _.findKey(obj, predicate, context);
}
if (key !== void 0 && key !== -1) return obj[key];
};
find
也是一个已在数组要领中完成的,对应于数组的 find
和 findIndex
函数。在 _
中,_.findKey
针关于对象,_.findIndex
针关于数组,又略有差别,然则议论和 reduce 的套路是一样的:
function createPredicateIndexFinder(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;
};
}
_.findIndex = createPredicateIndexFinder(1);
_.findLastIndex = createPredicateIndexFinder(-1);
有重点的来看
背面认为有的函数真的是太无聊了,套路都是一致的,细致看了也学不到太多的东西,认为照样有挑选的来聊聊吧。underscore-analysis,这篇博客里的内容写得挺不错的,许多内容都言必有中,预备根据博客中的思绪来解读源码,不盘算一步一步来了,太无聊。
范例推断
jQuery 内里有一个推断范例的函数,就是 $.type
,它最重要的优点就是一个函数能够对所以的范例举行推断,然后返回范例名。_
中的推断略坑,函数许多,而且都是以 is
开首,什么 isArray
,isFunction
等等。
var toString = Object.prototype.toString,
nativeIsArray = Array.isArray;
_.isArray = nativeIsArray || function(obj) {
return toString.call(obj) === '[object Array]';
};
能够看得出来,设计者的心计照样挺细致的,固然,另有:
_.isObject = function(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};
_.isBoolean = function(obj) {
return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
};
isObject
的流程看起来有点和 array、boolean 不一样,然则也是情理之中,很好明白,那末问题来了,如许会不会很贫苦,光组织这些函数就要花良久的时候吧,答案用下面的代码来诠释:
_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {
_['is' + name] = function(obj) {
return toString.call(obj) === '[object ' + name + ']';
};
});
关于一些不必特别处置惩罚的函数,直接用 each
函数来搞定。
除此之外,另有一些有意思的 is
函数:
// 只能用来推断 NaN 范例,由于只需 NaN !== NaN 建立,其他 Number 均不建立
_.isNaN = function(obj) {
return _.isNumber(obj) && obj !== +obj;
};
// null 严厉即是哪些范例?
_.isNull = function(obj) {
return obj === null;
};
// 又是一个严厉推断 ===
// 貌似 _.isUndefined() == true 空参数的状况也是建立的
_.isUndefined = function(obj) {
return obj === void 0;
};
不过关于 isNaN
函数,照样有 bug 的,比方:
_.isNaN(new Number(1)); // true
// new Number(1) 和 Number(1) 是有区分的
这边 github issue 上已有人提出了这个问题,_.isNaN,也合并到分支了 Fixes _.isNaN for wrapped numbers,然则不知道为何我这个 1.8.3 版本照样老样子,难度我下载了一个假的 underscore?issue 中供应了处置惩罚办法:
_.isNaN = function(obj) {
// 将 !== 换成 !=
return _.isNumber(obj) && obj != +obj;
};
我跑去最新宣布的 underscore 下面看了下,近来更新 4 month ago
,搜刮了一下 _.isNaN
:
_.isNaN = function(obj) {
// 真的很机灵,NaN 是 Number 且 isNaN(NaN) == true
// new Number(1) 此次返回的是 false 了
return _.isNumber(obj) && isNaN(obj);
};
来看一眼 jQuery 内里的范例推断:
// v3.1.1
var class2type = {
"[object Boolean]": "boolean",
"[object Number]": "number",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Date]": "date",
"[object RegExp]": "regexp",
"[object Object]": "object",
"[object Error]": "error",
"[object Symbol]": "symbol"
}
var toString = Object.prototype.toString;
jQuery.type = function (obj) {
if (obj == null) {
return obj + "";
}
return
typeof obj === "object" || typeof obj === "function" ?
class2type[toString.call(obj)] || "object" :
typeof obj;
}
比较了一下,发明 jQuery 比拟于 underscore,少了 Arguments
的推断,多了 ES6 的 Symbol
的推断(PS:underscore 良久没人保护了?)。所以 jQuery 对 Arguments 的推断只能返回 object
,_
中是没有 _.isSymbol
函数的。之前一向看 jQuery 的范例推断,居然不知道 Arguments 也能够零丁分为一类 arguments
。另有就是,假如让我来挑选在项目中运用哪一个,我一定挑选 jQuery 的这类体式格局,只管 underscore 更细致,然则函数拆分太多了。
其他有意思的 is 函数
前面说了,underscore 给人一种很烦琐的认为,is
函数太多,话虽如此,总有几个异常有意思的函数:
_.isEmpty = function(obj) {
if (obj == null) return true;
if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;
return _.keys(obj).length === 0;
};
isEmpty
用来推断是不是为空,我刚开始看到这个函数的时刻,有点懵,说究竟照样对 Empty
这个词明白的不够深入。究竟什么是 空 呢,看源码,我认为这是最好的答案,毕竟汇集了那末多优异多 JS 开发者。
一切与
null
相称的元素,都为空,没问题;数组、字符串、Arguments, 它们也能够为空,比方 length 属性为 0 的时刻;
末了,用自带的 _.keys 推断 obj key 鸠合的长度是不是为 0。
有时刻认为看代码,真的是一种升华。
另有一个 isElement
,很简单,只是不明白为何用了两次非来推断:
_.isElement = function(obj) {
// !!
return !!(obj && obj.nodeType === 1);
};
重点来讲下 isEqual
函数:
_.isEqual = function(a, b) {
return eq(a, b);
};
var eq = function(a, b, aStack, bStack) {
// 处置惩罚 0 和 -0 不应当相称的问题?
// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
if (a === b) return a !== 0 || 1 / a === 1 / b;
// 有一个为空,直接返回,但要注重 undefined !== null
if (a == null || b == null) return a === b;
// 假如 a、b 是 _ 对象,返回它们的 warpped
if (a instanceof _) a = a._wrapped;
if (b instanceof _) b = b._wrapped;
var className = toString.call(a);
// 范例差别,直接返回 false
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]':
// 经由过程 '' + a 组织字符串
return '' + a === '' + b;
case '[object Number]':
// +a 能够将范例为 new Number 的 a 转变为数字
// +a !== +a,只能申明 a 为 NaN,推断 b 是不是也为 NaN
if (+a !== +a) return +b !== +b;
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
case '[object Date]':
case '[object Boolean]':
// +true === 1
// +false === 0
return +a === +b;
}
// 假如以上都不能满足,能够推断的范例为数组或对象,=== 是没法处置惩罚的
var areArrays = className === '[object Array]';
// 非数组的状况,看一下它们是不是同先人,差别先人,failed
if (!areArrays) {
// 新鲜的数据
if (typeof a != 'object' || typeof b != 'object') return false;
// 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;
}
}
// 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--) {
// 这个应当是为了防备嵌套轮回
if (aStack[length] === a) return bStack[length] === b;
}
// Add the first object to the stack of traversed objects.
aStack.push(a);
bStack.push(b);
// Recursively compare objects and arrays.
if (areArrays) {
length = a.length;
// 数组长度都不等,一定不一样
if (length !== b.length) return false;
// 递归比较,假如有一个差别,返回 false
while (length--) {
if (!eq(a[length], b[length], aStack, bStack)) return false;
}
} else {
// 非数组的状况
var keys = _.keys(a), key;
length = keys.length;
// 两个对象的长度不等
if (_.keys(b).length !== length) return false;
while (length--) {
key = keys[length];
if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
}
}
// 清空数组
aStack.pop();
bStack.pop();
return true; // 一起究竟,没有失利,则返回胜利
};
总结一下,就是 _.isEqual
内部虽然用的是 ===
这类推断,然则关于 ===
推断失利的状况,isEqual
会尝试将比较的元素拆分比较,比方,假如是两个差别援用地点数组,它们元素都是一样的,则返回 true
:
[22, 33] === [22, 33]; // false
_.isEqual([22, 33], [22, 33]); // true
{id: 3} === {id: 3}; // false
_.isEqual({id: 3}, {id: 3}); // true
NaN === NaN; // false
_.isEqual(NaN, NaN); // true
/a/ === new RegExp('a'); // false
_.isEqual(/a/, new RegExp('a')); // true
能够看得出来,isEqual
是一个异常有心计的函数。
数组去重
关于数组去重,从口试笔试的水平来讲,是粗茶淡饭的问题,现实中也会经经常使用到,前段时候看到一篇去重的博客,认为含金量很高,地点在这:也谈JavaScript数组去重,年代在久一点,就是玉伯大虾的从 JavaScript 数组去重谈机能优化。
_
中也有去重函数 uniq 或许 unique:
_.uniq = _.unique = function(array, isSorted, iteratee, context) {
// 和 jQuery 一样,平移参数
if (!_.isBoolean(isSorted)) {
context = iteratee;
iteratee = isSorted;
isSorted = false;
}
// 又是回调 cb,三个参数
if (iteratee != null) iteratee = cb(iteratee, context);
var result = [];
var seen = [];
for (var i = 0, length = getLength(array); i < length; i++) {
var value = array[i],
computed = iteratee ? iteratee(value, i, array) : value;
// 假如已分列好,就直接和前一个举行比较
if (isSorted) {
if (!i || seen !== computed) result.push(value);
seen = computed;
} else if (iteratee) {
// seen 此时化身为一个去重数组,条件是有 iteratee 函数
if (!_.contains(seen, computed)) {
seen.push(computed);
result.push(value);
}
} else if (!_.contains(result, value)) {
result.push(value);
}
}
return result;
};
照样要从 unique
的几个参数提及,第一个参数是数组,第二个示意是不是已排好序,第三个参数是一个函数,示意对数组的元素举行如何的处置惩罚,第四个参数是第三个参数的上下文。返回值是一个新数组,思绪也很清楚,关于已排好序的数组,用后一个和前一个比拟,不一样就 push 到 result 中,关于没有排好序的数组,要用到 _.contains
函数对 result 是不是包括元素举行推断。
去重的话,假如数组是排好序的,效力会很高,时候复杂度为 n,只需遍历一次轮回马上,关于未排好序的数组,要频仍的运用 contains
函数,复杂度很高,平均为 n 的平方。去重所用到为相称为严厉即是 ===
,运用的时刻要警惕。
_.contains
函数以下所示:
_.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {
if (!isArrayLike(obj)) obj = _.values(obj);
if (typeof fromIndex != 'number' || guard) fromIndex = 0;
return _.indexOf(obj, item, fromIndex) >= 0;
};
_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
_.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
function createIndexFinder(dir, predicateFind, sortedIndex) {
return function(array, item, idx) {
var i = 0, length = getLength(array);
if (typeof idx == 'number') {
if (dir > 0) {
i = idx >= 0 ? idx : Math.max(idx + length, i);
} else {
length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
}
} else if (sortedIndex && idx && length) {
idx = sortedIndex(array, item);
return array[idx] === item ? idx : -1;
}
// 本身都不即是本身,让我想到了 NaN
if (item !== item) {
idx = predicateFind(slice.call(array, i, length), _.isNaN);
return idx >= 0 ? idx + i : -1;
}
for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
// 这里运用的是严厉即是
if (array[idx] === item) return idx; // 找到,返回索引
}
return -1; // 没找到,返回 -1
};
}
总结
认为 Underscore
的源码看起来照样很简单的,Underscore 内里有一些过期的函数,这些都能够拿过来进修,逻辑比较清楚,并不像 jQuery 那样,一个函数内里很多内部函数,看着看着就晕了。
参考
Underscore.js (1.8.3) 中文文档
经常使用范例推断以及一些有效的东西要领
也谈JavaScript数组去重
JavaScript 数组去重
欢迎来我的博客交换。