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

栏目: Node.js · 发布时间: 6年前

内容简介:系列文章:昨天阅读mem 的源码之后,提出了当参数为 RegExp 类型时,运行结果会存在问题。今天又仔细思考了一下,对于 Symbol 类型,也会存在同样的问题。通过今天阅读的 npm 模块是mimic-fn,mimic 的意思是模仿,它通过对原函数的复制从而模仿原函数的行为,可以在不修改原函数的前提下,扩充函数的功能,当前版本为 1.2.0,周下载量约为 421 万。

系列文章:

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

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

一句话介绍

今天阅读的 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()

undefined
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]
复制代码
每天阅读一个 npm 模块(3)- mimic-fn

回到源码

现在再来看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 内部机制多了一些了解。


以上所述就是小编给大家介绍的《每天阅读一个 npm 模块(3)- mimic-fn》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Inside Larry's and Sergey's Brain

Inside Larry's and Sergey's Brain

Richard Brandt / Portfolio / 17 Sep 2009 / USD 24.95

You’ve used their products. You’ve heard about their skyrocketing wealth and “don’t be evil” business motto. But how much do you really know about Google’s founders, Larry Page and Sergey Brin? Inside......一起来看看 《Inside Larry's and Sergey's Brain》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具