ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
第一种场景,内层变量可能会覆盖外层变量。
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined 上面这段代码中,console.log(tmp)和var tmp = 'hello world 都处于同一个作用域,即函数f创造的作用域。由于发生了变量提升,导致执行结果为undefined。
第二种场景,用来计数的循环变量泄露为全局变量。这种场景即下面for循环的例子。
ES6 的块级作用域
let实际上为 JavaScript 新增了块级作用域。
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
上面的函数有两个代码块,都声明了变量n,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值才是 10。
———————————— 下面就来看看let是怎么使用的吧! ————————————
let 基本用法:用来声明变量,但是声明的变量只能在块级作用域中使用。
🌰 栗子:
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1上面的代码中,声明a,b两个变量。但let声明的变量报错,var声明的变量返回了正确结果,说明let声明的变量只能在块级作用域中使用。
最常见到的场景:for循环的计数器就很适合用let声明
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); 上面的代码中i是由var声明的,在全局都有效,因此全局中只有一个i变量。每次循环都会改变全局i的值。因此,循环结束之后,全局的i值变成10 。这时,无论执行数组哪一项所对应的函数,取到的都是全局的i值。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6使用let声明计数器i后,每次循环中的i都具有块级作用域,即本次循环。因此,当循环结束之后,数组a每一项对应的i都是不同的,所以可以打印出期望的结果。
你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。但是在日常书写中,不建议使用这样容易引发冲突的命名方式,命名要有唯一性且尽量具有语意。
——————————————— 这是一条美丽的分割线 ———————————————
let不存在变量提升
var声明的变量会产生”变量提升“,即变量可以在声明前使用,值为undefined,可以想象成在使用未定义的变量时,变量声明语句“自动提升至”代码最上方,由于未真正声明赋值,所以值为undefined。
为了纠正这种看起来有些奇怪的现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;——————————————— 这是一条美丽的分割线 ———————————————
暂时性死区 (先定义再使用)
只要块级作用域出现了let声明,那么这个他所声明的变量就绑定了这个区域,不再受外部变量影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}在上面的代码中,存在全局tmp变量声明,但是又因为let声明了块级作用域,导致提前赋值报错。
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。
typeof x; // ReferenceError
let x; 上面代码中,变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError。
作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。
typeof undeclared_variable // "undefined" 上面代码中,undeclared_variable是一个不存在的变量名,结果返回“undefined”。
所以希望大家养成良好的编程习惯,任何变量先定义再使用。
——————————————— 这是一条美丽的分割线 ———————————————
不允许重复声明
let不允许在相同作用域内,重复声明同一个变量。
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
因此,不能在函数内部重新声明参数。
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错 ——————————————— 这是一条美丽的分割线 ———————————————
除了let外,es6新增了另一个命名关键字:const
const基本用法:声明一个不可以改变的常量。
const foo;
// SyntaxError: Missing initializer in const declaration由于const不可以重复声明,因此需要声明的同时赋值。
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}const与let一样也存在暂时性死区,要求先声明再使用。
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not definedconst一样也存在块级作用域,只在当前声明的作用域内有效。
var message = "Hello!";
let age = 25;
// 以下两行都会报错
const message = "Goodbye!";
const age = 30;const声明的变量,也与let一样不可重复声明。
🌟 ❓ 那么,是不是“所有”由const声明的变量都是不可变的呢?上个 🌰 栗子:
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only再来一个:
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 从上面两个例子中看到,分别对由const声明的对象和数组进行操作时,都是没有报错的,这是为什么呢?
const本质
const只能保证创建时变量指向的内存地址不变。对于简单数据类型,内存地址存放的就是这个值,所以不能改变。而对于复杂数据类型,内存地址存放的是一个指针,所以改变对象或数组的数据结构并不报错。