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) {
}
}
來自: http://www.w3cplus.com/javascript/variables-and-scoping-in-es6.html