Let’s start with the requirement to ensure that a function is called only once.

Very simple.

let called = false

function f() {
    if (called) {
        return
    }
    called = true
    console.log('function called')
    // write your logic here
}

f() // log: 'function called'
f() // log nothing
Copy the code

We define a function f that executes logic only when first called, and subsequent calls return when the function starts executing.

This requirement is not implemented well enough because there are three problems:

  1. The Boolean variable called pollutes the global environment.
  2. Function f is actually called, but exits as soon as it enters the function body.
  3. This implementation is intrusive, and we can only modify our own functions in this way. What if you’re the author of a development framework and you want to ensure that user-provided functions are called only once?

It’s easy.

function once(f) {
    let called = false

    return function (. args) {
        if (called) {
            return
        }
        called = truef(... args) } }Copy the code

All we need to do is wrap the code from version 1 in a function that takes the user-supplied function as an argument and returns it in an anonymous function.

Here we use closures to persist the variable called in the closure and use it to control whether or not the function is called.

In fact, version 2 of the code is not easy to understand for a beginner developer because it skips over some important details.

For example: What is a closure? Why can functions be used as arguments and return values?

And most importantly: why do we wrap up user-provided functions?

Let’s answer the above question step by step, starting with what a function is.

In JavaScript, a function is an object or, more simply, a value. A function is no different from the number 42 or the string Hello World. The difference, if anything, is the function object, which can be called and has an internal property (invisible to the developer) that stores the body of the function, which is a string of one or more statements.

A function is a value, and this is JavaScript’s biggest secret.

Since a function is a value, it can be used as an argument and return value for other functions.

What about closures?

The function also has an internal property called Scope, which holds all the upper-level variables that the function can read or write. Multiple levels of Scope form a Scope chain. A closure is an entity consisting of a function and its upper level of readable and writable variables.

We don’t have to worry about closure here, we just need to know that a function can read and write all of its upper variables, and any variable that is used in the function body will always be stored in the closure, taking up memory.

So when is the scope of a function defined? The answer is when you write code, so the scope of JavaScript is also called lexical scope.

Interestingly, the body of a function is also determined at code time.

So you can’t change the scope or body of a function at run time.

Now we can answer the most important question: Why do we wrap user-provided functions?

Because we’re looking for control over the function call process.

We have to wrap the user-provided function in another function, and then write our control logic in this outer function to control when and how the user function is called. Understand this sentence, we want to talk about the following anti – shake and throttle is very easy.

The idea of controlling how a function is called involves the concept of metaprogramming. What is metaprogramming? Normal programming is writing code to manipulate data; Metaprogramming is writing code and manipulating code. The Proxy class, for example, writes code that adds, subtracts, changes, and searches attributes of an object in other code. Normally, we delete an attribute of an object delete O.A, once deleted, it will also be deleted. You can’t control the real deletion process, but Proxy provides a method for you to control, you can do some things in the process of deleting attributes. Similarly, when you call a function f(), it is called, and after you call it, you have no control over the execution of the function body at runtime.

However, we now need to control the call process. JavaScript does not provide a class like Proxy to hijack the call to a function, nor does it need to, as we can do this through nesting of functions.

As you might have noticed, our code in version 2 has a question: what happens if a user uses the once function this way?

const o = {
    a: 1.g: once(function () {
        console.log(this.a)
    }),
}
o.g()
Copy the code

The answer is that the log function prints undefined (an error in strict mode) instead of 1.

Because our once factory function changes the way the input function is called: write to f(… The args).

But there are three ways to call a function, each of which corresponds to a different this value.

  1. When called as a normal function, as inf()In the body of the functionthisA value ofwindowOr in strict modeundefined.
  2. When called as a method on an object, as in:o.g()In the body of the functionthisValue is the objecto.
  3. When called as a constructor, as in:const o = new F()In the body of the functionthisValue is the newly constructed objecto.

Let’s write death as f(… Args), the value of this in f will always be window or undefined.

What to do?

It turns out that the function called directly by the user is not f, but the anonymous function returned by once. When the anonymous function is run, this in the function body is the correct this (we don’t care what the value is), so we just call the function apply and pass this to it.

function once(f) {
    let called = false

    return function (. args) {
        if (called) {
            return
        }
        called = true
        f.apply(this, args)
    }
}
Copy the code

Image stabilization

Now let’s talk about anti-shaking.

The essence of anti-shake is that we control the function to be called after a wait. If the function is called again after a wait, we cancel the previous call and start the timer again. That is, seeking control over the function call process.

Based on the above essence, we can easily write a simple implementation like the following:

function debounce(f, wait) {
    let timer
    return function (. args) {
        clearTimeout(timer)
        timer = setTimeout(() = > {
            f.apply(this, args)
        }, wait)
    }
}
Copy the code

The throttle

Next comes throttling.

The essence of throttling is that we control functions to be executed at intervals, during which calls are cancelled. That is, we’re going to call the function periodically. You see, basically, you’re looking for control over the function call process.

Based on the above essence, we can easily write a simple implementation like the following:

function throttle(f, interval) {
    let start = 0
    return function (. args) {
        const now = Date.now()
        if (now - start >= interval) {
            start = now
            f.apply(this, args)
        }
    }
}
Copy the code