Underscore.js 源码学习笔记(下)

时间:2023-10-25 17:12:08

上接 Underscore.js 源码学习笔记(上)

=== 756 行开始 函数部分。

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;
}; _.bind = restArguments(function(func, context, args) {
if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
var bound = restArguments(function(callArgs) {
return executeBound(func, bound, context, this, args.concat(callArgs));
});
return bound;
});

_.bind(func, context, args)  就是将 func 的 this 绑定到 context 并且预先传入参数 args (柯里化)

通过  args.concat(callArgs)  实现了柯里化

bound 是绑定 this 后的函数,func 是传入的函数

if (!(callingContext instanceof boundFunc))  如果 callingContext 不是 boundFunc 的实例 就通过 apply 实现指定函数运行的 this

如果 callingContext 是 boundFunc 的实例,那意味着你可能是这么使用的

function foo() {}
var bindFoo = _.bind(foo, context/*没写定义,随便什么东西*/);
var bindFooInstance = new bindFoo();

此时 bindFoo() 的 this 就是 bindFoo 的一个实例

那么 bindFooInstance 的 this 是应该绑定到 context 还是 bindFoo 的实例还是什么呢?

JavaScript 中 this 一共有四种绑定 默认绑定 < 隐式绑定 < 显示绑定 < new绑定

所以 这里应该优先使用... foo 的实例

思考一下嘛 如果是 ES5 中 new foo.bind(context) 是不是应该先使用 foo 的实例嘛 bound 只是一个中间函数

然后就是判断 foo 是否有返回值 有的话直接返回该值 否则返回 this 也是操作符 new 的规定

_.partial = restArguments(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 = _;
// e.g.
function add(a, b) { return a + b; }
var addOne = _.partial(add, 1, _);
addOne(3); //

默认占位符是 _ 先给函数指定部分参数 不指定的就用下划线占位 生成一个新的只需要填写剩余参数的函数

_.bindAll = restArguments(function(obj, keys) {
keys = flatten(keys, false, false);
var index = keys.length;
if (index < 1) throw new Error('bindAll must be passed function names');
while (index--) {
var key = keys[index];
obj[key] = _.bind(obj[key], obj);
}
});
// e.g.
var obj = {
name: 'xiaoming',
age: '25',
getName() {
return this.name;
},
getAge() {
return this.age;
},
sayHello() {
return 'hello, I am ' + this.name + ' and i am ' + this.age + ' years old.';
}
}
name = 'global name';
_.bindAll(obj, 'getName', 'getAge');
var getName = obj.getName, getAge = obj.getAge, sayHello = obj.sayHello;
getName(); // xiaoming
getAge(); //
sayHello(); // hello, I am global name and i am undefined years old.

把一个对象的指定方法绑定到该对象。keys 可以是要绑定的函数数组或函数。

_.memoize = function(func, hasher) {
var memoize = function(key) {
var cache = memoize.cache;
var address = '' + (hasher ? hasher.apply(this, arguments) : key);
if (!has(cache, address)) cache[address] = func.apply(this, arguments);
return cache[address];
};
memoize.cache = {};
return memoize;
};

这个函数还是简单实用的,通过缓存一个变量 cache 当传入相同的参数时直接返回上一次的结果即可。

hasher 是入参的哈希函数,来判断多次入参是否相同。如果不传哈希函数的话,默认就用第一个参数判断是否重复。所以如果入参不是只有一个的话,记得传 hasher 函数。

比如在计算斐波那契数列  fib(n) = fib(n - 1) + fib(n - 2) 可以通过记忆化递归防止大量重复计算。

_.delay = restArguments(function(func, wait, args) {
return setTimeout(function() {
return func.apply(null, args);
}, wait);
});

封装了一个函数,每次调用时都要等待 wait 毫秒再执行。

_.defer = _.partial(_.delay, _, 1);

通过 _.defer 来执行函数 _.defer(log) 可以使函数放到异步调用队列中,防止一些奇怪的错误吧。(确实遇到了一些时候需要  setTimeout(()=>{...}, 0)  来执行函数才有效的情况,但是还不知道怎么总结规律= =)

// 在指定时间间隔 wait 内只会被执行一次
// 在某一时间点 函数被执行 那么之后 wait 时间内的调用都不会被立即执行 而是设置一个定时器等到间隔等于 wait 再执行
// 如果在计时器等待的时间又被调用了 那么定时器将执行在等待时间内的最后一次调用
// options 有两个字段可填 { leading: false } 或 { trailing: false }
// { leading: false } 表示调用时不会立即执行 而是等待 wait 毫秒之后执行
// { trailing: false } 表示执行之后的 wait 时间内的调用都忽略掉
// 不要同时设置这两个字段
_.throttle = function(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {}; // later 函数是定时器指定执行的函数 context, args 不是设置定时器时指定的 而是执行 later 时决定的
var later = function() {
// 如果 options.leading = false 的话就将 previous 设置为 0 作为标记 下一次执行 func 时就不会被立即执行了
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
// 这里判断 !timeout 真的好迷啊...
if (!timeout) context = args = null;
}; var throttled = function() {
var now = _.now();
// 如果没有上一次调用 或者 之前的调用已经结束 且 leading = false 会设置 previous = 0
// previous = 0 且 options.leading = false 说明上一次 func 执行完成 此次的 fun 不需要立即执行 等 wait ms 再执行
if (!previous && options.leading === false) previous = now;
// 根据当前时间和上一次调用的时间间隔与 wait 比较判断
var remaining = wait - (now - previous);
context = this; // 注意每一次调用都会更新 context 和 args 而执行 later 用到的是这两个参数
args = arguments; // 也就是说设置定时器时对应的参数 不一定是执行对应的参数~
// remaining <= 0 则证明距离上次调用间隔大于 wait 了 可以被执行
// 理论上 remaining > wait 不会存在 除非 now < previous 也就是系统时间出错了(被修改了
if (remaining <= 0 || remaining > wait) {
// 当设置了 leading 是不会进入这个分支的= =
// 删除定时器 重置 previous 为当前时间 并执行 func
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
}
// 否则如果有 timeout 证明隔一段已经设置一段时间后执行 不再设置定时器 // 间隔小于 wait 而且没有 timeout 的话 就设置一个定时器 到指定时间间隔后再执行
// 如果 options.trailing = false 则忽略这次调用 因为时间间隔在 timeout 之内
else if (!timeout && options.trailing !== false) {
// 设置了 trailing 不会进入这个分支
timeout = setTimeout(later, remaining);
}
return result;
};
// 重置 throttled 的状态 同时取消还没有执行的定时器
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
}; return throttled;
};
// e.g.
function log(sth) {
console.log('===> ' + sth + ' ' + new Date().toLocaleTimeString());
}
var tLog = _.throttle(log, 1000);
// === start === 20:29:54
// ===> 1 20:29:54
// ===> 4 20:29:55
var tLog = _.throttle(log, 1000, { leading: false });
// === start === 20:30:15
// ===> 4 20:30:16
var tLog = _.throttle(log, 1000, { trailing: false });
// === start === 20:30:39
// ===> 1 20:30:39
// 不要同时设置 leading 和 trailing ~ 否则永远都不会被执行
// var tLog = _.throttle(log, 1000, { leading: false, trailing: false }); console.log('=== start === ' + new Date().toLocaleTimeString());
tLog(1);
tLog(2);
tLog(3);
tLog(4);

经典的函数来了= =

被称作节流函数 作用是在一定时间范围内只会被调用一次 即使被多次触发

_.debounce = function(func, wait, immediate) {
var timeout, result; var later = function(context, args) {
timeout = null;
if (args) result = func.apply(context, args);
}; var debounced = restArguments(function(args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
// 虽然有 timeout 但是这里的 later 没有传参所以不会执行 func
// 只是为了标记之后的 wait 时间内都不会再执行函数
// 如果等待的过程中又被调用 那么就从那个时间点开始再进行 wait 时间的不执行
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = _.delay(later, wait, this, args);
} return result;
}); debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
}; return debounced;
};

debounce 防抖函数 只有当隔指定时间没有重复调用该函数时才会执行,可应用于输入和页面滑动等情况

可以分成两种情况看 传 immediate 和不传 immediate

不传 immediate 的话 就是调用后设置定时器 wait 秒之后执行 这中间又被调用 那么从调用时刻开始重新计时

传 immediate 表示第一次调用就会被执行 然后标记之后的 wait ms 内不会被执行 这中间又被调用 那么从调用时刻开始重新计时

// _.partial(wrapper, func) 是预先给 wrapper 传入参数 func
// 所以 _.wrap(func, wrapper) 就是 返回 wrapper 先传入 func 后返回的函数
_.wrap = function(func, wrapper) {
return _.partial(wrapper, func);
};
// e.g.
function func(name) {
return 'hi ' + name;
}
function wrapper(func, ...args) {
return func(args).toUpperCase();
}
var sayHi = _.wrap(func, wrapper);
sayHi('saber', 'kido'); // HI SABER,KIDO
_.compose = function() {
var args = arguments;
var start = args.length - 1;
return function() {
var i = start;
// 从最后一个函数开始执行
var result = args[start].apply(this, arguments);
// 每一个函数的入参是上一个函数的出参
while (i--) result = args[i].call(this, result);
return result;
};
};
// e.g.
function getName(firstname, lastname) { return firstname + ' ' + lastname; }
function toUpperCase(str) { return str.toUpperCase(); }
function sayHi(str) { return 'Hi ' + str; }
_.compose(sayHi, toUpperCase, getName)('wenruo', 'duan'); // Hi WENRUO DUAN

我记得之前写过这个函数啊= =但是没找到 记忆出错了

就是一个把一堆函数从右到左连起来执行的函数。函数式编程中很重要的函数。

