JavaScript-你可能不瞭解的塊級作用域

先來兩個"栗子"

 

儘管你可能連一行帶有塊級作用風格的代碼都沒有寫過,但是你這種常見的JavaScript代碼一定很熟悉:

for (var i = 0; i < 5; i++) {
    console.log(i);
}

我們在for循環中直接定義了變量i,通常我們只想在循環體內部的上下文環境中使用i,但是事情並不是向着我們希望的發展,i會被隱式的綁定到外面的作用域(函數作用域或者是全局作用域)。

var a = true;
if (a) {
    var b = a * 2;
    b = func(b);
    console.log(b);
} 
function func(b) {
    return b + 1;
}

 

我們聲明的變量b在代碼使用時,僅僅在if聲明的上下文使用,如果能將它現在if的上下文將是一件很有意義的事情,但是"理想很飽滿,現在很骨感",使用var聲明變量,它在任何地方都是一樣的,因爲它將屬於外部作用域。

就好像,你屬於地球,但是你隱式的綁定到了宇宙一樣。

上面簡單敘述了兩個小"梨子"

爲什麼我們希望變量可以綁定到自己的塊級作用域了,不急我們慢慢往下看!

塊級作用域定義

我相信在座的小夥子,如果每一天有一個面試官坐你對面:

面試官:請簡單說一下塊級作用域。

你:(思考,首先想一想什麼是塊級作用域....)???...不知道?

其實可以拆分爲兩部分來解釋:作用域、塊級

1.作用域:還可以裝個B,不同角度去解釋作用域。如果覺得還不夠,還可以說一下作用域分類(全局作用域、函數作用域、塊級作用域)。

  • 廣義:可訪問變量、函數、對象的集合,決定代碼區域中變量和其他資源的可見性。
  • 狹義:所有編程語言最基本的功能就是存儲變量的的值,並且在之後能夠訪問和修改它,這種訪問或者修改變量的值的能力給程序帶來了“狀態”,如果沒有狀態,程序的靈活性會大大降低,在程序中如何存儲變量,已經變量的訪問,需要一套設計良好的規則,我們稱這套規則爲作用域。

2.塊級:javaScript的塊級就是{...}大括號內的代碼塊,我們稱之爲一個塊級。

所以總結一下就是,塊級作用域就是包含在{...}中的作用域。在這個作用域中,擁有着和函數作用域相同的行爲。

如何創建一個塊級作用域

就是大家一行ES6代碼都沒有寫過,但是你也可能知道,在包含let、const的代碼塊中存在一個塊級作用域。但是其實有很多種定義塊級作用域的方式。早在ES6之前就可以創建塊級作用域。

with

function m(obj) {
    with(obj) {
      a = 2;
      console.log(a);
    }
    console.log(obj);
    console.log(obj.a);
}
var obj = {}; m(obj);

with是一個難以理解的結構,JavaScript中有兩個機制可以欺騙"詞法作用域的方式,with就是其中之一,with本質上通過將一個對象的引用當做作用域來處理,將對象的屬性當做作用域的標識符來處理,從而創建一個新的詞法作用域(運行時)。

這裏with從對象中創建的作用域僅在with聲明中而非外部作用域中有效。

try/catch

try{
    undefined();
} catch(err) {
    console.log(err);
}
console.log(err);

非常少的人會注意到JavaScript的ES3規範中規定try/catch的分句會創建一個塊級作用域,其中的變量僅在catch內部有效。

let

到了大家都熟悉的ES6了。

var a = true;
if (a) {
    let b = a * 2;
    b = func(b);
    console.log(b);
} 
function func(b) {
    return b + 1;
}
console.log(b);

let關鍵字可以將變量變動到所在任意作用域(通常是{..}內部),換句話說,let爲其聲明的變量隱式的劫持了所在的作用域。

這裏有一個小的知識可能需要大家注意,看如下代碼:

function f() {
  console.log(a);
  let a = 2;
}
f(); // ReferenceError: a is not defined

這段代碼直接報錯a is not defined,let和const擁有類似的特徵,阻止了變量提升,當執行console.log(a)的時候變量沒有定義

  • MDN中寫到:In ECMAScript 2015, let do not support Variable Hoisting, which means the declarations made using "let", do not move to the top of the execution block.

在MDN中認爲let不存在變量提升

  • ECMA-262-13.3.1 Let and Const Declarations寫到: let and const declarations define variables that are scoped to the running execution context's LexicalEnvironment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable's LexicalBinding is evaluated.

這說明即使是 block 最後一行的 let 聲明,也會影響 block 的第一行。這就是提升(hoisting)

  • ECMA-262:8.2.1.2 Runtime Semantics: EvalDeclarationInstantiation( body, varEnv, lexEnv, strict)寫到:The environment of with statements cannot contain any lexical declaration so it doesn't need to be checked for var/let hoisting conflicts.

這句話也間接的證明 let hoisting 的存在。

那其實大家會有疑問,爲什麼上面的代碼會報錯。其實這並不是由於變量不提示導致的,而是由於TDZ(臨時性死區)導致的。

在舉個例子:

{
    a = 2;
    let a;
}

這段代碼可以解釋爲:

{
    let a;// 變量提升
    "start TDZ"
    a = 2; // 這裏在TDZ中間,所以會導致a = 2 報錯
    a;
    "end TDZ"
}

所以破案了:let是不存在變量提升。它“變量提升的行爲”,是由於TDZ導致的。

so...總結一下

  • let 聲明會提升到塊頂部

  • 從塊頂部到該變量的初始化語句,這塊區域叫做 TDZ(臨時死區)

  • 如果你在 TDZ 內使用該變量,JS 就會報錯,注意TDZ 跟 hoisting不等價。

const

除了let以外,ES6還引入了const,同樣可以用來創建塊級作用域變量,但其值是固定的(常量)。之後任何視圖修改值操作都會引起錯誤。

塊級作用域的好處

防止內層變量會覆蓋外層變量

var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined

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

let循環

for (let i = 0; i < 5; i++) {
    console.log(i);
}
console.log(i);

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

垃圾收集

function func(obj) {
    // doSomething
}
var obj = {...};
func(obj);
var bnt = document.getElementById('xxx');
bnt.addEventListener('click', function() {
    // doSomething 
});

在上述代碼中,點擊元素,觸發click事件,在這裏並不需要obj對象,理論上,當func執行後,在內存中obj就會被垃圾回收機制回收,但是click函數形成了一個覆蓋整個作用域的閉包。JavaScript引擎極有可能依然保持這個結構,而不進行回收。

function func(obj) {
    // doSomething
}
{
    let obj = {...};
    func(obj);
}
var bnt = document.getElementById('xxx');
bnt.addEventListener('click', function() {
    // doSomething 
});

塊級作用域可以讓引擎清楚的理解到沒有必要保持obj的內存,讓垃圾回收機制進行回收。

 


長得美的,長得帥的都關注了

現在就差你了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章