之前一直對this的指向很模糊,找了一些別人的博客看,又重新看了一下《你不知道的JavaScript》,感覺基本上是弄懂了,挑一些重點的地方記錄一下,有些地方對我來說書上解釋寫的不夠多,所以自己做下補充以方便理解,有理解錯的地方還望指出。 一.澄清誤區 首先你需要知道: 1.this並不指向函數自身 ...
之前一直對this的指向很模糊,找了一些別人的博客看,又重新看了一下《你不知道的JavaScript》,感覺基本上是弄懂了,挑一些重點的地方記錄一下,有些地方對我來說書上解釋寫的不夠多,所以自己做下補充以方便理解,有理解錯的地方還望指出。
一.澄清誤區
首先你需要知道:
1.this並不指向函數自身
2.this的作用域在任何情況下都不指向函數的詞法作用域。
舉個例子:
function foo() { var a = 2; this.bar(); } function bar() { console.log(this.a); } foo();//ReferenceError: a is not defined
第一次看這裡的時候就掉坑裡了,想當然的以為foo()中this指向window,而bar()又在全局中故能調用bar(),但實際上這是錯的,不能使用this來引用一個詞法作用域內部的東西。
this是在函數被調用時發生的綁定,它指向什麼完全取決於函數在哪裡調用。跟函數聲明的位置沒有任何關係,要區別於函數的作用域,函數的作用域是在它被定義的時候確定的。
要判斷一個運行中函數的this綁定,需要找到這個函數的直接調用位置。
二.調用位置
調用位置就是函數在代碼中被調用到的位置。
this有四條綁定規則:
1.預設綁定:非嚴格模式下this指向全局對象,嚴格模式綁定到undefined。 var bar = foo()
2.隱式綁定:this指向包含它的函數的對象,要註意隱式丟失的情況。 var bar = obj1.foo()
3.顯示綁定:this指向call()、apply()和bind()方法指定的對象。 var bar = foo.call(obj2)
4.new綁定:this指向構造的新對象。 var bar = new foo()
優先順序:new綁定 > 顯示綁定 > 隱式綁定 > 預設綁定
具體看下麵:
1.預設綁定
無法應用其他規則時預設使用預設綁定。如果函數獨立調用,使用預設綁定。
function foo() {
//看函數體是否處於嚴格模式,是看這個位置("use strict")
console.log(this.a); } var a = 2; foo(); //2 僅foo()函數本身,為獨立調用
在上面的代碼中,foo()是使用不帶任何修飾的函數引用進行調用的,所以會使用預設綁定,非嚴格模式下this指向全局對象,嚴格模式綁定到undefined。不是指調用位置是否處於嚴格模式,而是函數體是否處於嚴格模式。
2.隱式綁定
考慮函數的調用位置是否有上下文對象,或者說是否被某個對象擁有或者包含。
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo //foo()被當作引用屬性添加到obj中,此時它被obj對象包含,此時this指向obj } obj.foo(); //2
對象屬性引用鏈中只有最後一層會影響調用位置。
function foo() { console.log(this.a); } var obj2 = { a: 42, foo: foo } var obj1 = { a: 2, obj2: obj2 } obj1.obj2.foo(); //42 雖然這裡有obj1對象和obj2對象,但是obj2才是處於最後一層最接近foo(),所以會指向obj2
隱式丟失的情況
來看下麵的代碼:
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo } var bar = obj.foo; //雖然bar是obj.foo的一個引用,但實際上bar引用的只是foo函數本身,可以看成bar() = foo(),此時foo()是獨立調用故綁定到全局對象 var a = "oops, global"; bar(); //oops, global
還有一種常見的隱式丟失的情況是傳入回調函數
function foo() { console.log(this.a); } function doFoo(fn) { fn(); } var obj = { a: 2, foo: foo } var a = "oops, global"; doFoo(obj.foo); //oops, global obj.foo作為參數傳入實際上隱式賦值給了fn,可以看成fn = obj.foo,
//此時又回到了上一例代碼的情況,引用的是foo函數本身,看成fn()= foo(),獨立調用指向全局對象
3.顯示綁定
使用函數的call()和apply()方法可以使用顯示綁定,這兩個方法的第一個參數都是一個對象,他們會把這個對象綁定到this,在調用函數時指定這個this,因為可以指定綁定對象故稱做顯示綁定。
function foo() { console.log(this.a); } var obj = { a: 2 } foo.call(obj);
但是這種顯示綁定沒有解決隱式丟失的問題,要解決這個問題可以使用硬綁定。
function foo() { console.log(this.a); } var obj = { a: 2 } var bar = function() { foo.call(obj); } bar(); //2 setTimeout(bar, 100) //2 bar.call(window); //2
創建了一個函數bar(),併在它的內部手動調用foo.call(obj),因此強制把foo的this綁定到了obj,之後無論如何調用函數bar,它總會手動在obj上調用foo,這叫做硬綁定。
硬綁定非常常用,ES5中提供了內置的方法Function.prototype.bind,bind()會返回一個硬編碼的新函數,它會把參數設置為this的上下文並調用原始函數。
硬綁定的兩個典型應用場景:
- 創建一個包裹函數,傳入所有的參數並返回接收到的所有值
function foo(something) { console.log(this.a, something); return this.a + something; //返回接收到的所有this.a和something } var obj = { a: 2 } var bar = function() { return foo.apply(obj, arguments); //this指向obj,arguments為傳入的參數3 } var b = bar(3); //2 3 console.log(b); //5
- 創建一個可以重覆使用的輔助函數
function foo(something) { console.log(this.a, something); return this.a + something; } function bind(fn, obj) { return function() { return fn.apply(obj, arguments); }; } var obj = { a: 2 } var bar = bind(foo, obj); var b = bar(3); //2 3 console.log(b); //5
4.new綁定
使用new來調用函數,或者說發生構造函數調用時,會執行下麵的操作:
①創建(構造)一個全新的對象。
②這個新對象會被執行[[原型]]連接。
③這個新對象會綁定到函數調用的this。
④如果函數沒有返回其他對象,那麼new表達式中的函數調用會自動返回這個新對象。
function foo(a) { this.a = a; } var bar = new foo(2);//使用new操作符調用foo(),構造了一個新對象bar並把它綁定到foo()的this上 console.log(bar.a); //2
三.綁定例外
有一些例外情況需要註意
1.把null或undefined作為this的綁定對象傳入call、apply或bind。
null和undefined在調用時會被忽略,然後應用預設綁定。
function foo() { console.log(this.a); } var a = 2; foo.call(null);//2
有時你可能選擇null作為一個占位值而選擇null作為參數,但是總是用null來忽略this的綁定可能產生一些副作用。一種更安全的做法是傳入一個空的非委托對象,把this綁定到這個對象不會對你的程式產生任何副作用。
2.無意間創建了一個函數的“間接引用”
同樣會應用預設綁定,最容易在賦值時發生。
function foo() { console.log(this.a); } var a = 2; var o = { a: 3, foo: foo } var p = { a: 4 } o.foo();//3 (p.foo = o.foo);//2 p.foo = o.foo的返回值是目標函數的引用,故調用的位置是foo(),又會應用預設綁定
3.軟綁定 softBind()
硬綁定會降低函數的靈活性,使用硬綁定後就無法使用隱式綁定或顯示綁定來修改this。
軟綁定:給預設綁定指定一個全局對象和undefined以外的值,可以實現和硬綁定相同的效果,同時保留隱式綁定或顯示綁定修改this的能力。
除了軟綁定外,softBind()的其他原理和bind()類似。首先檢查調用時的this,如果this綁定到全局對象或者undefined,就把指定的對象綁定到this,否則不會修改this。
function foo() { console.log("name:" + this.name); } var obj = { name: "obj" } var obj2 = { name: "obj2" } var obj3 = { name: "obj3" } var fooOBJ = foo.softBind(obj); fooOBJ();//obj obj2.foo = foo.softBind(obj); obj2.foo();//obj2 obj2.foo()調用時,this綁定到obj2上,不是全局對象也不是undefined,所以不會調用指定的obj,而是使用原來的obj2。 fooOBJ.call(obj3);//硬綁定時綁定了obj3,後面不可再修改 setTimeout(obj2.foo, 10);//假設setTimeout(fn,10),fn實際上引用的只是foo(),可以看成fn = obj2.foo,類似前面的隱式綁定丟失,
//會使用預設綁定到全局,這時會發生軟綁定,綁定到指定對象obj
4.箭頭函數
箭頭函數無法使用this中的四種綁定規則,而是根據外層(函數或全局)作用域來決定this。
function foo() { return (a) => { console.log(this.a); } } var obj1 = { a: 2 } var obj2 = { a: 3 } var bar = foo.call(obj1); bar.call(obj2);//2 箭頭函數會捕獲調用時foo()的this,foo()的this會顯示綁定到obj1,
//bar(引用箭頭函數)的this也會綁定到obj1,箭頭函數的綁定無法被修改
箭頭函數常用於回調函數中,例如事件處理器或者定時器
function foo() { setTimeout( () => { console.log(this.a);//箭頭函數會繼承外層函數調用的this綁定,這裡箭頭函數的外層函數是foo(),foo()的this綁定到obj,所以箭頭函數的this也綁定到obj。 }, 100); } var obj = { a: 2 } foo.call(obj);//2