变量声明与变量提升
一、原始值与引用值
ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值
1. 定义
- 原始值(primitive value)就是最简单的数据
- 引用值(reference value)则是由多个值构成的对象
注意:在把一个值赋给变量时,JavaScript 引擎必须确定这个值是原始值还是引用值
2. 存储
-
原始值
javascriptUndefined、Null、Boolean、Number、String 和 Symbol
保存原始值的变量是按值(by value)访问的,因为我们操作的就是存储在变量中的实际值。
-
引用值是保存在内存中的对象
与其他语言不同,JavaScript 不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身;保存引用值的变量是按引用(by reference)访问的。
二、变量声明
ES6 之后,JavaScript 的变量声明经历了翻天覆地的变化。直到 ECMAScript 5.1,var 都是声明变量的唯一关键字。ES6 不仅增加了 let 和 const 两个关键字,而且还让这两个关键字压倒性地超越 var 成为首选。
1. 使用 var 的函数作用域声明
在使用 var 声明变量时,变量会被自动添加到最接近的上下文
-
在函数中,最接近的上下文就是函数的局部上下文。在 with 语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化了,那么它就会自动被添加到全局上下文。如下面的例子所示:
javascriptfunction add(num1, num2) { var sum = num1 + num2; return sum; } let result = add(10, 20); // 30 console.log(sum); // 报错:sum 在这里不是有效变量
-
如果省略上面例子中的关键字var,那么sum在add()被调用之后就变成可以访问的了,如下所示:
javascriptfunction add(num1, num2) { sum = num1 + num2; return sum; } let result = add(10, 20); // 30 console.log(sum); // 30
这次,变量 sum 被用加法操作的结果初始化时并没有使用 var 声明。在调用 add()之后,sum被添加到了全局上下文,在函数退出之后依然存在,从而在后面可以访问到
注意:未经声明而初始化变量是 JavaScript 编程中一个非常常见的错误,会导致很多问题。为此,读者在初始化变量之前一定要先声明变量。在严格模式下,未经声明就初始化变量会报错
2. 使用 let 的块级作用域声明
ES6 新增的 let 关键字跟 var 很相似,但它的作用域是块级的,这也是 JS中的新概念
-
块级作用域由最近的一对包含花括号{}界定。换句话说,if 块、while 块、function 块,甚至连单独的块也是 let 声明变量的作用域。
javascriptif (true) { let a; } console.log(a); // ReferenceError: a 没有定义 while (true) { let b; } console.log(b); // ReferenceError: b 没有定义 function foo() { let c; } console.log(c); // ReferenceError: c 没有定义 // 这没什么可奇怪的 // var 声明也会导致报错 // 这不是对象字面量,而是一个独立的块 // JavaScript 解释器会根据其中内容识别出它来 { let d; } console.log(d); // ReferenceError: d 没有定义
-
let 与 var 的另一个不同之处是在同一作用域内不能声明两次。重复的 var 声明会被忽略,而重复的 let 声明会抛出 SyntaxError。
javascriptvar a; var a; // 不会报错 { let b; let b; } // SyntaxError: 标识符 b 已经声明过了
-
let 的行为非常适合在循环中声明迭代变量。使用 var 声明的迭代变量会泄漏到循环外部,这种情况应该避免。来看下面两个例子:
javascriptfor (var i = 0; i < 10; ++i) {} console.log(i); // 10 for (let j = 0; j < 10; ++j) {} console.log(j); // ReferenceError: j 没有定义
严格来讲,let 在 JavaScript 运行时中也会被提升,但由于"暂时性死区"(temporal dead zone)的缘故,实际上不能在声明之前使用 let 变量。因此,从写 JavaScript 代码的角度说,let 的提升跟 var是不一样的。
3. 使用 const 的常量声明
除了 let,ES6 同时还增加了 const 关键字。使用 const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。
-
const常量声明的示例:
javascriptconst a; // SyntaxError: 常量声明时没有初始化 const b = 3; console.log(b); // 3 b = 4; // TypeError: 给常量赋值 // const 除了要遵循以上规则,其他方面与 let 声明是一样的: if (true) { const a = 0; } console.log(a); // ReferenceError: a 没有定义 while (true) { const b = 1; } console.log(b); // ReferenceError: b 没有定义 function foo() { const c = 2; } console.log(c); // ReferenceError: c 没有定义 { const d = 3; } console.log(d); // ReferenceError: d 没有定义
由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化。
二、变量提升
1. 使用 var 的函数作用域声明导致的变量提升
- 把所有var声明的变量提升到当前作用域的最前面
- 只提升声明, 不提升赋值
var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作"提升"(hoisting)。提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用。可是在实践中,提升也会导致合法却奇怪的现象,即在变量声明之前使用变量。下面的例子展示了在全局作用域中两段等价的代码:
javascript
var name = "Jake";
// 等价于:
name = 'Jake';
var name;
// 下面是两个等价的函数:
function fn1() {
var name = 'Jake';
}
// 等价于:
function fn2() {
var name;
name = 'Jake';
}
注意:严格来讲,let 在 JavaScript 运行时中也会被提升,但由于"暂时性区"(temporal dead zone)的缘故,实际上不能在声明之前使用 let 变量。因此,从写 JavaScript 代码的角度说,let 的提升跟 var是不一样的。
javascript
for (var i = 0; i < 10; ++i) {}
console.log(i); // 10
for (let j = 0; j < 10; ++j) {}
console.log(j); // ReferenceError: j 没有定义
2. 变量提升导致的结果
通过在声明之前打印变量,可以验证变量会被提升。声明的提升意味着会输出 undefined 而不是Reference Error:
javascript
console.log(name); // undefined
var name = 'Jake';
function() {
console.log(name); // undefined
var name = 'Jake';
}
3. 在WEB开发中的变量声明注意事项:
-
以后声明变量我们优先使用哪个?
const
:有了变量先给const,如果发现它后面是要被修改的,再改为let -
为什么const声明的对象可以修改里面的属性?
因为对象是引用类型,里面存储的是地址,只要地址不变,就不会报错。建议数组和对象使用 const 来声明
-
什么时候使用let声明变量?
如果基本数据类型的值或者引用类型的地址发生变化的时候,需要用let。比如 一个变量进行加减运算,比如 for循环中的 i++
注意:
- 变量在未声明即被访问时会报语法错误
- 变量在声明之前即被访问,变量的值为
undefined
let
声明的变量不存在变量提升,推荐使用let
- 变量提升出现在相同作用域当中
- 实际开发中推荐先声明再访问变量
注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用
let
可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料。