关于js中的作用域与变量
前言
说实话,虽然一直知道js
中有全局作用域、函数作用域和块级作用域这几种作用域,但一直说不清到底怎样才算是块级作用域以及作用域到底用来干啥的。还有就是虽然知道var
声明和let
、const
声明的一些表现差异,但根本无法解释这种差异性为何发生以及变量提升的准确定义是什么。总而言之,在实际使用中将js
与浏览器绑定得过于紧密,而忽略了js
作为一种独立的编程语言的基本特性了!
作用域
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。
LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。
——《你不知道的js(上卷)》
作用域是在js
编译过阶段中产生的,然后在运行阶段发挥作用的。而js
的编译过程发生在运行前不久(尽管编译时间很短,且编译完后立即执行,还是属于先编译然后执行,而非实时运行的),编译过程大概经历分词 → 解析 → 代码生成
这几个阶段:
- 分词:将代码字符串划分成具有
js
语法意义的代码块(词法单元); - 解析:将分词过程中产生的代码块列表转化成具有
js
程序语法结构的树(抽象语法树AST
); - 代码生成:将
AST
转化成可执行代码;
函数作用域:即函数在声明时的词法作用域。
块级作用域:一对大括号内(不包括function
声明后的大括号)的词法作用域,或标记块声明后的大括号内的词法作用域。比如:
1 | { |
变量提升与函数提升
所谓的提升指的就是在代码编译阶段将声明语法单独提前到该作用域的顶部,也就是说每个作用域内都有提升,但是根据声明类型和作用域类型的不同提升的方式也会有所区别。
变量提升
虽然在一般的变量声明赋值操作中,我们觉得好像写的就只有一条语句,然而编译器并不是这么认为,编译器会将诸如var a = 1
这样的语句拆分成两部分理解:var a
(声明语句)和a = 1
(赋值语句);然后在编译的时候将声明语句单独提升到该作用域的顶部,而赋值语句则留在原地不动(当然,如果仅仅只有变量声明,那就相当于没有赋值)!
举个例子:
1 | a = 1 |
在不了解变量提升之前,大多数人都会觉得打印出来的a
应该是undefined
(我之前就是这么觉得的╥﹏╥),然而在编译的时候有了变量提升,实际上执行的代码是这样的:
1 | var a // ←变量声明提升到作用域顶部了 |
函数作用域内也一样:
1 | // 编译前 |
然而并非所有的变量声明都有提升,ES6
新增的let
和const
声明就不会有提升:
1 | console.log(a) // ReferenceError: a is not defined |
除此之外,var
声明会自动忽略块级作用域,也就是说如果var
声明所在的作用域为块级作用域时会自动向上找到最近的非块级作用域(函数作用域或全局作用域),再进行提升;最著名的例子就是这个:
1 | for(var i = 0; i < 10; i++){ |
函数提升
实际上函数声明没有赋值这一说法,因此是整体提升到作用域的顶部的:
1 | foo() |
然而如果是函数表达式,就不存在函数提升了!如果是将函数表达式作为值进行变量赋值,实际上也只是变量提升罢了:
1 | foo() // TypeError: foo is not a function |
提升的优先级
当var
与function
在同一作用域下出现重复声明(即标识符一样)的情况下,优先进行函数提升!
1 | foo() // function |
let、const 和 var
虽然在感觉上知道let
,const
与var
在声明上有一些差异,但是总是说不出来具体差异有哪些,希望在这里详细记录这3种声明的差异性。
差异1:变量提升与暂时性死区
-
var
存在变量提升,而let
和const
则不存在变量提升; -
let
和const
存在暂时性死区(temporal dead zone
,简称TDZ
);
暂时性死区:指的是let
和const
在其声明的作用域内会该标识符强制绑定到该作用域,即该标识符不受外层作用域的影响(外层作用域有同名标识符时无法访问到外层作用域的同名标识符变量,相当于与外层作用域隔离),且无法在声明语句之前对其进行访问!
1 | let tmp = 123 |
差异2:重复声明
var
可以在同一作用域对同一变量进行多次声明,而let
和const
则不允许在同一作用域下对同一变量进行多次声明!
例1:
1 | var a = 1 |
例2:
1 | let a = 1 |
例3:(同一作用域下的let
声明特性优于var
声明特性执行?)
1 | let a = 1 |
差异3:const的独特性
const
相比其他两种声明又多了一些独特性质:
- 声明时必须赋值!
1 | const a // SyntaxError: Missing initializer in const declaration |
- 值(引用指向)不可修改。当
const
声明的变量赋值对象是基本类型时,就是值不可更改;,如果赋值对象是引用类型,那就是引用的指向不可修改(实际上引用对象的属性值依然可以更改)。
1 | // 例1: |
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。
——《ECMAScript 6 入门》