學習 lodash 源碼總體架構,打造屬於本身的函數式編程類庫

前言

這是學習源碼總體架構系列第三篇。總體架構這詞語好像有點大,姑且就算是源碼總體結構吧,主要就是學習是代碼總體結構,不深究其餘不是主線的具體函數的實現。文章學習的是打包整合後的代碼,不是實際倉庫中的拆分的代碼。html

上上篇文章寫了jQuery源碼總體架構學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫前端

上一篇文章寫了underscore源碼總體架構學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫vue

感興趣的讀者能夠點擊閱讀。git

underscore源碼分析的文章比較多,而lodash源碼分析的文章比較少。緣由之一多是因爲lodash源碼行數太多。註釋加起來一萬多行。github

分析lodash總體代碼結構的文章比較少,筆者利用谷歌、必應、github等搜索都沒有找到,多是找的方式不對。因而打算本身寫一篇。日常開發大多數人都會使用lodash,並且都或多或少知道,lodashunderscore性能好,性能好的主要緣由是使用了惰性求值這一特性。面試

本文章學習的lodash的版本是:v4.17.15unpkg.com地址 unpkg.com/lodash@4.17…編程

文章篇幅可能比較長,能夠先收藏再看,因此筆者使用了展開收縮的形式。segmentfault

導讀:數組

文章主要學習了runInContext() 導出_ lodash函數使用baseCreate方法原型繼承LodashWrapperLazyWrappermixin掛載方法到lodash.prototype、後文用結合例子解釋lodash.prototype.value(wrapperValue)Lazy.prototype.value(lazyValue)惰性求值的源碼具體實現。瀏覽器

匿名函數執行

;(function() {

}.call(this));
複製代碼

暴露 lodash

var _ = runInContext();
複製代碼

runInContext 函數

這裏的簡版源碼,只關注函數入口和返回值。

var runInContext = (function runInContext(context) {
	// 瀏覽器中處理context爲window
	// ...
	function lodash(value) {}{
		// ...
		return new LodashWrapper(value);
	}
	// ...
	return lodash;
});
複製代碼

能夠看到申明瞭一個runInContext函數。裏面有一個lodash函數,最後處理返回這個lodash函數。

再看lodash函數中的返回值 new LodashWrapper(value)

LodashWrapper 函數

function LodashWrapper(value, chainAll) {
	this.__wrapped__ = value;
	this.__actions__ = [];
	this.__chain__ = !!chainAll;
	this.__index__ = 0;
	this.__values__ = undefined;
}
複製代碼

設置了這些屬性:

__wrapped__:存放參數value

__actions__:存放待執行的函數體func, 函數參數 args,函數執行的this 指向 thisArg

__chain__undefined兩次取反轉成布爾值false,不支持鏈式調用。和underscore同樣,默認是不支持鏈式調用的。

__index__:索引值 默認 0。

__values__:主要clone時使用。

接着往下搜索源碼,LodashWrapper, 會發現這兩行代碼。

LodashWrapper.prototype = baseCreate(baseLodash.prototype);
LodashWrapper.prototype.constructor = LodashWrapper;
複製代碼

接着往上找baseCreate、baseLodash這兩個函數。

baseCreate 原型繼承

// 當即執行匿名函數
// 返回一個函數,用於設置原型 能夠理解爲是 __proto__
var baseCreate = (function() {
	// 這句放在函數外,是爲了避免用每次調用baseCreate都重複申明 object
	// underscore 源碼中,把這句放在開頭就申明瞭一個空函數 `Ctor`
	function object() {}
	return function(proto) {
		// 若是傳入的參數不是object也不是function 是null
		// 則返回空對象。
		if (!isObject(proto)) {
			return {};
		}
		// 若是支持Object.create方法,則返回 Object.create
		if (objectCreate) {
			// Object.create
			return objectCreate(proto);
		}
		// 若是不支持Object.create 用 ployfill new
		object.prototype = proto;
		var result = new object;
		// 還原 prototype
		object.prototype = undefined;
		return result;
	};
}());

// 空函數
function baseLodash() {
	// No operation performed.
}

// Ensure wrappers are instances of `baseLodash`.
lodash.prototype = baseLodash.prototype;
// 爲何會有這一句?由於上一句把lodash.prototype.construtor 設置爲Object了。這一句修正constructor
lodash.prototype.constructor = lodash;

