[toc]
this 到底指向誰?有一種廣泛流傳的說法是:*誰調用它,this 就指向誰。*
也就是說,this 的指向實在調用時確定的。這么說沒有太大的問題,可是并不全面。面試官要求我們用更加規范的語言進行總結,那么他到底再等什么樣的回答呢?
事實上,調用函數會創建新的屬于函數自身的執行上下文。執行上下文的調用創建階段會決定 this 的指向。到此,我們可以得出一個結論:
> this 的指向,是在調用函數時根據執行上下文所動態確定的。
具體環節和規則,可以先“死記硬背”以下幾條規律,后面來慢慢一一分析:
* 在函數體中,簡單調用函數時(非顯示/隱式綁定下),嚴格模式下 this 綁定到undefined,否則綁定到全局對象 window/global;
* 一般構造函數new調用,綁定到新創建的對象上;
* 一般由 bind/call/apply 方法顯示調用,綁定到指定參數的對象上;
* 一般由上下文對象調用,綁定再該對象上;
* 箭頭函數中,根據外層上下文綁定的this決定this的指向。
當然,真實環境多樣,我們來逐一梳理。
## 全局環境下的 this
在這種情況相對簡單直接,函數在瀏覽器全局環境中被簡單調用,非嚴格模式下 this 指向window;在 use strict 指明嚴格模式的情況下就是 undefined。我們來看例題,請描述打印結果:
```javascript
function f1() {
console.log(this)
}
function f2() {
'use strict'
console.log(this)
}
f1() // window
f2() // undefined
```
這樣的題目比較基礎,但是需要候選人格外注意其變種,請再看一題:
```javascript
const foo = {
bar: 10,
fn: function() {
console.log(this)
console.log(this.bar)
}
}
var fn1 = foo.fn
fn1()
```
這里的 this 任然指向的是 window。雖然 fn 函數在 foo 對象中作為方法被引用,但是在賦值給 fn1 之后,fn1 的執行任然是在 window 的全局環境中。因此輸出 window 和 undefined,他們相當于:
```javascript
console.log(window)
console.log(window.bar)
```
還是上面這道題目,如果調用改變為:
```javascript
const foo = {
bar: 10,
fn: function() {
console.log(this)
console.log(this.bar)
}
}
foo.fn()
```
將會輸出:
```javascript
{ bar: 10, fn: f }
10
```
因為這個時候 this 指向的時最后調用它的對象,在 foo.fn() 語句中 this 指向 foo 對象。請記住:
在執行函數時,如果函數中的 this 是被上一級的對象所調用,那么 this 指向的就是上一級對象;否則指向全局環境。
## 上下文對象調用中的this
如上結論,面對下面的題目的時候我們就不會在困惑了:
```javascript
const student = {
name: 'zhangsan',
fn: function() {
return this
}
}
console.log(student.fn() === student) // true
```
最終結果將返回 true。
當存在更復雜的調用關系時,請看例題:
```javascript
const person = {
name: 'zhangsan',
brother: {
name: 'lisi',
fn: function() {
return this.name
}
}
}
console.log(person.brother.fn())
```
在這種嵌套關系中,this 指向最后調用它的對象,因此輸出將會時:lisi。
到此,this 的上下文對象調用已經理解得比較清楚了。當我們再看一道更高階的題目:
```javascript
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o2 = {
text: 'o2',
fn: function() {
return o1.fn()
}
}
const o3 = {
text: 'o3',
fn: function() {
var fn = o1.fn
return fn()
}
}
console.log(o1.fn()) // o1
console.log(o2.fn()) // o1
console.log(o3.fn()) // undefined
```
答案是:o1、o1、undefined,你對對了嗎?
我們來一一分析:
* 第一個 console 最簡單,o1 沒有問題。難點在第二個和第三個上面,關鍵還是看調用this的哪個函數。
* 第二個 console 的 o2.fn(),最終還是調用 o1.fn(),因此答案當然時 undefined。
* 最后一個,在進行 var fn = o1.fn 賦值之后,時“裸奔”調用,因此這里的 this 指向 window,答案當然時undefined。
如果面試者回答順利,可以緊接著追問,如果我們需要讓:
```javascript
console.log(o2.fn())
```
輸出 o2 該怎么做?
一般開發者可能會想到使用 bind/call/apply 來對 this的指向進行干預,這確實時一種思路。但是我接著問,如果不能使用 bind/call/apply,有別的辦法嗎?
這樣可以考察候選人基礎掌握的深度及隨機應變的能力。答案為:
```javascript
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o2 = {
text: 'o2',
fn: o1.fn
}
console.log(o2.fn()) // o2
```
還是應用那個最重要的結論:this 指向最后調用它的對象,在 fn 執行時,掛到 o2 對象上即可,我們提前進行了賦值操作。
## bind/call/apply 改變 this 指向
上文提到 bind/call/apply,在這個概念上,比較常見的基礎考察點時:bind/call/apply 三個方法的區別。
這樣的問題相對基礎,我們直接上答案:一句話總結,**他們都是用來改變相關函數的this指向的,但是 直接 call/apply 是直接進行相關函數調用;bind 不會執行相關函數,而是返回一個新的函數,這個新的函數已經自動綁定了新的 this指向,開發者需要手動調用即可。** 再具體的 call/apply 之間的區別主要體現在參數設定上。
用代碼來總結:
```javascript
const target = {}
fn.call(target, 'arg1', 'arg2')
```
相當于
```javascript
const target = {}
fn.apply(target, ['arg1', 'arg2'])
```
相當于
```javascript
const target = {}
fn.bind(target, 'arg1', 'arg2')()
```
具體基礎用法這里不再科普,如果讀者尚不清楚,需要自己補充一下知識點。
我們來看一道例題:
```javascript
const foo = {
name: 'zhangsan',
logName: function() {
console.log(this.name)
}
}
const bar = {
name: 'lisi',
}
console.log(foo.logName.call(bar)) // lisi
```
將會輸出 lisi,這不難理解。但是對 bind/call/apply 的高級考察往往會結合構造函數以及組合式實現繼承。構造函數的使用案例,我們結合下面的例題進行分析。
## 構造函數和this
這方面最直接的例題為:
```javascript
function Foo() {
this.bar = 'zhangsan'
}
const instance = new Foo()
console.log(instance.bar) // zhangsan
```
答案將是zhangsan。但是這樣的場景往往伴隨這下一個問題:new 操作符調用構造函數,具體做了什么?以下供參考:
- 創建一個對象;
- 將構造函數的this指向這個新對象;
- 為這個對象添加屬性、方法等;
- 最終返回新對象。
以上過程,也可以用代碼表述:
```javascript
var obj = {}
obj.__proto__ = Foo.prototype
Foo.call(obj)
```
當然,這里對 new 的模擬是一個簡單基本版本。
需要指出的是,如果在構造函數中出現了顯示 return 的情況,那么需要注意分為兩種場景:
```javascript
function Foo() {
this.user = 'zhangsan'
const o = {}
return o
}
const instance = new Foo()
console.log(instance.user) // undefined
```
將會輸出undefined,此時 instance 是返回的空對象 o。
```javascript
function Foo() {
this.user = 'zhangsan'
return 1
}
const instance = new Foo()
console.log(instance.user) //zhangsan
```
將會輸出zhangsan,也就是說此時 instance 是返回目標對象示例 this。
**結論:如果構造函數中顯示返回一個對象,那么 this 就指向這個返回的對象;如果返回的不是一個對象,那么 this仍然指向實例。**
## 箭頭函數中的this指向
**箭頭函數使用 `this`?不適應以上標準規則,而是根據外層(函數或者全局)上下文來決定**。
來看題目:
```javascript
const foo = {
? ? fn: function() {
? ? ? ? setTimeout(function(){
? ? ? ? ? ? console.log(this)
? ??? ??})
? ??}
}
console.log(foo.fn()) // window
```
這道題中,`this` 出現在 `setTimeout()` 中的匿名函數里,因此 `this` 指向 window 對象。如果需要 `this` 指向 foo 這個 object 對象,可以巧用箭頭函數解決:
```javascript
const foo = {
? ? fn: function() {
? ? ? ? setTimeout(() => {
? ? ? ? ? ? console.log(this)
? ??? ??})
? ??}
}
console.log(foo.fn())
// { fn: f }
```
單純箭頭函數中的 `this` 非常簡單,但是綜合所有情況,結合 `this` 的優先級考察,這是哈哈 `this` 指向并不好確定。
## this 優先級相關
我們常常把通過 call、apply、bind、new 對 `this` 綁定的情況稱為顯示綁定;根據調用關系確定的 `this` 指向稱為隱式綁定。
那么顯示綁定和隱式綁定誰的優先級更高呢?
情況例題:
```javascript
function foo(a) {
? ? console.log(this.a)
}
const obj1 = {
? ? a: 1,
? ? foo: foo
}
const obj2 = {
? ? a: 2,
? ? foo: foo
}
obj1.foo.call(obj2)? ? // 2
obj2.foo.call(obj1)? ? // 1
```
輸出分別為2、1,也就是說 call、apply 的顯示綁定一般來說優先級更高。
```javascript
function foo(a) {
? ? this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a)? ? // 2
```
上述代碼通過 `bin` 將 bar 函數中的 `this` 綁定為 obj1 對象。執行 `bar(2) `后 `obj1.a` 的值為`2`。即經過 `bar(2)` 執行后,`obj1` 對象為: `{ a:2 }`。
當再使用 bar 作為構造函數時:
```javascript
var baz = new bar(3)
console.log(baz.a)? ? // 3
```
將會輸出3。我們看到 bar 函數本身式通過 `bind` 方法構造的函數,其內部已經對將 `this` 綁定為 obj1,它再作為構造函數,通過 `new` 調用時,返回的實例已經與 obj1 解綁。也就是說:
> `new` 綁定修改了 bind 綁定的 `this`,因此 `new` 綁定的優先級比顯示 bind綁定更高
我們再看看:
```javascript
function foo() {
? ? return a => {
? ? ? ? console.log(this.a)
? ??}
}
const obj1 = {
? ? a: 2
}
const obj2 = {
? ? a: 3
}
const bar = foo.call(obj1)
console.log(bar.call(obj2))? ? // 2
```
將會輸出2。由于 `foo()` 的 this 綁定到 obj1,bar(引用箭頭函數)的 `this` 也會綁定到 obj1,箭頭函數的綁定無法被修改。
如果將 foo 完全寫成箭頭函數的形式:
```javascript
var a = 123
const foo = () => a => {
? ? console.log(this.a)
}
const obj1 = {
? ? a: 2
}
const obj2 = {
? ? a: 3
}
var bar = foo.call(obj1)
console.log(bar.call(obj2))? ? // 123
```
將會輸出123。
這里再“抖個機靈”,僅僅將上述代碼的第一處變量 a 的賦值改為:
```javascript
const a = 123
/* 同上 */
```
答案將會輸出 `undefined`,原因時因為使用 `const` 聲明的變量不會掛載到 window 全局對象當中。
- 說明
- CSS與HTML
- BFC的特性及其常見應用
- CSS深入理解之margin
- CSS深入理解之line-height
- CSS盒模型相關知識
- CSS知識總結
- HTML知識總結
- 三欄布局五種方式
- JavaScript內置對象
- 1.循環
- 2.數組方法對比
- 3.字符串實用常操紀要
- JavaScript核心
- var、let、const定義變量
- this 的指向問題詳解
- 箭頭函數
- ES6部分知識歸納
- ES6的Class
- Promise和Async/await
- 面向對象的概念及JS中的表現
- 創建對象的九種方式
- JS的繼承
- 閉包總結
- 構造函數與作用域
- 原型與原型鏈
- 函數的四種調用模式
- apply、call、bind詳解
- JavaScript應用
- 1.JavaScript實現深拷貝與淺拷貝
- 2.函數防抖與節流
- 3.無阻塞腳本加載技術
- DOM
- 如何寫出高性能DOM?
- 事件探秘
- 事件委托
- 操作DOM常用API詳解
- 重排和重繪
- 運行機制與V8
- 瀏覽器的線程和進程
- Vue.js
- Vue.js知識點總結
- Vue-Router知識點總結
- 父子組件之間通信的十種方式
- 優化首屏加載
- 關于Vuex
- 前端路由原理及實現
- 在Vue.js編寫更好的v-for循環的6種技巧
- 12個Vue.js開發技巧和竅門
- 網絡協議
- HTTP緩存機制
- UDP協議
- TCP協議
- HTTPS協議
- HTTPS的背景知識、協議的需求、設計的難點
- HTTPS與HTTP的區別
- 框架與架構
- MVC、MVP、MVVM
- Gulp與Webpack的區別
- Angular React 和 Vue的比較
- 虛擬DOM和實際的DOM有何不同?
- 架構問題
- 工程化
- npm link命令
- npm scripts 使用指南
- 前端工程簡史
- 常見的構建工具及其對比
- Webpack基本配置與概念
- 設計模式
- 工廠設計模式
- 單例設計模式
- 適配器模式
- 裝飾器模式