js 的詞法作用域和 this

jinzhuming發表於2020-10-11

靜態作用域又叫做詞法作用域,採用詞法作用域的變數叫詞法變數。詞法變數有一個在編譯時靜態確定的作用域。詞法變數的作用域可以是一個函式或一段程式碼,該變數在這段程式碼區域內可見(visibility);在這段區域以外該變數不可見(或無法訪問)。詞法作用域裡,取變數的值時,會檢查函式定義時的文字環境,捕捉函式定義時對該變數的繫結。大多數現在程式設計語言都是採用靜態作用域規則,如 C/C++C#PythonJavaJavaScript ……相反,採用動態作用域的變數叫做動態變數。只要程式正在執行定義了動態變數的程式碼段,那麼在這段時間內,該變數一直存在;程式碼段執行結束,該變數便消失。這意味著如果有個函式 f,裡面呼叫了函式 g,那麼在執行 g 的時候,f 裡的所有區域性變數都會被 g 訪問到。而在靜態作用域的情況下,g 不能訪問 f 的變數。動態作用域裡,取變數的值時,會由內向外逐層檢查函式的呼叫鏈,並列印第一次遇到的那個繫結的值。顯然,最外層的繫結即是全域性狀態下的那個值。採用動態作用域的語言有 PascalEmacs LispCommon Lisp (兼有靜態作用域)、 Perl (兼有靜態作用域)。C/C++ 是靜態作用域語言,但在宏中用到的名字,也是動態作用域。

實踐

思考這麼一段程式碼:


function fun1() {
  var a = 2
  console.log(a)
}

function fun2() {
  var a = 3
  console.log(a)
  fun1()
}
 fun2()

答案會是多少呢?

3
2

當然這個很好理解,js 是函式作用域,fun1 內有 a,當然會列印 fun1 內的 a 的值,並沒有特殊之處,可是這個程式碼呢:

var a = 2
function fun1() {
  console.log(a)
}

function fun2() {
  var a = 3
  console.log(a)
  fun1()
}
 fun2()

答案和之前依舊一樣,是不是這樣就有點反直覺了? fun1 內沒有 a 的情況下不是應該讀取 fan2a 嗎?為什麼會讀取 全域性作用域的 a 呢?說好的作用域是一層一層向上的呢?

當然,作用域確實是向上查詢,可是 js 是靜態作用域(詞法作用域),並不是動態作用域,所以他不會看函式的呼叫位置,而是定義位置,並且沿著定義位置向上查詢。詞法作用域和動態作用域的區別如下:

  • 詞法作用域是在 ** 程式碼解析(定義)** 的時候確定的,關注的是函式在 ** 何處定義 **,並從定義處向上查詢作用域。
  • 動態作用域是在 ** 程式碼執行 ** 的時候確定的,關注的是程式碼在 ** 何處呼叫 **,並從呼叫棧向上查詢作用域。

所以現在很好理解,為什麼 fun1 內沒有 a 他會先去讀取全域性的 a,而不是 fun2a 了吧?不信可以看這個程式碼:

function fun1() {
  console.log(a) // a is not defined
}

function fun2() {
  var a = 3
  console.log(a)

  fun1()
}
fun2()

當然,js 有個特殊之處,就是 this,思考這段程式碼:

this.a =  2

function fun1() {
  console.log(this.a) // 1
}

function fun2() {
  this.a = 1
  console.log(this.a) // 1

  fun1()
}
fun2()

是不是疑惑了,說好的從定義的地方向上查詢呢,為什麼會列印出執行的作用域的值?
這裡可以先說答案:因為 this

this.a =  2
// this 指向 window

function fun1() {
  // 這裡 this 還是指向 window
  console.log(this) // window
  console.log(this.a) // 1
}

function fun2() {
  // this 依舊指向 window,不信可以列印看看那
  console.log(this.a) // 2

  // 這裡修改了外邊的 this.a
  this.a = 1
  // 列印修改後的值
  console.log(this.a) // 1

  fun1()
}
fun2()

所以明白了吧? 作用域依舊是在定義的地方向上查詢,只不過是兩個函式都指向了同一個 this 而已。

這裡插一嘴,雖然我認為 jsthis 是一個設計的非常糟糕的東西(他完全不符合正常人的思維邏輯),我也非常非常久都不再使用 this,但是我認為這個東西還是必須得理解的,不然早晚會搞出大麻煩,你可以不用,但是你必須要懂。

Ok, 接著上面所說,為什麼兩個函式指向了同一個 this(window)?這裡就要深入的瞭解一下 this 的指向問題:this 究竟指向哪裡,是都指向 window 麼?顯然不是,看一下程式碼:

this.n = 1

function fun2() {
  console.log(this.n) // 2
}
var a = {
  n: 2,
  fun1() {
    console.log(this) // {n: 2, fun1: function}
    console.log(this.n) // 2
    a.fun2()
  },
  fun2
}

a.fun1()

這裡的 fun1this 明顯指向了 a 本身,並不是 this,同樣 fun2 雖然定義在外部,但是也依然指向了 a ,是不是和之前想的不太一樣?fun2 定義在外邊,那麼他的 this 應該是 window 才對,列印的應該是 1 才對啊,可能這個時候你就在想了,是不是 this 就是動態作用域呢?並不! This 依舊是靜態作用域,參考這個程式碼:

this.n = 1

function fun2() {
  console.log(this.n) // 1
}
var a = {
  n: 2,
  fun1() {
    fun2()
  },
  fun2
}

a.fun1()

發現區別了嗎?this 依舊是指向 window,這就說明 this 只是在定義的時候強行繫結了執行他的環境,所以我們透過 a.fun2 呼叫,this 就指向 a,透過直接呼叫 fun2(實際等於 window.fun2),指向的則是 window

當然也有例外,比如箭頭函式:

this.n = 1

const fun2 = () =>  {
  console.log(this.n)
}
var a = {
  n: 2,
  fun1() {
    // console.log(this)  // {n: 2, fun1: function}
    // console.log(this.n) // 2
    a.fun2()
    // fun2()
  },
  fun2
}

a.fun1()

箭頭函式中,不管你是 a.fun2 還是直接 fun2,指向的都是 window,因為箭頭函式的 this 固定指向他的父作用域,而根據靜態作用域的原則,他父作用域是定義時的作用域,也就是 window,所以不管怎麼呼叫,他都是 window。透過以下這個例子更能看出來這一點,箭頭函式的this固定指向他定義的作用域:

var n = 1
var a = () => {
    console.log(this.n)
}

var b={
    n: 2,
    fun2: {
        n: 3,
        fun1:a,
        fun() {
            a() // 1
            console.log(this) // {n: 3,     fun1:function, fun: function}
            console.log(this.n) // 3
            this.fun1() // 1

        }
    }
}
b.fun2.fun()

透過這個你就能發現,箭頭函式的 this 並不指向呼叫他的物件,也不是指向呼叫他的物件的父作用域,而是指向他定義的位置的父作用域,不管你在哪裡呼叫,都是同一個指向。

總結

總結一下,對於 this,你只需要記住這幾點:

  • ** 正常情況下 this 指向呼叫他的上下文 **
  • 箭頭函式的 this 指向他的父作用域的 this(靜態作用域、靜態作用域、靜態作用域)
  • new 會建立一個新的物件,this 指向這個物件,詳情可以自行了解 new
  • callbindapply 會改變 this 的指向,詳情自行了解
a.xx()
xx 內的 this 就是 a
a.b.xx()
xx 內的 this 就是 b

.xxx,. 之前的上下文就是他的 this。
而在非嚴格模式的全域性環境中(嚴格模式會報錯),實際我們定義的變數都是掛載在 window 下,所以 this 指向的是 window

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章