理解 JavaScript this

這是本系列的第 5 篇文章。javascript

還記得上一篇文章中的閉包嗎?點擊查看文章 理解 JavaScript 閉包前端

在聊 this 以前,先來複習一下閉包:java

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    return function () {
      return 'Hi! My name is ' + this.name;
    }
  }
};

person.sayHi()(); // "Hi! My name is Neil"

上一篇文章說,咱們能夠把閉包簡單地理解爲函數返回函數。因此這裏的閉包結構是:segmentfault

// ...
function () {
  return 'Hi! My name is ' + this.name;
}
// ...

可是你有沒有發現,這個函數執行的結果是 「Hi! My name is Neil」 。等等,我不是叫 Leo 嗎?怎麼給我改了個名字?!瀏覽器

我一分析,原來是 this 在其中做祟,且聽我慢慢道來這「更名的由來」。閉包

§ this 從何而來

首先,你得確保你已經清楚執行棧與執行上下文的知識。點擊查看文章 理解 JavaScript 執行棧app

ECMAScript 5.1 中定義 this 的值爲執行上下文中的 ThisBinding。而 ThisBinding 簡單來講就是由 JS 引擎建立並維護,在執行時被設置爲某個對象的引用函數

在 JS 中有三種狀況能夠建立上下文:初始化全局環境、eval() 和執行函數。this

§ 全局中的 this

var num = 1;

function getName () {
  return "Leo";
}

this.num; // 1
this.getName(); // Leo

this == window; // true

當咱們在瀏覽器中運行這段代碼,JS 引擎會將 this 設置爲 window 對象。而聲明的變量和函數被做爲屬性掛載到 window 對象上。固然,在嚴格模式下,全局中 this 的值設置爲 undefined。spa

"use strcit";

var num = 1;
function getName () {
  return "Leo";
}

this.num; // TypeError
this.getName(); // TypeError

this == undefined; // true

開啓嚴格模式後,全局 this 將指向 undefined,因此調用 this.num 會報錯。

§ eval() 中的 this

eval() 不被推薦使用,我如今對其也不太熟悉,這裏嘗試着說一下。初學者能夠直接跳到下一節。

結合所查閱的資料,目前我對 eval() 的理解以下:

eval(...) 直接調用,被理解爲是一個 lvalue,也有說是 left unchanged,字面理解爲餘下不變。什麼是「餘下不變」?我理解爲直接調用 eval(...),其中代碼的執行環境不變,依舊爲當前環境,this 也依舊指向當前環境中的調用對象。

而使用相似 (1, eval)(...) 的代碼,被稱爲間接調用。(1, eval) 是一個表達式,你能夠這樣認爲 (true && eval) 或者 (0 : 0 ? eval)。間接調用的 eval 始終認爲其中的代碼執行在全局環境,將 this 綁定到全局對象。

var x = 'outer';
(function() {
  var x = 'inner';
  // "direct call: inner"
  eval('console.log("direct call: " + x)');
  // "indirect call: outer"
  (1, eval)('console.log("indirect call: " + x)');
})();

關於 eval(),如今不敢肯定,若有錯誤,歡迎指正。

§ 函數中的 this

◆ 通常狀況

首先,咱們須要明確的是,在 JS 中函數也屬於對象,它能夠擁有屬性,this 就是函數在執行時得到的屬性。通常狀況下,在全局環境中直接調用函數,函數中的 this 會在調用時被 JS 引擎設置爲全局對象 window(一樣在嚴格模式下爲 undefined)。

var name = "Leo";

function getName() {
  var name = "Neil";
  console.log(this); // [object Window]
  return this.name;
}

getName(); // Leo

◆ 做爲對象的方法

函數能夠做爲對象的方法被該對象調用,那麼這種狀況 this 會被設置爲該對象。

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    console.log(this); // person
    return 'Hi! My name is ' + this.name;
  }
};

person.sayHi(); // "Hi! My name is Leo"

當 person 對象調用 sayHi() 方法時,this 被指向 person。

◆ 特殊的內置函數

JS 還提供了一種供開發者自定義 this 的方式,它提供了 3 種方式。

  • Function.prototype.call(thisArg, argArray)
  • Function.prototype.apply(thisArg [, arg1 [, args2, ...]])
  • Function.prototype.bind(thisArg [, arg1 [, args2, ...]])

咱們能夠經過設置 thisArg 的值,來自定義函數中 this 的指向。

var leo = {
  name: 'Leo',
  sayHi: function () {
    return "Hi! My name is " + this.name;
  }
}

var neil = {
  name: 'Neil'
};

leo.sayHi(); // "Hi! My name is Leo"
​leo.sayHi.call(neil); // "Hi! My name is Neil"

這裏,咱們經過 call() 將 sayHi() 中 this 的指向綁定爲 neil 對象,從而取代了默認 的 this 指向 leo 對象。

