JavaScript中this的執行機制及爬坑指南
在 JavaScript 中,this 這個特殊的變數是相對比較複雜的,因為 this 不僅僅用在物件導向環境中,在其他任何地方也是可用的。 本篇博文中會解釋 this 是如何工作的以及使用中可能導致問題的地方,最後奉上最佳實踐。
為了更好理解 this,將 this 使用的場景分成三類:
-
在函式內部 this 一個額外的,通常是隱含的引數。
-
在函式外部(頂級作用域中): 這指的是瀏覽器中的全域性物件或者 Node.js 中一個模組的輸出。
-
在傳遞給eval()的字串中: eval() 或者獲取 this 當前值值,或者將其設定為全域性物件,取決於 this 是直接呼叫還是間接呼叫。
我們來看看每個類別。
this 在函式中
這是最常用的 this 使用方式,函式通過扮演三種不同的角色來表示 JavaScript 中的所有可呼叫結構體:
-
普通函式(this 在非嚴格模式下為全域性物件,在嚴格模式下為undefined)
-
建構函式(this 指向新建立的例項)
-
方法(this 是指方法呼叫的接收者)
在函式中,this 通常被認為是一個額外的,隱含的引數。
this 在普通函式中
在普通函式中,this 的值取決於模式:
- 非嚴格模式: this 是指向全域性物件 (在瀏覽器中為window物件)。
function sloppyFunc() { console.log(this === window); // true } sloppyFunc();
- 嚴格模式: this 的值為 undefined。
function strictFunc() { 'use strict'; console.log(this === undefined); // true } strictFunc();
也就是說,this 是一個設定了預設值(window或undefined)的隱式引數。 但是,可以通過 call() 或 apply() 進行函式呼叫,並明確指定this的值:
function func(arg1, arg2) { console.log(this); // a console.log(arg1); // b console.log(arg2); // c } func.call('a', 'b', 'c'); // (this, arg1, arg2) func.apply('a', ['b', 'c']); // (this, arrayWithArgs)
this 在建構函式中
如果通過new運算子呼叫函式,則函式將成為建構函式。 該運算子建立一個新的物件,並通過它通過this傳遞給建構函式:
var savedThis; function Constr() { savedThis = this; } var inst = new Constr(); console.log(savedThis === inst); // true
在JavaScript中實現,new運算子大致如下所示(更精確的實現稍微複雜一點):
function newOperator(Constr, arrayWithArgs) { var thisValue = Object.create(Constr.prototype); Constr.apply(thisValue, arrayWithArgs); return thisValue; }
this 在方法中
在方法中,類似於傳統的物件導向的語言:this指向接受者,方法被呼叫的物件。
var obj = { method: function () { console.log(this === obj); // true } } obj.method();
this 在頂級作用域中
在瀏覽器中,頂層作用域是全域性作用域,它指向global object(如window):
console.log(this === window); // true
在Node.js中,通常在模組中執行程式碼。 因此,頂級作用域是一個特殊的模組作用域:
// `global` (不是 `window`) 指全域性物件: console.log(Math === global.Math); // true // `this` 不指向全域性物件: console.log(this !== global); // true // `this` refers to a module’s exports: console.log(this === module.exports); // true
this 在 eval() 中
eval() 可以被直接(通過真正的函式呼叫)或間接(通過其他方式)。 詳細解釋在這裡。
如果間接呼叫evaleval() ,則this指向全域性物件:
(0,eval)('this === window') true
否則,如果直接呼叫eval() ,則this與eval()的環境中保持一致。 例如:
// 普通函式 function sloppyFunc() { console.log(eval('this') === window); // true } sloppyFunc(); function strictFunc() { 'use strict'; console.log(eval('this') === undefined); // true } strictFunc(); // 構造器 var savedThis; function Constr() { savedThis = eval('this'); } var inst = new Constr(); console.log(savedThis === inst); // true // 方法 var obj = { method: function () { console.log(eval('this') === obj); // true } } obj.method();
與this相關的陷阱
有三個你需要知道的與this相關的陷阱。請注意,在各種情況下,嚴格模式更安全,因為this在普通函式中為undefined,並且會在出現問題時警告。
陷阱:忘記new操作符
如果你呼叫一個建構函式時忘記了new操作符,那麼你意外地將this用在一個普通的函式。this會沒有正確的值。 在非嚴格模式下,this指向window物件,你將建立全域性變數:
function Point(x, y) { this.x = x; this.y = y; } var p = Point(7, 5); // 忘記new! console.log(p === undefined); // true // 建立了全域性變數: console.log(x); // 7 console.log(y); // 5
幸運的,在嚴格模式下會得到警告(this === undefined):
function Point(x, y) { 'use strict'; this.x = x; this.y = y; } var p = Point(7, 5); // TypeError: Cannot set property 'x' of undefined
陷阱:不正確地提取方法
如果獲取方法的值(不是呼叫它),則可以將該方法轉換為函式。 呼叫該值將導致函式呼叫,而不是方法呼叫。 當將方法作為函式或方法呼叫的引數傳遞時,可能會發生這種提取。 實際例子包括setTimeout()和事件註冊處理程式。 我將使用函式callItt() 來模擬此用例:
/**類似setTimeout() 和 setImmediate() */ function callIt(func) { func(); }
如果在非嚴格模式下把一個方法作為函式來呼叫,那麼this將指向全域性物件並建立全域性變數:
var counter = { count: 0, // Sloppy-mode method inc: function () { this.count++; } } callIt(counter.inc); // Didn’t work: console.log(counter.count); // 0 // Instead, a global variable has been created // (NaN is result of applying ++ to undefined): console.log(count); // NaN
如果在嚴格模式下把一個方法作為函式來呼叫,this為undefined。 同時會得到一個警告:
var counter = { count: 0, // Strict-mode method inc: function () { 'use strict'; this.count++; } } callIt(counter.inc); // TypeError: Cannot read property 'count' of undefined console.log(counter.count);
修正方法是使用[bind()](http://speakingjs.com/es5/ch17.html#Function.prototype.bind): The fix is to use bind():
var counter = { count: 0, inc: function () { this.count++; } } callIt(counter.inc.bind(counter)); // 成功了! console.log(counter.count); // 1
bind()建立了一個新的函式,它總是接收一個指向counter的this。
陷阱:shadowing this
當在一個方法中使用普通函式時,很容易忘記前者具有其自己this(即使其不需要this)。 因此,你不能從前者引用該方法的this,因為該this會被遮蔽。 讓我們看看出現問題的例子:
var obj = { name: 'Jane', friends: [ 'Tarzan', 'Cheeta' ], loop: function () { 'use strict'; this.friends.forEach( function (friend) { console.log(this.name+' knows '+friend); } ); } }; obj.loop(); // TypeError: Cannot read property 'name' of undefined
在前面的例子中,獲取this.name失敗,因為函式的this個是undefined,它與方法loop()的不同。 有三種方法可以修正this。
修正1: that = this。 將它分配給一個沒有被遮蔽的變數(另一個流行名稱是self)並使用該變數。
loop: function () { 'use strict'; var that = this; this.friends.forEach(function (friend) { console.log(that.name+' knows '+friend); }); }
修正2: bind()。 使用bind()來建立一個this總是指向正確值的函式(在下面的例子中該方法的this)。
loop: function () { 'use strict'; this.friends.forEach(function (friend) { console.log(this.name+' knows '+friend); }.bind(this)); }
修正3: forEach的第二個引數。 此方法具有第二個引數,this值將作為此值傳遞給回撥函式。
loop: function () { 'use strict'; this.friends.forEach(function (friend) { console.log(this.name+' knows '+friend); }, this); }
最佳實踐
從概念上講,我認為普通函式沒有它自己的this,並且想到上述修復是為了保持這種想法。 ECMAScript 6通過[箭頭函式](http://2ality.com/2012/04/arrow-functions.html)支援這種方法 - 沒有它們自己的this。 在這樣的函式裡面,你可以自由使用this,因為不會被遮蔽:
loop: function () { 'use strict'; // The parameter of forEach() is an arrow function this.friends.forEach(friend => { // `this` is loop’s `this` console.log(this.name+' knows '+friend); }); }
我不喜歡使用this作為普通函式的附加引數的API:
beforeEach(function () { this.addMatchers({ toBeInRange: function (start, end) { ... } }); });
將這樣的隱含引數變成明確的引數使得事情更加明顯,並且與箭頭函式相容。
beforeEach(api => { api.addMatchers({ toBeInRange(start, end) { ... } }); });
相關文章
- JavaScript 中 this 的執行機制及爬坑指南JavaScript
- Javascript中的執行機制——Event LoopJavaScriptOOP
- Javascript 執行機制JavaScript
- JavaScript執行機制JavaScript
- 探索JavaScript執行機制JavaScript
- JavaScript的程式碼執行機制JavaScript
- JavaScript執行緒機制與事件機制JavaScript執行緒事件
- Javascript執行機制(setTimeout/Promise)JavaScriptPromise
- javascript執行機制:Event LoopJavaScriptOOP
- nextTick的原理及執行機制
- 傻傻分不清的javascript執行機制JavaScript
- 談談 Javascript 的執行機制及對同步非同步的理解JavaScript非同步
- 【執行機制】 JavaScript的事件迴圈機制總結 eventLoopJavaScript事件OOP
- 深入淺出JavaScript執行機制JavaScript
- JavaScript執行機制深層剖析JavaScript
- JavaScript執行機制:event-loopJavaScriptOOP
- JavaScript執行機制-node事件迴圈JavaScript事件
- JavaScript 執行機制--Event Loop詳解JavaScriptOOP
- java 執行shell命令及日誌收集避坑指南Java
- JavaScript執行機制詳解:再談EvemtJavaScript
- 帶你瞭解JavaScript的執行機制—Event LoopJavaScriptOOP
- JS執行機制及ES6JS
- JavaScript 執行機制-瀏覽器事件迴圈JavaScript瀏覽器事件
- 一次性搞懂JavaScript 執行機制JavaScript
- js的執行機制JS
- Electron 打包爬坑指南
- Session的執行機制及怎樣適用於微信小程式中Session微信小程式
- 【踩坑指南】執行緒池使用不當的五個坑執行緒
- 夯實基礎上篇-圖解 JavaScript 執行機制圖解JavaScript
- JS在瀏覽器中的執行機制JS瀏覽器
- JavaScript正則爬坑JavaScript
- 一起分析執行緒的狀態及執行緒通訊機制執行緒
- 程式的機器級表示:定址方式、指令及棧的執行機制
- 談談JavaScript中的this機制JavaScript
- Java的執行機制分析!Java
- React的setState執行機制React
- JS引擎的執行機制JS
- 好程式設計師web前端教程分享JavaScript的執行機制!程式設計師Web前端JavaScript