Function encapsulation

Function encapsulation is the combination of function-related data and behavior that hides the internal processing from callers.

Function encapsulation is often overlooked and easily broken. First look at the following “traffic light” effect implementation:

The HTML and CSS are as follows:

    html.body {
      width: 100%;
      height: 100%;
      padding: 0;
      margin: 0;
      overflow: hidden;

      /* Set the layout of HTML and body elements to elastic */
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }
    header {
      line-height: 2rem;
      font-size: 1.2 rem;
      margin-bottom: 20px;
    }
    .traffic { /* Set the class=traffic element to an elastic layout, with its children arranged from top to bottom */
      padding: 10px;
      display: flex;
      flex-direction: column;
    }
    .traffic .light {
      width: 100px;
      height: 100px;
      background-color: # 999;
      border-radius: 50%;
    }

    /* Set the background color of the first class=light element under the class=traffic & class=pass element to green */
    .traffic.pass .light:nth-child(1) {
      background-color: #0a6; / * green * /
    }
    .traffic.wait .light:nth-child(2) {
      background-color: #cc0; / * yellow light * /
    }
    .traffic.stop .light:nth-child(3) {
      background-color: #c00; / * * / a red light
    }
Copy the code
  <header>Simulated traffic lights</header>
  <main>
    <div class="traffic pass">
      <div class="light"></div>
      <div class="light"></div>
      <div class="light"></div>
    </div>
  </main>
Copy the code

The specific requirements are as follows: simulate the traffic light signal and switch the green light (pass state), yellow light (wait state) and red light (stop state) in cycles of 5 seconds, 1.5 seconds and 3.5 seconds respectively.

Here is a simple implementation:

const traffic = document.querySelector('.traffic');

function loop() {
  traffic.className = 'traffic pass';
  setTimeout(() = > {
    traffic.className = 'traffic wait';
    setTimeout(() = > {
      traffic.className = 'traffic stop';
      setTimeout(loop, 3500);
    }, 1500);
  }, 5000);
}

loop();
Copy the code

The code executes as follows: get the class=traffic element, and then in the loop function, first set the class of the traffic element to traffic pass, that is, the green light; Then the setTimeout method is nested, and the first layer is executed after 5 seconds, which will change the traffic class to Tranffic wait, that is, the yellow light. Then the setTimeout method is executed after 1.5 seconds and turns red. Then it turns green 3.5 seconds later. Repeat the run.

The above code has a major design flaw: the loop function accesses the external environment TRAFFIC. There are two problems:

  1. If you modify ittrafficElement, the function will not work
  2. If you reuse this function somewhere else, you have to rebuild thistrafficObject.

Both of these problems are because the encapsulation of functions is completely broken.

Therefore, you cannot write traffic directly into the loop function.

As follows, passed in as a parameter:

const traffic = document.querySelector('.traffic');

function loop(subject) {
  subject.className = 'traffic pass';
  setTimeout(() = > {
    subject.className = 'traffic wait';
    setTimeout(() = > {
      subject.className = 'traffic stop';
      setTimeout(loop.bind(null, subject), 3500);
    }, 1500);
  }, 5000);
}

loop(traffic);
Copy the code

There should be no variables inside the function body that are entirely external to the environment, unless the function is not intended to be reused.

Currently, there are other data in the loop that are written “dead” in the code, and if not extracted, the code is still poorly reusable.

The bind function changes the scope of the function execution, and returns a new function as a list of arguments. It doesn’t execute the original function directly

Realization of asynchronous state switch function encapsulation

How do you encapsulate a function that can switch between the states of a particular piece of data?

A function is simply the smallest unit of processing data. It consists of two parts: data and processing. To make a function universal, you can abstract the data as well as the process.

Step 1: Data abstraction

Data abstraction is the definition and aggregation of data into objects that can be processed by a particular process. In a nutshell, it is the structuring of data.

Abstract data is the redefinition and combination of data into a specific structure of the object, used by a process to complete the required task.

For the asynchronous state switch above, first, find the data to process: state pass, wait, and stop, and switch times of 5 seconds, 1.5 seconds, and 3.5 seconds.

To strip data out of the loop function:

const traffic = document.querySelector('.traffic');

function signalLoop(subject, signals = []) {
  const signalCount = signals.length;
  function updateState(i) {
    let mod=i % signalCount; // Prevent I +1 from continuously increasing
    const {signal, duration} = signals[mod];
    subject.className = signal;
    setTimeout(updateState.bind(null, mod + 1), duration); // Execute I +1 to the next state
  }
  updateState(0);
}

