系列文章:javascript
昨天閱讀 mem 的源碼以後,提出了當參數爲 RegExp 類型時,運行結果會存在問題。今天又仔細思考了一下,對於 Symbol 類型,也會存在一樣的問題。經過 mem - Issue #20 和做者 Sindre Sorhus 討論以後,已經得出了初步的解決方法,相信這個 bug 會在最近被 fix 😊java
今天閱讀的 npm 模塊是 mimic-fn,mimic 的意思是模仿,它經過對原函數的複製從而模仿原函數的行爲,能夠在不修改原函數的前提下,擴充函數的功能,當前版本爲 1.2.0,周下載量約爲 421 萬。git
const mimicFn = require('mimic-fn');
function foo() {}
foo.date = '2018-08-27';
function wrapper() {
return foo() {};
}
console.log(wrapper.name);
//=> 'wrapper'
// 此處複製 foo 函數後,
// foo 擁有的功能,wrapper 均有
mimicFn(wrapper, foo);
console.log(wrapper.name);
//=> 'foo'
console.log(wrapper.date);
//=> '2018-08-27'
複製代碼
實現 mimic-fn 功能的難點在於如何得到原函數全部的屬性並將其賦值給新函數。其實源碼很是很是很是(重要的事情說三遍)短:github
// 源碼 3-1
module.exports = (to, from) => {
for (const prop of Object.getOwnPropertyNames(from).concat(Object.getOwnPropertySymbols(from))) {
Object.defineProperty(to, prop, Object.getOwnPropertyDescriptor(from, prop));
}
return to;
};
複製代碼
雖然源碼只有四五行,可是涉及 JavaScript 中很是核心基礎的內容 —— property descriptor
(屬性描述符),仍是值得好好研究一下的。npm
形如 const obj = {x: 1}
是最簡單的對象,x
是 obj
的一個屬性。ES5 帶給了咱們對屬性 x
進行定製化的能力。經過 Object.defineProperty(obj, 'x', descriptor)
能夠實現一些有意思的效果:segmentfault
const obj = {};
// 定於不能被修改的 x 屬性
Object.defineProperty(obj, 'x', {
value: 1,
writable: false,
});
console.log(obj.x);
// => 1
obj.x = 2;
console.log(obj.x);
// => 1
複製代碼
const obj = {};
// 定義不能被刪除的 y 屬性
Object.defineProperty(obj, 'y', {
value: 1,
configurable: false,
});
console.log(obj.y);
// => 1
console.log(delete obj.y);
// => false
console.log(obj.y);
// => 1
複製代碼
const obj = {};
// 定義不能被遍歷的 z 屬性
Object.defineProperty(obj, 'z', {
value: 1,
enumerable: false,
});
console.log(obj, obj.z);
// => {}, 1
for (const key in obj) {
console.log(key, obj[key]);
}
// => 沒有輸出
複製代碼
const obj = {};
// 定義輸入與輸出不一樣的 u 屬性
Object.defineProperty(obj, 'u', {
get: function() {
return this._u * 2;
},
set: function(value) {
this._u = value;
},
});
obj.u = 1;
console.log(obj.u);
// => 2
複製代碼
從上面的例子中能夠了解到經過屬性描述符的 value | writable | configurable | enumerable | set | get 字段能夠實現神奇的效果,相信它們的含義你們也能猜出來,下面的介紹摘自 MDN - Object.defineProperty():數組
undefined
。undefined
。當屬性值修改時,觸發執行該方法。該方法將接受惟一參數,即該屬性新的參數值。須要注意的是,屬性描述符分爲兩類:app
能夠看出,一個屬性不可能同時設置 value 和 get 或者同時設置 writable 和 set 等。函數
對於咱們最經常使用的對象自變量 const obj = {x: 1}
的屬性 x,其屬性描述符的值爲:post
{
value: 1,
writable: true,
enumerable: true,
configurable: true,
}
複製代碼
衆所周知在 JavaScript 中一切皆對象,因此函數也有本身的屬性描述符,經過 Object.getOwnPropertyDescriptors()
來看看對於一個已定義的函數,其具備哪些屬性:
function foo(x) {
console.log('foo..');
}
console.log(Object.getOwnPropertyDescriptors(foo));
{
length:
{ value: 1,
writable: false,
enumerable: false,
configurable: true },
name:
{ value: 'foo',
writable: false,
enumerable: false,
configurable: true },
arguments:
{ value: null,
writable: false,
enumerable: false,
configurable: false },
caller:
{ value: null,
writable: false,
enumerable: false,
configurable: false },
prototype:
{ value: foo {},
writable: true,
enumerable: false,
configurable: false }
}
複製代碼
從上面的代碼中能夠看出函數一共有 5 個屬性,分別爲:
length:函數定義的參數個數。
name:函數名,注意其 writable
爲 false,因此直接改變函數名 foo.name = bar
是不起做用的。
arguments:函數執行時的參數,是一個類數組,在 'use strict' 嚴格模式下沒法使用。對於 ES6+,能夠經過 Rest Parameters 實現一樣的功能,並且在嚴格模式下仍能使用。
function foo(x) {
console.log('foo..', arguments);
}
function bar(...rest) {
console.log('bar..', rest)
}
foo(); bar();
// => foo.. [Arguments]
// => bar.. []
foo(1); bar(1);
// => foo.. [Arguments] { '0': 1 }
// => bar.. [ 1 ]
foo(1, 2); bar(1, 2);
// => foo.. [Arguments] { '0': 1, '1': 2 }
// => bar.. [ 1, 2 ]
複製代碼
caller:指向函數的調用者,在 'use strict' 嚴格模式下沒法使用:
function foo() { console.log(foo.caller) }
function bar() { foo() }
bar();
// => [Function: bar]
複製代碼
prototype:指向函數的原型,與 JavaScript 中的原型鏈相關,這裏不作展開。
知道了屬性描述符的字段和做用,那麼固然要嘗試對其進行修改,在 JavaScript 中有四種方法能夠對其進行修改,分別爲:
經過這些函數能夠實現一些有意思的功能,例如阻止數組新添或刪除元素:
const arr = [ 1 ];
arr.push(2);
// => TypeError: Cannot add property 1, object is not extensible
arr.pop();
// => TypeError: Cannot delete property '0' of [object Array]
複製代碼
如今再來看 mimic-fn 的源碼就十分簡單了,其實它只作了兩件事情:
// 源碼 3-1
module.exports = (to, from) => {
for (const prop of Object.getOwnPropertyNames(from).concat(Object.getOwnPropertySymbols(from))) {
Object.defineProperty(to, prop, Object.getOwnPropertyDescriptor(from, prop));
}
return to;
};
複製代碼
這段代碼只有一個地方須要解釋一下:當對象的屬性爲 Symbol 類型時,getOwnPropertyNames
沒法得到,須要再經過 getOwnPropertySymbols
得到以後訪問:
const obj= {
x: 1,
[Symbol('elvin')]: 2,
};
console.log(Object.getOwnPropertyNames(obj));
// => [ 'x' ]
console.log(Object.getOwnPropertySymbols(obj));
// => [ Symbol(elvin) ]
console.log(Reflect.ownKeys(obj));
// => [ 'x', Symbol(elvin) ]
複製代碼
能夠看到 Object.getOwnPropertyNames()
只能得到 x,而 Object.getOwnPropertySymbols(obj)
只能得到 Symbol('elvin'),二者一塊兒使用的話則能夠得到對象全部的屬性。
另外對於 Node.js >= 6.0,能夠經過 Reflect.ownKeys(obj)
的方式來實現一樣的功能,並且代碼更加的簡潔,因此我嘗試作了以下的更改:
module.exports = (to, from) => {
for (const prop of Reflect.ownKeys(from)) {
Object.defineProperty(to, prop, Object.getOwnPropertyDescriptor(from, prop));
}
return to;
};
複製代碼
上述代碼目前已被合進最新的 master 分支,詳情可查看 mimic-fn PR#9。
今天所寫的內容在平時工做中其實幾乎不會用到,因此假如你們要問了解這個有什麼用的話?
瞭解這個沒用,看完忘記了也沒問題,開心就好,權當對 JavaScript 內部機制多了一些瞭解。
關於我:畢業於華科,工做在騰訊,elvin 的博客 歡迎來訪 ^_^