捕捉JavaScript中this的指向

JavaScript的this機制很複雜,雖然從一開始從事前端工做就和它打交道,但一直未能弄清楚,道明白。在工做中遇到this相關問題,就知道var self = this,一旦去面試遇到各類this相關面試題目時腦子就一片空白,拿不定結果。本文綜合了一些書籍和網上文章對this的分析和講解,提供一些實例來分析各類場景下this是如何指向的。javascript

全局做用域

在瀏覽器宿主環境中,this指向window對象,而且在全局做用域下,使用var聲明變量其實就至關於操做全局thishtml

this === window; // true

var foo = 'bar';
this.foo === window.foo; // true

在嚴格模式下,this會綁定到undefined前端

var a = 2;
function foo() {
  'use strict';
  
  console.log(this.a);
}

foo(); // TypeError: this is not undefined

若是在變量的聲明過程沒有使用let或者var,會隱式建立一個全局變量,但這個變量和普通全局變量的區別在於它是做爲window的一個屬性建立的。兩者在使用delete操做符上有明顯的區別:變量不能夠刪除,而對象的屬性是能夠刪除的java

var a = 2;
b = 3;
a; // 2
b; // 3
delete a;
delete b;
a; // 2
b; // Uncaught ReferenceError: b is not defined

局部做用域

這裏的做用域主要是指在對象函數中的this指向。面試

函數調用

做爲函數調用時,函數中的this默認指向window瀏覽器

var a = 1;
function foo() {
  console.log(this.a);
}

foo(); // 1

若是在當即執行函數中使用了this,它一樣指向windowapp

var a = 1;
(function() {
  var a = 2;
  console.log(this.a);
})(); // 1

方法調用

做爲方法調用時,函數中的this老是指向方法所在的對象。函數

var obj = {
  a: 1,
  foo: function() {
    console.log(this.a);
  }
}

obj.foo();

構造函數調用

構造函數調用將一個全新的對象做爲this變量的值,並隱式返回這個新對象做爲調用結果。也就是說指向新生成的實例。this

function Foo(name) {
  this.name = name;
  this.getName = function() {
    console.log(this.name);
  }
}

var a = new Foo('a');
a.getName(); // "a"

使用call和apply方法

能夠經過call()apply()方法顯示改變函數的this指向。prototype

var a = 1;
var obj = {
  a: 2
}
function foo() {
  console.log(this.a);
}

foo(); // 1
foo.call(obj); // 2
foo.apply(obj); // 2

使用bind方法

bind()方法建立一個新的函數, 當被調用時,將其this關鍵字設置爲提供的值,在調用新函數時,在任何提供以前提供一個給定的參數序列,而後返回由指定的this值和初始化參數改造的原函數拷貝。

var a = 1;
var obj = {
  a: 2
}
function foo() {
  console.log(this.a);
}

var bar = foo.bind(obj);
bar();

箭頭函數中調用

ES6引入了箭函數的概念,在箭頭函數中因爲沒有this綁定,因此它的默認指向是由外圍最近一層非箭頭函數決定的。

var a = 1;
function Foo(a) {
  this.a = a;
  this.getA = function() {
    var x = () => {
      this.a = 3; // 改變了外圍函數Foo屬性a的值
      console.log(this.a); // 3
    }
    x();
    console.log(this.a); // 3
  }
}

var foo = new Foo(1);
foo.getA();

問題的產生

上面列舉了在正常狀況下this的指向結果。可是在實際開發過程當中,對於不一樣場景,不一樣的聲明方式、調用方式、賦值和傳值方式都會影響到this的具體指向。

調用方式引發的改變

函數的調用方式最多見的是方法調用構造函數調用,或者使用apply/bind/call調用,也能夠是當即執行函數。

var a = 10;
var obj = {
  a: 20,
  fn: function() {
    var a = 30;
    console.log(this.a);
  }
}

obj.fn(); // 20
obj.fn.call(); // 10
(obj.fn)(); // 20
(obj.fn, obj.fn)(); // 10
(obj.fn = obj.fn)(); // 10
new obj.fn(); // undefined

對於applycall第一個參數若是不傳或者傳遞undefinednull則默認綁定到全局對象,因此obj.fn.call()的調用實際上把this指向了window對象。

對於(obj.fn)(),咋一看,是當即執行函數,那麼它的this確定指向了window對象,其實否則,這裏obj.fn只是一個obj對象方法的引用,並無改變this的指向。

