One, foreword
The core idea behind executing a function once is simple: “The function needs to be called by recording whether it has been executed or not.”
function start() {
console.log('invoke start function');
}
// Record the status
let invoked = false;
if(! invoked) {
invoked = ture;
start();
}
Copy the code
But there is a huge drawback to using the above implementation directly: “a lot of template code.”
Next, let’s explore how to optimize it step by step.
Second, the closure
Bind the state to the target function using the closure’s “extend the life of a variable” feature:
function once(fn) {
let called = false;
return function _once() {
if (called) {
return _once.value;
}
called = true;
_once.value = fn.apply(this.arguments);
}
}
Copy the code
The above code uses the called variable declared in the closure to hold the status of whether the current function is executed or not, and records the return value of the function’s first execution for later use.
The usage is as follows:
let index = 0;
const increment = (a)= > index++;
const onceIncrement = once(increment);
onceIncrement();
onceIncrement();
onceIncrement();
console.log(index); / / 1
Copy the code
Metaprogramming
Use ES6’s metaprogramming Reflect API to define this as the behavior of a function:
Reflect.defineProperty(Function.prototype, 'once', {
value () {
return once(this);
},
configurable: true.
})
Copy the code
It can then be used like this:
let index = 0;
const increment = (a)= > index++;
const onceIncrement = increment.once();
onceIncrement();
onceIncrement();
onceIncrement();
console.log(index); / / 1
Copy the code
Four, the loss of their own attributes
Although the ability to execute functions once is encapsulated as a general utility function, there are still some problems. Because the closure returns a new function, some attributes carried by the original function are lost:
const increment = (a)= > index++;
increment._flag_ = true;
const onceIncrement = increment.once();
console.log(onceIncrement._flag_); // undefined
Copy the code
When you return the _once function, you need to copy the properties of the original function.
When JavaScript gets an object’s property set, there are several methods:
- Keys: Gets its own enumerable collection of properties
- Object. GetOwnPropertyNames: contains an enumeration attribute collection, but does not include the Symbol properties
- Object. GetOwnPropertySymbols: access to their own all the Symbol properties
- Reflect.ownKeys: Gets all of its own properties
const once = fn= > {
let called = false;
const _once = function () {
if (called) {
return _once.value;
}
called = true;
_once.value = fn.apply(this.arguments);
}
for (const property of Reflect.ownKeys(fn)) {
copyProperty(to, from, property)
}
return _once;
}
Copy the code
When we get the set of all the attributes of the object itself, we cannot simply assign the value through the equal sign. Here we mainly consider:
- The function itself has some unique attributes that should not be overridden
- Keep the property descriptor of the original property
function copyProperty(to, from, property) {
// Some special attributes should not be copied
if (property === 'length' || property === 'prototype' || property === 'arguments' || property === 'caller') {
return;
}
const toDescriptor = Object.getOwnPropertyDescriptor(to, property);
const fromDescriptor = Object.getOwnPropertyDescriptor(from, property);
if(! canCopyProperty(toDescriptor, fromDescriptor)) {
return;
}
Object.defineProperty(to, property, fromDescriptor);
}
function canCopyProperty(toDescriptor, fromDescriptor) {
if (toDescriptor === undefined) {
return true;
}
if (toDescriptor.configurable) {
return true;
}
if (toDescriptor.writable === fromDescriptor.writable &&
toDescriptor.enumerable === fromDescriptor.enumerable &&
toDescriptor.configurable === fromDescriptor.configurable &&
(toDescriptor.writable || toDescriptor.value === fromDescriptor.value)) {
return true;
}
return false;
}
Copy the code
Properties on the prototype chain
When an object does not have an attribute, JavaScript’s look-up mechanism works its way up its prototype chain, so you need to consider the correct setting of the prototype chain:
const changePrototype = (to, from) = > {
const fromPrototype = Object.getPrototypeOf(from);
if (fromPrototype === Object.getPrototypeOf(to)) {
return;
}
Object.setPrototypeOf(to, fromPrototype);
};
Copy the code
Write at the end
The above is the content of this article, I hope to bring you help, welcome to “attention”, “like”, “forward”.
References:
- https://github.com/isaacs/once
- https://github.com/sindresorhus/onetime