1. 程式人生 > >let和const----你所不知道的JavaScript系列

let和const----你所不知道的JavaScript系列

err 刪除 read AS 對象 ble 試題 都是 AR

let

眾所周知,在ES6之前,聲明變量的關鍵字就只有var。var 聲明變量要麽是全局的,要麽是函數級的,而無法是塊級的。

var a=1;
console.log(a);  //1
console.log(window.a);  //1

function test(){   var b=2;   function print(){     console.log(a,b);   } print(); } test(); //1 2 console.log(b); //Uncaught ReferenceError: b is not defined
for(var i=0;i<=10;i++){
var sum=0; sum+=i; } console.log(i); //11 console.log(sum); //10 聲明在for循環內部的i和sum,跳出for循環一樣可以使用。

再來看看下面這個栗子:

HTML:
<ul>
    <li>0</li>
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>
JS:
window.onload = function(){
     var aLi = document.getElementsByTagName(‘li‘);
     
for (var i=0;i<aLi.length;i++){ aLi[i].onclick = function(){ alert(i); }; }

這是一道很經典的筆試題,也是很多初學者經常犯錯而且找不到原因的一段代碼。想要實現的效果是點擊不同的<li>標簽,alert出其對應的索引值,但是實際上代碼運行之後,我們會發現不管點擊哪一個<li>標簽,alert出的i都為4。因為在執行for循環之後,i的值已經變成了4,等到點擊<li>標簽時,alert的i值是4。在ES6之前,大部分人會選擇使用閉包來解決這個問題,今天我們使用ES6提供的let來解決這個問題。接下來就看看let的神奇吧。

window.onload = function(){
    var aLi = document.getElementsByTagName(‘li‘);
    for (let i=0;i<aLi.length;i++){
        aLi[i].onclick = function(){
            alert(i);
        }
    };     
}            

有看出什麽區別嗎?奧秘就在for循環中var i=0變成了let i=0,我們僅僅只改了一個關鍵字就解決了這個問題,還避免了使用閉包可能造成的內存泄漏等問題。

上述代碼中的for 循環頭部的 let 不僅將 i 綁定到了 for 循環的塊中, 事實上它將其重新綁定到了循環的每一個叠代中, 確保使用上一個循環叠代結束時的值重新進行賦值。

後面就讓我們好好來了解一下let這個神奇的關鍵字吧。

let 關鍵字可以將變量綁定到所在的任意作用域中(通常是 { .. } 內部)。換句話說,let為其聲明的變量隱式地了所在的塊作用域。 ----《你所不知道的JavaScript》P32

上述代碼,可以通過另一種方式來說明每次叠代時進行重新綁定的行為:

window.onload = function(){
    var aLi = document.getElementsByTagName(‘li‘);
    for (let i=0;i<aLi.length;i++){
        let j = i;
        aLi[j].onclick = function(){
            alert(j);
        }
    };     
}      

在這裏還有個點要說明的,就是 for循環還有一個特別之處,就是循環語句部分是一個父作用域,而循環體內部是一個單獨的子作用域。

這就很好理解上面這段代碼的意思了。每次循環體執行的時候,let聲明的變量 j 會從父作用域(循環語句塊)取值保存到自己的塊級作用域內,由於塊級作用域內的變量不受外部幹擾,所以每次循環體生成的塊級作用域相互獨立,各自保存著各自的 j 值。


來看一下 let var 的一些異同吧

1、函數作用域 vs 塊級作用域

function varTest() {
  var x = 31;
  if (true) {
    var x = 71;  // same variable!
    console.log(x);  // 71
  }
  console.log(x);  // 71
}

function letTest() {
  let x = 31;
  if (true) {
    let x = 71;  // different variable
    console.log(x);  // 71
  }
  console.log(x);  // 31
}

可以看出在letTest函數的 if 判斷中重新聲明的x並不會影響到 if 代碼塊之外的代碼,而varTest函數中用var聲明的卻會。這是因為let聲明的變量只在代碼塊(通常是{ }所形成的代碼塊)中有效。

2、變量提升 vs 暫時性死區

我們都知道,var聲明的變量會有變量提升的作用,如下

console.log(a);  //1
var a=1;

console.log(b);  //undefined
var b;

可以看出,雖然代碼中console調用a在前,聲明a在後,但是由於在js中,函數及變量的聲明都將被提升到函數的最頂部,也就是說(var聲明的)變量可以先使用再聲明。

然後,使用let,const(後面會提及)聲明的變量卻不存在變量提升。

console.log(foo); // Uncaught ReferenceError: foo is not defined
let foo = 2;

console.log(foo1); // Uncaught ReferenceError: foo1 is not defined
let foo1;

ES6明確規定,如果區塊中存在命令,這個區塊對這些命令聲明的變量,從一開始就形成了封閉作用域。凡是在聲明之前就使用這些變量,就會報錯。所以在代碼塊內,使用let命令聲明變量之前,該變量都是不可用的。這在語法上,稱為“暫時性死區”(temporal dead zone,簡稱 TDZ)。

總之,暫時性死區的本質就是,只要一進入當前作用域,所要使用的變量就已經存在了,但是不可獲取,只有等到聲明變量的那一行代碼出現,才可以獲取和使用該變量。

註:“暫時性死區”也意味著typeof不再是一個百分之百安全的操作,因為會使typeof報錯。

3、let不允許重復聲明

if (true) {
  let aa;
  let aa; // Uncaught SyntaxError: Identifier ‘aa‘ has already been declared
}

if (true) {
  var _aa;
  let _aa; // Uncaught SyntaxError: Identifier ‘_aa‘ has already been declared
}

if (true) {
  let aa_;
  var aa_; // Uncaught SyntaxError: Identifier ‘aa_‘ has already been declared
}

let不允許在相同作用域內,重復聲明同一個變量。

4、全局變量 vs 全局對象的屬性

ES5中全局對象的屬性與全局變量基本是等價的,但是也有區別,比如通過var聲明的全局變量不能使用delete從 window/global ( global是針對與node環境)上刪除,不過在變量的訪問上基本等價。

ES6 中做了嚴格的區分,使用 var 和 function 聲明的全局變量依舊作為全局對象的屬性,使用 let, const 命令聲明的全局變量不屬於全局對象的屬性。

let let_test = ‘test‘;
console.log(window.let_test);   // undefined
console.log(this.let_test);   // undefined

var var_test = ‘test‘;
console.log(window.var_test);  // test
console.log(this.var_test);  // test

const

除了let以外,ES6還引入了const,同樣可以用來創建塊作用域變量,但其值是固定的(常量)。使用const聲明變量的時候,必須同時賦值,否則會報錯。並且之後任何試圖修改值的操作都會引起錯誤.

const data;  //Uncaught SyntaxError: Missing initializer in const declaration
if (true) {
    var a = 2;
    const b = 3; // 包含在 if 中的塊作用域常量
    a = 3; // 正常 !
    b = 4; // Uncaught TypeError: Assignment to constant variable.
} 
console.log( a ); // 3
console.log( b ); // Uncaught ReferenceError: b is not defined

註:復合類型const變量保存的是引用。因為復合類型的常量不指向數據,而是指向數據(heap)所在的地址(stack),所以通過 const 聲明的復合類型只能保證其地址引用不變,但不能保證其數據不變。

const arr= [1, 2];

// 修改數據而不修改引用地址,正確執行
arr.push(3);  // [1, 2, 3]

// 修改 arr 常量所保存的地址的值,報錯
arr = [];     // Uncaught TypeError: Assignment to constant variable.

簡單的使用const無法完成對象的凍結。可以通過Object.freeze()方法實現對對象的凍結。使用Object.freeze()方法返回的對象將不能對其屬性進行配置(definedProperty()不可用)同時不能添加新的屬性和移除(remove)已有屬性。徹底凍結對象時需要遞歸的對它的對象屬性進行凍結。

let obj = {
  a: 1,
  b: {
    b1: 2
  }
};

obj.b.b1 = 3;
console.log(obj.b.b1 ); //3

function freeze(obj){
  Object.freeze(obj);
  Object.values(obj).forEach(function (value,index) {
    if(typeof value === ‘object‘){
      freeze(value);
    }
  })
}

freeze(obj);

obj.b.b1 = 4;
console.log(obj.b.b1); //3

總結:

塊級作用域的出現,讓廣泛使用的 IIFE (立即執行匿名函數)不再必要。

// 匿名函數寫法
(function () {
  var jQuery = function() {};
  // ...
  window.$ = jQuery
})();
 
// 塊級作用域寫法
{
  let jQuery = function() {};
  // ...
  window.$ = jQuery;
}

附:在ES6之前,關鍵字with和關鍵字try/catch都會創建相關的塊級作用域。關鍵字with已經不推薦使用了,我們在這裏就不多描述。在ES3規範中規定try/catch的catch分句會創建一個塊作用域,其中聲明的變量僅在catch內部有效。

try {
    undefined(); // 執行一個非法操作來強制制造一個異常
}
catch (err) {
    console.log( err ); // 能夠正常執行!
} 
console.log( err ); // Uncaught ReferenceError: err is not defined

let和const----你所不知道的JavaScript系列