This is this

this的指向是JavaScript中一個經久不衰的熱門問題,在不一樣的場景下指向規則也不一樣,在此本文總結了this在不一樣場景下的指向規則以及ES6中新增的箭頭函數中this的指向問題。數組

關於this

this提供了一種更優雅的方式來隱式"傳遞"一個對象引用,所以能夠將API設計得更加簡潔而且易於複用.瀏覽器

this常見的誤解

  1. 指向自身
  2. 指向函數的做用域安全

    1. this在任何狀況下都不指向函數的詞法做用域。在JavaScript內部,做用域確實和對象相似,可見的標識符都是它的屬性。可是做用域「對象」沒法經過JavaScript代碼訪問,它存在於JavaScript引擎內部。

this的機制

  1. this是在運行時進行綁定的,並非在編寫時綁定,它的上下文取決於函數調用時的各類條件。this的綁定和函數聲明的位置沒有任何關係,只取決於函數的調用方式.
  2. 當一個函數被調用時,會建立一個活動記錄(有時候也稱爲執行上下文)。這個記錄會包含函數在哪裏被調用(調用棧)、函數的調用方法、傳入的參數等信息。this就是記錄的其中一個屬性,會在函數執行的過程中用到。

this指向解析

this的指向:函數在代碼中被調用的位置(而不是聲明位置)bash

調用棧:爲了到達當前執行位置所調用的全部函數.
function baz() {
    // 當前調用棧是:baz
    // 所以,當前調用位置是全局做用域
    console.log( "baz" );
    bar(); // <-- bar的調用位置
}
function bar() {
    // 當前調用棧是baz -> bar
    // 所以,當前調用位置在baz中
    console.log( "bar" );
    foo(); // <-- foo的調用位置
}
function foo() {debugger;
    // 當前調用棧是baz -> bar -> foo
    // 所以當前調用位置在bar中
       console.log( "foo" );
}
baz(); // <-- baz的調用位置

經過瀏覽器的調試工具能夠查看調用棧,能夠查看當前函數體內this的指向:app


this的五種綁定規則

1. 默認綁定

直接使用不帶任何修飾的函數引用進行調用的,會進行默認綁定。函數

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

2. 隱式綁定

調用位置是否有上下文對象,或者說是否被某個對象擁有或包含。工具

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

須要注意的是不管是直接在obj中定義仍是先定義再添加爲引用屬性,這個函數嚴格來講都不屬於obj對象。 然而,調用位置會使用obj上下文來引用函數,所以你能夠說函數被調用時obj對象「擁有」或者「包含」它。
當foo()被調用時,它的落腳點確實指向obj對象。當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this綁定到這個上下文對象。由於調用foo()時this被綁定到obj,所以this.a和obj.a是同樣的。oop

隱式丟失問題

function foo() { 
    console.log( this.a );
}
var obj = { 
    a: 2,
    foo: foo 
};
var bar = obj.foo; // 函數別名!
var a = "oops, global"; // a是全局對象的屬性
bar(); // "oops, global"
雖然bar是obj.foo的引用,但實際上其引用的是foo自己,所以此時的bar是不帶修飾的函數調用,所以應用默認綁定。

實際狀況裏比較常見的是回調函數的this丟失問題:this

function foo() { 
    console.log( this.a );
}
var obj = { 
    a: 2,
    foo: foo 
};
var a = "oops, global"; // a是全局對象的屬性
setTimeout( obj.foo, 100 ); // "oops, global"
回調函數失去this值綁定也是同樣的緣由,func(fn)中fn實際上是賦值給了函數的參數

在上面這段代碼裏obj.foo在傳入setTimeout時實際上是作了一個隱式的賦值操做,所以,在setTimeout函數體內調用時,其實調用的參數直接引用了function foo(){},致使this綁定到全局對象上。編碼

3. 顯式綁定

像隱式綁定中的例子裏,若是不想將函數包含在對象體內的屬性上,那麼能夠利用顯式綁定方法(call、apply),它們會把這個對象綁定到this,接着在調用函數時指定這個this。

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

利用顯式綁定的一個變種方法能夠解決隱式丟失的問題,即建立一個包裹函數,傳入全部的參數並返回接收到的全部值:

function foo(something) { 
    console.log( this.a, something ); 
    return this.a + something;
}
var obj = { 
    a:2
};
var bar = function() {
    return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5

經過建立函數bar(),並在它的內部手動調用foo.call(obj),從而強制把foo的this綁定到了obj。不管以後如何調用函數bar,它總會手動在obj上調用foo。這種綁定是一種顯式的強制綁定。
在ES5中也提供了內置的這種綁定方法:Function.prototype.bind,bind(..)會返回一個硬編碼的新函數,它會把參數設置爲this的上下文並調用原始函數。

bind()與call()和apply()這兩種方法不一樣之處在於其處理後函數不會執行,而是返回了一個改變了上下文的函數副本,然後二者則會直接執行函數。

4. new綁定

與其餘語言中不一樣,在JavaScript中,構造函數只是一些使用new操做符時被調用的函數。它們並不會屬於某個類,也不會實例化一個類。實際上,它們甚至都不能說是一種特殊的函數類型,它們只是被new操做符調用的普通函數而已。
包括內置對象函數在內的全部函數均可以用new來調用,這種函數調用被稱爲構造函數調用。這裏有一個重要可是很是細微的區別:實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」

new操做符進行構造函數調用時的操做流程:

  1. 建立一個全新的對象
  2. 這個新對象會被執行[[原型]]鏈接
  3. 這個新對象會綁定到函數調用的this
  4. 若是函數返回沒有其它對象,那麼new表達式中的函數調用會自動返回這個新對象.
function foo(a) { 
    this.a = a;
} 
var bar = new foo(2);
console.log( bar.a ); // 2

使用new來調用foo(..)時,咱們會構造一個新對象並把它綁定到foo(..)調用中的this上.

利用優先級規則判斷非箭頭函數的this指向

  1. 函數是否在new中調用(new綁定)?若是是的話this綁定的是新建立的對象。
  2. 函數是否經過call、apply(顯式綁定)或者硬綁定調用?若是是的話,this綁定的是指定的對象。
  3. 函數是否在某個上下文對象中調用(隱式綁定)?若是是的話,this綁定的是那個上下文對象。
  4. 若是都不是的話,使用默認綁定。若是在嚴格模式下,就綁定到undefined,不然綁定到全局對象。

5. 箭頭函數綁定

箭頭函數是ES6新增的一種函數表達式,不是使用function關鍵字來定義,也不適用上述四條斷定規則,而是依據外層做用域來決定this

  1. 箭頭函數可讓this綁定定義時的做用域,而不是調用時的做用域。
  2. 箭頭函數可讓this指向固定化,這種特性頗有利於封裝回調函數。
this指向的固定化,並非由於箭頭函數內部有綁定this的機制,實際緣由是箭頭函數根本沒有本身的this,致使內部的this就是外層代碼塊的this。正是由於它沒有this,因此也就不能用做構造函數。

箭頭函數實質上就是保存了其外層做用域的this指向,從而在函數體內進行調用。ES6在Babel轉換後的ES5代碼以下:

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}
// ES5
function foo() {
  var _this = this;
  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}

綁定例外問題

  1. 把null或undefined做爲綁定對象傳入call或者apply,此時實際綁定的是默認規則(全局對象).
這種方法主要是用apply展開數組並傳入函數.可是若是這個函數確實使用了this,那麼this會綁定到全局對象,形成惡劣後果。
  1. 「更安全」的作法是傳入一個特殊的對象,把this綁定到這個對象不會對你的程序產生任何反作用
function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
// 咱們的DMZ空對象
var ø = Object.create( null );
// 把數組展開成參數
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用bind(..)進行柯里化
var bar = foo.bind( ø, 2 ); 
bar( 3 ); // a:2, b:3

參考:《你不知道的JavaScript》等
相關文章
相關標籤/搜索