JavaScript 中的 this 指向問題有很多部落格在解釋,仍然有很多人問。
# 與我們常見的許多語言不同,JavaScript 函數中的 this 指向並不是在函數定義的時候確定的,而是在呼叫的時候確定的。換句話說,函數的呼叫方式決定了 this 指向。
JavaScript 中,普通的函數呼叫方式有三種:直接呼叫、方法呼叫和 new 呼叫。除此之外,還有一些特殊的呼叫方式,例如透過 bind() 將函數綁定到物件之後再進行呼叫、透過 call()、apply() 進行呼叫等。而 es6 引入了箭頭函數之後,箭頭函數呼叫時,其 this 指向又有所不同。下面就來分析這些情況下的 this 指向。
直接調用,就是透過 函數名(...) 這種方式調用。這時候,函數內部的 this 指向全域對象,在瀏覽器中全域物件是 window,在 NodeJs 中全域物件是 global。
來看一個例子:
// 简单兼容浏览器和 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() 的影響。 Function.prototype.bind() 的作用是將當前函數與指定的物件綁定,並傳回一個新函數,這個新函數無論以什麼樣的方式調用,其 this 始終指向綁定的物件。還是來看範例:
const obj = {}; function test() { console.log(this === obj); } const testObj = test.bind(obj); test(); // false testObj(); // true
那麼 bind() 幹了啥?不妨模擬一個 bind() 來了解它是如何做到對 this 產生影響的。
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() 的可行性。
上面的範例中用到了 Function.prototype.apply(),與之類似的還有 Function.prototype.call()。這兩種方法的用法請大家自己透過連結去看文件。不過,它們的第一個參數都是指定函數執行階段其中的 this 指向。
不過使用 apply 和 call 的時候仍然需要注意,如果目錄函數本身是一個綁定了 this 物件的函數,那麼 apply 和 call 不會像預期那樣執行,例如
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 指向全域對象,如果不是因為 bind(),那就一定是因為不是用的方法呼叫方式,例如
const obj = { test() { console.log(this === obj); } }; const t = obj.test; t(); // false
t 是 obj 的 test 方法,但是 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 會指向全域物件。要解決這個問題有很多種方法
// 这是在 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 指向,同樣的語句語意完全不同。
在 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
先来看看 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。
以上是有關JavaScript中this指向問題的深度解析的詳細內容。更多資訊請關注PHP中文網其他相關文章!