javascript 物件導向學習(三)——this,bind、apply 和 call

Stroyer發表於2020-06-11

this 是 js 裡繞不開的話題,也是非常容易混淆的概念,今天試著把它理一理。

this 在非嚴格模式下,總是指向一個物件,在嚴格模式下可以是任意值,本文僅考慮非嚴格模式。記住它總是指向一個物件對於理解它的意義很重要。this 在實際使用中,大致分為以下幾種情況:

1. 函式作為物件的方法呼叫時,this 指向呼叫該函式的物件

var obj = {
    name: 'jack',
    getName: function() {
        console.log(this === obj) // true
        console.log(this.name)  // jack
    }
}
obj.getName()

這個應該很好理解,不多說了。

2. 函式作為普通函式被呼叫時,this 指向全域性物件。在瀏覽器中,全域性物件是window。

var name = 'global'
function getName() {
    console.log(this === window) // true
    console.log(this.name) // global
}
getName()

我的理解是上面的程式碼可以改寫為

window.name = 'global'
window.getName = function() {
    console.log(this === window) // true
    console.log(this.name) // global
}
window.getName()

這樣其實與情況1是一樣的,相當於函式作為物件的方法呼叫,只不過這裡的物件是全域性物件。

《Javascript 設計模式與開發實踐》一書中有個例子如下:

window.name = 'globalName';
var myObject = {
    name: 'seven',
    getName: function(){
        return this.name
    } 
}

var getName = myObject.getName
console.log(getName())  // globalName

getName 是定義在myObject 物件中的方法,在呼叫getName 方法時,列印出的卻是全域性物件的name,而不是myObject物件的name,這再次證明了 this 並非指向函式被宣告時的環境物件,而是指向函式被呼叫時的環境物件

3. 函式作為建構函式呼叫時,指向構造出的新物件

function Person(name) {
    this.name = name  
}

var jack = new Person('Jack')
console.log(jack.name) // Jack
var rose = new Person('Rose')
console.log(rose.name) // Rose

這裡建立了兩個不同名字的物件,列印出的name也是不一樣的,說明建構函式的 this 會根據建立物件的不同而變化。需要注意的是,如果建構函式裡返回了一個Object型別的物件,那麼this會指向這個物件,而不是利用建構函式建立出的物件。我們在建構函式一章裡也提到過,new 操作符所做的最後一步就是返回新物件,而如果我們顯式地返回一個物件,就會覆蓋這步操作,this也就不再指向新物件。

4. 函式作為事件處理函式呼叫時,指向觸發事件的元素

document.getElementById("myBtn").addEventListener("click", function(e){
    console.log(this === e.currentTarget) // true
});

5. 箭頭函式

由於箭頭函式沒有this,它的 this 是繼承父執行上下文裡面的 this。執行上下文後面再討論,現在只要知道簡單物件(非函式)是沒有執行上下文的。

var obj = {
    name:  'obj',
    getName: function() {
console.log(this) // 執行上下文裡的 this
return (()=>{ console.log(this.name) }) } } var fn = obj.getName() fn() // obj

按照情況2來處理的話,this 指向全域性物件,應該輸出 undefined,結果並不是。與普通函式不同,箭頭函式的 this 是在函式被宣告時決定的,而不是函式被呼叫時。在這裡,父執行上下文是 getName 函式,也就繼承了 getName 的 this,即 obj。

利用 bind、apply、call 改變 this 指向

bind、apply、call 都是定義在 Function 原型物件上的方法,所有函式物件都能繼承這個方法,三者都能用來改變 this 指向,我們來看看它們的聯絡與區別。

function fn() {
    console.log(this.name)
}

// bind
var bindfn = fn.bind({name: 'bind'})
bindfn() // bind

// apply
fn.apply({name: 'apply'}) // apply

// call
fn.call({name: 'call'}) // call

我們定義了一個函式fn,然後分別呼叫了它的 bind、apply、call 方法,並傳入一個物件引數,通過列印出的內容可以看到 this 被繫結到了引數物件上。bind 似乎有些不同,多了一步 bindfn() 呼叫,這是因為 bind 方法返回的是一個函式,不會立即執行,而呼叫 apply 和 call 方法會立即執行。

下面再來看一下 fn 函式存在引數的情況:

function fn(a, b, c) {
    console.log(a, b, c)
}

var bindfn = fn.bind(null, 'bind');
bindfn('A', 'B', 'C');           // bind A B

fn.apply(null, ['apply', 'A']) // apply A undefined

fn.call(null, 'call', 'A');  // bind A undefined

bindfn 列印出的結果是fn呼叫bind方法時的傳遞的引數加上bindfn傳遞的引數,引數 'C' 被捨棄掉了。呼叫 apply 和 call 方法列印出的則是傳遞給它們的引數,不一樣的是,apply 的引數是一個陣列(或類陣列),call 則是把引數依次傳入函式。這時候再看它們的定義應該會好理解很多:

bind() 方法建立一個新的函式,在 bind() 被呼叫時,這個新函式的 this 被指定為 bind() 的第一個引數,而其餘引數將作為新函式的引數,供呼叫時使用。

apply() 方法呼叫一個具有給定 this 值的函式,以及作為一個陣列(或類陣列物件)提供的引數。

call() 方法使用一個指定的 this 值和單獨給出的一個或多個引數來呼叫一個函式。

我們可以利用它們來借用其他物件的方法。已知函式的引數列表 arguments 是一個類陣列物件,比如上例中函式 fn 的引數 a, b, c,因為它不是一個真正的陣列,不能呼叫陣列方法,這時借用 apply/call 方法(bind 也可以,就是用得比較少)將 this 指向 arguments 就能借用陣列方法:

(function(){
    Array.prototype.push.call(arguments, 'c')
    console.log(arguments) // ['a', 'b', 'c']
})('a','b')

值得一提的是,push 方法並不是只有陣列才能呼叫,一個物件只要滿足1.可讀寫 length 屬性;2.物件本身可存取屬性. 就可以利用 call / apply 呼叫 push 方法。

 

參考:

《Javascript 設計模式與開發實踐》

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this

http://www.imooc.com/article/80117

https://blog.csdn.net/weixin_42519137/article/details/88053339

相關文章