Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.
Functions are first-class citizens (first-class objects) — in JavaScript, functions coexist with other objects and can be used just like any other object. Functions can be created from literals, assigned to variables, passed as function parameters, or even returned from functions as return values.
Anything an object can do, a function can do. A function is also an object, special only in that it is callable.
Today will come to the system to learn about JavaScript and function related knowledge points! 📣 organisation (‘ ᴗ ‘)
First, look at compilation and scope
Before JavaScript code is run, there is a compilation process. The traditional compilation process is: lexical analysis > parsing > code generation.
- Lexical analysis: Breaking our code strings into blocks that make sense (for the programming language), such as:
var a = 2
// This statement has four parts: var, a, =, 2
Var is the keyword, a is the variable name, = is the operator, and 2 is the specific value
Copy the code
- Parsing: Converting lexical units into AST (Abstract syntax tree)
- Code generation: Transform the AST into executable code
JavaScript compilation, on the other hand, adds some extra processing on the basis of traditional compilation. For example, there are specific steps in the parsing and code generation stages to optimize performance, including the optimization of redundant elements. But JavaScript engines don’t have a lot of time to compile, because compilation usually takes place in the microseconds before code is executed, and in those microseconds, JavaScript engines do everything they can (such as delay compilation/recompilation) to ensure optimal performance.
In short, any JavaScript code is compiled before execution to convert the code we write into code that the final engine can execute. (This is a reference to Vue’s template compilation process: parsing template strings into AST syntax trees, optimizing syntax trees, generating executable code to convert our template code into JS code. Anyway, compilation is to parse the code we wrote for final execution.)
We know that a scope is a set of rules for finding variables, and that the nesting of scopes forms a scope chain. At compile time, scope is defined, where no matter where or how a function is called, its scope is determined only by the position at which the function is declared. There are two ways in JavaScript to “cheat” scopes: eval and with, but they are not recommended (it can make compile-time optimizations meaningless). There are three types of scope in JavaScript: function scope, block scope (new in ES6), and global scope.
The relationship between engine, compiler, and scope is tight:
😆 | engine | The compiler | scope |
---|---|---|---|
JavaScript code | Responsible for compiling and executing the entire JavaScript program from start to finish | Responsible for the dirty work of parsing and code generation | Responsible for collecting and maintaining a series of queries made up of all declared variables and determining access to them by currently executing code |
var a = 2 | There are two things: var a Let the compiler handle this! 2, a = 2, etc | 1. To declare an A, ask if a exists in the scope. If not, I will declare; 2. Generate the code that the engine needs to run | Feel free to ask me if you have any questions |
What else happens at compile time besides code conversion? Part of the compile phase is finding all the declarations and associating them with the appropriate scope. Therefore, all declarations at compile time, including variables and functions, are processed first before any code is executed.
As a result, we often hear that variables and functions (declarations) are promoted, which means that the declarations of variables and functions are “moved” from where they appear in code to the top of their scope.
PS: variable promotion only applies to variables declared by the var keyword & functions are promoted first, then variables.
PPS: The scope here refers to the lexical scope, which is defined when we write the code. The opposite concept is dynamic scope. JavaScript only has lexical scope, but the execution mechanism for this looks a lot like dynamic scope. The main difference between lexical scope and dynamic scope is that lexical scope is determined at code writing or definition time, while dynamic scope is determined at run time (as is this!). ; Lexical scopes focus on where functions are declared, while dynamic scopes focus on where functions are called from. In JavaScript, scopes are determined at compile time, but the scope chain is fully generated during the creation of the execution context.
JavaScript features this and closures
When a function is called, the corresponding execution context is created. The execution context is the execution environment/scope of the current code. The execution context includes the variable object, the scope chain, and the reference to this. The concepts of execution context and scope chains are easy to understand and will not be repeated. Here’s a look at JavaScript’s unique this and closure.
1, this
1.1, according to
Why this?
function introduce(context){
console.log('Hello, I am.' + context.name)
}
const Hah = {
name: 'Hah'
}
const Kris = {
name: 'Kris'
}
introduce(Hah) // Hello, I am Hah
introduce(Kris) // Hello, MY name is Kris
Copy the code
As you can see from the above code, if we want to reuse a function between different context objects, we have to explicitly pass in a context object parameter. As our usage patterns get more complex, passing context objects explicitly makes our code more confusing.
With this:
function introduce(){
console.log('Hello, I am.' + this.name)
}
const Hah = {
name: 'Hah'
}
const Kris = {
name: 'Kris'
}
introduce.call(Hah) // Hello, I am Hah
introduce.call(Kris) // Hello, MY name is Kris
Copy the code
This provides a more elegant way to implicitly “pass” an object reference, so the API can be designed to be cleaner and easier to reuse.
1.2, WHAT
Now that we know why, we need to clear up some misunderstandings about this:
- Myth 1: This points to itself 🙅
- Myth 2: This refers to the scope of the function 🙅
We shouldn’t get stuck in the literal meaning of “this” to create some misunderstandings about this, but with that out of the way, let’s take a look at what kind of mechanism this is.
To summarize: This is bound at run time, not at write time, and its context depends on various conditions at the time the function is called. The binding of this has nothing to do with the position of the function declaration, except how the function is called.
1.3, HOW
What are the situations in which this points to the question?
(1) Default binding — bind to the global object Window
Observe the following code:In code, foo() is used directlyA function reference without any decorationSo the internal this is the default binding rule, i.eIndependent function callThis will point to the global object, which, in terms of the previous execution context, was in the global execution context when the standalone function was called. Note that if you use Strict mode, you cannot use global objects for the default binding, so this is bound to undefined.
(2) Implicit binding
Observe the following code:You can see that foo is declared separately and then added to OBj as a reference property. Function foo is not strictly declared as an obj object. However, the call to obj.foo() refers to function foo using the obj execution context, so that the obj object can “own” or “contain” function foo when it is called. When a function references an execution context object, the implicit binding rule binds this in the function call to that context object. Because this is bound to obj when foo() is called, this.a and obj.a are the same.
Why is it called implicit binding? Because we must bind this indirectly to an object by including a property that points to a function inside the object and by referring to the function indirectly through this property.
Ps: object attribute reference chainOnly the previous or last level matters in the call location, which allows us to locate the reference to this in nested calls. Here’s an example:
(3) Implicit loss
Observe the following code:Bar looks like a reference to obj.foo, but it actually refers to foo itself, so bar() is an undecorated function call that conforms to the default binding rules, so the default binding is applied. Similarly, functions are passed as arguments, which is also a form of implicit assignment.
(4) Explicit binding
Unlike implicit binding, what if we want to force a function call on an object instead of including a function reference inside the object? Use the call/apply/bind methods of the function (f.call()/f.apply()/f.bind())).
How do these three methods work? Their first argument is an object for this, which is then bound to the object when the function is called. Because the binding object for this can be specified directly, we call it an explicit binding. In summary, they are used to change the reference of the related function this, call/apply is used to call the related function directly; Bind does not execute the related function. Instead, it returns a new function that is automatically bound to the new this pointer and can be called manually.
(5) New binding
As we know, calling a function with new automatically does the following: (1) create (or construct) a brand new object; (2) This new object will be connected by [[Prototype]]; (3) The new object is bound to the function call’s this; (4) If the function returns no other object, the function call in the new expression automatically returns the new object. When a function is called with new, a new object is constructed and bound to this in the function call.
(6) Priority
Conclusion: New binding > Explicit binding (call, apply, bind) > Implicit binding > Default binding
So it can be determined in the following order: (1) Is the function called in new (new binding)? If so, this binds to the newly created object. (2) Is the function called through call, apply (explicit binding) or hard binding? If so, this binds to the specified object. (3) Is the function called in a context object (implicit binding)? If so, this binds to that context object. (4) If neither, use the default binding. In strict mode, it is bound to undefined, otherwise it is bound to Window.
1.4. Arrow function
The binding rules described earlier can already contain all normal functions, but ES6 has added a special function type: arrow functions. Arrow functions are not defined using the function keyword, but using the => operator. Instead of using the four standard rules for this, arrow functions determine this based on the outer (function or global) scope.
In particular, the arrow function inherits the this binding of the outer function call (whatever this is bound to), just like self = this in the previous ES6 code.
1.5, summary
The reference to this is dynamically determined when the function is called based on the execution context.
To determine the this binding of a running function, we need to find where the function is called directly, in the following order:
(1) Is the function called in new (new binding)? If so, this binds to the newly created object.
(2) Is the function called through call, apply (explicit binding) or hard binding? If so, this binds to the specified object.
(3) Is the function called in a context object (implicit binding)? If so, this binds to that context object.
(4) If neither, use the default binding. In strict mode, it is bound to undefined, otherwise it is bound to Window.
The exception is the arrow function, which inherits the this binding of the outer function call (whatever this is bound to).
2, closures
When I first learned JavaScript, my understanding of a closure was that it was a function nested function, and the inner function could access variables in the outer function
Closures are an almost mythical concept in JavaScript, and the creation and use of closures are ubiquitous in your code. What you lack is the mental environment to identify, embrace, and influence closures at your own will.
Douglas describes closures as “the most important discovery in the history of programming languages to date, and JavaScript is made magic by it. Without closures, JavaScript has no soul.”
Closures play such an important role in JavaScript that today let’s get to know and embrace the magic of closures.
2.1. What is closure?
In JavaScript you Don’t Know, the authors define closures as occurring when a function can remember and access its lexical scope, even if the function is executed outside the current lexical scope.
Another definition of a closure is easier to understand: when a function nested a function, the inner function references variables in the scope of the outer function, and the inner function is accessible in the global context.
It’s important here is “the inner function can be accessed in a global environment”, that is to say, if we just wrote a nested function code is not strictly a closure or it couldn’t reflect the characteristics of the closure (normal is sufficient to explain the scope of the chain), only when the inner function when it is outside the scope of the visit, to stand out the characteristics of the closure The most common approach is to return an internal function as an argument. (Function is also a value, can be passed around at will ~)
An 🌰 :
function outer(){
var a = 2
function inner(){
console.log(a)
}
return inner // This produces a closure
}
var func = outer()
func() / / 2
Copy the code
2.2. Special features of closures
Let’s start with this example:
function outer(){
var a = 2
function inner(){
console.log(a)
}
return inner // This produces a closure
}
var func = outer()
func() / / 2
Copy the code
We know that when a function is called, its entire inner scope is destroyed, meaning that we can’t access what’s inside the function. Func (); outer(func); func(); func(); func(); func(); func(); func(); func(); func(); func(); func() That’s what makes closures special! Closures prevent scopes from being destroyed!
As a refresher, the internal variables of a function are normally inaccessible, and the context is destroyed after the function is executed. But in a function, if we return another function that uses a variable inside the function, then the outside world can use the returned function to get the value of the variable inside the original function. This is the basic principle of closures.
2.3. Take a look at some closure examples
(1) Loops and closures
Here’s a classic interview question:
for (var i=1; i<=5; i++) {
setTimeout(() = >{
console.log(i)
}, 1000)}// Outputs 5 6's
Copy the code
A few lines of code covering scopes, closures, asynchrony, and other important concepts in JavaScript. Those of you who aren’t familiar with this might think the code prints 1, 2, 3, 4, 5, but it doesn’t. Why is that?
First, setTimeout is an asynchronous task that does not execute immediately; Second, this code is in scope, that is, there is only one I; Therefore, after the synchronization task is completed, when it is time to execute the code logic in setTimeout, I becomes 6, and 5 6 ~ will be output naturally
What if we just want to print one, two, three, four, five?
- Improvement 1: IIFE(Execute functions now)
for (var i=1; i<=5; i++) {
((i) = >{
setTimeout(() = >{
console.log(i)
}, 1000)
})(i)
}
// Create closure via IIFE, preserve reference to I, output 12345 fine
Copy the code
- Improvement 2: Use let
for (var i=1; i<=5; i++) {
let j = i
setTimeout(() = >{
console.log(j)
}, 1000)}// Let has hijacked the block scope.
Copy the code
- This is gonna get worse
for (let i=1; i<=5; i++) {
setTimeout(() = >{
console.log(i)
}, 1000)}/ / output 12345
Copy the code
🐂! Let block scope and closure together are the best! Feeling refreshed after reading the code ^^
“I don’t know about you, but this feature has made me a happy JavaScript programmer.”
(2), module
Modules are one of the most commonly used code patterns that leverage the power of closures.
There are two requirements for the module pattern:
- There must be an external enclosing function that must be called at least once;
- Enclosing functions must return at least one inner function so that the inner function can form a closure in the private scope and can access or modify the private state. An object with function attributes is not a real module in itself. From a convenience point of view, an object returned from a function call with only data attributes but no closure functions is not really a module.
These two conditions mean that the modular pattern is implemented only when the special features of closures are exploited. In real development, we have been using the module pattern to encapsulate and introduce other functions. Once we learn about closures, we find that closures are everywhere in our code, and now we can use them to do something useful.
3. Call stack and tail recursion
When a function is called, the engine throws the function into a place called the function stack, but why use the stack instead of some other data structure?
We know that the stack is a first in, last out data structure, so the point of using the stack is really simple — keep the entry environment. To illustrate this, use a simple piece of code:
function main() {
/ /...
foo1()
/ /...
foo2()
/ /...
return
}
main()
Copy the code
Here is a simple example of the main call:
- Start by building a stack of functions
- The main function call pushes the main function onto the stack
- After doing some work, we call foo1, and foo1 is pushed onto the stack
- The foo1 function returns and exits the stack
- After doing some work, call foo2, and push foo2 onto the stack
- The foo2 function returns and exits the stack
- After the rest of the operation, main returns and exits the stack
The above procedure illustrates the function stack’s role — steps 4 and 6 allow foo1 and foo2 to return to the same place where main called foo1 and foo2. This is where stack, a last in, first out data structure, comes in.
Let’s look at a recursive summation of Fibonacci numbers:
const fibonacci = n= > {
if (n === 0) return 0
if (n === 1) return 1
return fibonacci(n - 1) + fibonacci(n - 2)}Copy the code
Recursion is very memory intensive and prone to “stack overflow” errors. A common optimization is:
const fibonacciTail = (n, a = 0, b = 1) = > {
if (n === 0) return a
return fibonacciTail(n - 1, b, a + b)
}
Copy the code
This type of recursion is called tail-recursion — the last step of a recursive function is to call itself, and tail-recursion is optimized because no new stack frames are added to the call stack during the entire execution. Instead, the call stack is updated so that “stack overflow” errors never occur. In the case of tail-recursion, it makes no sense to keep the entry environment of this function, so you can optimize the stack of this function.
PS: Starting with ES6, all ES implementations must deploy tail-call optimization, so here’s a quick overview.
Third, after the function call
Take a look at JavaScript’s garbage collection mechanism 🗑
1. Memory management
Memory management is the dynamic allocation of memory to the system as needed, and then the release of memory for objects that are no longer used.
We know that memory space can be divided into stack space and heap space, where
- Stack space: automatically allocated by the operating system to store function parameter values, local variable values, etc., operates in a manner similar to stacks in data structures.
- Heap space: Usually allocated by the developer, this space should be considered for garbage collection.
In JavaScript, generally speaking, base data types are held in stack memory and reference types are held in heap memory.
2. Browser garbage collection
The behavior of allocating and reading and writing memory is relatively consistent across all languages, but freeing memory varies from language to language. JavaScript relies on the host browser’s garbage collection mechanism to free up memory. Although in most cases you don’t have to worry about freeing memory, you should have some understanding of memory management and garbage collection mechanisms. We must know WHAT, WHY and HOW so that we can find a proper solution when we encounter a problem.
There are two main garbage collection mechanisms for browsers:
- Tag cleanup (since 2012, all modern browsers are based on the “tag cleanup” recycling algorithm) : When an object can no longer be accessed, the object is marked, and when the next garbage collection event occurs, the object is cleared.
Can no longer be accessed? Starting with the root element (the window object), all child variables are recursively judged, and anything that cannot be reached from the root element is considered garbage. Eg: In general (without closures), when a function completes, the internal variables are inaccessible to other code, so it is marked “inaccessible”. Therefore, the difficulty of tag clearing is how to determine that an object can no longer be accessed. SetInterval, DOM events, and closures cannot be marked as unreachable and therefore cannot be reclaimed, resulting in memory leaks.
- Reference counting: An object can be said to refer to another object as long as it explicitly or implicitly (like Prototype) refers to another object. When an object is referenced zero times, it can be reclaimed. The problem here is that “circular reference”, if the object attribute references for b, and b properties cited a, because the engine is only under the condition of the variables of citations of 0 to recycling, cited a and b here at least 1, so even if they are in the functions performed, the two variables cannot be recycled.
3. Memory leaks
A memory leak is when memory that is no longer needed is, for some reason, not returned to the operating system or into the available memory pool.
As mentioned earlier with closures, you can protect the memory blocks of certain data variables from being collected by the garbage collection mechanism. Therefore, improper use of closures can cause memory leaks and requires special attention.
An 🌰 :
function foo(){
let value = 123
function bar() { console.log(value) }
return bar
}
let bar = foo()
Copy the code
In this case, the variable value will always be held in memory if:
bar = null
Copy the code
As bar is no longer referenced, value is also cleared.
There are also a few situations that can cause memory leaks:
- The global variable
- Some forgotten timer or callback
- DOM references
- Console outputs objects with references
4. What can we do
While the browser’s garbage collection mechanism is convenient, they have a plan of their own, and one of them is uncertainty. In other words, garbage collection is unpredictable, and it’s impossible to know when a collector will be executed. This means that in some cases the program will use more memory than it actually needs.
Only the developer can figure out if a chunk of memory can be reclaimed. This requires an understanding of memory management and garbage collection so that we can avoid these situations in the real world or manually free the memory.