黄子涵

第2章 let和const命令

2.1 let命令

2.1.1 基本用法

ES6 新增了 let 命令,用于声明变量。其用法类似于 var,但是所声明的变量只在 let 命令所在的代码块内有效。

{
let hzh1 = 1;
var hzh2 = 2;
}
console.log("hzh2 = " + hzh2);
console.log("hzh1 = " + hzh1);
[Running] node "e:\HMV\Babel\hzh.js"
hzh2 = 2
e:\HMV\Babel\hzh.js:7
console.log("hzh1 = " + hzh1);
^
ReferenceError: hzh1 is not defined
at Object.<anonymous> (e:\HMV\Babel\hzh.js:7:25)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
[Done] exited with code=1 in 0.264 seconds

上面的代码在代码块中分别用 let 和 var 声明了两个变量。然后在代码块之外调用这两个变量,结果let 声明的变量报错,var 声明的变量返回了正确的值。这表明,let 声明的变量只在其所在代码块内有效。

for循环的计数器就很适合使用let命令。

for(let hzh = 1; hzh < 0; hzh++) {
// ...
}
console.log(hzh);
[Running] node "e:\HMV\Babel\hzh.js"
e:\HMV\Babel\hzh.js:5
console.log(hzh);
^
ReferenceError: hzh is not defined
at Object.<anonymous> (e:\HMV\Babel\hzh.js:5:13)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
[Done] exited with code=1 in 0.178 seconds

以上代码中的计数器 hzh 只在 for 循环体内有效,在循环体外引用就会报错。

下面的代码如果使用 var,最后将输出10。

// hzh.js
var hzh1 = [];
for (var hzh2 = 0; hzh2 < 10; hzh2++) {
hzh1[hzh2] = function () {
console.log("hzh2 = " + hzh2);
};
console.log("第 " + hzh2 + " 轮输出时:" + "hzh2 = " + hzh2);
}
console.log("调用hzh1[0]():");
hzh1[0]();
console.log("调用hzh1[1]():");
hzh1[1]();
console.log("调用hzh1[2]():");
hzh1[2]();
console.log("调用hzh1[3]():");
hzh1[3]();
console.log("调用hzh1[4]():");
hzh1[4]();
console.log("调用hzh1[5]():");
hzh1[5]();
console.log("调用hzh1[6]():");
hzh1[6]();
console.log("调用hzh1[7]():");
hzh1[7]();
console.log("调用hzh1[8]():");
hzh1[8]();
console.log("调用hzh1[9]():");
hzh1[9]();
[Running] node "e:\HMV\Babel\hzh.js"
0 轮输出时:hzh2 = 0
1 轮输出时:hzh2 = 1
2 轮输出时:hzh2 = 2
3 轮输出时:hzh2 = 3
4 轮输出时:hzh2 = 4
5 轮输出时:hzh2 = 5
6 轮输出时:hzh2 = 6
7 轮输出时:hzh2 = 7
8 轮输出时:hzh2 = 8
9 轮输出时:hzh2 = 9
调用hzh1[0]():
hzh2 = 10
调用hzh1[1]():
hzh2 = 10
调用hzh1[2]():
hzh2 = 10
调用hzh1[3]():
hzh2 = 10
调用hzh1[4]():
hzh2 = 10
调用hzh1[5]():
hzh2 = 10
调用hzh1[6]():
hzh2 = 10
调用hzh1[7]():
hzh2 = 10
调用hzh1[8]():
hzh2 = 10
调用hzh1[9]():
hzh2 = 10
[Done] exited with code=0 in 0.191 seconds

上面的代码中,变量 hzh2 是 var 声明的,在全局范围内都有效,所以全局只有一个变量 hzh2 。每一次循环,变量 hzh2 的值都会发生改变,而循环内,被赋给数组 hzh1 的函数内部的 console.log(hzh2) 中的 hzh2 指向全局的 hzh2 。也就是说,所有数组 hzh1 的成员中的 hzh2 指向的都是同一个 hzh2 ,导致运行时输出的是最后一轮的 hzh2 值,也就是 10 。

如果使用 let,声明的变量仅在块级作用域内有效,最后将输出 6 。

