JavaScript高級深入淺出:掌握 this 指向

alexzhang1030 2021-11-25 18:14:06
javascript 深入 掌握 指向

介紹

本文是 JavaScript 高級深入淺出系列的第六篇,詳細介紹了 JS 中的 this 指向

正文

1. 為什麼需要 this

在常見的編程語言中,幾乎都會有this關鍵字(Objective-C中是self)。但是 JS 中的 this 和常見的面向對象的 this 不同:

  • 常見的面向對象的編程語言中:this 通常出現在類的方法中(特別是實例方法中),指的是當前的調用對象
  • 但是 JS 中的 this 更加靈活,無論是它出現的比特置還是代錶的含義
const foo = {
name: "foo",
greeting() {
// 在實際的開發中,想要獲取當前對象中的屬性,有沒有 this 其實都是可以的
// 但是如果不使用 this ,那麼在開發中將會非常的不方便
console.log(`Hello, My name is ${this.name}`)
console.log(`Hello, My name is ${foo.name}`)
}
}
複制代碼

2. this 的指向問題

2.1 在全局環境中

// 在大多數情况下,this 都是在類中使用,但是全局其實也可以訪問 this
console.log(this)
複制代碼
  • 瀏覽器環境下,全局this指向 GlobalObject,也就是window
  • Node 環境下,全局this指向一個空對象{}

2.2 在函數中

在開發中,我們一般不再全局使用 this,通常是在函數中使用。

  • 所有的函數在調用時,都會創建一個函數執行上下文
  • 這個上下文中記錄著函數的調用棧,AO對象等
  • this 也存在與這個上下文中
function foo() {
console.log(this)
}
// 1. 全局調用
foo() // window
const bar = {
name: `bar's name`,
foo,
}
// 2. 放在對象中使用
bar.foo() // bar 本身
// 3. 通過 apply 調用
foo.apply('abc') // abc
複制代碼

同一個函數,調用的方式不同,那麼 this 就不同。說明:this 指向和函數的比特置是沒有關系的,和函數被調用的方式有關系

  1. 函數在調用時, JS 會給函數一個默認的 this 值
  2. this 的綁定和函數所處的比特置是沒有關系的
  3. this 的綁定和調用方式以及調用的比特置有關系
  4. this 是在運行時綁定的

3. this 的綁定規則

3.1 默認綁定

獨立調用函數的時候,綁定規則是默認綁定,獨立調用函數,就會指向全局。

獨立調用函數我們可以理解為函數沒有被綁定在某個對象上調用

function foo1() {
console.log(this)
}
function foo2() {
console.log(this)
foo1()
}
function foo3() {
console.log(this)
foo2()
}
foo3() // 獨立調用函數,3個函數的 this 均指向 window
複制代碼
// 案例二
var obj = {
name: `obj's name`,
foo() {
console.log(this)
}
}
var bar = obj.foo
bar() // 獨立調用,還是 window
複制代碼
// 案例三
function foo() {
return function() {
console.log(this)
}
}
var obj = {
name: `obj's name`,
foo
}
var bar = obj.foo()
bar() // 獨立調用,this 一定會是 window
複制代碼

3.2 隱式綁定

通過某個對象調用的可以觸發隱式綁定,隱式綁定,this 指向調用該函數的對象本身。也就是說它的調用比特置中,是通過某個對象發起的函數調用

// 案例
const obj = {
name: `obj's name`,
greeting() {
console.log(`my name is ${this.name}`)
}
}
obj.greeting() // 這裏的 this 就是 obj 本身
複制代碼
// 案例二
const obj1 = {
name: `obj1's name`,
greeting() {
console.log(`my name is ${this.name}`)
}
}
const obj2 = {
name: `obj2's name`,
greeting: obj1.greeting
}
obj2.greeting() // 這裏的 this 綁定的是 obj2
複制代碼

3.3 顯式綁定

隱式綁定有一個前提條件:

  • 必須在調用的對象內部有一個對函數的引用(比如一個屬性)obj.foo = function() { console.log(this) }
  • 如果沒有這樣的引用,在進行調用時,就無法找到此對象中的函數,也就無法執行此代碼
  • 正是因為這個引用,間接的將this綁定到了調用函數的對象上