對於(obj.fn, obj.fn)(),這種操做比較少見,工做中也不會去這樣寫。這裏首先咱們須要瞭解逗號操做符會對每一個操做數求值,並返回最後一個操做數的值,其次是這裏使用了逗號操做符,裏面必然是一個表達式,這種狀況下里面的函數this指向其實已經改變了,指向了全局。對於(obj.fn = obj.fn)()this一樣指向全局。所以能夠大膽猜想:若是(x)();中x是一個表達式,而且返回一個函數(引用),那麼函數x中的this指向全局window。這裏還更多的方式來達到一樣目的,好比:(true && obj.fn)() 或者 (false || obj.fn)()。總的來講,咱們經過這種方式建立了一個函數的「間接引用」,從而致使函數綁定規則的改變。

對於new obj.fn()的結果其實也沒有什麼好說的,函數使用new操做符調用後返回一個新的實例對象,因爲該對象並無一個叫a的屬性,因此返回undefined

函數做爲參數(變量)傳遞時

不少時候,函數的定義在一個地方,而對象定義方法時只是引用了該函數。一樣在調用對象方法時,先把它賦值給一個變量(別名),而後使用函數別名進行調用。使用時有可能致使this綁定的改變。

示例一

var a = 10;
function foo() {
  console.log(this.a);
}

var obj = {
  a: 20,
  foo: foo
}

var bar = obj.foo; // 函數別名
bar(); // 10

雖然barobj.foo的一個引用,可是實際上,它引用的是foo函數自己,所以應用了函數的默認綁定規則。

示例二

var a = 10;
function foo() {
  console.log(this.a);
}

function doFoo(cb) {
  cb(); // cb 實際上引用的仍是foo
}

var obj = {
  a: 20,
  foo: foo
}

doFoo(obj.foo); // 10
setTimeout(obj.foo, 100); // 10

這裏咱們將obj.foo以參數的形式傳遞給函數doFoo和內置函數setTimeout。參數傳遞實際上就是一種賦值,和示例一的結果是同樣的。所以,調用回調函數的函數會丟失this的指向

改變構造函數的默認返回對象

構造函數使用new操做符調用後會返回一個新的實例對象,可是在定義構造函數時,能夠在函數中返回任何值來覆蓋默認該返回的實例,這樣一來極可能致使實例this的指向改變。

var a = 10;
function f() {
  this.a = 20;
  function c() {
    console.log(this.a);
  }
  return c();
}

new f(); // 10

這裏咱們將構造函數foo的默認返回值改爲返回一個函數c執行後的結果。當調用new f()後,內部函數c中的this實際上指向的是全局。可是若是咱們將return c()改爲return new c()的話,那麼new foo()執行的結果是返回一個構造函數c的實例,因爲實例對象中並無屬性a,所以結果爲undefined

方法的接收者引發的問題

在方法的調用中由調用表達式自身來肯定this變量的綁定。綁定的this變量的對象被稱爲調用接收者

var buffer = {
  entries: [],
  add: function(s) {
    this.entries.push(s);
  },
  concat: function() {
    return this.entries.join('');
  }
}

var source = ['123', '-', '456'];
source.forEach(buffer.add); // Uncaught TypeError: Cannot read property 'push' of undefined

因爲方法buffer.add()的接收者不是buffer自己,而是forEach方法。事實上,forEach方法的實現使用全局對象做爲默認的接收者。因爲全局沒有entries屬性,所以會拋出一個錯誤。

要解決上面的問題,一個是使用forEach方法提供的可選參數做爲函數的接收者。

source.forEach(buffer.add, buffer);

其次是使用bind方法來指定接收者

source.forEach(buffer.add.bind(buffer));

對象的實例屬性和原型屬性

這裏想要說明的是,在一個對象的實例中,this便可以訪問實例對象的值,也能夠獲取原型上的值。

function Foo() {}
Foo.prototype.name = 'bar';
Foo.prototype.logName = function() {
  console.log(this.name);
}
Foo.prototype.setName = function(name) {
  this.name = name;
}
Foo.prototype.deleteName = function() {
  delete this.name;
}

var foo = new Foo();
foo.setName('foo');
foo.logName(); // "foo"

foo.deleteName();
foo.logName(); // "bar"

delete foo.name;
foo.logName(); // "bar"

當執行foo.setName('foo')後,給實例對象foo增長了一個屬性name,同時覆蓋了原型中的同名屬性。當執行foo.deleteName()時,其實是將新增值刪除了,還原了初始狀態。執行delete foo.name時,試圖刪除的仍是新增的屬性,可是如今已經不存在這個值了。若是須要刪除原始值,能夠經過delete foo.__proto__.name來實現。

總結

本文只是介紹了一部分有關this的問題,更多知識點能夠參考《詳解this》以及MDNthis

相關文章
相關標籤/搜索