Underscore.js 源码学习笔记(下)
上接 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 为当前时间 并执行 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)) // 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 传入的参数除了第一个都被合成了一个数组 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 ] // 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 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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '`': '`' }; 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>') // <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 中属性名相同来覆盖 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 这三部分花了很长时间思考。当然抽丝剥茧后搞懂明白的感觉,真的很爽呀哈哈哈哈哈
总之,完结撒花吧~