關於 this 你想知道的一切都在這裏

不管在 javascript 的平常使用中仍是前端面試過程當中,this 的出鏡率都極高。這無疑說明了,this 的重要性。可是 this 很是靈活,致使不少人以爲 this 的行爲難以理解。本文從爲何要有 this 做爲切入點,總結了 this 的六大規則,但願能幫助你解答困惑。javascript

簡介

this 實際上至關於一個參數,這個參數多是開發中手動傳入的,也多是 JS 或者第三方傳入的。
這個參數,一般指向的是函數執行時的「擁有者」。this 的機制,可讓函數設計的更加簡潔,而且複用性更好。html

this 是在函數執行時進行綁定的,綁定規則一共六條,分別是:前端

  • new 綁定:使用 new 關鍵字建立對象時,this 會綁定到建立的對象上。java

  • 顯式綁定:使用 callapplybind 方法顯式綁定時, this 爲其第一個參數。node

  • 隱式綁定:當函數掛在對象上執行時,系統會隱式地將 this 綁定到該對象上。git

  • 默認綁定:當函數獨立執行時,嚴格模式 this 的默認綁定值爲 undefined,不然爲全局對象。github

  • 箭頭函數綁定:使用箭頭函數時,this的綁定值等於其外層的普通函數(或者全局對象自己)的this面試

  • 系統或第三方綁定:當函數做爲參數,傳入系統或者第三方提供的接口時,傳入函數中的 this 是由系統或者第三方綁定的。數組

this 的做用

this 的機制提供了一個優雅的方式,隱式地傳遞一個對象,這可讓函數設計的更加簡潔,而且複用性更好。瀏覽器

考慮下面一個例子,有兩個按鈕,點擊後將其背景改成紅色。

function changeBackgroundColor(ele) {
  ele.style.backgroundColor = 'red';
}

btn1.addEventListener('click',function () {
  changeBackgroundColor(btn1);
});
btn2.addEventListener('click',function () {
  changeBackgroundColor(btn2);
});

在這裏,咱們顯式地將被點擊的元素傳遞給了 changeBackgroundColor 函數。但實際上,這裏能夠利用 this 隱式傳遞上下文的特色,直接在函數獲取當前被點擊的元素。以下:

function changeBackgroundColor() {
    this.style.backgroundColor = 'red';
}

btn1.addEventListener('click',changeBackgroundColor);
btn2.addEventListener('click',changeBackgroundColor);

在第一個例子中,被點擊元素是經過 ele ,這個形式參數來代替的。而在第二個例子中,是經過一個特殊的關鍵字 this 來代替。this 它的做用和形式參數相似,其本質上是一個對象的引用,它的特殊性在於不須要手動傳值,因此使用起來會更加簡單和方便。

六大規則

在實際使用中, this 究竟指向哪一個對象是最使人困惑的。本文歸類了六類情景,總結六條 this 的綁定規則。

new 綁定

使用 new 建立對象的時候,類中的 this 指的是什麼?

class Person {
  constructor(name){
    this.name = name;
  }

  getThis(){
    return this
  }
}


const xiaoMing = new Person("小明");

console.log(xiaoMing.getThis() === xiaoMing); // true
console.log(xiaoMing.getThis() === Person); // false
console.log(xiaoMing.name === "小明"); // true

在上面例子中,使用了 ES6 的語法建立了 Person 類。在使用 new 關鍵字建立對象的過程當中,this 會由系統自動綁定到建立的對象上,也就是 xiaoMing

規則一:在使用 new 關鍵字建立對象時,this 會綁定到建立的對象上。

顯式綁定

情景二,使用 callapplybind 方法,顯式綁定 this 參數。

call 爲例,call 方法的第一個傳入的參數,是 this 引用的對象。

function foo() {
  console.log( this === obj ); // true
  console.log( this.a === 2 ); // true
}

const obj = {
  a: 2
};

foo.call( obj );

在顯式傳遞的狀況下,this 指向的對象很明顯,就是 callapplybind 方法的第一個參數。

規則二:使用 callapplybind 方法顯式綁定時, this 爲其第一個參數。

隱式綁定

隱式綁定和顯式綁定不一樣的地方在於,顯式綁定由開發者來指定 this;而隱式綁定時,函數或方法都會有一個「擁有者」,這個「擁有者」指的是直接調用的函數或方法對象。

例一

先看一個最簡單的例子。

function bar() {
  console.log( this === obj );
}

const obj = {
  foo: function () {
    console.log( this === obj );
  },
  bar: bar
};

obj.foo(); // true
obj.bar(); // true

函數 foo 是直接掛在對象 obj 裏面的,函數 bar 是在外面定義的,而後掛在對象 obj 上的。不管函數是在何處定義,但最後函數調用時,它的「擁有者」是 obj。因此 this 指向的是函數調用時的「擁有者」 obj

例二

爲了更加深刻的理解,再考慮函數從新賦值到新的對象上的狀況,來看看下面的例子。

function bar() {
  console.log( this === obj1 ); // false
  console.log( this === obj2 ); // true
}

const obj1 = {
  foo: function () {
    console.log( this === obj1 ); // false
    console.log( this === obj2 ); // true
  },
  bar: bar
};

const obj2 = {
  foo: obj1.foo,
  bar: obj1.bar
};

obj2.foo();
obj2.bar();

在該例子中,將 obj1 中的 foobar 方法賦值給了 obj2。函數調用時,「擁有者」是 obj2,而不是 obj1。因此 this 指向的是 obj2

例三

對象能夠多層嵌套,在這種狀況下執行函數,函數的「擁有者」是誰呢?

const obj1 = {
  obj2: {
    foo: function foo() {
      console.log( this === obj1 );      // false
      console.log( this === obj1.obj2 ); // true
    }
  }
};

