JavaScript中this的運行機制及爬坑指南
在 JavaScript 中,this 這個特殊的變量是相對比較復雜的,因為 this 不僅僅用在面向?qū)ο蟓h(huán)境中,在其他任何地方也是可用的。 本篇博文中會解釋 this 是如何工作的以及使用中可能導致問題的地方,***奉上***實踐。
為了更好地理解 this,將 this 使用的場景分成三類:
- 在函數(shù)內(nèi)部 this 一個額外的,通常是隱含的參數(shù)。
- 在函數(shù)外部(***作用域中): 這指的是瀏覽器中的全局對象或者 Node.js 中一個模塊的輸出。
- 在傳遞給eval()的字符串中: eval() 或者獲取 this 當前值值,或者將其設置為全局對象,取決于 this 是直接調(diào)用還是間接調(diào)用。
我們來看看每個類別。
this 在函數(shù)中
這是最常用的 this 使用方式,函數(shù)通過扮演三種不同的角色來表示 JavaScript 中的所有可調(diào)用結(jié)構體:
- 普通函數(shù)(this 在非嚴格模式下為全局對象,在嚴格模式下為undefined)
- 構造函數(shù)(this 指向新創(chuàng)建的實例)
- 方法(this 是指方法調(diào)用的接收者)
在函數(shù)中,this 通常被認為是一個額外的,隱含的參數(shù)。
this 在普通函數(shù)中
在普通函數(shù)中,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)的隱式參數(shù)。 但是,可以通過 call() 或 apply() 進行函數(shù)調(diào)用,并明確指定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 在構造函數(shù)中
如果通過new運算符調(diào)用函數(shù),則函數(shù)將成為構造函數(shù)。 該運算符創(chuàng)建一個新的對象,并通過this傳遞給構造函數(shù):
- var savedThis;
- function Constr() {
- savedThis = this;
- }
- var inst = new Constr();
- console.log(savedThis === inst); // true
- 在JavaScript中實現(xiàn),new運算符大致如下所示(更精確的實現(xiàn)稍微復雜一點):
- function newOperator(Constr, arrayWithArgs) {
- var thisValue = Object.create(Constr.prototype);
- Constr.apply(thisValue, arrayWithArgs);
- return thisValue;
- }
this 在方法中
在方法中,類似于傳統(tǒng)的面向?qū)ο蟮恼Z言:this指向接受者,方法被調(diào)用的對象。
- var obj = {
- method: function () {
- console.log(this === obj); // true
- }
- }
- obj.method();
this 在***作用域中
在瀏覽器中,頂層作用域是全局作用域,它指向 global object(如window):
- console.log(this === window); // true
在Node.js中,通常在模塊中執(zhí)行代碼。 因此,***作用域是一個特殊的模塊作用域:
- // `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() 可以被直接(通過真正的函數(shù)調(diào)用)或間接(通過其他方式)調(diào)用。
如果間接調(diào)用evaleval() ,則this指向全局對象:
- (0,eval)('this === window')
- true
否則,如果直接調(diào)用eval() ,則this與eval()的環(huán)境中保持一致。 例如:
- // 普通函數(shù)
- 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在普通函數(shù)中為undefined,并且會在出現(xiàn)問題時警告。
陷阱:忘記new操作符
如果你調(diào)用一個構造函數(shù)時忘記了new操作符,那么你意外地將this用在一個普通的函數(shù)。this會沒有正確的值。 在非嚴格模式下,this指向window對象,你將創(chuàng)建全局變量:
- function Point(x, y) {
- this.x = x;
- this.y = y;
- }
- var p = Point(7, 5); // 忘記new!
- console.log(p === undefined); // true
- // 創(chuàng)建了全局變量:
- 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
陷阱:不正確地提取方法
如果獲取方法的值(不是調(diào)用它),則可以將該方法轉(zhuǎn)換為函數(shù)。 調(diào)用該值將導致函數(shù)調(diào)用,而不是方法調(diào)用。 當將方法作為函數(shù)或方法調(diào)用的參數(shù)傳遞時,可能會發(fā)生這種提取。 實際例子包括setTimeout()和事件注冊處理程序。 我將使用函數(shù)callItt() 來模擬此用例:
- /**類似setTimeout() 和 setImmediate() */
- function callIt(func) {
- func();
- }
如果在非嚴格模式下把一個方法作為函數(shù)來調(diào)用,那么this將指向全局對象并創(chuàng)建全局變量:
- 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
如果在嚴格模式下把一個方法作為函數(shù)來調(diào)用,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():
- var counter = {
- count: 0,
- inc: function () {
- this.count++;
- }
- }
- callIt(counter.inc.bind(counter));
- // 成功了!
- console.log(counter.count); // 1
bind()創(chuàng)建了一個新的函數(shù),它總是能得到一個指向counter的this。
陷阱:shadowing this
當在一個方法中使用普通函數(shù)時,很容易忘記前者具有其自己this(即使其不需要this)。 因此,你不能從前者引用該方法的this,因為該this會被遮蔽。 讓我們看看出現(xiàn)問題的例子:
- 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失敗,因為函數(shù)的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()來創(chuàng)建一個this總是指向正確值的函數(shù)(在下面的例子中該方法的this)。
- loop: function () {
- 'use strict';
- this.friends.forEach(function (friend) {
- console.log(this.name+' knows '+friend);
- }.bind(this));
- }
修正3: forEach的第二個參數(shù)。 此方法具有第二個參數(shù),this值將作為此值傳遞給回調(diào)函數(shù)。
- loop: function () {
- 'use strict';
- this.friends.forEach(function (friend) {
- console.log(this.name+' knows '+friend);
- }, this);
- }
***實踐
從概念上講,我認為普通函數(shù)沒有它自己的this,并且想到上述修復是為了保持這種想法。 ECMAScript 6通過箭頭函數(shù)支持這種方法 - 沒有它們自己的this。 在這樣的函數(shù)里面,你可以自由使用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作為普通函數(shù)的附加參數(shù)的API:
- beforeEach(function () {
- this.addMatchers({
- toBeInRange: function (start, end) {
- ...
- }
- });
- });
將這樣的隱含參數(shù)變成明確的參數(shù)使得事情更加明顯,并且與箭頭函數(shù)兼容。
- beforeEach(api => {
- api.addMatchers({
- toBeInRange(start, end) {
- ...
- }
- });
- });