系列文章: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 的博客 欢迎来访 ^_^