A thousand readers, a thousand Hamlets.

This series of articles will provide a comprehensive overview of how to write high-quality functions in JavaScript, from the execution mechanism, robustness, functional programming, design patterns, and more.

The introduction

How to write high quality functions in JavaScript is a difficult question to answer. Different people have their own ideas about high quality. Here I will give a comprehensive overview of my own ideas about how to write high quality functions. Our opinions may not be comprehensive enough, or there may be some wrong opinions. Welcome to discuss with us. Just like people in daily life, small quarrels will always appear inadvertently.

I intend to complete this series on How to Write Quality Functions in JavaScript with a few articles.

Mainly from the following aspects:

  • Function (anything is possible)

  • Function naming

  • Function comments

  • Function complexity

  • Function robustness (defensive programming)

  • Input and output parameters of a function (return)

  • How to use functional programming to get through the functions of both

  • How to use design patterns to make functions more powerful

  • What is the style of writing v8-friendly functions

  • Front-end engineer function rhapsody record

This article only said the first section of the function, to catch the thief before the king, let’s plate a plate of function 7888.

Function (anything is possible)

The word “function” means anything is possible.

Let’s think about how far away we are from the function we are using. It’s like mah-jongg. You think you can touch whatever you want, like The Finch.

Deal with function everyday, what is the purpose that function appears? What is the execution mechanism of a function? Let’s do a simple analysis.

First, the purpose of the function

Functions are by far the most important means invented to save space and improve performance.

PS: Note that there is no one.

Second, the execution mechanism of the function

As the saying goes, if you know your enemy and know yourself, you can win a hundred battles. If you want to win, you must know your enemy well. JS is certainly not the enemy, but to master JS functions, to write high-quality functions more easily, it is necessary to master the function execution mechanism in JS.

How to explain the execution mechanism of functions?

Let’s start by imitating a front end test: What happens when you enter a URL?

What happens when you execute a function?

Refer to the following code:

function say() {  let str = 'hello world'  console.log(str)    }Copy the code

How much of this interview question would you answer if you were given it?

If I had to answer it, I would say something like this:

First I’m going to create a function. If you’ve learned C++, you might say I’m going to create a heap first.

So, I’m going to analyze the three levels, from creating the function to executing the function and its underlying implementation.

1. Create functions

Functions are not created for nothing; they need to be created. What happens when you create a function?

Step 1: Create a new heap memory

Every letter is to store space, as long as there is data, there must be a place to store data. On the other hand, the heap allows a program to dynamically claim a certain amount of memory at runtime, so you can claim memory for functions while the program is running.

Step 2: Create a function say and place the code from the function body in the heap memory.

The function body is placed in heap memory as a string.

Why is that? Let’s look at the code for the say function body:

let str = 'hello world'console.log(str)Copy the code

These statements are better placed in heap memory as strings because there is no pattern. If it is an object, it can be stored in heap memory as key-value pairs due to regularity. And the ones that don’t are usually just strings.

Step 3: Declare the say function (variable) in the current context, with the function declaration and definition promoted to the top

Note that the current context, which we can understand as the context stack, is placed on the stack and has a heap memory address to the right of it that points to the body of the function in the heap.

PS: It is recommended to learn about data structures, blocks on the stack, called frames. You can think of stacks as DOM trees, frames as nodes, and each frame has its own name and content.

Step 4: Assign the heap memory address to the function name say

The key here is to assign the heap memory address to the function name say.

Below I draw a simple schematic diagram:

Combined with the storage on the right side of say in the figure above, and then to understand the above four steps, is it a bit enlightened?

2. Do you really understand assignment?

Assignment is mentioned here. What does it mean if I assign the heap memory address to the function name say?

Assignment operation from the perspective of computer composition principle, memory is divided into several areas, such as code area, stack area, heap area and so on.

Each of these areas has a different memory address. In other words, the operation of assignment (reference type) is to transfer (copy) an address of the heap area through the bus pipe to an address of the corresponding stack area, so that an address of the stack area has an address of the heap area in the storage space. The industry calls it a handle, which is a pointer. But in high-level languages, Pointers are hidden and variables are used instead.

So a simple assignment is very complicated to implement at the bottom of the computer. Here, perhaps through assembly language, you can better understand the real meaning of assignment, such as 1 + 1 written in assembly language, is the following code:

Start:mov ax, 1mov bx, 1add ax, bxend start;Copy the code

From the code above, we can see that assigning 1 to AX uses the mov instruction. Mov is short for move, which also proves that assignment is essentially the flow of data or data handles in an address table.

