這篇內容呢,講的是另一個技術棧 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);
}
確實如此,ToObject
對 val
進行了一個判斷,如果它的值為 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 引擎的兼容性。像是這種代碼,恐怕在大多教科書類的內容中會很少強調,但在實際應用中,為了加強代碼的健壯性,卻是非常重要。這也是讀代碼學習編程的最大好處,讓我們可以從實際生產環境的角度去思考問題。
關於這個開源庫的分析就到這里啦,還是那句話,水平有限,只為拋磚引玉給大家提出一個思路,相信各位的聰明才智一定能夠發現更多。