這又是一個面試經典問題~/(ㄒoㄒ)/~~也是 ES5中眾多坑中的一個,在 ES6 中可能會極大避免 this 產生的錯誤,但是為了一些老程式碼的維護,最好還是瞭解一下 this 的指向和 call、apply、bind 三者的區別。
本文首發於我的個人網站:cherryblog.site/
this 的指向
在 ES5 中,其實 this 的指向,始終堅持一個原理:this 永遠指向最後呼叫它的那個物件,來,跟著我朗讀三遍:this 永遠指向最後呼叫它的那個物件,this 永遠指向最後呼叫它的那個物件,this 永遠指向最後呼叫它的那個物件。記住這句話,this 你已經瞭解一半了。
下面我們來看一個最簡單的例子:
例 1:
var name = "windowsName";
function a() {
var name = "Cherry";
console.log(this.name); // windowsName
console.log("inner:" + this); // inner: Window
}
a();
console.log("outer:" + this) // outer: Window複製程式碼
這個相信大家都知道為什麼 log 的是 windowsName,因為根據剛剛的那句話“this 永遠指向最後呼叫它的那個物件”,我們看最後呼叫 a
的地方 a();
,前面沒有呼叫的物件那麼就是全域性物件 window,這就相當於是 window.a()
;注意,這裡我們沒有使用嚴格模式,如果使用嚴格模式的話,全域性物件就是 undefined
,那麼就會報錯 Uncaught TypeError: Cannot read property `name` of undefined
。
再看下這個例子:
例 2:
var name = "windowsName";
var a = {
name: "Cherry",
fn : function () {
console.log(this.name); // Cherry
}
}
a.fn();複製程式碼
在這個例子中,函式 fn 是物件 a 呼叫的,所以列印的值就是 a 中的 name 的值。是不是有一點清晰了呢~
我們做一個小小的改動:
例 3:
var name = "windowsName";
var a = {
name: "Cherry",
fn : function () {
console.log(this.name); // Cherry
}
}
window.a.fn();複製程式碼
這裡列印 Cherry 的原因也是因為剛剛那句話“this 永遠指向最後呼叫它的那個物件”,最後呼叫它的物件仍然是物件 a。
我們再來看一下這個例子:
例 4:
var name = "windowsName";
var a = {
// name: "Cherry",
fn : function () {
console.log(this.name); // undefined
}
}
window.a.fn();複製程式碼
這裡為什麼會列印 undefined
呢?這是因為正如剛剛所描述的那樣,呼叫 fn 的是 a 物件,也就是說 fn 的內部的 this 是物件 a,而物件 a 中並沒有對 name 進行定義,所以 log 的 this.name
的值是 undefined
。
這個例子還是說明了:this 永遠指向最後呼叫它的那個物件,因為最後呼叫 fn 的物件是 a,所以就算 a 中沒有 name 這個屬性,也不會繼續向上一個物件尋找 this.name
,而是直接輸出 undefined
。
再來看一個比較坑的例子:
例 5:
var name = "windowsName";
var a = {
name : null,
// name: "Cherry",
fn : function () {
console.log(this.name); // windowsName
}
}
var f = a.fn;
f();複製程式碼
這裡你可能會有疑問,為什麼不是 Cherry
,這是因為雖然將 a 物件的 fn 方法賦值給變數 f 了,但是沒有呼叫,再接著跟我念這一句話:“this 永遠指向最後呼叫它的那個物件”,由於剛剛的 f 並沒有呼叫,所以 fn()
最後仍然是被 window 呼叫的。所以 this 指向的也就是 window。
由以上五個例子我們可以看出,this 的指向並不是在建立的時候就可以確定的,在 es5 中,永遠是this 永遠指向最後呼叫它的那個物件。
再來看一個例子:
例 6:
var name = "windowsName";
function fn() {
var name = `Cherry`;
innerFunction();
function innerFunction() {
console.log(this.name); // windowsName
}
}
fn()複製程式碼
讀到現在了應該能夠理解這是為什麼了吧(o゚▽゚)o。
怎麼改變 this 的指向
改變 this 的指向我總結有以下幾種方法:
- 使用 ES6 的箭頭函式
- 在函式內部使用
_this = this
- 使用
apply
、call
、bind
- new 例項化一個物件
例 7:
var name = "windowsName";
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
},100);
}
};
a.func2() // this.func1 is not a function複製程式碼
在不使用箭頭函式的情況下,是會報錯的,因為最後呼叫 setTimeout
的物件是 window,但是在 window 中並沒有 func1 函式。
我們在改變 this 指向這一節將把這個例子作為 demo 進行改造。
箭頭函式
眾所周知,ES6 的箭頭函式是可以避免 ES5 中使用 this 的坑的。箭頭函式的 this 始終指向函式定義時的 this,而非執行時。,箭頭函式需要記著這句話:“箭頭函式中沒有 this 繫結,必須通過查詢作用域鏈來決定其值,如果箭頭函式被非箭頭函式包含,則 this 繫結的是最近一層非箭頭函式的 this,否則,this 為 undefined”。
例 8 :
var name = "windowsName";
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( () => {
this.func1()
},100);
}
};
a.func2() // Cherry複製程式碼
在函式內部使用 _this = this
如果不使用 ES6,那麼這種方式應該是最簡單的不會出錯的方式了,我們是先將呼叫這個函式的物件儲存在變數 _this
中,然後在函式中都使用這個 _this
,這樣 _this
就不會改變了。
例 9:
var name = "windowsName";
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
var _this = this;
setTimeout( function() {
_this.func1()
},100);
}
};
a.func2() // Cherry複製程式碼
這個例子中,在 func2 中,首先設定 var _this = this;
,這裡的 this
是呼叫 func2
的物件 a,為了防止在 func2
中的 setTimeout 被 window 呼叫而導致的在 setTimeout 中的 this 為 window。我們將 this(指向變數 a)
賦值給一個變數 _this
,這樣,在 func2
中我們使用 _this
就是指向物件 a 了。
使用 apply、call、bind
使用 apply、call、bind 函式也是可以改變 this 的指向的,原理稍後再講,我們先來看一下是怎麼實現的:
使用 apply
例 10:
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
}.apply(a),100);
}
};
a.func2() // Cherry複製程式碼
使用 call
例 11:
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
}.call(a),100);
}
};
a.func2() // Cherry複製程式碼
使用 bind
例 12:
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
}.bind(a)(),100);
}
};
a.func2() // Cherry複製程式碼
apply、call、bind 區別
剛剛我們已經介紹了 apply、call、bind 都是可以改變 this 的指向的,但是這三個函式稍有不同。
在 MDN 中定義 apply 如下;
apply() 方法呼叫一個函式, 其具有一個指定的this值,以及作為一個陣列(或類似陣列的物件)提供的引數
語法:
fun.apply(thisArg, [argsArray])
- thisArg:在 fun 函式執行時指定的 this 值。需要注意的是,指定的 this 值並不一定是該函式執行時真正的 this 值,如果這個函式處於非嚴格模式下,則指定為 null 或 undefined 時會自動指向全域性物件(瀏覽器中就是window物件),同時值為原始值(數字,字串,布林值)的 this 會指向該原始值的自動包裝物件。
- argsArray:一個陣列或者類陣列物件,其中的陣列元素將作為單獨的引數傳給 fun 函式。如果該引數的值為null 或 undefined,則表示不需要傳入任何引數。從ECMAScript 5 開始可以使用類陣列物件。瀏覽器相容性請參閱本文底部內容。
apply 和 call 的區別
其實 apply 和 call 基本類似,他們的區別只是傳入的引數不同。
call 的語法為:
fun.call(thisArg[, arg1[, arg2[, ...]]])複製程式碼
所以 apply 和 call 的區別是 call 方法接受的是若干個引數列表,而 apply 接收的是一個包含多個引數的陣列。
例 13:
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.apply(a,[1,2]) // 3複製程式碼
例 14:
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.call(a,1,2) // 3複製程式碼
bind 和 apply、call 區別
我們先來將剛剛的例子使用 bind 試一下
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.bind(a,1,2)複製程式碼
我們會發現並沒有輸出,這是為什麼呢,我們來看一下 MDN 上的文件說明:
bind()方法建立一個新的函式, 當被呼叫時,將其this關鍵字設定為提供的值,在呼叫新函式時,在任何提供之前提供一個給定的引數序列。
所以我們可以看出,bind 是建立一個新的函式,我們必須要手動去呼叫:
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.bind(a,1,2)() // 3複製程式碼
==================================== 更新==============================
JS 中的函式呼叫
看到留言說,很多童靴不理解為什麼 例 6 的 innerFunction 和 例 7 的 this 是指向 window 的,所以我就來補充一下 JS 中的函式呼叫。
例 6:
var name = "windowsName";
function fn() {
var name = `Cherry`;
innerFunction();
function innerFunction() {
console.log(this.name); // windowsName
}
}
fn()複製程式碼
例 7:
var name = "windowsName";
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
},100);
}
};
a.func2() // this.func1 is not a function複製程式碼
函式呼叫的方法一共有 4 種
- 作為一個函式呼叫
- 函式作為方法呼叫
- 使用建構函式呼叫函式
- 作為函式方法呼叫函式(call、apply)
作為一個函式呼叫
比如上面的 例 1:
例 1:
var name = "windowsName";
function a() {
var name = "Cherry";
console.log(this.name); // windowsName
console.log("inner:" + this); // inner: Window
}
a();
console.log("outer:" + this) // outer: Window複製程式碼
這樣一個最簡單的函式,不屬於任何一個物件,就是一個函式,這樣的情況在 JavaScript 的在瀏覽器中的非嚴格模式預設是屬於全域性物件 window 的,在嚴格模式,就是 undefined。
但這是一個全域性的函式,很容易產生命名衝突,所以不建議這樣使用。
函式作為方法呼叫
所以說更多的情況是將函式作為物件的方法使用。比如例 2:
例 2:
var name = "windowsName";
var a = {
name: "Cherry",
fn : function () {
console.log(this.name); // Cherry
}
}
a.fn();複製程式碼
這裡定義一個物件 a
,物件 a
有一個屬性(name
)和一個方法(fn
)。
然後物件 a
通過 .
方法呼叫了其中的 fn 方法。
然後我們一直記住的那句話“this 永遠指向最後呼叫它的那個物件”,所以在 fn 中的 this 就是指向 a 的。
使用建構函式呼叫函式
如果函式呼叫前使用了 new 關鍵字, 則是呼叫了建構函式。
這看起來就像建立了新的函式,但實際上 JavaScript 函式是重新建立的物件:
// 建構函式:
function myFunction(arg1, arg2) {
this.firstName = arg1;
this.lastName = arg2;
}
// This creates a new object
var a = new myFunction("Li","Cherry");
a.lastName; // 返回 "Cherry"複製程式碼
這就有要說另一個面試經典問題:new 的過程了,(ಥ_ಥ)
這裡就簡單的來看一下 new 的過程吧:
虛擬碼表示:
var a = new myFunction("Li","Cherry");
new myFunction{
var obj = {};
obj.__proto__ = myFunction.prototype;
var result = myFunction.call(obj,"Li","Cherry");
return typeof result === `obj`? result : obj;
}複製程式碼
- 建立一個空物件 obj;
- 將新建立的空物件的隱式原型指向其建構函式的顯示原型。
- 使用 call 改變 this 的指向
- 如果無返回值或者返回一個非物件值,則將 obj 返回作為新物件;如果返回值是一個新物件的話那麼直接直接返回該物件。
所以我們可以看到,在 new 的過程中,我們是使用 call 改變了 this 的指向。
作為函式方法呼叫函式
在 JavaScript 中, 函式是物件。
JavaScript 函式有它的屬性和方法。
call() 和 apply() 是預定義的函式方法。 兩個方法可用於呼叫函式,兩個方法的第一個引數必須是物件本身在 JavaScript 嚴格模式(strict mode)下, 在呼叫函式時第一個引數會成為 this 的值, 即使該引數不是一個物件。
在 JavaScript 非嚴格模式(non-strict mode)下, 如果第一個引數的值是 null 或 undefined, 它將使用全域性物件替代。
這個時候我們再來看例 6:
例 6:
var name = "windowsName";
function fn() {
var name = `Cherry`;
innerFunction();
function innerFunction() {
console.log(this.name); // windowsName
}
}
fn()複製程式碼
這裡的 innerFunction() 的呼叫是不是屬於第一種呼叫方式:作為一個函式呼叫(它就是作為一個函式呼叫的,沒有掛載在任何物件上,所以對於沒有掛載在任何物件上的函式,在非嚴格模式下 this 就是指向 window 的)
然後再看一下 例 7:
例 7:
var name = "windowsName";
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
},100 );
}
};
a.func2() // this.func1 is not a function複製程式碼
這個簡單一點的理解可以理解為“匿名函式的 this 永遠指向 window”,你可以這樣想,還是那句話this 永遠指向最後呼叫它的那個物件,那麼我們就來找最後呼叫匿名函式的物件,這就很尷尬了,因為匿名函式名字啊,笑哭,所以我們是沒有辦法被其他物件呼叫匿名函式的。所以說 匿名函式的 this 永遠指向 window。
如果這個時候你要問,那匿名函式都是怎麼定義的,首先,我們通常寫的匿名函式都是自執行的,就是在匿名函式後面加 ()
讓其自執行。其次就是雖然匿名函式不能被其他物件呼叫,但是可以被其他函式呼叫啊,比如例 7 中的 setTimeout。