Series of articles:

  1. Read one NPM module (1) – username every day
  2. Read one NPM module (2) – MEM every day

After reading the meM source code yesterday, I pointed out that there are problems with the results when the parameter is of type RegExp. Today, I thought about it again and found that the same problem exists for Symbol type. After discussion with author Sindre Sorhus through MEM-Issue #20, we have come up with a preliminary solution. We believe this bug will be fixed in the near future 😊

A one-sentence introduction

Mimic fn means to imitate. Mimic mimics the behavior of the original function by copying the original function. It can expand functions of the function without modifying the original function.

usage

const mimicFn = require('mimic-fn');

function foo() {}
foo.date = '2018-08-27';

function wrapper() {
	return foo() {};
}

console.log(wrapper.name);
//=> 'wrapper'

// After copying foo here,
// Foo has functions that wrapper has
mimicFn(wrapper, foo);

console.log(wrapper.name);
//=> 'foo'

console.log(wrapper.date);
/ / = > '2018-08-27'
Copy the code

The source code to learn

The difficulty of mimic-FN is how to obtain all the attributes of the original function and assign them to the new function. Actually the source code is very, very, very short (important things say three times) :

/ / source 3 to 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;
};
Copy the code

The source code is only four or five lines long, but it’s worth looking into property descriptors, which are very fundamental to JavaScript.

Introduction to attribute descriptors

Const obj = {x: 1} is the simplest object, and x is an attribute of obj. ES5 gives us the ability to customize attribute X. Object.defineproperty (obj, ‘x’, descriptor) can do some interesting things:

Properties that cannot be modified

const obj = {};

// The x property cannot be modified
Object.defineProperty(obj, 'x', {
   value: 1.writable: false});console.log(obj.x);
/ / = > 1

obj.x = 2;
console.log(obj.x);
/ / = > 1
Copy the code

Attributes that cannot be deleted

const obj = {};

// Define y attributes that cannot be deleted
Object.defineProperty(obj, 'y', {
    value: 1.configurable: false});console.log(obj.y);
/ / = > 1

console.log(delete obj.y);
// => false

console.log(obj.y);
/ / = > 1
Copy the code

Properties that cannot be traversed

const obj = {};

// Define z attributes that cannot be traversed
Object.defineProperty(obj, 'z', {
    value: 1.enumerable: false});console.log(obj, obj.z);
/ / = > {}, 1

for (const key in obj) {
    console.log(key, obj[key]);
}
// => No output is displayed
Copy the code

Different attributes for input and output

const obj = {};

// Define u attributes that differ between input and output
Object.defineProperty(obj, 'u', {
    get: function() {
        return this._u * 2;
    },
    set: function(value) {
        this._u = value; }}); obj.u =1;
console.log(obj.u);
/ / = > 2

Copy the code

From the above example can learn through property value | of the descriptor writable | configurable | enumerable | set | get fields can realize the magical effect, believe that their meaning you can guess, The following introduction is taken from MDN-object.defineProperty () :

  • The description of this property can be changed and deleted from any additional device only if and if the property is true, works freely and freely. The default is false.
  • Enumerable: An attribute can appear in an object’s enumerable property if and only if its Enumerable is true. The default is false.
  • Value: indicates the value of the attribute. It can be any valid JavaScript value (numeric value, object, function, etc.). The default is undefined.
  • Writable: Value can be changed by the assignment operator if and only if the writable of this property is true. The default is false.
  • Get: A method that provides a getter for a property, or if there is no getterundefined.
  • Set: a method that provides a setter for a property, or if no setter existsundefined. This method is triggered when the property value is modified. This method takes a unique parameter, the new parameter value for the property.

Note that attribute descriptors fall into two categories:

  • Data descriptor (data descriptor) : can be set up configurable | enumerable value | | writable.
  • Storage descriptor (access descriptor) : can be set up configurable | enumerable | get | set.

As you can see, it is not possible for a property to set both value and GET, or writable and set, etc.

For the property x of our most common object argument const obj = {x: 1}, the value of the property descriptor is:

{ 
	value: 1.writable: true.enumerable: true.configurable: true,}Copy the code

The property descriptor of a function

Known everything in JavaScript Object, so the function also has its own property descriptors, through Object. GetOwnPropertyDescriptors () to look for a defined function, it has which attributes:

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}}Copy the code

From the above code, we can see that the function has five attributes, which are:

  1. Length: Number of parameters defined by the function.

  2. Name: function name, note that its writable is false, so directly changing the function name foo.name = bar will not work.

  3. Arguments: Arguments to function execution, is an array of classes, cannot be used in ‘use strict’ mode. For ES6+, the same functionality can be achieved through Rest Parameters and still be used in strict mode.

    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]
    Copy the code
  4. 14. Caller: a caller pointing to a function that cannot be used in ‘use strict’ mode:

    
    function foo() { console.log(foo.caller) }
    
    function bar() { foo() }
    
    bar();
    // => [Function: bar]
    Copy the code
  5. Prototype: refers to the function’s prototype chain, which is not expanded here.

Property descriptor operation

Knowing the fields and effects of property descriptors, you should of course try to modify them. There are four ways to modify them in JavaScript:

  • DefineProperty (OBj, prop, Descriptor) : The descriptor of an existing property can be changed if the property is configured with true.
  • Object.preventextensions (obj) : Prevent obj from adding new properties.
  • Object.seal(obj) : Prevents obj from adding new attributes or deleting existing ones.
  • Object.freeze(obj) : Prevents obj from adding new properties, deleting existing properties, or updating existing properties.

You can do some interesting things with these functions, such as preventing new elements from being added or removed from an array:

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]
Copy the code

Back to the source code

Now it’s easy to look at the mimic-FN source code. It does two things:

  1. Reads the properties of the original function.
  2. Sets the properties of the original function to the new function.
/ / source 3 to 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;
};
Copy the code

There is only one explanation for this code: getOwnPropertyNames cannot be obtained when the object property is of type Symbol and needs to be accessed via 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) ]
Copy the code

Can see the Object. GetOwnPropertyNames () can only get x, and Object. The getOwnPropertySymbols (obj) can only get the Symbol (‘ elvin), if they are used together can obtain all the properties of the Object.

For node.js >= 6.0, you can use reflect.ownkeys (obj) to achieve the same function, and the code is much simpler, so I tried to make the following changes:

module.exports = (to, from) = > {
	for (const prop of Reflect.ownKeys(from)) {
		Object.defineProperty(to, prop, Object.getOwnPropertyDescriptor(from, prop));
	}

	return to;
};
Copy the code

The above code has now been incorporated into the latest master branch. Details can be found in mimic-fn PR#9.

Write in the last

What I’m going to write today is almost never used at work, so if you ask what’s the use of knowing this?

It’s useless to know this, but it’s ok to forget it after reading it. Just have fun, but just have a little more understanding of JavaScript internals.

About me: graduated from huake, working in Tencent, elvin’s blog welcome to visit ^_^