溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

this的四個綁定規(guī)則是什么

發(fā)布時間:2022-11-02 09:38:04 來源:億速云 閱讀:132 作者:iii 欄目:web開發(fā)

這篇文章主要介紹“this的四個綁定規(guī)則是什么”,在日常操作中,相信很多人在this的四個綁定規(guī)則是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”this的四個綁定規(guī)則是什么”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

1. 關于 this 的簡單介紹

this 關鍵字是 JavaScript 中最復雜的機制之一。它是一個很特別的關鍵字,被自動定義在所有函數(shù)的作用域中。但是即使是非常有經驗的 JavaScript 開發(fā)者也很難說清它到底指向什么。

任何足夠先進的技術都和魔法無異?!?Arthur C. Clarke

實際上,JavaScript 中 this 的機制并沒有那么先進,但是開發(fā)者往往會把理解過程復雜化, 毫無疑問,在缺乏清晰認識的情況下,this 對你來說完全就是一種魔法。

2. 為什么使用 this?

const obj = {
  title: '掘金',
  reading() {
    console.log(this.title + ',一個幫助開發(fā)者成長的社區(qū)');
  }
}

this 提供了一種更優(yōu)雅的方式來隱式“傳遞”一個對象引用,因此可以將 API 設計得更加簡潔并且易于復用。

隨著你的使用模式越來越復雜,顯式傳遞上下文對象會讓代碼變得越來越混亂,使用 this 則不會這樣。當我們介紹對象和原型時,你就會明白函數(shù)可以自動引用合適的上下文對象有多重要

3. 關于this 的常見的誤解

人們很容易把 this 理解成指向函數(shù)自身,JavaScript 的新手開發(fā)者通常會認為,既然把函數(shù)看作一個對象(JavaScript 中的所有函數(shù) 都是對象),那就可以在調用函數(shù)時存儲狀態(tài)(屬性的值)。但結果通常讓他們大吃一驚,例如下面這段代碼

function foo() {
  // 讓新添加的 count + 1
  this.count++
}

// 向函數(shù)對象 foo 添加了一個屬性 count
foo.count = 0

foo()

console.log(foo.count);   // 0

這段代碼看起來沒什么問題,但請把目光聚焦到最后一行,輸出 foo.count 的結果竟然是0 ?!

疑問:為什么會這樣?我明明向函數(shù)對象 foo 添加了一個屬性 count,并且函數(shù)內部也寫了 this.count++,為什么最后還是0呢?

解答:因為this.count 中此時的 this,根本不是指向 foo 函數(shù)自身,而是指向全局 window。再細心一點,我們可以發(fā)現(xiàn),在 window 身上,被添加了個 count 屬性,值為 NaN。 (為什么 this 指向 window 后面會闡述)

this的四個綁定規(guī)則是什么

所以,單純的把 this 理解為指向函數(shù)自身是錯誤的。

this 實際上是在函數(shù)被調用時發(fā)生的綁定,它指向什么完全取決于函數(shù)在哪里被調用。

4. this 的綁定規(guī)則

我們來看看在函數(shù)的執(zhí)行過程中調用位置如何決定 this 的綁定對象。

找到調用位置,然后判斷需要應用下面四條規(guī)則中的哪一條。我首先會分別解釋 這四條規(guī)則,然后解釋多條規(guī)則都可用時它們的優(yōu)先級如何排列。

4.1 默認綁定

首先要介紹的是最常用的函數(shù)調用類型:獨立函數(shù)調用??梢园堰@條規(guī)則看作是無法應用其他規(guī)則時的默認規(guī)則。