// Data abstraction
const signals = [
  {signal: 'traffic pass'.duration: 5000},
  { signal: 'traffic wait'.duration: 1500 },
  { signal: 'traffic stop'.duration: 3500},]; signalLoop(traffic, signals);Copy the code

Abstract the state and time into an array of three objects and pass this structured data to the signalLoop method. A recursive call to the updateState method implements the state switch.

Data abstracted code can be adapted to business needs at different states and times by modifying the data abstraction without modifying the signalLoop method.

However, signalLoop method has not been fully encapsulated after data abstraction reconstruction at present. The signalLoop function contains a part of the code that changes the external state.

The part of the code that changes the external state is called a side-effect. In general, you can strip out the code inside the function body that has side effects to improve the versatility, stability, and testability of the function.

Step 2: De-side effects

In signalLoop, subject.classname = signal; Changes the state of the external, since subject is an external variable, this sentence changes the className state of the variable. So it needs to be stripped out of the function:

const traffic = document.querySelector('.traffic');

function signalLoop(subject, signals = [], onSignal) {
  const signalCount = signals.length;
  function updateState(i) {
    let mod=i % signalCount; // Prevent I +1 from continuously increasing
    const {signal, duration} = signals[mod];
    if(typeof onSignal === "function"){
        onSignal(subject, signal);
    }
    setTimeout(updateState.bind(null, mod + 1), duration);
  }
  updateState(0);
}

// Data abstraction
const signals = [
  {signal: 'pass'.duration: 5000},
  { signal: 'wait'.duration: 1500 },
  { signal: 'stop'.duration: 3500},]; signalLoop(traffic, signals,(subject, signal) = > {
  subject.className = `traffic ${signal}`;
});
Copy the code

As above, the operation to change the external variable is passed to singalLoop as a callback. This modification improves the versatility of the signalLoop function, which can also be used to perform state transitions for other DOM elements.

This encapsulation improves the “purity” of signalLoop functions.

The “semantics” and readability of code

The code for the above versions of the function is not very readable.

To improve the readability of asynchronous state-switching code, use the ES6 asynchronous specification, Promise, and refactor the code:

function wait(ms) {
  return new Promise((resolve) = > {
    setTimetout(resolve, ms);
  });
}
Copy the code

This code encapsulates the setTimeout method as a wait function. This function wraps the setTimeout method around a Promise and returns the Promise object.

Thus, the somewhat obscure setTimeout can be nested into an await loop in an async function:

function wait(ms) {
  return new Promise((resolve) = > {
    setTimeout(resolve, ms);
  });
}

const traffic = document.querySelector('.traffic');

(async function () {
  while(1) {
    await wait(5000);
    traffic.className = 'traffic wait';
    await wait(1500);
    traffic.className = 'traffic stop';
    await wait(3500);
    traffic.className = 'traffic pass';
  }
}());
Copy the code

Change the loop method to call the function immediately, and change the three setTimeout sections to a while loop. The parts of the loop body are easy to understand: Wait 5 seconds -> Change the className attribute of traffic to Traffic wait -> Wait 1.5 seconds -> Change the className attribute of traffic to traffic Stop -> Wait 3.5 seconds -> Change the className attribute of traffic to traffic pass.

The readability is much better than before.

Again, modify the version of signalLoop with a Promise

function wait(ms) {
  return new Promise((resolve) = > {
    setTimeout(resolve, ms);
  });
}

const traffic = document.querySelector('.traffic');

async function signalLoop(subject, signals = [], onSignal) {
  const signalCount = signals.length;
  for(let i = 0; ; i++) {let mod=i % signalCount;
    const {signal, duration} = signals[mod];
    if(typeof onSignal === "function") {await onSignal(subject, signal);
    }
    awaitwait(duration); i=mod; }}const signals = [
  {signal: 'pass'.duration: 5000},
  { signal: 'wait'.duration: 1500 },
  { signal: 'stop'.duration: 3500},]; signalLoop(traffic, signals,(subject, signal) = > {
  subject.className = `traffic ${signal}`;
});
Copy the code

This time it’s about refactoring inside the code. Using async/await reduces asynchronous recursion to a loop that is easier to read and understand, and also allows the onSignal callback to be an asynchronous procedure, further increasing the use of the signalLoop function.

Promsie and Async /await create not just syntax, but a new semantics.

Code is read by humans and only occasionally executed by computers.

The correctness and efficiency of functions

Correctness of code is more important than encapsulation and readability.

In real development, we might write the wrong code without even knowing it. For example: the pitfalls of the shuffle algorithm.

A lottery scenario: Given a set of generated lottery numbers, a module needs to be implemented. The function of the module is to break up the group of numbers (i.e., shuffle cards) and output a winning number.

