JS进阶:闭包和作用域

首先我们来看一个常见的问题

为什么用let声明的时候,显示的是1,2,3,4;而用var声明的时候就显示5,5,5,5呢?

这其中涉及到两个问题:1.var和let的作用域 2. setTimeout的异步

今天先解决第一个问题。


学习自:JS忍者秘籍

原生JavaScript不支持私有变量。但是,通过使用闭包,我们可以实现很接近的、可接受的私有变量。闭包内部的变量可以通过闭包内的方法访问,构造器外部的代码则不能访问闭包内部的变量。

一句话理解闭包:内部的嵌套函数可以访问到外部函数的变量,即使这个外部函数已经执行完毕退出了。

当在外部函数中声明内部函数时,不仅定义了函数的声明,而且还创建了一个闭包。该闭包不仅包含了函数的声明,还包含了在函数声明时该作用域中的所有变量。当最终执行内部函数时,尽管声明时的作用域已经消失了,但是通过闭包,仍然能够访问到原始作用域。闭包不是在创建的那一时刻的状态的快照,而是一个真实的状态封装,只要闭包存在,就可以对变量进行修改。

谨记每一个通过闭包访问变量的函数都具有一个作用域链,作用域链包含闭包的全部信息使用闭包时,所有的信息都会存储在内存中,直到JavaScript引擎确保这些信息不再使用(可以安全地进行垃圾回收)或页面卸载时,才会清理这些信息。只要至少有一个通过闭包访问这些变量的函数存在,这个环境就会一直保持。

JavaScript引擎执行代码时,每一条语句都处于特定的执行上下文中。有两种执行上下文:全局执行上下文和函数执行上下文。二者最重要的差别是:全局执行上下文只有一个,当JavaScript程序开始执行时就已经创建了全局上下文;而函数执行上下文是在每次调用函数时,就会创建一个新的。

注意区分函数上下文(this)和执行上下文(调用栈)

一旦发生函数调用,当前的执行上下文必须停止执行,并创建新的函数执行上下文来执行函数。当函数执行完成后,将函数执行上下文销毁,并重新回到发生调用时的执行上下文中。

词法环境(lexical environment)是JavaScript引擎内部用来跟踪标识符与特定变量之间的映射关系。通常称为作用域(scopes)。

无论何时调用函数,都会创建一个新的执行环境,被推入执行上下文栈。此外,还会创建一个与之相关联的词法环境,词法环境通常用于保持跟踪函数中定义的变量。并存储在名为[[Environment]]的内部属性上(也就是说无法直接访问或操作)。JavaScript引擎将调用函数的内置[[Environment]]属性与创建函数时的环境进行关联。

在作用域范围内,每次执行代码时,代码结构都获得与之关联的词法环境。内部代码结构可以访问外部代码结构中定义的变量。如果在当前环境中无法找到某一标识符,就会对外部环境进行查找。

var、let与const

const静态变量在声明时需要写初始值,一旦声明完成之后,其值就无法更改。对于对象类型而言,我们不能将一个全新的对象赋值给const变量。但是,我们可以修改const变量已有的对象。如给已有对象添加属性,给数组push新值

当使用关键字var时,该变量是在距离最近的函数内部或是在全局词法环境中定义的。(注意:忽略块级作用域)


变量forMessage与i虽然是被包含在for循环中,但实际是在reportActivity环境中注册的

当使用let与const声明变量时,变量是在距离最近的环境中定义的。

注册标识符的过程

对于所找到的函数声明,将创建函数,并绑定到当前环境与函数名相同的标识符上。若该标识符已经存在,那么该标识符的值将被重写

因此我们可以直接使用函数的引用,而不需要强制指定函数的定义顺序。

需要注意的是,这种情况仅针对函数声明有效。函数表达式与箭头函数都不在此过程中,而是在程序执行过程中执行定义的。

变量提升。例如,变量的声明提升至函数顶部,函数的声明提升至全局代码顶部。变量和函数的声明并没有实际发生移动。只是在代码执行之前,先在词法环境中进行注册。

变量提升

变量提升就好比JavaScript引擎用一个很小的代码起重机将所有var声明和function函数声明都举起到所属作用域的最高处

预解析处理的工作主要是变量提升和给变量分配内存,具体过程是在每个作用域中查找var声明的变量、函数定义和命名参数(函数参数),找到它们后,在当前作用域中给它们分配内存,并给它们设置初始值。预解析设置的初始值分别是:对于var声明的变量,初始值为undefinded;对函数定义,变量名为函数名,函数变量的初始值为函数定义本身;对命名参数,如果函数调用时没有指定参数值,则命名参数的初始值为undefined,如果函数调用时指定了参数值,则命名参数的初始值为指定的参数值。

var声明支持变量提升,而const和let声明不支持变量提升。

如果你使用了一个从未声明过的变量,程序运行会直接抛出异常。但是如果代码中有过对此变量的声明,无论在声明前使用还是在声明后使用,程序都不会抛出异常。

还有一点,如果你直接对一个未经声明的变量进行赋值,就相当于直接在全局作用域中创建了这样一个变量

注意只是把声明进行了提升。

JavaScript解释器在对代码进行扫描时,会将全局作用域中声明的变量和函数先定义为全局符号,运行到具体声明处才进行赋值。

i和sum都成了全局变量,name也被声明过了。

存在形参的情况

1
2
3
4
5
6
7
function fn(a) {
console.log(a)
var a = 2
function a() {}
console.log(a)
}
fn(1)

这个题目,最终会输出function a(){} 和 2

因为如果函数体内的变量名和形参的变量名重复时,则不会进行普通变量的编译赋值 undefined 的过程。但是,如果存在该变量是函数时,那么则会进行函数变量的编译赋值,即直接指向函数在堆空间中的地址。

所以,实际上是

1
2
3
4
5
6
7
8
function fn() {
var a = undefined
a = 1
a = function a() {}
console.log(a)
a = 2
console.log(2)
}

注意点