談談JavaScript中的this機制

NO IMAGE

thisJavaScript中比較複雜的機制之一,本篇文章希望可以帶大家瞭解this相關的知識。本文內容來自書籍《你不知道的JavaScript(上卷)》,只是自己稍微整理一下。

☕️為什麼使用this

問題來了,既然this比較複雜,我們為什麼還要使用呢?看一段代碼:

function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call( this ); 
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify.call( me ); // KYLE
identify.call( you ); // READER
speak.call( me ); // Hello, 我是 KYLE
speak.call( you ); // Hello, 我是 READER

這段代碼可以在不同的上下文對象(meyou)複用函數,並且代碼中使用了this,如果不使用this代碼會是這個樣子


function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify( context ); 
console.log( greeting );
}
identify( you ); // READER
speak( me ); //hello, 我是 KYLE

可以看出來,比起顯示地傳遞上下文對象,使用this這種隱式的傳遞一個對象的引用,更加方便

⬇️this的誤區

關於this,由於它的語義性的問題,會帶來很多的誤解:

誤區一:指向自身

function foo(num) {
console.log( "foo: " + num );
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) { 
if (i > 5) {
foo( i ); 
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9 
// foo 被調用了多少次?
console.log( foo.count ); // 0

運行後我們發現foo.count仍然是0,說明this並沒有指向foo自身。

誤區二:指向它的作用域

在某種情況下這個說法是正確的,而在某些情況下這個說法又是錯誤的,但是要注意!!this 在任何情況下都不指向函數的詞法作用域!!
為什麼這麼說呢?

function bar() { 
console.log(1);
}
this.bar(); // 1

在上例中,this指向了全局作用域,但是隻是特殊情況,因此會有這個說法是正確的,而在某些情況下這個說法又是錯誤的結論

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

上文this.a視圖引用foo詞法作用域定義的變量a,這是永遠也不可能實現的

❤️this到底是什麼

說了它的使用方式以及誤區,那麼this到底是什麼呢?首先明確一點:this的綁定和函數聲明的位置沒有任何關係,只取決於函數的調用方式。

調用位置

this是在調用時被綁定的,完全取決於函數的調用位置,因此要搞清楚函數的調用位置,但是某些編程模式會隱藏函數的調用位置,最重要的分析它的調用棧(就是為了達到當前運行位置的所有調用函數)

function baz() {
// 當前調用棧是:baz
// 因此,當前調用位置是全局作用域
bar(); // <-- bar 的調用位置
}
function bar() {
// 當前調用棧是 baz -> bar
// 因此,當前調用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的調用位置
}
function foo() {
// 當前調用棧是 baz -> bar -> foo 
// 因此,當前調用位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的調用位置

☕️綁定規則

下面介紹this綁定的4種規則,下次看到this出現時,便可以使用這些規則

默認綁定

這是比較常見的函數調用類型:獨立函數調用

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

如何判斷應用了默認綁定呢?foo是直接使用不帶任何修飾符的函數進行引用調用的

注意:如果使用了嚴格模式,this會綁定到undefined

function foo() {
console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undefined

隱式綁定

當函數引用有上下文對象時(嚴格來說函數被對象“擁有”或者“包含”),隱式綁定規則會把函數調用中的this綁定到這個上下文對象

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

嚴格來說,foo不屬於obj對象,但是落腳點卻指向obj對象,因此你可以說函數被調用時 obj 對象“擁 有”或者“包含”它。

隱式丟失

一個最常見的問題就是:隱式綁定會丟失綁定對象,從而執行默認綁定

function foo() { 
console.log( this.a );
}
var obj = { 
a: 2,
foo: foo 
};
var bar = obj.foo; // 函數別名!
var a = "oops, global"; // a 是全局對象的屬性 
bar(); // "oops, global"

雖然barobj.foo的一個引用,但實際上引用的事foo函數本身,因此bar其實是一個不帶任何修飾的函數調用

另外一種情況就是參數傳遞。

參數傳遞其實就是一種隱式賦值,我們在傳入函數時也是隱式賦值

function foo() { 
console.log( this.a );
}
function doFoo(fn) {
// fn 其實引用的是 foo 
fn(); // <-- 調用位置!
}
var obj = { 
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局對象的屬性 
doFoo( obj.foo ); // "oops, global"

綜上所述:有兩種情況會導致隱式綁定的綁定丟失。

  • 進行引用賦值var bar = obj.foo;
  • 進行傳遞參數doFoo( obj.foo );

顯式綁定

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

通過foo.call(..)可以在調用時強制把this綁定到obj上,但是這樣的方式也無法解決掉丟失綁定問題

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

我們發現this隨著調用一直在改變,即this丟失。

我們可以通過以下方式解決:

硬綁定

創建一個包裹函數,傳入所有的參數並返回接收到的所有值

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

API調用的上下文

許多內置函數都提供了一個可選參數,通常被稱為上下文context,其作用和bind一樣,確保你的回調 函數使用指定的 this。

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

new綁定

JavaScript中的new機制與面向對象的語言完全不同,實際上,在JavaScript中並不存在所謂的”構造函數”,只有對與函數的”構造調用”

使用new來調用函數,或者說發生構造函數調用時的流程:

  • 創建(構造一個全新的對象)
  • 這個新對象會被執行[[原型]]連接
  • 這個新對象會被綁定到函數調用的this
  • 如果函數沒有返回其他對象,那麼new表達式中的函數調用會自動返回這個新對象
function foo(a) { 
this.a = a;
}
var bar = new foo(2); 
console.log( bar.a ); // 2

❤️優先級

上面介紹了this的4種綁定規則,那麼它們的優先級誰高誰低呢,首先,確認一點的是默認綁定的優先級最低

比較隱式綁定和顯示綁定

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
// new 和 隱式綁定同時存在,obj1的a是2,而this指向了bar
var bar = new obj1.foo( 4 ); 
console.log( obj1.a ); // 2 
console.log( bar.a ); // 4

可以看出來new綁定高於隱式綁定

比較new綁定和顯示綁定

由於newcall/apply無法一起使用,我們可以使用硬綁定測試優先級

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的優先級大於硬綁定。

但是使用剛開始的裸bind

function foo(something) { 
this.a = something;
}
function bind(obj, fn) {
return function() {
fn.apply(obj. arguments);
}
}
var obj1 = {};
var bar = bind( obj1, foo ); 
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3); 
console.log( obj1.a ); // 3 
console.log( baz.a ); // undefined

會驚奇地發現,new bar(3)obj1.a修改為 3
因此內置bind的實現是非常複雜的,不在此進行研究,既然這麼複雜,為什麼還要使用呢?

這種做法稱為“部 分應用”,是“柯里化”的一種,它的主要目的是預設函數的一些參數,這樣在使用new進行初始化時就可以只傳入其餘的參數。

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

從上我們可以總結出可以通過以下順序判斷this

  • 函數是否在new中調用(new綁定)?
  • 函數是否通過call、apply(顯式綁定)或者硬綁定調用
  • 函數是否在某個上下文對象中調用(隱式綁定)
  • 如果都不是的話,使用默認綁定

☕️綁定例外

規則總有例外,當你認為應用了其他規則時,有可能只應用了默認規則

被忽略的this

如果我們把null或者undefined作為this的綁定對象傳遞入callapply、或者bind,會使用默認綁定規則

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

那麼什麼情況下會使用這種方式呢?利用apply展開數組或者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

es6中可以使用...來代替“apply(…)“`,但是ES6中沒有柯里化的相關方法

忽略this會存在一個問題,比如第三方庫的函數真的使用了this,我們這種方式把this綁定到了全局作用域,會存在問題,需要使用更安全的this,創建空的非委託的對象Object.create( null )

function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我們的 DMZ 空對象
var ø = Object.create( null ); 
// 把數組展開成參數
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進行柯里化 
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

間接引用

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(),而不是p.foo()或者o.foo(),因此還是會調用默認規則

軟綁定

硬綁定可以把this強制綁定到指定的對象上,防止函數調用應用默認規則綁定,但是有一個弊端就是無法通過隱式或者顯示綁定來修改this

如果可以給默認綁定指定一個全局對象和undefined以外的值,就可以實現和硬綁定相同的效果,同時保留隱式綁定或者顯式綁定修改 this 的能力

這種叫做軟綁定。

if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
console.log('fn', this);
// 捕獲所有 curried 參數
var curried = [].slice.call( arguments, 1 );
var bound = function() {
console.log('this', this);
return fn.apply(
(!this || this === (window || global)) ? obj : this,
curried.concat.apply( curried, arguments )
);
}
bound.prototype = Object.create( fn.prototype );
return bound;
}
}
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" }, 
obj2 = { name: "obj2" }, 
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj); 
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj <---- 應用了軟綁定

☕️this詞法

最後介紹es6中的箭頭函數,箭頭函數不使用this的四種規則,而是根據外層(函數或者全局)作用域來決定this

function foo() {
// 返回一個箭頭函數 
return (a) => {
//this 繼承自 foo()
console.log( this.a ); 
};
}
var obj1 = { 
a:2
};
var obj2 = { 
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !

foothis綁定到了obj1bar引用箭頭函數的this也會綁定到obj1,箭頭函數的綁定無法被修改

相關文章

關於三次握手與四次揮手的那些事,你真的明白了嗎?不看後悔系列

webpack編譯優化

CSS垂直居中的12種實現方式

JavaScript萬物產生順序