每天阅读一个 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》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

CSS禅意花园

CSS禅意花园

[美] Dave Shea、Molly E. Holzschlag / 陈黎夫、山崺颋 / 人民邮电出版社 / 2007-6 / 49.00元

这本书的作者是世界著名的网站设计师,书中的范例来自网站设计领域最著名的网站——CSS Zen Garden(CSS禅意花园)。全书分为两个主要部分。第1章为第一部分,讨论网站“CSS禅意花同”及其最基本的主题,包含正确的标记结构和灵活性规划等。第二部分包括6章,占据了书中的大部分篇幅。 每章剖析“CSS禅意花园”收录的6件设计作品,这些作品围绕一个主要的设计概念展开,如文字的使用等。通过探索......一起来看看 《CSS禅意花园》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器