詳細講解JavaScript中的this綁定_javascript技巧

來源:互聯網
上載者:User

this 可以說是 javascript 中最耐人尋味的一個特性,就像高中英語裡各種時態,比如被動時態,過去時,現在時,過去進行時一樣,無論弄錯過多少次,下一次依然可能弄錯。本文啟發於《你不知道的JavaScript上卷》,對 javasript 中的 this 進行一個總結。

學習 this 的第一步就是明白 this 既不是指向函數自身也不指向函數的範圍。this 實際上是在函數被調用時發生的綁定,它指向什麼地方完全取決於函數在哪裡被調用。

預設綁定

在 javascript 中 ,最常用的函數調用類型就是獨立函數調用,因此可以把這條規則看作是無法應用其他規則時的預設規則。如果在調用函數的時候,函數不帶任何修飾,也就是“光禿禿”的調用,那就會應用預設綁定規則, 預設綁定的指向的是全域範圍。

function sayLocation() { console.log(this.atWhere)}var atWhere = "I am in global"sayLocation() // 預設綁定,this綁定在全域對象,輸出 “I am in global”

再看一個例子

var name = "global"function person() { console.log(this.name) // (1) "global"  person.name = 'inside' function sayName() {  console.log(this.name) // (2) "global" 不是 "inside" } sayName() // 在person函數內部執行sayName函數,this指向的同樣是全域的對象}person()

在這個例子中,person 函數在全域範圍中被調用,因此第(1)句中的 this 就綁定在了全域對象上(在瀏覽器中是是window,在node中就是global),因此第(1)句自然輸出的是一個全域對象的 name 屬性,當然就是"global"了。sayName函數在person函數內調用,即使這樣第(2)句中的this指代的仍然是全域對象,即使 person 函數設定了 name 屬性。

這就是預設綁定規則,它是 javascript 中最常見的一種函數調用模式,this 的綁定規則也是四種綁定規則中最簡單的一種,就是綁定在全域範圍上。

預設綁定裡的strict 模式

在 javascript 中,如果使用了strict 模式,則 this 不能綁定到全域對象。還是以第一個例子,只不過這次加上了strict 模式聲明

'use strict'function sayLocation() { console.log(this.atWhere)}var atWhere = "I am in global"sayLocation()// Uncaught TypeError: Cannot read property 'atWhere' of undefined

可以看出,在strict 模式下,把 this 綁定到全域對象上時,實際上綁定的是 undefined ,因此上面這段代碼會報錯。

隱式綁定

當函數在調用時,如果函數有所謂的“落腳點”,即有內容物件時,隱式綁定規則會把函數中的 this 綁定到這個內容物件。如果覺得上面這段話不夠直白的話,還是來看代碼。

function say() { console.log(this.name)}var obj1 = { name: "zxt", say: say}var obj2 = { name: "zxt1", say: say}obj1.say() // zxtobj2.say() // zxt1

很簡單是不是。在上面這段代碼中,obj1 , obj2 就是所謂的 say 函數的落腳點,專業一點的說法就是內容物件,當給函數指定了這個內容物件時,函數內部的this 自然指向了這個內容物件。這也是很常見的一種函數調用模式。

隱式綁定時丟失上下文

function say() { console.log(this.name)}var name = "global"var obj = { name: "inside", say: say}var alias = obj.say // 設定一個簡寫 (1) alias() // 函數調用 輸出"global" (2)

可以看到這裡輸出的是 ”global“ ,為什麼就和上例中不一樣,我們明明只是給 obj.say 換了個名字而已?
首先我們來看上面第(1)句代碼,由於在 javascript 中,函數是對象,對象之間是引用傳遞,而不是值傳遞。因此,第(1)句代碼只是 alias = obj.say = say ,也就是 alias = say ,obj.say 只是起了一個橋樑的作用,alias 最終引用的是 say 函數的地址,而與 obj 這個對象無關了。這就是所謂的”丟失上下文“。最終執行 alias 函數,只不過簡單的執行了say函數,輸出"global"。

顯式綁定

顯式綁定,顧名思義,顯示地將this綁定到一個上下文,javascript中,提供了三種顯式綁定的方法,apply,call,bind。apply和call的用法基本相似,它們之間的區別是:

apply(obj,[arg1,arg2,arg3,...] 被調用函數的參數以數組的形式給出
call(obj,arg1,arg2,arg3,...) 被調用函數的參數依次給出
而bind函數執行後,返回的是一個新函數。下面以代碼說明。

// 不帶參數function speak() {  console.log(this.name)}var name = "global"var obj1 = {  name: 'obj1'}var obj2 = {  name: 'obj2'}speak() // global 等價於speak.call(window)speak.call(window)speak.call(obj1) // obj1speak.call(obj2) // obj2

因此可以看出,apply, call 的作用就是給函數綁定一個執行內容,且是顯式綁定的。因此,函數內的this自然而然的綁定在了 call 或者 apply 所調用的對象上面。

// 帶參數function count(num1, num2) {  console.log(this.a * num1 + num2)}var obj1 = {  a: 2}var obj2 = {  a: 3}count.call(obj1, 1, 2) // 4count.apply(obj1, [1, 2]) // 4count.call(obj2, 1, 2) // 5count.apply(obj2, [1, 2]) // 5

上面這個例子則說明了 apply 和 call 用法上的差異。
而 bind 函數,則返回一個綁定了指定的執行內容的新函數。還是以上面這段代碼為例

// 帶參數function count(num1, num2) {  console.log(this.a * num1 + num2)}var obj1 = {  a: 2}var bound1 = count.bind(obj1) // 未指定參數bound1(1, 2) // 4var bound2 = count.bind(obj1, 1) // 指定了一個參數bound2(2) // 4var bound3 = count.bind(obj1, 1, 2) // 指定了兩個參數bound3() //4var bound4 = count.bind(obj1, 1, 2, 3) // 指定了多餘的參數,多餘的參數會被忽略bound4() // 4

所以,bind 方法只是返回了一個新的函數,這個函數內的this指定了執行內容,而返回這個新函數可以接受參數。

new 綁定

最後要講的一種 this 綁定規則,是指通過 new 操作符調用建構函式時發生的 this 綁定。首先要明確一點的是,在 javascript 中並沒有其他語言那樣的類的概念。建構函式也僅僅是普通的函數而已,只不過建構函式的函數名以大寫字母開頭,也只不過它可以通過 new 操作符調用而已.

function Person(name,age) {  this.name = name  this.age = age  console.log("我也只不過是個普通函數")}Person("zxt",22) // "我也只不過是個普通函數"console.log(name) // "zxt"console.log(age) // 22var zxt = new Person("zxt",22) // "我也只不過是個普通函數"console.log(zxt.name) // "zxt"console.log(zxt.age) // 22

上面這個例子中,首先定義了一個 Person 函數,既可以普通調用,也可以以建構函式的形式的調用。當普通調用時,則按照正常的函數執行,輸出一個字串。 如果是通過一個new操作符,則構造了一個新的對象。那麼,接下來我們再看看兩種調用方式, this 分別綁定在了何處首先普通調用時,前面已經介紹過,此時應用預設綁定規則,this綁定在了全域對象上,此時全域對象上會分別增加 name 和 age 兩個屬性。當通過new操作符調用時,函數會返回一個對象,從輸出結果上來看 this 對象綁定在了這個返回的對象上。
因此,所謂的new綁定是指通過new操作符來調用函數時,會產生一個新對象,並且會把建構函式內的this綁定到這個對象上。
事實上,在javascript中,使用new來調用函數,會自動執行下面的操作。

  1. 建立一個全新的對象
  2. 這個新對象會被執行原型串連
  3. 這個新對象會綁定到函數調用的this
  4. 如果函數沒有返回其他對象,那麼new運算式中的函數調用會自動返回這個新對象

四種綁定的優先順序

上面講述了javascript中四種this綁定規則,這四種綁定規則基本上涵蓋了所有函數調用情況。但是如果同時應用了這四種規則中的兩種甚至更多,又該是怎麼樣的一個情況,或者說這四種綁定的優先順序順序又是怎麼樣的。
首先,很容易理解,預設綁定的優先順序是最低的。這是因為只有在無法應用其他this綁定規則的情況下,才會調用預設綁定。那隱式綁定和顯式綁定呢?還是上代碼吧,代碼可從來不會說謊。

function speak() {  console.log(this.name)}var obj1 = {  name: 'obj1',  speak: speak}var obj2 = {  name: 'obj2'}obj1.speak() // obj1 (1)obj1.speak.call(obj2) // obj2 (2)

所以在上面代碼中,執行了obj1.speak(),speak函數內部的this指向了obj1,因此(1)處代碼輸出的當然就是obj1,但是當顯式綁定了speak函數內的this到obj2上,輸出結果就變成了obj2,所有從這個結果可以看出顯式綁定的優先順序是要高於隱式綁定的。事實上我們可以這麼理解obj1.speak.call(obj2)這行代碼,obj1.speak只是間接獲得了speak函數的引用,這就有點像前面所說的隱式綁定丟失了上下文。好,既然顯式綁定的優先順序要高於隱式綁定,那麼接下來再來比較一下new 綁定和顯式綁定。

function foo(something) {  this.a = something}var obj1 = {}var bar = foo.bind(obj1) // 返回一個新函數bar,這個新函數內的this指向了obj1 (1)bar(2) // this綁定在了Obj1上,所以obj1.a === 2console.log(obj1.a)var baz = new bar(3) // 調用new 操作符後,bar函數的this指向了返回的新執行個體baz (2)console.log(obj1.a)console.log(baz.a) 

我們可以看到,在(1)處,bar函數內部的this原本指向的是obj1,但是在(2)處,由於經過了new操作符調用,bar函數內部的this卻重新指向了返回的執行個體,這就可以說明new 綁定的優先順序是要高於顯式綁定的。
至此,四種綁定規則的優先順序排序就已經得出了,分別是

new 綁定 > 顯式綁定 > 隱式綁定 > 預設綁定

箭頭函數中的this綁定

箭頭函數是ES6裡一個重要的特性。
箭頭函數的this是根據外層的(函數或者全域)範圍來決定的。函數體內的this對象指的是定義時所在的對象,而不是之前介紹的調用時綁定的對象。舉一個例子

var a = 1var foo = () => {  console.log(this.a) // 定義在全域對象中,因此this綁定在全域範圍}var obj = {  a: 2}foo() // 1 ,在全域對象中調用foo.call(obj) // 1,顯示綁定,由obj對象來調用,但根本不影響結果

從上面這個例子看出,箭頭函數的 this 強制性的綁定在了箭頭函數定義時所在的範圍,而且無法通過顯示綁定,如apply,call方法來修改。在來看下面這個例子

// 定義一個建構函式function Person(name,age) {  this.name = name  this.age = age   this.speak = function (){    console.log(this.name)    // 普通函數(非箭頭函數),this綁定在調用時的範圍  }  this.bornYear = () => {    // 本文寫於2016年,因此new Date().getFullYear()得到的是2016    // 箭頭函數,this綁定在執行個體內部    console.log(new Date().getFullYear() - this.age)    }  }}var zxt = new Person("zxt",22)zxt.speak() // "zxt"zxt.bornYear() // 1994// 到這裡應該大家應該都沒什麼問題var xiaoMing = {  name: "xiaoming",  age: 18 // 小明永遠18歲}zxt.speak.call(xiaoMing)// "xiaoming" this綁定的是xiaoMing這個對象zxt.bornYear.call(xiaoMing)// 1994 而不是 1998,這是因為this永遠綁定的是zxt這個執行個體

因此 ES6 的箭頭函數並不會使用四條標準的綁定規則,而是根據當前的詞法範圍來決定 this ,具體來說就是,箭頭函數會繼承 外層函數調用的this綁定 ,而無論外層函數的this綁定到哪裡。

小結

以上就是javascript中所有this綁定的情況,在es6之前,前面所說的四種綁定規則可以涵蓋任何的函數調用情況,es6標準實施以後,對於函數的擴充新增了箭頭函數,與之前不同的是,箭頭函數的範圍位於箭頭函數定義時所在的範圍。

而對於之前的四種綁定規則來說,掌握每種規則的調用條件就能很好的理解this到底是綁定在了哪個範圍。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.