let命令
基本用法
ES6新增了let命令,用來聲明變量。它的用法類似於var,但是所聲明的變量,只在let命令所在的代碼塊內有效。
'use strict';
{
let a = 10;
var b = 1;
}
a // 報錯,ReferenceError: a is not defined.
b // 1
不存在變量提升
let不像var那樣會發生“變量提升”現象。所以,變量一定要在聲明后使用,否則報錯。
'use strict';
console.log(foo); // 輸出undefined
console.log(bar); // 報錯,ReferenceError: bar is not defined.
var foo = 2;
let bar = 2;
暫時性死區
只要塊級作用域內存在let命令,它所聲明的變量就“綁定”(binding)這個區域,不再受外部的影響。
ES6明確規定,如果區塊中存在let和const命令,這個區塊對這些命令聲明的變量,從一開始就形成了封閉作用域。凡是在聲明之前就使用這些變量,就會報錯。
總之,在代碼塊內,使用let命令聲明變量之前,該變量都是不可用的。這在語法上,稱為“暫時性死區”(temporal dead zone,簡稱TDZ)。
'use strict';
if (true) {
// TDZ開始
tmp = 'abc'; // 報錯,ReferenceError: tmp is not defined.
console.log(tmp); // 報錯,ReferenceError: tmp is not defined.
let tmp; // TDZ結束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
不允許重復聲明
let不允許在相同作用域內,重復聲明同一個變量。
'use strict';
// 報錯,TypeError: Duplicate declaration "a".
function test() {
let a = 10;
var a = 1;
}
// 報錯,TypeError: Duplicate declaration "a".
function test() {
let a = 10;
let a = 1;
}
因此,不能在函數內部重新聲明參數。
'use strict';
function func(arg) {
let arg; // 報錯,TypeError: Duplicate declaration "arg"
}
function func(arg) {
{
let arg; // 不報錯
}
}
塊級作用域
為什么需要塊級作用域?
ES5只有全局作用域和函數作用域,沒有塊級作用域,這帶來很多不合理的場景。
第一種場景,內層變量可能會覆蓋外層變量。
'use strict';
var tmp = new Date();
function f(){
console.log(tmp);
if (false){
var tmp = "hello world";
}
}
f() // undefined
上面代碼中,函數f執行后,輸出結果為undefined,原因在於變量提升,導致內層的tmp變量覆蓋了外層的tmp變量。
第二種場景,用來計數的循環變量泄露為全局變量。
'use strict';
var s = 'hello';
for (var i = 0; i < s.length; i++){
console.log(s[i]);
}
console.log(i); // 5
上面代碼中,變量i只用來控制循環,但是循環結束后,它並沒有消失,泄露成了全局變量。
ES6的塊級作用域
let實際上為JavaScript新增了塊級作用域。
'use strict';
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
上面的函數有兩個代碼塊,都聲明了變量n,運行后輸出5。這表示外層代碼塊不受內層代碼塊的影響。如果使用var定義變量n,最后輸出的值就是10。
允許塊級作用域的任意嵌套
'use strict';
{{{{{
let insane = 'Hello World'
}}}}}
外層作用域無法讀取內層作用域的變量
'use strict';
{{{{
{
let insane = 'Hello World'
}
console.log(insane); // 報錯,ReferenceError: insane is not defined.
}}}}
內層作用域可以定義外層作用域的同名變量
'use strict';
{{{{
let insane = 'outside';
{
let insane = 'inside';
console.log(insane); // inside
}
console.log(insane); // outside
}}}}
取代立即執行匿名函數(IIFE)
'use strict';
// IIFE寫法
(function () {
var tmp = 'hello world!';
}());
console.log(tmp); // 報錯,ReferenceError: tmp is not defined.
// 塊級作用域寫法
{
let tmp = 'hello world!';
}
console.log(tmp); // 報錯,ReferenceError: tmp is not defined.
函數本身的作用域,在其所在的塊級作用域之內
'use strict';
function f() {
console.log('outside');
}
(function () {
if(false) {
// 重復聲明一次函數f
function f() {
console.log('inside');
}
}
f();
}());
上面代碼在ES5中運行,會得到“inside”,但是在ES6中運行,會得到“outside!”。這是因為ES5存在函數提升,不管會不會進入 if代碼塊,函數聲明都會提升到當前作用域的頂部,得到執行;
而ES6支持塊級作用域,不管會不會進入if代碼塊,其內部聲明的函數皆不會影響到作用域的外部。
塊級作用域外部,無法調用塊級作用域內部定義的函數
'use strict';
let f;
{
let a = 'secret';
let b = 'publish';
f = function () {
return a;
};
function p() {
return b;
}
}
f(); // "secret"
p(); // 報錯,ReferenceError: p is not defined.
其它
ES5的嚴格模式規定,函數只能在頂層作用域和函數內聲明,其他情況(比如if代碼塊、循環代碼塊)的聲明都會報錯。
ES6由於引入了塊級作用域,這種情況可以理解成函數在塊級作用域內聲明,因此不報錯,但是構成區塊的大括號不能少,否則還是會報錯。
'use strict';
// ES5
'use strict';
if (true) {
function f() {} // 報錯
}
// ES6
// 不報錯
'use strict';
if (true) {
function f() {}
}
// 報錯,SyntaxError: Unexpected token.
'use strict';
if (true)
function f() {}
const命令
聲明只讀的常量
'use strict';
const PI = 3.1415;
PI // 3.1415
PI = 3; // 報錯,TypeError: "PI" is read-only.
聲明時必須立即初始化
'use strict';
const foo; // 報錯,SyntaxError: missing = in const declaration.
與let命令相同
- 只在聲明所在的塊級作用域內有效
- 聲明不提升
- 存在暫時性死區,只能在聲明的位置后面使用
- 不可重復聲明
只保證變量名指向的地址不變,並不保證該地址的數據不變
對於復合類型的變量,變量名不指向數據,而是指向數據所在的地址。const命令只是保證變量名指向的地址不變,並不保證該地址的數據不變,所以將一個對象聲明為常量必須非常小心。
'use strict';
const foo = {};
foo.prop = 123;
foo.prop // 123
foo = {} // TypeError: "foo" is read-only
上面代碼中,常量foo儲存的是一個地址,這個地址指向一個對象。不可變的只是這個地址,即不能把foo指向另一個地址,但對象本身是可變的,所以依然可以為其添加新屬性。
跨模塊常量
const聲明的常量只在當前代碼塊有效。如果想設置跨模塊的常量,可以采用下面的寫法。
// constants.js 模塊
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模塊
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3
// test2.js 模塊
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
其它
ES5只有兩種聲明變量的方法:var命令和function命令。ES6除了添加let和const命令,后面章節還會提到,另外兩種聲明變量的方法:import命令和class命令。所以,ES6一共有6種聲明變量的方法。
全局對象的屬性
全局對象是最頂層的對象,在瀏覽器環境指的是window對象,在Node.js指的是global對象。ES5之中,全局對象的屬性與全局變量是等價的。
window.a = 1;
a // 1
a = 2;
window.a // 2
上面代碼中,全局對象的屬性賦值與全局變量的賦值,是同一件事。(對於Node來說,這一條只對REPL環境適用,模塊環境之中,全局變量必須顯式聲明成global對象的屬性。)
這種規定被視為JavaScript語言的一大問題,因為很容易不知不覺就創建了全局變量。
ES6為了改變這一點,一方面規定,var命令和function命令聲明的全局變量,依舊是全局對象的屬性;另一方面規定,let命令、const命令、class命令聲明的全局變量,不屬於全局對象的屬性。
var a = 1;
// 如果在Node的REPL環境,可以寫成global.a
// 或者采用通用方法,寫成this.a
window.a // 1
let b = 1;
window.b // undefined
上面代碼中,全局變量a由var命令聲明,所以它是全局對象的屬性;全局變量b由let命令聲明,所以它不是全局對象的屬性,返回undefined。