上接 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); // 4
默认占位符是 _ 先给函数指定部分参数 不指定的就用下划线占位 生成一个新的只需要填写剩余参数的函数
_.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(); // 25 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 为当前时间 并执行 funcif (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)) // 1 console.log(foo(2)) // 2 console.log(foo(3)) // 2 (第 n 次开始调用不再执行 func 直接返回上一次的结果 console.log(foo(4)) // 2
只有前 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 传入的参数除了第一个都被合成了一个数组 keysvar 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 属性)就继续返回链式对象// 否则直接返回 objreturn 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 ] // 4
通过例子可以感受的更清晰。 接下来_.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 checksvar 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) {// 如果不是数组也不是对象的话 其他情况都已经比较完了 所以一定是 falseif (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 memberkey = 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 不能小于 0iteratee = 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 = {'&': '&','<': '<','>': '>','"': '"',"'": ''','`': '`' }; 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'); // 生成的正则表达式 /(?:&|<|>|"|'|`)/greturn 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>') // <html></html> _.unescape('<html></html>') // <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'); // 2 _.result({ a: () => ({ b: 2 }) }, ['a','b'], 'failed'); // 2 _.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><pwd></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 字符进行转义 如 < 转义为 < 防止 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 中属性名相同来覆盖 templateSettingssettings = _.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: _ 使用于可以在插值中使用 _ 里的函数// 函数体 sourcerender = new Function(settings.variable || 'obj', '_', source);/* 生成函数 renderfunction 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 这三部分花了很长时间思考。当然抽丝剥茧后搞懂明白的感觉,真的很爽呀哈哈哈哈哈
总之,完结撒花吧~