# 变量
程序在执行任务的时候需要对值进行操作, 因此程序需要追踪值的变化. 实现这一点的最简单方法为将值赋给一个符号容器, 这个符号容器称为变量。顾名思义, 这个容器中的值是可以变化的. JavaScript 中变量的名字又叫做 『 标识符 』.
JavaScript 中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。这被称为 『 动态类型 』或 『 弱类型 』
# 命名规则
一个 JavaScript 标识符必须以字母、下划线(_)或者美元符号($)开头.
后续的字符也可以是数字(0-9), 或大部分 ISO 8859-1 或 Unicode 编码的字符. 也可以使用 Unicode 转义字符 作标识符。
# 声明变量
相关参考:
# var
var
关键字可以用来声明变量; 通过逗号 (,) 分隔可以同时创建多个对象; 通过等号 (=) 可以在声明时初始化变量.
var a;
var b, c, d = 3;
未经初始化的变量值为 undefined
.
在一个作用域下重复声明的变量, 会被解析器给忽略. 如果试图访问一个没有声明的变量, 引擎因为找不到变量, 会沿着作用域链一直向上搜寻, 直到全局作用域. 在非严格模式下, 会在全局作用域创建一个同名变量; 在严格模式下会抛出 ReferenceError 异常.
# let
let
声明的变量只可在其所在的代码块中被访问. 由此可以实现 "块级作用域".
var a = 123;
{
let a = 456;
console.log(a); // 456
}
# 不会变量提升
var
声明的变量会发生 "变量提升", 即变量可以在声明之前使用, 值为undefined
。为了纠正这种现象,let
命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
# 暂时性死区
只要块级作用域内存在 let
命令,它所声明的变量就 “绑定”(binding)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上面代码中, 即使 if 代码块外面有已经声明 tmp
变量, 但因为在块中存在 let
关键字, 那么 tmp
变量不受外部影响, 使用 let
命令声明变量之前,该变量都是不可用的。
# 不允许重复声明
let不允许在相同作用域内,重复声明同一个变量。
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
# 函数声明 & 块级作用域
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
ES6 规定,块级作用域之中,函数声明语句的行为类似于 let
,在块级作用域之外不可引用。
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
上面代码在 ES5 中运行,会得到 “I am inside!”,因为在 if
内声明的函数 f
会被提升到函数头部. 而在 ES6 中,理论上会得到“I am outside!”。但因为浏览器为了保证兼容性, 在处理方式上有自己的办法:
- 允许在块级作用域内声明函数;
- 函数声明会提升到全局作用域或函数作用域的头部;
- 同时,函数声明还会提升到所在的块级作用域的头部;
上面的代码在浏览器的 ES6 环境中, 实际运行的代码是下面这样:
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式.
# const
const
声明一个只读的常量。一旦声明,常量的值就不能改变。在声明时, 必须进行初始化.
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
const
实际上保证的是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址。对于引用类型,变量指向的内存地址,保存的是一个指向保存在堆内存的对象的指针.
# 注册标识符过程
创建了新的词法环境之后,就会执行第一阶段。在第一阶段,没有执行代码,JavaScript 引擎会访问并注册在当前词法环境中所声明的变量和函数。
第二阶段,具体如何执行取决于标识符的声明的方式(let
、var
、const
和 函数声明)以及环境类型(全局环境、函数环境 或 块级作用域):
- 如果创建一个函数环境,那么创建形参及函数参数的默认值。如果是非函数环境,将跳过此步骤。
- 如果创建全局或函数环境,就扫描当前代码进行函数声明(不会扫描其他函数的函数体),但是不会扫描函数表达式或箭头函数。对于所找到的函数声明,将创建函数,并绑定到当前环境与函数名相同的标识符上。若该标识符已经存在,那么该标识符的值将被重写。如果是块级作用域,将跳过此步骤。
- 扫描当前代码进行变量声明。在函数或全局环境中,找到所有当前函数以及其他函数之外通过
var
声明的变量,并找到所有在其他函数或代码块之外通过let
或const
定义的变量。在块级环境中,仅查找当前块中通过let
或const
定义的变量。对于所查找到的变量,若该标识符不存在,进行注册并将其初始化为undefined
。若该标识符已经存在,将保留其值。
# 变量提升
"提升" 指的是变量和函数声明从它们在代码中出现的位置好像被 “移动” 到了最上面。
编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。也就是包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
var a = 2;
会被看成 var a;
和 a = 2;
。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。
# 函数优先
函数声明和变量声明都会被提升。但是函数会首先被提升,然后才是变量。
foo(); // 1
var foo;
function foo() { console.log( 1 ); }
foo = function () { console.log( 2 ); };
会输出 1 而不是 2 !这个代码片段会被 引擎 理解为如下形式:
function foo() { console.log( 1 ); }
foo(); // 1
foo = function () { console.log( 2 ); };
注意, var
声明因为是重复的声明, 所以会被忽略, "作用域篇" 里讲过.