這又是一個面試經典問題~/(ㄒoㄒ)/~~也是 ES5中衆多坑中的一個,在 ES6 中可能會極大避免 this 產生的錯誤,可是爲了一些老代碼的維護,最好仍是瞭解一下 this 的指向和 call、apply、bind 三者的區別。面試
本文首發於個人我的網站:cherryblog.site/windows
在 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
。bash
再看下這個例子:
例 2:app
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。ui
咱們再來看一下這個例子:
例 4:this
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 = this
apply
、call
、bind
例 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 函數也是能夠改變 this 的指向的,原理稍後再講,咱們先來看一下是怎麼實現的:
例 10:
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
}.apply(a),100);
}
};
a.func2() // Cherry複製代碼
例 11:
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
}.call(a),100);
}
};
a.func2() // Cherry複製代碼
例 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 都是能夠改變 this 的指向的,可是這三個函數稍有不一樣。
在 MDN 中定義 apply 以下;
apply() 方法調用一個函數, 其具備一個指定的this值,以及做爲一個數組(或相似數組的對象)提供的參數
語法:
fun.apply(thisArg, [argsArray])
其實 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 試一下
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複製代碼
==================================== 更新==============================
看到留言說,不少童靴不理解爲何 例 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 種
好比上面的 例 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;
}複製代碼
因此咱們能夠看到,在 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。