1. 程式人生 > >ES6之用let,const和用var來宣告變數的區別

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語句處才開始,所以有暫時性死區。
參考文章:

原文地址:https://segmentfault.com/a/1190000017027339