function foo() {
  console.log(this.a)
}
var a = 2
foo() // 2

  • 我們可以看到當調用 foo() 時,this.a 被解析成了全局變量 a。為什么?因為在本例中,函數(shù)調用時應用了 this 的默認綁定,因此 this 指向全局對象。

  • 那么我們怎么知道這里應用了默認綁定呢?可以通過分析調用位置來看看 foo() 是如何調 用的。在代碼中,foo() 是直接使用不帶任何修飾的函數(shù)引用進行調用的,因此只能使用默認綁定,無法應用其他規(guī)則

  • 這條規(guī)則也解釋了上面 count 的代碼中,為什么函數(shù)里面的this指向了window,因為 foo屬于獨立函數(shù)調用的,觸發(fā)了默認綁定,從而指向全局window。(瀏覽器中全局是 window對象,node 中是空對象{})

  • 屬于默認綁定規(guī)則的還有:

    • 函數(shù)調用鏈(一個函數(shù)又調用另外一個函數(shù))

    • 將函數(shù)作為參數(shù),傳入到另一個函數(shù)中

(擴展:如果使用嚴格模式(strict mode),則不能將全局對象用于默認綁定,因此 this 會綁定到 undefined:)

結論:默認綁定的 this,都指向全局。

4.2 隱式綁定

4.2.1 一般的對象調用

這一條需要考慮的規(guī)則是調用位置是否有上下文對象,或者說是通過某個對象發(fā)起的函數(shù)調用

function foo() {
  console.log(this.a)
}

const obj = {
  a: 2,
  foo: foo
}

// 通過 obj 對象調用 foo 函數(shù)
obj.foo() // 2

  • 調用位置會使用 obj 上下文來引用函數(shù),因此你可以說函數(shù)被調用時 obj 對象“擁 有”或者“包含”它。

  • foo() 被調用時,它的前面確實加上了對 obj 的引用。當函數(shù)引用有上下文對象時,隱式綁定規(guī)則會把函數(shù)調用中的 this 綁定到這個上下文對象。因為調用 foo()this 被綁定到 obj 上,因此 this.aobj.a 是一樣的。

4.2.2 對象屬性引用鏈

對象屬性引用鏈中只有上一層或者說最后一層在調用位置中起作用。舉例來說:

function foo() {
  console.log(this.a)
}

var obj2 = {
  a: 2,
  foo: foo
}

var obj1 = {
  a: 1,
  obj2: obj2
}

obj1.obj2.foo() // 2

最終 this 指向的是 obj2

4.2.3 隱式丟失

一個最常見的 this 綁定問題就是被隱式綁定的函數(shù)會丟失綁定對象,也就是說它會應用默認綁定,從而把 this 綁定到全局對象或者 undefined 上(取決于是否是嚴格模式)

第一種情況:將對象里的函數(shù)賦值給一個變量

function foo() {
  console.log(this.a)
}

var obj = {
  a: 2,
  foo: foo
}

var bar = obj.foo // 函數(shù)別名!

var a = 'global' // a 是全局對象的屬性
bar() // "global"

雖然 barobj.foo 的一個引用,但是實際上,它引用的是 foo 函數(shù)本身,因此此時的 bar() 其實是一個不帶任何修飾的函數(shù)調用,因此應用了默認綁定

第二種情況:傳入回調函數(shù)時

function foo() {
  console.log(this.a)
}

function doFoo(fn) {
  // fn 其實引用的是 foo
  fn() // <-- 調用位置!
}

var obj = {
  a: 2,
  foo: foo
}

var a = 'global'  // a 是全局對象的屬性
doFoo(obj.foo)    // "global"

參數(shù)傳遞其實就是一種隱式賦值,因此我們傳入函數(shù)時也會被隱式賦值,所以結果和上一 個例子一樣。

結論:隱式綁定的 this,指向調用函數(shù)的上下文對象。

4.3 顯式綁定

4.3.1 使用 call(...) 和 apply(...)

如果我們不想在對象內部包含函數(shù)引用,而想在某個對象上強制調用函數(shù),該怎么做呢?可以使用函數(shù)的 call(..)apply(..) 方法

  • JavaScript 提供的絕大多數(shù)函數(shù)以及你自 己創(chuàng)建的所有函數(shù)都可以使用 call(..)apply(..) 方法。

