Javascript this全攻略

Javascript this

在JavaScript中,<font color=#7FFFD4 >this</font> 是當前執行函數的上下文。

JavaScript有4種不同的函數調用方式:

  • 函數調用: alert('Hello World!')
  • 方法調用: console.log('Hello World!')
  • 構造函數調用: new RegExp('\d')
  • 隱式調用: alert.call(undefined, 'Hello World!')

并且每種方法都定義了自己的上下文,<font color=#7FFFD4>this</font> 會表現得跟程序員預期的不太一樣。
同時,<font color=#7FFFD4 >strict</font> 模式也會影響函數執行時的上下文。

函數調用與方法調用

  • 函數調用:當一個表達式為函數接著一個 <font color=#7FFFD4>(</font> ,一些用逗號分隔的參數以及一個 <font color=#7FFFD4>)</font> 時,函數調用被執行。例如 <font color=#7FFFD4>parseInt('18')</font>

  • 方法調用(屬性訪問):<font color=#7FFFD4>myObject.myFunction</font>,是一個方法調用。<font color=#7FFFD4>[1,5].join(',')</font> 是一個方法調用。實質就是對象屬性的訪問

在函數調用中的this

this 在函數調用中是一個全局對象

function sum(a, b) {
   console.log(this === window); // => true
   this.myNumber = 20; // add 'myNumber' property to global object
   return a + b;
}
// sum() is invoked as a function
// this in sum() is a global object (window)
sum(15, 16); // => 31
window.myNumber; // => 20

strict模式中, this 不再是全局對象而是變成undefined

function multiply(a, b) {
 'use strict'; // enable the strict mode
  console.log(this === undefined); // => true
  return a * b;
}
// multiply() function invocation with strict mode enabled
// this in multiply() is undefined
multiply(2, 5); // => 10

當multiply(2, 5)作為函數被調用時,this是undefined。

陷阱: 內部函數中的 <font color=#7FFFD4>this</font>

一個函數調用中的常見錯誤就是以為this在內部函數中跟在外部函數中一樣。

正確來說,內部函數的上下文依賴于調用方法,而不是外部函數的上下文。

var numbers = {
   numberA: 5,
   numberB: 10,
   sum: function() {
     console.log(this === numbers); // => true
     function calculate() {
       // this is window or undefined in strict mode
       console.log(this === numbers); // => false
       return this.numberA + this.numberB;
     }
     return calculate();
   }
};
numbers.sum(); // => NaN or throws TypeError in strict mode

<font color=#7FFFD4>numbers.sum()</font> 是一個對象上的方法調用,所以 <font color=#7FFFD4>sum</font> 中的上下文是 <font color=#7FFFD4>numbers</font> 對象。

<font color=#7FFFD4>calculate</font> 函數定義在 <font color=#7FFFD4>sum</font> 內部,所以你會指望 <font color=#7FFFD4>calculate()</font> 中的 <font color=#7FFFD4>this</font> 也是 <font color=#7FFFD4>numbers</font> 對象。

然而,<font color=#7FFFD4>calculate()</font> 是一個函數調用(而不是方法調用),它的 <font color=#7FFFD4>this</font> 是全局對象 <font color=#7FFFD4>window</font> 或者 <font color=#7FFFD4>strict</font> 模式下的 <font color=#7FFFD4>undefined</font>

即使外部函數 <font color=#7FFFD4>sum</font> 的上下文是 <font color=#7FFFD4>numbers</font> 對象,它在這里也沒有影響。

為了解決這個問題,calculate應該跟sum有一樣的上下文,以便于使用numberA和numberB。解決方法之一是使用 <font color=#7FFFD4>.call()</font> 方法

var numbers = {
   numberA: 5,
   numberB: 10,
   sum: function() {
     console.log(this === numbers); // => true
     function calculate() {
       console.log(this === numbers); // => true
       return this.numberA + this.numberB;
     }
     // use .call() method to modify the context
     return calculate.call(this);
   }
};
numbers.sum(); // => 15

