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);

  },

  configurabletrue.

})

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