Javascript的this理解
在理解javascript的this之前,首先先了解一下作用域。
作用域分為兩種:
1、詞法作用域:引擎在當前作用域或者巢狀的子作用域查詢具有名稱識別符號的變數。(引擎如何查詢和在哪查詢。定義過程發生在程式碼書寫階段)
2、動態作用域:在執行時被動態確定的作用域。
詞法作用域和動態作用域的區別是:詞法作用域是在寫程式碼或定義時確定的;動態作用域是在執行時確定的。
this的繫結規則
this是在呼叫時被繫結,取決於函式的呼叫位置。由此可以知道,一般情況下(非嚴格模式下),this都會根據函式呼叫(呼叫棧)的上下文來繫結物件。
一、預設繫結
預設繫結:預設繫結是指在非嚴格模式下,且沒有使用別的繫結規則時,this根據函式呼叫(呼叫棧)的上下文來繫結物件(全域性物件)。(嚴格模式下則繫結undefined)
舉個栗子:
function foo() { console.log(this.a); }; function bar() { var a = 3; foo(); } var a = 2; bar();//呼叫棧在全域性作用域,this繫結全域性物件 執行結果為: 2 //加上"use strict"執行結果則會變成this is undefined
這裡的函式呼叫時,使用了預設繫結,函式呼叫(呼叫棧)的上下文是全域性作用域,因此this綁定了全域性物件(global)。
eg2: function foo() { console.log(this.a) }; var a = 2; (function() { "use strict" foo(); })(); 執行結果為: 2
這裡需要注意: 對於預設繫結,決定this繫結物件的不是呼叫位置是否處於嚴格模式,而是函式體是否處於嚴格模式 (函式體處於嚴格模式則this繫結undefined;否則this繫結全域性物件) 。另外:嚴格模式和非嚴格模式雖然有可能可以繫結,但是最好不混用。
間接引用一般也是會應用預設繫結規則。
eg: 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。
二、隱式繫結
隱式繫結:由上下文物件呼叫,繫結到上下文物件。
舉個栗子:
function foo() { console.log(this.a); }; var obj = { a: 2, foo: foo }; obj.foo();//2 foo();//undefined
這段程式碼中,foo()被當做引用屬性新增到obj物件中,obj呼叫這個引用屬性函式時,會使用該引用屬性上下文,this會被繫結到obj物件。(這個函式嚴格來說不屬於obj物件,只是作為引用屬性)。屬於隱式繫結。
而下面foo()函式的直接執行,並不是obj物件引用,所以上下文物件是全域性物件。故this綁定了undefined。屬於預設繫結。
物件引用鏈中只有上一層或者說最後一層在呼叫位置中起作用。
注意:
-
隱式繫結的函式會丟失繫結物件。此時它會應用預設繫結,將this繫結到全域性物件或者undefined上,取決於是否是嚴格模式。
eg:
function foo() { console.log(this.a); }; var obj = { a: 2; foo: foo } var bar = obj.foo; var a = 'biubiubiu'; bar(); 執行結果:"biubiubiu"
解析:看似bar是obj.foo的一個引用,實際上bar是直接引用了函式foo,是一個單純的函式呼叫,故實為預設繫結。
-
引數傳遞就是隱式賦值,因此傳入函式時也會被隱式賦值。
eg:
function foo() { console.log(this.a); }; var obj = { a: 2, foo: foo }; function bar(fn) { fn(); }; var a = "biubiubiu"; bar(obj.foo); 執行結果: "biubiubiu"
解析:實際上引數也是隱式賦值,但是引數傳入函式中,並在函式中執行。此時也是直接引用了函式foo,因此也是單純的函式呼叫,採用了預設繫結。
- 把函式傳入語言內建函式。 (與上面情況基本相似,將自己宣告函式改成語言內建函式) 回撥函式丟失this的情況比較常見,況且還有呼叫回撥函式的函式可能還會修改this。
三、顯式繫結
顯式繫結:直接將this繫結到指定物件上。 Javascript中絕大多數函式和自己所建立的函式都可以使用這兩種顯式繫結的方法。
1、.call()
2、.apply()
這兩種繫結方法,第一個引數是this繫結的物件。 (如果傳入的引數是原始值(字串型別、布林型別、數字型別),這個原始值就會被轉成物件形式(new String、new Boolean、new Number)這個稱為:裝箱)
舉個栗子:
function foo() { console.log(this.a); }; var obj = { a: 2 }; foo.call(obj); 執行結果: 2
然鵝,顯示繫結並不能解決繫結丟失的問題。這個時候來了一位新朋友 -- 硬繫結(bind)。
3、.bind()(硬繫結是常見場景,故es5提供了該內建方法 Function.prototype.bind。)
bind()會返回一個新編碼函式,把this繫結在指定引數上,並呼叫函式。
舉個栗子:
function foo(e) { console.log(this.a + e); return this.a + e; }; var obj = { a: 2 } var bar = foo.bind(obj); //新編碼函式 var b = bar(3); // 2 3 console.log(b); // 5
bind()還有一個功能 :將除了第一個用於繫結this的引數之外的其他引數傳給下層的函式 (部分應用 ,是“柯里化 ”的一種)。
這裡涉及到一個概念:把null或者undefined作為this的繫結物件傳入call、apply、bind,這些值在呼叫的時候會被忽略,實際應用預設繫結規則。
應用場景:
1、使用apply()展開一個數組,並作為引數傳遞給一個函式。
2、bind()對引數進行柯里化(預先設定一些引數)。
舉個栗子:
function foo(a,b) { console.log("a:" + a + ",b:" + b); }; //陣列“展開”成引數 foo.apply(null,[2,3]);//a:2,b:3 //bind()柯里化 var bar = foo.bind(null,2); bar(3);//a:2,b:3
解析:傳入一個引數作為this繫結物件,如果不傳則使用佔位符(null),此時會使用預設繫結規則。
上面這個例子可能會產生一定的副作用,如果需要運用這種場景並且更加安全。可以建立一個空物件(可以用任意喜歡的名字來命名)。
var ∅ = Object.create(null); //上面這個例子就可以改寫為: foo.apply(∅,[2,3]); //a:2,b:3 var bar = foo.bind(∅,2); bar(3);//a:2,b:3
注意:硬繫結之後不能使用隱式繫結和顯式繫結對this進行修改
在這裡介紹一種軟繫結 的方法softBind() ,檢查this繫結到全域性物件或者undefined後,繫結this到指定的預設物件 。繫結後效果和硬繫結一樣,但是保留隱式繫結或者顯式繫結修改this的能力。
四、new繫結
Javascript中的new機制與面向類語言的完全不同。在Javascript中,建構函式只是一些使用new操作符時被呼叫的函式 ,不屬於一個類,也不會例項化一個類。稱為對函式的“構造呼叫”。
舉個栗子:
function foo(a) { this.a = a; } var bar = new foo(2); console.log(bar.a); //2
使用new的過程會建立一個全新的物件,this會繫結這個新物件 。如果函式沒有返回其他物件,則new表示式函式呼叫會返回該新物件 。(這個新物件會連線prototype)
四種繫結規則的優先順序為:new>顯式>隱式>預設
箭頭函式
箭頭函式是根據外層作用域(函式或全域性)來決定this。(詞法作用域取代this機制)
箭頭函式this會繫結呼叫時的物件,且箭頭函式的繫結無法修改(new也不行)。
其實可以理解為,箭頭函式的this在詞法上繼承的是它所在的作用域(函式或全域性)的this,而它繼承的函式作用域的this繫結的是在該函式呼叫上下文物件,所以箭頭函式的this間接的繫結在呼叫上下文物件。
簡述: 箭頭函式this(繫結作用域this)-- 作用域this(繫結在呼叫上下文物件)。
故:箭頭函式this == 呼叫的上下文物件
舉個栗子:
function foo() { setTimeout(function() { //這裡的this在詞法上繼承自foo() console.log(this.a); },100); }; var obj = { a: 2 }; foo.call(obj);//2
其實這個栗子也等價於:
function foo() { var that = this;//lexical capture of this setTimeout(function() { console.log(self.a) },100); } ...與上面一樣
所以,有兩種風格:this風格(四種規則) 和詞法作用域風格(that = this和箭頭函式) 可供使用。使用時儘量避免混用,否則會造成難以維護的後果。