方法調用

一個方法是作為一個對象的屬性存儲的函數。例如:

var myObject = {
  // helloFunction is a method
  helloFunction: function() {
    return 'Hello World!';
  }
};
var message = myObject.helloFunction();

方法調用中的 <font color=#7FFFD4>this</font>

在方法調用中,this是擁有這個方法的對象

當調用一個對象上的方法時,this變成這個對象自身。

var calc = {
  num: 0,
  increment: function() {
    console.log(this === calc); // => true
    this.num += 1;
    return this.num;
  }
};
// method invocation. this is calc
calc.increment(); // => 1
calc.increment(); // => 2

構造函數調用

function Country(name, traveled) {
   this.name = name ? name : 'United Kingdom';
   this.traveled = Boolean(traveled); // transform to a boolean
}
Country.prototype.travel = function() {
  this.traveled = true;
};

var france = new Country('France', false);

france.traveled // false

france.travel();

france.traveled // true

從ECMAScript 6開始,JavaScript允許用class關鍵詞來定義構造函數:

class City {
  constructor(name, traveled) {
    this.name = name;
    this.traveled = false;
  }
  travel() {
    this.traveled = true;
  }
}

var paris = new City('Paris', false);
paris.travel();

當屬性訪問 <font color=#7FFFD4>myObject.myFunction</font> 前面有一個 <font color=#7FFFD4>new</font> 關鍵詞時,JavaScript會執行 <font color=#7FFFD4>構造函數調用</font> 而不是原來的方法調用。

例如new myObject.myFunction():它相當于先用屬性訪問把方法提取出來extractedFunction = myObject.myFunction,

然后利用把它作為構造函數創建一個新的對象: new extractedFunction()。

構造函數中的 <font color=#7FFFD4>this</font>

在構造函數調用中this指向新創建的對象

function Foo () {
  console.log(this instanceof Foo); // => true
  this.property = 'Default Value';
}
// Constructor invocation
var fooInstance = new Foo();
fooInstance.property; // => 'Default Value'

陷阱: 忘了new

function Vehicle(type, wheelsCount) {
  this.type = type;
  this.wheelsCount = wheelsCount;
  return this;
}
// Function invocation
var car = Vehicle('Car', 4);
car.type; // => 'Car'
car.wheelsCount // => 4
car === window // => true

你可能以為它正確地創建并初始化了對象。

然而,在函數調用中,<font color=#7FFFD4>this</font><font color=#7FFFD4>window</font> 對象,<font color=#7FFFD4>Vehicle('Car', 4)</font> 實際上是在給 <font color=#7FFFD4>window</font> 對象設置屬性--這是錯的。它并沒有創建一個新的對象。

當你希望調用構造函數時,確保你使用了new操作符:

function Vehicle(type, wheelsCount) {
  if (!(this instanceof Vehicle)) {
    throw Error('Error: Incorrect invocation');
  }
  this.type = type;
  this.wheelsCount = wheelsCount;
  return this;
}
// Constructor invocation
var car = new Vehicle('Car', 4);
car.type // => 'Car'
car.wheelsCount // => 4
car instanceof Vehicle // => true

// Function invocation. Generates an error.
var brokenCat = Vehicle('Broken Car', 3);

在構造函數里我們添加了一個驗證 <font color=#7FFFD4>this instanceof Vehicle</font> 來確保執行的上下文是正確的對象類型。如果this不是Vehicle,那么就會報錯。

隱式調用

當函數被.call()或者.apply()調用時,執行的是隱式調用。

方法.call(thisArg[, arg1[, arg2[, ...]]])將接受的第一個參數thisArg作為調用時的上下文,arg1, arg2, ...這些則 <font color=#7FFFD4>作為參數</font> 傳入被調用的函數。

方法.apply(thisArg, [args])將接受的第一個參數thisArg作為調用時的上下文,并且接受另一個 <font color=#7FFFD4>類似數組的對象[args]</font> 作為被調用函數的參數傳入。

