首先我们来看一个常见的问题
为什么用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 | function fn(a) { |
这个题目,最终会输出function a(){} 和 2
因为如果函数体内的变量名和形参的变量名重复时,则不会进行普通变量的编译赋值 undefined 的过程。但是,如果存在该变量是函数时,那么则会进行函数变量的编译赋值,即直接指向函数在堆空间中的地址。
所以,实际上是
1 | function fn() { |