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

1、先來兩個"梨子"

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

1.1

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

咱們在for循環中直接定義了變量i,一般咱們只想在循環體內部的上下文環境中使用i,可是事情並非向着咱們但願的發展,i會被隱式的綁定到外面的做用域(函數做用域或者是全局做用域)。es6

1.2

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聲明變量,它在任何地方都是同樣的,由於它將屬於外部做用域。\color{red}{就好像,你屬於地球,可是你隱式的被綁定到了宇宙同樣}面試

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

爲何咱們但願變量能夠綁定到本身的塊級做用域了,不急咱們慢慢往下看!bash

2、塊級做用域定義

我相信在座的小夥子,若是每一天有一個面試官坐你對面:閉包

面試官:請簡單說一下塊級做用域。
你:(思考,首先想想什麼是塊級做用域....)???...不知道?
複製代碼

其實能夠拆分爲兩部分來解釋:做用域、塊級。編程語言

  1. 做用域:還能夠裝個B,不一樣角度去解釋做用域。若是以爲還不夠,還能夠說一下做用域分類(全局做用域、函數做用域、塊級做用域)。
  • 廣義:可訪問變量、函數、對象的集合,決定代碼區域中變量和其餘資源的可見性。
  • 狹義:全部編程語言最基本的功能就是存儲變量的的值,而且在以後可以訪問和修稿它,這種訪問或者修改變量的值得能力給程序帶來了「狀態」,若是沒有狀態,程序的靈活性會大大下降,在程序中如何存儲變量,已經變量的訪問,須要一套設計良好的規則,咱們稱這套規則爲做用域。
  1. 塊級:javaScript的塊級就是{...}大括號內的代碼塊,咱們稱之爲一個塊級。

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

3、如何建立一個塊級做用域

就是你們一行ES6代碼都沒有寫過,可是你也可能知道,在包含let、const的代碼塊中存在一個塊級做用域。可是其實有不少種定義塊級做用域的方式。早在ES6以前就能夠建立塊級做用域。post

3.1 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本質上經過將一個對象的引用當作做用域來處理,將對象的屬性當作做用域的標識符來處理,從而建立一個新的詞法做用域(運行時)。ui

\color{red}{在這裏with從對象中建立的做用域僅在with聲明中而非外部做用域中有效。}

3.2 try/catch

try{
    undefined();
} catch(err) {
    console.log(err);
}
console.log(err);
複製代碼

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

3.3 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 的存在。

\color{red}{在 ECMAScript 2015中, let 也會提高到語句塊的頂部。可是,在這個語句塊中,在變量聲明以前引用這個變量會致使一個 ReferenceError的結果。}

那其實你們會有疑問,爲何上面的代碼會報錯。其實這並非因爲變量不提示致使的,而是因爲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不等價。

3.4 const

處了let之外,ES6還引入了const,一樣能夠用來建立塊級做用域變量,但其值是固定的(常量)。以後任何視圖修改\color{red}{值}的操做都會引發錯誤。

4、塊級做用域的好處

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

var tmp = new Date();

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

f(); // undefined
複製代碼

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

4.2let循環

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

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

4.3垃圾收集

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的內存,讓垃圾回收機制進行回收。

5、總結

但願小夥伴喜歡個人文章,咱們一塊兒成長,謝謝你們!

參考:JavaScript變量提高運行機制

參考:es6.ruanyifeng.com/

相關文章
相關標籤/搜索