The JS fragment of the number is as follows:

function shuffle(items) {
  return [...items].sort((a, b) = > Math.random() > 0.5 ? -1 : 1);
}
Copy the code

A real project would be complex, and the lottery code would not run on the client side, but on the server side.

The problem with this code is that random methods aren’t random enough at all.

For example, pass the following test:

function shuffle(items) {
  return items.sort((a, b) = > Math.random() > 0.5 ? -1 : 1);
}

const weights = Array(9).fill(0);

for(let i = 0; i < 10000; i++) {
  const testItems = [1.2.3.4.5.6.7.8.9];
  shuffle(testItems);
  testItems.forEach((item, idx) = > weights[idx] += item);
}

console.log(weights);

// [44645, 45082, 49934, 50371, 50903, 50344, 50677, 52427, 55617]
// The result changes each time, but generally speaking, the first digit is smaller and the second digit is larger
Copy the code

Shuffle the numbers 1 to 9 randomly 10,000 times, then add up each digit to get the total.

Run several times, check to find the sum array, basically is the front number is smaller, the back number is larger.

This means that larger numbers are more likely to appear at the end of the array.

The reason for this is that there is a sort algorithm inside the sort method of arrays. We don’t know the implementation of this algorithm, but generally a sort algorithm uses some sort of rule to take two elements in sequence and compare them, and then swap places based on the comparison result.

This algorithm gives sorting a random comparison operator (a, b) => math.random () > 0.5? -1:1, so that the exchange process of array elements code randomness, but the randomness of the exchange process can not guarantee that every element has the same probability to appear in every position mathematically, because the sorting algorithm for elements at each position and other elements exchange order, times are different.

To implement a fairly random algorithm, you take one element out of the array at a time, place it in a new queue until it’s all gone, and you get a random permutation. Random (regardless of the randomness inherent in the JavaScript engine’s math. random function) guarantees that every position of an element in the array is retrieved with the same probability.

As follows:

function shuffle(items) {
  items = [...items];
  const ret = [];
  while(items.length) {
    const idx = Math.floor(Math.random() * items.length); // Randomly get the positions of the elements in the array
    const item = items.splice(idx, 1) [0];
    ret.push(item);
  }
  return ret;
}

let items = [1.2.3.4.5.6.7.8.9];
items = shuffle(items);
console.log(items);
Copy the code

Each time you randomly pick an element from the array, remove that element from the original copy of the array (a copy is created here so as not to affect the array of primiples) and put it into the new array, so that each number is guaranteed to have the same probability at each location.

The processing of this algorithm is no problem, but in efficiency, because of the splice method, the algorithm time complexity is O(n^2).

To make things faster, instead of splice pulling elements one by one from the original array copy, you can swap elements in the original array copy by swapping randomly placed elements with the “I” (current) element of the array on each extraction.

As follows:

function shuffle(items) {
  items = [...items];
  for(let i = items.length; i > 0; i--) {
    const idx = Math.floor(Math.random() * i);
    [items[idx], items[i - 1]] = [items[i - 1], items[idx]];
  }
  return items;
}

let items = [1.2.3.4.5.6.7.8.9];
items = shuffle(items);
console.log(items);
Copy the code

Swap the first I element with the ith element (subscript I -1) at random, and then subtract I by 1 until I is less than 1.

The time complexity of this algorithm is order n. Performance will be better.

We can go one step further because there is a limit to how many times a user can draw a lucky number, and if a lucky number has already been drawn as many times as possible, there is no need to draw further, so there is no need to completely randomize the entire array.

At this point, you can use a generator instead:

function* shuffle(items) {
  items = [...items];
  for(let i = items.length; i > 0; i--) {
    const idx = Math.floor(Math.random() * i);
    [items[idx], items[i - 1]] = [items[i - 1], items[idx]];
    yield items[i - 1]; }}let items = [1.2.3.4.5.6.7.8.9];
items = shuffle(items);
console.log(... items);Copy the code

By changing return to yield, you can change a function to a generator, either for partial shuffling or for raffles.

As follows, 5 numbers are randomly selected from 100 numbers:

function *shuffle(items) {
  items = [...items];
  for(let i = items.length; i > 0; i--) {
    const idx = Math.floor(Math.random() * i);
    [items[idx], items[i - 1]] = [items[i - 1], items[idx]];
    yield items[i - 1]; }}let items = [...new Array(100).keys()];

let n = 0;
// 5 of 100 numbers are randomly selected
for(let item of shuffle(items)) {
  console.log(item);
  if(n++ >= 5) break;
}
Copy the code

This article is part of the “Gold Nuggets For Free!” Event, click to view details of the event