JavaScript中this總結

JavaScript中的this其實是在函數被調用時發生的綁定,它指向什麼徹底取決於函數在哪裏被調用。javascript

先來列舉一下都有哪些函數調用方式:java

  • 普通函數調用
  • 對象方法調用
  • call()、apply() 調用
  • bind()
  • 構造函數調用
  • ES6箭頭函數

通俗來講,「函數被誰調用,this就指向誰」。git

普通函數調用

函數在全局做用域下運行;在非嚴格模式下,this指向全局對象window,在嚴格模式下,this會變成undefinedes6

function foo1(){
  return this;
}

foo1() === window; // true 

function foo2(){
  'use strict';
  return this;
}

foo2() === undefined; // true

對象方法調用

當以對象裏的方法的方式調用函數時,它們的this是調用函數的對象。github

function foo() {
  console.log(this); // {x: 1, foo: ƒ}
  console.log(this.x); // 1
}

var obj = {
  x: 1,
  foo: foo
};

obj.foo();

上面代碼中,調用foo的是對象obj ,因此這裏的this指向對象obj數組

若是將對象裏的foo方法賦給一個變量:app

function foo() {
  console.log(this); // window
  console.log(this.x); // undefined
}

var obj = {
  x: 1,
  foo: foo
};

var fn = obj.foo;
fn();

obj.foo賦給了fn變量,而fn是在全局做用域中運行,所以this指向window,因爲window下沒有x這個變量,因此this.xundefined函數

若是把obj.foo傳遞給一個本身聲明的函數。this

function foo() {
  console.log(this); // window
  console.log(this.x); // undefined
}

var obj = {
  x: 1,
  foo: foo
};

// 自定義函數
function todoFn(cb) {
  cb();    
};

todoFn(obj.foo);

obj.foo傳遞給自定義函數做爲回調函數執行。結果同樣。由於它仍然是在全局做用域中運行。prototype

最後來看看在setTimeout()中又會輸出怎樣的結果。

function foo() {
  console.log(this); // {x: 1, foo: ƒ}
  setTimeout(function(){
    console.log(this); // window
  }, 0);
}

var obj = {
  x: 1,
  foo: foo
}

obj.foo();

第一個this仍是對象方法調用方式;但在setTimeout()裏的this指向的是全局做用域window。爲何會這樣?

緣由是,由setTimeout()調用的代碼運行在與所在函數徹底分離的執行環境上,該函數是在全局做用域中執行的,因此this就指向 window

思考一個問題。

上面代碼的後三段代碼中,都出現了一樣的問題,就是它們的this都丟失了。怎麼作才能讓它們的this仍然指向原來的對象?

後面即將提到的call()apply()bind()方法都能解決這一問題。

call()、apply() 調用

使用call()或者apply()能夠改變上下文的this值。

// 定義一個對象
var obj = {
  x: 'local'
};

// 定義一個全局變量
var x = 'global';

// 定義函數,返回值取決於調用方式
function speak(){
  return this.x;
}

speak(); // "global"
speak.call(obj); // "local"
speak.apply(obj); // "local"

直接調用sayName()時,至關於普通函數調用,this指向window,值爲global;使用call()apply()做用一致,都讓this指向了對象obj。只不過在給call()apply()傳遞參數時有區別,如:

function add(c, d) {
  return this.a + this.b + c + d;
}

var obj = {
  a: 1,
  b: 2
};

// 第一個參數做爲 this 使用的對象,然後續參數做爲參數傳遞給函數
add.call(obj, 3, 4); // 10

// 第一個參數一樣是做爲 this 使用的對象
// 第二個參數是傳遞一個數組
add.apply(obj, [3, 4]); // 10

bind() 方法

Function.prototype.bind()會建立一個新的包裝函數,這個函數會忽略它當前的this綁定(不管綁定的對象是什麼), 並把咱們提供的對象綁定到this上。

var x = 9;
var obj = {
  x: 81,
  getX: function() {
    return this.x;
  }
};

// 至關於對象方法調用,this指向對象 o
obj.getX(); // 81

// 將對象 o 的方法getX賦值給 f,f 執行時是在全局環境中,this指向全局做用域
var f = obj.getX;
f(); // 9

// 建立一個新函數,將this綁定到對象 o 中
var f2 = obj.getX.bind(o);
f2(); // 81

接着上面【對象方法調用】裏的幾個丟失this的例子。如何用call()
apply()bind()來綁定this到指定的對象。

先看看這個例子:

function foo() {
  console.log(this); // {x: 1, foo: ƒ}
  console.log(this.x); // 1
}

var obj = {
  x: 1,
  foo: foo
};

// 使用call()或apply()
var fn1 = obj.foo;
fn1.call(obj);
//fn.apply(obj);

