Advanced function use-recursion-closure

Preface

Summarize the differences caused by different function definitions and call locations, understand recursion and master basic usage, explore the principles of closure generation, and understand the concept of memory leaks. Other related notes:

1. Summary of the location of function definitions and calls

Before summarizing recursion and closures, I first made a summary of the different situations where the function is defined and where it is called. The main characteristics of each situation are as follows:

  1. How to use ordinary functions.
  2. Callback function usage is often combined with anonymous functions to manipulate the processing results of asynchronous APIs .
  3. The main feature of this situation is that a closure will be generated, which will be described in detail below .
  4. Of course, using the returned closure function as a global variable can also be called from within other functions.
  5. Although this kind of situation will also produce closures, but the scope of the closure is limited to the inside of the containing function. After the containing function is executed, the closure function reference is destroyed .
  6. This kind of function is also called recursive function. Pay attention to the exit condition when using it to prevent infinite loop .
    // 1、函数在全局作用域定义并调用
    function outer1(x) {
    
    
        console.log(x);

    };
    outer1(1);
    
    // 2、在函数内部调用另一个外部定义的函数
    function outer2(callback) {
    
    
        let a = 2;
        callback && callback(a);
    }
    // 这种方式常用于操作异步API处理结果
    outer2(outer1);

    function outer3() {
    
    
        let a = 3;
        // 直接调用跟上面的调用没有本质区别,不易维护
        outer1(a);
    }
    outer3();

    // 3、函数内部定义的函数在函数外部调用(产生闭包)
    function outer4() {
    
    
        let a = 4;
        // 这里内部函数有没有传参均一样,因为只是定义没有调用
        return function insider() {
    
    
            console.log(a);
        };
    };
    // 如果只是定义没有返回则只是私有方法,外部无法访问
    const insider1 = outer4();
    insider1();

    // 4、内部定义的函数返回后也可以在其他函数内部使用,形同 2

    // 5、内部定义的函数在函数内部自己调用(也会产生闭包)
    function outer5() {
    
    
        let a = 5;
        // 这里内部函数有没有传参均一样,因为只是定义没有调用
        function insider2() {
    
    
            console.log(a);
        };
        insider2();
    };
    // 这里产生的闭包会在outer5执行结束后销毁
    outer5();
    // 6、外部的函数在自己内部调用自己
    function outer6(num) {
    
    
        return num == 1 ? num : num * outer6(num - 1);
    }
    console.log(outer6(4));

2. Recursion

Recursion: The way to call the function itself in the function definition is recursion. Take the definition of a function that calculates the factorial of n as an example. The definition of n! is that when n is equal to 0 or 1, the value of n! is 1; when the value of n is other values, n! = nx (n-1)!

    function fn(num) {
    
    
        if (num == 1 || num == 0) {
    
    
            return 1;
        } else {
    
    
            return num * fn(num - 1)
        };
    };
    console.log(fn(4));

The execution process is shown in the following figure:

Insert picture description here

Two key characteristics of recursion:

  • Chain: There is a regular and orderly recursive chain in the calculation process. For example, the factorial of n is equal to the product of the factorial of n and n-1, then the factorial of n and n-1 constitutes a recursive chain.
  • Base cases: basic examples, there are one or more base cases that do not need to recurse again, they are the basis for ending recursion. For example, when n is 0 or n is 1, the factorial value is the exact value 1, and there is no recursive relationship with other values.

The above two characteristics of recursion are indispensable, but the following detailed problems will arise when implementing recursive functions.

    // 1、递归求阶乘
    function fn(num) {
    
    
        return num == 1 ? num : num * fn(num - 1);
    };
    console.log(fn(4));

    var factorial = fn;
    console.log(factorial(4));
    // 删除fn指向递归函数的指针
    fn = null;
    // 由于在内层调用时找不到fn函数而报错
    console.log(factorial(4)) // ->error

	// 2、在非严格模式下可以使用arguments.callee代替递归函数本身
	// 'use strict'
	function fn(num) {
    
    
		// arguments.callee指向当前执行的函数
        return num == 1 ? num : num * arguments.callee(num - 1);
    };
    console.log(fn(4));

    var factorial = fn;
    console.log(factorial(4));
    fn = null;
    console.log(factorial(4))

   // 3、严格模式下不能通过脚本访问callee,采用函数表达式的形式
    var factorial = (function fn(num) {
    
    
        return num == 1 ? num : num * fn(num - 1);
    });
    console.log(factorial(4));
    fn = null;
    console.log(factorial(4));
    // 即使再次赋值给另一个变量,函数名fn依旧有效
    var fn2 = factorial;
    factorial = null;
    console.log(fn2(4));

