摘要:本文屬於原創,歡迎轉載,轉載請保留出處: https://github.com/jasonGeng88/blog
理解this以前, 先糾正一個觀點,this 既不指向函數自身,也不指函數的詞法做用域。若是僅經過this的英文解釋,太容易產生誤導了。它實際是在函數被調用時才發生的綁定,也就是說this具體指向什麼,取決於你是怎麼調用的函數。git
this的4種綁定規則分別是:默認綁定、隱式綁定、顯示綁定、new 綁定。優先級從低到高。es6
什麼叫默認綁定,即沒有其餘綁定規則存在時的默認規則。這也是函數調用中最經常使用的規則。github
來看這段代碼:app
function foo() { } console.log( this.a ); var a = 2; foo(); //打印的是什麼?
foo()
打印的結果是2。函數
由於foo()是直接調用的(獨立函數調用),沒有應用其餘的綁定規則,這裏進行了默認綁定,將全局對象綁定this上,因此this.a 就解析成了全局變量中的a,即2。優化
注意:在嚴格模式下(strict mode),全局對象將沒法使用默認綁定,即執行會報undefined的錯誤this
function foo() { "use strict"; console.log( this.a ); } var a = 2; foo(); // Uncaught TypeError: Cannot read property 'a' of undefined
除了直接對函數進行調用外,有些狀況是,函數的調用是在某個對象上觸發的,即調用位置上存在上下文對象。prototype
function foo() { console.log( this.a ); } var a = 2; var obj = { a: 3, foo: foo }; obj.foo(); // ?
obj.foo()
打印的結果是3。調試
這裏foo函數被當作引用屬性,被添加到obj對象上。這裏的調用過程是這樣的:code
獲取obj.foo屬性 -> 根據引用關係找到foo函數,執行調用
因此這裏對foo的調用存在上下文對象obj,this進行了隱式綁定,即this綁定到了obj上,因此this.a被解析成了obj.a,即3。
function foo() { console.log( this.a ); } var a = 2; var obj1 = { a: 4, foo: foo }; var obj2 = { a: 3, obj1: obj1 }; obj2.obj1.foo(); //?
obj2.obj1.foo()
打印的結果是4。
一樣,咱們看下函數的調用過程:
先獲取obj2.obj1 -> 經過引用獲取到obj1對象,再訪問 obj1.foo -> 最後執行foo函數調用
這裏調用鏈不僅一層,存在obj一、obj2兩個對象,那麼隱式綁定具體會綁哪一個對象。這裏原則是獲取最後一層調用的上下文對象,即obj1,因此結果顯然是4(obj1.a)。
注意:這裏存在一個陷阱,你們在分析調用過程時,要特別當心
先看個代碼:
function foo() { console.log( this.a ); } var a = 2; var obj = { a: 3, foo: foo }; var bar = obj.foo; bar(); //?
<font color="red">bar()
打印的結果是2。</font>
爲何會這樣,obj.foo 賦值給bar,那調用bar()
爲何沒有觸發隱式綁定,使用的是默認綁定呢。
這裏有個概念要理解清楚,obj.foo 是引用屬性,賦值給bar的實際上就是foo函數(即:bar指向foo自己)。
那麼,實際的調用關係是:經過bar找到foo函數,進行調用。整個調用過程並無obj的參數,因此是默認綁定,全局屬性a。
function foo() { console.log( this.a ); } var a = 2; var obj = { a: 3, foo: foo }; setTimeout( obj.foo, 100 ); // ?
<font color="red">打印的結果是2。</font>
一樣的道理,雖然參傳是obj.foo
,由於是引用關係,因此傳參實際上傳的就是foo對象自己的引用。對於setTimeout
的調用,仍是 setTimeout -> 獲取參數中foo的引用參數 -> 執行 foo 函數,中間沒有obj的參與。這裏依舊進行的是默認綁定。
相對隱式綁定,this值在調用過程當中會動態變化,但是咱們就想綁定指定的對象,這時就用到了顯示綁定。
顯示綁定主要是經過改變對象的prototype關聯對象,這裏不展開講。具體使用上,能夠經過這兩個方法call(...)或apply(...)來實現(大多數函數及本身建立的函數默認都提供這兩個方法)。
call與apply是一樣的做用,區別只是其餘參數的設置上
function foo() { console.log( this.a ); } var a = 2; var obj1 = { a: 3, }; var obj2 = { a: 4, }; foo.call( obj1 ); // ? foo.call( obj2 ); // ?
打印的結果是3, 4。
這裏由於顯示的申明瞭要綁定的對象,因此this就被綁定到了obj上,打印的結果天然就是obj1.a 和obj2.a。
function foo() { console.log( this.a ); } var a = 2; var obj1 = { a: 3, }; var obj2 = { a: 4, }; var bar = function(){ foo.call( obj1 ); } bar(); // 3 setTimeout( bar, 100 ); // 3 bar.call( obj2 ); // 這是多少
前面兩個(函數別名、回調函數)打印3,由於顯示綁定了,沒什麼問題。
最後一個打印是3。
這裏須要注意下,雖然bar被顯示綁定到obj2上,對於bar,function(){...} 中的this確實被綁定到了obj2,而foo由於經過foo.call( obj1 )
已經顯示綁定了obj1,因此在foo函數內,this指向的是obj1,不會由於bar函數內指向obj2而改變自身。因此打印的是obj1.a(即3)。
js中的new操做符,和其餘語言中(如JAVA)的new機制是不同的。js中,它就是一個普通函數調用,只是被new修飾了而已。
使用new來調用函數,會自動執行以下操做:
從第三點能夠看出,this指向的就是對象自己。
看個代碼:
function foo(a) { this.a = a; } var a = 2; var bar1 = new foo(3); console.log(bar1.a); // ? var bar2 = new foo(4); console.log(bar2.a); // ?
最後一個打印是3, 4。
由於每次調用生成的是全新的對象,該對象又會自動綁定到this上,因此答案顯而易見。
上面也說過,這裏在重複一下。優先級是這樣的,以按照下面的順序來進行判斷:
數是否在new中調用(new綁定)?若是是的話this綁定的是新建立的對象。 數是否經過call、apply(顯式綁定)或者硬綁定調用?若是是的話,this綁定的是 指定的對象。 數是否在某個上下文對象中調用(隱式綁定)?若是是的話,this綁定的是那個上下文對象。 果都不是的話,使用默認綁定。若是在嚴格模式下,就綁定到undefined,不然綁定到 全局對象。 var bar = foo()
在顯示綁定中,對於null和undefined的綁定將不會生效。
代碼以下:
function foo() { console.log( this.a ); } foo.call( null ); // 2 foo.call( undefined ); // 2
這種狀況主要是用在不關心this的具體綁定對象(用來忽略this),而傳入null實際上會進行默認綁定,致使函數中可能會使用到全局變量,與預期不符。
因此對於要忽略this的狀況,能夠傳入一個空對象ø,該對象經過Object.create(null)
建立。這裏不用{}的緣由是,ø是真正意義上的空對象,它不建立Object.prototype委託,{}和普通對象同樣,有原型鏈委託關係。
1. 這裏傳null的一種具體使用場景是函數柯里化的使用
最後,介紹一下ES6中的箭頭函數。經過「=>」而不是function建立的函數,叫作箭頭函數。它的this綁定取決於外層(函數或全局)做用域。
function foo(){ console.log( this.a ); } var a = 2; var obj = { a: 3, foo: foo }; obj.foo(); //3
var foo = () => { console.log( this.a ); } var a = 2; var obj = { a: 3, foo: foo }; obj.foo(); //2 foo.call(obj); //2 ,箭頭函數中顯示綁定不會生效
function foo(){ return function(){ console.log( this.a ); } } var a = 2; var obj = { a: 3, foo: foo }; var bar = obj.foo(); bar(); //2
function foo(){ return () => { console.log( this.a ); } } var a = 2; var obj = { a: 3, foo: foo }; var bar = obj.foo(); bar(); //3
經過上面兩個列子,咱們看到箭頭函數的this綁定<font color="red">只取決於外層(函數或全局)的做用域</font>,對於前面的4種綁定規則是不會生效的。它也是做爲this機制的一種替換,解決以前this綁定過程各類規則帶來的複雜性。
注意:對於ES6以前,箭頭函數的替換版本是這樣的
// es6 function foo(){ return () => { console.log( this.a ); } } var a = 2; var obj = { a: 3, foo: foo }; var bar = obj.foo(); bar(); //3
經過上面兩個列子,咱們看到箭頭函數的this綁定<font color="red">只取決於外層(函數或全局)的做用域</font>,對於前面的4種綁定規則是不會生效的。它也是做爲this機制的一種替換,解決以前this綁定過程各類規則帶來的複雜性。
注意:對於ES6以前,箭頭函數的替換版本是這樣的
// es6 function foo(){ return () => { console.log( this.a ); } } // es6以前的替代方法 function foo(){ var self = this; return () => { console.log( self.a ); } }
咱們在使用js的過程當中,對於this的理解每每以爲比較困難,再調試過程當中有時也會出現一些不符合預期的現象。不少時候,咱們都是經過一些變通的方式(如:使用具體對象替換this)來規避的問題。可問題一直存在那兒,咱們沒有真正的去理解和解決它。
本文主要參考了《你不知道的JavaScript(上卷)》,對this究竟是什麼,具體怎麼綁定的,有什麼例外狀況以及ES6中的一個優化方向,來完全搞清楚咱們一直使用的this究竟是怎麼玩的。