// hzh.js
var hzh1 = [];
for (let hzh2 = 0; hzh2 < 10; hzh2++) {
hzh1[hzh2] = function () {
console.log("hzh2 = " + hzh2);
};
console.log("第 " + hzh2 + " 轮输出时:" + "hzh2 = " + hzh2);
}
console.log("调用hzh1[0]():");
hzh1[0]();
console.log("调用hzh1[1]():");
hzh1[1]();
console.log("调用hzh1[2]():");
hzh1[2]();
console.log("调用hzh1[3]():");
hzh1[3]();
console.log("调用hzh1[4]():");
hzh1[4]();
console.log("调用hzh1[5]():");
hzh1[5]();
console.log("调用hzh1[6]():"); // 这个数书上的输出
hzh1[6]();
console.log("调用hzh1[7]():");
hzh1[7]();
console.log("调用hzh1[8]():");
hzh1[8]();
console.log("调用hzh1[9]():");
hzh1[9]();
[Running] node "e:\HMV\Babel\hzh.js"
0 轮输出时:hzh2 = 0
1 轮输出时:hzh2 = 1
2 轮输出时:hzh2 = 2
3 轮输出时:hzh2 = 3
4 轮输出时:hzh2 = 4
5 轮输出时:hzh2 = 5
6 轮输出时:hzh2 = 6
7 轮输出时:hzh2 = 7
8 轮输出时:hzh2 = 8
9 轮输出时:hzh2 = 9
调用hzh1[0]():
hzh2 = 0
调用hzh1[1]():
hzh2 = 1
调用hzh1[2]():
hzh2 = 2
调用hzh1[3]():
hzh2 = 3
调用hzh1[4]():
hzh2 = 4
调用hzh1[5]():
hzh2 = 5
调用hzh1[6]():
hzh2 = 6
调用hzh1[7]():
hzh2 = 7
调用hzh1[8]():
hzh2 = 8
调用hzh1[9]():
hzh2 = 9
[Done] exited with code=0 in 0.172 seconds

上面的代码中,变量 hzh2 是 let 声明的,当前的 hzh2 只在本轮循环有效。所以每一次循环的 hzh2 其实都是一个新的变量,于是最后输出的是 6 。大家可能会问,如果每一轮循环的变量 i 都是重新声明的,那它怎么知道上一轮循环的值从而计算出本轮循环的值呢?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量 hzh2 时,就在上一轮循环的基础上进行计算。

另外,for 循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

// hzh.js
console.log("看看是输出数字还是hzh?")
for (let hzh = 0; hzh < 3; hzh++) {
let hzh = 'hzh';
console.log(hzh);
}
[Running] node "e:\HMV\Babel\hzh.js"
看看是输出数字还是hzh?
hzh
hzh
hzh
[Done] exited with code=0 in 0.173 seconds

正确运行以上代码将输出3次 hzh。这表明函数内部的变量 hzh 与循环变量 hzh 不在同一个作用域,而是有各自单独的作用域。

2.1.2 不存在变量提升

var 命令会发生“变量提升”现象,即变量可以在声明之前使用,值为 undefined 。这种现象多少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。

为了纠正这种现象,let 命令改变了语法行为,它所声明的变量一定要在声明后使用,否则便会报错。

// hzh.js
// var 的情况
console.log("hzh1 = " + hzh1); // 输出 undefined
var hzh1 = 2;
console.log("");
// let 的情况
console.log("hzh2 = " + hzh2); // 报错 ReferenceError
let hzh2 = 2;
[Running] node "e:\HMV\Babel\hzh.js"
hzh1 = undefined
e:\HMV\Babel\hzh.js:8
console.log("hzh2 = " + hzh2); // 报错 ReferenceError
^
ReferenceError: Cannot access 'hzh2' before initialization
at Object.<anonymous> (e:\HMV\Babel\hzh.js:8:25)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
[Done] exited with code=1 in 0.174 seconds

在以上代码中,变量 hzh1 用 var 命令声明会发生变量提升,即脚本开始运行时,变量 hzh1 便已经存在,但是没有值,所以会输出 undefined 。变量 hzh2 用 let 命令声明则不会发生变量提升。这表示在声明它之前,变量 hzh2 是不存在的,这时如果用到它,就会抛出一个错误。

