1. 程式人生 > 其它 >你不知道的JavaScript——this全面解析

你不知道的JavaScript——this全面解析

之前一直不知道this到底代表啥,只知道它和一般面向物件程式語言中的this一定不同。

JavaScript中的this可以在函式中使用,是編譯器通過一些條件在函式被呼叫時繫結進對應作用域的的一個變數,可以明確知道的是,這個變數一定是一個物件,所以你可以用this.xxx的方式訪問一個屬性。

可以看到this被自動繫結進了作用域中,所以我們可以把它看成普通變數一樣對待即可,沒啥大不了的。

圖中的this指向了Window物件,也就是瀏覽器環境下的全域性作用域,實際上this可以指向任意位置,具體的規則可以分四種情況。

預設繫結

預設情況下,this指向全域性作用域。

function foo(){
    console.log(this.a);
}

var a = 10;
foo(); // 10

在瀏覽器執行這段程式碼,可以得到10的結果。

但是,在NodeJS環境下,你會得到一個undefined,這個理論失效了......難道是Node下的this和瀏覽器上的還不一樣??那xx還得學兩遍?

NodeJS和瀏覽器的不同

別太擔心,只是NodeJS下的全域性作用域和瀏覽器的有些區別。

如上是瀏覽器的全域性作用域,Window物件直接作為所有js檔案的全域性作用域,所以每個js檔案中使用var定義變數時,是直接定義在Window下的,這樣容易造成命名衝突。

如上是NodeJs的做法,它使用了一個global作為全域性作用域,為所有檔案通用,但每個js檔案又有一個單獨的作用域,它們使用var

定義的東西會定義在這個檔案級別的單獨作用域中,這樣就不會出命名衝突的問題了。

回過頭來看程式碼

function foo(){
    console.log(this.a);
}

var a = 10;
foo(); // undefined

咱說了,預設情況下this被繫結到全域性作用域,那就是global,而a現在在哪兒啊?在檔案單獨的作用域裡,那怎麼找啊?

function foo(){
    console.log(this.a);
}

global.a = 10;
foo(); // 10

這樣是可以的,但如非必要,請不要影響global作用域。

嘶,,你說不要影響全域性作用域,那...你使用this不很容易影響全域性作用域嗎???

對了,注意,嚴格模式下這個預設行為會被禁止,也就是說,嚴格模式下,全域性變數不會被繫結給this,預設情況下的this是undefined

隱式繫結

隱式綁定出現在呼叫位置有上下文物件的情況,this指向上下文物件。

function foo(){
    console.log(this.a);
}

var obj = {
    a: 10,
    foo: foo
}

var a = 20; // Turn to a comment in node env
// global.a = 20 // Release in node env

foo(); // 20
obj.foo(); // 10

第一個foo呼叫,按照剛剛的預設繫結,毫無疑問會尋找全域性作用域中的20列印,但第二個obj.foo(),由於我們呼叫的位置是在obj的上下文中,所以this指向obj,列印10

這說明,隱式繫結優先順序 > 預設繫結,這顯然是句廢話,但先記著。

看下面的程式碼

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

這應該不難理解,因為呼叫動作實際是obj2發出的,上下文理應是obj2,所以this也是obj2

再看

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

這...列印的結果是全域性作用域中的a,而不是obj中的。

因為var bar = obj.foo,這句,只是把foo函式賦值給bar了,這和你直接呼叫foo又有啥區別捏??嘿嘿。

再看

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"

這個和上面的原理一樣,傳參本就是一種隱式賦值。這些都是日常編寫程式碼時容易犯的錯誤。

所以,這些問題讓我感覺預設繫結和隱式繫結是如此的不可靠,一丁點兒的不小心就可能會釀成大錯。

顯式繫結

JS中的函式物件有兩個預設方法,applycall,實際上都是呼叫這個函式,只是它倆可以在呼叫的同時傳遞要繫結的this物件。(這和Java裡的invoke需要傳遞一個上下文例項不差不多嗎)。

function foo(){
    console.log(this.a);
}

var obj = {
    a: 2
};

foo.call(obj);

上面我們就把物件obj繫結給本次呼叫的this了。

前面說了,this一定是個物件型別,所以你這裡如果傳遞基本型別,則會被轉換成包裝型別。

但這好像仍然解決不了前面方法一被賦值this就會被改變的問題,你不能指望你的使用者每次呼叫你的方法時都使用call或者apply並且自己繫結物件。

一個最簡單的辦法是提供一個函式幫助使用者去完成呼叫call,繫結this的操作

function bar(){
    foo.call(obj)
}

bar();

bar暴露給使用者,無論它的作用域是由於疏忽的賦值操作被修改還是被顯式的惡意修改了,都不會影響實際內層foo所繫結的this物件。

可以考慮提供一個通用的方法來做這步操作,而不是把每一個函式都包裝一層。

function bind(fn,obj){
    return function(){
        return fn.apply(obj,arguments);
    }
}

function sayHello(name){
    console.log(this.prefix + ',' + name);
}

var speaker = {
    prefix: 'Hello',
}

speaker.sayHello = bind(sayHello,speaker);

speaker.sayHello('Julia'); // Hello,Julia

bind方法用於提供將一個物件obj繫結給函式fn的能力,它返回一個新的方法,這個方法和我們之前的包裝方法無異。

這樣寫程式碼會更優雅一點,以後我們的每個方法都可以直接使用bind來繫結this,而不用單獨提供一個包裝方法。

ES5提供了預設的bind方法。

function sayHello(name){
    console.log(this.prefix + ',' + name);
}
var speaker = {
    prefix: 'Hello',
}

speaker.sayHello = sayHello.bind(speaker);
speaker.sayHello('Julia'); // Hello,Julia

效果和我們所寫的一樣,但更加符合面向物件的程式設計模式。

使用callapply來繫結的this的優先順序毫無疑問要高於隱式繫結和預設繫結。

改寫我們之前的程式碼

function sayHello(name){
    console.log(this.prefix + ',' + name);
}
var speaker = {
    prefix: 'Hello',
    sayHello: sayHello
}
var anotherSpeaker = {
    prefix: 'Hi',
}

speaker.sayHello('Julia'); // Hello,Julia
speaker.sayHello.call(anotherSpeaker,'Julia'); // Hi,Julia

正常呼叫speaker.sayHello使用的就是當前speaker的上下文物件,這是隱式呼叫。而當你使用callapply給它繫結一個其他的物件時,this物件會被繫結成你指定的物件。

這也不用故意去記,這是很自然的事。

截至目前,優先順序排序為:預設繫結 < 隱式繫結 < 顯式繫結

new繫結

JS中的new和其他語言的不同。

new後面可以跟一個函式,然後會自動執行這個函式,並在執行之前做幾件事情:

  1. 建立一個新物件
  2. 這個新物件被執行[[原型]]連結 (先不管他是啥)
  3. 這個新物件會被繫結到後面所跟的函式的this
  4. 如果函式沒返回其他物件,這個新物件會被預設返回
function foo(a){
    this.a = a;
}

var bar = new foo(2);
console.log(bar.a); // 2

嘿,即使js中new的執行原理看似和傳統面嚮物件語言並不搭邊兒,但出來的效果還真和建構函式挺像呢哈。準確點來說應該是構造呼叫。

因為new建立了一個新物件,所有操作在這個新物件上進行,所以它的優先順序理應是最高的,注意這裡說它優先順序最高的意思是它不會被其他繫結操作所影響,因為這個新物件和之前的老物件已經完全沒關係了。

最終的優先順序排序為:預設繫結 < 隱式繫結 < 顯式繫結 < new繫結

未完。。。