Example 3 seems more difficult to understand. My personal understanding is that when using an expression to declare a function, the value of the fn function expression is assigned to the factorial variable. Since it is an expression, it needs to return its final value, so it needs to return a reference to the function fn. And this is not the final value, the inner fn is also replaced with the reference value. So no matter how the external variable name changes, the internal reference value will not change. Example 2 is different, it just defines a function object named fn in the global scope (just a declaration statement, not an expression statement). The fn inside is still just an identifier and does not return a reference to fn.

The above is the understanding of recursion. Recursion has many applications besides calculating factorial. For example, finding the Fibonacci sequence and solving the Tower of Hanoi problem.

Fibonacci sequence : The value of the first and second terms of the sequence is 1, and the values ​​of other terms are the sum of the first two items. Find the value of the nth term.

    var fact = function fn(n) {
    
    
        return n == 1 || n == 2 ? 1 : fn(n - 1) + fn(n - 2);
    };

    console.log(fact(8)); // ->21 (1, 1, 2, 3, 5, 8, 13, 21)

Tower of Hanoi problem : There are three pillars ABC, pillar A is stacked with n discs in order from bottom to top. The requirement is to move the disc one at a time, and the disc cannot be enlarged on the small disc. Finally, all the discs are placed on another pillar in the same order.

    var hanoi = function fn(disc, src, aux, dst) {
    
    

        if (disc > 0) {
    
    

            fn(disc - 1, src, dst, aux);

            console.log(disc + ':' + src + '-->' + dst);

            fn(disc - 1, aux, src, dst)

        }

    }

    hanoi(3, 'A', 'B', 'C')

3. Closure

3.1 Overview of closures

Closures are functions that have access to variables in the scope of another function . For example, a simple example in the above function definition and call location summary:

    function outer4() {
    
    
        let a = 4;

        return function insider() {
    
    
        	let b = 'insider';
            console.log(a);
        };
    };
    // 如果只是定义没有返回则只是私有方法,外部无法访问
    const insider1 = outer4();
    insider1();

Insert picture description here

The insider() function in the above code accesses the variable a in the external function outer4(). Even if this internal function is returned and is not called inside outer(), it can still access the variable a. You must understand what happens when the function is created and called before you thoroughly understand the closure.

3.2 Principle of Closure Generation

When creating a function object , an internal [[Scopes]] property is created for it . This attribute holds the scope chain that can be directly accessed in the execution environment when the function is defined . Note that only the function definition is not executed at this time, so the front end of the scope chain pointed to by this attribute is not the variable object pointer of the function. A concept must be clarified here: the scope chain is essentially a list of pointers to variable objects, which only refers to but does not actually contain variable objects . As shown below:

Insert picture description here

When a function is called , an execution environment and corresponding scope chain will be created. Then use the values ​​of arguments and other named parameters to initialize the active object of the function. When constructing the scope chain, the object in the [[Scopes]] property of the function is (shallowly) copied and the active object (variable object) created for the function is pushed into the front end of the scope of the execution environment .

The figure shows the relationship and status of the scope chain and their respective [[Scopes]] properties during the definition and calling of the functions outer4() and insider().

Insert picture description hereOnly one pointer to the Window object is stored in the [[Scopes]] pointer list of all functions defined in the global scope . However, in the [[Scopes]] pointer list of the function insider() defined in outer4(), in addition to the Window object pointer, there is also a pointer to the active object of its outer function outer4(). After outer() is executed, after exiting the function execution environment, insider() is returned to the global scope ( the memory space of the function is not reclaimed, that is, the returned function cannot be destroyed, it already exists as a global variable ). Then the first pointer in the [[Scopes]] list property of insider() (this is the first one and Window is the second) still holds the outer4() active object reference, so this active object is also accompanied by insider() is always stored in memory . When calling this function, the [[Scopes]] property will be used to construct the scope chain according to the above method, so the variable a inside outer() can also be accessed. This is the principle of closure.

The insider() function returned by the outer() function is not the same function object every time the outer() function is executed, so the first pointer of their [[Scopes]] property points to a different object ( the principle of a safe constructor ). Therefore, the values ​​of the variables in the formed scope chain do not affect each other. Also note that the closure can only get the final value of any variable in the containing function (the value after the end of the outer function).

    function outer4() {
    
    
        let a = 'insider' + arguments[0];
        console.dir(outer4);

        function insider() {
    
    
            console.log(a);
        };
        return insider;

    };
    // 返回两个不同的 insider 函数
    var insider1 = outer4(1);
    var insider2 = outer4(2);
    insider1(); // ->'insider1'
    insider2(); // ->'insider2'

3.3 Simple application

Closures mainly have the function of extending scope. Here are two simple applications. Other application scenarios will be met in the future and then added.

<body>
    <ul class="nav">
        <li>第 0 个小li</li>
        <li>第 1 个小li</li>
        <li>第 2 个小li</li>
        <li>第 3 个小li</li>
    </ul>
    <script>
        var lis = document.querySelectorAll('li')

        // 错误示范:i此时是全局变量,点击事件外异步任务。点击时i的值已经为4
        for (var i = 0; i < lis.length; i++) {
     
     
            // 绑定点击事件输出i
            lis[i].onclick = function() {
     
     
                console.log('点击元素li的索引:' + i);
            }

        };
        // 解决方案: 利用闭包的方式得到当前小li 的索引号
        for (var i = 0; i < lis.length; i++) {
     
     
            // 利用for循环创建了4个立即执行函数
            // 立即执行函数也成为小闭包因为立即执行函数里面的任何一个函数都可以使用它的i这变量
            (function(i) {
     
     
                // console.log(i);
                lis[i].onclick = function() {
     
     
                    console.log(i);
                }
            })(i);
        }
        // 另一个应用:3秒钟之后,打印所有li元素的内容
        for (var i = 0; i < lis.length; i++) {
     
     
            (function(i) {
     
     
                setTimeout(function() {
     
     
                    console.log(lis[i].innerHTML);
                }, 3000)
            })(i);
        }
    </script>
</body>

Of course, the above method is not the only solution to the problem. For example, you can add a custom attribute index to li before binding the click event, or directly use the let keyword in ES6 to declare block-level scope variables (the immediate execution function above actually has an imitation block The role of level scope) and so on.

3.4 memory leak problem

Memory leak refers to the phenomenon that when the program no longer needs to occupy (use) a certain piece of memory, due to some reasons, this piece of memory is not returned to the operating system or the free memory pool. There are many situations that cause memory leaks, and only the memory leaks caused by closures are recorded here. For example, event processing callbacks lead to bidirectional references between DOM objects and objects in scripts. This is a common cause of leaks.

    function fn1() {
    
    
        var submitBtn = document.getElementById('submitBtn');
        submitBtn.onclick = function() {
    
    
            console.log(submitBtn.id);
        };
    }
    fn1();
    // 改进
    function fn2() {
    
    
        var submitBtn = document.getElementById('submitBtn');
        var id = submitBtn.id;
        submitBtn.onclick = function() {
    
    
            console.log(id);
        };
        submitBtn = null;
    }
    fn2();

In the above example, a closure will be formed. The first closure contains a DOM object submitBtn, but the callback function of the click event accesses only an id attribute of the object. So in order to prevent memory leaks, fn2() declares an id variable to hold the id attribute of the object. Of course, the submitBtn variable must be finally set to null, so that the reference to the DOM object is manually removed, the number of references is smoothly reduced, and the memory occupied by it is guaranteed to be recovered normally.

Guess you like

Origin blog.csdn.net/TKOP_/article/details/114984997