場景 1:全局環(huán)境下的 this
這種情況相對簡單直接,函數(shù)在瀏覽器全局環(huán)境中被簡單調(diào)用,非嚴格模式下 this 指向 window; 在 use strict 指明嚴格模式的情況下就是 undefined:
function f1() {
console.log(this);
}
function f2() {
"use strict";
console.log(this);
}
f1(); // window
f2(); // undefined
這樣的題目比較基礎,但是如果你是在面試,那么需要候選人格外注意其變種(為什么總有這種變態(tài)無聊面試題),請再看:
const foo = {
bar: 10,
fn: function () {
console.log(this);
console.log(this.bar);
}
};
var fn1 = foo.fn;
fn1();
這里 this 仍然指向的是 window。雖然 fn 函數(shù)在 foo 對象中作為方法被引用,但是在賦值給 fn1 之后,fn1 的執(zhí)行仍然是在 window 的全局環(huán)境中。因此輸出 window 和 undefined,還是上面這道題目,如果調(diào)用改變?yōu)椋?/p>
const foo = {
bar: 10,
fn: function () {
console.log(this);
console.log(this.bar);
}
};
foo.fn();
將會輸出:
{bar: 10, fn: ?}
10
這其實屬于第二種情況了,因為這個時候 this 指向的是最后調(diào)用它的對象,在 foo.fn() 語句中 this 指向 foo 對象。請記?。?strong>在執(zhí)行函數(shù)時,如果函數(shù)中的 this 是被上一級的對象所調(diào)用,那么 this 指向的就是上一級的對象;否則指向全局環(huán)境。
場景 2:上下文對象調(diào)用中的 this
我們直接來看“難”一點的:當存在更復雜的調(diào)用關系時,
const person = {
name: "Lucas",
brother: {
name: "Mike",
fn: function () {
return this.name;
}
}
};
console.log(person.brother.fn());
在這種嵌套的關系中,this 指向最后調(diào)用它的對象,因此輸出將會是: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沒有問題。難點在第二個和第三個上面,關鍵還是看調(diào)用this的那個函數(shù)。 - 第二個
console的o2.fn(),最終還是調(diào)用o1.fn(),因此答案仍然是o1。 - 最后一個,在進行
var fn = o1.fn賦值之后,是“裸奔”調(diào)用,因此這里的this指向window,答案當然是undefined。
如果是在面試中,我作為面試官,就會追問:如果我們需要讓 console.log(o2.fn()) 輸出 o2,該怎么做?
一般開發(fā)者可能會想到使用 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 指向最后調(diào)用它的對象,在 fn 執(zhí)行時,掛到 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 的高級考察往往會結合構造函數(shù)以及組合式實現(xiàn)繼承。實現(xiàn)繼承的話題,我們會單獨講到。構造函數(shù)的使用案例,我們結合下面的場景進行分析。
場景 4:構造函數(shù)和 this
function Foo() {
this.bar = "Lucas";
}
const instance = new Foo();
console.log(instance.bar);
答案將會輸出 Lucas。但是這樣的場景往往伴隨著下一個問題:new 操作符調(diào)用構造函數(shù),具體做了什么?以下供參考:
- 創(chuàng)建一個新的對象;
- 將構造函數(shù)的
this指向這個新對象; - 為這個對象添加屬性、方法等;
- 最終返回新對象。
以上過程,也可以用代碼表述:
var obj = {};
obj.__proto__ = Foo.prototype;
Foo.call(obj);
當然,這里對 new 的模擬是一個簡單基本版的,更復雜的情況這個問題下我不會贅述。
需要指出的是,如果在構造函數(shù)中出現(xiàn)了顯式 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。
結論:如果構造函數(shù)中顯式返回一個值,且返回的是一個對象,那么 this 就指向這個返回的對象;如果返回的不是一個對象,那么 this 仍然指向?qū)嵗?/p>
場景 5:箭頭函數(shù)中的 this 指向
箭頭函數(shù)使用 this 不適用以上標準規(guī)則,而是根據(jù)外層(函數(shù)或者全局)上下文作用域來決定。
const foo = {
fn: function () {
setTimeout(function () {
console.log(this);
});
}
};
console.log(foo.fn());
這道題中,this 出現(xiàn)在 setTimeout() 中的匿名函數(shù)里,因此 this 指向 window 對象。如果需要 this 指向 foo 這個 object 對象,可以巧用箭頭函數(shù)解決:
const foo = {
fn: function () {
setTimeout(() => {
console.log(this);
});
}
};
console.log(foo.fn());
// {fn: ?}
單純箭頭函數(shù)中的 this 非常簡單,但是綜合所有情況,結合 this 的優(yōu)先級考察,這時候 this 指向并不好確定。請繼續(xù)閱讀。
終極場景 6:this 優(yōu)先級相關
我們常常把通過 call、apply、bind、new對 this 綁定的情況稱為顯式綁定;根據(jù)調(diào)用關系確定的 this 指向稱為隱式綁定。
那么顯式綁定和隱式綁定誰的優(yōu)先級更高呢?
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 的顯式綁定一般來說優(yōu)先級比隱式綁定更高。
function foo(a) {
this.a = a;
}
const obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);
上述代碼通過 bind,將 bar 函數(shù)中的 this 綁定為 obj1 對象。執(zhí)行 bar(2) 后,obj1.a 值為 2。即經(jīng)過 bar(2) 執(zhí)行后,obj1 對象為:{a: 2}。
當再使用 bar 作為構造函數(shù)時:
var baz = new bar(3);
console.log(baz.a);
將會輸出 3。我們看 bar 函數(shù)本身是通過 bind 方法構造的函數(shù),其內(nèi)部已經(jīng)對將 this 綁定為 obj1,它再作為構造函數(shù),通過 new 調(diào)用時,返回的實例已經(jīng)與 obj1 解綁。 也就是說:
new 綁定修改了 bind 綁定中的 this,因此 new 綁定的優(yōu)先級比顯式 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(引用箭頭函數(shù))的 this 也會綁定到 obj1,箭頭函數(shù)的綁定無法被修改。
如果將 foo 完全寫成箭頭函數(shù)的形式:
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 等聲明變量的方式不再本課的主題當中,我們后續(xù)也將專門進行介紹。