// 使用bind()
var fn2 = obj.foo.bind(obj);
fn2();

再看這個回調函數的例子:

function foo() {
  console.log(this); // {x: 1, foo: ƒ}
  console.log(this.x); // 1
}

var obj = {
  x: 1,
  foo: foo
};

// 自定義函數
function todoFn(cb) {
  cb.call(obj);
  // cb.apply(obj);
};

todoFn(obj.foo);

只須要在執行回調函數的時候使用call()apply()this綁定到obj對象便可。

最後看定時器的例子:

function foo() {
  console.log(this); // {x: 1, foo: ƒ}
  setTimeout(function(){
    console.log(this); // {x: 1, foo: ƒ}
  }.bind(this), 0);
}

var obj = {
  x: 1,
  foo: foo
}

obj.foo();

這裏是使用了bind()方法,將上下文的this綁定到定時器裏的匿名函數,它最終仍是指向對象obj

構造函數調用

若是函數或者方法調用以前帶有關鍵字new,它就構成構造函數調用。使用new來調用函數, 或者說發生構造函數調用時,會自動執行下面的操做。

  • 建立(或者說構造) 一個全新的對象。
  • 這個新對象會被執行 [[ 原型 ]] 鏈接。
  • 這個新對象會綁定到函數調用的this
  • 若是函數沒有返回其餘對象, 那麼new表達式中的函數調用會自動返回這個新對象。

來看一個例子:

function foo(x){
  this.x = x;
  console.log(this); // foo {x: 2}
}
var f = new foo(2); 
console.log(f.x); // 2

使用new來調用foo()時, 會構造一個新對象並把它綁定到foo()調用中的this上。即構造函數的this指向它實例化出來的對象。

箭頭函數

箭頭函數自己沒有this,是根據外層(函數或者全局)做用域來決定this

先來看一個例子:

var x = 1;
function testFn() {
  var fn1 = function() {
    console.log(this); // window
    console.log(this.x); // 1
  }
  var fn2 = () => {
    console.log(this); // window 
    console.log(this.x); // 1
  }
  fn1(); 
  fn2(); 
}
testFn();

兩段代碼輸出的結果一致。但運行的機制不同。

  • fn1是普通函數調用,前面說過普通函數調用this指向window
  • fn2是箭頭函數,它會繼承外層函數testFnthis,而testFn是在全局做用域中運行的,因此它的thiswindow,所以fn2箭頭函數的this也是window

對上面對例子作一點改動:

var x = 1;
function testFn() {
  var fn1 = function() {
    console.log(this); // window
    console.log(this.x); // 1
  }
  var fn2 = () => {
    console.log(this); // {x: 2} 
    console.log(this.x); // 2
  }
  fn1(); 
  fn2(); 
}
testFn.call({ x: 2 });

改變了testFn的調用方式。

  • fn1依然是普通函數調用,this指向window
  • fn2箭頭函數依然是繼承它外層函數testFnthis,但這裏testFn經過call使this指向了對象{id: 2},因此fn2this也指向該對象。

接着上面【對象方法調用】裏的setTimeout例子。若是使用箭頭函數會怎樣?

function foo() {
  console.log(this); // {x: 1, foo: ƒ}
  console.log(this.x); // 1
  setTimeout(() => {
    console.log(this); // {x: 1, foo: ƒ}
    console.log(this.x); // 1
  }, 0);
}

var obj = {
  x: 1,
  foo: foo
}

obj.foo();

這裏定時器回調的this由它外層函數foo的做用域決定,而foo是由obj調用,因此它的this指向對象obj,因此定時器的this也指向對象obj

另外,因爲箭頭函數沒有本身的this,因此固然也就不能用call()apply()bind()這些方法去改變this的指向。

小結

JavaScript的this取決於被調用的方式。

  • 若是是普通函數調用方式。非嚴格模式下,this指向window,嚴格模式下,thisundefined
  • 若是是對象調用方式,this指向該對象;
  • 若是是call()apply()bind()方式調用,this指向被綁定的對象;
  • 若是是構造函數調用方式,this指向實例化出來的新對象;
  • 若是是箭頭函數,是根據當前的詞法做用域來決定this, 具體來講,箭頭函數會繼承外層函數調用的this綁定。

原文地址:https://github.com/daijingfeng/blog/issues/2

參考資料

一、https://developer.mozilla.org/zh-CN/docs/Web/API/Window/setTimeout

二、https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions

三、http://es6.ruanyifeng.com/#docs/function#箭頭函數

四、《JavaScript權威指南(第6版)(中文版)》

五、《你不知道的JavaScript(上卷)》

相關文章
相關標籤/搜索