ES6let命令和塊級作用域和const命令

佀無極發表於2018-09-05

2018年3月7日

ES6 let命令和塊級作用域和const命令

一、let命令
1.ES6 新增了let命令,用來宣告變數。它的用法類似於var,但是所宣告的變數,只在let命令所在的程式碼塊內有效。

{
  let a = 10;
  var b = 1;
}
console.log(b);//1
console.log(a);//Uncaught ReferenceError: a is not defined

上面程式碼let宣告的變數只在它所在的程式碼塊有效。

2.計數器i只在for迴圈體內有效,for迴圈設定迴圈變數的那部分是一個父作用域,迴圈體內部是一個單獨的子作用域。

for (let i = 0; i < 10; i++) {
   //...
}
console.log(i);//Uncaught ReferenceError: i is not defined

上面程式碼中,計數器i只在for迴圈體內有效,在迴圈體外引用就會報錯。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6]();//10

上面程式碼中,變i量是var命令宣告的,i是全域性變數。每一次迴圈,變數i的值都會發生改變,而迴圈內被賦給陣列a的函式內部的console.log(i)的i指向全域性的i。導致執行時輸出的是最後一輪的i的值,也就是 10。

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6]();//6

上面程式碼中,變數i是let宣告的,當前的i只在本輪迴圈有效,所以每一次迴圈的i其實都是一個新的變數,所以最後輸出的是6。JavaScript 引擎內部會記住上一輪迴圈的值,初始化本輪的變數i時,就在上一輪迴圈的基礎上進行計算。

for (let i = 0; i < 3; i++) {
  let i = `hello`;
  console.log(i);
}
//hello
//hello
//hello

上面程式碼中,輸出3次hello。函式內部的變數i與迴圈變數i不在同一個作用域,有各自單獨的作用域。for迴圈有一個特別之處,設定迴圈變數的那部分是一個父作用域,迴圈體內部是一個單獨的子作用域。

3.不存在變數提升。let命令改變了語法行為,它所宣告的變數一定要在宣告後使用,否則報錯。

console.log(a);//undefined
var a = 1;
console.log(b);//Uncaught ReferenceError: b is not defined
let b = 1;

上面程式碼中,變數a用var命令宣告,會發生變數提升,指令碼開始執行時,變數a已經存在了,但沒有值,所以會輸出undefined。變數b用let命令宣告,不會發生變數提升。在宣告它之前,變數b是不存在的,這時就會丟擲一個錯誤。

4.暫時性死區。暫時性死區和let、const語句不出現變數提升,主要是為了減少執行時錯誤,防止在變數宣告前就使用這個變數,從而導致意料之外的行為。只要一進入當前作用域,所要使用的變數就已經存在了,但是不可獲取,只有宣告變數後,才可以獲取和使用該變數。

var a = 123;
if (true) {
  a = `hello`; // ReferenceError
  let a;
}

上面程式碼中,全域性變數a已經存在,在當前作用域中宣告之後才可以使用。

typeof x; // ReferenceError
let x;

上面程式碼中,在宣告之前typeof執行時丟擲一個ReferenceError。

typeof a;//undefined

如上,變數a沒有被宣告,typeof不報錯,“暫時性死區”意味著typeof不再是百分之百安全的操作。

function a(x = y, y = 2) {
  return [x, y];
}
a();//Uncaught ReferenceError: y is not defined

如上,引數x預設值等於另一個引數y,而此時y還沒有宣告,屬於“死區”,這裡是隱式宣告容易犯錯。

function a(x = 2, y = x) {
  return [x, y];
}
a();//[2, 2]

如上隱式宣告,y的預設值是x,就不會報錯,此時x已經宣告瞭。

5.不允許重複宣告,不允許在相同作用域內重複宣告,不能在函式內部重新宣告引數。

function func() {
  let a = 10;
  let a = 1;
}
// ReferenceError 報錯
function func() {
  let a = 10;
  var a = 1;
}
// ReferenceError 報錯

不允許在相同作用域內,重複宣告同一個變數。

function func(arg) {
  let arg; // ReferenceError 報錯
}
function func(arg) {
  {
    let arg; // undefined 不報錯
  }
}

不能在函式內部重新宣告引數。

二、塊級作用域
1.塊級作用域,ES5 只有全域性作用域和函式作用域,沒有塊級作用域,帶來很多不合理的場景。塊級作用域允許任意巢狀,外層作用域無法讀取內層作用域的變數,內層作用域可以定義外層作用域的同名變數,塊級作用域寫法可以替代立即執行函式表示式(IIFE)。

var tmp = new Date();
function f() {
  console.log(tmp);
  if (true) {
    var tmp = `hello world`;
  }
}
f();// undefined

上面程式碼中,if程式碼塊的外部使用外層的tmp變數,內部使用內層的tmp變數。但是,函式f執行後,輸出結果為undefined,原因在於變數提升,導致內層的tmp變數覆蓋了外層的tmp變數。

var s = `hello`;
for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}
console.log(i);

上面程式碼中,變數i只用來控制迴圈,但是迴圈結束後,它並沒有消失,洩露成了全域性變數。

function f() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}
f();// 5

