javascript 面向對象學習(三)——this,bind、apply 和 call
- 2020 年 6 月 11 日
- 筆記
- javascript
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 方法將 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 設計模式與開發實踐》
//developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this
//blog.csdn.net/weixin_42519137/article/details/88053339