關於函數的 call(), apply(), bind() 我將在後面另寫一篇文章,敬請期待。

§ this 引發的使人費解的現象

◆ 閉包

經過前面的介紹,我想你對 this 已經有了初步的印象。那麼,回到文章開頭的問題,this 是怎麼改變了個人名字?換句話說,this 在閉包的影響下指向發生了怎樣的變更?

再看一下代碼:

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    return function () {
      return 'Hi! My name is ' + this.name;
    }
  }
};

person.sayHi()(); // "Hi! My name is Neil"

經過上一篇文章 理解 JavaScript 閉包,函數返回函數會造成閉包。在這種狀況下,閉包每每所執行的環境與所定義的環境不一致,而 this 的值倒是在執行時決定的。因此,當上面代碼中的閉包在執行時,它所在的執行上下文是全局環境,this 將被設置爲 window(嚴格模式下爲 undefined)。

怎麼解決?咱們能夠利用 call / apply / bind 來修改 this 的指向。

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    return function () {
      return 'Hi! My name is ' + this.name;
    }
  }
};

person.sayHi().call(person); // "Hi! My name is Leo"

這裏利用 call() 將 this 指向 person。OK,個人名字回來了,「Hi! My name is Leo」 ^^

固然,咱們還有第二種解決方法,閉包的問題就讓閉包本身解決。

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    var that = this; // 定義一個局部變量 that
    return function () {
      return 'Hi! My name is ' + that.name; // 在閉包中使用 that
    }
  }
};

person.sayHi()(); // "Hi! My name is Leo"

在 sayHi() 方法中定義一個局部變量,閉包能夠將這個局部變量保存在內存中,從而解決問題。

◆ 回調函數

在回調函數中 this 的指向也會發生變化。

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    return 'Hi! My name is ' + this.name;
  }
};

var btn = document.querySelector('#btn');

btn.addEventListener('click', person.sayHi);
// "Hi! My name is undefined"

這裏 this 既不指向 person,也不指向 window。那它指向什麼?

btn 對象,它是一個 DOM 對象,有一個 onclick 方法,在這裏定義爲 person.sayHi。

{
  // ...
  onclick: person.sayHi
  // ...
}

因此,當咱們執行上面的代碼,this.name 的值爲 undefined,由於 btn 對象上沒有定義 name 屬性。咱們給 btn 對象自定義一個 name 屬性來驗證一下。

var btn = document.querySelector('#btn');

btn.name = 'Jackson';

btn.addEventListener('click', person.sayHi);
// "Hi! My name is Jackson"

緣由說清楚了,解決方案一樣可用過 call / apply / bind 來改變 this 的指向,使其綁定到 person 對象。

btn.addEventListener('click', person.sayHi.bind(person));
// "Hi! My name is Leo"

◆ 賦值

var name = 'Neil';

var person = {
  name: 'Leo',
  sayHi: function() {
    return 'Hi! My name is ' + this.name;
  }
};

person.sayHi(); // "Hi! My name is Leo"

var foo = person.sayHi;

foo(); // "Hi! My name is Neil"

當把 person.sayHi() 賦值給一個變量,這個時候 this 的指向又發生了變化。由於 foo 執行時是在全局環境中,因此 this 指向 window(嚴格模式下指向 undefined)。

一樣,咱們能夠經過 call / apply / bind 來解決,這裏就不貼代碼了。

§ 別忘了 new

在 JS 中,咱們聲明一個類,而後 new 一個實例。

function Person(name) {
  this.name = name;
}

var her = Person('Angelia');
console.log(her.name); // TypeError

var me = new Person('Leo');
console.log(me.name); // "Leo"

若是咱們直接把調用這個函數,this 將指向全局對象,Person 在這裏就是一個普通函數,沒有返回值,默認 undefined,而嘗試訪問 undefined 的屬性就會報錯。

若是咱們使用 new 操做符,那麼 new 其實會生成一個新的對象,並將 this 指向這個新的對象,而後將其返回,因此 me.name 能打印出 「Leo」。

關於 new 的原理,我會在後面的文章分享,敬請期待。

§ 小結

你看,this 是否是變幻無窮。可是咱們得以不變應萬變。

在這麼多場景下,this 的指向萬變不離其宗:它必定是在執行時決定的,指向調用函數的對象。在閉包、回調函數、賦值等場景下咱們均可以利用 call / apply / bind 來改變 this 的指向,以達到咱們的預期。

接下來,請期待文章《理解 JavaScript call/apply/bind》。

◆ 文章參考

§ JavaScript 系列文章

理解 JavaScript 閉包

理解 JavaScript 執行棧

理解 JavaScript 做用域

理解 JavaScript 數據類型與變量

Be Good. Sleep Well. And Enjoy.

原文發佈在個人公衆號 camerae,點擊查看

clipboard.png

前端技術 | 我的成長

相關文章
相關標籤/搜索