昨天阅读 https://www.npmjs.com/package/mem 的源码之后, 提出了当参数为 RegExp 类型时, 运行结果会存在问题. 今天又仔细思考了一下, 对于 Symbol 类型, 也会存在同样的问题. 通过 mem - Issue #20 https://github.com/sindresorhus/mem/issues/20 和作者 Sindre Sorhus 讨论之后, 已经得出了初步的解决方法, 相信这个 bug 会在最近被 fix
一句话介绍
今天阅读的 npm 模块是 https://www.npmjs.com/package/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'
复制代码
源码学习
实现 https://www.npmjs.com/package/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() https://developer.mozilla.org/zh-CN/docs/web/JavaScript/Reference/Global_Objects/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 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/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]
复制代码
回到源码
现在再来看 https://www.npmjs.com/package/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 https://github.com/sindresorhus/mimic-fn/pull/9 .
写在最后
今天所写的内容在平时工作中其实几乎不会用到, 所以假如大家要问了解这个有什么用的话?
了解这个没用, 看完忘记了也没问题, 开心就好, 权当对 JavaScript 内部机制多了一些了解.
来源: https://juejin.im/post/5b84d38e51882542b526d6e5