每天阅读一个 npm 模块(3)- mimic-fn
发布于 6 年前 作者 elvinn 3316 次浏览 来自 分享

每天阅读一个 npm 模块(3)- mimic-fn

系列文章:

  1. 每天阅读一个 npm 模块(1)- username
  2. 每天阅读一个 npm 模块(2)- mem

昨天阅读 mem 的源码之后,提出了当参数为 RegExp 类型时,运行结果会存在问题。今天又仔细思考了一下,对于 Symbol 类型,也会存在同样的问题。通过 mem - Issue #20 和作者 Sindre Sorhus 讨论之后,已经得出了初步的解决方法,相信这个 bug 会在最近被 fix 😊

一句话介绍

今天阅读的 npm 模块是 mimic-fn,mimic 的意思是模仿,它通过对原函数的复制从而模仿原函数的行为,可以在不修改原函数的前提下,扩充函数的功能,当前版本为 1.2.0,周下载量约为 421 万。

用法

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 功能的难点在于如何获得原函数所有的属性并将其赋值给新函数。其实源码非常非常非常(重要的事情说三遍)短:

// 源码 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(属性描述符),还是值得好好研究一下的。

属性描述符介绍

形如 const obj = {x: 1} 是最简单的对象,xobj 的一个属性。ES5 带给了我们对属性 x 进行定制化的能力。通过 Object.defineProperty(obj, 'x', descriptor) 可以实现一些有意思的效果:

不能被修改的属性

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。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。

需要注意的是,属性描述符分为两类:

  • 数据描述符(data descriptor):可设置 configurable | enumerable |value | writable。
  • 存储描述符(access descriptor):可设置 configurable | enumerable | get | set。

可以看出,一个属性不可能同时设置 value 和 get 或者同时设置 writable 和 set 等。

对于我们最常用的对象自变量 const obj = {x: 1} 的属性 x,其属性描述符的值为:

{ 
	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 的博客 欢迎来访 ^_^

3 回复

所到之处,必留pr

@yuu2lee4 哈哈哈哈哈哈

其实,挺好的文章,,工作基本用不到,哈哈,

回到顶部