這兩個方法是如何工作的呢?它們的第一個參數(shù)是一個對象,是給 this 準備的,接著在調用函數(shù)時將其綁定到 this。因為你可以直接指定 this 的綁定對象,因此我們稱之為顯式綁定。 思考以下代碼:

function foo() {
  console.log(this.a)
}

var obj = {
  a: 2
}

foo.call(obj) // 2

  • 通過 foo.call(..),我們可以在調用 foo 時強制把它的 this 綁定到 obj 上。

  • 如果你傳入了一個原始值(字符串類型、布爾類型或者數(shù)字類型)來當作 this 的綁定對 象,這個原始值會被轉換成它的對象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..))。這通常被稱為“裝箱”。

從 this 綁定的角度來說,call(..) 和 apply(..) 是一樣的,它們的區(qū)別體現(xiàn)在參數(shù)上:第一個參數(shù)是相同的,后面的參數(shù),call為參數(shù)列表,apply為數(shù)組,他們內部的實現(xiàn)原理也不難理解。

4.3.2 硬綁定(一個函數(shù)總是顯示的綁定到一個對象上)

由于硬綁定是一種非常常用的模式,所以 ES5 提供了內置的方法 Function.prototype.bind, 它的用法如下

function foo(num) {
  console.log(this.a, num)
  return this.a + num
}

var obj = {
  a: 2
}

// 調用 bind() 方法,返回一個函數(shù)
var bar = foo.bind(obj)

var b = bar(3) // 2 3

console.log(b) // 5

調用 bind(...) 方法,會返回一個新函數(shù),那么這個新函數(shù)的 this,永遠指向我們傳入的obj對象

關于 bind 方法的簡單實現(xiàn)。

4.3.3 API調用的 “上下文(內置函數(shù))

第三方庫的許多函數(shù),以及 JavaScript 語言和宿主環(huán)境中許多新的內置函數(shù),都提供了一個可選的參數(shù),通常被稱為“上下文”(context),其作用和 bind(..) 一樣,確保你的回調函數(shù)使用指定的 this。例如:

(1)數(shù)組方法 forEach()

function foo(el) {
  console.log(el, this.id)
}

var obj = {
  id: 'bin'
};

[1, 2, 3].forEach(foo, obj)

// 輸出:
// 1 bin 
// 2 bin 
// 3 bin

  • 調用 foo(..) 時把 this 綁定到 obj 身上

(2)setTimeout()

setTimeout(function() {
  console.log(this); // window
}, 1000);

  • 在使用 setTimeout 時會傳入一個回調函數(shù),而這個回調函數(shù)中的this一般指向 window,這個和 setTimeout 源碼的內部調用有關,這個不再展開贅述

結論:顯式綁定的 this,指向我們指定的綁定對象。

4.4 new 綁定

在 JavaScript 中,普通函數(shù)可以使用 new 操作符去調用,此時的普通函數(shù)則被稱為 “構造函數(shù)”。沒錯,凡是由 new 操作符調用的函數(shù),都稱為 “構造函數(shù)”

使用 new 來調用函數(shù),或者說發(fā)生構造函數(shù)調用時,會自動執(zhí)行下面的操作。

  • 在內存中創(chuàng)建一個新對象。

  • 這個新對象內部的[[Prototype]] 特性被賦值為構造函數(shù)的 prototype 屬性。

  • 構造函數(shù)內部的this 被賦值為這個新對象(即this 指向新對象)。

  • 執(zhí)行構造函數(shù)內部的代碼(給新對象添加屬性)。

  • 如果構造函數(shù)返回非空對象,則返回該對象;否則,返回剛創(chuàng)建的新對象。

function foo(a) {
  this.a = a
}

var bar = new foo(2)

console.log(bar.a) // 2

