1. 程式人生 > >JavaScript 的 this 指向和繫結詳解

JavaScript 的 this 指向和繫結詳解

JavaScript 中的 new、bind、call、apply 實際這些都離不開 this,因此本文將著重討論 this,在此過程中分別講解其他相關知識點。 注意: 本文屬於基礎篇,請大神繞路。如果你不夠了解,或者瞭解的還不完整,那麼可以通過本文來複習一下。 ## this 指向的型別 剛開始學習 JavaScript 的時候,this 總是最能讓人迷惑,下面我們一起看一下在 JavaScript 中應該如何確定 this 的指向。 this 是在函式被呼叫時確定的,它的指向完全取決於函式呼叫的地方,而不是它被宣告的地方(除箭頭函式外)。當一個函式被呼叫時,會建立一個執行上下文,它包含函式在哪裡被呼叫(呼叫棧)、函式的呼叫方式、傳入的引數等資訊,this 就是這個記錄的一個屬性,它會在函式執行的過程中被用到。 this 在函式的指向有以下幾種場景: 1. 作為建構函式被 new 呼叫 2. 作為物件的方法使用 3. 作為函式直接呼叫 4. 被 call、apply、bind 呼叫 5. 箭頭函式中的 this 下面我們分別來討論一下這些場景中 this 的指向。 ### 1.new 繫結 函式如果作為建構函式使用 new 呼叫時, this 繫結的是新建立的建構函式的例項。 ```js function Foo() { console.log(this) } var bar = new Foo() // 輸出: Foo 例項,this 就是 bar ``` 實際上使用 new 呼叫建構函式時,會依次執行下面的操作: - 建立一個新物件 - 建構函式的 prototype 被賦值給這個新物件的 `__proto__` - 將新物件賦給當前的 this - 執行建構函式 - 如果函式沒有返回其他物件,那麼 new 表示式中的函式呼叫會自動返回這個新物件,如果返回的不是物件將被忽略 ### 2.顯式繫結 通過 call、apply、bind 我們可以修改函式繫結的 this,使其成為我們指定的物件。通過這些方法的第一個引數我們可以顯式地繫結 this。 ```js function foo(name, price) { this.name = name this.price = price } function Food(category, name, price) { foo.call(this, name, price) // call 方式呼叫 // foo.apply(this, [name, price]) // apply 方式呼叫 this.category = category } new Food('食品', '漢堡', '5塊錢') // 瀏覽器中輸出: {name: "漢堡", price: "5塊錢", category: "食品"} ``` call 和 apply 的區別是 call 方法接受的是引數列表,而 apply 方法接受的是一個引數陣列。 ```js func.call(thisArg, arg1, arg2, ...) // call 用法 func.apply(thisArg, [arg1, arg2, ...]) // apply 用法 ``` 而 bind 方法是設定 this 為給定的值,並返回一個新的函式,且在呼叫新函式時,將給定引數列表作為原函式的引數序列的前若干項。 ```js func.bind(thisArg[, arg1[, arg2[, ...]]]) // bind 用法 ``` 舉個例子: ```js var food = { name: '漢堡', price: '5塊錢', getPrice: function (place) { console.log(place + this.price) }, } food.getPrice('KFC ') // 瀏覽器中輸出: "KFC 5塊錢" var getPrice1 = food.getPrice.bind({ name: '雞腿', price: '7塊錢' }, '肯打雞 ') getPrice1() // 瀏覽器中輸出: "肯打雞 7塊錢" ``` 關於 bind 的原理,我們可以使用 apply 方法自己實現一個 bind 看一下: ```js // ES5 方式 Function.prototype.bind = Function.prototype.bind || function () { var self = this var rest1 = Array.prototype.slice.call(arguments) var context = rest1.shift() return function () { var rest2 = Array.prototype.slice.call(arguments) return self.apply(context, rest1.concat(rest2)) } } // ES6 方式 Function.prototype.bind = Function.prototype.bind || function (...rest1) { const self = this const context = rest1.shift() return function (...rest2) { return self.apply(context, [...rest1, ...rest2]) } } ``` ES6 方式用了一些 ES6 的知識比如 rest 引數、陣列解構。 注意: 如果你把 null 或 undefined 作為 this 的繫結物件傳入 call、apply、bind,這些值在呼叫時會被忽略,實際應用的是預設繫結規則。 ```js var a = 'hello' function foo() { console.log(this.a) } foo.call(null) // 瀏覽器中輸出: "hello" ``` ### 3.隱式繫結 函式是否在某個上下文物件中呼叫,如果是的話 this 繫結的是那個上下文物件。 ```js var a = 'hello' var obj = { a: 'world', foo: function () { console.log(this.a) }, } obj.foo() // 瀏覽器中輸出: "world" ``` 上面程式碼中,foo 方法是作為物件的屬性呼叫的,那麼此時 foo 方法執行時,this 指向 obj 物件。也就是說,此時 this 指向呼叫這個方法的物件,如果嵌套了多個物件,那麼指向最後一個呼叫這個方法的物件: ```js var a = 'hello' var obj = { a: 'world', b: { a: 'China', foo: function () { console.log(this.a) }, }, } obj.b.foo() // 瀏覽器中輸出: "China" ``` 最後一個物件是 obj 上的 b,那麼此時 foo 方法執行時,其中的 this 指向的就是 b 物件。 ### 4.預設繫結 函式獨立呼叫,直接使用不帶任何修飾的函式引用進行呼叫,也是上面幾種繫結途徑之外的方式。非嚴格模式下 this 繫結到全域性物件(瀏覽器下是 winodw,node 環境是 global),嚴格模式下 this 繫結到 undefined (因為嚴格模式不允許 this 指向全域性物件)。 ```js var a = 'hello' function foo() { var a = 'world' console.log(this.a) console.log(this) } foo() // 相當於執行 window.foo() // 瀏覽器中輸出: "hello" // 瀏覽器中輸出: Window 物件 ``` 上面程式碼中,變數 a 被宣告在全域性作用域,成為全域性物件 window 的一個同名屬性。函式 foo 被執行時,this 此時指向的是全域性物件,因此打印出來的 a 是全域性物件的屬性。 注意有一種情況: ```js var a = 'hello' var obj = { a: 'world', foo: function () { console.log(this.a) }, } var bar = obj.foo bar() // 瀏覽器中輸出: "hello" ``` 此時 bar 函式,也就是 obj 上的 foo 方法為什麼又指向了全域性物件呢,是因為 bar 方法此時是作為函式獨立呼叫的,所以此時的場景屬於預設繫結,而不是隱式繫結。這種情況和把方法作為回撥函式的場景類似: ```js var a = 'hello' var obj = { a: 'world', foo: function () { console.log(this.a) }, } function func(fn) { fn() } func(obj.foo) // 瀏覽器中輸出: "hello" ``` 引數傳遞實際上也是一種隱式的賦值,只不過這裡 obj.foo 方法是被隱式賦值給了函式 func 的形參 fn,而之前的情景是自己賦值,兩種情景實際上類似。這種場景我們遇到的比較多的是 setTimeout 和 setInterval,如果回撥函式不是箭頭函式,那麼其中的 this 指向的就是全域性物件. 其實我們可以把預設綁定當作是隱式繫結的特殊情況,比如上面的 bar(),我們可以當作是使用 window.bar() 的方式呼叫的,此時 bar 中的 this 根據隱式繫結的情景指向的就是 window。 ## this 繫結的優先順序 this 存在多個使用場景,那麼多個場景同時出現的時候,this 到底應該如何指向呢。這裡存在一個優先順序的概念,this 根據優先順序來確定指向。優先順序:new 繫結 > 顯示繫結 > 隱式繫結 > 預設繫結 所以 this 的判斷順序: - new 繫結: 函式是否在 new 中呼叫?如果是的話 this 繫結的是新建立的物件; - 顯式繫結: 函式是否是通過 bind、call、apply 呼叫?如果是的話,this 繫結的是指定的物件; - 隱式繫結: 函式是否在某個上下文物件中呼叫?如果是的話,this 繫結的是那個上下文物件; - 如果都不是的話,使用預設繫結。如果在嚴格模式下,就繫結到 undefined,否則繫結到全域性物件。 ## 箭頭函式中的 this 箭頭函式 是根據其宣告的地方來決定 this 的,它是 ES6 的知識點。 箭頭函式的 this 繫結是無法通過 call、apply、bind 被修改的,且因為箭頭函式沒有建構函式 constructor,所以也不可以使用 new 呼叫,即不能作為建構函式,否則會報錯。 ```js var a = 'hello' var obj = { a: 'world', foo: () => { console.log(this.a) }, } obj.foo() // 瀏覽器中輸出: "hello" ``` 我們可以看看 ECMAScript 標準中對箭頭函式的描述。 原文: > An Arrow Function does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment. Typically this will be the Function Environment of an immediately enclosing function. 翻譯: > 箭頭函式不為 arguments、super、this 或 new.target 定義本地繫結。箭頭函式中對 arguments、super、this 或 new.target 的任何引用都解析為當前所在詞法作用域中的繫結。通常,這是箭頭函式所在的函式作用域。

— ECMAScript Language Specification - Arrow Function | ECMA 標準 - 箭頭函式

## 一個 this 的小練習 用一個小練習來實戰一下: ```js var a = 20 var obj = { a: 40, foo: () => { console.log(this.a) function func() { this.a = 60 console.log(this.a) } func.prototype.a = 50 return func }, } var bar = obj.foo() // 瀏覽器中輸出: 20 bar() // 瀏覽器中輸出: 60 new bar() // 瀏覽器中輸出: 60 ``` 稍微解釋一下: 1)`var a = 20` 這句在全域性變數 window 上建立了個屬性 a 並賦值為 20; 2)首先執行的是 `obj.foo()`,這是一個箭頭函式,箭頭函式不建立新的函式作用域直接沿用語句外部的作用域,因此 `obj.foo()` 執行時箭頭函式中 this 是全域性 window,首先打印出 window 上的屬性 a 的值 20,箭頭函式返回了一個原型上有個值為 50 的屬性 a 的函式物件 func 給 bar; 3)繼續執行的是 `bar()`,這裡執行的是剛剛箭頭函式返回的閉包 func,其內部的 this 指向 window,因此 this.a 修改了 window.a 的值為 60 並打印出來; 4)然後執行的是 `new bar()`,根據之前的表述,new 操作符會在 func 函式中建立一個繼承了 func 原型的例項物件並用 this 指向它,隨後 `this.a = 60` 又在例項物件上建立了一個屬性 a,在之後的列印中已經在例項上找到了屬性 a,因此就不繼續往物件原型上查找了,所以打印出第三個 60。 如果把上面例子的箭頭函式換成普通函式呢,結果會是什麼樣? ```js var a = 20 var obj = { a: 40, foo: function () { console.log(this.a) function func() { this.a = 60 console.log(this.a) } func.prototype.a = 50 return func }, } var bar = obj.foo() // 瀏覽器中輸出: 40 bar() // 瀏覽器中輸出: 60 new bar() // 瀏覽器中輸出: 60 ``` 這個例子就不詳細講解了。 如果把上面兩個例子弄懂原理,基本上 this 的指向就掌握的差不