JavaScript 的 this 指向問題深度解析

JavaScript 中的 this 指向問題有不少博客在解釋,仍然有不少人問。上週咱們的開發團隊連續兩我的遇到相關問題,因此我不得不將關於前端構建技術的交流會延長了半個時候討論 this 的問題。javascript

與咱們常見的不少語言不一樣,JavaScript 函數中的 this 指向並非在函數定義的時候肯定的,而是在調用的時候肯定的。換句話說,函數的調用方式決定了 this 指向前端

JavaScript 中,普通的函數調用方式有三種:直接調用、方法調用和 new 調用。除此以外,還有一些特殊的調用方式,好比經過 bind() 將函數綁定到對象以後再進行調用、經過 call()apply() 進行調用等。而 es6 引入了箭頭函數以後,箭頭函數調用時,其 this 指向又有所不一樣。下面就來分析這些狀況下的 this 指向。java

直接調用

直接調用,就是經過 函數名(...) 這種方式調用。這時候,函數內部的 this 指向全局對象,在瀏覽器中全局對象是 window,在 NodeJs 中全局對象是 globales6

來看一個例子:express

// 簡單兼容瀏覽器和 NodeJs 的全局對象
const _global = typeof window === "undefined" ? global : window;

function test() {
    console.log(this === _global);    // true
}

test();    // 直接調用

這裏須要注意的一點是,直接調用並非指在全局做用域下進行調用,在任何做用域下,直接經過 函數名(...) 來對函數進行調用的方式,都稱爲直接調用。好比下面這個例子也是直接調用瀏覽器

(function(_global) {
    // 經過 IIFE 限定做用域

    function test() {
        console.log(this === _global);  // true
    }

    test();     // 非全局做用域下的直接調用
})(typeof window === "undefined" ? global : window);

bind() 對直接調用的影響

還有一點須要注意的是 bind() 的影響。Function.prototype.bind() 的做用是將當前函數與指定的對象綁定,並返回一個新函數,這個新函數不管以什麼樣的方式調用,其 this 始終指向綁定的對象。仍是來看例子:閉包

const obj = {};

function test() {
    console.log(this === obj);
}

const testObj = test.bind(obj);
test();     // false
testObj();  // true

那麼 bind() 幹了啥?不妨模擬一個 bind() 來了解它是如何作到對 this 產生影響的。app

const obj = {};

function test() {
    console.log(this === obj);
}

// 自定義的函數,模擬 bind() 對 this 的影響
function myBind(func, target) {
    return function() {
        return func.apply(target, arguments);
    };
}

const testObj = myBind(test, obj);
test();     // false
testObj();  // true

從上面的示例能夠看到,首先,經過閉包,保持了 target,即綁定的對象;而後在調用函數的時候,對原函數使用了 apply 方法來指定函數的 this。固然原生的 bind() 實現可能會不一樣,並且更高效。但這個示例說明了 bind() 的可行性。前端構建

call 和 apply 對 this 的影響

上面的示例中用到了 Function.prototype.apply(),與之相似的還有 Function.prototype.call()。這兩方法的用法請你們本身經過連接去看文檔。不過,它們的第一個參數都是指定函數運行時其中的 this 指向。函數

不過使用 applycall 的時候仍然須要注意,若是目錄函數自己是一個綁定了 this 對象的函數,那 applycall 不會像預期那樣執行,好比

const obj = {};

function test() {
    console.log(this === obj);
}

// 綁定到一個新對象,而不是 obj
const testObj = test.bind({});
test.apply(obj);    // true

// 指望 this 是 obj,即輸出 true
// 可是由於 testObj 綁定了不是 obj 的對象,因此會輸出 false
testObj.apply(obj); // false

因而可知,bind() 對函數的影響是深遠的,慎用!

方法調用

方法調用是指經過對象來調用其方法函數,它是 對象.方法函數(...) 這樣的調用形式。這種狀況下,函數中的 this 指向調用該方法的對象。可是,一樣須要注意 bind() 的影響。

const obj = {
    // 第一種方式,定義對象的時候定義其方法
    test() {
        console.log(this === obj);
    }
};

// 第二種方式,對象定義好以後爲其附加一個方法(函數表達式)
obj.test2 = function() {
    console.log(this === obj);
};

// 第三種方式和第二種方式原理相同
// 是對象定義好以後爲其附加一個方法(函數定義)
function t() {
    console.log(this === obj);
}
obj.test3 = t;

// 這也是爲對象附加一個方法函數
// 可是這個函數綁定了一個不是 obj 的其它對象
obj.test4 = (function() {
    console.log(this === obj);
}).bind({});

obj.test();     // true
obj.test2();    // true
obj.test3();    // true

// 受 bind() 影響,test4 中的 this 指向不是 obj
obj.test4();    // false

這裏須要注意的是,後三種方式都是預約定義函數,再將其附加給 obj 對象做爲其方法。再次強調,函數內部的 this 指向與定義無關,受調用方式的影響。

方法中 this 指向全局對象的狀況

注意這裏說的是方法中而不是方法調用中。方法中的 this 指向全局對象,若是不是由於 bind(),那就必定是由於不是用的方法調用方式,好比

const obj = {
    test() {
        console.log(this === obj);
    }
};

const t = obj.test;
t();    // false

t 就是 objtest 方法,可是 t() 調用時,其中的 this 指向了全局。

之因此要特別提出這種狀況,主要是由於經常將一個對象方法做爲回調傳遞給某個函數以後,卻發現運行結果與預期不符——由於忽略了調用方式對 this 的影響。好比下面的例子是在頁面中對某些事情進行封裝以後特別容易遇到的問題:

class Handlers {
    // 這裏 $button 假設是一個指向某個按鈕的 jQuery 對象
    constructor(data, $button) {
        this.data = data;
        $button.on("click", this.onButtonClick);
    }

    onButtonClick(e) {
        console.log(this.data);
    }
}

const handlers = new Handlers("string data", $("#someButton"));
// 對 #someButton 進行點擊操做以後
// 輸出 undefined
// 但預期是輸出 string data

this.onButtonClick 做爲一個參數傳入 on() 以後,事件觸發時,理論上是對這個函數進行的直接調用,而不是方法調用,因此其中的 this 會指向全局對象 —— 但實際上因爲調用事件處理函數的時候,this 指向會綁定到觸發事件的 DOM 元素上,因此這裏的 this 是指向觸發事件的的 DOM 元素(注意:this 並不是 jQuery 對象),即 $button.get(0)(注意代碼前註釋中的假設)。

要解決這個問題有不少種方法:

// 這是在 es5 中的解決辦法之一
var _this = this;
$button.on("click", function() {
    _this.onButtonClick();
});

// 也能夠經過 bind() 來解決
$button.on("click", this.onButtonClick.bind(this));

// es6 中能夠經過箭頭函數來處理,在 jQuery 中慎用
$button.on("click", e => this.onButtonClick(e));

不過請注意,將箭頭函數用做 jQuery 的回調時形成要當心函數內對 this 的使用。jQuery 大多數回調函數(非箭頭函數)中的 this 都是表示調用目標,因此能夠寫 $(this).text() 這樣的語句,但 jQuery 沒法改變箭頭函數的 this 指向,一樣的語句語義徹底不一樣。

new 調用

在 es6 以前,每個函數均可以看成是構造函數,經過 new 調用來產生新的對象(函數內無特定返回值的狀況下)。而 es6 改變了這種狀態,雖然 class 定義的類用 typeof 運算符獲得的仍然是 "function",但它不能像普通函數同樣直接調用;同時,class 中定義的方法函數,也不能看成構造函數用 new 來調用。

而在 es5 中,用 new 調用一個構造函數,會建立一個新對象,而其中的 this 就指向這個新對象。這沒有什麼懸念,由於 new 自己就是設計來建立新對象的。

var data = "Hi";    // 全局變量

function AClass(data) {
    this.data = data;
}

var a = new AClass("Hello World");
console.log(a.data);    // Hello World
console.log(data);      // Hi

var b = new AClass("Hello World");
console.log(a === b);   // false

箭頭函數中的 this

先來看看 MDN 上對箭頭函數的說明

An arrow function expression has a shorter syntax than a function expression and does not bind its own this, arguments, super, or new.target. Arrow functions are always anonymous. These function expressions are best suited for non-method functions, and they cannot be used as constructors.

這裏已經清楚了說明了,箭頭函數沒有本身的 this 綁定。箭頭函數中使用的 this,實際上是直接包含它的那個函數或函數表達式中的 this。好比

const obj = {
    test() {
        const arrow = () => {
            // 這裏的 this 是 test() 中的 this,
            // 由 test() 的調用方式決定
            console.log(this === obj);
        };
        arrow();
    },

    getArrow() {
        return () => {
            // 這裏的 this 是 getArrow() 中的 this,
            // 由 getArrow() 的調用方式決定
            console.log(this === obj);
        };
    }
};

obj.test();     // true

const arrow = obj.getArrow();
arrow();        // true

示例中的兩個 this 都是由箭頭函數的直接外層函數(方法)決定的,而方法函數中的 this 是由其調用方式決定的。上例的調用方式都是方法調用,因此 this 都指向方法調用的對象,即 obj

箭頭函數讓你們在使用閉包的時候不須要太糾結 this,不須要經過像 _this 這樣的局部變量來臨時引用 this 給閉包函數使用。來看一段 Babel 對箭頭函數的轉譯可能能加深理解:

// ES6
const obj = {
    getArrow() {
        return () => {
            console.log(this === obj);
        };
    }
}
// ES5,由 Babel 轉譯
var obj = {
    getArrow: function getArrow() {
        var _this = this;
        return function () {
            console.log(_this === obj);
        };
    }
};

另外須要注意的是,箭頭函數不能用 new 調用,不能 bind() 到某個對象(雖然 bind() 方法調用沒問題,可是不會產生預期效果)。無論在什麼狀況下使用箭頭函數,它自己是沒有綁定 this 的,它用的是直接外層函數(即包含它的最近的一層函數或函數表達式)綁定的 this

勘誤

  • this.onButtonClick 用於 jQuery 事件的時候,this 已經被 jQuery 改成指向觸發事件的元素,感謝 @月亮哥哥@QoVoQ 指出。此錯誤已經在文中修改了。
相關文章
相關標籤/搜索