开头
源码的开头部分是一个立即执行函数, 第一个参数是上下文对象,第二个参数是一个函数,主要代码在这个函数里面。立即执行函数主要是支持多种模式导出模块。
(function(global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define('underscore', factory) :
(function() {
var current = global._;
var exports = factory();
global._ = exports;
exports.noConflict = function() { global._ = current; return exports; };
})();
}(this, (function() {...}))
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :''
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :''
这里判断是不是node环境,如果exports是一个对象并且module存在,CommonJs的导出模块方法:module.exports = factory()。暴露出了一个函数的调用,可以通过这样的方式使用_(obj),factory返回的也是一个函数。可以const _ = require(''underscore);这样来引入,这是通过npm install underscore安装后,node环境可以这样来引入。
typeof define === 'function' && define.amd ? define('underscore', factory) : ''
RequireJS
定义了一个define
函数,用来定义模块
define([id], [dependencies], factory)
参数:
- id:可选,字符串类型,定义模块标识,如果没有提供参数,默认为文件名
- dependencies:可选,字符串数组,AMD 推崇依赖前置,即当前模块依赖的其他模块,模块依赖必须在真正执行具体的
factory
方法前解决 - factory:必需,工厂方法,初始化模块需要执行的函数或对象。如果为函数,它只被执行一次。如果是对象,此对象会作为模块的输出值
如果使用的是RequireJS规范,define是一个函数并且define.amd存在,就需要用define函数来定义模块,导出的模块就是factory函数返回的内容。可以在require的回调函数中访问underscore。
require(['underscore'], function(underscore){})
如果即不是CommonJs规范,又不是RequireJS规范,这里是一个立即执行函数,这里主要是把underscore挂在window上,并解决命名冲突。如果页面中已经定义了_这个变量,这里会重写,将_这个变量释放给underscore使用。并且给underscore上面扩展了一个方法,调用这个方法就可以重新给underscore起一个名字,将window上原来的_变量,还给window。
(function() {
var current = global._;
var exports = factory();
global._ = exports;
exports.noConflict = function() { global._ = current; return exports; };
})();
举例:注意引入顺序
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
var _ = function() {
console.log(1)
}
</script>
<script src="underScore.js"></script>
<script>
const newUnderScore = _.noConflict()
_(); // 1
</script>
</html>
如果不调用 _.noConflict(),_()将不会打印1。
主要内容
主要内容都在factory中,这个函数中最上面的是下面内容:
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
Function('return this')() ||
{};
这里主要目的是将_挂在全局对象上,浏览器环境的全局对象是window,node环境的全局对象是global,但是这里并没有出现window。浏览器内打印一下self,可以看到self指向window。打印一下window.self。
可以看到两者都指向window,self.self === self 也就是说window.self === self。那为什么使用self不是window呢?
是因为Web Worker,在 Web Worker 标准中,定义了解决客户端 JavaScript 无法多线程的问题。其中定义的 “worker” 是指执行代码的并行过程。不过,Web Worker 处在一个自包含的执行环境中,无法访问 Window 对象和 Document 对象,和主线程之间的通信业只能通过异步消息传递机制来实现。虽然在 Web Worker 中不能访问到 Window 对象,但是我们却能通过 self 访问到 Worker 环境中的全局对象。所以完全可以挂到 self 中。
Function('return this')()
上面这句代码又是什么意思呢?浏览器中返回:
上面的代码相当于是这样的,一个立即执行函数,return this。this就是全局对象。那为什么不直接使用下面的代码呢?
(function(){return this})()
来测试一下
console.log( window === function(){
return (function(){return this})();
}() ); // true
console.log( window === function(){
"use strict";
return (function(){return this})();
}() ); // false
console.log( window === function(){
return Function('return this')();
}() ); // true
console.log( window === function(){
"use strict";
return Function('return this')();
}() ); // true
区别就在这里,用下面这种方式,无论是严格模式还是非严格模式,拿到的都是全局对象。
Function('return this')();
微信小程序中,即不能访问window, 也没有global和self,又强制严格模式:
console.log(this) // undefined
console.log(Function('return this')()) // {}
为了防止有些环境中没有全局对象导致root为undefined的报错,最后给root一个{}。
函数式编程
接下来会看到很多个这样的函数,这是第一个函数,也是underscore的主函数,_(obj)调用的就是这个函数,这个函数返回的是一个实例,为什么要返回一个实例呢?
function _(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
}
看一下unique方法的使用
var arr = [2, 4, 3, 4, 2, 3, 4]
console.log(_(arr).unique()) // [2, 4, 3]
console.log(_.unique(arr)) // [2, 4, 3]
一个是函数式调用,一个是面向对象调用,结果都是一样的,怎么实现的呢?
_()返回的是一个实例,_的原型上有 unique方法,所以每个实例都可以使用,_.unique()是_的静态方法,说明这两个方法是同一个方法,不只有这个这一个,很多方法都可以这样来使用,也就是说underscore原型上方法,与函数静态方法共享。_()调用返回实例也就可以理解了。
实现方法:
先省略中间定义的方法和属性如:
var VERSION = _.VERSION = '1.10.2';
.
.
.
// sparse array-likes as if they were dense.
function each(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;
}
.
.
.
直接到最后看下factory返回了什么?
var allExports = ({
'default': _,
VERSION: VERSION,
iteratee: iteratee,
restArguments: restArguments,
each: each,
forEach: each,
map: map,
collect: map,
reduce: reduce,
foldl: reduce,
inject: reduce,
reduceRight: reduceRight,
foldr: reduceRight,
find: find,
detect: find,
filter: filter,
select: filter,
reject: reject,
every: every,
all: every,
some: some,
any: some,
contains: contains,
includes: contains,
include: contains,
invoke: invoke,
pluck: pluck,
where: where,
findWhere: findWhere,
max: max,
min: min,
shuffle: shuffle,
sample: sample,
sortBy: sortBy,
groupBy: groupBy,
indexBy: indexBy,
countBy: countBy,
toArray: toArray,
size: size,
partition: partition,
first: first,
head: first,
take: first,
initial: initial,
last: last,
rest: rest,
tail: rest,
drop: rest,
compact: compact,
flatten: flatten,
without: without,
uniq: uniq,
unique: uniq,
union: union,
intersection: intersection,
difference: difference,
unzip: unzip,
zip: zip,
object: object,
findIndex: findIndex,
findLastIndex: findLastIndex,
sortedIndex: sortedIndex,
indexOf: indexOf,
lastIndexOf: lastIndexOf,
range: range,
chunk: chunk,
bind: bind,
partial: partial,
bindAll: bindAll,
memoize: memoize,
delay: delay,
defer: defer,
throttle: throttle,
debounce: debounce,
wrap: wrap,
negate: negate,
compose: compose,
after: after,
before: before,
once: once,
keys: keys,
allKeys: allKeys,
values: values,
mapObject: mapObject,
pairs: pairs,
invert: invert,
functions: functions,
methods: functions,
extend: extend,
extendOwn: extendOwn,
assign: extendOwn,
findKey: findKey,
pick: pick,
omit: omit,
defaults: defaults,
create: create,
clone: clone,
tap: tap,
isMatch: isMatch,
isEqual: isEqual,
isEmpty: isEmpty,
isElement: isElement,
isArray: isArray,
isObject: isObject,
isArguments: isArguments,
isFunction: isFunction,
isString: isString,
isNumber: isNumber,
isDate: isDate,
isRegExp: isRegExp,
isError: isError,
isSymbol: isSymbol,
isMap: isMap,
isWeakMap: isWeakMap,
isSet: isSet,
isWeakSet: isWeakSet,
isFinite: isFinite,
isNaN: isNaN,
isBoolean: isBoolean,
isNull: isNull,
isUndefined: isUndefined,
has: has,
identity: identity,
constant: constant,
noop: noop,
property: property,
propertyOf: propertyOf,
matcher: matcher,
matches: matcher,
times: times,
random: random,
now: now,
escape: escape,
unescape: unescape,
result: result,
uniqueId: uniqueId,
templateSettings: templateSettings,
template: template,
chain: chain,
mixin: mixin
});
// Add all of the Underscore functions to the wrapper object.
var _$1 = mixin(allExports);
// Legacy Node.js API
_$1._ = _$1;
return _$1;
_$1就是它暴露出来的方法,allExports上所有的属性和方法,都被扩展到了_$1的自身和原型上,而扩展的方法就是mixin,所以核心方法就是这个mixin。
mixin
源码中mixin方法返回了_,这个_已经把obj传递进来的属性和方法全部挂在了原型和自身上。
// Add your own custom functions to the Underscore object.
function mixin(obj) {
each(functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return chainResult(this, func.apply(_, args));
};
});
return _;
}
这里调用了each方法,functions方法和chainResult方法,先来看前两个
each
这里是一个简化的each方法,接受一个数组,和一个迭代器,迭代器接受数组成员和index。那么调用functions(obj)返回的应该就是一个数组。
_.each = function(target, callbacks) {
var key , i = 0;
if(_.isArray(target)) {
for(; i < target.length; i++) {
callbacks.call(target, target[i], i);
}
} else {
for(key in target) {
callbacks.call(target, key, target[key])
}
}
}
functions
函数接受一个对象,返回一个数组,数组是对象的key,value是函数的key才会被加进来,所以原型和自身上扩展的是方法,过滤掉了属性。
// Return a sorted list of the function names available on the object.
function functions(obj) {
var names = [];
for (var key in obj) {
if (isFunction(obj[key])) names.push(key);
}
return names.sort();
}
方法添加到自身:
var func = _[name] = obj[name];
方法添加到了原型,这里为什么不直接像上面那样简单的写呢?
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return chainResult(this, func.apply(_, args));
};
还记得上面的面向对象吗?这样调用直接把参数arr传递给了_,并没有给到unique()。而unique()内部却可以访问这个参数。
console.log(_(arr).unique()) // [2, 4, 3]
_函数把参数存在了this._wrapped上面,可以通过这个拿到,但是有时又可以这样调用_.unique(arr)。所以我们还需要获取到arguments。
function _(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
}
push.apply(args, arguments);在underscore中就是Array.prototype.push(args, arguments );
拿到参数并合并为数组。
function test() {
console.log(arguments)
var args = [undefined]
Array.prototype.push.apply(args, arguments)
console.log(args) // [ undefined, { a: 9 }, { b: 3 } ]
}
test({ a: 9 }, { b: 3 })
传递参数
return chainResult(this, func.apply(_, args));
这里返回了一个函数调用的结果,使用的是链式调用
链式调用
var arr = [2, 4, 3, 4, 2, 3, 4]
var a = _.unique(arr);
console.log(a) // [2,4,3]
var a = _(arr).each(function(item, index) {
console.log(item) // 2, 4, 3, 4, 2, 3, 4
});
链式操作:
var arr = [2, 4, 3, 4, 2, 3, 4]
var a = _(arr).unique().each(function(item) {
console.log(item)
})
console.log(a)
报错了,说明不能直接使用链式掉用,需要用一个chain开启链式调用:
var arr = [2, 4, 3, 4, 2, 3, 4]
var a = _(arr).chain().unique().each(function(item) {
console.log(item) // 2,4,3
})
console.log(a)
这里是a的打印结果: 返回的是一个实例
对比直接调用underScore返回的也是一个实例,但是没有_chain属性:
var arr = [2, 4, 3, 4, 2, 3, 4]
var a = _(arr)
console.log(a)
如果这样来使用:可以看到返回的不再 是一个实例,而是一个数组,这种情况下肯定是不能进行链式调用的。
var arr = [2, 4, 3, 4, 2, 3, 4]
var a = _.unique(arr);
console.log(a) // [2,4,3]
由上面分析得出,之所以可以进行链式调用,关键就在chain函数,源码中chain函数很简单:它只是返回一个实例,并且在实例上增加一个属性_chain = true。这样好像也并不能实现链式调用。
// Add a "chain" function. Start chaining a wrapped Underscore object.
function chain(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
}
上面的mixin方法中,原型上扩展方法的最后,这里返回的是调用了 chainResult函数返回的结果,
return chainResult(this, func.apply(_, args));
看下 chainResult函数返回了什么?instance是当前实例,obj就是实例上函数返回的结果,也就是调用unique函数的返回值,[2,4,3]
function chainResult(instance, obj) {
return instance._chain ? _(obj).chain() : obj;
}
如果_chain是true,说明开启了链式调用,又去调用了_([2,4,3]),返回了一个实例,实例可以接着调用原型上的方法。如果没有开启链式调用,直接将[2,4,3]返回。
也就是说,如果开启了链式调用,返回结果一直是实例,如果没有开启,返回结果直接是处理参数后的返回值。
平时简单的实现链式调用,
var jQuery = {
css: function() {
console.log(1) // 1
return this
},
style: function() {
console.log(2) // 2
return this
}
}
jQuery.css().style()
每一个方法都返回this,就可以直接进行链式调用,但是underScore原型上所有的方法都不是return this。而是同过chain 和chainResult方法,动态的返回实例或者参数处理后的结果。
// obj要处理的数据源,可以是对象,可以是数组,iteratee迭代器函数,context
_.map = function(obj, iteratee, context) {
// 初始化迭代器,生成不同功能的迭代器
var iteratee = cb(iteratee, context);
// 如果是对象,我们需要找出属性的个数
var keys = !_.isArray(obj) && Object.keys(obj);
var length = (keys || obj).length;
var result = Array(length);
for(var index = 0; index < length; index++) {
var currentKey = keys ? keys[index] : index;
result[index] = iteratee(obj[currentKey], index, obj)
}
return result;
}
判断是不是数组的函数
_.isArray = function(obj) {
return {}.toString.call(obj) === '[object Array]'
}
如果给map函数传递了迭代器,就返回该迭代器函数,如果没有传递,就返回一个初始化的迭代器
var cb = function(iteratee, context, count) {
if(iteratee == null) {
return _.identify;
}
if(_.isFunction(iteratee)) {
return optimizeCb(iteratee, context, count)
}
}
初始化迭代器
// 这是一个默认迭代器 没有对参数做任何的处理
_.identify = function(value) {
return value
}
这个 optimizeCb方法是写在underScore内部使用的,不对外暴露接口,根据传递的参数不通,返回不通的迭代器。如果传递了context参数,也就是要给迭代器制定上下文this,所以就需要用call来调用改变this指向。switch中的内容,是为了改变this指向的。如果没有传递,就默认返回该函数。这里面count为4的情况是为后面的 reduce函数提供服务的,这里先不用关注。count代表迭代器函数能接受到的参数的个数。一般默认是3个,value,index,和数据源本身。
var optimizeCb = function (func, context, count) {
if(context == void 0) {
return func;
}
switch (count == null ? 3 : count) {
case 1:
return function(value) {
return func.call(context, value)
};
case 3:
return function(value, index ,obj) {
return func.call(conrtext,value, index, obj)
};
case 4:
return function(memo, value, index, obj) {
func.call(context, memo, value, index, obj)
}
}
}
5 reduce方法
用法: 可以看到memo就是每一次迭代的返回值,重新传递给下一个迭代。就实现了累加的功能。
console.log(_.reduce([3,4,5,4,3,2], function(memo,value) {
return memo + value
}, 0)) // 21
reduce是基于内部的一个createReduce函数来实现的。这个方法是在内部调用的,默认传递了1,就是说从0开始,-1是从最后一位开始。
_.reduce = creatReducer(1);
// Create a reducing function iterating left or right.
var createReduce = function(dir) {
// Wrap code that reassigns argument variables in a separate function than
// the one that accesses `arguments.length` to avoid a perf hit. (#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;
};
return function(obj, iteratee, memo, context) {
var initial = arguments.length >= 3;
return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
};
};
4 rest参数
通过_.restArguments方法传递一个函数,
es6用...变量名的方式获取函数的参数,并以数组的形式存储,我们想要达到的效果,掉用_.restArguments方法,返回一个函数,使得该函数支持rest参数,并以数组的形式传递
var test = function(a, rest) {
console.log(rest) // [3, 4, 4, 5, 4]
}
var rest = _.restArguments(test);
rest(1,3,4,4,5,4)
_.restArguments = function(func) {
// func.length是函数行参的个数, startIndex除rest以外的参数个数
var startIndex = func.length - 1;
return function() {
// 计算rest数组的长度
var length = arguments.length - startIndex,
rest = Array(length),
index = 0;
for(; index < length; index++) {
rest[index] = arguments[index+startIndex]
}
// func函数的参数个数
var args = Array(startIndex+1);
for(index = 0; index < startIndex; index++) {
args[index] = arguments[index]
}
args[startIndex] = rest
return func.apply(this, args)
}
}
偏函数(partial)反映了新函数是原函数的一部分,underScore的_.partial方法 返回的就是一个偏函数。