javascript之闭包三(理解返回函数的函数)

1,076 阅读5分钟

三、理解返回函数的函数

在第一个示例中,函数 addTwo 返回一个数值。请记住,函数可以返回任何内容。让我们看一个返回函数的函数的示例,因为这对理解闭包来说很重要。

let val = 7
function createAdder() {
    function addNumbers(a, b) {
        let ret = a + b
        return ret
    }
    return addNumbers
}
let adder = createAdder()
let sum = adder(val, 8)
console.log('example of function returning a function: ', sum)

让我们来逐步分析它的执行过程:

1、第 1 行,我们在全局执行上下文中声明一个变量 val,并将数值 7 赋给该变量。

2、第 2-8 行,我们在全局执行上下文中声明了一个名为 createAdder 的变量,并为其分配了一个函数定义。第 3 至 7 行是这个函数的定义,这个时候我们并没有跳进那个函数,只是将函数定义存储到该变量(createAdder)中。

3、第 9 行,我们在全局执行上下文中声明一个名为 adder 的新变量,并暂时赋值为 undefined。

4、第 9 行,我们看到了括号 ()。我们需要执行或调用函数。我们在全局执行上下文的内存中查找名为 createAdder 的变量,它是在第 2 步中创建的。找到它,然后调用它。

5、第 2 行,调用一个函数,创建了一个新的本地执行上下文。我们可以在新的执行上下文中创建局部变量,引擎将新上下文添加到调用栈。这个函数没有参数,所以直接进入它的函数体。

6、3-6 行,我们有一个新的函数声明,我们在本地执行上下文中创建变量 addNumbers,这很重要。addNumbers 仅在本地执行上下文中存在,我们将函数定义存储在名为 addNumbers 的局部变量中。

7、第 7 行,我们返回变量 addNumbers 的内容。引擎查找名为 addNumbers 的变量,这是一个函数定义。一个函数可以返回任何东西,包括函数定义,所以我们返回 addNumbers 的定义。第 4 行和第 5 行的括号之间的任何内容构成了函数定义,我们还从调用栈中删除了本地执行上下文。

8、在 return 语句之后,本地执行上下文被销毁,addNumbers 变量也不复存在,但函数定义仍然存在,它从函数返回并赋给变量 adder,这是我们在第 3 步中创建的变量。

9、第 10 行,我们在全局执行上下文中定义了一个新的变量 sum,临时赋值 undefined。

10、接下来我们需要执行一个函数,哪个函数?在名为 adder 的变量中定义的函数。我们在全局执行环境中查找它,这是一个带两个参数的函数。

11、我们先拿到两个参数,这样就可以调用函数并将正确的参数传给它。第一个是变量 val,在步骤 1 中定义的,它代表数值 7,第二个是数值 8。

12、现在我们要执行这个函数,函数体是在第 3-5 行定义的。创建一个新的本地执行上下文。在本地上下文中,创建了两个新变量:a 和 b。它们分别被赋值为 7 和 8,因为它们是在上一步传给函数的参数。

1)第 4 行,在本地执行上下文中声明一个新变量 ret。

2)第 4 行,执行加法运算,其中我们变量 a 的内容和变量 b 的内容相加,再将相加的结果(15)赋给变量 ret。

3)从函数返回变量 ret,本地执行上下文被销毁,并从调用栈中删除,变量 a、b 和 ret 不再存在。

4)返回的值被赋给我们在步骤 9 中定义的 sum 变量。

5)我们将 sum 的值打印到控制台。

正如预期的那样,控制台将打印出 15。我想说明几点:首先,函数定义可以存储在变量中,函数定义在被调用之前对程序是不可见的。其次,每次调用函数时,就会临时创建一个本地执行上下文,当函数执行结束时,执行上下文就被销毁。函数在遇到 return 语句或结束括号}时执行结束。

函数生命周期

直接上图,点击图片放大查看。要记住函数对象、作用域链对象、执行环境(EC)和活动对象(AO)这几个东西都啥时候出现,啥时候消失。

例子实战

看下面这个函数,函数对象的地址仅作标识,不代表真实的地址。

var getNum;//------------------------1
function getCounter() { // ----------2
    var n = 1; 
    var inner = function () { return n++; }
    return inner;
}
getNum = getCounter();//------------3
console.log(getNum()); //1 ---------4
console.log(getNum()); //2 ---------5

程序运行到2的时候:

程序运行到3的时候:

运行到4的时候,外层函数调用结束,AO对象释放,图中红线断了:

4处的代码执行时:
4处的代码执行完:

总结:可以看到内层函数对象被全局的变量getNum引用和外层函数的AO对象引用,外层函数的AO对象,内层函数本身的作用域链对象在函数调用完都无法被内存回收,因此占用了更多的内存空间,但是这样持久的保存了需要的n。