_.after = function(times, func) {
return function() {
if (--times < 1) {
return func.apply(this, arguments);
}
};
};
// e.g.
function ajax(url, fn) {
console.log(`获取 ${url} 资源...`);
setTimeout(() => {
console.log(`获取 ${url} 资源完成`);
fn();
}, Math.random() * 1000);
}
function finish() {
console.log('资源全部获取完成 可以进行下一步操作...');
} var urls = ['urla', 'urlb', 'urlc'];
var finishWithAfter = _.after(urls.length, finish); for (var i = 0; i < urls.length; i++) {
ajax(urls[i], finishWithAfter);
}
// 获取 urla 资源...
// 获取 urlb 资源...
// 获取 urlc 资源...
// 获取 urla 资源完成
// 获取 urlc 资源完成
// 获取 urlb 资源完成
// 资源全部获取完成 可以进行下一步操作...

函数调用 times 遍才会被执行

_.before = function(times, func) {
var memo;
return function() {
if (--times > 0) {
memo = func.apply(this, arguments);
}
if (times <= 1) func = null;
return memo;
};
}; // 调用前 times-1 次执行 之后每一次都返回之前的运行的值
var foo = _.before(3, _.identity); console.log(foo(1)) //
console.log(foo(2)) //
console.log(foo(3)) // 2 (第 n 次开始调用不再执行 func 直接返回上一次的结果
console.log(foo(4)) //

只有前 times-1 次执行传入的函数 func 后面就直接返回上一次调用的值。

_.once = _.partial(_.before, 2);

就是只有一次调用的时候会只执行,后面直接返回之前的值。

使用场景比如……单例模式?

_.restArguments = restArguments;

将 restArguments 函数导出。

969行===下面是对象相关的函数了

// 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);
}
}
};

IE9一下浏览器有bug就是一些属性重写后 不能在 for ... in 中遍历到,所以要单独判断。

_.keys = function(obj) {
if (!_.isObject(obj)) return [];
if (nativeKeys) return nativeKeys(obj);
var keys = [];
for (var key in obj) if (has(obj, key)) keys.push(key);
// Ahem, IE < 9.
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};

如果ES5的  Object.keys 存在就直接调用,否则通过 for..in 获取所有的属性。

_.allKeys = function(obj) {
if (!_.isObject(obj)) return [];
var keys = [];
for (var key in obj) keys.push(key);
// Ahem, IE < 9.
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};

获取对象的所有属性,包括原型链上的。

_.values = function(obj) {
var keys = _.keys(obj);
var length = keys.length;
var values = Array(length);
for (var i = 0; i < length; i++) {
values[i] = obj[keys[i]];
}
return values;
};

所有对象自有属性的值的集合

_.mapObject = function(obj, iteratee, context) {
iteratee = cb(iteratee, context);
var keys = _.keys(obj),
length = keys.length,
results = {};
for (var index = 0; index < length; index++) {
var currentKey = keys[index];
results[currentKey] = iteratee(obj[currentKey], currentKey, obj);
}
return results;
};
// e.g.
var _2camel = str => str.replace(/_(\w)/g, (item, letter) => letter.toUpperCase());
var obj = { first: 'mo_li_xiang_pian', second: 'yong_ren_zi_rao' };
_.mapObject(obj, _2camel); // { first: 'moLiXiangPian', second: 'yongRenZiRao' }

对对象中每一个值执行 iteratee 函数,和 _.map 的区别是它返回的是对象。

_.pairs = function(obj) {
var keys = _.keys(obj);
var length = keys.length;
var pairs = Array(length);
for (var i = 0; i < length; i++) {
pairs[i] = [keys[i], obj[keys[i]]];
}
return pairs;
};

返回一个数组,每一项都是键、值组成的数组。

_.invert = function(obj) {
var result = {};
var keys = _.keys(obj);
for (var i = 0, length = keys.length; i < length; i++) {
result[obj[keys[i]]] = keys[i];
}
return result;
};

对象的键值互换,值要变成建,所以确保值是可序列化的。

_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

对象中所有属性值为函数的属性名的集合按照字典序排序后返回。

var createAssigner = function(keysFunc, defaults) { // [defaults] {Boolean}
return function(obj) {
var length = arguments.length;
if (defaults) obj = Object(obj); // 把 obj 转成对象
if (length < 2 || obj == null) return obj;
for (var index = 1; index < length; index++) {
var source = arguments[index],
keys = keysFunc(source), // keysFunc 是获取对象指定的 key 集合的函数
l = keys.length;
for (var i = 0; i < l; i++) {
var key = keys[i];
// 如果设置 defaults 则只有在在当前对象没有 key 属性的时候 才添加 key 属性
// 否则就为 obj 添加 key 属性 存在就替换
if (!defaults || obj[key] === void 0) obj[key] = source[key];
}
}
return obj;
};
}; _.extend = createAssigner(_.allKeys);
// _.extend(obj, ...otherObjs)
// 把 otherObjs 上面的所有的属性都添加到 obj 上 相同属性后面会覆盖前面的 _.extendOwn = _.assign = createAssigner(_.keys);
// _.extendOwn(obj, ...otherObjs)
// 把 otherObjs 上面的所有的自有属性都添加到 obj 上 相同属性后面会覆盖前面的 _.defaults = createAssigner(_.allKeys, true);
// _.extend(obj, ...otherObjs)
// 对 otherObjs 上面的所有的属性 如果 obj 不存在相同属性名的话 就添加到 obj 上 相同属性后面被忽略

扩展对象的一些函数。

