ES6學(xué)習(xí)筆記之let和const聲明

作為一個(gè)前端開發(fā)者,ES6則是必備技能了,針對(duì)自己的學(xué)習(xí),總結(jié)一些筆記,供自己以后回顧....
今天學(xué)習(xí)的是let和const聲明,參考阮一峰的《ECMAScript 6 入門》http://es6.ruanyifeng.com/

let和const聲明

let 聲明,我把它稱為塊級(jí)聲明,因?yàn)樗淖饔糜蛑辉谒诘膲K級(jí)作用域里有效,所以它和var是不同的,var的作用域是全局作用域.如下代碼:

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

在開發(fā)過程中,強(qiáng)烈建議使用let替換var聲明變量,看如下代碼:

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

上面代碼中,因?yàn)閕是var聲明出來的,屬于全局變量,每一次循環(huán)變量i都會(huì)發(fā)生改變,所以最后調(diào)用數(shù)組a索引為6的函數(shù)時(shí),里面輸出的i的值就是循環(huán)最后一次得到的i的值,所以結(jié)果為10.
這里一開始我是有點(diǎn)不清楚的,所以把執(zhí)行步驟過一遍:
步驟1:var a = [],
步驟2:執(zhí)行for循環(huán),此例為10次,每次執(zhí)行a[i] = function(){console.log(i)}
這時(shí)候,數(shù)組a里面的每個(gè)索引里的值都分別為function(){console.log(i)},如:

a[1] = function(){console.log(i)}
a[2] = function(){console.log(i)}
a[3] = function(){console.log(i)}
a[4] = function(){console.log(i)}
a[5] = function(){console.log(i)}
a[6] = function(){console.log(i)}
以此類推...

步驟3:調(diào)用a數(shù)組索引為6的方法

a[6]() //10

錯(cuò)誤思維:認(rèn)為console.log(i)中的i為獨(dú)立的,所以當(dāng)調(diào)用函數(shù)時(shí),索引為6的i值也為6.這個(gè)思維時(shí)錯(cuò)誤的,因?yàn)閕是全局變量,當(dāng)調(diào)用函數(shù)時(shí),i值已經(jīng)改變?yōu)樽詈笠淮窝h(huán)的i值,則為10
換為let聲明

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

此時(shí)變量i是let聲明的,當(dāng)前的i只在本輪循環(huán)有效,所以每一次循環(huán)的i其實(shí)都是一個(gè)新的變量,所以最后輸出的是6。你可能會(huì)問,如果每一輪循環(huán)的變量i都是重新聲明的,那它怎么知道上一輪循環(huán)的值,從而計(jì)算出本輪循環(huán)的值?這是因?yàn)?JavaScript 引擎內(nèi)部會(huì)記住上一輪循環(huán)的值,初始化本輪的變量i時(shí),就在上一輪循環(huán)的基礎(chǔ)上進(jìn)行計(jì)算。
for循環(huán)的特別之處:
循環(huán)語句部分是一個(gè)父作用域,而循環(huán)體內(nèi)部是一個(gè)單獨(dú)的子作用域。

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

上面代碼正確運(yùn)行,輸出了 3 次abc。這表明函數(shù)內(nèi)部的變量i與循環(huán)變量i不在同一個(gè)作用域,有各自單獨(dú)的作用域。

let聲明的特性

不存在變量提升
var命令會(huì)發(fā)生“變量提升”現(xiàn)象,即變量可以在聲明之前使用,值為undefined。這種現(xiàn)象多多少少是有些奇怪的,按照一般的邏輯,變量應(yīng)該在聲明語句之后才可以使用。

為了糾正這種現(xiàn)象,let命令改變了語法行為,它所聲明的變量一定要在聲明后使用,否則報(bào)錯(cuò)。

// var 的情況
console.log(foo); // 輸出undefined
var foo = 2;

// let 的情況
console.log(bar); // 報(bào)錯(cuò)ReferenceError
let bar = 2;

暫時(shí)性死區(qū)
由于let聲明不存在變量提升,因此使用let聲明變量之前先使用了該變量,則會(huì)報(bào)錯(cuò).這種情況則稱為暫時(shí)性死區(qū).

if (true) {
  // TDZ開始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ結(jié)束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

上面代碼中,在let命令聲明變量tmp之前,都屬于變量tmp的“死區(qū)”。
還有一些比較隱蔽的死區(qū),詳情請(qǐng)移步阮一峰的ES6,本文也是參考這本書來總結(jié)筆記的.上面已經(jīng)講的非常詳細(xì)了.總之造成暫時(shí)性死區(qū)的原因是因?yàn)樽兞课绰暶骶褪褂?
不允許重復(fù)聲明
使用let聲明是不允許在相同作用域內(nèi)被重復(fù)聲明的,會(huì)報(bào)錯(cuò).
先看看var重復(fù)聲明代碼:

 function x() {
    var x = 2;
    var x = 3;
    console.log(x);        
}
 x();//3

可見,var允許被重復(fù)聲明,并且會(huì)覆蓋前面的聲明.

 function x() {
    let x = 2;
    let x = 3;
    console.log(x);        
}
 x();//aught SyntaxError: Identifier 'x' has already been declared

注意:函數(shù)的參數(shù)也是一種隱式的let聲明,因此不能在函數(shù)內(nèi)部聲明參數(shù),也會(huì)報(bào)錯(cuò).

function func(arg) {
  let arg;
}
func() // 報(bào)錯(cuò)

function func(arg) {
  {
    let arg;
  }
}
func() // 不報(bào)錯(cuò)

塊級(jí)作用域
ES5的時(shí)候,只有全局作用域,和函數(shù)作用域,沒有塊級(jí)作用域的概念,
ES6引入了塊級(jí)作用域.學(xué)習(xí)了let之后應(yīng)該也能明顯的感受到塊級(jí)作用域的存在了吧.因?yàn)閘et聲明只作用在當(dāng)前塊級(jí)作用域,

function f1() {
  let n = 5;
  if (true) {
    let n = 10;//變量n只在當(dāng)前塊有效
  }
  console.log(n); // 5

ES6允許塊級(jí)作用域任意嵌套,每一層都是單獨(dú)的作用,并且內(nèi)層作用域可以與外層作用域有同名的變量.

{
      let insane = 'Hello World';
      {
        {
          {

            {
              let insane = 'Hello World'
              let x = 'default'
            }
            console.log(x) //報(bào)錯(cuò),因?yàn)楫?dāng)前塊作用域未聲明x,
          }
        }
      }
    };

塊級(jí)作用域的出現(xiàn),幾乎可以替換掉廣泛應(yīng)用的匿名立即執(zhí)行函數(shù)表達(dá)式(匿名 IIFE)

// IIFE 寫法
(function () {
  var tmp = ...;
  ...
}());

// 塊級(jí)作用域?qū)懛?{
  let tmp = ...;
  ...
}

塊級(jí)作用域與函數(shù)聲明
ES5規(guī)定,函數(shù)只能在頂層作用域和函數(shù)作用域中聲明,不能在塊級(jí)作用域聲明.
ES6允許函數(shù)在塊級(jí)作用域中聲明,并且僅作用于當(dāng)前塊,類似于let聲明.

function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重復(fù)聲明一次函數(shù)f
    function f() { console.log('I am inside!'); }
  }

  f();
}());

上例代碼在ES5環(huán)境中運(yùn)行,得到的結(jié)果為:"I am inside",因?yàn)樵趇f內(nèi)聲明的函數(shù)f會(huì)被提升到函數(shù)頭部,實(shí)際運(yùn)行代碼如下:

// ES5 環(huán)境
function f() { console.log('I am outside!'); }

(function () {
  function f() { console.log('I am inside!'); }
  if (false) {
  }
  f();
}());

ES6允許在塊級(jí)作用域中聲明函數(shù),相當(dāng)于使用let聲明,因此,當(dāng)前代碼在ES6環(huán)境中,在if內(nèi)聲明的函數(shù)f不會(huì)被提升到函數(shù)頭部,而且作用域僅在if語句的塊中有效.
運(yùn)行結(jié)果:
我的想法是會(huì)報(bào)錯(cuò),報(bào)f is not defined
但實(shí)際上并不是報(bào)這個(gè)錯(cuò),而是報(bào)了f is not a function,
原因是在ES6中聲明函數(shù)有三條特殊的規(guī)定:

  • 允許在塊級(jí)作用域內(nèi)聲明函數(shù)。
  • 函數(shù)聲明類似于var,即會(huì)提升到全局作用域或函數(shù)作用域的頭部。
  • 同時(shí),函數(shù)聲明還會(huì)提升到所在的塊級(jí)作用域的頭部。
    根據(jù)這三條規(guī)則,上面的代碼實(shí)際運(yùn)行代碼如下:
