ES6中的变量和作用域
这篇文章主要是探讨如何处理ES6中的变量和作用域。
通过let和const确定块作用域
使用let
和const
创建块作用域,他们声明的变量只存在块内。比如下面的示例,let
声明的变量tmp
只存在于if
语句块,也只有在if
语句块内有效。
function func () {
if (true) {
let tmp = 123;
}
console.log(tmp); // ReferenceError: tmp is not defined
}
相比之下,使用var
声明的变量,在整个函数域内都有效:
function func () {
if (true) {
var tmp = 123;
}
console.log(tmp); // 123
}
块作用域也可以存在于整个函数内:
function func () {
let foo = 5;
if (...) {
let foo = 10;
console.log(foo); // 10
}
console.log(foo); // 5
}
const创建不可变的变量(常量)
let
创建的变量是可变的:
let foo = "abc";
foo = "def";
console.log(foo); // def
而使用const
创建的变量是不可变量,其是一个常量:
const foo = "abc";
foo = "def"; // TypeError
注意:const
并不影响一个常数是否可变,如果一个常数是一个对象,那它总是一个引用对象,但仍然可以改变对象的本身(如果它是可变的)。
const obj = {};
obj.prop = 123;
console.log(obj.prop); // 123
obj = {}; // TypeError
如果你想让obj
是一个真正的常数,可以使用freeze
方法来冻结其值:
const obj = Object.freeze({});
obj.prop = 123; // TypeError
循环体内的const
一旦通过const
创建的变量它就是一个常量,它是不能被改变的。但这也并不意味着你不能重新给其设置一个新值。例如,可以通过一个循环来操作:
function logArgs (...args) {
for (let [index, elem] of args.entries()) {
const message = index + '. ' + elem;
console.log(message);
}
}
logArgs("Hello", "everyon");
输出的结果
0. Helloe
1. everyone
什么应该使用let
,什么时候应该使用const
如果你想改变一个变量保存其原始值,你就不能使用const
来声明:
const foo = 1;
foo++; // TypeError
然而,你可以使用const
声明变量,来引用可变量的值:
const bar = [];
bar.push("abc"); // array是一个可变的
我还在仔细考虑使用什么方式才是最好的方式,但是目前情况使用的都是像前面的示例,因为bar
变量是可变的。我使用const
表明变量和值是不可变的:
const EMPTY_ARRAY = Object.freeze([]);
暂时性死区
使用let
或const
声明的变量有一个所谓的暂时性死区(TDZ):当进入作用域范围,它就不能接受(got
或set
)访问,直到其被声明。
我们来来看一个有关于var
变量的生命周期,它没有暂时性死区:
- 当
var
声明了一个变量,其就有一个存储空间(创建一个所谓的绑定)。变量就初始化了,其默认值为undefined
- 当执行的范围到达声明处,变量设置为指定的值(如果有赋值的话),如果变量没有赋值,其值仍然是
undefined
通过let
声明变量存在暂时性死区,这意味着他们的生命周期如下:
- 当使用
let
创建一个变量,其就有一个块作用域,也具有一个存储空间(也就是创建一个所谓的绑定)。其值仍未初始化变量 - 获取或设置一个未初始化的变量,得到一个错误ReferenceError
- 当执行范围内到达声明的变量处,如果有赋值的话,变量的初始值为指定的初始化值。如果没有,变量的值仍为
undefined
使用const
声明的变量工作类似于let
,但它必须要有一个初始化值,而且不能被改变。
在一个TDZ内,通过if
语句秋设置或获取一个变量将会报错:
if (true) { // TDZ开始
// 未初始化tmp变量
tmp = "abc"; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束,tmp已初始化,其初始值为undefined
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
下面的例子演示了死区是时间(基于时间),而不是空间(基于位置):
if (true) { // 进入新作用域,TDZ开始
const func = function () {
console.log(myVar); // OK
}
//在TDZ内访问myVar,会引起ReferenceError错误
let myVar = 3; // TDZ结束
func (); // 调用外面的TDZ
}
typeof和TDZ
一个变量在难以接近TDZ时,这也意味着你不能使用typeof
:
if (true) {
console.log(typeof tmp); // ReferenceError
let tmp;
}
在实践中我不认为这是一个问题,因为你不能有条的通过let
声明变量范围。相反,你可以使用var
声明变量,而且可以通过window
创建一个全局变量:
if (typeof myVarVariable === 'undefined') {
// `myVarVariable` does not exist => create it
window.myVarVariable = 'abc';
}
循环头中的let
在循环中,你可以通过let
声明变量,为每次迭代重新绑定变量。比如在for
、for-in
和for-of
循环中。
看起来像下面:
let arr = [];
for (let i = 0; i < 3; i++) {
arr.push(() = > i);
}
console.log(arr.map(x => x())); // [0,1,2]
相比之下,使用var
声明的变量将在整个循环中都可以工作:
let arr = [];
for (var i = 0; i < 3; i++) {
arr.push(() => i);
}
console.log(arr.map(x => x())); // [3,3,3]
每次迭代得到一个新的绑定似乎有些奇怪,但当你使用循环创建功能(比如回调事件处理),它显得就非常有用。
参数
参数和局部变量
如果使用let
声明变量,它有一个相同的名称,称作参数。静态加载会出错:
function func (arg) {
let arg; // Uncaught SyntaxError: Identifier 'arg' has already been declared
}
同样的,将其放在一个作用块里:
function func (arg) {
{
let arg; // undefined
}
}
相比之下,使用var
声明一个和参数相同变量,其作用范围在同一个范围内:
function func (arg) {
var arg;
}
或者
function func (arg) {
{
var arg;
}
}
参数默认值和TDZ
如果参数有默认值,他们会当作一系列的let
语句,而且存在TDZ。
// OK: `y` accesses `x` after it has been declared
function foo(x=1, y=x) {
return [x, y];
}
foo(); // [1,1]
// Exception: `x` tries to access `y` within TDZ
function bar(x=y, y=2) {
return [x, y];
}
bar(); // ReferenceError
默认参数不知道其自身的范围
参数默认值的范围是独立于其自身范围。这意味着内部定义的方法或函数参数的默认值不知道其内部的局部变量:
let foo = 'outer';
function bar(func = x => foo) {
let foo = 'inner';
console.log(func()); // outer
}
bar();
全局对象
JavaScript全局对象(浏览器中的window,Node.js中的global)存在的问题比他的特性多,尤其是性能。这也是为什么ES6中不引用的原因。
全局对象的属性都是全局变量,在全局作用域下都有效,可以通过var
或function
方式声明。
但现在全局变量也不是全局对象。在全局作用域下,可以通过let
、const
或者class
声明。
函数声明和class声明
function
声明:
- 像
let
一样,声明的是一个块作用域 - 像
var
一样,在全局对象创建属性(全局作用域) - 存在生命提升:独立于一个函数声明中提到它的范围,它总是存在于开始时创建的范围内
下面的代码演示了函数声明的提升:
{ // Enter a new scope
console.log(foo()); // OK, due to hoisting
function foo() {
return 'hello';
}
}
类声明:
- 是一个块作用域
- 不能在全局对象上创建属性
- 不存在生命提升
class
不存在生命提升可能令人惊讶,那是因为其存在于引擎下,而不是一个函数。这种行为的理由是,他们扩展表达式。这些表达式在适当的时间内被执行。
{ // Enter a new scope
const identity = x => x;
// Here we are in the temporal dead zone of `MyClass`
let inst = new MyClass(); // ReferenceError
// Note the expression in the `extends` clause
class MyClass extends identity(Object) {
}
}
本文根据@Dr. Axel Rauschmayer的《Variables and scoping in ECMAScript 6》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://www.2ality.com/2015/02/es6-scoping.html。
如需转载,烦请注明出处:https://www.fedev.cn/javascript/variables-and-scoping-in-es6.htmlKobe 11 Mentality