博客 / 詳情

返回

爪哇學習筆記——上下文、作用域和閉包

執行上下文

執行上下文(Execution Contexts),簡稱上下文,是一種規範策略,用於跟蹤ECMAScript實現對於代碼運行時的評估。在任何時間點,每個實際執行代碼的代理最多有一個執行上下文。 這稱為代理的運行執行上下文(running execution context)。

簡而言之,變量或函數的上下文決定了它們可以訪問哪些數據,以及它們的行為。

上下文一共有以下三種:

  • 全局上下文
  • 函數上下文(局部上下文)
  • eval()調用內部的上下文

執行上下文棧

執行上下文堆棧(execution context stack)用於跟蹤執行上下文。 正在運行的執行上下文始終是此堆棧的頂部元素。 每當控制從與當前運行的執行上下文相關聯的可執行代碼轉移到與該執行上下文無關的可執行代碼時,就會創建一個新的執行上下文。 新創建的執行上下文被壓入堆棧,成為運行的執行上下文。

全局上下文

全局上下文是最外層的上下文。根據ECMAScript實現的宿主環境,表示全局上下文的對象可能不一樣。在瀏覽器環境中,全局上下文就是我們常説的window對象,因此所有通過var定義的全局變量和函數都會成為window對象的屬性和方法。使用letconst的頂級聲明不會定義在全局上下文中,但在作用域鏈解析效果上是一樣的。

函數上下文

每個函數調用都有自己的函數上下文。當代碼執行流進入函數時,函數的上下文被推到一個上下文棧上。在函數執行完成之後,上下文棧就會彈出該函數上下文,將控制權返還給之前的執行上下文。

eval()調用內部的上下文

在非嚴格模式下,eval函數內部變量的聲明會影響調用上下文(callerContext

"use strict";

var x = 1;
let y = 3;
eval("var x = 2;let y = 4;");
eval("console.log(x, y);"); // 嚴格模式輸出1 3;非嚴格模式輸出2 3
console.log(x, y); // 嚴格模式輸出1 3;非嚴格模式輸出2 3
如果調用上下文的代碼或eval碼是嚴格模式代碼,則eval代碼不能實例化調用eval的調用上下文的變量環境中的變量或函數綁定。 相反,這樣的綁定在一個新的VariableEnvironment中實例化,只有eval代碼可以訪問。 由letconstclass聲明引入的綁定總是在新的LexicalEnvironment中實例化。

What's the difference between "LexicalEnvironment" and "VariableEnvironment" in spec

A LexicalEnvironment is a local lexical scope, e.g., for let-defined variables. If you define a variable with let in a catch block, it is only visible within the catch block, and to implement that in the spec, we use a LexicalEnvironment. VariableEnvironment is the scope for things like var-defined variables. vars can be thought of as "hoisting" to the top of the function. To implement this in the spec, we give functions a new VariableEnvironment, but say that blocks inherit the enclosing VariableEnvironment.

所以,非嚴格模式下,使用var聲明的變量會影響調用上下文,由letconstclass聲明的變量不會影響調用上下文。

作用域

在代碼執行之前,所有ECMAScript代碼都必須與作用域(Realms)相關聯。 從概念上講,一個作用域由一組內在對象、一個ECMAScript全局環境、在該全局環境範圍內加載的所有ECMAScript代碼以及其他相關的狀態和資源組成。

當我們創建了一個函數或者 {} 塊,就會生成一個新的作用域。需要注意的是,通過 var 創建的變量只有函數作用域,而通過 letconst 創建的變量既有函數作用域,也有塊作用域。

作用域分為以下兩種:

  • 詞法作用域(靜態作用域)
  • 動態作用域

詞法作用域

What is lexical scope?

詞法作用域指一個函數由定義即可確定能訪問的作用域,在編譯時即可推導出來。
function foo() {
    let a = 5;

    function foo2() {
        console.log(a);
    }

    return foo2;
}

動態作用域

動態作用域,指函數由調用函數的作用域鏈確定的可訪問作用域,是動態的。
function fn() {
    console.log('隱式綁定', this.a);
}
const obj = {
    a: 1,
    fn
}

obj.fn = fn;
obj.fn();

作用域鏈

每一個作用域都有對其父作用域的引用。當我們使用一個變量的時候,Javascript引擎 會通過變量名在當前作用域查找,若沒有查找到,會一直沿着作用域鏈一直向上查找,直到 global 全局作用域。作用域鏈決定了各級上下文中的代碼在訪問變量和函數時的順序。

作用域鏈增強

某些語句會導致在作用域鏈前端臨時添加一個上下文,這個上下文在代碼執行後會被刪除。

  • try/catch語句的catch
  • with語句

    function buildUrl() {
      let qs = "?debug=true";
    
      with(location) {
          let url = href + qs;
      }
    
      return url;
    }

this指向問題

this是在執行時動態讀取上下文決定的,而不是創建時

全局上下文中的this

無論是否在嚴格模式下,在全局執行上下文中(在任何函數體外部)this都指代全局對象

// 在瀏覽器中,window對象同時也是全局對象
console.log(this===window); // true

函數上下文中的this

在函數內部,this的值取決於函數被調用的方式

函數直接調用

函數直接調用中,非嚴格模式下this指向的是window,嚴格模式下this指向的是undefined

function foo() {
    console.log('函數內部this', this);
}

foo();

隱式綁定

this指向它的調用者,即誰調用函數,他就指向誰。

function fn() {
    console.log('隱式綁定', this.a);
}
const obj = {
    a: 1,
    fn
}

obj.fn = fn;
obj.fn(); // 1

顯式綁定

通過callapplycall方法改變this的行為。

var name = 'a';
function foo() {
    console.log('函數內部this', this.name);
}

foo(); // a
foo.call({name: 'b'}); // b
foo.apply({name: 'c'}); // c
const bindFoo = foo.bind({name: 'd'});
bindFoo(); // d

call、apply、bind的區別:

  • call基本等價於apply,傳參方式不同, call為依次傳入function.call(thisArg, arg1, arg2, ...)apply是數組傳入func.apply(thisArg, [argsArray])
  • callapply是直接返回改變this指向之後函數的執行結果,而bind是返回改變了this指向的函數

構造函數的new綁定

當一個函數用作構造函數時(使用new關鍵字),它的this被綁定到正在構造的新對象。

new關鍵字會進行如下操作:

  1. 創建一個空的簡單JavaScript對象(即{});
  2. 為步驟1新創建的對象添加屬性__proto__,將該屬性鏈接至構造函數的原型對象 ;
  3. 將步驟1新創建的對象作為this的上下文 ;
  4. 如果該函數沒有返回對象,則返回this。

箭頭函數中的this

箭頭函數中,this與封閉詞法上下文的this保持一致。

setTimeout回調函數中的this

setTimeout回調函數中的this指向window。可以使用箭頭函數作為回調函數,讓回調中的this指向父級作用域中的this。

閉包

MDN的解釋:

一個函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者説函數被引用包圍),這樣的組合就是閉包(closure)。

下面介紹一些閉包的應用:

事件處理(異步執行)的閉包

解決var的變量提升問題

    let lis = document.getElementsByTagName('li');

    for(var i = 0; i < lis.length; i++) {
        (function(i) {
            lis[i].onclick = function() {
                console.log(i);
            }
        })(i);
    }

實現私有變量

function People(num) { // 構造函數
    let age = num;
    this.getAge = function() {
        return age;
    };
    this.addAge = function() {
        age++;
    };
}

let tom = new People(18);
let pony = new People(20);
console.log(tom.getAge()); // 18
pony.addAge();
console.log(pony.getAge()); // 21

裝飾器函數

A function decorator is a higher-order function that takes one function as an argument and returns another function, and the returned function is a variation of the argument function—Javascript Allongé
  • 函數防抖與函數節流
  • once(fn)

    function once(fn){
      let returnValue;
      let canRun = true;
      return function runOnce(){
          if(canRun) {
              returnValue = fn.apply(this, arguments);
              canRun = false;
          }
          return returnValue;
      }
    }
    var processonce = once(process);
    processonce(); //process
    processonce(); //

使用閉包綁定函數上下文(實現bind函數的功能)

function.bind(thisArg[, arg1[, arg2[, ...]]])
arg1, arg2, ...表示當目標函數被調用時,被預置入綁定函數的參數列表中的參數。

// bind應該掛在Function的原型下
Function.prototype.myBind = function() {
    let fn = this; // 需要使用bind改變this指向的原函數
    let args = [...arguments];
    let newThis = args.shift();
    // bind返回一個函數
    return function() {
        // bind返回的函數執行時需要返回綁定新this的原函數的執行結果
        return fn.apply(newThis, args.concat(...arguments));
    }
}

// 實現apply
Function.prototype.myApply = function() {
    let fn = this;
    let args = [...arguments];
    let newThis = args.shift();
    
    if(args.length > 0) {
        args = args[0];
    } else {
        args = [];
    }

    // 臨時掛載函數
    newThis.fn = fn;

    // 執行掛載函數
    let result = newThis.fn(...args);

    // 銷燬臨時掛載
    delete newThis.fn;

    return result;
}

// 測試
let obj = {
    a: 1
}

function foo(b) {
    console.log(this.a, b);
}

foo.myApply(obj, [2]); // 1 2

let foo1 = foo.myBind(obj, 2);
foo1(); // 1 2

實戰練習

寫出如下代碼的輸出結果:

const foo = {
    bar: 10,
    fn: function() {
        console.log(this.bar);
        console.log(this);
    }
}
// 取出函數
let fn1 = foo.fn;
// 單獨執行
fn1();

// 輸出undefined  window對象

如何改變fn的指向:

const o1 = {
    text: 'o1',
    fn: function() {
        // 直接使用上下文 - 傳統分活
        return this.text;
    }
}

const o2 = {
    text: 'o2',
    fn: function() {
        // 呼叫領導執行 - 部門協作
        return o1.fn();
    }
}

const o3 = {
    text: 'o3',
    fn: function() {
        // 直接內部構造 - 公共人
        let fn = o1.fn;
        return fn();
    }
}

console.log('o1fn', o1.fn()); // o1
console.log('o2fn', o2.fn()); // o1
console.log('o3fn', o3.fn()); // undefined 因為先取出了o1對象的fn,然後再直接執行

現在我要使console.log('o2fn', o2.fn())的輸出結果為o2:

 const o1 = {
    text: 'o1',
    fn: function() {
        return this.text;
    }
}

const o2 = {
    text: 'o2',
    // 將o1的fn搶過來,掛到o2下面
    fn: o1.fn
}

console.log('o2fn', o2.fn());
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.