var keyInObj = function(value, key, obj) {
return key in obj;
}; _.pick = restArguments(function(obj, keys) {
// 通过 restArguments 传入的参数除了第一个都被合成了一个数组 keys
var result = {}, iteratee = keys[0];
if (obj == null) return result;
if (_.isFunction(iteratee)) {
// 如果 iteratee (keys[0]) 是一个函数
// 可以看做是 _.pick(obj, iteratee, context)
// obj 中符合 iteratee(value, key, obj) 的键值对被返回
if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]);
keys = _.allKeys(obj);
} else {
// 如果 iteratee (keys[0]) 不是函数
// 将 keys 数组递归压平 成为一个新数组 keys
// 对于 obj 中的属性在 keys 中的键值对被返回
iteratee = keyInObj;
keys = flatten(keys, false, false);
obj = Object(obj);
}
for (var i = 0, length = keys.length; i < length; i++) {
var key = keys[i];
var value = obj[key];
if (iteratee(value, key, obj)) result[key] = value;
}
return result;
});

筛选对象中部分符合条件的属性。

_.omit = restArguments(function(obj, keys) {
var iteratee = keys[0], context;
if (_.isFunction(iteratee)) {
iteratee = _.negate(iteratee);
if (keys.length > 1) context = keys[1];
} else {
keys = _.map(flatten(keys, false, false), String);
iteratee = function(value, key) {
return !_.contains(keys, key);
};
}
return _.pick(obj, iteratee, context);
});

逻辑同上,相当于反向 pick 了。

_.create = function(prototype, props) {
var result = baseCreate(prototype);
if (props) _.extendOwn(result, props);
return result;
};

给定原型和属性创建一个对象。

_.clone = function(obj) {
if (!_.isObject(obj)) return obj;
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};

浅克隆一个对象。

看到  _.tap 有点没看懂,感觉事情有点不简单……于是向下翻到了 1621 行,看到这有一堆代码……

首先一开始的时候 (42行) 我们看过   _  的定义,_ 是一个函数,_(obj) 返回一个 _ 实例,该实例有一个 _wrapped 属性是传入的 obj 。

我们上面的函数都是 _ 的属性,所以 _(obj) 中是没有这些属性的(_.prototype 中的属性才能被获得)

// chain 是一个函数 传入一个对象 obj 返回一个下划线的实例,该实例有一个 _wrapped 属性为 obj 同时有 _chain 属性为 true 标记此对象用于链式调用
_.chain = function(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
}; // 返回链式结果 如果当前实例就有 _chain 则将结果包装成链式对象返回 否则就直接返回对象本身
var chainResult = function(instance, obj) {
return instance._chain ? _(obj).chain() : obj;
}; // 将对象 obj 中的函数添加到 _.prototype
_.mixin = function(obj) {
// 对于 obj 中每一为函数的属性
_.each(_.functions(obj), function(name) {
// 都将该属性赋值给下划线
var func = _[name] = obj[name];
// 同时在下划线的原型链上挂这个函数 同时这个函数可以支持链式调用
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
// 将 this._wrapped 添加到 arguments 最前面传入 func
// 因为 this._wrapped 就是生成的一个下划线实例的原始的值
// func 运行的 this 是 _ 把 this._wrapped 也就是上一个链式函数的运行结果 传入 func
// 将 this 和 func 的返回值传入 chainResult
// 如果 this 是一个链式对象(有 _chain 属性)就继续返回链式对象
// 否则直接返回 obj
return chainResult(this, func.apply(_, args));
};
});
return _;
}; // Add all of the Underscore functions to the wrapper object.
// 将 _ 传入 mixin
// 下划线上每一个函数都会被绑定到 _.prototype 这样这些函数才能被实例访问
_.mixin(_); // Add all mutator Array functions to the wrapper.
// 把一些数组相关的函数也加到 _.prototype
_.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = ArrayProto[name];
_.prototype[name] = function() {
var obj = this._wrapped;
method.apply(obj, arguments);
if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
return chainResult(this, obj);
};
}); // Add all accessor Array functions to the wrapper.
_.each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name];
_.prototype[name] = function() {
return chainResult(this, method.apply(this._wrapped, arguments));
};
}); // 从一个含有链式的 _ 实例中获取本来的值
_.prototype.value = function() {
return this._wrapped;
};

在 _.prototype 上添加一个函数,同时支持链式调用。惊叹于其实现的巧妙。

现在可以继续看 _.tap 作用就是插入一个链式调用中间,查看中间值。

_.tap = function(obj, interceptor) {
interceptor(obj);
return obj;
};
// e.g.
let obj = [1, 2, 3];
let interceptor = (x) => { console.log('中间值是:', x) }
let result = _(obj).chain().map(x => x * x).tap(interceptor).filter(x => x < 5).max().value();
// [1,2,3] [1,4,9] 打印中间值 [1,4] 取最大值 4
// .value() 就是从 _ 实例 这里是 { [Number: 4] _wrapped: 4, _chain: true } 获取本来的数据
console.log(result);
// 中间值是: [ 1, 4, 9 ]
//

通过例子可以感受的更清晰。 接下来_.isMatch 前面看过了,略。