2.1.3 暂时性死区

只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

// hzh.js
var hzh = 123;
if(true) {
hzh = 'huangzihan'; // ReferenceError
let hzh;
}
[Running] node "e:\HMV\Babel\hzh.js"
e:\HMV\Babel\hzh.js:6
hzh = 'huangzihan'; // ReferenceError
^
ReferenceError: Cannot access 'hzh' before initialization
at Object.<anonymous> (e:\HMV\Babel\hzh.js:6:9)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
[Done] exited with code=1 in 0.197 seconds

上面的代码中存在全局变量 hzh,但是块级作用域内 let 又声明了一个局部变量 hzh,导致后者绑定这个块级作用域,所以在 let 声明变量前,对 hzh 赋值会报错。

ES6 明确规定,如果区块中存在 let 和 const 命令,则这个区块对这些命令声明的变量从一开始就形成封闭作用域。只要在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上称为“暂时性死区”(temporal dead zone,简称TDZ)。

// hzh.js
if(true) {
// TDZ 开始
hzh = 'huangzihan'; // ReferenceError
console.log("hzh = " + hzh);
let hzh; // TDZ 结束
console.log("hzh = " + hzh); // undefined
hzh = 123;
console.log("hzh = " + hzh); //123
}
[Running] node "e:\HMV\Babel\hzh.js"
e:\HMV\Babel\hzh.js:5
hzh = 'huangzihan'; // ReferenceError
^
ReferenceError: Cannot access 'hzh' before initialization
at Object.<anonymous> (e:\HMV\Babel\hzh.js:5:9)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
[Done] exited with code=1 in 0.172 seconds

上面的代码中,在 let 命令声明变量 hzh 之前,都属于变量 hzh 的“死区”。

“暂时性死区”也意味着 typeof 不再是一个百分百安全的操作。

上面的代码中,变量x使用let命令声明,所以在声明之前都属于
x的“死区”,只要用到该变量就会报错。因此,typeof运行时就
会抛出一个ReferenceError。
作为比较,如果一个变量根本没有被声明,使用typeof反而不会
报错。

上面的代码中,undeclared_variable是一个不存在的变量名,结
果返回“undefined”。所以,在没有let之前,typeof运算符是百分
之百安全的,永远不会报错。现在这一点不成立了。这样的设
计是为了让大家养成良好的编程习惯,变量一定要在声明之后
使用,否则就会报错。
有些“死区”比较隐蔽,不太容易发现。

上面的代码中,调用bar函数之所以报错(某些实现可能不报错
),是因为参数x的默认值等于另一个参数y,而此时y还没有声
明,属于“死区”。如果y的默认值是x,就不会报错,因为此时x
已声明。

另外,下面的代码也会报错,与var的行为不同。

以上代码报错也是因为暂时性死区。使用let声明变量时,只要
变量在还没有声明前使用,就会报错。以上示例就属于这种情
况,在变量x的声明语句还没有执行完成前就尝试获取x的值,
导致出现“x未定义”的错误。
ES6规定暂时性死区和let、const语句不出现变量提升,主要是
为了减少运行时错误,防止在变量声明前就使用这个变量,从
而导致意料之外的行为。这样的错误在ES5中是很常见的,现
在有了这种规定,避免此类错误就很容易了。
总之,暂时性死区的本质就是,只要进入当前作用域,所要使
用的变量就已经存在,但是不可获取,只有等到声明变量的那
一行代码出现,才可以获取和使用该变量。

2.1.4 不允许重复声明

let不允许在相同作用域内重复声明同一个变量。

因此,不能在函数内部重新声明参数。

2.2 块级作用域

2.2.1 为什么需要块级作用域

ES5只有全局作用域和函数作用域,没有块级作用域,这导致
很多场景不合理。
第一种场景,内层变量可能会覆盖外层变量

以上代码的原意是,if代码块的外部使用外层的tmp变量,内部
使用内层的tmp变量。但是,函数f执行后,输出结果为
undefined,原因在于变量提升导致内层的tmp变量覆盖了外层的
tmp变量。
第二种场景,用来计数的循环变量泄露为全局变量

上面的代码中,变量i只用来控制循环,但是循环结束后,它并
没有消失,而是泄露成了全局变量

2.2.2 ES6的块级作用域

let实际上为JavaScript新增了块级作用域。

上面的函数有两个代码块,都声明了变量 n,运行后输出 5。
这表示外层代码块不受内层代码块的影响。如果使用var定义变
量n,最后输出的值就是10。
ES6允许块级作用域的任意嵌套。

上面的代码使用了一个5层的块级作用域。外层作用域无法读
取内层作用域的变量。
内层作用域可以定义外层作用域的同名变量。

块级作用域的出现,实际上使得获得广泛应用的立即执行匿名
函数(IIFE)不再必要了。

2.2.3 块级作用域与函数声明

函数能不能在块级作用域之中声明?这是一个相当令人困惑的
问题。
ES5规定,函数只能在顶层作用域和函数作用域之中声明,不
能在块级作用域声明。

上面两种函数声明在ES5中都是非法的。
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还
是支持在块级作用域之中声明函数,因此上面两种情况实际上
都能运行,并不会报错。
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数
。ES6 规定,在块级作用域之中,函数声明语句的行为类似于
let,在块级作用域之外不可引用。

以上代码在ES5中运行会得到 I am inside!,因为在if内声明的
函数f会被提升到函数头部,实际运行的代码如下。

而在ES6中运行就完全不一样了,理论上会得到 I am
outside!。因为块级作用域内声明的函数类似于let,对作用域
之外没有影响。但是,如果真的在ES6浏览器中运行上面的代
码,是会报错的,这是为什么呢?
原来,如果改变了块级作用域内声明的函数的处理规则,显然
会对旧代码产生很大影响。为了减轻因此产生的不兼容问题
,ES6 在附录
B(www.ecma-international.org/ecma-262/6.0/index.html#sec-blo
ck-level-function-declarations-web-legacy-compatibility-semantics)
中规定,浏览器的实现可以不遵守上面的规定,而有自己的行
为方式
(stackoverflow.com/questions/31419897/what-are-the-precise-sem
antics-of-block-level-functions-in-es6),具体如下。
· 允许在块级作用域内声明函数。
· 函数声明类似于var,即会提升到全局作用域或函数作用域的
头部。
· 同时,函数声明还会提升到所在的块级作用域的头部。
注意!
上面3条规则只对ES6的浏览器实现有效,其他环境的实现不用
遵守,仍旧将块级作用域的函数声明当作let处理即可。
根据这3条规则,在浏览器的ES6环境中,块级作用域内声明函
数的行为类似于var声明变量。

上面的代码在符合ES6的浏览器中都会报错,因为实际运行的
是以下代码。

考虑到环境导致的行为差异太大,应该避免在块级作用域内声
明函数。如果确实需要,也应该写成函数表达式的形式,而不
是函数声明语句。

另外,还有一个需要注意的地方。ES6 的块级作用域允许声明
函数的规则只在使用大括号的情况下成立,如果没有使用大括
号,就会报错。

2.2.4 do表达式

本质上,块级作用域是一个语句,将多个操作封装在一起,没
有返回值。

上面的代码中,块级作用域将两个语句封装在一起。但是,在
块级作用域以外,没有办法得到t的值,因为块级作用域不返回
值,除非t是全局变量。
现在有一个提案
(wiki.ecmascript.org/doku.php?id=strawman:do
expressions),使得块级作用域可以变为表达式,即可以返回
值,办法就是在块级作用域之前加上do,使它变为do表达式。

上面的代码中,变量x会得到整个块级作用域的返回值。

2.3 const命令

const声明一个只读的常量。一旦声明,常量的值就不能改变。
上面的代码表明改变常量的值会报错。
const声明的常量不得改变值。这意味着,const一旦声明常量,
就必须立即初始化,不能留到以后赋值。
上面的代码表示,对于const而言,只声明不赋值就会报错。
const的作用域与let命令相同:只在声明所在的块级作用域内有
效。

2.3.1 基本用法