function increment(number) {
  return ++number;
}
increment.call(undefined, 10); // => 11
increment.apply(undefined, [10]); // => 11

隱式調用中的 <font color=#7FFFD4>this</font>

在隱式調用.call()或.apply()中,this是第一個參數

var rabbit = { name: 'White Rabbit' };
function concatName(string) {
  console.log(this === rabbit); // => true
  return string + this.name;
}
// Indirect invocations
concatName.call(rabbit, 'Hello '); // => 'Hello White Rabbit'
concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'

當一個函數應該在特定的上下文中執行時,隱式調用就非常有用。例如為了解決方法調用時,this總是window或strict模式下的undefined的上下文問題。隱式調用可以用于模擬在一個對象上調用某個方法。

綁定函數

綁定函數是一個與對象綁定的函數。通常它是通過在原函數上使用 <font color=#7FFFD4>.bind()</font> 來創建的。原函數和綁定的函數共享代碼跟作用域,但是在執行時有不同的上下文。

方法.bind(thisArg[, arg1[, arg2[, ...]]])接受第一個參數thisArg作為綁定函數執行時的上下文,并且它接受一組可選的參數 arg1, arg2, ...作為被調用函數的參數。它返回一個綁定了thisArg的新函數。

下面的代碼創建了一個綁定函數并在之后調用它:

function multiply(number) {
 'use strict';
  return this * number;
}
// create a bound function with context
var double = multiply.bind(2);
// invoke the bound function
double(3); // => 6
double(10); // => 20
var say = concatName.bind(rabbit);
say('Hello'); // => 'Hello White Rabbit'

綁定函數中的 <font color=#7FFFD4>this</font>

在調用綁定函數時,this是.bind()的第一個參數。

var numbers = {
  array: [3, 5, 10],
  getNumbers: function() {
    return this.array;
  }
};
// Create a bound function
var boundGetNumbers = numbers.getNumbers.bind(numbers);
boundGetNumbers(); // => [3, 5, 10]
// Extract method from object
var simpleGetNumbers = numbers.getNumbers;
simpleGetNumbers(); // => undefined or throws an error in strict mode

numbers.getNumbers函數能在不綁定的情況下賦值給變量simpleGetNumbers。

在之后的函數調用中,<font color=#7FFFD4>simpleGetNumbers()</font><font color=#7FFFD4>this</font><font color=#7FFFD4>window</font> 或者strict模式下的undefined,不是 <font color=#7FFFD4>number</font> 對象。

在這個情況下,simpleGetNumbers()不會正確返回數組。

<font color=#7FFFD4>.bind()</font> 永久性地建立了一個上下文的鏈接,并且會一直保持它。

一個綁定函數不能通過 <font color=#7FFFD4>.call()</font> 或者 <font color=#7FFFD4>.apply()</font> 來改變它的上下文,甚至是再次綁定也不會有什么作用。

只有用綁定函數的構造函數調用方法能夠改變上下文,但并不推薦這個方法(因為構造函數調用用的是常規函數而不是綁定函數)。

下面的例子聲明了一個綁定函數,接著試圖改變它預先定義好的上下文:

function getThis() {
 'use strict';
  return this;
}
var one = getThis.bind(1);
// Bound function invocation
one(); // => 1
// Use bound function with .apply() and .call()
one.call(2); // => 1
one.apply(2); // => 1
// Bind again
one.bind(2)(); // => 1
// Call the bound function as a constructor
new one(); // => Object

只有new one()改變了綁定函數的上下文,其他方式的調用中this總是等于1。

箭頭函數

箭頭函數是 <font color=#7FFFD4>匿名的</font>,這意味著它的name屬性是個空字符串''。

var sumArguments = (...args) => {
   console.log(typeof arguments); // => 'undefined'
   return args.reduce((result, item) => result + item);
};
sumArguments.name // => ''
sumArguments(5, 5, 6); // => 16

箭頭函數中的 <font color=#7FFFD4>this</font>

