Have you covered the basics of these JS design patterns? In this article, we cover prototypes, prototype chains, this pointing, call(), apply(), bind(), and how to implement inheritance in JS. The previous article provided the necessary basics. This article will explore JavaScript patterns from closures and higher-order functions.

JavaScript is a complete object-oriented programming language. JavaScript was designed with reference to and introduction of features such as Lambda expressions, closures, and higher-order functions.

Some design patterns in JavaScript rely on closures and higher-order functions, so it’s important to have a good knowledge of both.

Closure (Closure)

The formation of closures is related to the “scope” and “lifecycle” of variables, so have a clear understanding of these two concepts.

1.1 Scope of variables

Scope of a variable: The valid scope of a variable.

Here is an example:

var a = 0;
function func() {
    var a = 1;
    var b = 2;
    console.log(a, b); 
}
func();          // output: 1 2;
console.log(a);  // output: 0
console.log(b);  // output: Uncaught ReferenceError: b is not defined
Copy the code

We can see that a variable declared inside a function is a local variable, which is only valid inside the function body. It is not accessible outside the function, and JS will throw an undefined error when executing.

When a variable is declared in a function without the keyword var, it becomes a global variable. Therefore, it is recommended that you program normally (with TypeScript+Eslint) and declare variables as const and let as possible to avoid unnecessary memory usage.

In JavaScript, a function can be used to create a function scope, where the execution environment inside the function body can access variables outside the function, but the variables inside the function body cannot be accessed from the outside. If a function is searching internally for a variable, if the variable does not exist, it will look for the value of the variable in the scope chain from the inside out. If it does, it will return the value of the variable. Otherwise, it will return “Uncaught ReferenceError: Variable is not defined”

Here you can try to “brain run” to deepen the understanding of “variable scope” :

var a = 1;
var func = function() {
    var b = 2;
    var func2 = function() {
        var c = 3;
        console.log(a);
        console.log(b);
        console.log(c);
    }
    func2();
    console.log(c);
}
func();
Copy the code

The final correct output:

1
2
3
Uncaught ReferenceError: c is not defined
Copy the code

1.2 Life cycle of variables

If the scope of a variable is a rule, then the life cycle of the variable is the enforcer of the rule.

Life cycle of a variable: it is simply understood as the valid time of a variable. For example, global variables are valid throughout the execution of a program, and local variables in a function are destroyed after the execution of the function.

function func() {
    var a = 1;      // The function is automatically destroyed after execution
    console.log(a)
}
func();
Copy the code

1.3 Closures change the life cycle of local variables

Let’s start with an example:

function func() {
    var a = 0;      // The function is automatically destroyed after execution
    return function() {
        a = a + 1;
        console.log(a); }}var f = func();

f();    // output: 1
f();    // output: 2
f();    // output: 3
f();    // output: 4
console.log(a);  // output: Uncaught ReferenceError: a is not defined
Copy the code

The output appears to violate the “variable lifecycle” rule, as if the local variable a was not destroyed, and the last console.log(a) code execution reported that the variable a was undefined. So where is the variable A stored?

Var f = func(); F returns a reference to an anonymous function that has access to the context in which func() was called and in which the local variable a is always present. The local variable a can also be accessed from outside, so it has a reason not to be destroyed. Here a closure structure is generated, and the life cycle of the local variable is continued.

Scopes (scope) by looking at f.trototype:

Function f has two scopes: one is global, and the other is Closure. In the Closure, you can see that variable a has a value of 4. In other words, the local variable, a, is actually stored in a closure environment.

1.4 More functions of closures

Closures are widely used because they can change the lifecycle of a local variable without changing its scope.

1.4.1 cache

For example, if we want to implement a “product” function, which requires a large amount of computing resources, if it would be a waste of computing resources to have to recalculate every time an argument is passed in, then we want to cache the results.

If we use a global variable to store the results, we somewhat “contaminate” the global variable, because the product is only used inside the “product” function, and we still want to be able to decouple the variables, so we can use closures to do that.

const multiplication = (function() {
    const cache = {};
    return function() {
        const args = Array.prototype.join.call(arguments.', ');
        if (args in cache) {
            return cache[args];
        }
        let sum = 1;
        for (let i = 0; i < arguments.length; i++) {
            sum = sum * arguments[i];
        }
        return cache[args] = sum;
    }
})();

multiplication(1.2.3.4);
Copy the code

In this way, when we calculate the same multiplication, we can return the product result directly through the cache, so as to save computing resources and improve the performance and stability of the program.

Software development is about “high cohesion, low coupling,” and some generic method functions can be isolated, so the above code can be optimized.

const multiplication = (function() {
    const cache = {};

    const calculate = function() {
        let sum = 1;
        for (let i = 0; i < arguments.length; i++) {
            sum = sum * arguments[i];
        }
        return sum;
    }

    return function() {
        const args = Array.prototype.join.call(arguments.', ');
        if (args in cache) {
            return cache[args];
        }
        return cache[args] = calculate.apply(null.arguments);
    }
})();

multiplication(1.2.3.4);
Copy the code

1.4.2 Object-oriented Programming:

/****************** write 1 *******************/
var Person = function() {
    var age = 18;
    return {
        addAge: function() {
            age++;
            console.log('age:', age); }}}var person = Person();

person.addAge();    // output: age: 19
person.addAge();    // output: age: 20
person.addAge();    // output: age: 21


/****************** write 2 *******************/
var person = {
    age: 18.addAge: function() {
        this.age = this.age + 1;
        console.log('age:'.this.age); }}; person.addAge();// output: age: 19
person.addAge();    // output: age: 20
person.addAge();    // output: age: 21


/****************** write 3 *******************/
var Person = function() {
    this.age = 18;
}
Person.prototype.addAge = function() {
    this.age++;
    console.log(this.age);
}
var person = new Person();

person.addAge();    // output: age: 19
person.addAge();    // output: age: 20
person.addAge();    // output: age: 21
Copy the code

The closure feature is already reflected in the object-oriented programming style.

1.5 Closures and memory

Often asked by interviewers during interviews, “What do you know about closures?”

Interviewees often answered that closures may leak memory because they are not destroyed in time, that they need to minimize the use of closures, and that they need to actively assign null values to free memory in time.

Because the effect of putting a local variable into a global variable is to keep memory occupied for a long time and not free, the real cause of the memory leak is not the use of closures. The key point of the memory leak is that using closures is easy to form “circular references”. For example, the closure’s scope chain holds some DOM nodes, and neither of the two objects in the circular reference will be reclaimed based on “reference counting garbage collection mechanism”. So the root cause is a memory leak caused by “circular references” to objects.

2. Higher order Functions (HOF)

A higher-order Function is a Function that satisfies at least one of the following conditions:

  1. Functions can be passed as arguments
  2. Functions can be output as return values

It is common in JavaScript to see a callback function being passed as an argument, and a closure as a return function

2.1 Simple Examples

Here’s an example of a singleton pattern that takes both functions as arguments and as return values:

const getSingleBuider = function(fn) {
    let instance;
    return function() {
        return instance || (instance = fn.apply(this.arguments)); }}Copy the code

2.2 Higher order functions and AOP

The primary role of AOP (Aspect Oriented Programming) is to isolate functions that are not related to core business logic, such as log statistics, exception handling, security control, and so on. After these functions are removed, they are incorporated into the business logic module by “dynamic weaving”. It can ensure the high cohesion of the business logic modules and the reuse of the extracted functions.

Implementing AOP in JavaScript usually involves “dynamically weaving” a function into another function, and this can be done by reading the basics of these JS Design patterns in the previous basic article, “Have you learned the basics?” Mentioned in the prototype chain to implement.

Let’s look at a simple example to better understand higher-order functions and AOP:

Function.prototype.before = function(beforeFn) {
    var _self = this;                           // Store a reference to the original function
    // Return the "proxy" function of the original function and the new function
    return function() {
        beforeFn.apply(this.arguments);        // Execute the new function
        return _self.apply(this.arguments);    // Execute the original function and return the result}}Function.prototype.after = function(afterFn) {
    var _self = this;
    return function() {
        const result = _self.apply(this.arguments);
        afterFn.apply(this.arguments);
        returnresult; }}let func = function() {
    console.log('run');
}

func = func.before(function(){
    console.log('berfore run');
}).after(function() {
    console.log('after run');
});

func();
Copy the code

This allows us to add new middleware without affecting the original logic at all, similar to Koa’s “Onion model”.

Using AOP to dynamically add responsibility (functionality) to functions is consistent with the idea of one of the design patterns, the “decorator pattern.”

2.3 Curring

Currie, also known as “value” part, a first curring function will accept some parameters, after received these parameters, the function will not immediately, but continue to return to another function, just in the function of the incoming parameters stored in the environment in the closure, when a function is that really needs to be evaluated, prior to the incoming parameters will be one-time used for evaluation.

For example, in the interview, you will be asked to implement a summation function, using the following method:

sum(1) (2) (3); // output: 6
Copy the code

The first thing we might think of when we look at this is using higher-order functions to keep returning functions, so that the arguments are stored in closures, as described above. Our first version of code might look like this:

function sum(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        }
    }
}
sum(1) (2) (3);  // output: 6
Copy the code

The first version of the code doesn’t look very elegant, right? If I want sum(1)(2)… (1000) : return (1000) : return (1000) : return (1000)

The number of recursions depends on the length of the arguments, so a generic curring, we’re actually recursing the behavior of summing two numbers, thinking that you can curify the function, so you can chain the arguments.

So let’s start by currying over the sum of two numbers

// The first version of the curlization function
function curry(fn) {
    // Remove the passed function fn from the real argument group
    const args = Array.prototype.slice.call(arguments.1);
    // The return function is used to take the next argument
    return function() {
        // Save the next input required by the return function to newArgs and slice a shallow copy
        const newArgs = args.concat(Array.prototype.slice.call(arguments));
        // Execute the newArgs argument in the curlized function
        return fn.apply(this, newArgs);
    };
}

function add(a, b) {
    return a + b;
}

const addCurry = curry(add, 1.2);
addCurry() / / 3
/ / or
const addCurry = curry(add, 1);
addCurry(2) / / 3
/ / or
const addCurry = curry(add);
addCurry(1.2) / / 3
Copy the code

In the first version of the code we can see that one certainty is that it doesn’t implement the chain that we want in the form sum(1)(2)(3), and the idea is to curify the return function as well.

For declared functions, the length attribute in the stereotype can be used to get the length of the function’s argument.

So for the second edition:

const curry = function(fn) {
  return function inner() {
    // Shallow copy into the parameter
    const args = Array.prototype.slice.call(arguments);
    // If the length of the next argument is greater than the number of row arguments in the function, the recursion is broken
    if (arguments.length >= fn.length) {
      return fn.apply(undefined, args);
    } else {
      // Otherwise, proceed with the subsequent arguments and return the curring function
      return function() {
        // Get the last and next merge input
        const allArgs = args.concat(Array.prototype.slice.call(arguments));
        return inner.apply(undefined, allArgs); }; }}; }function sum(a, b, c) {
    return a + b + c;
}

const currySum = curry(sum);
Copy the code

If you use ES6, you can write it more concisely:

const curry = fn= >
  judge = (. args) = >args.length >= fn.length ? fn(... args) :arg= >judge(... args, arg);Copy the code

2.4 Debounce and Throttle

Generally, we are to put these two concepts together, both are to prevent users from frequently triggering function calls, but the handling strategy of the two is different, the author summed up a phrase to help you remember the difference: “tremble triggered many times, the last time effective; Throttling is triggered several times and takes effect periodically “.

I will not elaborate on the example analysis of anti-shake throttling here. I believe that you have used debounce and Throttle in Lodash, or the separate three-party library of anti-shake or throttling, and you have a clear understanding of these two.