// Internal recursive comparison function for `isEqual`.
var eq, deepEq;
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;
// `null` or `undefined` only equal to itself (strict comparison).
if (a == null || b == null) return false;
// `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;
case '[object Symbol]':
return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
} var areArrays = className === '[object Array]';
if (!areArrays) {
// 如果不是数组也不是对象的话 其他情况都已经比较完了 所以一定是 false
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.
// 如果都是自定义类型的实例 都有 constructor 的话 那么构造函数一定要相等
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.
// 比较 stack 是为了防止对象的一个属性是对象本身这种情况
// let obj = {}; obj.prop = obj;
// 这种情况下比较对象再比较对象的每一个属性 就会发生死循环
// 所以比较到每一个属性的时候都要判断和之前的对象有没有相等的
// 如果相等的话 就判断另一个对象是不是也这样 来判断两个对象是否相等
// 而不需要继续比较下去了~ 是不是很巧妙~
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;
} // Add the first object to the stack of traversed objects.
aStack.push(a);
bStack.push(b); // Recursively compare objects and arrays.
if (areArrays) {
// 如果是数组的话 需要比较其每一项都相等
// Compare array lengths to determine if a deep comparison is necessary.
length = a.length;
if (length !== b.length) return false;
// Deep compare the contents, ignoring non-numeric properties.
while (length--) {
if (!eq(a[length], b[length], aStack, bStack)) return false;
}
} else {
// 如果是对象的话 需要比较其每一个键都相等 对应的值再深度比较
// Deep compare objects.
var keys = _.keys(a), key;
length = keys.length;
// Ensure that both objects contain the same number of properties before comparing deep equality.
if (_.keys(b).length !== length) return false;
while (length--) {
// Deep compare each member
key = keys[length];
if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
}
}
// Remove the first object from the stack of traversed objects.
// 讨论一个为什么要出栈 这个有点像 dfs 哈
// obj = { a: { a1: ... }, b: { b1: ... } }
// 判断属性 a 的时候栈里是 [obj] 然后判断 a != obj
// 接下来会递归判断 a1 以及其下属性
// 到 a1 的时候 栈中元素为 [obj, a]
// 当属性 a 被判断完全相等后 需要继续比较 b 属性
// 当比较到 b 的时候 栈中应该是 [obj] 而不是 [obj, a]
// a == b 不会造成死循环 我们不需要对不是父子(或祖先)关系的属性进行比较
// 综上 这里需要出栈(大概没讲明白...反正我明白了...
aStack.pop();
bStack.pop();
return true;
}; // Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) {
return eq(a, b);
};

深度比较两个对象是否相等。我已经开始偷懒了,英文有注释的地方不想翻译成中文了。

虽然很长,但是真的,考虑的很全面。

_.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;
};

判断一个值是否为空。为 null、undefined、长度为空的(类)数组、空字符串、没有自己可枚举属性的对象。

_.isElement = function(obj) {
return !!(obj && obj.nodeType === 1);
};

判断一个值是否是 DOM 元素。

nodeType 属性返回节点类型。

如果节点是一个元素节点,nodeType 属性返回 1。

如果节点是属性节点, nodeType 属性返回 2。

如果节点是一个文本节点,nodeType 属性返回 3。

如果节点是一个注释节点,nodeType 属性返回 8。

该属性是只读的。

_.isArray = nativeIsArray || function(obj) {
return toString.call(obj) === '[object Array]';
}; // Is a given variable an object?
_.isObject = function(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};

isArray 判断一个值是否是数组

isObject 判断对象是否是 object 或 function 注意判断 null

_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error', 'Symbol', 'Map', 'WeakMap', 'Set', 'WeakSet'], function(name) {
_['is' + name] = function(obj) {
return toString.call(obj) === '[object ' + name + ']';
};
});

批量增加一些判断类型的函数,逻辑和 isArray 一样呀。Map WeakMap Set WeakSet 都是 ES6 新增的数据类型。WeakSet 和 WeakMap 都没听过。该补习一波了~~~

if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
return has(obj, 'callee');
};
}

一开始看到的,这个文件就是一个大的IIFE所以会有 arguments ,在 IE 低版本有 bug 不能通过

Object.prototype.toString.apply(arguments) === '[object Arguments]'

来判断。callee 是 arguments 对象的一个属性。可以通过该属性来判断。

都 8102 年了 放过 IE 不好吗?Edge 都开始使用 Chromium 内核了~~~~

// Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,
// IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236).
var nodelist = root.document && root.document.childNodes;
if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') {
_.isFunction = function(obj) {
return typeof obj == 'function' || false;
};
}

优化 isFunction 因为在一些平台会出现bug 看了下提到的 issue #1621 (https://github.com/jashkenas/underscore/issues/1621)也不是很明白……

反正我试了下 nodejs v8 和最新版 Chrome 都进入了这个分支……emmm不管了……

// Is a given object a finite number?
_.isFinite = function(obj) {
return !_.isSymbol(obj) && isFinite(obj) && !isNaN(parseFloat(obj));
}; // Is the given value `NaN`?
_.isNaN = function(obj) {
return _.isNumber(obj) && isNaN(obj);
}; // Is a given value a boolean?
_.isBoolean = function(obj) {
return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
}; // Is a given value equal to null?
_.isNull = function(obj) {
return obj === null;
}; // Is a given variable undefined?
_.isUndefined = function(obj) {
return obj === void 0;
};