obj1.obj2.foo()

foo 方法/函數中的直接調用者是 obj2,而不是 obj1,因此函數的「擁有者」指向的是離它最近的直接調用者。

例四

若是一個方法/函數,在它的直接對象上調用執行,又同時執行了 call 方法,那麼它是屬於隱式綁定仍是顯式綁定呢?

const obj1 = {
  a: 1,
  foo: function () {
    console.log(this === obj1); // false
    console.log(this === obj2); // true
    console.log(this.a === 2);  // true
  }
};

const obj2 = {
  a: 2
};

obj1.foo.call(obj2); // true

由上,能夠看出,若是顯式綁定存在,它就不可能屬於隱式綁定。

規則三:若是函數是掛在對象上執行的,這個時候系統會隱式的將 this 綁定爲函數執行時的「擁有者」。

默認綁定

前一小段,討論了函數做爲對象的方法執行時的狀況。本小段,要討論的是,函數獨立執行的狀況。

在函數直接調用的狀況下,this 綁定的行爲,稱之爲默認綁定。

例一

爲了簡單起見,先討論在瀏覽器的非嚴格模式的下綁定行爲。

function foo() {
  console.log( this === window); // true
}

foo();

在上面的例子中,系統將 window 默認地綁定到函數的 this 上。

例二

在這裏,先介紹一種咱們可能會在代碼中見到的顯式綁定 null 的寫法。

function foo() {
  console.log( this == window ); // true
}

foo.apply(null);

將例一默認綁定的狀況,改成了顯式綁定 null 的狀況。

在實際開發中,咱們可能會用到 apply 方法,並在第一個參數傳入 null 值,第二個參數傳入數組的方式來傳遞數組類型的參數。這是一種傳統的寫法,固然如今能夠用 ES6 的寫法來代替,可是這不在本文的討論範圍內。

在本例最須要關注的是,this 居然指向的 window 而不是 null。我的測試的結果是,在函數獨立調用時,或者顯式調用,傳入的值爲 nullundefined 的狀況下,會將 window 默認綁定到 this 上。

在函數屢次調用,造成了一個調用棧的狀況下,默認綁定的規則也是成立的。

例三

接着,探討下嚴格模式下,this 的默認綁定的值。

"use strict";

function foo() {
  console.log( this === undefined );
}

foo();               // true
foo.call(undefined); // true
foo.call(null);      // false

在嚴格模式下,this 的默認綁定的值爲 undefined

規則四:在函數獨立執行的狀況下,嚴格模式 this 的默認綁定值爲 undefined,不然默認綁定的值爲 window

箭頭函數綁定

箭頭函數實際上,只是一個語法糖,實際上箭頭函數中的 this 其實是其外層函數(或者 window/global 自己)中的 this

// ES6
function foo() {
  setTimeout(() => {
    console.log(this === obj); // true
  }, 100);
}

const obj = {
  a : 1
}

foo.call(obj);

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log(_this === obj); // true
  }, 100);
}

var obj = {
  a : 1
}

foo.call(obj);

規則五:使用箭頭函數時,this 的綁定值和其外層的普通函數(或者 window/global 自己) this 綁定值相同。

系統或第三方綁定

在 JavaScript 中,函數是第一公民,能夠將函數以值的方式,傳入任何系統或者第三方提供的函數中。如今討論,最後一種狀況。當將函數做爲值,傳入系統函數或者第三方函數中時,this 到底是如何綁定的。

咱們在文章一開始提到的,兩個按鈕例子,系統自動將 this 綁定爲點擊的按鈕。

function changeBackgroundColor() {
    console.log(this === btn1); // true
}

btn1.addEventListener('click',changeBackgroundColor);

接着測試系統提供的 setTimeout 接口在瀏覽器和 node 中綁定行爲。

// 瀏覽器
setTimeout(function () {
  console.log(this === window); // true
},0)

// node
setTimeout(function () {
  console.log(this === global); // false
  console.log(this); // Timeout
},0)

很神奇的是,setTimeout 在 node 和瀏覽器中的綁定行爲不一致。若是咱們將 node 的中的 this 打印出來,會發現它綁定是一個 Timeout 對象。

若是是第三發提供的接口,狀況會更加複雜。由於在其內部,會將什麼值綁定到傳入的函數的 this 上,事先是不知道的,除非查看文檔或者源碼。

系統或者第三方,在其內部,可能會使用前面的五種規則一種或多種規則,對傳入函數的 this 進行綁定。因此,規則六,實際上一條在由前五條規則上衍生出來的規則。

規則六:調用系統或者第三方提供的接口時,傳入函數中的 this 是由系統或者第三方綁定的。

參考文章:

You-Dont-Know-JS

The this keyword

MDN this


後期補充

查完規範後,用僞代碼再總結一下。

規範地址:

Construct:http://www.ecma-international...
Function Objects:http://www.ecma-international...
Function Calls:http://www.ecma-international...
ArrowFunction:http://www.ecma-international...

僞代碼

if (`newObj = new Object()`) {
  this = newObj
} else if (`bind/call/apply(thisArgument,...)`) {
  if (`use strict`) {
    this = thisArgument
  } else {
    if (thisArgument == null || thisArgument == undefined) {
      this = window || global
    } else {
      this = ToObject(thisArgument)
    }
  }
} else if (`Function Call`) {
  if (`obj.foo()`) {
    // base value . Reference = base value + reference name + strict reference
    // 例外: super.render(obj).  this = childObj ?
    this = obj 
  } else if (`foo()`) {
    // 例外: with statement. this = with object
   
    this = `use strict` ? undefined : window || global
  }
}
相關文章
相關標籤/搜索