1. 程式人生 > >ES6之用let,const和用var來聲明變量的區別

ES6之用let,const和用var來聲明變量的區別

++ .com www console 引擎 變量提升 相同 隱藏 3.1

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來聲明變量的區別