this
上次講了閉包,其實我感覺我上次沒講什么,閉包的確神秘,但是又不是很復雜,需要長篇大論去解釋。剛好,我看到了js中的另一個重點,this。this也是會讓初學者頭疼的一個知識點。有時候感覺莫名其妙,直覺上應該是這個答案,實際上卻是錯的。
錯誤的認識
- this不只在js中出現,java, c++ 這些強類型語言也是有this的,不過它們都是叫this, 本質是不太一樣的。學過這些強類型語言的同學很容易就認為this是指向自己的。然而,這在js中,是不正確的。下面的代碼會打臉。
function fun (val) {
this.a = val;
}
fun(0);
fun.a++;
console.log(fun.a);
console.log(a);
如果this是指向自己的話,那么現在fun.a應該是1咯,對吧。然而真的是這樣么? 打開chrome, F12將代碼拷貝在控制臺看看結果,fun.a的輸出是NaN,第二個log出卻是0。傻眼了吧?好吧,其實當時我也是一臉懵逼。這個結果,告訴了我們js的this不如你想象的那樣。這是你不知道的javaScript的this。
- 除了認為this指向自己,《你不知道的javaScript》中也提到另一種錯誤的理解。this指向函數的作用域。詳細點說,就是一個函數,它的this是指向它的父級作用域。嗯,好吧,我覺得這種理解很自然,很舒服。下面的代碼,就是這種錯誤理解導致的錯誤寫法。
function a () {
console.log(this.c);
}
function b() {
let c = 1;
this.a();
}
b(); // undefined
其實上面的結果不會輸出1,所以說明了這個理解也是錯誤的。
this的正確使用姿勢
this的指向呢,其實一句話就概況了。
this的綁定和函數聲明的位置沒有任何關系,只取決于函數的調用方式。
其實this的指向就看誰調用了它吧,總結下來,有以下4種情況:
-
默認綁定
默認綁定,我曾經也聽到說成是直接函數調用。下面的代碼就是默認綁定
function fun () {
console.log(this.a);
}
var a = 1;
fun() // 1
默認綁定的情況,函數中的this是綁定在window上,當然是在非嚴格模式下。在嚴格模式下,this是指向undefined, 上面的代碼就會拋出一個錯誤。說回非嚴格模式下的默認綁定,由于this指向了window。所以, this.a其實就是window.a。那么自然輸出了1。
-
隱式綁定
當一個函數是某個對象的屬性時,我們直接對象.函數名調用該函數時,該函數的this是指向該對象的。其實,我個人認為,默認綁定的情況下fun()
等價于window.fun()
,這也正好解釋了為什么默認綁定時,this是指向了全局對象。下面是隱式綁定的一個例子:
let obj = {
name: '123',
func () {
console.log(this.name);
}
}
obj.func() // '123'
注意:隱式綁定當使用不當時,會出現this的丟失現象。比如:
// 情況一
window.name = '1234';
let obj = {
name: '123',
func () {
console.log(this.name);
}
}
func = obj.func;
func(); // '1234'
// 情況二
window.name = '1234';
let obj = {
name: '123',
func () {
return function () {
console.log(this.name);
}
}
}
func = obj.func();
func(); // '1234'
// 情況三
window.name = '1234';
let obj = {
name: '123',
func () {
console.log(this.name);
}
}
setTimeout(obj.func, 1000); // '1234'
情況一:不是隱式綁定了obj么,輸出應該是123才對啊。結果卻輸出了1234。嗯,我們要根據代碼來分析,this的指向要對比四種情況,然后去找到對應的情況來確定。func()
很明顯就是之前我們說的默認綁定,所以輸出了全局對象中的a屬性的值。
情況二:情況二其實就是返回了一個函數,然后這個函數在全局作用域直接調用了,所以就是默認綁定,如果需要綁定原來的obj,可以用以下的辦法:
// 解法1
window.name = '1234';
let obj = {
name: '123',
func () {
let that = this;
return function () {
console.log(that.name);
}
}
}
func = obj.func();
func(); // '123'
// 解法2
window.name = '1234';
let obj = {
name: '123',
func () {
return () => console.log(this.name);
}
}
func = obj.func();
func(); // '123'
// 解法3
window.name = '1234';
let obj = {
name: '123',
func () {
return function () {
console.log(this.name);
}.bind(this);
}
}
func = obj.func();
func(); // '123'
情況三: setTimeout的第一個參數是一個函數,其實setTimeout的內部實現的偽代碼應該是這樣的
function setTimeout (fn, delay) {
// 等了 delay 毫秒
fn()
}
所以其實也是變成了默認綁定嘛。
-
顯式綁定
顯式綁定就是用js的call, apply, bind的這些函數來綁定this啦。
window.name = 'window';
let xiaoming = { name: 'xiaoming' };
function bar() { console.log(this.name); }
// call
bar.call(xiaoming); // 'xiaoming';
// apply
bar.apply(xiaoming); // 'xiaoming';
// bind
bar.bind(xiaoming)(); // 'xiaoming';
它們間的異同:
- 同
- 它們三者可以用來改變函數this的指向, 第一個參數都是要指向的對象
- 異
- apply 和 call 接受的參數不太一樣,call只能一個一個傳入參數,apply可以傳入一個參數的數組
- bind返回的是綁定后的函數,apply 和 call是綁定時同時執行。
-
new 綁定
在js中,我們使用new運算符時,其實,經過了下面的操作
1、創建(或者說構造)一個全新的對象
2、這個新對象會被執行[[原型]]連接
3、這個新對象會被綁定到函數的this
4、如果函數沒有返回其他對象,那么new表達式中的函數調用會自動返回這個新對象
綜上,那么下面的代碼,
function fn (a) { this.a = a; }
let a = new fn(1);
console.log(a.a); // 1
就是上面的四個步驟過一遍的結果了,this綁定到了的這個新對象a上。
總結
逼逼了這么多。。其實this的指向只要弄清楚這四種情況,絕大部分的情況都可以自己判斷出來。其實還是會有些很奇葩的情況的,如果我沒記錯,但是根據我目前的開發經驗,只要你或者你同事不作死,就不會寫出那么奇葩的情況。如果說是面試題,當我沒說。來來來,最后總結遍,四個方法就是:
- 如果是new出來的對象,那么this指向該對象無誤。
- 如果有用顯式綁定,那么this指向綁定的對象
- 是否作為一個對象的屬性來調用,是的話,this指向該對象。當然要注意this丟失的情況。
- 如果上面都沒有,那么默認綁定應該無誤。
多練習慢慢就會一眼就看出this的指向了,工多手熟。如果有誤,請指出。還有祝大家國慶快樂!