背景:var声明与变量提升
众所周知,ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理场景。
用 var 关键字声明变量,会出现所谓的变量提升。
即无论其实际声明位置在何处,都会被视为声明于所在函数的顶部(如果声明不在任意函数内,则视为在全局作用域的顶部)。
示例说明变量提升:
function getValue(condition) {
if (condition) {
var value = "blue";
// 其他代码
return value;
} else {
// value 在此处可访问,值为 undefined
return null;
}
// value 在此处可访问,值为 undefined
}
如果不太熟悉 JS ,或许会认为 仅当 condition 的值为 true 时,变量 value 才会被创建。但实际, value 无论如何都会被创建。 JS 引擎在后台对 getValue 函数调整成了下面这样:
function getValue(condition) {
var value;
if (condition) {
value = "blue";
// 其他代码
return value;
} else {
return null;
}
}
也就是说:value 变量的声明被提升到了顶部,而初始化工作则保留在原处。这意味着:在 else 分支内value 变量也是可访问的,此处它的值会是 undefined ,因为它并没有被初始化。
为什么需要块级作用域?--先看两种不合理场景
第一种场景,内层变量覆盖外层变量。
var tmp = new Date();
console.log(tmp ); // Wed Dec 11 2019 10:39:11 GMT+0800 (中国标准时间)
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。
第二种场景,用来计数的循环变量泄露为全局变量。
var s = 'china';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
console.log(i);
}
console.log(i); // 5
//由于变量提升,变量i只用来控制循环,但循环结束,它并没消失,泄露成了全局变量。
因此,ES5求我们习惯var声明变量提升,如不理解,就可能导致bug 。
也因此, ES6 引入了块级作用域,让变量的生命周期更加可控。
块级作用域和块级声明
块级声明也就是让所声明的变量在指定块的作用域外无法被访问。
块级作用域(又称词法作用域)在如下情况被创建:
1、在一个函数内部
2、 在一个代码块(由一对花括号包裹)内部
块级作用域是很多类 C 语言的工作机制, ES6 引入块级声明,给 JS 添加灵活性也与其他语言保持了一致性。
一、let 声明变量
基本用法
let 命令,用来声明变量。用法与 var 类似,基本可用 let 代替 var 声明变量。
- let所声明的变量,只在let命令所在的代码块内有效
- 不存在变量提升
- 不允许(禁止)重复声明
- 暂时性死区(TDZ)
即 let将变量的作用域限制在当前代码块中, let 声明不会被提升到当前代码块顶部,因此需手动将 let 声明放置到顶部,以便让变量在整个代码块内可用。
// var 的情况-var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况-为纠正var现象,let改变了语法行为,let声明变量一定要声明后使用,否则报错
console.log(bar); // 报错ReferenceError
let bar = 2;
//--let不允许在相同作用域内,重复声明同一个变量。
function func() {
let a = 10;
var a = 1; // 报错
}
function func() {
let a = 10;
let a = 1; // 报错
}
//--因此,不能在函数内部重新声明参数。
function func(arg) {
let arg; // 报错
}
function func(arg) {
{
let arg; // 不报错
}
}
func()
暂时性死区
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。
if (condition) {
console.log(typeof value); // 引用错误
let value = "blue";
}
然而,在变量被定义的代码块之外可以对该变量使用 typeof。
console.log(typeof value); // "undefined"
if (condition) {
let value = "blue";
}
/*当 typeof 运算符被使用时,value 并没在暂时性死区内,而在定义 value 变量的代码块外。
这意味着此时并没有绑定 value 变量,而 typeof 仅单纯返回了"undefined" 。
*/
有些“死区”比较隐蔽,不太容易发现。
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错--因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。
//--如果y的默认值是x,就不会报错,因为此时x已经声明了。
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
var x = x; // 不报错
let x = x; // ReferenceError: x is not defined // 报错--与var的行为不同。
//因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。
暂时性死区只是块级绑定的一个独特表现,而另一个独特表现则是在循环时使用它。
二、const 声明常量
基本用法
const 命令,声明的变量会被认为是常量。一旦声明,常量的值就不能改变。
意味着,const一旦声明变量,就必须立即初始化赋值,不能留到以后赋值。对于const来说,只声明不赋值,就会报错。
const与let相似之处:
- const所声明的变量,只在const声明所在的块级作用域内有效
- 不存在变量提升
- 不允许(禁止)重复声明
- 暂时性死区(TDZ)
const与let重大区别:
试图对用const 声明的常量进行重新赋值会抛出错误,无论是在严格模式还是非严格模式下:
const maxItems = 5;
maxItems = 6; // 抛出错误
const 本质
const 保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
对于简单数据类型(number、string、boolean),值就保存在变量指向的那个内存地址,因此等同于常量。
但对于复杂数据类型的(主要是对象和数组),变量指向的内存地址(栈内存),保存的只是一个指向实际数据(堆内存)的指针。const只能保证(栈内存中)这个指针是固定的(即总是指向堆内存的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,如果const声明的常量是一个对象,它所包含的值是可以被修改的。
可以简单理解为:
const 声明阻止的是 变量绑定与变量自身值的修改,而不阻止对变量成员的修改。
const person = {
name: "Nicholas"
};
// 工作正常 --修改的是变量成员
person.name = "Greg";
// 抛出错误 --修改的是变量自身值,所以报错
person = {
name: "Greg"
};
三、块级绑定新的最佳实践
在 ES6 的发展阶段,被广泛认可的变量声明方式是:默认情况下应当使用 let 而不是 var。对于多数 JS 开发者来说, let 的行为方式正是 var 本应有的方式,因此直接用 let 替代 var 更符合逻辑。在这种情况下,你应当对需要受到保护的变量使用 const 。
一种替代方案更为流行,那就是在默认情况下使用 const 、并且只在知道变量值需要被更改的情况下才使用 let 。
其理论依据是大部分变量在初始化之后都不应当被修改,因为预期外的改动是 bug 的源头之一。这种理念有着足够强大的吸引力,是值得在代码中照此进行探索实践的。
小结
- let 与 const所声明的变量,只在它们声明所在的块级作用域内有效
- let 与 const 块级声明都不会进行变量提升
- let 与 const 块级声明都禁止重复声明
- let 与 const 都存在暂时性死区:不能在变量声明位置之前访问。
即便使用的是 typeof 这样的安全运算符。由于块级绑定存在暂时性死区( TDZ ),试图在声明位置之前访问它就会导致错误。-副作用 - 另 const声明的变量必须初始化,且不得对变量重新赋值,否则报错。
块级绑定当前的最佳实践
默认情况下使用 const ,在你知道变量值需要被更改的情况下才使用 let 。
这在代码中能确保基本层次的不可变性,有助于防止某些类型的错误。
ES6 声明变量的六种方法
ES5 只有两种声明变量的方法:var命令和function命令。
ES6 添加 let和const命令 及另两种声明变量的方法:import和class命令。
所以,ES6 一共有 6 种声明变量的方法。
网友评论