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深入理解---let存在變量提升嗎?
- 我用了兩個月的時間才理解 let
看完這些文章,最後總結到:
- 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語句處才開始,所以有暫時性死區。
參考文章:
- ECMAScript 6 入門
- let深入理解---let存在變量提升嗎?
原文地址:https://segmentfault.com/a/1190000017027339
ES6之用let,const和用var來聲明變量的區別