上面程式碼中,外層程式碼塊不受內層程式碼塊的影響。

{{{{{let a = `Hello World`}}}}};
{{{{
  {let a = `Hello World`}
  console.log(a); // 報錯
}}}};

上面程式碼中,塊級作用域允許任意巢狀,外層作用域無法讀取內層作用域的變數。

{{{{
  let a = `Hello World`;
  {let a = `Hello World`}
}}}};

上面程式碼中,內層作用域可以定義外層作用域的同名變數。
// IIFE 寫法

(function () {
  var a = ...;
}());
// 塊級作用域寫法
{
  let a = ...;
}

上面程式碼中,塊級作用域的出現,實際上使獲得廣泛應用的立即執行函式表示式(IIFE)不再必要了。

2.塊級作用域與函式,函式在塊級作用域之外不可引用,應避免在塊級作用域內宣告函式,如果確實需要,也應該寫成函式表示式,而不是函式宣告語句,只在使用大括號的情況下成立。

// 瀏覽器的 ES6 環境
function f() { console.log(`I am outside!`); }
(function () {
  if (false) {
    // 重複宣告一次函式f
    function f() { console.log(`I am inside!`); }
  }
  f();
}());
// Uncaught TypeError: f is not a function

ES6 規定,塊級作用域之中允許宣告函式,語句的行為類似於let,在塊級作用域之外不可引用。
上面程式碼在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

如上,考慮到環境導致的行為差異太大,應該避免在塊級作用域內宣告函式。

// 函式宣告語句
{
  let a = `secret`;
  function f() {
    return a;
  }
}

// 函式表示式
{
  let a = `secret`;
  let f = function () {
    return a;
  };
}

如上,如果確實需要,也應該寫成函式表示式,而不是函式宣告語句。

// 不報錯
`use strict`;
if (true) {
  function f() {}
}
// 報錯
`use strict`;
if (true)function f() {}

如上,ES6 的塊級作用域允許宣告函式的規則,只在使用大括號的情況下成立,如果沒有使用大括號,就會報錯。

三、const命令:const宣告一個只讀的常量。一旦宣告變數,就必須立即初始化,不能留到以後賦值。只在宣告所在的塊級作用域內有效。存在暫時性死區,只能在宣告的位置後面使用。不可重複宣告。宣告常量的值不能改變(數值、字串、布林值),對於複合型別的資料(物件和陣列)需要凍結。
1.必須立刻初始化。

const a;// SyntaxError: Missing initializer in const declaration

2.const的作用域與let命令相同:只在宣告所在的塊級作用域內有效。

{
  const a = 1;
}
a // ReferenceError: a_d is not defined

3.const命令宣告的常量也是不提升,同樣存在暫時性死區,只能在宣告的位置後面使用。

    {
        console.log(a);// ReferenceError: a is not defined
        const a = 1;
    }

4.const宣告的常量,與let一樣不可重複宣告。

    {
        var a = 1;
        let b = `hello`;
        const a = 2; // SyntaxError: Identifier `a` has already been declared
        const b = `world`; // SyntaxError: Identifier `b` has already been declared
    }

5.常量的值不能改變,將一個複合型別資料(物件、陣列)宣告為常量必須非常小心,需要凍結物件使用Object.freeze方法,。

    const a = 1;
    a = 2;// TypeError: Assignment to constant variable.

對於簡單型別的資料(數值、字串、布林值),值就儲存在變數指向的那個記憶體地址,因此等同於常量。但對於複合型別的資料(主要是物件和陣列),變數指向的記憶體地址,儲存的只是一個指標,const只能保證這個指標是固定的,至於它指向的資料結構是不是可變的,就完全不能控制了。

{
    const a = [`hello`];
    a.push(`world`);
    console.log(a); // ["hello", "world"]
    a = [`js`]; // TypeError: Assignment to constant variable.
}

如上,陣列a本身是可寫的,但是如果將另一個陣列賦值給a,就會報錯。

{
    const a = Object.freeze([`hello`]);
    a.push(`world`);// TypeError: Cannot add property 1, object is not extensible
}

如上,想將物件凍結,應該使用Object.freeze方法。

{
    const a = [`hello`];
    let func = (obj) => {
          Object.freeze(obj);
          Object.keys(obj).forEach( (key, i) => {
            if ( typeof obj[key] === `object` ) {
                  func( obj[key] );
            }
          });
    };
    func(a);
    Object.isFrozen(a); // true
}

如上,除了將物件本身凍結,物件的屬性也應該凍結,使用Object.isFrozen方法檢測是否凍結返回true。

ES6改變了很多,更加有邏輯性,目前還沒有完全支援ES6的JavaScript代理(無論是瀏覽器環境還是伺服器環境),熱衷於使用語言最新特性的開發者需要將ES6程式碼轉譯為ES5程式碼。儘管ES6做了大量的更新,但是它依舊完全向後相容以前的版本,標準化委員會決定避免由不相容版本語言導致的“web體驗破碎”。結果是,所有老程式碼都可以正常執行,整個過渡也顯得更為平滑,但隨之而來的問題是,多年的老問題依然存在。
Babel轉化工具
ES6相容性


相關文章