如果我們不希望在該對象上有這樣的某個屬性,但是又希望函數的 this 指向的時這個對象,這個時候需要顯式綁定:

  • JS 中所有的函數都可以使用callapply
    • callapply的區別在於,call 後面的參數是單獨的,apply 後面的參數是一個數組
  • 這兩個函數的第一個參數都要求傳入一個對象,該對象就是修改函數的 this 所使用的
  • call 和 apply 在執行函數時,是可以明確綁定 this,這個綁定規則稱之為顯式綁定
function foo() {
console.log(this)
}
foo() // 直接調用,觸發默認綁定,this => window
const obj = {
name: `obj's name`
}
// 使用 call 方法,第一個參數就修改了此函數內部的 this 指向
foo.call(obj) // { name: `obj's name` } 指向 obj 本身
複制代碼

call 和 apply 的區別

function sum(num1, num2) {
console.log(num1 + num2, this)
}
sum(10, 20) // 30, window
const obj = {
name: `obj'name`,
}
// 除了第一個參數,後面的參數就是傳入函數的參數
// 區別在於,call的參數需要一個一個的
sum.call(obj, 10, 20)
// 但是 apply 的參數是一個數組
sum.apply(obj, [10, 20])
複制代碼

bind

call 和 apply 沒有返回值

function foo() {
console.log(this)
}
// 多次調用很麻煩
foo.call('aaa')
foo.call('aaa')
foo.call('aaa')
複制代碼

bind返回一個綁定了 this 的新函數

function foo() {
console.log(this)
}
// 此時 newFoo 就是已經將 this 修改為 "bar" 的新函數了
const newFoo = foo.bind('bar')
newFoo() // "bar"
複制代碼

3.4 new 綁定

JS 中的函數可以當作一個類的構造函數來使用,可以使用new關鍵字

使用 new 關鍵字時,有以下過程:

  1. 創建一個全新的對象
  2. 這個新對象會被執行 prototype 連接
  3. 這個新對象會綁定到函數調用的 this 上(this 的綁定在這個步驟中完成)
  4. 如果函數沒有返回其他對象,錶達式會返回這個新對象

new綁定時,this = 創建的新對象(也就是我們常說的實例對象)

4. 綁定規則的優先級

  • 默認規則的優先級最低
  • 顯式綁定優先級高於隱式綁定
  • new 綁定優先級高於隱式綁定
  • new 綁定優先級高於 bind
    • new 綁定和 call、apply 是不允許同時使用的,因此不存在沖突問題
    • new 綁定可以同時用 call,但是 new 高於 call

new > 顯式綁定 > 隱式綁定 > 默認綁定

5. 內置函數的 this 分析

定時器

setTimeout/setInterval

setTimeout(function() {
console.log(this) // window
}, 1000)
複制代碼

DOM 事件

const div = document.querySelector('.box')
div.addEventListener('click', function() {
console.log(this) // div 元素本身
})
複制代碼

數組內置函數

const names = ['Alex', 'John', 'Tom']
names.forEach(function(item) {
console.log(item, this) // window
})
// forEach 默認的 this 是 window
// 但是可以傳入第二個參數,第二個參數可以修改函數中的值
names.forEach(function(item) {
console.log(item, this) // "aaa"
}, 'aaa')
// map、filter 等數組內置函數 this 都默認是 window,都可以傳入第二個參數,修改內部 this 的指向
複制代碼

6. this 的特殊綁定

之前的 4 個規則已經能够應對日常的開發,但是總會有一些語法會跳出規則之外

6.1 忽略顯式綁定

function foo() {
console.log(this)
}
foo.apply("aaa") // aaa 沒問題
foo.apply(null) // window
foo.apply(undefined) // window
複制代碼

如果顯式綁定(apply、call、bind 都是這樣)傳入指向為null/undefined,this 將自動被綁定為全局對象

6.2 間接函數引用

const obj1 = {
name: 'obj1',
foo() {
console.log(this)
},
}
const obj2 = {
name: 'obj2',
}
obj2.bar = obj1.foo
obj2.bar() // this 指向 obj2
// 這裏記得加一個分號,不然小括號在進行詞法分析中將和上文的聲明 obj2 視為一個整體
;(obj2.bar = obj1.foo)() // 這裏視為一個單獨的函數調用,this 指向 window
複制代碼

這種寫法最好不要在生產環境中使用

7. 箭頭函數中的 this 指向

7.1 什麼是箭頭函數

箭頭函數是 ES6 之後的一種聲明函數的方法

  • 箭頭函數不會綁定 this 、arguments 屬性
  • 箭頭函數不能作為構造函數使用(不能和 new 一起使用,會拋出錯誤)

箭頭函數如何編寫:

  • () 參數,如果只有一個參數可以省略 ()
  • => 箭頭
  • {} 函數體,如果函數體內只有一行代碼,則會視為該行代碼的結果為返回值,同時這種情况下可以省略 {}
() => {}
num => { // do something... }
const sum = (num1, num2) => num1 + num2
console.log(sum(10, 20)) // 30
// 箭頭函數使用例子:需求:將數組中所有的偶數 * 100後相加
const nums = [2, 12, 24, 35, 48]
const res = nums
.filter(num => num % 2 === 0)
.map(num => num * 100)
.reduce((prev, curr) => prev + curr)
console.log(res)
複制代碼

7.2 箭頭函數簡寫

  • 如果只有一個參數,可以省略 (),沒有參數不能省略 ()
  • 如果只有一行代碼,可以省略 {},同時這行代碼的計算結果將作為返回值
  • 如果返回的是一個對象,還想要省略 {} 的話,需要在外層使用 () 包裹
// 這種寫法,引擎不知道 {} 到底是對象字面量還是函數執行體
const foo = () => { name: 'alex', age: 18 }
// 需要這樣
const foo = () => ({ name: 'alex', age: 18 })
複制代碼

7.3 箭頭函數中的 this 指向

箭頭函數不根據上文中的 4 中綁定規則來確定 this 指向,而是根據外層作用域去確定 this

const foo = function() {
console.log(this.message)
}
const obj = {
message: 'alex',
}
// 一個正常的函數,是根據 4 種綁定規則來確定 this 指向的
foo.call(obj) // alex
複制代碼
window['message'] = 'global message'
const bar = () => {
console.log(this.message)
}
const obj = {
message: `obj's message`,
}
// 但是箭頭函數是跟著外層作用域的 this 走的
bar.call(obj) // global message
複制代碼

看一個案例

const obj = {
message: `obj's name`,
foo: function() {
return () => {
console.log(this.message)
}
},
}
const obj2 = {
message: `obj2' name`,
}
obj.foo().call(obj2) // 最終打印 obj's name 
// 雖然使用 call 顯式修改了 this 指向,但是由於箭頭函數的 this 跟著外層作用域走,而 foo 函數的 this 是 obj,最終箭頭函數的 this 其實也是 obj,而不是 obj2
複制代碼

應用場景

const obj = {
data: [],
fetchData() {
// ES6之前沒有箭頭函數的解决方案
// var _this = this 。以下文 _this 代指 this,即可訪問到 obj
setTimeout(function() {
// 這段代碼其實是有問題的
// 因為 setTimeout 的 this 指向是 window
// 所以無法訪問到期望的 this 也就是 obj 
console.log('fetched Data !!!')
this.data = [1, 2, 3]
this.getData()
}, 3000)
},
getData() {
console.log('data is', this.data)
},
}
obj.fetchData()
複制代碼
// 將代碼修改為箭頭函數
const obj = {
data: [],
fetchData() {
// 由於箭頭函數的 this 指向是外層作用域的 this,因為這裏 fetchData 的 this 指向 obj,所以箭頭函數定時器回調函數就可以成功訪問到 obj 
setTimeout(() => {
console.log('fetched Data !!!')
this.data = [1, 2, 3]
this.getData()
}, 3000)
},
getData() {
console.log('data is', this.data)
},
}
obj.fetchData()
複制代碼

8. 面試題 this 指向案例

8.1 第一題

var name = 'window'
var person = {
name: 'person',
sayName: function() {
console.log(this.name)
},
}
function sayName() {
var sss = person.sayName
sss() // window
person.sayName() // person
(person.sayName)() // person
;(b = person.sayName)() // window,上文中說的間接函數引用,這裏相當於就是一個單獨的函數調用
}
sayName()
複制代碼

這一題還是比較簡單的,直接根據 4 種綁定規則去判斷就可以,默認綁定和隱式綁定,唯一的一個難點在於(person.sayName)(),這個可以看作是person.sayName(),所以是person

8.2 第二題

var name = 'window'
var person1 = {
name: 'person1',
foo1: function() {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function() {
return function() {
console.log(this.name)
}
},
foo4: function() {
return () => {
console.log(this.name)
}
},
}
var person2 = { name: 'person2' }
person1.foo1() // person1,隱式綁定
person1.foo1.call(person2) // person2 顯式綁定優先級大於隱式綁定,所以是 person2
person1.foo2() // window 箭頭函數不綁定 this,因此繼承 person1 的上層作用域就是全局作用域
person1.foo2.call(person2) // window 雖然綁定了 foo2 的 this 給了 person2,但是還是繼承了 person2 上層的作用域,也就是全局
person1.foo3()() // window 默認綁定,因為 person1.foo() 返回一個函數,接著就單獨調用此函數,所以是 window
person1.foo3.call(person2)() // 還是 window,這個和上面的一樣
person1.foo3().call(person2) // person2,這裏把返回的函數顯式綁定了 this 給 person2
person1.foo4()() // person1,箭頭函數不綁定 this,因此繼承 foo4 的 this
person1.foo4.call(person2)() // person2,這裏把 foo4 的 this 顯式綁定給了 person2,因此是 person2
person1.foo4().call(person2) // person1,同是箭頭函數不綁定 this
複制代碼

8.3 第三題

這道題一定要注意,和上一題不一樣,要好好看解析

var name = 'window'
function Person(name) {
this.name = name
this.foo1 = function() {
console.log(this.name)
}
this.foo2 = () => console.log(this.name)
this.foo3 = function() {
return function() {
console.log(this.name)
}
}
this.foo4 = function() {
return () => {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.foo1() // person1(隱式綁定)
person1.foo1.call(person2) // person2(顯式綁定高於隱式綁定)
person1.foo2() // person1 這裏需要說一嘴,雖然 foo2 是一個箭頭函數,但是在聲明函數的時候使用的是 this.xx 所以上層作用域就找到了 this,也就是實例對象 person1
person1.foo2.call(person2) // person1 箭頭函數不綁定 this,繼承於 this.foo2 的作用域,即this
person1.foo3()() // window,這裏還是 person1.foo3() 返回一個函數,然後緊接著單獨調用這個函數,適用於默認綁定
person1.foo3.call(person2)() // window,和上一個一樣
person1.foo3().call(person2) // person2,這裏是將返回的函數的 this 顯式綁定給了 person2
person1.foo4()() // person1,箭頭函數不綁定 this,繼承上層,也就是 this
person1.foo4.call(person2)() // person2,這裏是將 foo4 的作用域綁定給了 person2
person1.foo4().call(person2) // person1,箭頭函數不綁定 this,雖然顯式綁定了,但是還是繼承的是 person1.foo4 的this
複制代碼

8.4 第四題

var name = 'window'
function Person(name) {
this.name = name
this.obj = {
name: 'obj',
foo1: function() {
return function() {
console.log(this.name)
}
},
foo2: function() {
return () => {
console.log(this.name)
}
},
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.obj.foo1()() // window,foo1 函數返回一個函數,緊接著獨立調用此函數
person1.obj.foo1.call(person2)() // window,還是獨立調用此函數
person1.obj.foo1().call(person2) // person2,這裏將返回的函數內部的 this 顯式綁定給了 person2
person1.obj.foo2()() // obj,箭頭函數不綁定 this,將繼承 foo2 的 this 也就是 obj
person1.obj.foo2.call(person2)() // person2,這裏將 foo2 的 this 顯式綁定給了 person2
person1.obj.foo2().call(person2) // obj,同箭頭函數不綁定 this,將繼承 foo2 也就是 obj
複制代碼

總結

本文中,你學習到了 5 個知識點:

  • 為什麼需要 this:提高開發效率
  • this 的指向問題:this 在運行時動態綁定
  • this 的綁定規則:4 種綁定規則,但是箭頭函數並不適用於這四種,而是繼承於外層作用域的 this
  • 綁定規則的優先級:new 綁定 > 顯式綁定 > 隱式綁定 > 默認綁定
  • this 的特殊綁定:某種情况下(開發中極少極少會用到),this 的綁定時很特殊的
版权声明
本文为[alexzhang1030]所创,转载请带上原文链接,感谢
https://javamana.com/2021/11/20211125181405546j.html

  1. MySQL Learning - Logging System Redo log and Bin log
  2. Springboot Common comments | @ configuration
  3. Mécanisme d'expiration du cache redis et d'élimination de la mémoire
  4. Analyse concise du code source redis 01 - configuration de l'environnement
  5. Redis source Concise Analysis 02 - SDS String
  6. Spring cloud gateway practice 2: more routing configuration methods
  7. Principe de mise en œuvre ultime du mécanisme de concurrence Java sous - jacent
  8. [démarrer avec Java 100 exemples] 13. Modifier l’extension de fichier - remplacement de chaîne
  9. Java期末作业——王者荣耀的洛克王国版游戏
  10. Elasticsearch聚合学习之五:排序结果不准的问题分析,阿里巴巴java性能调优实战
  11. Java期末作業——王者榮耀的洛克王國版遊戲
  12. Java final work - King's Glory Rock Kingdom Game
  13. 【网络编程】TCP 网络应用程序开发
  14. 【网络编程入门】什么是 IP、端口、TCP、Socket?
  15. 【網絡編程入門】什麼是 IP、端口、TCP、Socket?
  16. [Introduction à la programmation réseau] qu'est - ce que IP, port, TCP et socket?
  17. [programmation réseau] développement d'applications réseau TCP
  18. [Java Basics] comprendre les génériques
  19. Dix outils open source que les architectes de logiciels Java devraient maîtriser!!
  20. Java经典面试题详解,突围金九银十面试季(附详细答案,mysql集群架构部署方案
  21. java架构之路(多线程)synchronized详解以及锁的膨胀升级过程,mysql数据库实用教程pdf
  22. java整理,java高级特性编程及实战第一章
  23. java教程——反射,mongodb下载教程
  24. Java岗大厂面试百日冲刺 - 日积月累,每日三题【Day12,zookeeper原理作用
  25. Java后端互联网500道中高级面试题(含答案),linux钩子技术
  26. java8 Stream API及常用方法,java初级程序员面试
  27. java-集合-Map(双列)——迪迦重制版,2021Java开发社招面试解答之性能优化
  28. Flink处理函数实战之二:ProcessFunction类,java线程面试题目
  29. flex 布局详解,【Java面试题
  30. Linux basic command learning
  31. Why did docker lose to kubernetes? Docker employee readme!
  32. MySQL安装
  33. Elastic Search Aggregate Learning five: Problem Analysis of Uncertainty of sequencing results, Alibaba Java Performance Tuning Practical
  34. Installing, configuring, starting and accessing rabbitmq under Linux
  35. Oracle SQL injection summary
  36. Installation MySQL
  37. L'exposition à la photo d'essai sur la route i7 du nouveau vaisseau amiral de BMW Pure Electric a également été comparée à celle de Xiaopeng p7.
  38. spring JTA 关于异常处理的时机问题
  39. Le problème du temps de traitement des exceptions dans la JTA printanière
  40. Flink Handling Function Real War II: processfunction class, Java thread interview subject
  41. Oracle SQL injection summary
  42. [Java data structure] you must master the classic example of linked list interview (with super detailed illustration and code)
  43. Do you really know MySQL order by
  44. Record a java reference passing problem
  45. spring JTA 關於异常處理的時機問題
  46. Java - Set - Map (double file) - dija Rewriting, 2021 Java Developer's Performance Optimization
  47. Android入门教程 | OkHttp + Retrofit 取消请求的方法
  48. Java 8 Stream API and common methods, Java Junior Program interview
  49. Github 疯传!史上最强!BAT 大佬,2021年最新Java大厂面试笔试题分享
  50. git(3)Git 分支,zookeeper下载教程
  51. Java Backend Internet 500 questions d'entrevue moyennes et avancées (y compris les réponses), technologie de crochet Linux
  52. Entretien d'entretien d'usine Java post sprint de 100 jours - accumulation de jours et de mois, trois questions par jour [jour 12, fonction de principe de Zookeeper
  53. Tutoriel Java - reflection, tutoriel de téléchargement mongodb
  54. How to analyze several common key and hot issues in redis from multiple dimensions
  55. GIT (3) GIT Branch, Zookeeper Download tutoriel
  56. Tutoriel de démarrage Android | okhttp + Retrofit comment annuler une demande
  57. Design pattern [3.3] - Interpretation of cglib dynamic agent source code
  58. Share the actual operation of private collection project nodejs backend + Vue + Mysql to build a management system
  59. Springboot has 44 application initiators
  60. GitHub上标星2,java项目开发实训教程