ES6之用let,const和用var來宣告變數的區別
var(掌握)
不區分變數和常量
用var宣告的變數都是變數,都是可變的,我們可以隨便對它進行運算操作。這樣當多個人進行同一個專案時,區分變數和常量會越來越難,一不小心就會把設計為常量的資料更改了。
允許重新宣告
在相同作用域下用var宣告的一個變數,當再次宣告時,程式不會報錯,並且會把該變數重新賦值。
存在變數提升
變數在宣告它們的指令碼或函式中都是有定義的,變數宣告語句會被“提前”至指令碼或者函式的頂部。但是初始化的操作則還在原來var語句的位置執行,在宣告語句之前變數的值是undefined。
需要注意的是,var語句同樣可以作為for迴圈或者for/in迴圈的組成部分(和在迴圈之外宣告的變數宣告一樣,這裡宣告的變數也會“提前”)。
console.log("a:",a);//undefined
var a = 2;
function b() {
console.log("c:",c);//undefined
var c = 3;
console.log("c:",c);//3
}
console.log("a:",a);//2
沒有塊級作用域
只有全域性作用域和函式作用域,所有函式外的全域性變數在程式的任何地方都是可見的,函式內部的變數只在函式內部可見,函式外無法訪問該變數。
是頂層物件的屬性
JavaScript全域性變數是全域性物件的屬性,這是在ECMAScript規範中強制規定的。對於區域性變數則沒有如此規定,但我們可以想象得到,區域性變數當做跟函式呼叫相關的某個物件的屬性。ECMAScript 5規範稱為“宣告上下文物件”(declarative environment record)。
let宣告變數(掌握)
不存在變數提升
let命令改變了語法行為,它所宣告的變數一定要在聲明後使用,否則報錯。
console.log(bar); // 報錯ReferenceError
let bar = 2;
但是真的不存在變數提升嗎?請看這些篇文章:
看完這些文章,最後總結到:
- let 的「建立」過程被提升了,但是初始化沒有提升。
- var 的「建立」和「初始化」都被提升了。
- function 的「建立」「初始化」和「賦值」都被提升了。
暫時性死區
ES6 明確規定,如果區塊中存在let和const命令,這個區塊對這些命令宣告的變數,從一開始就形成了封閉作用域。凡是在宣告之前就使用這些變數,就會報錯。
總之,在程式碼塊內,使用let命令宣告變數之前,該變數都是不可用的。這在語法上,稱為“暫時性死區”(temporal dead zone,簡稱 TDZ)。
從塊頂部到該變數的初始化語句,這塊區域叫做 TDZ(臨時死區)
if (true) {
// TDZ開始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ結束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
所謂暫時死區,就是不能在初始化之前,使用變數。
不允許重複宣告
let不允許在相同作用域內,重複宣告同一個變數。
// 報錯
function func() {
let a = 10;
var a = 1;
}
// 報錯
function func() {
let a = 10;
let a = 1;
}
function func(arg) {
let arg; // 報錯
}
function func(arg) {
{
let arg; // 不報錯
}
}
塊作用域
它的用法類似於var,但是所宣告的變數,只在let命令所在的程式碼塊內有效。{}包裹就是一個作用域,用let宣告的變數在{}中可見,在{}外面不可見。
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
for迴圈的計數器,就很合適使用let命令
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i);
// ReferenceError: i is not defined
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
for迴圈還有一個特別之處,就是設定迴圈變數的那部分是一個父作用域,而迴圈體內部是一個單獨的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
/*
for( let i = 0; i< 5; i++) 這句話的圓括號之間,有一個隱藏的作用域
for( let i = 0; i< 5; i++) { 迴圈體 } 在每次執行迴圈體之前,JS 引擎會把 i 在迴圈體的上下文中重新宣告及初始化一次。
其他細節就不說了,太細碎了*/
ES6 允許塊級作用域的任意巢狀
{{{{
{let insane = 'Hello World'}
console.log(insane); // 報錯
}}}};
內層作用
{{{{
let insane = 'Hello World';
{let insane = 'Hello World'}
}}}};
最後導致IIFE不再必要了
// IIFE 寫法
(function () {
var tmp = ...;
...
}());
// 塊級作用域寫法
{
let tmp = ...;
...
}
擴充套件:塊級作用域與函式宣告
ES5 規定,函式只能在頂層作用域和函式作用域之中宣告,不能在塊級作用域宣告。但是,瀏覽器沒有遵守這個規定,為了相容以前的舊程式碼,還是支援在塊級作用域之中宣告函式,因此實際都能執行,不會報錯。
ES6 引入了塊級作用域,明確允許在塊級作用域之中宣告函式。ES6 規定,塊級作用域之中,函式宣告語句的行為類似於let,在塊級作用域之外不可引用。
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重複宣告一次函式f
function f() { console.log('I am inside!'); }
}
f();
}());//ES5 環境會得到“I am inside!”
//ES6 環境會得到“I am outside!”
為了減輕因此產生的不相容問題,ES6 在附錄 B裡面規定,瀏覽器的實現可以不遵守上面的規定,有自己的行為方式。
- 允許在塊級作用域內宣告函式。
- 函式宣告類似於var,即會提升到全域性作用域或函式作用域的頭部。
- 同時,函式宣告還會提升到所在的塊級作用域的頭部。
允許在塊級作用域內宣告函式。
函式宣告類似於var,即會提升到全域性作用域或函式作用域的頭部。
同時,函式宣告還會提升到所在的塊級作用域的頭部。上面的程式碼在符合 ES6 的瀏覽器中,都會報錯,因為實際執行的是下面的程式碼。
// 瀏覽器的 ES6 環境
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
考慮到環境導致的行為差異太大,應該避免在塊級作用域內宣告函式。如果確實需要,也應該寫成函式表示式,而不是函式宣告語句。
不是頂層物件的屬性
頂層物件的屬性與全域性變數掛鉤,被認為是 JavaScript 語言最大的設計敗筆之一。這樣的設計帶來了幾個很大的問題,首先是沒法在編譯時就報出變數未宣告的錯誤,只有執行時才能知道(因為全域性變數可能是頂層物件的屬性創造的,而屬性的創造是動態的);其次,程式設計師很容易不知不覺地就建立了全域性變數(比如打字出錯);最後,頂層物件的屬性是到處可以讀寫的,這非常不利於模組化程式設計。另一方面,window物件有實體含義,指的是瀏覽器的視窗物件,頂層物件是一個有實體含義的物件,也是不合適的。
用let命令宣告的全域性變數,不屬於頂層物件的屬性。
const宣告常量(掌握)
值不能改變
const宣告一個只讀的常量。一旦宣告,常量的值就不能改變。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
const宣告的變數不得改變值,這意味著,const一旦宣告變數,就必須立即初始化,不能留到以後賦值。
const foo;
// SyntaxError: Missing initializer in const declaration
本質
const實際上保證的,並不是變數的值不得改動,而是變數指向的那個記憶體地址所儲存的資料不得改動。對於簡單型別的資料(數值、字串、布林值),值就儲存在變數指向的那個記憶體地址,因此等同於常量。但對於複合型別的資料(主要是物件和陣列),變數指向的記憶體地址,儲存的只是一個指向實際資料的指標,const只能保證這個指標是固定的(即總是指向另一個固定的地址),至於它指向的資料結構是不是可變的,就完全不能控制了。因此,將一個物件宣告為常量必須非常小心。
const foo = {};
// 為 foo 新增一個屬性,可以成功
foo.prop = 123;
foo.prop // 123
// 將 foo 指向另一個物件,就會報錯
foo = {}; // TypeError: "foo" is read-only
上面程式碼中,常量foo儲存的是一個地址,這個地址指向一個物件。不可變的只是這個地址,即不能把foo指向另一個地址,但物件本身是可變的,所以依然可以為其新增新屬性。
下面是一個將物件徹底凍結的函式
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
// 常規模式時,下面一行不起作用;
// 嚴格模式時,該行會報錯
不是頂層物件的屬性(同let)
塊級作用域(同let)
重複宣告(同let)
不存在變數提升(同let)
暫時性死區(同let)
其實 const 和 let 只有一個區別,那就是 const 只有「建立」和「初始化」,沒有「賦值」過程。
在同一作用域中const的建立被提升了,初始化在const語句處才開始,所以有暫時性死區。
參考文章: