每天阅读一个 npm 模块(3)- mimic-fn
系列文章:
昨天阅读 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}
是最简单的对象,x
是 obj
的一个属性。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 个属性,分别为:
-
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 中有四种方法可以对其进行修改,分别为:
- 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 的源码就十分简单了,其实它只做了两件事情:
- 读取原函数的属性。
- 将原函数的属性设置到新函数上。
// 源码 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 的博客 欢迎来访 ^_^