this是箭頭函數定義時封裝好的上下文

箭頭函數并不會創建它自己的上下文,它從它定義處的外部函數獲得this上下文。下面的例子說明了這個上下文透明的特性:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  log() {
    console.log(this === myPoint); // => true
    setTimeout(()=> {
      console.log(this === myPoint); // => true
      console.log(this.x + ':' + this.y); // => '95:165'
    }, 1000);
  }
}
var myPoint = new Point(95, 165);
myPoint.log();

setTimeout在調用箭頭函數時跟 <font color=#7FFFD4>log()</font> 使用了相同的上下文( <font color=#7FFFD4>myPoint對象</font> )。

正如所見,箭頭函數從它定義處“繼承”了函數的上下文。如果在這個例子里嘗試用常規函數,它會建立自己的上下文( <font color=#7FFFD4>window</font><font color=#7FFFD4>undefined</font> )。

所以,為了讓同樣的代碼能在函數表達式中正確運行,需要手動綁定上下文: <font color=#7FFFD4>setTimeout(function() {...}.bind(this)</font> )。

這樣一來就顯得很啰嗦,不如用箭頭函數來得簡短。

如果箭頭函數定義在最上層的作用域(在所有函數之外),那么上下文就總是全局對象(瀏覽器中的window對象):

var getContext = () => {
   console.log(this === window); // => true
   return this;
};
console.log(getContext() === window); // => true

箭頭函數會一勞永逸地綁定詞法作用域。即使使用修改上下文的方法,this也不能被改變:

var numbers = [1, 2];
(function() {
  var get = () => {
    console.log(this === numbers); // => true
    return this;
  };
  console.log(this === numbers); // => true
  get(); // => [1, 2]
  // Use arrow function with .apply() and .call()
  get.call([0]); // => [1, 2]
  get.apply([0]); // => [1, 2]
  // Bind
  get.bind([0])(); // => [1, 2]
}).call(numbers);

一個函數表達式通過.call(numbers)被隱式調用了,這使得這個調用的this變成了numbers。這樣一來,箭頭函數get的this也變成了numbers,因為它是從詞法上獲得的上下文。

無論get是怎么被調用的,它一直保持了一開始的上下文numbers。用其他上下文的隱式調用(通過.call()或.apply())或者重新綁定(通過.bind())都不會起作用

箭頭函數不能用作構造函數。如果像構造函數一樣調用new get(), JavaScript會拋出異常:TypeError: get is not a constructor。

陷阱: 用箭頭函數定義方法

function Period (hours, minutes) {
  this.hours = hours;
  this.minutes = minutes;
}
Period.prototype.format = () => {
  console.log(this === window); // => true
  return this.hours + ' hours and ' + this.minutes + ' minutes';
};
var walkPeriod = new Period(2, 30);
walkPeriod.format(); // => 'undefined hours and undefined minutes'

由于format是一個箭頭函數,并且它定義在全局上下文(最頂層的作用域)中,它的this指向window對象。即使format作為方法在一個對象上被調用如walkPeriod.format(),window仍然是這次調用的上下文。之所以會這樣是因為箭頭函數有靜態的上下文,并不會隨著調用方式的改變而改變。

函數表達式可以解決這個問題,因為一個常規的函數會隨著調用方法而改變其上下文:

function Period (hours, minutes) {
  this.hours = hours;
  this.minutes = minutes;
}
Period.prototype.format = function() {
  console.log(this === walkPeriod); // => true
  return this.hours + ' hours and ' + this.minutes + ' minutes';
};
var walkPeriod = new Period(2, 30);
walkPeriod.format(); // => '2 hours and 30 minutes'

結論

因為函數調用對this有最大的影響,從現在起,不要再問你自己:

this是從哪里來的?

而要問自己:

函數是怎么被調用的?

對于箭頭函數,問問你自己:

在這個箭頭函數被定義的地方,this是什么?

這是處理this時的正確想法,它們可以讓你免于頭痛。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容