LodashWrapper.prototype = baseCreate(baseLodash.prototype);
LodashWrapper.prototype.constructor = LodashWrapper;
複製代碼

筆者畫了一張圖,表示這個關係。

lodash 原型關係圖

衍生的 isObject 函數

判斷typeof value不等於null,而且是object或者function

function isObject(value) {
	var type = typeof value;
	return value != null && (type == 'object' || type == 'function');
}
複製代碼

Object.create() 用法舉例

面試官問:可否模擬實現JS的new操做符 以前這篇文章寫過的一段,因此這裏收縮起來了。

點擊 查看 Object.create() 用法舉例

筆者以前整理的一篇文章中也有講過,能夠翻看JavaScript 對象全部API解析

MDN Object.create()

Object.create(proto, [propertiesObject]) 方法建立一個新對象,使用現有的對象來提供新建立的對象的__proto__。 它接收兩個參數,不過第二個可選參數是屬性描述符(不經常使用,默認是undefined)。

var anotherObject = {
    name: '若川'
};
var myObject = Object.create(anotherObject, {
    age: {
        value:18,
    },
});
// 得到它的原型
Object.getPrototypeOf(anotherObject) === Object.prototype; // true 說明anotherObject的原型是Object.prototype
Object.getPrototypeOf(myObject); // {name: "若川"} // 說明myObject的原型是{name: "若川"}
myObject.hasOwnProperty('name'); // false; 說明name是原型上的。
myObject.hasOwnProperty('age'); // true 說明age是自身的
myObject.name; // '若川'
myObject.age; // 18;
複製代碼

對於不支持ES5的瀏覽器,MDN上提供了ployfill方案。

if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        if (typeof proto !== 'object' && typeof proto !== 'function') {
            throw new TypeError('Object prototype may only be an Object: ' + proto);
        } else if (proto === null) {
            throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
        }

        if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

        function F() {}
        F.prototype = proto;
        return new F();
    };
}
複製代碼

lodash上有不少方法和屬性,但在lodash.prototype也有不少與lodash上相同的方法。確定不是在lodash.prototype上從新寫一遍。而是經過mixin掛載的。

mixin

mixin 具體用法

_.mixin([object=lodash], source, [options={}])
複製代碼

添加來源對象自身的全部可枚舉函數屬性到目標對象。 若是 object 是個函數,那麼函數方法將被添加到原型鏈上。

注意: 使用 _.runInContext 來建立原始的 lodash 函數來避免修改形成的衝突。

添加版本

0.1.0

參數

[object=lodash] (Function|Object): 目標對象。

source (Object): 來源對象。

[options={}] (Object): 選項對象。

[options.chain=true] (boolean): 是否開啓鏈式操做。

返回

(*): 返回 object.

mixin 源碼

點擊這裏展開mixin源碼,後文註釋解析
function mixin(object, source, options) {
	var props = keys(source),
		methodNames = baseFunctions(source, props);

	if (options == null &&
		!(isObject(source) && (methodNames.length || !props.length))) {
		options = source;
		source = object;
		object = this;
		methodNames = baseFunctions(source, keys(source));
	}
	var chain = !(isObject(options) && 'chain' in options) || !!options.chain,
		isFunc = isFunction(object);

	arrayEach(methodNames, function(methodName) {
		var func = source[methodName];
		object[methodName] = func;
		if (isFunc) {
			object.prototype[methodName] = function() {
				var chainAll = this.__chain__;
				if (chain || chainAll) {
					var result = object(this.__wrapped__),
						actions = result.__actions__ = copyArray(this.__actions__);

					actions.push({ 'func': func, 'args': arguments, 'thisArg': object });
					result.__chain__ = chainAll;
					return result;
				}
				return func.apply(object, arrayPush([this.value()], arguments));
			};
		}
	});

	return object;
}
複製代碼
接下來先看衍生的函數。

其實看到具體定義的函數代碼就大概知道這個函數的功能。爲了避免影響主線,致使文章篇幅過長。具體源碼在這裏就不展開。

感興趣的讀者能夠自行看這些函數衍生的其餘函數的源碼。

mixin 衍生的函數 keys

mixin 函數中 其實最終調用的就是 Object.keys

function keys(object) {
	return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
}
複製代碼

mixin 衍生的函數 baseFunctions

返回函數數組集合

function baseFunctions(object, props) {
	return arrayFilter(props, function(key) {
		return isFunction(object[key]);
	});
}
複製代碼

mixin 衍生的函數 isFunction

判斷參數是不是函數

function isFunction(value) {
	if (!isObject(value)) {
		return false;
	}
	// The use of `Object#toString` avoids issues with the `typeof` operator
	// in Safari 9 which returns 'object' for typed arrays and other constructors.
	var tag = baseGetTag(value);
	return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
}
複製代碼

mixin 衍生的函數 arrayEach

相似 [].forEarch

function arrayEach(array, iteratee) {
	var index = -1,
		length = array == null ? 0 : array.length;

	while (++index < length) {
		if (iteratee(array[index], index, array) === false) {
			break;
		}
	}
	return array;
}
複製代碼

mixin 衍生的函數 arrayPush

相似 [].push

function arrayPush(array, values) {
	var index = -1,
		length = values.length,
		offset = array.length;

	while (++index < length) {
	array[offset + index] = values[index];
	}
	return array;
}
複製代碼

mixin 衍生的函數 copyArray

拷貝數組

function copyArray(source, array) {
	var index = -1,
		length = source.length;

	array || (array = Array(length));
	while (++index < length) {
		array[index] = source[index];
	}
	return array;
}
複製代碼

mixin 源碼解析

lodash 源碼中兩次調用 mixin

// Add methods that return wrapped values in chain sequences.
lodash.after = after;
// code ... 等 153 個支持鏈式調用的方法

// Add methods to `lodash.prototype`.
// 把lodash上的靜態方法賦值到 lodash.prototype 上
mixin(lodash, lodash);

// Add methods that return unwrapped values in chain sequences.
lodash.add = add;
// code ... 等 152 個不支持鏈式調用的方法


// 這裏其實就是過濾 after 等支持鏈式調用的方法,獲取到 lodash 上的 add 等 添加到lodash.prototype 上。
mixin(lodash, (function() {
	var source = {};
	// baseForOwn 這裏其實就是遍歷lodash上的靜態方法,執行回調函數
	baseForOwn(lodash, function(func, methodName) {
		// 第一次 mixin 調用了因此賦值到了lodash.prototype
		// 因此這裏用 Object.hasOwnProperty 排除不在lodash.prototype 上的方法。也就是 add 等 152 個不支持鏈式調用的方法。
		if (!hasOwnProperty.call(lodash.prototype, methodName)) {
			source[methodName] = func;
		}
	});
	return source;
// 最後一個參數options 特地註明不支持鏈式調用
}()), { 'chain': false });
複製代碼

結合兩次調用mixin 代入到源碼解析以下

點擊這裏展開mixin源碼及註釋
function mixin(object, source, options) {
	// source 對象中能夠枚舉的屬性
	var props = keys(source),
		// source 對象中的方法名稱數組
		methodNames = baseFunctions(source, props);

	if (options == null &&
		!(isObject(source) && (methodNames.length || !props.length))) {
		// 若是 options 沒傳爲 undefined undefined == null 爲true
		// 且 若是source 不爲 對象或者不是函數
		// 且 source對象的函數函數長度 或者 source 對象的屬性長度不爲0
		// 把 options 賦值爲 source
		options = source;
		// 把 source 賦值爲 object
		source = object;
		// 把 object 賦值爲 this 也就是 _ (lodash)
		object = this;
		// 獲取到全部的方法名稱數組
		methodNames = baseFunctions(source, keys(source));
	}
	// 是否支持 鏈式調用
	// options 不是對象或者不是函數,是null或者其餘值
	// 判斷options是不是對象或者函數,若是不是或者函數則不會執行 'chain' in options 也就不會報錯
	// 且 chain 在 options的對象或者原型鏈中
	// 知識點 in [MDN in : https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/in
	// 若是指定的屬性在指定的對象或其原型鏈中,則in 運算符返回true。

	// 或者 options.chain 轉布爾值
	var chain = !(isObject(options) && 'chain' in options) || !!options.chain,
		// object 是函數
		isFunc = isFunction(object);

	// 循環 方法名稱數組
	arrayEach(methodNames, function(methodName) {
		// 函數自己
		var func = source[methodName];
		// object 一般是 lodash 也賦值這個函數。
		object[methodName] = func;
		if (isFunc) {
			// 若是object是函數 賦值到 object prototype 上,一般是lodash
			object.prototype[methodName] = function() {
				// 實例上的__chain__ 屬性 是否支持鏈式調用
				// 這裏的 this 是 new LodashWrapper 實例 相似以下
				/** { __actions__: [], __chain__: true __index__: 0 __values__: undefined __wrapped__: [] } **/

				var chainAll = this.__chain__;
				// options 中的 chain 屬性 是否支持鏈式調用
				// 二者有一個符合鏈式調用 執行下面的代碼
				if (chain || chainAll) {
					// 一般是 lodash
					var result = object(this.__wrapped__),
					// 複製 實例上的 __action__ 到 result.__action__ 和 action 上
					actions = result.__actions__ = copyArray(this.__actions__);

					// action 添加 函數 和 args 和 this 指向,延遲計算調用。
					actions.push({ 'func': func, 'args': arguments, 'thisArg': object });
					//實例上的__chain__ 屬性 賦值給 result 的 屬性 __chain__
					result.__chain__ = chainAll;
					// 最後返回這個實例
					return result;
				}

				// 都不支持鏈式調用。直接調用
				// 把當前實例的 value 和 arguments 對象 傳遞給 func 函數做爲參數調用。返回調用結果。
				return func.apply(object, arrayPush([this.value()], arguments));
			};
		}
	});

	// 最後返回對象 object
	return object;
}
複製代碼

小結:簡單說就是把lodash上的靜態方法賦值到lodash.prototype上。分兩次第一次是支持鏈式調用(lodash.after153個支持鏈式調用的方法),第二次是不支持鏈式調用的方法(lodash.add152個不支持鏈式調用的方法)。

lodash 究竟在_和_.prototype掛載了多少方法和屬性

再來看下lodash究竟掛載在_函數對象上有多少靜態方法和屬性,和掛載_.prototype上有多少方法和屬性。

使用for in循環一試便知。看以下代碼:

var staticMethods = [];
var staticProperty = [];
for(var name in _){
	if(typeof _[name] === 'function'){
		staticMethods.push(name);
	}
	else{
		staticProperty.push(name);
	}
}
console.log(staticProperty); // ["templateSettings", "VERSION"] 2個
console.log(staticMethods); // ["after", "ary", "assign", "assignIn", "assignInWith", ...] 305個
複製代碼

其實就是上文說起的 lodash.after153個支持鏈式調用的函數 、lodash.add152不支持鏈式調用的函數賦值而來。

var prototypeMethods = [];
var prototypeProperty = [];
for(var name in _.prototype){
	if(typeof _.prototype[name] === 'function'){
		prototypeMethods.push(name);
	}
	else{
		prototypeProperty.push(name);
	}
}
console.log(prototypeProperty); // []
console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 317個
複製代碼

相比lodash上的靜態方法多了12個,說明除了 mixin 外,還有12個其餘形式賦值而來。

支持鏈式調用的方法最後返回是實例對象,獲取最後的處理的結果值,最後須要調用value方法。

筆者畫了一張表示lodash的方法和屬性掛載關係圖。

`lodash`的方法和屬性掛載關係

請出貫穿下文的簡單的例子

var result = _.chain([1, 2, 3, 4, 5])
.map(el => {
	console.log(el); // 1, 2, 3
	return el + 1;
})
.take(3)
.value();
// lodash中這裏的`map`僅執行了`3`次。
// 具體功能也很簡單 數組 1-5 加一,最後獲取其中三個值。
console.log('result:', result);
複製代碼

也就是說這裏lodash聰明的知道了最後須要幾個值,就執行幾回map循環,對於很大的數組,提高性能頗有幫助。
underscore執行這段代碼其中map執行了5次。 若是是日常實現該功能也簡單。

var result = [1, 2, 3, 4, 5].map(el => el + 1).slice(0, 3);
console.log('result:', result);
複製代碼

而相比lodash這裏的map執行了5次。

// 不使用 map、slice
var result = [];
var arr = [1, 2, 3, 4, 5];
for (var i = 0; i < 3; i++){
	result[i] = arr[i] + 1;
}
console.log(result, 'result');
複製代碼

簡單說這裏的map方法,添加 LazyWrapper 的方法到 lodash.prototype存儲下來,最後調用 value時再調用。 具體看下文源碼實現。

添加 LazyWrapper 的方法到 lodash.prototype

主要是以下方法添加到到 lodash.prototype 原型上。

// "constructor"
["drop", "dropRight", "take", "takeRight", "filter", "map", "takeWhile", "head", "last", "initial", "tail", "compact", "find", "findLast", "invokeMap", "reject", "slice", "takeRightWhile", "toArray", "clone", "reverse", "value"]
複製代碼
點擊這裏展開具體源碼及註釋
// Add `LazyWrapper` methods to `lodash.prototype`.
// baseForOwn 這裏其實就是遍歷LazyWrapper.prototype上的方法,執行回調函數
baseForOwn(LazyWrapper.prototype, function(func, methodName) {
	// 檢測函數名稱是不是迭代器也就是循環
	var checkIteratee = /^(?:filter|find|map|reject)|While$/.test(methodName),
		// 檢測函數名稱是否head和last
		// 順便提一下 ()這個是捕獲分組 而加上 ?: 則是非捕獲分組 也就是說不用於其餘操做
		isTaker = /^(?:head|last)$/.test(methodName),
		// lodashFunc 是 根據 isTaker 組合 takeRight take methodName
		lodashFunc = lodash[isTaker ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName],
		// 根據isTaker 和 是 find 判斷結果是否 包裝
		retUnwrapped = isTaker || /^find/.test(methodName);

	// 若是不存在這個函數,就不往下執行
	if (!lodashFunc) {
		return;
	}
	// 把 lodash.prototype 方法賦值到lodash.prototype
	lodash.prototype[methodName] = function() {
		// 取實例中的__wrapped__ 值 例子中則是 [1,2,3,4,5]
		var value = this.__wrapped__,
			// 若是是head和last 方法 isTaker 返回 [1], 不然是arguments對象
			args = isTaker ? [1] : arguments,
			// 若是value 是LayeWrapper的實例
			isLazy = value instanceof LazyWrapper,
			// 迭代器 循環
			iteratee = args[0],
			// 使用useLazy isLazy value或者是數組
			useLazy = isLazy || isArray(value);

		var interceptor = function(value) {
			// 函數執行 value args 組合成數組參數
			var result = lodashFunc.apply(lodash, arrayPush([value], args));
			// 若是是 head 和 last (isTaker) 支持鏈式調用 返回結果的第一個參數 不然 返回result
			return (isTaker && chainAll) ? result[0] : result;
		};

		// useLazy true 而且 函數checkIteratee 且迭代器是函數,且迭代器參數個數不等於1
		if (useLazy && checkIteratee && typeof iteratee == 'function' && iteratee.length != 1) {
			// Avoid lazy use if the iteratee has a "length" value other than `1`.
			// useLazy 賦值爲 false
			// isLazy 賦值爲 false
			isLazy = useLazy = false;
		}
		// 取實例上的 __chain__
		var chainAll = this.__chain__,
			// 存儲的待執行的函數 __actions__ 二次取反是布爾值 也就是等於0或者大於0兩種結果
			isHybrid = !!this.__actions__.length,
			// 是否不包裝 用結果是否不包裝 且 不支持鏈式調用
			isUnwrapped = retUnwrapped && !chainAll,
			// 是否僅Lazy 用isLazy 和 存儲的函數
			onlyLazy = isLazy && !isHybrid;

		// 結果不包裝 且 useLazy 爲 true
		if (!retUnwrapped && useLazy) {
			// 實例 new LazyWrapper 這裏的this 是 new LodashWrapper()
			value = onlyLazy ? value : new LazyWrapper(this);
			// result 執行函數結果
			var result = func.apply(value, args);

			/* * // _.thru(value, interceptor) // 這個方法相似 _.tap, 除了它返回 interceptor 的返回結果。該方法的目的是"傳遞" 值到一個方法鏈序列以取代中間結果。 _([1, 2, 3]) .tap(function(array) { // 改變傳入的數組 array.pop(); }) .reverse() .value(); // => [2, 1] */

			// thisArg 指向undefined 或者null 非嚴格模式下是指向window,嚴格模式是undefined 或者nll
			result.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined });
			// 返回實例 lodashWrapper
			return new LodashWrapper(result, chainAll);
		}
		// 不包裝 且 onlyLazy 爲 true
		if (isUnwrapped && onlyLazy) {
			// 執行函數
			return func.apply(this, args);
		}
		// 上面都沒有執行,執行到這裏了
		// 執行 thru 函數,回調函數 是 interceptor
		result = this.thru(interceptor);
		return isUnwrapped ? (isTaker ? result.value()[0] : result.value()) : result;
	};
});
複製代碼

小結一下,寫了這麼多註釋,簡單說:其實就是用LazyWrapper.prototype 改寫原先在lodash.prototype的函數,判斷函數是否須要使用惰性求值,須要時再調用。

讀者能夠斷點調試一下,善用斷點進入函數功能,對着註釋看,可能會更加清晰。

點擊查看斷點調試的部分截圖

例子的chain和map執行後的debugger截圖

例子的chain和map執行後的結果截圖

鏈式調用最後都是返回實例對象,實際的處理數據的函數都沒有調用,而是被存儲存儲下來了,最後調用value方法,才執行這些函數。

lodash.prototype.value 即 wrapperValue

function baseWrapperValue(value, actions) {
	var result = value;
	// 若是是lazyWrapper的實例,則調用LazyWrapper.prototype.value 方法,也就是 lazyValue 方法
	if (result instanceof LazyWrapper) {
		result = result.value();
	}
	// 相似 [].reduce(),把上一個函數返回結果做爲參數傳遞給下一個函數
	return arrayReduce(actions, function(result, action) {
		return action.func.apply(action.thisArg, arrayPush([result], action.args));
	}, result);
}
function wrapperValue() {
	return baseWrapperValue(this.__wrapped__, this.__actions__);
}
lodash.prototype.toJSON = lodash.prototype.valueOf = lodash.prototype.value = wrapperValue;
複製代碼

若是是惰性求值,則調用的是 LazyWrapper.prototype.valuelazyValue

LazyWrapper.prototype.value 即 lazyValue 惰性求值

點擊這裏展開lazyValue源碼及註釋
function LazyWrapper(value) {
	// 參數 value
	this.__wrapped__ = value;
	// 執行的函數
	this.__actions__ = [];
	this.__dir__ = 1;
	// 過濾
	this.__filtered__ = false;
	// 存儲迭代器函數
	this.__iteratees__ = [];
	// 默認最大取值個數
	this.__takeCount__ = MAX_ARRAY_LENGTH;
	// 具體取值多少個,存儲函數和類型
	this.__views__ = [];
}
/** * Extracts the unwrapped value from its lazy wrapper. * * @private * @name value * @memberOf LazyWrapper * @returns {*} Returns the unwrapped value. */
function lazyValue() {
	// this.__wrapped__ 是 new LodashWrapper 實例 因此執行.value 獲取原始值
	var array = this.__wrapped__.value(),
		//
		dir = this.__dir__,
		// 是不是函數
		isArr = isArray(array),
		// 是否從右邊開始
		isRight = dir < 0,
		// 數組的長度。若是不是數組,則是0
		arrLength = isArr ? array.length : 0,
		// 獲取 take(3) 上述例子中 則是 start: 0,end: 3
		view = getView(0, arrLength, this.__views__),
		start = view.start,
		end = view.end,
		// 長度 3
		length = end - start,
		// 若是是是從右開始
		index = isRight ? end : (start - 1),
		// 存儲的迭代器數組
		iteratees = this.__iteratees__,
		// 迭代器數組長度
		iterLength = iteratees.length,
		// 結果resIndex
		resIndex = 0,
		// 最後獲取幾個值,也就是 3
		takeCount = nativeMin(length, this.__takeCount__);

	// 若是不是數組,或者 不是從右開始 而且 參數數組長度等於take的長度 takeCount等於長度
	// 則直接調用 baseWrapperValue 不須要
	if (!isArr || (!isRight && arrLength == length && takeCount == length)) {
		return baseWrapperValue(array, this.__actions__);
	}
	var result = [];

	// 標籤語句 label
	// MDN label 連接
	// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/label
	// 標記語句能夠和 break 或 continue 語句一塊兒使用。標記就是在一條語句前面加個能夠引用的標識符(identifier)。
	outer:
	while (length-- && resIndex < takeCount) {
		index += dir;

		var iterIndex = -1,
			// 數組第一項
			value = array[index];

		while (++iterIndex < iterLength) {
			// 迭代器數組 {iteratee: function{}, typy: 2}
			var data = iteratees[iterIndex],
				iteratee = data.iteratee,
				type = data.type,
				// 結果 迭代器執行結果
				computed = iteratee(value);

			if (type == LAZY_MAP_FLAG) {
				// 若是 type 是 map 類型,結果 computed 賦值給value
				value = computed;
			} else if (!computed) {
				if (type == LAZY_FILTER_FLAG) {
					// 退出當前此次循環,進行下一次循環
					continue outer;
				} else {
					// 退出整個循環
					break outer;
				}
			}
		}
		// 最終數組
		result[resIndex++] = value;
	}
	// 返回數組 例子中則是 [2, 3, 4]
	return result;
}
// Ensure `LazyWrapper` is an instance of `baseLodash`.
LazyWrapper.prototype = baseCreate(baseLodash.prototype);
LazyWrapper.prototype.constructor = LazyWrapper;

