天天閱讀一個 npm 模塊(3)- mimic-fn

系列文章:javascript

  1. 天天閱讀一個 npm 模塊(1)- username
  2. 天天閱讀一個 npm 模塊(2)- mem

昨天閱讀 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} 是最簡單的對象,xobj 的一個屬性。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()數組

  • configurable:當且僅當該屬性的 configurable 爲 true 時,該屬性描述符纔可以被改變,同時該屬性也能從對應的對象上被刪除。默認爲 false。
  • enumerable:當且僅當該屬性的 enumerable 爲 true 時,該屬性纔可以出如今對象的枚舉屬性中。默認爲 false。
  • value:該屬性對應的值。能夠是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 undefined。
  • writable:當且僅當該屬性的 writable 爲 true 時,value 才能被賦值運算符改變。默認爲 false。
  • get:一個給屬性提供 getter 的方法,若是沒有 getter 則爲 undefined
  • set:一個給屬性提供 setter 的方法,若是沒有 setter 則爲 undefined。當屬性值修改時,觸發執行該方法。該方法將接受惟一參數,即該屬性新的參數值。

須要注意的是,屬性描述符分爲兩類:app

  • 數據描述符(data descriptor):可設置 configurable | enumerable |value | writable。
  • 存儲描述符(access descriptor):可設置 configurable | enumerable | get | set。

能夠看出,一個屬性不可能同時設置 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 個屬性,分別爲:

  1. length:函數定義的參數個數。

  2. name:函數名,注意其 writable 爲 false,因此直接改變函數名 foo.name = bar 是不起做用的。

  3. 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 ]
    複製代碼
  4. caller:指向函數的調用者,在 'use strict' 嚴格模式下沒法使用:

    function foo() { console.log(foo.caller) }
    
    function bar() { foo() }
    
    bar();
    // => [Function: bar]
    複製代碼
  5. prototype:指向函數的原型,與 JavaScript 中的原型鏈相關,這裏不作展開。

屬性描述符操做

知道了屬性描述符的字段和做用,那麼固然要嘗試對其進行修改,在 JavaScript 中有四種方法能夠對其進行修改,分別爲:

  • Object.defineProperty(obj, prop, descriptor):當屬性的 configurable 爲 true 時,能夠對已有的屬性的描述符進行變動。
  • Object.preventExtensions(obj):阻止 obj 被添加新的屬性。
  • Object.seal(obj):阻止 obj 被添加新的屬性或者刪除已有的屬性。
  • Object.freeze(obj):阻止 obj 被添加新的屬性、刪除已有的屬性或者更新已有的屬性。

經過這些函數能夠實現一些有意思的功能,例如阻止數組新添或刪除元素:

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 的源碼就十分簡單了,其實它只作了兩件事情:

  1. 讀取原函數的屬性。
  2. 將原函數的屬性設置到新函數上。
// 源碼 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 的博客 歡迎來訪 ^_^

相關文章
相關標籤/搜索