場景 1:全局環境下的 this
這種情況相對簡單直接,函數在瀏覽器全局環境中被簡單調用,非嚴格模式下 this
指向 window
; 在 use strict
指明嚴格模式的情況下就是 undefined
:
function f1() {
console.log(this);
}
function f2() {
"use strict";
console.log(this);
}
f1(); // window
f2(); // undefined
這樣的題目比較基礎,但是如果你是在面試,那麼需要候選人格外注意其變種(爲什麼總有這種變態無聊面試題),請再看:
const foo = {
bar: 10,
fn: function () {
console.log(this);
console.log(this.bar);
}
};
var fn1 = foo.fn;
fn1();
這裏 this
仍然指向的是 window
。雖然 fn
函數在 foo
對象中作爲方法被引用,但是在賦值給 fn1
之後,fn1
的執行仍然是在 window
的全局環境中。因此輸出 window
和 undefined
,還是上面這道題目,如果調用改變爲:
const foo = {
bar: 10,
fn: function () {
console.log(this);
console.log(this.bar);
}
};
foo.fn();
將會輸出:
{bar: 10, fn: ƒ}
10
這其實屬於第二種情況了,因爲這個時候 this
指向的是最後調用它的對象,在 foo.fn()
語句中 this
指向 foo
對象。請記住:在執行函數時,如果函數中的 this
是被上一級的對象所調用,那麼 this
指向的就是上一級的對象;否則指向全局環境。
場景 2:上下文對象調用中的 this
我們直接來看“難”一點的:當存在更復雜的調用關係時,
const person = {
name: "Lucas",
brother: {
name: "Mike",
fn: function () {
return this.name;
}
}
};
console.log(person.brother.fn());
在這種嵌套的關係中,this
指向最後調用它的對象,因此輸出將會是:Mike
我們再看一道更復雜的題目,請跟我一起做好“應試”的準備:
const o1 = {
text: "o1",
fn: function () {
return this.text;
}
};
const o2 = {
text: "o2",
fn: function () {
return o1.fn();
}
};
const o3 = {
text: "o3",
fn: function () {
var fn = o1.fn;
return fn();
}
};
console.log(o1.fn());
console.log(o2.fn());
console.log(o3.fn());
答案是:o1
、o1
、undefined
,你答對了嗎?
我們來一一分析。
- 第一個
console
最簡單,o1
沒有問題。難點在第二個和第三個上面,關鍵還是看調用this
的那個函數。 - 第二個
console
的o2.fn()
,最終還是調用o1.fn()
,因此答案仍然是o1
。 - 最後一個,在進行
var fn = o1.fn
賦值之後,是“裸奔”調用,因此這裏的this
指向window
,答案當然是undefined
。
如果是在面試中,我作爲面試官,就會追問:如果我們需要讓 console.log(o2.fn())
輸出 o2
,該怎麼做?
一般開發者可能會想到使用 bind
/call
/apply
來對 this
的指向進行干預,這確實是一種思路。但是我接着問,如果不能使用 bind
/call
/apply
,有別的方法嗎?
const o1 = {
text: "o1",
fn: function () {
return this.text;
}
};
const o2 = {
text: "o2",
fn: o1.fn
};
console.log(o2.fn());
還是應用那個重要的結論:this
指向最後調用它的對象,在 fn
執行時,掛到 o2
對象上即可,我們提前進行了類似賦值的操作。
場景 3:bind/call/apply 改變 this 指向
上文提到 bind
/call
/apply
:
const foo = {
name: "lucas",
logName: function () {
console.log(this.name);
}
};
const bar = {
name: "mike"
};
console.log(foo.logName.call(bar));
將會輸出 mike
,這不難理解。但是對 call
/apply
/bind
的高級考察往往會結合構造函數以及組合式實現繼承。實現繼承的話題,我們會單獨講到。構造函數的使用案例,我們結合下面的場景進行分析。
場景 4:構造函數和 this
function Foo() {
this.bar = "Lucas";
}
const instance = new Foo();
console.log(instance.bar);
答案將會輸出 Lucas
。但是這樣的場景往往伴隨着下一個問題:new
操作符調用構造函數,具體做了什麼?以下供參考:
- 創建一個新的對象;
- 將構造函數的
this
指向這個新對象; - 爲這個對象添加屬性、方法等;
- 最終返回新對象。
以上過程,也可以用代碼表述:
var obj = {};
obj.__proto__ = Foo.prototype;
Foo.call(obj);
當然,這裏對 new
的模擬是一個簡單基本版的,更復雜的情況這個問題下我不會贅述。
需要指出的是,如果在構造函數中出現了顯式 return
的情況,那麼需要注意分爲兩種場景:
function Foo() {
this.user = "Lucas";
const o = {};
return o;
}
const instance = new Foo();
console.log(instance.user);
將會輸出 undefined
,此時 instance
是返回的空對象 o
。
function Foo() {
this.user = "Lucas";
return 1;
}
const instance = new Foo();
console.log(instance.user);
將會輸出 Lucas
,也就是說此時 instance
是返回的目標對象實例 this
。
結論:如果構造函數中顯式返回一個值,且返回的是一個對象,那麼 this
就指向這個返回的對象;如果返回的不是一個對象,那麼 this
仍然指向實例。
場景 5:箭頭函數中的 this 指向
箭頭函數使用 this
不適用以上標準規則,而是根據外層(函數或者全局)上下文作用域來決定。
const foo = {
fn: function () {
setTimeout(function () {
console.log(this);
});
}
};
console.log(foo.fn());
這道題中,this
出現在 setTimeout()
中的匿名函數裏,因此 this
指向 window
對象。如果需要 this
指向 foo
這個 object
對象,可以巧用箭頭函數解決:
const foo = {
fn: function () {
setTimeout(() => {
console.log(this);
});
}
};
console.log(foo.fn());
// {fn: ƒ}
單純箭頭函數中的 this
非常簡單,但是綜合所有情況,結合 this
的優先級考察,這時候 this
指向並不好確定。請繼續閱讀。
終極場景 6:this 優先級相關
我們常常把通過 call
、apply
、bind
、new
對 this
綁定的情況稱爲顯式綁定;根據調用關係確定的 this
指向稱爲隱式綁定。
那麼顯式綁定和隱式綁定誰的優先級更高呢?
function foo(a) {
console.log(this.a);
}
const obj1 = {
a: 1,
foo: foo
};
const obj2 = {
a: 2,
foo: foo
};
obj1.foo.call(obj2);
obj2.foo.call(obj1);
輸出分別爲 2、1,也就是說 call
、apply
的顯式綁定一般來說優先級比隱式綁定更高。
function foo(a) {
this.a = a;
}
const obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);
上述代碼通過 bind
,將 bar 函數中的 this
綁定爲 obj1 對象。執行 bar(2)
後,obj1.a
值爲 2。即經過 bar(2)
執行後,obj1
對象爲:{a: 2}。
當再使用 bar
作爲構造函數時:
var baz = new bar(3);
console.log(baz.a);
將會輸出 3。我們看 bar
函數本身是通過 bind
方法構造的函數,其內部已經對將 this
綁定爲 obj1
,它再作爲構造函數,通過 new
調用時,返回的實例已經與 obj1
解綁。 也就是說:
new
綁定修改了 bind
綁定中的 this
,因此 new
綁定的優先級比顯式 bind
綁定更高。
function foo() {
return (a) => {
console.log(this.a);
};
}
const obj1 = {
a: 2
};
const obj2 = {
a: 3
};
const bar = foo.call(obj1);
console.log(bar.call(obj2));
將會輸出 2。由於 foo()
的 this
綁定到 obj1
,bar
(引用箭頭函數)的 this
也會綁定到 obj1
,箭頭函數的綁定無法被修改。
如果將 foo
完全寫成箭頭函數的形式:
var a = 123;
const foo = () => (a) => {
console.log(this.a);
};
const obj1 = {
a: 2
};
const obj2 = {
a: 3
};
var bar = foo.call(obj1);
console.log(bar.call(obj2));
將會輸出 123。
這裏我再“抖個機靈”,僅僅將上述代碼的第一處變量 a
的賦值改爲:
const a = 123;
const foo = () => (a) => {
console.log(this.a);
};
const obj1 = {
a: 2
};
const obj2 = {
a: 3
};
var bar = foo.call(obj1);
console.log(bar.call(obj2));
答案將會輸出爲 undefined
,原因是因爲使用 const
聲明的變量不會掛載到 window
全局對象當中。
因此 this
指向 window
時,自然也找不到 a
變量了。關於 const
或者 let
等聲明變量的方式不再本課的主題當中,我們後續也將專門進行介紹。