Node.js 中開源庫探秘 object-assign | 全棧之路


這篇內容呢,講的是另一個技術棧 Node.js 系列,雖然和咱們這里的主題不是特別吻合,不過嘛,汲取多樣性的養分是快速成長的好方法,也是現在流行的全棧工程師的必經之路。

由於這篇內容涉及的是 Node.js 社區相關技術,所以要更好的讀懂相關代碼,還需要有一些 javascript 的基礎知識。

咱們開始進入正題, Node.js 是一套服務端體系,也是非常流行和好用的框架。Node.js 社區高人輩出,比如大名鼎鼎的 TJ Holowaychuk,就是 Node 社區的大神級人物。關於他如何被稱為大神的問題,大家可以參看知乎上的解答:

http://zhuanlan.zhihu.com/FrontendMagazine/19572823

其中有一句話很有意思:

要問到我是“如何”學習的——沒什么特別的地方,也不讀書,從不去聽課,我就是去閱讀別人的代碼,並搞清楚那些代碼是如何工作的。

確實是這樣,讀代碼是一種最好的學習方式。特別是 Node.js 的社區資源非常豐富,數不盡的開源庫。

如何使用

那么咱們就來探索一個叫做 object-assign 的開源庫吧。這個庫的地址在這里:

https://github.com/sindresorhus/object-assign

它的作用也非常的簡單:

var objectAssign = require('object-assign');

objectAssign({foo: 0}, {bar: 1});
//=> {foo: 0, bar: 1}

// multiple sources
objectAssign({foo: 0}, {bar: 1}, {baz: 2});
//=> {foo: 0, bar: 1, baz: 2}

// overwrites equal keys
objectAssign({foo: 0}, {foo: 1}, {foo: 2});
//=> {foo: 2}

// ignores null and undefined sources
objectAssign({foo: 0}, null, {bar: 1}, undefined);
//=> {foo: 0, bar: 1}

這個庫只對外暴露了一個函數,它的作用就是將參數中的幾個對象合並到一起。簡單解釋一下哈。

var objectAssign = require('object-assign'); 這行代碼的作用是將 objc-assign 庫引入到當前的代碼中,並用 objectAssign 符號作為引用。引入完成后,接下來就可以調用了。 比如:

objectAssign({foo: 0}, {bar: 1});

這個調用傳入了兩個對象 - {foo: 0}{bar: 1},函數的作用就是將他們合並成一個對象,然后返回:{foo: 0, bar: 1}

並且,如果參數中對象的屬性有重疊,會用后面對象參數中得屬性覆蓋前面的,比如:

objectAssign({foo: 0}, {foo: 1}, {foo: 2});

這個最終得到的結果是 {foo: 2}。 因為這次參數中的三個對象,都包含 foo 屬性,最后一個將前兩個覆蓋了。

好了,關於這個庫的基本使用方式咱們說完了,接下來就看看它的代碼吧。

分析代碼

object-assign 只有一個源文件,而且,所有的代碼都在這里了:

'use strict';
var propIsEnumerable = Object.prototype.propertyIsEnumerable;

function ToObject(val) {
	if (val == null) {
		throw new TypeError('Object.assign cannot be called with null or undefined');
	}

	return Object(val);
}

function ownEnumerableKeys(obj) {
	var keys = Object.getOwnPropertyNames(obj);

	if (Object.getOwnPropertySymbols) {
		keys = keys.concat(Object.getOwnPropertySymbols(obj));
	}

	return keys.filter(function (key) {
		return propIsEnumerable.call(obj, key);
	});
}

module.exports = Object.assign || function (target, source) {
	var from;
	var keys;
	var to = ToObject(target);

	for (var s = 1; s < arguments.length; s++) {
		from = arguments[s];
		keys = ownEnumerableKeys(Object(from));

		for (var i = 0; i < keys.length; i++) {
			to[keys[i]] = from[keys[i]];
		}
	}

	return to;
};

首先,來看這兩行:

'use strict';
var propIsEnumerable = Object.prototype.propertyIsEnumerable;

第一行 'use strict' 是一個代碼指示,表示這個文件使用 javascript 嚴格語法標准。緊接着的這行,是將 Object.prototype.propertyIsEnumerable 方法做一個引用,以備后面使用。

提到這里順便說一句,javascript 中的函數和變量是都可以賦值給另一個變量的,並且如果變量指向的是一個函數,也可以通過這個變量來調用它指向的函數。這點 Swift 與它比較相似。

然后我們跳過中間部分,來看最下面的部分:

module.exports = Object.assign || function (target, source) {
	var from;
	var keys;
	var to = ToObject(target);

	for (var s = 1; s < arguments.length; s++) {
		from = arguments[s];
		keys = ownEnumerableKeys(Object(from));

		for (var i = 0; i < keys.length; i++) {
			to[keys[i]] = from[keys[i]];
		}
	}

	return to;
};

module.exports 是 Node.js 的一個通用變量,它表示當前模塊對外導出的函數,也就是在外面調用這個模塊時,是 module.exports 中的內容。

賦值操作是通過一個邏輯判斷來進行的,首先判斷了 Object.assign 方法是否存在,如果已經存在就直接用這個方法了。

這個主要是基於 JS 引擎的版本考慮的,老版本的 ECMAScript 6 以下引擎是不支持 Object.assign 函數的,所以我們就必須自己實現,這個判斷的作用就是這樣。

接下來,如果 Object.assign 判斷失敗了,我們就要使用我們自己對 assigin 操作的實現了:

function (target, source) {
	var from;
	var keys;
	var to = ToObject(target);

	for (var s = 1; s < arguments.length; s++) {
		from = arguments[s];
		keys = ownEnumerableKeys(Object(from));

		for (var i = 0; i < keys.length; i++) {
			to[keys[i]] = from[keys[i]];
		}
	}

	return to;
};

這個實現中,對 target 變量調用了 ToObject(target) 方法,實際上是對 target 做了一次非空判斷,我們來看看 ToObject 的實現細節:

function ToObject(val) {
	if (val == null) {
		throw new TypeError('Object.assign cannot be called with null or undefined');
	}

	return Object(val);
}

確實如此,ToObjectval 進行了一個判斷,如果它的值為 null 就拋出異常,如果正常,就返回這個對象。

好了 ToObject 分析完了,我們再回頭看 assign 函數。調用完成后,我們將結果存放到了 to 變量中:

var to = ToObject(target);

接下來,通過一個循環,將后面幾個參數的對象中的屬性與 to 對象進行合並:

for (var s = 1; s < arguments.length; s++) {
  from = arguments[s];
  keys = ownEnumerableKeys(Object(from));

  for (var i = 0; i < keys.length; i++) {
    to[keys[i]] = from[keys[i]];
  }
}

注意這個 - var s = 1; 我們之所以將 s 其實值設置為 1,是因為我們要跳過第一個參數,因為第一個參數就是我們的目標, to 變量的值。

然后將每個參數取出來后,臨時存放到 from 變量中,然后調用 ownEnumerableKeys 函數獲取 from 變量中可遍歷的屬性,我們再來看看 ownEnumerableKeys 函數的定義:

function ownEnumerableKeys(obj) {
	var keys = Object.getOwnPropertyNames(obj);

	if (Object.getOwnPropertySymbols) {
		keys = keys.concat(Object.getOwnPropertySymbols(obj));
	}

	return keys.filter(function (key) {
		return propIsEnumerable.call(obj, key);
	});
}

先調用了 getOwnPropertyNames 獲取了這個對象所有的屬性名。

接下來,判斷了 Object.getOwnPropertySymbols 方法是否存在。這個方法是干什么的呢,這時 ES 6 標准新引進的一個特性,為了防止命名沖突。InfoQ 的這篇文章中有非常詳細的介紹 http://www.infoq.com/cn/articles/es6-in-depth-symbols?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=global

簡單來說,如果我們當前所使用的 javascript 解析引擎是支持 ES 6 的話,那么用 var keys = Object.getOwnPropertyNames(obj); 方法並不能得到所有的屬性鍵值,還需要進行一下這個操作:

if (Object.getOwnPropertySymbols) {
  keys = keys.concat(Object.getOwnPropertySymbols(obj));
}

這樣我們才能得到所有的鍵值。這個其實是新的 javascript 標准中的一個特性,就是 javascript 對象,現在除了屬性名,每個屬性名還對應了不同的 Symbol 之后獲取了這個 Symbol 后才能得到真正的屬性名稱。

着了對 Symbol 做了簡單的介紹,更加詳細的描述,大家可以參考 InfoQ 的那篇文章。

最后,調用了這個方法:

return keys.filter(function (key) {
  return propIsEnumerable.call(obj, key);
});

filter 方法會根據條件篩選數組中的元素,生成一個新的元素。篩選條件就是調用的我們最初看到的 propIsEnumerable 變量中的方法。再次判斷了一下屬性的有效性。

現在,這個屬性集合准備好了。 我們再次回到最初調用的地方:

for (var s = 1; s < arguments.length; s++) {
  from = arguments[s];
  keys = ownEnumerableKeys(Object(from));

  for (var i = 0; i < keys.length; i++) {
    to[keys[i]] = from[keys[i]];
  }
}

這個 for 循環用 ownEnumerableKeys 方法的到要遍歷的后面幾個參數中的有效屬性的名稱,存入 keys 變量中。

然后緊接着,遍歷這個 keys 集合,將 from 對象中得屬性賦值給 to 對象相應的屬性,如果有同名的屬性,就會用 from 中得值覆蓋 to 的原始值。

這樣,object-assign 的所有代碼就都分析完了。

結論回顧

object-assign 的代碼量非常少,但是經過咱們這樣分析一下,是不是感覺麻雀雖小,五臟俱全呢。這里面包含了很多 javascript 的特性,以及大部分教程類書籍都不常提及的細節處理。比如:

為什么要使用 module.exports = Object.assign ||function(){ ... } 這樣的寫法。它用來判斷 JS 引擎的兼容性。像是這種代碼,恐怕在大多教科書類的內容中會很少強調,但在實際應用中,為了加強代碼的健壯性,卻是非常重要。這也是讀代碼學習編程的最大好處,讓我們可以從實際生產環境的角度去思考問題。

關於這個開源庫的分析就到這里啦,還是那句話,水平有限,只為拋磚引玉給大家提出一個思路,相信各位的聰明才智一定能夠發現更多。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM