day11 通过JS引擎的堆栈了解闭包原理

静态和动态作用域
静态作用域,取决于变量和函数在何处声明,这里你可以想象成它的“出生地”,并在它执行之前就已经确定了。所以静态作用域又被称为词法作用域(lexical scope),因为函数的“出生地”是在词法分析时“登记”的。
动态作用域下,函数的作用域是在函数调用的时候才决定的。所以取决于在何处调用,这里你可以想象成它的“居住地”,这个是可以后天修改的。
JavaScript 代码,通常是通过前端浏览器编译后运行的,这个过程是先编译、后执行。所以 JavaScript 代码的作用域是在编译过程中通过分析它在何处声明来确定的,属于静态(词法)作用域。
作用域:代码编译

栈是线性连续的数据结构的存储空间,里面主要存有 JavaScript 原始数据类型以及对象等复杂数据类型的地址。除此之外还有函数的执行状态和 this 值。堆是树形非连续的数据结构的存储空间,里面存储了对象、数组、函数等复杂数据类型,还有系统内置的 window 和 document 对象。
下面通过一段代码,我们再来看下从词法到语法分析的过程。var base = 0;

var scope = "global";
function addOne () {
    var base = 1;
    return base +1;
}

function displayVal () {
    var base = 2;
    var scope = "local"
    increment = addOne();
    return base + increment;
}


分词或词法分析(tokenizing/lexing)的过程。在这个过程中,比如 var base = 0 会被分为 var 变量、base、赋值表达、数字常量 0。 词法作用域指的就是拆分成词法标记时这段代码所在的作用域。如下图红色虚线框部分所示:

在词法拆分之后,在下一步的解析(parsing)动作中,上面一段段的代码会被转换成一个抽象语法树(AST, Abstract Syntax Tree),这就到了语法分析。

根据流程图中的红色虚线框部分所示,在词法分析后,JavaScript 引擎会在做语法分析的同时,更新全局作用域和创建局部作用域。

在作用域创建后,上面的代码就会变为中间代码,V8 会混合使用编译器和解释器技术的双轮驱动设计实时编译(JIT Just in Time),这个双轮的一个轮子是直接执行,另一个发现热点代码会优化成机器码再执行,这样做的目的是为了性能的权衡和提升。
我们抽象总结一下。这里我们从空间角度了解到,函数在创建伊始是存放在堆空间中的,并且通过栈空间中的地址来查找。我们通过编译的过程,了解了作用域在代码未执行的解析阶段就完成了。
生命周期:代码执行
咱们就来看看在代码执行的阶段,一个函数从调用到结束的过程,也就是它的生命周期。

函数的生命周期
在 JavaScript 执行的时候,全局执行上下文会在一个类似栈的数据结构里面,根据函数调用链依次执行,所以又称为调用栈。

一开始,base、scope、addOne、displayVal 都会被记录在变量环境。可执行的代码包含了 base 和 scope 的赋值,还有 displayVal() 函数的调用。当赋值结束就会执行 displayVal 函数。

在执行 displayVal 函数的时候,displayVal 函数相关的全局上下文就会被压入栈内,因为 base 和 scope 都有函数内声明,所以它们在函数内也会有变量提升到 increment 的上面。作为可执行代码,完成 base 和 scope 的赋值。下面执行 addOne 函数的调用。

再后面,需要继续将 addOne 压入栈内,base 变量再次赋值,然后执行返回 base+1 的结果。在此以后,函数 addOne 的上下文会从栈里弹出,作为值返回到 displayVal 函数。

在最后的运行步骤里,displayVal 的 increment 会被赋值为 2,之后函数会返回 2+2 的值,为 4。之后 displayVal 的函数执行上下文也会被弹出,栈中将只剩下全局的执行上下文。addOne 和 displayVal 这两个函数的生命周期就随着执行的结束而结束了,并且会在之后的垃圾回收过程中被回收。

执行时变量查找
以 var base = 0 为例,在下图的左边,我们可以看到当编译器遇到 var base 的时候,会问作用域这个 base 是否已经存在,如果是的话,会忽略这个声明;如果 base 不存在,则会让作用域创建一个新变量 base,之后会让引擎处理 base=2 的赋值。
在当前执行的作用域当中有没有 base 这个变量,如果有的话,执行赋值,否则会继续寻找,一直到找到为止。
day11 通过JS引擎的堆栈了解闭包原理_第1张图片
如果引擎在当前执行作用域找不到相关变量,会一直找或返回报错。“一直找”:从内往外地找。
day11 通过JS引擎的堆栈了解闭包原理_第2张图片
IIFE:利用作用域封装
块级作用域和函数级作用域都可以帮助我们对代码进行封装,控制代码的可见性。
带来的两个问题:
第一个是如果我们以声明式函数为目的来做封装的话,它会间接地创建 foo 这个函数,会对全局作用域造成污染;
第二个问题是我们需要通过一个 foo() 来对它进行调用,解决这个问题的办法就是使用一个立刻调用的函数表达 (IIFE,immediately invoked function expression)。

var a = 2;
(function foo(){
    var a = 3;
    console.log( a ); // 3
})();
console.log( a ); // 2

闭包:突破作用域限制
运用了函数的 3 个特点:
在函数内部生成了一个局部的变量 i;
嵌入了 increment 和 getValue 这两个函数方法;
把函数作为返回值用 return 来返回。

function createCounter(){
    let i=0;
    function increment(){
        i++;
    }  
 
    function getValue(){
        return i; 
    }
    return {increment,getValue}
}

const counter = createCounter();

考虑到性能、内存和执行速度,当使用闭包的时候,尽量使用本地而不要用全局变量。
提升问题和解决方法

base = 2;
var base;
console.log(base); // 2

day11 通过JS引擎的堆栈了解闭包原理_第3张图片

day11 通过JS引擎的堆栈了解闭包原理_第4张图片
函数提升的只是声明式函数,而表达式函数则和变量赋值一样,不会被提升。
ES6 块级作用域 let 和 const,这两个变量和常量就是块级作用域的变量,它们不会被提升。

{
    console.log(base); // ReferenceError!
    let base = 0;
}
var base = 1;
if (base) {
    let count = base * 2;
    console.log( count );
}
console.log( count ); // ReferenceError

你可能感兴趣的:(前端javascript)