this是 JavaScript 語言的一個關鍵字。
它是函數運行時,在函數體內部自動生成的一個對象,只能在函數體內部使用。
var obj = {
foo: function () { console.log(this.bar) },
bar: 1
};
var foo = obj.foo;
var bar = 2;
obj.foo() // 1
foo() // 2
上述調用foo()函數,輸出不同的結果,這種差異的原因,就在于函數體內部使用了this關鍵字。this指的是函數運行時所在的環(huán)境。對于obj.foo()來說,foo運行在obj環(huán)境,所以this指向obj;對于foo()來說,foo運行在全局環(huán)境,所以this指向全局環(huán)境。所以,兩者的運行結果不一樣。
內存的數據結構
JavaScript 語言之所以有this的設計,跟內存里面的數據結構有關系。
var obj = { foo: 5 };
上面的代碼將一個對象賦值給變量obj。JavaScript 引擎會先在內存里面,生成一個對象{ foo: 5 },然后把這個對象的內存地址賦值給變量obj
也就是說,變量obj是一個地址(reference)。后面如果要讀取obj.foo,引擎先從obj拿到內存地址,然后再從該地址讀出原始的對象,返回它的foo屬性。
原始的對象以字典結構保存,每一個屬性名都對應一個屬性描述對象。舉例來說,上面例子的foo屬性,實際上是以下面的形式保存的。
{
foo: {
[[value]]: 5
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
注意,foo屬性的值保存在屬性描述對象的value屬性里面。
函數
這樣的結構是很清晰的,問題在于屬性的值可能是一個函數。
var obj = { foo: function () {} };
這時,引擎會將函數單獨保存在內存中,然后再將函數的地址賦值給foo屬性的value屬性。
{
foo: {
[[value]]: 函數的地址
...
}
}
由于函數是一個單獨的值,所以它可以在不同的環(huán)境(上下文)執(zhí)行。
var f = function () {};
var obj = { f: f };
// 單獨執(zhí)行
f()
// obj 環(huán)境執(zhí)行
obj.f()
環(huán)境變量
JavaScript 允許在函數體內部,引用當前環(huán)境的其他變量。
var f = function () {
console.log(x);
};
上面代碼中,函數體里面使用了變量x。該變量由運行環(huán)境提供。
現在問題就來了,由于函數可以在不同的運行環(huán)境執(zhí)行,所以需要有一種機制,能夠在函數體內部獲得當前的運行環(huán)境(context)。所以,this就出現了,它的設計目的就是在函數體內部,指代函數當前的運行環(huán)境。
var f = function () {
console.log(this.x);
}
上面代碼中,函數體里面的this.x就是指當前運行環(huán)境的x。
var f = function () {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
// 單獨執(zhí)行
f() // 1
// obj 環(huán)境執(zhí)行
obj.f() // 2
上面代碼中,函數f在全局環(huán)境執(zhí)行,this.x指向全局環(huán)境的x。
在obj環(huán)境執(zhí)行,this.x指向obj.x。
- obj.foo()是通過obj找到foo,所以就是在obj環(huán)境執(zhí)行。
- 一旦var foo = obj.foo,變量foo就直接指向函數本身,所以foo()就變成在全局環(huán)境執(zhí)行。
this的綁定方式
默認綁定
默認綁定是在不使用其他綁定規(guī)則時的規(guī)則,通常是獨立函數的調用。
function greeting() {
console.log(`Hello, ${this.name}`);
}
var name = 'Eric';
greeting();
// Hello, Eric
隱式綁定
隱式綁定指的是在一個對象上調用函數。
通過 obj 調用 greeting 方法,this 就指向了 obj
將 obj.greeting 賦給了一個全局的變量 otherGreeting,所以在執(zhí)行 otherGreeting 時,this 會指向 window.
function greeting() {
console.log(`Hello,${this.name}`);
};
var name = 'Eric';
var obj = {
name: 'World',
greeting,
};
var otherGreeting = obj.greeting;
greeting(); // Hello,Eric
obj.greeting(); // Hello,World
otherGreeting().greeting(); // Hello,Eric
異步操作時候隱式綁定丟失問題
如果涉及到回調函數(異步操作),就要小心隱式綁定的丟失問題。
function greeting() {
console.log(`Hello,${this.name}`);
};
var name = 'Eric';
var obj1 = {
name: 'Obj1',
greeting() {
setTimeout(function() {
console.log(`Hello,${this.name}`);
})
}
};
var obj2 = {
name: 'Obj2',
greeting,
};
obj1.greeting(); //Hello,Eric
obj2.greeting(); //Hello,Obj2
setTimeout(obj2.greeting, 100); //Hello,Eric
setTimeout(function() {
obj2.greeting();
}, 200); //Hello,Obj2
- obj1.greeting()調用
因為涉及到異步操作setTimeout。在JavaScript中,一段代碼執(zhí)行時,會先執(zhí)行宏任務中的同步代碼,當遇到setTimeout之類的宏任務,那么就把這個 setTimeout內部的函數推入「宏任務的隊列」中,下一輪宏任務執(zhí)行時調用。當本輪宏任務調用結束后,下一輪宏任務執(zhí)行時,此時函數位于內存中,this指向全局環(huán)境。此時 this.name 就是 Eric
- obj2.greeting()調用
greeting函數位于內存中,通過obj2來調用,那么this的指向環(huán)境變成了obj2。如前面講述的圖所示:
- setTimeout(obj2.greeting, 100)調用
可以理解為將 obj2.greeting 賦值給一個新的變量(此時與obj1.greeting類似),所以此時 this 也是指向了 window。
- setTimeout(function() {obj2.greeting();}, 200)調用
此時setTimeout的function參數里面包含了obj2.greeting()方法,調用則是隱式綁定,此時 this 指向 obj2。我們可以做個實驗來驗證
setTimeout(function() {
console.log(this);
obj2.greeting();
}, 200);
其中,打印出來的this指向window,所以在200毫秒后,function回調函數里面的this環(huán)境指向window,這個與前面的實驗obj1.greeting()調用是一致的,此時相當于在全局環(huán)境中,我們來調用了obj2.greeting(),這個與前面的實驗obj2.greeting()調用是一致的,所以打印的結果是Hello,Obj2
顯式綁定
顯示綁定就是通過 call, apply, bind 來顯式地指定 this 的綁定對象。三者的第一個參數都是傳遞 this 指向的對象,call 與 apply 的區(qū)別是前者從第二個參數起傳遞一個參數序列,后者傳遞一個數組,call, apply 和 bind 的區(qū)別是前兩個都會立即執(zhí)行對應的函數,而 bind 方法不會。
我們通過 call 顯式綁定 this 指向的對象來解決隱式綁定丟失的問題。
function greeting() {
console.log(`Hello,${this.name}`);
};
var name = 'Eric';
var obj = {
name: 'Obj',
greeting,
};
var otherGreeting = obj.greeting;
// 強制將 this 綁定到 obj
otherGreeting.call(obj); // Hello,Obj
setTimeout(obj.greeting.call(obj), 100); // Hello,Obj
在使用顯式綁定時,如果將 null, undefined 作為第一個參數傳入 call, apply 或者 bind,實際應用的是默認綁定。
function greeting() {
console.log(`Hello,${this.name}`);
};
var name = 'Eric';
var obj = {
name: 'Obj',
greeting,
};
var otherGreeting = obj.greeting;
// this 仍然指向 window
otherGreeting.call(null); //Hello,Eric
箭頭函數
- 函數體內的 this 對象,繼承的是外層代碼塊的 this
- 不可以當作構造函數,也就是說,不可以使用 new 命令,否則會拋出一個錯誤。
- 不可以使用 arguments 對象,該對象在函數體內不存在。如果要用,可以用 rest 參數代替。
- 不可以使用 yield 命令,因此箭頭函數不能用作 Generator 函數。
- 箭頭函數沒有自己的 this,因此不能使用 call()、apply()、bind()等方法改變 this 的指向。
var obj = {
hi: function() {
console.log(this);
return () => {
console.log(this);
};
},
sayHi: function() {
return function() {
console.log(this);
return () => {
console.log(this);
};
};
},
say: () => {
console.log(this);
}
};
let hi = obj.hi(); // 輸出 obj 對象
hi(); // 輸出 obj 對象
let sayHi = obj.sayHi(); // 輸出 window
fun1(); // 輸出 window
obj.say(); // 輸出 window
- 第一步是隱式綁定,此時 this 指向 obj,所以打印出 obj 對象
- 第二步執(zhí)行 hi() 方法,雖然看著像閉包,但這是一個箭頭函數,它會繼承上一層的 this,也就是 obj,所以打印出 obj 對象
- 因為 obj.sayHi() 返回一個閉包,所以 this 指向 window,因此打印出 window 對象
- 同樣箭頭函數繼承上一層的 this,所以 this 指向 window,因此打印出 window 對象
- 最后一次輸出,因為 obj 中不存在 this,因此按作用域鏈找到全局的 this,也就是 window,所以打印出 window 對象
var obj = {
name: 'Eric',
greeting() {
setTimeout(() => {
console.log(`Hello, ${this.name}`);
})
},
greeting2() {
console.log(`Hello, ${this.name}`);
},
greeting3() {
setTimeout(function() {
console.log(`Hello, ${this.name}`);
});
}
};
var name = 'Global';
obj.greeting(); //Hello, Eric
obj.greeting2(); //Hello, Eric
obj.greeting3(); //Hello, Global
- obj.greeting(),雖然 setTimeout 會將 this 指向全局,但箭頭函數繼承上一層的 this,也就是 obj.greeting() 的 this,因為這是一個隱式綁定,所以 this 指向 obj,所以箭頭函數的 this 也會指向 obj.
- obj.greeting2(),這里是一個隱式綁定,所以 this 指向 obj
- greeting3(),setTimeout 會將 this 指向全局
解析一
var number = 5;
var obj = {
number: 3,
fn: (function() {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function() {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
};
})(),
};
var fn = obj.fn;
fn.call(null);
obj.fn();
console.log(window.number);
因為 obj.fn 是一個立即執(zhí)行函數(this 會指向 window),所以在 obj 創(chuàng)建時就會執(zhí)行一次,并返回閉包函數。
var number; // 創(chuàng)建了一個私有變量 number 但未賦初值
this.number *= 2; // this.number 指向的是全局那個 number,所以 window.number = 10
number = number * 2; // 因為私有變量 number 未賦初值,所以乘以 2 會變?yōu)?NaN
number = 3; // 此時私有變量 number 變?yōu)?3
接著執(zhí)行下面兩句:
var fn = obj.fn;
fn.call(null);
因為將 obj.fn 賦值給一個全局變量 fn,所以此時 this 指向 window。接著,當 call 的第一個參數是 null 或者 undefined 時,調用的是默認綁定,因此 this 仍然指向 window.
var num = this.number; // 因為 window.number = 10,所以 num 也就是 10
this.number *= 2; // window.number 變成了 20
console.log(num); // 打印出 10
number *= 3; // 因為是閉包函數,有權訪問父函數的私有變量,所以此時 number 為 9
console.log(number); // 打印出 9
當執(zhí)行 obj.fn(); 時,此時的 this 指向的是 obj:
var num = this.number; // 因為 obj.number = 3,所以 num 也就為 3
this.number *= 2; // obj.number 變?yōu)?6
console.log(num); // 打印出 3
number *= 3; // 上一輪私有變量為變成了 9,所以這里變成 27
console.log(number); // 打印出 27
最后打印出 window.number 就是 20
最終結果:
10
9
3
27
20
解析二
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments[0]();
},
};
obj.method(fn, 1);
最終結果:
10
2
傳入了 fn 而非 fn(),相當于把 fn 函數賦值給 method 里的 fn 執(zhí)行,所以這里是默認綁定,此時 this 指向 window,所以執(zhí)行 fn() 時會打印出 10
arguments0,就相當于執(zhí)行 fn(),所以是隱式綁定,此時 this 指向 arguments,所以 this.length 就相當于 arguments.length,因為我們傳遞了兩個參數,因此返回 2
window.val = 1;
var obj = {
val: 2,
dbl: function() {
this.val *= 2;
val *= 2;
console.log('val:', val);
console.log('this.val:', this.val);
},
};
obj.dbl();
var func = obj.dbl;
func();
最終結果:
2, 4
8, 8
第一次調用是隱式調用,因此 this 指向 obj,所以 this.val 也就是 obj.val 變成了 4,但是 dbl 方法中沒有定義 val,所以會沿著作用域鏈找到 window.val,所以會依次打印出 2,4
第二次是默認調用,this 指向 window,window.val 會經歷兩次乘 2 變成 8,所以會依次打印出 8,8
總結
- 函數是否在 new 中調用(new 綁定),如果是,那么 this 綁定的是新創(chuàng)建的對象。
- 函數是否通過 call,apply 調用,或者使用了 bind(即硬綁定),如果是,那么 this 綁定的就是指定的對象。
- 函數是否在某個上下文對象中調用(隱式綁定),如果是的話,this 綁定的是那個上下文對象。一般是 obj.foo()
- 如果以上都不是,那么使用默認綁定。如果在嚴格模式下,則綁定到 undefined,否則綁定到全局對象。
- 如果把 Null 或者 undefined 作為 this 的綁定對象傳入 call、apply 或者 bind,這些值在調用時會被忽略,實際應用的是默認綁定規(guī)則。
- 如果是箭頭函數,箭頭函數的 this 繼承的是外層代碼塊的 this。
Notes from
http://www.ruanyifeng.com/blog/2018/06/javascript-this.html