使用 new 來調用 foo(..) 時,我們會構造一個新對象并把它綁定到 foo(..) 調用中的 this 上。new 是最后一種可以影響函數(shù)調用時 this 綁定行為的方法,我們稱之為 new 綁定

結論:new 綁定的 this,都指向通過 new 調用的函數(shù)的實例對象(就是該函數(shù))

5. 綁定規(guī)則的優(yōu)先級

現(xiàn)在我們已經了解了函數(shù)調用中 this 綁定的四條規(guī)則,你需要做的就是找到函數(shù)的調用位置并判斷應當應用哪條規(guī)則。但是,如果某個調用位置可以應用多條規(guī)則呢?所以就需要有綁定規(guī)則的優(yōu)先級。

它們之間的優(yōu)先級關系為:

默認綁定 < 隱式綁定 < 顯示綁定(bind) < new綁定

這里提前列出優(yōu)先級,想看詳細代碼解析的可以往下看,也可以直接拖到最后面的例題部分

5.1 默認綁定的優(yōu)先級最低

毫無疑問,默認規(guī)則的優(yōu)先級是最低的,因為存在其他規(guī)則時,就會通過其他規(guī)則的方式來綁定this

5.2 隱式綁定和顯式綁定的優(yōu)先級比較

測試一下即可知道,有以下代碼:

function foo() {
  console.log(this.a)
}

var obj1 = {
  a: 1,
  foo: foo
}

var obj2 = {
  a: 2,
  foo: foo
}

// 同時使用隱式綁定和顯示綁定
obj1.foo.call(obj2) // 2

可以看到,輸出的結果為 2,說明 foo 函數(shù)內 this 指向的是 obj2,而 obj2 是通過顯示綁定調用的,所以:顯示綁定的優(yōu)先級更高

5.3 隱式綁定和 new 綁定的優(yōu)先級比較

有以下測試代碼:

function foo() {
  console.log(this);
}

var obj = {
  title: "juejin",
  foo: foo
}

// 同時使用隱式綁定和new綁定
new obj.foo(); // foo對象

最后 foo 函數(shù)輸出的 this 為 foo 對象,說明new綁定優(yōu)先級更高(否則應該輸出 obj 對象),所以:new 綁定的優(yōu)先級更高

5.4 new 綁定和顯示綁定的優(yōu)先級比較

最后,new 綁定和顯式綁定誰的優(yōu)先級更高呢?

new綁定和call、apply是不允許同時使用的,只能和 bind 相比較,如下:

function foo() {
  console.log(this)
}

var obj = {
  title: "juejin"
}

var foo = new foo.call(obj);  // 直接報錯

但是 new 綁定可以和 bind 方法返回后的函數(shù)一起使用

function foo() {
  console.log(this);
}

var obj = {
  title: "juejin"
}

var bar = foo.bind(obj);

var foo = new bar(); // foo 對象, 說明使用的是new綁定

最后 foo 函數(shù)輸出的 this 為 foo 對象,說明new綁定優(yōu)先級更高(否則應該輸出 obj 對象),所以:new 綁定的優(yōu)先級更高

優(yōu)先級結論:默認綁定 < 隱式綁定 < 顯示綁定(bind)< new綁定

6. 判斷this

現(xiàn)在我們可以根據(jù)優(yōu)先級來判斷函數(shù)在某個調用位置應用的是哪條規(guī)則。可以按照下面的 順序來進行判斷:

  • 函數(shù)是否在 new 中調用(new 綁定)?如果是的話 this 綁定的是新創(chuàng)建的對象。 var bar = new foo()

  • 函數(shù)是否通過 call、apply(顯式綁定)或者硬綁定調用?如果是的話,this 綁定的是 指定的對象。 var bar = foo.call(obj2)

  • 函數(shù)是否在某個上下文對象中調用(隱式綁定)?如果是的話,this 綁定的是那個上 下文對象。 var bar = obj1.foo()

  • 如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到 undefined,否則綁定 到全局對象。 var bar = foo()

就是這樣。對于正常的函數(shù)調用來說,理解了這些知識你就可以明白 this 的綁定原理了。 不過……凡事總有例外

7. 綁定例外

規(guī)則總有例外,這里也一樣。在某些場景下 this 的綁定行為會出乎意料,你認為應當應用其他綁定規(guī)則時,實際上應用的可能是默認綁定規(guī)則。

7.1 箭頭函數(shù)

箭頭函數(shù)不使用 this 的四種標準規(guī)則,而是根據(jù)外層(函數(shù)或者全局)作用域來決定 this。

7.2 被忽略的this

如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call、apply 或者 bind,這些值在調用時會被忽略,實際應用的是默認綁定規(guī)則:

function foo() {
  console.log(this.a)
}

var a = 2

foo.call(null) // 2
foo.call(undefined) // 2
foo.bind(null)();

最后輸出的結果都是 2,說明 this 指向的是全局 window

7.3 間接引用

另一個需要注意的是,你有可能(有意或者無意地)創(chuàng)建一個函數(shù)的間接引用,在這種情況下,調用這個函數(shù)會應用默認綁定規(guī)則。 間接引用最容易在賦值時發(fā)生:

function foo() {
  console.log(this.a)
}
var a = 2
var o = { a: 3, foo: foo }
var p = { a: 4 }

o.foo(); // 3

// 函數(shù)賦值
(p.foo = o.foo)()  // 2

賦值表達式 p.foo = o.foo 的返回值是目標函數(shù)的引用,因此調用位置是 foo() 屬于獨立函數(shù)調用,而不是 p.foo() 或者 o.foo()。根據(jù)我們之前說過的,這里會應用默認綁定。

8. this 判斷例題

請說出例題中的輸出結果

8.1 例題一

var name = "window";
var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};
function sayName() {
  var sss = person.sayName;
  sss(); 
  person.sayName(); 
  (person.sayName)(); 
  (b = person.sayName)(); 
}
sayName();

解析:

function sayName() {
  var sss = person.sayName;
  // 獨立函數(shù)調用,沒有和任何對象關聯(lián)
  sss(); // window
  // 關聯(lián)
  person.sayName(); // person
  (person.sayName)(); // person
  (b = person.sayName)(); // window
}

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.foo1.call(person2); 

person1.foo2();
person1.foo2.call(person2);

person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);

person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);

解析:

// 隱式綁定,肯定是person1
person1.foo1(); // person1
// 隱式綁定和顯示綁定的結合,顯示綁定生效,所以是person2
person1.foo1.call(person2); // person2

// foo2()是一個箭頭函數(shù),不適用所有的規(guī)則
person1.foo2() // window
// foo2依然是箭頭函數(shù),不適用于顯示綁定的規(guī)則
person1.foo2.call(person2) // window

// 獲取到foo3,但是調用位置是全局作用于下,所以是默認綁定window
person1.foo3()() // window
// foo3顯示綁定到person2中
// 但是拿到的返回函數(shù)依然是在全局下調用,所以依然是window
person1.foo3.call(person2)() // window
// 拿到foo3返回的函數(shù),通過顯示綁定到person2中,所以是person2
person1.foo3().call(person2) // person2

// foo4()的函數(shù)返回的是一個箭頭函數(shù)
// 箭頭函數(shù)的執(zhí)行找上層作用域,是person1
person1.foo4()() // person1
// foo4()顯示綁定到person2中,并且返回一個箭頭函數(shù)
// 箭頭函數(shù)找上層作用域,是person2
person1.foo4.call(person2)() // person2
// foo4返回的是箭頭函數(shù),箭頭函數(shù)只看上層作用域
person1.foo4().call(person2) // person1

到此,關于“this的四個綁定規(guī)則是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關知識,請繼續(xù)關注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI