this,call和apply(這三個東西,如何牢牢記住)

緣自世界發表於2018-01-03

這三個東西雖然一直再用,也用的很順手,知道它的用法,也知道它的區別,但是最近在攻克設計模式這個高地時總感覺缺點什麼,沒得辦法,就只好重新學習一下。並總結了些許個人心得,分享給大家。

this

跟別的語言不太一樣,JavaScript的this總是指向一個物件,而具體指向那個物件又是基於函式的執行環境(有人理解為上下文)動態繫結的,不是函式被宣告的環境,而是函式被引用的環境。

this指向

這個在‘度娘’上一搜文章多的是,但是存在這樣的問題,要麼是總結的不全,要麼是談的是自己的理解,一個用途的總結就能寫上幾段不適用,沒得辦法只好翻牆Google一下,找到幾篇不錯的文章,文章連結請轉到文章結尾參考資料部分。

個人感覺Google搜尋出來的學習資料價值更高,國內的搜尋引擎廠商估計都把精力用在如何賺使用者的錢上了。

我們知道this指向物件,所以相對來說它的含義就比較豐富,它可以是全域性物件,當前物件,或者任意物件,這完全取決於函式的呼叫方式。JavaScript中函式的呼叫有以下幾種方式。

  • 作為普通函式呼叫(全域性物件)
  • 作為物件的方法呼叫(當前物件)
  • 作為建構函式呼叫(任意物件)
  • Function.prototype.apply或Function.prototype.call的呼叫(任意物件)

1.作為物件的方法呼叫

window.name = `globalName`; 
var myObj = {
    name: `localName`,
    getName: function(){
        return this.name;
    }
};   
myObj.getName(); // localName

2.作為普通函式呼叫

window.name = `globalName`; 
var getName = function(){
    return this.name;
}; 
 
getName(); // globalName 

我們將物件的方法呼叫賦值給一個變數,這樣將它轉化為普通的方法,就會出現下面的結果

window.name = `globalName`; 
var myObj = {
    name: `localName`,
    getName: function(){
        return this.name;
    }
};   
var getName = myObj.getName;
getName(); // globalName

相信大家都遇到過這樣的情況,我們在操作dom節點時,一個區域性的callback方法被當作不同函式使用時callback中的this指向window,而在實際的開發中,我們希望的是它能夠指向window,我們往往這樣做。

html

<div id="div">this is a div</div>

js

window.id = `window`; 
document.getElementById(`div`).onclick = function(){
    var that = this; // 儲存 div 的引用
    // 被轉換為不同函式的callback
    var callback = function() {
        alert ( that.id ); // `div`
    }
    // 呼叫callback
    callback();
}; 

3.作為建構函式呼叫

眾所周知,JavaScript時一門物件導向的語言,但與其他的物件導向程式語言不同,它並沒有類(class)這個感念,取而代之的是基於原型(prototype)的繼承方式。JavaScript的建構函式也很特別,如果沒有new關鍵字,就和不同函式沒有區別,在在開發的過程中建構函式的名字以大寫字母開頭,這裡有些人注意到了,有些人沒有注意到,或者注意到了也不知道為什麼,雖然你寫成小寫也一樣使用,但是這恰恰說明你的業餘,在這裡大寫首字母就是為了協作開發,統一標準,同時也是再提醒呼叫者使用正確的方式呼叫,this才會繫結到新建立的物件上。

大部分的JavaScript函式都可以被當作構造器來使用。構造器的外表和普通函式一樣,區別在於被呼叫的方式(new)。

我們應該注意建構函式經new呼叫後,如果不指定返回物件型別,那麼預設返回this,如果指定了,就返回該指定的物件型別。返回指定型別物件的情況,我相信寫過外掛的童鞋都應該有或多或少的瞭解。

var MyClass = function(){
    this.name = `lisi`;
    // 顯式地返回一個物件
    return {
        name: `lzb`
    }
}; 
var obj = new MyClass();
obj.name; // lzb

注:顯示的返回的資料型別需保證是物件,其他的型別沒有用。

apply/call的呼叫

相信大家對JavaScript有了解的同學都知道‘JavaScript中一切皆物件’這句話。因此,函式也是物件,是物件就有方法,而apply和call就是函式物件的方法,這兩個方法很強大,它可以實現切換函式的執行環境(上下文),也即是可以改變this繫結的物件,這樣的方法被廣泛應用。

function MyObj() {
    this.name = `lisi`;
    this.getName = function(){
        return this.name;
    }
}; 
var obj = new MyObj();
var obj2 = {name: `lzb`}
// 將this指向的obj改為obj2
obj.getName.apply(obj2); // lzb

殊途同歸

其實,作為開發人員的我不喜歡這樣的總結,這不是原理的理解,而是把凡是所有涉及到this的使用進行了分類總結,看過Understanding JavaScript Function Invocation and “this”文章後有點小收穫,作者將apply/call作為函式呼叫的基本方式,其它的3種方式都是在這一基礎上演變而來的,也就是我們常說的語法糖。

函式呼叫過程就是this的繫結過程,這4種情況都要完成這樣一個繫結,不同的是作為一般函式呼叫時,this繫結的是全域性物件;作為物件的方法呼叫時,this繫結到該方法所屬的物件。

找不到this

我們都見過這樣的實現通過document.getElementById(`div`)獲得元素物件,但是我們又嫌它使用起來太麻煩,所以,我們會將他進行封裝用一個短函式來代替,如下所示:

var getId = function(id) {
    return document.getElementById(id);
};

getId(`div`);

但是,有沒有人這樣用如下的方式實現過:

var getId = document.getElementById;

getId(`div`);

我想在沒有接觸第一種實現方法之前,有人肯定這樣想過,這個方法肯定有人用過,不過之後還是使用了第一種,很明顯這種方法實現是不可行,學過以上的知識我們知道document.getElementById 方法的內部實現要用到this,而在第二種中getId作為一個普通函式它的this指向window,所以getELementById中this指向window,但是我們知道getElementById方法是document物件的方法,方法要想正常使用應該指向document,而第二種方法指向了window,所以,第二種方法有問題,但是我們可以使用apply來修正,把document當作this傳入getId,程式碼如下所示:

document.getElementById = (function(fn) {
    return function () {
        return fn.apply(document. arguments)
    }
})(document.getElementById);

var getId = document.getElementById;

getId(`div`);

這個可以參考dojo中的lang.hitch實現

button.onclick = lang.hitch(myObject, myObject.handler);

call 和 apply

在函數語言程式設計風格的程式碼編寫中,call和apply被廣泛應用,在JavaScript的設計模式中這兩個方法也是應用廣泛。

call vs apply

apply接受兩個引數,第一個引數指定了函式體內this物件的指向,第二個引數為一個帶有下表的集合(它可以是陣列,也可以是類陣列),apply把集合中的元素作為引數傳遞給被呼叫的函式。

call接受的引數不固定,第一個引數與apply作用一樣,剩餘的引數傳遞給被呼叫的函式。

當一個函式被呼叫是,JavaScript的直譯器並不會計較形參和實參在數量、型別、順序上的差別,在js的引數內部是使用一個陣列來標識的(apply的使用效率比call要高)。

在我們使用call/apply的時候如果第一個引數為null,函式體內的this指向全域性物件,但是在嚴格模式下返回的還是null。

可以在控制檯輸入如下程式碼:

var func = function(a) {
    console.log(this === window)
}

func.apply(null) // true

// 嚴格模式
var func = function(a) {
    "use strict";
    console.log(this === null)
}

func.apply(null) // true

call/apply 的用途

通過上面的例子我們已經知道了他們的第一個用途就是改變函式內部的this的指向

我們在做html的互動工作時,所寫的js都要求功能和實現的分離。

var oDiv = document.getElementById(`div`);
oDiv.onClick = function() {
    func.apply(this)
};

function alertId() {
    alert(this.id)
}

Function.prototype.bind()

這個方法自大多數高版本瀏覽器中都實現了,它的作用就是用來指定函式內部的this指向,雖然存在相容性,但是在我們知道了call/apply 的作用之後,我們可以自己寫一個,程式碼如下:

Function.prototype.bind = function (context) {
    var that = this; // 儲存原函式的this
    return function() {
        return that.apply(context, arguments) // 執行函式前會將context傳入函式體內當作this
    }
};

var obj = {
    name: `lzb`
};

var getName = function() {
    console.log(this.name)
}.bind(obj);

getName(); // lzb

在這裡我麼們還可以向傳遞其他引數,程式碼修改如下:

Function.prototype.bind = function() {
    var that = this;
    var context = [].shift.apply(arguments);
    var args = [].slice.apply(arguments);

    return function() {
        return that.apply( context, [].concat.apply( [].slice.apply( arguments ), args) )
    }
}

var obj = {
    name: `lzb`
};

var setName = function(name) {
    this.name = name;
}.bind(obj, `xxx`); // 設定setName 的預設引數

var getName = function() {
    console.log(this.name)
}.bind(obj);

setName(); // 預設為 xxx
// setName(`snalv`);
getName(); // sanlv

這裡我們可以用bind實現更多更復雜的操作,這裡就不一一介紹了。
相信到這裡你應該對this,call/apply有了深刻的理解吧。

參考資料

Understanding the “this” keyword in JavaScript

深入淺出 JavaScript 中的 this

Understanding JavaScript Function Invocation and “this”

文章後續更新請關注我的GitHub


相關文章