[TOC]
參考資料:《你不知道的 JavaScript 上卷》
this 是一個很特別的關鍵字,被自動定義在所有函數的作用域中。
# this 的作用域
一種常見的誤解是,this 指向函數的作用域。這個說法在某種情況下是正確的,但是在其他情況下卻是錯誤的。需要明確的是,this 在任何情況下都不指向函數的詞法作用域。在 JavaScript 內部,作用域確實和對象類似,可見的標識符都是它的屬性。但是作用域“對象”無法通過 JavaScript 代碼訪問,它存在于 JavaScript 引擎內部。
思考下面一段代碼,它試圖(但是沒有成功)跨越邊界,使用 this 來隱式調用函數的詞法作用域:
```js
function foo () {
var a = 2
this.bar()
}
function bar () {
console.log(this.a)
}
foo()
```
首先,這段代碼試圖通過 this.bar() 來引用 bar() 函數,這是不可能成功的。調用 bar() 最自然的方法就是省略前面的 this,直接使用詞法引用標識符。
此外,這段代碼還試圖使用 this 聯通 foo() 和 bar() 的詞法作用域,從而讓 bar() 可以訪問 foo() 作用域里的變量 a,這是不可能實現的,你不能使用 this 來引用一個詞法作用域內部的東西。
# this 到底是什么
this 是在運行時綁定的,并不是在編寫時綁定的,它的上下文取決于函數調用時的各種條件。this 的綁定和函數聲明的位置沒有任何關系,只取決于函數的調用方式。
當一個函數被調用時,會創建一個活動記錄(有時也稱為執行上下文),這個記錄會包含函數在哪里被調用(調用棧)、函數的調用方法、傳入的參數等信息。this 就是記錄的其中一個屬性,會在函數執行的過程中用到。
# 調用位置
在理解 this 的綁定過程之前,首先要理解調用棧(就是為了到達當前執行位置所調用的所有函數)和調用位置。
```js
function baz () {
// 當前調用棧是:baz
// 因此,當前調用位置是全局作用域
console.log('baz')
bar() // <-- bar 的調用位置
}
function bar () {
// 當前調用棧是 baz -> bar
// 因此,當前調用位置在 baz 中
console.log('bar')
foo() // <-- foo 的調用位置
}
function foo () {
// 當前調用棧是 baz -> bar -> foo
// 因此,當前調用位置在 bar 中
console.log('foo')
}
baz() // <-- baz 的調用位置
```
# 綁定規則
最后,來看看在函數的執行過程中調用位置如何決定 this 的綁定對象。
你必須找到調用位置,然后判斷需要應用下面四條規則中的哪一條,我們首先分別解釋這四條規則,然后解釋多條規則都可用時它們的優先級如何排列。
## 默認綁定
首先要介紹的是最常用的函數調用類型:獨立函數調用。可以把這條規則看作是無法應用其他規則時的默認規則。
思考如下一段代碼:
```js
function foo () {
console.log(this.a)
}
var a = 2
foo () // 2
```
首先要注意的是,聲明在全局作用域中的變量(如 var a = 2)就是全局對象的一個同名屬性,它們本質上就是同一個東西,并不是通過復制得到的,就像一個硬幣的兩面一樣。
接下來我們可以看到當調用 foo() 時,this.a 被解析成了全局變量 a,因為在本例中,函數調用時應用了 this 的 **默認綁定**,因此 this 指向全局對象。
我們怎么知道這里應用了 **默認綁定** 呢?可以通過分析調用位置來看看 foo() 是如何調用的。在代碼中,foo() 是直接使用不帶任何修飾的函數引用進行調用的,因此只能使用默認綁定,無法應用其他規則。
如果使用嚴格模式(strict mode),那么全局對象將無法使用默認綁定,因此 this 會綁定到 undefined
```js
function foo () {
'use strict'
console.log(this.a)
}
var a = 2
foo () // TypeError: Cannot read property 'a' of undefined
```
## 隱式綁定
```js
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
obj.foo() // 2
```
當函數引用有上下文對象時,隱式綁定規則會把函數調用中的 this 綁定到這個上下文對象。(函數作為對象的屬性被調用)
需要注意的是,對象屬性引用鏈中只有最后一層會影響調用位置,舉例來說:
```js
function foo () {
console.log(this.a)
}
var obj2 = {
a: 42,
foo: foo
}
var obj1 = {
a: 2,
obj2: obj2
}
obj1.obj2.foo() // 42
```
<span style="font-size: 20px;">隱式丟失</span>
一個最常見的 this 綁定問題就是被隱式綁定的函數會丟失綁定對象,那么它就會應用默認綁定,從而把 this 綁定到全局對象或 undefined 上。
```js
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo // 函數別名
var a = 'oops, global' // a 是全局對象的屬性
bar() // 'oops, global'
```
雖然 bar 是 obj.foo 的一個引用,但是實際上,它引用的是 foo 函數本身,因此此時的 bar() 其實是一個不帶任何修飾的函數調用,因此應用了默認綁定。
一種更常見的情況發生在傳入回調函數時:
```js
function foo () {
console.log(this.a)
}
function doFoo (fn) {
// fn 其實引用的是 foo
fn () // <-- 調用位置!
}
var obj = {
a: 2,
foo: foo
}
var a = 'oops, global'
doFoo(obj.foo) // 'oops, global'
```
參數傳遞其實是一種隱式復制,因此我們傳入函數時也會被隱式復制,所以結果和上一個例子一樣。
如果把函數傳入語言內置的函數而不是我們自己聲明的函數時會發生什么?結果是一樣的,沒有區別:
```js
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var a = 'oops, global'
setTimeout(obj.foo, 100) // 'oops, global'
```
JavaScript 環境內置的 setTimeout() 函數的實現和如下的偽代碼類似:
```js
function setTimeout (fn, delay) {
// 等待 delay 毫秒
fn() // <-- 調用位置
}
```
## 顯式綁定
調用一個函數時指定它的 this,所有的函數都具有 call、apply、bind 方法(這和原型有關)。
- call 方法在使用一個指定的 this 和若干個指定的參數值的前提下調用某個函數或方法
- apply 和 call 基本一樣,只是參數變為數組了而已
- bind() 方法會創建一個新函數,當這個新函數被調用時,bind() 的第一個參數將作為它運行時的 this,傳遞參數時,可以在 bind 時傳遞部分參數,調用時又傳遞其余參數。一個綁定函數也能使用 new 操作符創建對象:這種行為就像把原函數當成構造器
需要解釋的是 bind 創建的新函數作為構造函數時的行為:當 bind 返回的函數作為構造函數的時候,bind 時指定的 this 值會失效,但傳入的參數依然生效,舉例如下:
```js
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'daisy');
var obj = new bindFoo('18'); // this 并沒有綁定為 foo,失效了
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin
```
## new 綁定
使用 new 來調用函數,或者說發生構造函數調用時,會自動執行以下操作:
1. 創建一個新對象,并將該對象的\_\_proto\_\_屬性指向構造函數的 prototype 屬性指向的對象(這一過程可以稱為 [[ 原型 ]] 連接)
2. 將 this 綁定到這個新對象上
3. 執行構造函數中的代碼(為這個新對象添加屬性)
4. 返回這個新對象。(一般情況下,構造函數不返回值,但是用戶可以選擇主動返回對象,來覆蓋正常的對象創建步驟)
## 判斷 this
可以按照下面的順序來進行判斷(同時也表明了上述四條規則的優先級):
1.函數是否在 new 中調用(new 綁定)?如果是的話 this 綁定的是新創建的對象
`var bar = new foo()`
2.函數是否通過 call、apply、bind 顯式綁定調用?如果是的話,this 綁定的是指定的對象
`var bar = foo.call(obj2)`
3.函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this 綁定的是那個上下文對象
`var bar = obj.foo()`
4.如果都不是,使用默認綁定。在嚴格模式下會綁定到 undefined,否則綁定到全局對象。
`var bar = foo()`
# 其他注意事項
## 被忽略的 this
如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call、apply 或者 bind,這些值在調用時會被忽略,實際應用的是默認綁定規則:
```js
function foo () {
console.log(this.a)
}
var a = 2
foo.call(null) // 2 瀏覽器環境
```
那么什么情況下會傳入 null 呢?
一般是做函數綁定時如果不關心 this 的話,可以傳入一個 null 作為占位值。
```js
function foo (a, b) {
console.log(`a: ${a} b: ${b}`)
}
foo.apply(null, [2, 3])
var bar = foo.bind(null, 2)
bar(3)
```
## 箭頭函數
箭頭函數的`this`只取決于他外面的第一個不是箭頭函數的函數的`this`
```js
function foo () {
setTimeout(() => {
// 這里的 this 在詞法上繼承自 foo()
console.log(this.a)
}, 100)
}
var obj = {
a: 2
}
foo.call(obj) // 2
```
## 閉包中的 this
每個函數在被調用時都會自動取得兩個特殊變量:this 和 arguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象為止,因此永遠不可能直接訪問外部函數中的這兩個變量
(具體見作用域、作用域鏈、閉包章節)因此,下面的匿名函數是無法取得其包含作用域(或外部作用域)的 this 對象的。
``` js
var name = 'The Window'
var object = {
name: 'My Object',
getNameFunc: function () {
return function () {
return this.name
}
}
}
console.log(object.getNameFunc()()) // 'The Window' 瀏覽器環境 & 非嚴格模式
```
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs