1. 程式人生 > >js中this的繫結規則及優先順序

js中this的繫結規則及優先順序

一.   this繫結規則

函式呼叫位置決定了this的繫結物件,必須找到正確的呼叫位置判斷需要應用下面四條規則中的哪一條。 

1.1 預設繫結

看下面程式碼:

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

var a = 1;

foo(); // 2

呼叫foo的時候,this應用了預設繫結,this指向了全域性物件,但是在嚴格模式下,那麼全域性物件將無法進行預設繫結,因此this會繫結到undefined

function foo() {
    'use strict';

    console.log(
this.a); } var a = 1; foo(); // TypeRrror: this is undefined

嚴格模式下與 foo() 的呼叫位置無關:

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

(function(){
    "use strict";
    foo(); // 2
})();

 

1.2 隱式繫結

另一條需要考慮的規則是呼叫位置是否有上下文物件

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

但是無論是直接在 obj 中定義還是先定義再新增為引用屬性, 這個函式嚴格來說都不屬於 obj 物件,然而, 呼叫位置會使用 obj 上下文來引用函式, 因此你可以說函式被呼叫時 obj 物件“ 擁有” 或者“ 包含” 它

物件屬性引用鏈中只有最頂層或者說最後一層會影響呼叫位置。 舉例來說:

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

1.2.1 隱式丟失

一個最常見的 this 繫結問題就是被隱式繫結的函式會丟失繫結物件, 也就是說它會應用預設繫結, 從而把 this 繫結到全域性物件或者 undefined 上, 取決於是否是嚴格模式,

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() 其實是一個不帶任何修飾的函式呼叫, 因此應用了預設繫結。在js內建函式中如setTimeout也是如此:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全域性物件的屬性
setTimeout(obj.foo, 100); // "oops, global"

和下面虛擬碼類似:

function setTimeout(fn, delay) {
    // 等待 delay 毫秒
    fn(); // <-- 呼叫位置!
}

1.3.顯示繫結

call(...),apply(...)可以指定this的繫結物件(前者接收多個引數如call(this, param1, param2, param3...),後者接受一個或兩個引數apply(this, [...]))

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2
};
foo.call(obj); // 2

通過 foo.call(..), 我們可以在呼叫 foo 時強制把它的 this 繫結到 obj 上。 如果你傳入了一個原始值( 字串型別、 布林型別或者數字型別) 來當作 this 的繫結對 象, 這個原始值會被轉換成它的物件形式( 也就是 new String(..)、 new Boolean(..) 或者 new Number(..))。 這通常被稱為“ 裝箱”。

1.3.1 硬繫結

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2
};
var bar = function() {
    foo.call(obj);
};
bar(); // 2
setTimeout(bar, 100); // 2
// 硬繫結的 bar 不可能再修改它的 this
bar.call(window); // 2

我們建立了函式 bar(), 並在它的內部手動呼叫 了 foo.call(obj), 因此強制把 foo 的 this 繫結到了 obj。 無論之後如何呼叫函式 bar, 它 總會手動在 obj 上呼叫 foo。 這種繫結是一種顯式的強制繫結, 因此我們稱之為硬繫結。建立一個 i可以重複使用的輔助函式:

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

由於硬繫結是一種非常常用的模式, 所以在 ES5 中提供了內建的方法 Function.prototype.bind, bind(..) 會返回一個硬編碼的新函式, 它會把引數設定為 this 的上下文並呼叫原始函式  

1.3.2 API呼叫的“上下文”

function foo(el) {
    console.log(el, this.id);
}
var obj = {
    id: "awesome"
};
// 呼叫 foo(..) 時把 this 繫結到 obj
[1, 2, 3].forEach(foo, obj);
// 1 awesome 2 awesome 3 awesome

這些函式實際上就是通過 call(..) 或者 apply(..) 實現了顯式繫結, 這樣你可以少些一些程式碼。

1.4. new繫結

在 JavaScript 中, 建構函式只是一些 使用 new 操作符時被呼叫的函式。 它們並不會屬於某個類, 也不會例項化一個類。 實際上, 它們甚至都不能說是一種特殊的函式型別, 它們只是被 new 操作符呼叫的普通函式而已。使用 new 來呼叫函式, 或者說發生建構函式呼叫時, 會自動執行下面的操作:

    1. 建立( 或者說構造) 一個全新的物件

    2. 這個新物件會被執行 [[ 原型 ]] 連線

    3. 這個新物件會繫結到函式呼叫的 this

    4. 如果函式沒有返回其他物件, 那麼 new 表示式中的函式呼叫會自動返回這個新物件

 

二. 優先順序

毫無疑問, 預設繫結的優先順序是四條規則中最低的,來看看隱式繫結和顯示繫結

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

顯式繫結優先順序更高, new 繫結和隱式繫結的優先順序誰高誰低:

function foo(something) {
    this.a = something;
}
var obj1 = {
    foo: foo
};
var obj2 = {};
obj1.foo(2);
console.log(obj1.a); // 2
obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3
var bar = new obj1.foo(4);
console.log(obj1.a); // 2
console.log(bar.a); // 4

可以看到 new 繫結比隱式繫結優先順序高。 但是 new 繫結和顯式繫結誰的優先順序更高呢?

function foo(something) {
    this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

bar 被硬繫結到 obj1 上, 但是 new bar(3) 並沒有像我們預計的那樣把 obj1.a 修改為 3。 相反,new 修改了硬繫結( 到 obj1 的) 呼叫 bar(..) 中的 this。 因為使用了 new 繫結, 我們得到了一個名字為 baz 的新物件, 並且 baz.a 的值是 3。之所以要在 new 中使用硬繫結函式, 主要目的是預先設定函式的一些引數, 這樣在使用 new 進行初始化時就可以只傳入其餘的引數。 bind(..) 的功能之一就是可以把除了第一個 引數( 第一個引數用於繫結 this) 之外的其他引數都傳給下層的函式( 這種技術稱為“ 部 分應用”, 是“ 柯里化” 的一種)。 舉例來說:

function foo(p1, p2) {
    this.val = p1 + p2;
}
//之所以使用 null 是因為在本例中我們並不關心硬繫結的 this 是什麼
// 反正使用 new 時 this 會被修改
var bar = foo.bind(null, "p1");
var baz = new bar("p2");
baz.val; // p1p2