emmm 显而易见了吧

_.has = function(obj, path) {
if (!_.isArray(path)) {
return has(obj, path);
}
var length = path.length;
for (var i = 0; i < length; i++) {
var key = path[i];
if (obj == null || !hasOwnProperty.call(obj, key)) {
return false;
}
obj = obj[key];
}
return !!length;
};
// e.g.
let obj = { a: { b: { c: 1 } } };
_.has(obj, ['a', 'b', 'c']); // true
_.has(obj, ['a', 'b', 'd']); // false
_.has(obj, []); // false

判断一个对象是否有指定属性,如果是数组则判断嵌套属性。空数组返回 false。和前面 deepGet 不同的是这里有 hasOwnProperty 判断是否是自有属性。

=== 1390 行 下面是 Utility Functions 一些工具方法 胜利在望✌️

_.noConflict = function() {
root._ = previousUnderscore;
return this;
};

如果运行在浏览器等环境 不能直接导出变量 只能将 _ 赋值到全局变量 如果之前已经有变量叫做 _ 可以通过  var underscore = _.noConflict();  获得_工具函数同时将 _ 赋值回原来的值。

_.identity = function(value) {
return value;
};

是一个传入什么就返回什么的函数。看起来好像没什么用,但是前面有用到哒,可以作为 map 等函数的默认 iteratee

var a = [null, null, [1,2,3], null, [10, 12], null];
a.filter(_.identity)

参考 Stack Overflow 上面的一个找到的 >_<

_.constant = function(value) {
return function() {
return value;
};
};
// e.g.
// api: image.fill( function(x, y) { return color })
image.fill( _.constant( black ) );

代码不难 同样让人困惑的是用途,在 Stack Overflow 找到一个用法举例。

_.noop = function(){};

返回一个空函数。可以用在需要填写函数但又不需要做任何操作的地方。

_.propertyOf = function(obj) {
if (obj == null) {
return function(){};
}
return function(path) {
return !_.isArray(path) ? obj[path] : deepGet(obj, path);
};
};

_.propertyOf 返回获取指定对象属性的方法。

_.times = function(n, iteratee, context) {
var accum = Array(Math.max(0, n)); // n 不能小于 0
iteratee = optimizeCb(iteratee, context, 1);
for (var i = 0; i < n; i++) accum[i] = iteratee(i);
return accum;
};
// e.g.
_.times(6, i => i * i); // [ 0, 1, 4, 9, 16, 25 ]
_.times(6, _.identity); // [ 0, 1, 2, 3, 4, 5 ]

运行一个函数 n 次来生成一个数组。每一次参数都是运行的次数,从 0 开始。

_.now = Date.now || function() {
return new Date().getTime();
};

Date.now 是 ES5(还是6)新增的,旧版本没有,通过new Date().getTime()获得

// 一些 HTML 的转义字符
var escapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": ''',
'`': '`'
};
var unescapeMap = _.invert(escapeMap); // Functions for escaping and unescaping strings to/from HTML interpolation.
var createEscaper = function(map) {
// 以传入 escapeMap 举例
var escaper = function(match) {
// 返回对应的转义后的字符串
return map[match];
};
// 生成一个正则表达式用来匹配所有的需要转义的字符 (?:&|<|>|"|'|`)
// 正则表达式有两种创建方式 通过 /.../ 字面量直接创建 或者通过 new RegExp(regStr) 创建
// 这里的 ?: 表示正则表达不捕获分组 如果不添加这个的话 在 replace 中可使用 $i 代替捕获的分组
// 比如
// '2015-12-25'.replace(/(\d{4})-(\d{2})-(\d{2})/g,'$2/$3/$1'); --> "12/25/2015"
// '2015-12-25'.replace(/(?:\d{4})-(\d{2})-(\d{2})/g,'$2/$3/$1'); --> "25/$3/12"
// 为了防止 $1 变成捕获的字符串这里使用了 ?: (其实好像也用不到吧= =
var source = '(?:' + _.keys(map).join('|') + ')';
var testRegexp = RegExp(source); // 生成的正则表达式 /(?:&|<|>|"|'|`)/
var replaceRegexp = RegExp(source, 'g'); // 生成的正则表达式 /(?:&|<|>|"|'|`)/g
return function(string) {
string = string == null ? '' : '' + string;
return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
};
};
_.escape = createEscaper(escapeMap);
_.unescape = createEscaper(unescapeMap);
// e.g.
_.escape('<html></html>') // &lt;html&gt;&lt;/html&gt;
_.unescape('&lt;html&gt;&lt;/html&gt;') // <html></html>

html实体字符的一些转义和反转义。