LazyWrapper.prototype.value = lazyValue;
複製代碼

筆者畫了一張 lodashLazyWrapper的關係圖來表示。

`lodash`和`LazyWrapper`的關係圖

小結:lazyValue簡單說實現的功能就是把以前記錄的須要執行幾回,把記錄存儲的函數執行幾回,不會有多少項數據就執行多少次,而是根據須要幾項,執行幾項。 也就是說如下這個例子中,map函數只會執行3次。若是沒有用惰性求值,那麼map函數會執行5次。

var result = _.chain([1, 2, 3, 4, 5])
.map(el => el + 1)
.take(3)
.value();
複製代碼

總結

行文至此,基本接近尾聲,最後總結一下。

文章主要學習了runInContext() 導出_ lodash函數使用baseCreate方法原型繼承LodashWrapperLazyWrappermixin掛載方法到lodash.prototype、後文用結合例子解釋lodash.prototype.value(wrapperValue)Lazy.prototype.value(lazyValue)惰性求值的源碼具體實現。

分享一個只知道函數名找源碼定位函數申明位置的VSCode 技巧Ctrl + p。輸入 @functionName 定位函數functionName在源碼文件中的具體位置。若是知道調用位置,那直接按alt+鼠標左鍵便可跳轉到函數申明的位置。

若是讀者發現有不妥或可改善之處,再或者哪裏沒寫明白的地方,歡迎評論指出。另外以爲寫得不錯,對您有些許幫助,能夠點贊、評論、轉發分享,也是對筆者的一種支持。萬分感謝。

推薦閱讀

lodash github倉庫
lodash 官方文檔
lodash 中文文檔
打造一個相似於lodash的前端工具庫
惰性求值——lodash源碼解讀
luobo tang:lazy.js 惰性求值實現分析
lazy.js github 倉庫
本文章學習的lodash的版本v4.17.15 unpkg.com連接

筆者往期文章

學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫
學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫
面試官問:JS的繼承
面試官問:JS的this指向
面試官問:可否模擬實現JS的call和apply方法
面試官問:可否模擬實現JS的bind方法
面試官問:可否模擬實現JS的new操做符
前端使用puppeteer 爬蟲生成《React.js 小書》PDF併合並

關於

做者:常以若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
我的博客-若川,使用vuepress重構了,閱讀體驗可能更好些
掘金專欄,歡迎關注~
segmentfault前端視野專欄,歡迎關注~
知乎前端視野專欄,歡迎關注~
github blog,相關源碼和資源都放在這裏,求個star^_^~

微信公衆號 若川視野

可能比較有趣的微信公衆號,長按掃碼關注。也能夠加微信 lxchuan12,註明來源,拉您進【前端視野交流羣】。

微信公衆號  若川視野
相關文章
相關標籤/搜索