上面的代码在常量MAX声明之前就被调用,结果报错。
使用const声明常量也与let一样,不可重复声明。

2.3.2 本质

const实际上保证的并不是变量的值不得改动,而是变量指向的
那个内存地址不得改动。对于简单类型的数据(数值、字符串
、布尔值)而言,值就保存在变量指向的内存地址中,因此等
同于常量。但对于复合类型的数据(主要是对象和数组)而言
,变量指向的内存地址保存的只是一个指针,const只能保证这
个指针是固定的,至于它指向的数据结构是不是可变的,这完
全不能控制。因此,将一个对象声明为常量时必须非常小心

上面的代码中,常量foo储存的是一个地址,这个地址指向一个
对象。不可变的只是这个地址,即不能把foo指向另一个地址,
但对象本身是可变的,所以依然可以为其添加新属性。
来看另一个例子。

上面的代码中,常量a是一个数组,这个数组本身是可写的,
但是如果将另一个数组赋值给a,就会报错。
如果真的想将对象冻结,应该使用Object.freeze方法。
上面的代码中,常量foo指向一个冻结的对象,所以添加新属性
时不起作用,严格模式时还会报错。
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将
对象彻底冻结的函数。

2.3.3 ES6声明变量的6种方法

ES5只有两种声明变量的方法:使用var命令和function命令。
ES6除了添加了let和const命令,后面的章节中还会介绍另外两
种声明变量的方法:使用import命令和class命令。所以,ES6一
共有6种声明变量的方法。

2.4 顶层对象的属性

顶层对象在浏览器环境中指的是window对象,在Node环境中指
的是global对象。在ES5中,顶层对象的属性与全局变量是等价
的。

上面的代码中,顶层对象的属性赋值与全局变量的赋值是同一
件事。
顶层对象的属性与全局变量相关,被认为是 JavaScript 语言中
最大的设计败笔之一。这样的设计带来了几个很大的问题:首
先,无法在编译时就提示变量未声明的错误,只有运行时才能
知道(因为全局变量可能是顶层对象的属性创造的,而属性的
创造是动态的);其次,程序员很容易不知不觉地就创建全局
变量(比如打字出错);最后,顶层对象的属性是到处都可以
读写的,这非常不利于模块化编程。另一方面,window 对象
有实体含义,指的是浏览器的窗口对象,这样也是不合适的。
ES6为了改变这一点,一方面规定,为了保持兼容性,var命令
和function命令声明的全局变量依旧是顶层对象的属性;另一方
面规定,let命令、const命令、class命令声明的全局变量不属于
顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与
顶层对象的属性隔离

2.5 global对象

ES5的顶层对象本身也是一个问题,因为它在各种实现中是不
统一的。
· 在浏览器中,顶层对象是window,但Node和Web Worker没有
window。
· 在浏览器和Web Worker中,self也指向顶层对象,但是Node
没有self。
· 在Node中,顶层对象是global,但其他环境都不支持。
同一段代码为了能够在各种环境中都取到顶层对象,目前一般
是使用 this 变量,但是也有局限性。
· 在全局环境中,this会返回顶层对象。但是,在Node模块和
ES6模块中,this返回的是当前模块。
· 对于函数中的this,如果函数不是作为对象的方法运行,而是
单纯作为函数运行,this会指向顶层对象。但是,严格模式下
,this会返回undefined。
· 不管是严格模式,还是普通模式,new Function(′return
this′)()总会返回全局对象。但是,如果浏览器用了
CSP(Content Security Policy,内容安全政策),那么eval、new
Function这些方法都可能无法使用。
综上所述,很难找到一种方法可以在所有情况下都取到顶层对
象。以下是两种勉强可以使用的方法

现在有一个提案(github.com/tc39/proposal-global),在语言标
准的层面引入 global 作为顶层对象。也就是说,在所有环境下
,global都是存在的,都可以拿到顶层对象。
垫片库 system.global(github.com/ljharb/System.global)模拟了
这个提案,可以在所有环境下拿到global。

上面的代码可以保证,在各种环境中global对象都是存在的。
上面的代码将顶层对象放入变量global中。

posted @   黄子涵  阅读(36)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示