1. 程式人生 > 程式設計 >由 JavaScript 的 with 引發的探索

由 JavaScript 的 with 引發的探索

目錄
  • 1. 背景
  • 2. with
    • 2.1. with 的效能問題
  • 3. LHS 和 RHS
    • 4. 執行上下文和作用域鏈
      • 4.1. VO
      • 4.2. AO
      • 4.3. 作用域鏈

    一下文章來源於微信公眾號前端巔峰

    1. 背景

    某天吃飯的時候突然想到,都說 with 會有問題,那麼是什麼問題,是怎樣導致的呢?知其然不知其所以然,在好奇心的驅使下,從 with 出發,一路追溯到 VO、AO。那麼先來複習一下 with 是幹嘛的吧。

    2. with

    with 是為物件訪問提供名稱空間式的訪問方式,with 建立一個物件的名稱空間,在這個名稱空間內你可以直接訪問物件的屬性,而不需要通過物件來訪問:

    const o = { a: 1,b: 2 };
    
    with (o) {
      console.log(a); // 1
      b = 3; // o: { a: www.cppcns.com
    1,b: 3 } }

    看起來挺方便的哈?

    const o = { a: 1,b: 2 };
    const p = { a: 3 };
    with (o) {
      console.log(a); // 1
      with (p) {
        console.log(a); // 3
        b = 4; // o: { a: 1,b: 4 }
        c = 5; // window.c = 5
      }
    }

    嗯,他還有作用域鏈的性質。但是,如果給不存在的屬性賦值,將會沿著作用域鏈給該變數賦值,在沒有找到變數時,非嚴格模式下將會自動在全域性建立一個變數。這就導致了資料洩露。

    那麼導致資料洩露的原因是什麼呢?這需要了解 LHS 查詢,這個待會再說。

    那來看看 js 是怎麼查詢的:當 with 物件 o 的時候,with 宣告的作用域是 o,從這裡對 c 進行 LHS 查詢。o 的作用域和全域性作用域都沒有找到 c,在非嚴格模式下,失敗的 LHS 會自動隱式的在全域性建立一個識別符號 c,如果是嚴格模式,則會丟擲 ReferenceError

    2.1. with 的效能問題

    使用 with:

    function f () {
      const o = { a: 1 }
      console.time();
    
      with(o) { 
        for (let i = 0; i < 100000; i++) {
          a = 2;
        }
      }
    
      console.timeEnd();
    }

    由JavaScript的with引發的探索

    正常情況:

    function f () {
      const o = { a: 1 }
      console.time();
    
      for (let i = 0; i < 100000; i++) {
        o.a = 2;
      }
    
      console.timeEnd();
    }

    由JavaScript的with引發的探索

    將近 10 倍的差距。

    原因是什麼呢?

    js 預編譯階段會進行的優化,由於 with 建立新的詞法作用域,導致 o 的 a 屬性和 o 分離開位於兩個不同的作用域,不能快速找到識別符號,引擎將不會做任何優化。

    這就好比你去某家店,引擎給了你一個牛逼的老大,老闆一眼就知道該怎麼做;套上 with 之後,老闆不知道你的老大是誰,還要花時間去找,時間就這樣浪費了。

    3. LHS 和 RHS

    • LHS:賦值操作的目標是誰
    • RHS:誰是賦值操作的源頭

    所以我們來看這段程式碼:

    var a = 1;
    
    

    在執行的時候,這段程式碼會被拆成兩部分

    var a;
    a = 1;

    當我們使用 a

    console.log(a);

    對 a 進行了 RHS 查詢,以獲得 a 的值,並隱式賦值給console.log 函式的形參,賦值物件的查詢也是 LHS 查詢。

    當然,更直白的 LHS 也如上文寫到的 a = 1

    在變數還沒有宣告(在任何http://www.cppcns.com作用域中都無法找到該變數)情況下,這兩種查詢行為是不一樣的。

    LHS 和 RHS 查詢都會在當前執行作用域中開始,沿著作用域鏈向上查詢,直到全域性作用域。

    而不成功的 RHS 會丟擲 ReferenceError ,不成功的 LHS 會自動隱式地建立一個全域性變數(非嚴格模式),並作為 LHS 的查詢結果(嚴格模式也會丟擲 ReferenceError)。

    那麼,複習一下作用域鏈的查詢吧。

    4. 執行上下文和作用域鏈

    在 js 中有三種程式碼執行環境:

    • 全域性執行環境
    • 函式執行環境
    • Eval 執行環境

    js 程式碼執行的時候,為了區分執行環境,會進入不同的執行上下文(Execution context,EC),這些執行上下文會構成一個執行上下文棧(Execution context stackECS)。

    對於每個 EC 都有一個變數物件(Variable object,VO),作用域鏈(Scope chain)和 this 三個主要屬性。

    4.1. VO

    變數物件(Variable object)是與 EC 相關的作用域,儲存了在 EC 中定義的變數和函式宣告。VO 中一般會包含以下資訊:

    • 變數
    • 函式宣告式
    • 函式的形參

    而函式表示式和沒有用 varletconst 宣告的變數(全域性變數,存在於全域性 EC 的 VO)不存在於 VO 中。對於全域性 VO,有 VO === this === global。

    4.2. AO

    在函式 EC 中,VO 是不能直接訪問的,此時由啟用物件(Activation ObjectAO)來替代 VO 的角色。AO 是在進入函式 EC 時被建立的,它通過函式的 arguments 進行初始化。這時,VO === AO。

    如:

    function foo(b) {
      const a = 1;
    }
    foo(1);
    // AO:{ arguments: {...},UTamNqa: 1,b: 1 }

    4.3. 作用域鏈

    瞭解了 EC、AO、VO,再來看作用域鏈

    var x = 10;
    function foo() {
      var y = 20;
     
      function bar() {
        var z = 30;
       
        console.log(x + y + z);
      };
     
      bar()
    };
    foo();

    由JavaScript的with引發的探索

    • 橙色箭頭指向 VO/AO
    • 藍色箭頭們則是作用域鏈(VO/AO + All Parent VO/AOs)

    理解了查詢過程,很容易想到 js 中的原型鏈,而作用域鏈和原型鏈則可以組合成一個二維查詢:先通過作用域鏈查詢到某個物件,再查詢這個物件上的屬性。

    const foo = {}
    function bar() {
      Object.prototype.a = 'Set foo.a from prototype';
      returnfunction () {
        console.log(foo.a);
      }
    }
    bar()();
    // Set foo.a from prototype

    到此這篇關於由 的 with 引發的探索的文章就介紹到這了,更多相關 Script 的 with 引發的探索內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!