// 瀏覽器的 ES6 環(huán)境
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

強(qiáng)烈建議:避免在塊級(jí)作用域中聲明函數(shù),如果實(shí)在需要,可以寫成函數(shù)表達(dá)式,而不是函數(shù)聲明語句

// 塊級(jí)作用域內(nèi)部的函數(shù)聲明語句,建議不要使用
{
  let a = 'secret';
  function f() {
    return a;
  }
}

// 塊級(jí)作用域內(nèi)部,優(yōu)先使用函數(shù)表達(dá)式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}
const

const聲明一個(gè)只讀的常量.顧名思義,常量就是一旦聲明就不能改變的,改變常量的值就會(huì)報(bào)錯(cuò).

const PI = 3.1415;
console.log(PI)//3.1415
PI = 3; // TypeError: Assignment to constant variable.

const聲明之后一定要賦值,否則會(huì)報(bào)錯(cuò)

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

const的特性和let完全一樣,只在聲明所在的塊級(jí)作用域內(nèi)有效,并且不存在變量提升,因此也會(huì)存在暫時(shí)性死區(qū),同時(shí)也跟let一樣不能重復(fù)被聲明.詳情可以查看阮一峰的ES6
重點(diǎn)注意
對(duì)于簡單類型的數(shù)據(jù)(數(shù)據(jù),字符串,布爾值),const聲明的值是不可變的,
對(duì)于復(fù)雜類型的數(shù)據(jù)(數(shù)組,對(duì)象),const聲明的值不能保證不可變,
原因是簡單類型的數(shù)據(jù)保存在棧中,而復(fù)雜類型的數(shù)據(jù)保存在堆中,保存在棧中的只是一個(gè)指向?qū)嶋H數(shù)據(jù)指針,const聲明只能保證保存在棧中的數(shù)據(jù)不可變,不能保證保存在堆中的數(shù)據(jù)不可變.
因此,如果需要對(duì)復(fù)雜類型進(jìn)行凍結(jié),不讓它改變,可以使用Object.freeze方法。
來自MDN的解釋:
Object.freeze() 方法可以凍結(jié)一個(gè)對(duì)象。一個(gè)被凍結(jié)的對(duì)象再也不能被修改;凍結(jié)了一個(gè)對(duì)象則不能向這個(gè)對(duì)象添加新的屬性,不能刪除已有屬性,不能修改該對(duì)象已有屬性的可枚舉性、可配置性、可寫性,以及不能修改已有屬性的值。此外,凍結(jié)一個(gè)對(duì)象后該對(duì)象的原型也不能被修改。freeze() 返回和傳入的參數(shù)相同的對(duì)象。
這里不明白阮一峰寫的這段話,希望有知道的同學(xué)能幫忙解釋一下:
除了將對(duì)象本身凍結(jié),對(duì)象的屬性也應(yīng)該凍結(jié)。下面是一個(gè)將對(duì)象徹底凍結(jié)的函數(shù)。

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

這個(gè)Object.freeze()不是已經(jīng)凍結(jié)了對(duì)象,然后對(duì)象的屬性和值都不能被修改,不就代表屬性也被凍結(jié)了嗎.為什么還需要做遞歸凍結(jié)?

最后補(bǔ)充:
在let和const之間,建議優(yōu)先使用const,尤其是在全局環(huán)境,不應(yīng)該設(shè)置變量,只應(yīng)設(shè)置常量。
const聲明常量有兩個(gè)好處,一是閱讀代碼的人立刻會(huì)意識(shí)到不應(yīng)該修改這個(gè)值,二是防止了無意間修改變量值所導(dǎo)致的錯(cuò)誤。
所以聲明時(shí)優(yōu)先考慮使用const,當(dāng)遇到該值需要改變時(shí),再將const改為let,這是比較好的代碼習(xí)慣.
今天關(guān)于ES6的let和const聲明學(xué)習(xí)筆記就到這里了, 希望能夠幫助到正在閱讀的你, 如文中有所紕漏, 希望能夠指出, 感謝閱讀.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容