_.result = function(obj, path, fallback) {
if (!_.isArray(path)) path = [path];
var length = path.length;
if (!length) {
return _.isFunction(fallback) ? fallback.call(obj) : fallback;
}
for (var i = 0; i < length; i++) {
var prop = obj == null ? void 0 : obj[path[i]];
if (prop === void 0) {
prop = fallback;
i = length; // Ensure we don't continue iterating.
}
obj = _.isFunction(prop) ? prop.call(obj) : prop;
}
return obj;
};
// e.g.
_.result({ a: { b: 2 } }, ['a','d'], () => 'failed'); // failed
_.result({ a: { b: 2 } }, ['a','b'], () => 'failed'); //
_.result({ a: () => ({ b: 2 }) }, ['a','b'], 'failed'); //
_.result({ a: () => ({ b: 2 }) }, ['a','d'], 'failed'); // failed

又是一个看得莫名其妙的函数...

根据 path 获取 obj 的属性值,当获取不到时就返回 fallback 的执行结果。当遇到属性为函数时就把 上一层对象作为 this 传入执行函数然后继续向下查找。

var idCounter = 0;
_.uniqueId = function(prefix) {
var id = ++idCounter + '';
return prefix ? prefix + id : id;
};
// e.g.
_.uniqueId('DWR'); // DWR1
_.uniqueId('DWR'); // DWR2
_.uniqueId('XIA'); // XIA3

就是通过闭包 返回一个不断递增的 id

_.template 我觉得值得用单独一篇博客来讲 = = 但其实我都是胡诌的!

首先要理解一下这个函数的用法

学过 jsp 的同学应该知道 jsp 中表达式可以写在 <%= %> 之间 而脚本可以写在 <% %> 在渲染的时候 会将脚本执行 表达式也会替换成实际值

这里的用法和那个基本一样

let template = `
<lable>用户ID:</lable><span><%= userId %></span>
<lable>用户名:</lable><span><%= username %></span>
<lable>用户密码:</lable><span><%- password %></span>
<%
if (userId === 1) { console.log('管理员登录...') }
else { console.log('普通用户登录...') }
%>
` let render = _.template(template); render({userId: 1, username: '管理员', password: '<pwd>'});
/* render 返回:
<lable>用户ID:</lable><span>1</span>
<lable>用户名:</lable><span>管理员</span>
<lable>用户密码:</lable><span>&lt;pwd&gt;</span>
*/
// 同时控制台打印: 管理员登录...

前端三门语言中 只有 JavaScript 是图灵完备语言,你以为你写的模板是 html 添加了一些数据、逻辑,实际上 html 并不能处理这些代码

所以我们需要使用 JS 来处理它。处理后在生成对应的 HTML

把模板先生成一个 render 函数 然后为函数传入数据 就能生成对应 html 了。

除了上面的基础用法 我们可以自定义模板的语法 注意 key 要和 underscore 中定义的相等

默认是这样的

_.templateSettings = {
evaluate: /<%([\s\S]+?)%>/g, // <% %> js脚本
interpolate: /<%=([\s\S]+?)%>/g, // <%= %> 表达式
escape: /<%-([\s\S]+?)%>/g // <%- %> 表达式 生成后对 html 字符进行转义 如 < 转义为 &lt; 防止 XSS 攻击
};

我们可以自定义

let settings = { interpolate: /{{([\s\S]+?)}}/ }

现在 Vue 不是很火嘛 用一下 Vue 的语法

let template = `
<div>欢迎{{ data }}登录</div>
`;
let render = _.template(template, { interpolate: /{{([\s\S]+?)}}/, variable: 'data' });
render('OvO'); // <div>欢迎OvO登录</div>

variable 指定了作用域 不指定时传入 render 的参数为 obj 的话 那么插值中 prop 获取到是 obj.prop 的值

variable 指定传入 render 函数参数的名字

理解了用法 现在思考怎样实现 如果让你写程序传入一段 js 代码输出运行结果 你会怎么办

憋说写一个解释器 >_<

大概就两种选择 eval() 和 new Function() (原谅我学艺不精 还有其他办法吗?)而 eval 只能运行一次 function 是生成一个函数 可以运行多次

生成的 render 有一个参数 source 是生成的函数字符串

这样我们可以达到预编译的效果 就像 vue 打包后的文件里面是没有 template 的 都是编译好的 render 函数

为什么要预编译?我们应该不想每一次运行都 new Function 吧 这个效率低大家应该都知道。其次,动态生成的函数,debug 不方便。

我们传入字符串 但这个字符串中不只有 js 代码还有些不相关的字符串。所以需要使用正则表达式将其中的 js 代码找出来,templateSettings 定义的就是这个正则表达式

如果是表达式就把运行结果和前后的字符串连接起来 如果是脚本就执行

具体看代码就好了

// \s  匹配一个空白字符,包括空格、制表符、换页符和换行符。
// \S 匹配一个非空白字符。
// 所以 \s\S 就是匹配所有字符 和 . 比起来它多匹配了换行
_.templateSettings = {
evaluate: /<%([\s\S]+?)%>/g, // <% %>
interpolate: /<%=([\s\S]+?)%>/g, // <%= %>
escape: /<%-([\s\S]+?)%>/g // <%- %>
}; // 这是一个一定不会匹配的正则表达式
var noMatch = /(.)^/; // 因为后面要拼接一个函数体 有些字符放到字符串需要被转义 这里定义了需要转义的字符
// \u2028 和 \u2029 不知道是啥 不想查了= =
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\u2028': 'u2028',
'\u2029': 'u2029'
}; var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; var escapeChar = function(match) {
return '\\' + escapes[match];
}; _.template = function(text, settings, oldSettings) { // oldSettings 为了向下兼容 可以无视
if (!settings && oldSettings) settings = oldSettings;
// 可以传入 settings 要和 _.templateSettings 中属性名相同来覆盖 templateSettings
settings = _.defaults({}, settings, _.templateSettings); // reg.source 返回正则表达式两个斜杠之间的字符串 /\d+/g --> "\d+"
// matcher 就是把三个正则连起来 /<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
// 加了一个 $ 表示匹配字符串结尾
var matcher = RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g'); var index = 0;
var source = "__p+='";
// 假设传入的 text 是 '<p><%=x+1%></p>'
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
// 函数的参数分别是:
// 匹配的字符串
// 匹配的分组(有三个括号,所以有三个分组,分别表示 escape, interpolate, evaluate 匹配的表达式)
// 匹配字符串的下标
// 第一次匹配: "<p><%=x+1%></p>" 会和 interpolate: /<%=([\s\S]+?)%>/g 匹配 interpolate 的值为 "x+1"
// index = 0, offset 匹配的起始下标 就是截取字符串最前面未匹配的那一段
// text.slice(index, offset) 就是 "<p>" 此时的 source 就是 "__p+='<p>"
// replace(escapeRegExp, escapeChar) 的作用是:
// source 拼接的是一个 '' 包裹的字符串 有些字符放到 ' ' 里需要被转义 // 第二次匹配:匹配字符串("<p><%=x+1%></p>")结尾
// text.slice(index, offset) 此时获取的是 "</p>"
// 拼接后 source 为 "__p+='<p>'+\n((__t=(x+1))==null?'':__t)+\n'</p>"
source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
index = offset + match.length; // 匹配的起始下标+匹配字符串长度 就是匹配字符串末尾的下标 if (escape) {
// ((__t = (_.escape(escape))) == null ? '' : __t)
// _.escape 是将生成的表达式中的 html 字符进行转义
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
} else if (interpolate) {
// ((__t = (interpolate)) == null ? '' : __t)
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
} else if (evaluate) {
// 前面的字符串加分号 同时执行该脚本
source += "';\n" + evaluate + "\n__p+='";
}
// 第一次匹配后 interpolate 为 "x+1"
// 此时 source 是 "__p+='<p>'+\n((__t=(x+1))==null?'':__t)+\n'"
// 第二次匹配 escape、interpolate、evaluate 都不存在 不会改变 source // Adobe VMs need the match returned to produce the correct offset.
// 返回 match 只是为了获取正确的 offset 而替换后的 text 并没有改变
return match;
});
source += "';\n"; // 如果没有指定 settings.variable 就添加 with 指定作用域
// 添加 with 之后 source 为 "with(obj||{}){\n__p+='<p>'+\n((__t=(x+1))==null?'':__t)+\n'</p>\';\n}\n"
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + 'return __p;\n';
// 最后生成的 source 为
// "var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};
// with(obj||{}){
// __p+='<p>'+
// ((__t=(x+1))==null?'':__t)+
// '</p>';\n}\nreturn __p;
// " var render;
try {
// 传入的参数1: settings.variable || obj
// 传入的参数2: _ 使用于可以在插值中使用 _ 里的函数
// 函数体 source
render = new Function(settings.variable || 'obj', '_', source);
/* 生成函数 render
function anonymous(obj, _) {
var __t, __p = '',
__j = Array.prototype.join,
print = function() {
__p += __j.call(arguments, '');
};
with(obj || {}) {
__p += '<p>' +
((__t = (x + 1)) == null ? '' : __t) +
'</p>';
}
return __p;
}
*/
} catch (e) {
e.source = source;
throw e;
} 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 + '}'; return template;
}; var template = _.template("<p><%=x+1%></p>");
template({x: 'void'}) // <p>void1</p>

尽管我看的一知半解,但是还是感觉学到了好多。

再下面就是 OOP 的部分上面已经基本分析过了

_.prototype.value = function() {
return this._wrapped;
}; _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; _.prototype.toString = function() {
return String(this._wrapped);
};

重写下划线的实例的 valueOf 、 toJSON 和 toString 函数

if (typeof define == 'function' && define.amd) {
define('underscore', [], function() {
return _;
});
}

AMD(异步模块定义,Asynchronous Module Definition),这里为了兼容 amd 规范。

到此就把 下划线 1693 行全部看完了。

其实这是我第二遍看,到这次才能说勉强看懂,第一次真的是一头雾水。这期间看了点函数式编程的文章,也许有点帮助吧。

也开始理解了大家为什么说新手想阅读源码的话推荐这个,因为短、耦合度低、而且涉及到很多基础知识。

整体看下来,executeBound、OOP部分 和 _.template 这三部分花了很长时间思考。当然抽丝剥茧后搞懂明白的感觉,真的很爽呀哈哈哈哈哈

总之,完结撒花吧~