PS: So if it is a value type, it is to stream the data directly to the specified memory location.

Above is my computer from the bottom to explain some of the most basic function creation phenomenon, first explained here.

3. Execute the function

The process of executing the function is also very important, and I will explain the process of executing the function with a personal summary.

Think about a point.

We know that the code for the function body is stored as a string in heap memory. If we want to execute code in heap memory, we first turn the string into real JS code, just like serialization and deserialization in data transfer.

Question 1: Why serialization and deserialization exist? Everybody can think by oneself, some more simple truth, backside is having extraordinary thought more.

4. Turn strings into real JS code

For each function call, frames are created in the function context stack. A stack is a basic data structure.

Why should function execution be performed on the stack?

The stack is a first in, last out data structure, which means that the call scene can be well saved and recovered.

Take a look at this code:

function f1() {  let b = 1;  function f2() {    cnsole.log(b)  }  return f2}let fun = f1()fun()Copy the code

What is the function context stack?

A function context stack is a data structure, and if you’ve learned C++ or C, you can think of it as a struct. This structure is responsible for managing function execution and closing variable scope. The function context stack is created while the program is running, and the first frame added to the stack is the global context frame, located at the bottom of the stack.

5. Start executing the function

First, understand this:

Executing functions (function calls) is done on the stack.

This is why JS functions can be recursive. Because of the data structure of stack first out, it gives recursion ability.

Moving on, there are roughly four steps to function execution:

Step 1: Form an environment for code execution, also a stack memory.

Here, let’s first consider a few questions:

  • What is the environment in which the code is executed?

  • How is stack memory allocated?

  • What does the inside of the stack look like?

Step 2: copy the stored string into the newly created stack memory to make it into real JS code.

Step 3: Assign the parameter and then promote the variable, such as the var function variable.

Step 4: Execute from the top down in this new scope.

Question to ponder: Why top-down execution?

Returns the execution result to the currently called function

Question to consider: How is it possible to return the result of execution to the currently called function?

Third, talk about the underlying implementation

1. The most essential closure interpretation in computers

When a function is executed, it forms a new private scope, also known as private stack memory.

The purpose is as follows:

First point: turn the strings stored in the original heap memory into real JS code.

Second point: protect the stack memory private variables from outside interference.

This protection mechanism for function execution is known in computers as closures.

Maybe some people don’t understand, why is it private?

No problem. We can do it backwards. If the stack is not private, then when you perform a recursion, you basically end up with a lot of the same JS code in a function context stack, such as local variables, etc. If you don’t privatize it, it will be a mess. So assuming the contradiction, private stack memory holds.

2. How is stack memory allocated?

First, JS stack memory is automatically allocated by the system, the size is fixed. If adaptive, there is almost no stack overflow other than the case of an infinite loop.

3. What does the inside of the stack look like?

For example, if you write a return statement every day, do you know what the underlying implementation of return is? Write subroutines every day. Do you know some of the underlying facts about subroutines?

Let’s look at a picture:

The figure above shows the stack structure of a function call. From the structure, you can see that the internal components, such as real parameters, local variables, return addresses.

Look at the following code:

function f1() {  return 'hello godkun'    }let result = f1()f2(result)Copy the code

The bottom line of the above code is that after f() is executed in private stack memory, the result is passed to the EAX (cumulative register) with return, which is often used to return a value from the function.

So Return Addr, the main purpose of Addr is to allow the subroutine to be called multiple times.

Look at the following code:

