=== 756 行開始 函數部分。php
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 (柯里化)html
經過 args.concat(callArgs) 實現了柯里化前端
bound 是綁定 this 後的函數,func 是傳入的函數vue
if (!(callingContext instanceof boundFunc)) 若是 callingContext 不是 boundFunc 的實例 就經過 apply 實現指定函數運行的 thisnode
若是 callingContext 是 boundFunc 的實例,那意味着你多是這麼使用的git
function foo() {} var bindFoo = _.bind(foo, context/*沒寫定義,隨便什麼東西*/); var bindFooInstance = new bindFoo();
此時 bindFoo() 的 this 就是 bindFoo 的一個實例github
那麼 bindFooInstance 的 this 是應該綁定到 context 仍是 bindFoo 的實例仍是什麼呢?ajax
JavaScript 中 this 一共有四種綁定 默認綁定 < 隱式綁定 < 顯示綁定 < new綁定正則表達式
因此 這裏應該優先使用... foo 的實例express
思考一下嘛 若是是 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 這三部分花了很長時間思考。固然抽絲剝繭後搞懂明白的感受,真的很爽呀哈哈哈哈哈
總之,完結撒花吧~