Recommended reading: Debounce and Throttle

2.5 Time-sharing function

Time sharing function is an application for program performance optimization. Recently, I came into contact with it in the process of program performance optimization. I think it is necessary to say it.

A common example is the insertion of a large number of DOM nodes, which causes the page initialization load to be extremely slow (suspended animation).

One-time insertion:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Time-sharing function</title>
</head>
<body>
    <div>Performance optimization verification of time-sharing function</div>

    <script>
        // Add one time to the page
        const dataSource = new Array(10000).fill('DYBOY');
        / / create the DOM
        const createDiv = (text = 'DYBOY') = > {
            const div = document.createElement('div');
            div.innerHTML = text;
            document.body.appendChild(div);
        }
        // Batch add
        for (data of dataSource) {
            createDiv(data);
        }
    </script>
</body>
</html>
Copy the code

The idea of time-sharing function is to perform a large number of repeated operations at one time, in batches of time cycle, so that the first screen of the page can not block rendering, to avoid the phenomenon of suspended animation.

The modified code is as follows:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Time-sharing function</title>
</head>
<body>
    <div>Performance optimization verification of time-sharing function</div>

    <script>
        // Add one time to the page
        const dataSource = new Array(10000).fill('DYBOY');
        / / create the DOM
        const createDiv = (text = 'DYBOY') = > {
            const div = document.createElement('div');
            div.innerHTML = text;
            document.body.appendChild(div);
        }
        // Batch add
        // for (data of dataSource) {
        // createDiv(data);
        // }

        /** * time sharing function *@param dataSource- Data array *@param fn- Time-sharing functions *@param count- Number of times the function is executed in each segment time *@param duration- Segment duration, unit: ms **/
        const timeChunk = (dataSource, fn, count = 1, duration = 200) = > {
            let timer;
            const start = () = > {
                const minCount = Math.min(count, dataSource.length);
                for(let i = 0; i < minCount; i++) fn(dataSource.shift());
            }
            return () = > {
                timer = setInterval(() = > {
                    if (dataSource.length === 0) return clearInterval(timer); start(); }, duration); }}const newRender = timeChunk(dataSource, createDiv, 100.300);

       newRender();
    </script>
</body>
</html>
Copy the code

In comparison, the first screen time for scripting was 425ms, while the first screen time for scripting was 2410ms. The first screen performance was saved by 500% with the time sharing function, which is quite impressive.

In addition to the time-sharing function, performance optimization process, the time is very long, if a function calculating task would lead to “white during long time page” phenomenon, here we can approach the computing tasks for a long time, see have what time-consuming operation in this task, can look for lengthy operations do cache, time slice, micro and macro tasks task to jump the queue, I will sort it out and share it with you in the following articles.

2.6 Lazy loading functions

Lazy loading is a method of program performance optimization. Its purpose is to make the execution branch of a function occur only once.

Similar to saving the result of a time-consuming operation in a variable, rather than re-executing the function every time in the for loop.

There are two ways to load a function lazily:

  1. In the function call processing: function internal copy function, directly return the value;
  2. Processing at function declaration: When a function is declared, the return value is determined.

Third, summary

This article is to undertake the previous article “these JS design patterns in the basic knowledge you will?” Content, from the Javascript this point, the prototype, the prototype chain, the JS inheritance implementation, to closures and higher-order functions (HOF), is a necessary foundation for learning the design patterns that rely in many ways on closures and higher-order functions. So can master and skilled use of closures and high – order functions, help you to quickly understand and realize the program design in JS.

For design mode and front-end advanced students may wish to pay attention to the wechat public number: DYBOY, add the author’s wechat, exchange learning, internal push dafa!

Reference

  • Have you mastered the basics of these JS design patterns?
  • AOP programming full parsing
  • JS performance optimization for lazy loading functions
  • Function currying in JavaScript
  • Functional programming refers to north
  • Understanding Higher-Order Functions in JavaScript