这篇文章主要是探讨如何处理 ES6 中的变量和作用域。
使用 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 }
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
创建一个变量,其就有一个块作用域,也具有一个存储空间(也就是创建一个所谓的绑定)。其值仍未初始化变量 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 }
一个变量在难以接近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; } }
如果 参数有默认值 ,他们会当作一系列的 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
声明。
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 。
常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《 图解CSS3:核心技术与案例实战 》。