這是Lodash源碼分析系列文章的第三篇,前面兩篇文章(Lodash 源碼分析(一)「Function」 Methods、Lodash 源碼分析(二)「Function」 Methods)分別分析了Lodash "Function" 中的一些重要函數,也給出了簡化的實現,爲理解其內部機理和執行方式提供了便利。這篇文章將專一於Array,Array是Lodash中很是重要的內容,咱們將分析其代碼實現以及同相似庫中的實現對比。javascript
_.head
_.head
函數其實很簡單,返回一個數組的第一個元素,徹底能夠在兩三行代碼中實現。能夠看到Lodash中是這麼實現的:java
function head(array) { return (array && array.length) ? array[0] : undefined; }
Lodash進行了簡單的判斷,而後返回了第一個元素。這麼簡單的函數其實沒有什麼好說的,但我拿出來講是想介紹另外一個庫Ramda.js
的實現:程序員
module.exports = nth(0);
它是用nth
函數實現該功能的,那麼這個函數式怎麼樣的呢?編程
module.exports = _curry2(function nth(offset, list) { var idx = offset < 0 ? list.length + offset : offset; return _isString(list) ? list.charAt(idx) : list[idx]; });
這個函數就有點意思了,用了柯里化,是一個函數式的實現,當head
函數返回一個nth(0)
時,其實返回的是一個柯里化以後的函數,而後再接受一個數組,判斷數組類型以後返回list[offset]
的值。segmentfault
再看看Lodash的nth
的實現:數組
function nth(array, n) { return (array && array.length) ? baseNth(array, toInteger(n)) : undefined; } function baseNth(array, n) { var length = array.length; if (!length) { return; } n += n < 0 ? length : 0; return isIndex(n, length) ? array[n] : undefined; }
仔細對比兩個庫的實現,兩個庫都容許負下標的處理,可是對於Ramda而言,若是list是一個null
或者undefined
類型的數據的話,將會拋出TypeError
,而Lodash則優雅一些。app
_.join
_.join
函數是另外一個簡單的函數:ide
var arrayProto = Array.prototype; var nativeJoin = arrayProto.join; function join(array, separator) { return array == null ? '' : nativeJoin.call(array, separator); }
重寫以後函數變爲:函數式編程
function join(array,separator) { return array == null ? '' : Array.prototype.join.call(array, separator); }
咱們再對比一下Ramda的實現:函數
var invoker = require('./invoker'); module.exports = invoker(1, 'join');
再看看invoker函數:
module.exports = _curry2(function invoker(arity, method) { return curryN(arity + 1, function() { var target = arguments[arity]; if (target != null && _isFunction(target[method])) { return target[method].apply(target, Array.prototype.slice.call(arguments, 0, arity)); } throw new TypeError(toString(target) + ' does not have a method named "' + method + '"'); }); });
invoker
函數就是爲了返回一個curry化的函數,那麼咱們其實能夠這麼理解若是用Lodash實現一個函數化的join
能夠這麼實現:
function _join(array,separator){ return Array.prototype.join.call(array,seprator); } var join = _.curry(_join);
那麼咱們能夠和Ramda的使用方式同樣使用:
join(_,",")([1,2,3]); // 1,2,3
_.remove
這個方法頗有意思,咱們能夠看到不一樣的實現方式(一般實現/函數式實現),兩種實現差異很大,因此拿出來分析一下。
先看看Lodash的實現:
/** * Removes all elements from `array` that `predicate` returns truthy for * and returns an array of the removed elements. The predicate is invoked * with three arguments: (value, index, array). * * **Note:** Unlike `_.filter`, this method mutates `array`. Use `_.pull` * to pull elements from an array by value. * * @static * @memberOf _ * @since 2.0.0 * @category Array * @param {Array} array The array to modify. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @returns {Array} Returns the new array of removed elements. * @example * * var array = [1, 2, 3, 4]; * var evens = _.remove(array, function(n) { * return n % 2 == 0; * }); * * console.log(array); * // => [1, 3] * * console.log(evens); * // => [2, 4] */ function remove(array, predicate) { var result = []; if (!(array && array.length)) { return result; } var index = -1, indexes = [], length = array.length; predicate = getIteratee(predicate, 3); while (++index < length) { var value = array[index]; if (predicate(value, index, array)) { result.push(value); indexes.push(index); } } basePullAt(array, indexes); return result; }
必定要注意的是,該方法會修改原數組。官方也對其進行了說明。該方法同_.fliter
的區別也就在是否會修改原對象上。
咱們分析一下Lodash是如何實現這個功能的,首先判斷數組是否合法,若是不合法就直接返回。在Lodash中的實現其實很簡單,首先獲得一個predicate
謂詞函數,該謂詞函數用於判斷元素是否符合條件,若是符合條件就將其從原數組中移除。邏輯也比較簡單,可是該函數會修改原array,該功能是經過basePullAt()
實現的:
/** * The base implementation of `_.pullAt` without support for individual * indexes or capturing the removed elements. * * @private * @param {Array} array The array to modify. * @param {number[]} indexes The indexes of elements to remove. * @returns {Array} Returns `array`. */ function basePullAt(array, indexes) { var length = array ? indexes.length : 0, lastIndex = length - 1; while (length--) { var index = indexes[length]; if (length == lastIndex || index !== previous) { var previous = index; if (isIndex(index)) { splice.call(array, index, 1); } else { baseUnset(array, index); } } } return array; }
須要說明的是,這裏的splice
方法的原型是Array.prototype.splice
,該方法同Array.prototype.slice
的區別是,splice
會修改原數組的內容,而slice
不會修改原數組的內容,而僅僅作的是一次淺拷貝。
還須要說明一下的是baseUnset
:
/** * The base implementation of `unset`. * * @private * @param {Object} object The object to modify. * @param {Array|string} path The property path to unset. * @returns {boolean} Returns `true` if the property is deleted, else `false`. */ function baseUnset(object, path) { path = castPath(path, object) object = parent(object, path) return object == null || delete object[toKey(last(path))] } export default baseUnset
這個方法其實很簡單,就是刪除對象中的某一個屬性/鍵。
因此Lodash的整個_.remove
的脈絡就捋清楚了,按照慣例,咱們須要稍微簡化一下這個函數,把核心邏輯抽取出來:
function remove(list,predicated){ var indexes = []; for(var i=0;i < list.length;i++){ if(predicated(list[i])){ indexes.push(i); } } for(var idx = indexes.length -1; idx >=0;idx--){ Array.prototype.splice.call(list,indexes[idx],1); } return list; } var a = [1,2,3,4]; remove(a,function(a){if (a == 3) return true; else return false;}); console.log(a); // [1,2,4]
恩,感受好像也挺好用的。
可是咱們不能止步於此,做爲一個熱衷函數式編程的程序員,最終目標是代碼中沒有循環沒有分支。咱們看看Ramda.js是怎麼實現的:
/** * Removes the sub-list of `list` starting at index `start` and containing * `count` elements. _Note that this is not destructive_: it returns a copy of * the list with the changes. * <small>No lists have been harmed in the application of this function.</small> * * @func * @memberOf R * @since v0.2.2 * @category List * @sig Number -> Number -> [a] -> [a] * @param {Number} start The position to start removing elements * @param {Number} count The number of elements to remove * @param {Array} list The list to remove from * @return {Array} A new Array with `count` elements from `start` removed. * @example * * R.remove(2, 3, [1,2,3,4,5,6,7,8]); //=> [1,2,6,7,8] */ module.exports = _curry3(function remove(start, count, list) { var result = Array.prototype.slice.call(list, 0); result.splice(start, count); return result; });
其實Ramda就是對splice
進行了curry化,什麼也沒有作,毫無參考價值。沒有達到咱們的預期,因此只能本身動手了:
function remove2(list,predicated){ return _remove(list,list.length-1,predicated); } function _remove(list,idx,predicated){ if(predicated(list[idx])){ list.splice(idx,1); } if (idx == 0){return list;}else{ _remove(list,idx-1,predicated); } } //調用 var a = [1,2,3,4]; remove2(a,function(a){if (a == 3) return true; else return false;}); console.log(a); //[1,2,4]
感受舒服多了,對於JavaScript而言沒有分支語句是不可能的,可是能夠把全部的循環用遞歸取代,感受代碼也簡潔了許多,函數式可以讓人以另外一個角度思考問題,真的是一個很好的編程範式。
最近工做很是忙,也沒有時間寫第三篇連載,忙裏抽空用午休時間將本文寫完了。成文比較匆忙不免有一些謬誤望各位看官海涵,也但願可以直接指出我文章中的錯誤,感激涕零!
本系列文章還有後續內容,包括數組和集合的操做,以及對象的操做,具體尚未想好涉及哪方面內容,總之敬請期待!