this的指向是JavaScript中一個經久不衰的熱門問題,在不一樣的場景下指向規則也不一樣,在此本文總結了this在不一樣場景下的指向規則以及ES6中新增的箭頭函數中this的指向問題。數組
this提供了一種更優雅的方式來隱式"傳遞"一個對象引用,所以能夠將API設計得更加簡潔而且易於複用.瀏覽器
指向函數的做用域安全
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
直接使用不帶任何修飾的函數引用進行調用的,會進行默認綁定。函數
function foo() { console.log(this.a); } var a = 2; foo(); // 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綁定到全局對象上。編碼
像隱式綁定中的例子裏,若是不想將函數包含在對象體內的屬性上,那麼能夠利用顯式綁定方法(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()這兩種方法不一樣之處在於其處理後函數不會執行,而是返回了一個改變了上下文的函數副本,然後二者則會直接執行函數。
與其餘語言中不一樣,在JavaScript中,構造函數只是一些使用new操做符時被調用的函數。它們並不會屬於某個類,也不會實例化一個類。實際上,它們甚至都不能說是一種特殊的函數類型,它們只是被new操做符調用的普通函數而已。
包括內置對象函數在內的全部函數均可以用new來調用,這種函數調用被稱爲構造函數調用。這裏有一個重要可是很是細微的區別:實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」。
new操做符進行構造函數調用時的操做流程:
function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2
使用new來調用foo(..)時,咱們會構造一個新對象並把它綁定到foo(..)調用中的this上.
箭頭函數是ES6新增的一種函數表達式,不是使用function關鍵字來定義,也不適用上述四條斷定規則,而是依據外層做用域來決定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); }
這種方法主要是用apply展開數組並傳入函數.可是若是這個函數確實使用了this,那麼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》等