function main() {  say()  // TODO:  say()}Copy the code

As mentioned above, the say subroutine is called multiple times in the main function. The underlying implementation is to store the start address of the function by storing a Addr in the stack structure. When the first SAY function is run, Addr points to the start address, in case the subroutine is called multiple times.

How do JS engines execute functions

The above has analyzed the mechanism of function execution from many aspects. Now take a brief look at how the JS engine executes functions.

I recommend a blog called “Exploring how THE JS Engine works” where I’ll break down some of the important details.

The code is as follows:

// define a global variable xvar x = 1function A(y) {// define a local variable xvar x = 2function B(z) {// define an inner function Bconsole.log(x + y + z)}// Return a reference to function Breturn B}// Execute A to return Bvar C = A(1)// Execute function BC(1)Copy the code

When I execute function A

The ESCstack structure constructed by the JS engine is as follows:

Diagram A for short:

When I execute B

The ESCstack structure constructed by the JS engine is as follows:

Diagram B for short:

1. How are local variables stored

Core code:

EC(B) = {  [scope]:AO(A),  var AO(B) = {    z:1,    arguments:[],    this:window  },  scopeChain:<AO(B),B[[scope]]>  }Copy the code

This is the execution environment (an object structure) of the B function created when the B function is executed. Inside there is an AO(B), which is the active object of the B function.

So what’s the purpose of AO(B)? In fact, AO(B) is what each list node points to.

We also define the [scope] property, which we can think of as A pointer. [scope] points to AO(A), which is the active object of function A.

Function active objects hold local variables, parameter arrays, and the this attribute. This is why you can use this and arguments inside functions.

ScopeChain is a scoped chain. If you are familiar with data structures, a scoped chain is essentially a linked list. If you execute a function, the list is initialized as the scope of that function, and then you place the currently pointed function active object in the header of the scopeChain list.

For example, if B is executed, the list of B looks like AO(B) –> AO(A).

At the same time, A function also has its own linked list, AO(A) –> VO(G). AO(B) –> AO(A) –> VO(G)

The list is a closed loop, because if you go around and you get back to yourself, if you haven’t found it, you return undefined.

Question: Why are [scope] and [[scope] named in this form?

2. What can we see from the ECS of A function

We can see that JS language is A static scoped language, before the execution of the function, the scope chain of the whole program is determined, from the function B in figure A B[[scope]] can see that the scope chain has been determined. Unlike Lisp, where scopes are determined at run time.

3. Execution environment, what kind of existence is context environment

The data structure of the execution environment is the stack structure, which is essentially adding properties and methods to an array.

The execution environment can be represented by ECStack, which can be understood as ECSack = [].

The stack (execution environment) is dedicated to storing various types of data, such as the classic seed data structure for the execution of a function. For example, function A is executed in EC(A). When A is executed, the equivalent of ecstack.push [A], everything belonging to A is wrapped in A private stack memory when it is added to the stack.

How is the private stack formed? From the point of view of assembly language, the memory allocation of a stack and various transformations of the stack structure are controlled by the underlying standards.

4. Go into God mode and see through this

Why is this determined at run time

The red arrows in the two images above, the information at the arrows is very, very important.

If we look at figure A, only function A has this when we execute function A, and only function B has this when we execute function B, which confirms that this exists only at runtime.

This points to the truth

Var C = A(1); var C = A(1); var C = A(1); C() is executing B, EC(B) is already at the top of the stack, and this is the window global variable.

Through the comparison between Figure A and Figure B, the essence of this is directly shown.

5. The essence of a scope is a node in a linked list

By comparing the A and B figures, directly kill all uses of scope

In figure A, when function A is executed, function B’s scope is to create function A’s active object AO(A). A scope is A property that belongs to the execution environment of A function. Its name is [scope].

[scope] refers to a function active object. The core point is to think of this function object as a scope, best understood as a linked list node.

PS: when B executes B, only B has this, which cross-verifies that this exists only at runtime.

6. The essence of scoped chains is linked lists

Firstly, by comparing the scopeChain of Figure A and Figure B, it can be determined that:

Scope chain is essentially a linked list. The list is initialized as the scope of any function executed, and then the function’s [scope] is placed in the header to form a closed loop linked list. The scope chain is found through the linked list. If you haven’t found it yet, return undefined.

5. Use an Interview Question to Get you to the next level

For another example, this is a frequently asked interview question, look at the following code:

The first procedure is as follows:

function kun() {  var result = []  for (var i = 0; i < 10; i++) {    result[i] = function() {      return i    }  }  return result} let r = kun()r.forEach(fn => {  console.log('fn',fn())})Copy the code

The second procedure is as follows:

function kun() {  var result = []  for (var i = 0; i < 10; i++) {    result[i] = (function(n) {      return function() {        return n      }    })(i)  }  return result} let r = kun()r.forEach(fn => {  console.log('fn', fn())})Copy the code

The output should be known, as shown in the screenshot below:

The first program prints 10 10s:

The second program outputs 0 to 9:

So the question is, what’s the mechanism?

  • Some of the coders can only answer immediate call closure.

  • Most coders can answer the questions about scope.

  • Very few coders can be analyzed in terms of core underlying causes.

The following from the core underlying reasons to analyze.

1. Analyze the output of 10 10s

The code is as follows:

function kun() {  var result = []  for (var i = 0; i < 10; i++) {    result[i] = function() {      return i    }  }  return result} let r = kun()r.forEach(fn => {  console.log('fn',fn())})Copy the code

The execution environment of a function is generated only when the function is being executed. According to this rule, when r = kun() is done, the kun function is executed only once, generating the corresponding AO(kun). As follows:

AO(kun):{  i = 10;kun = function(){... };  kun[[scope]] = this;}Copy the code

At this point, after kun(), the value of I is already 10. Note that the KUN function is executed only once, which means:

The I attribute in AO(kun) of the kun function is 10.

To continue sharing, the scope chain of the KUN function is as follows:

AO(kun) –> VO(G)

And the kun function has been removed from the top of the stack, leaving only AO(kun).

Note: AO(kun) here represents a node. This node has Pointers to VO(G) and data, which is the active object of the kun function.

So what happens when an array in Result is executed once?

Note: Each function in the Result array is scoped, whereas JS is a statically scoped language and all scoped is scoped at the program declaration stage.

So, each function in the Result array has the following scope chain:

AO(result[i]) –> AO(kun) –> VO(G)

So each function in Result executes with the value of I found along the scope chain, and since the kun function is executed only once, the value of I is the final result, which is 10. So the output is 10 tens.

AO(result[I]) –> AO(kun) –> VO(G) –> VO(G) But the AO(kun) in all 10 scope chains is the same. So the result is, the output is 10 tens.

Let’s analyze the output 0 through 9.

2. Analyze outputs 0 to 9

The code is as follows:

function kun() {  var result = []  for (var i = 0; i < 10; i++) {    result[i] = (function(n) {      return function() {        return n      }    })(i)  }  return result} let r = kun()r.forEach(fn => {  console.log('fn', fn())})Copy the code

First, the anonymous function was executed 10 times by the time the function kun was declared. The function generates an execution environment at execution time, which means that in the ECS stack, there are 10 EC(KUN) execution environments, each corresponding to the 10 functions in the Result array.

This is shown in pseudocode

ECSack = [  EC(kun) = {    [scope]: VO(G)    AO(kun) = {      i: 0,result[0] = function() {... // return i},      arguments:[],      this: window    },    scopeChain:<AO(kun), kun[[scope]]>  },/ /...  EC(kun) = [    [scope]: VO(G)    AO(kun) = {      i: 9,result[9] = function() {... // return i},      arguments:[],      this: window    },    scopeChain:<AO(kun), kun[[scope]]>  ]]Copy the code

The internal state of the kun function when it is declared is simply expressed in structured language.

First point: each EC(kun) in AO(kun) in the I property value is different, for example, through the above structured representation, can see:

  • The parent execution environment of the result[0] function is EC(kun), and the I value in the VO(kun) is 0.

  • The parent execution environment of the result[9] function is EC(kun), and the value of I in the VO(kun) is 9.

Remember that AO(kun) is a piece of storage space.

For scoped chains (scopeChain), the list form of the function in result is still the following:

AO(result[i]) –> AO(kun) –> VO(G)

But the difference is that the storage address of the corresponding node is different, which is equivalent to 10 new AO(kun). The I value in the node content of each AO(kun) is different.

So the summary is: executing the 10 functions in the Result array runs 10 different linked lists, and the AO(kun) nodes of each list are different. The I value in each AO(kun) node is also different.

So the output will end up being 0 to 9.

Six, summarized

Through the analysis of the underlying implementation principle, we can understand the function execution mechanism more deeply, so as to write high-quality functions.

How do I reduce scoped chain (linked list) lookups

For example, many libraries, such as JQ, pass a window argument at the end of the immediate function. This is done because window is a global object, and by passing parameters, it avoids searching the entire scope chain and improves the efficiency of function execution.

How to prevent stack overflow? Each time a function is executed, it creates an execution environment for the function, which means that some stack memory is occupied, and the stack memory size is fixed. If you write a large recursive function, the stack memory will run out and cause errors.

I think we should strive to achieve an achievement like this:

When I write a function by hand, I clearly know in my mind every line of code I am writing, how it is represented in memory, or how it is executed at the bottom, so as to achieve the realm of code in my eyes and no code in my heart.

If you can do that, why not write a high-quality function?

Reference documentation

  1. JS function creation and execution mechanism

  2. Explore how the JS engine works

  3. Program memory space (code segment, data segment, stack segment)

  4. Function call – function stack

  5. An in-depth understanding of stack growth up and down


Vivo Internet Technology

Vivo mobile Internet is a complete mobile Internet ecosystem based on Vivo smart phones. It operates around Vivo big data to create a full-service ecosystem including applications, games, information, brands, e-commerce, content and finance to meet the diversified needs of massive users.

You’ll look better if you click on it