定義
函數的 this 關鍵字在 JAVAScript 中的表現略有不同,此外,在嚴格模式和非嚴格模式之間也會有一些差別
在絕大多數情況下,函數的調用方式決定了 this 的值(運行時綁定)
this 關鍵字是函數運行時自動生成的一個內部對象,只能在函數內部使用,總指向調用它的對象
舉個例子:
function baz() {
// 當前調用棧是:baz
// 因此,當前調用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar的調用位置
}
function bar() {
// 當前調用棧是:baz --> bar
// 因此,當前調用位置在baz中
console.log( "bar" );
foo(); // <-- foo的調用位置
}
function foo() {
// 當前調用棧是:baz --> bar --> foo
// 因此,當前調用位置在bar中
console.log( "foo" );
}
baz(); // <-- baz的調用位置
同時,this在函數執行過程中,this一旦被確定了,就不可以再更改
var a = 10;
var obj = {
a: 20
}
function fn() {
this = obj; // 修改this,運行后會報錯
console.log(this.a);
}
fn();
綁定規則
根據不同的使用場合,this有不同的值,主要分為下面幾種情況:
- 默認綁定
- 隱式綁定
- new綁定
- 顯示綁定
默認綁定
全局環境中定義person函數,內部使用this關鍵字
var name = 'Jenny';
function person() {
return this.name;
}
console.log(person()); //Jenny
上述代碼輸出Jenny,原因是調用函數的對象在游覽器中位window,因此this指向window,所以輸出Jenny
注意:
嚴格模式下,不能將全局對象用于默認綁定,this會綁定到undefined,只有函數運行在非嚴格模式下,默認綁定才能綁定到全局對象
隱式綁定
函數還可以作為某個對象的方法調用,這時this就指這個上級對象
function test() {
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m(); // 1
這個函數中包含多個對象,盡管這個函數是被最外層的對象所調用,this指向的也只是它上一級的對象
var o = {
a:10,
b:{
fn:function(){
console.log(this.a); //undefined
}
}
}
o.b.fn();
上述代碼中,this的上一級對象為b,b內部并沒有a變量的定義,所以輸出undefined
這里再舉一種特殊情況
var o = {
a:10,
b:{
a:12,
fn:function(){
console.log(this.a); //undefined
console.log(this); //window
}
}
}
var j = o.b.fn;
j();
此時this指向的是window,這里的大家需要記住,this永遠指向的是最后調用它的對象,雖然fn是對象b的方法,但是fn賦值給j時候并沒有執行,所以最終指向window
new綁定
通過構建函數new關鍵字生成一個實例對象,此時this指向這個實例對象
function test() {
this.x = 1;
}
var obj = new test();
obj.x // 1
上述代碼之所以能過輸出1,是因為new關鍵字改變了this的指向
這里再列舉一些特殊情況:
new過程遇到return一個對象,此時this指向為返回的對象
function fn()
{
this.user = 'xxx';
return {};
}
var a = new fn();
console.log(a.user); //undefined
如果返回一個簡單類型的時候,則this指向實例對象
function fn()
{
this.user = 'xxx';
return 1;
}
var a = new fn;
console.log(a.user); //xxx
注意的是null雖然也是對象,但是此時new仍然指向實例對象
function fn()
{
this.user = 'xxx';
return null;
}
var a = new fn;
console.log(a.user); //xxx
顯示修改
Apply()、call()、bind()是函數的一個方法,作用是改變函數的調用對象。它的第一個參數就表示改變后的調用這個函數的對象。因此,這時this指的就是這第一個參數
var x = 0;
function test() {
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply(obj) // 1
關于apply、call、bind三者的區別,我們后面再詳細說
箭頭函數
在 ES6 的語法中還提供了箭頭函語法,讓我們在代碼書寫時就能確定 this 的指向(編譯時綁定)
舉個例子:
const obj = {
sayThis: () => {
console.log(this);
}
};
obj.sayThis(); // window 因為 JavaScript 沒有塊作用域,所以在定義 sayThis 的時候,里面的 this 就綁到 window 上去了
const globalSay = obj.sayThis;
globalSay(); // window 瀏覽器中的 global 對象
雖然箭頭函數的this能夠在編譯的時候就確定了this的指向,但也需要注意一些潛在的坑
下面舉個例子:
綁定事件監聽
const button = document.getElementById('mngb');
button.addEventListener('click', ()=> {
console.log(this === window) // true
this.innerhtml = 'clicked button'
})
上述可以看到,我們其實是想要this為點擊的button,但此時this指向了window
包括在原型上添加方法時候,此時this指向window
Cat.prototype.sayName = () => {
console.log(this === window) //true
return this.name
}
const cat = new Cat('mm');
cat.sayName()
同樣的,箭頭函數不能作為構建函數
優先級
隱式綁定 VS 顯式綁定
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
顯然,顯示綁定的優先級更高
new綁定 VS 隱式綁定
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
可以看到,new綁定的優先級>隱式綁定
new綁定 VS 顯式綁定
因為new和apply、call無法一起使用,但硬綁定也是顯式綁定的一種,可以替換測試
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
bar被綁定到obj1上,但是new bar(3) 并沒有像我們預計的那樣把obj1.a修改為3。但是,new修改了綁定調用bar()中的this
我們可認為new綁定優先級>顯式綁定
綜上,new綁定優先級 > 顯示綁定優先級 > 隱式綁定優先級 > 默認綁定優先級
執行上下文
簡單的來說,執行上下文是一種對Javascript代碼執行環境的抽象概念,也就是說只要有Javascript代碼運行,那么它就一定是運行在執行上下文中
執行上下文的類型分為三種:
- 全局執行上下文:只有一個,瀏覽器中的全局對象就是 window對象,this 指向這個全局對象
- 函數執行上下文:存在無數個,只有在函數被調用的時候才會被創建,每次調用函數都會創建一個新的執行上下文
- Eval 函數執行上下文: 指的是運行在 eval 函數中的代碼,很少用而且不建議使用
下面給出全局上下文和函數上下文的例子:
紫色框住的部分為全局上下文,藍色和橘色框起來的是不同的函數上下文。只有全局上下文(的變量)能被其他任何上下文訪問
可以有任意多個函數上下文,每次調用函數創建一個新的上下文,會創建一個私有作用域,函數內部聲明的任何變量都不能在當前函數作用域外部直接訪問
生命周期
執行上下文的生命周期包括三個階段:創建階段 → 執行階段 → 回收階段
創建階段
創建階段即當函數被調用,但未執行任何其內部代碼之前
創建階段做了三件事:
- 確定 this 的值,也被稱為 This Binding
- LexicalEnvironment(詞法環境) 組件被創建
- VariableEnvironment(變量環境) 組件被創建
偽代碼如下:
ExecutionContext = {
ThisBinding = <this value>, // 確定this
LexicalEnvironment = { ... }, // 詞法環境
VariableEnvironment = { ... }, // 變量環境
}
This Binding
確定this的值我們前面講到,this的值是在執行的時候才能確認,定義的時候不能確認
詞法環境
詞法環境有兩個組成部分:
- 全局環境:是一個沒有外部環境的詞法環境,其外部環境引用為null,有一個全局對象,this 的值指向這個全局對象
- 函數環境:用戶在函數中定義的變量被存儲在環境記錄中,包含了arguments 對象,外部環境的引用可以是全局環境,也可以是包含內部函數的外部函數環境
偽代碼如下:
GlobalExectionContext = { // 全局執行上下文
LexicalEnvironment: { // 詞法環境
EnvironmentRecord: { // 環境記錄
Type: "Object", // 全局環境
// 標識符綁定在這里
outer: <null> // 對外部環境的引用
}
}
FunctionExectionContext = { // 函數執行上下文
LexicalEnvironment: { // 詞法環境
EnvironmentRecord: { // 環境記錄
Type: "Declarative", // 函數環境
// 標識符綁定在這里 // 對外部環境的引用
outer: <Global or outer function environment reference>
}
}
變量環境
變量環境也是一個詞法環境,因此它具有上面定義的詞法環境的所有屬性
在 ES6 中,詞法環境和變量環境的區別在于前者用于存儲函數聲明和變量( let 和 const )綁定,而后者僅用于存儲變量( var )綁定
舉個例子
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
執行上下文如下:
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: { // 詞法環境
EnvironmentRecord: {
Type: "Object",
// 標識符綁定在這里
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: { // 變量環境
EnvironmentRecord: {
Type: "Object",
// 標識符綁定在這里
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 標識符綁定在這里
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 標識符綁定在這里
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
留意上面的代碼,let和const定義的變量a和b在創建階段沒有被賦值,但var聲明的變量從在創建階段被賦值為undefined
這是因為,創建階段,會在代碼中掃描變量和函數聲明,然后將函數聲明存儲在環境中
但變量會被初始化為undefined(var聲明的情況下)和保持uninitialized(未初始化狀態)(使用let和const聲明的情況下)
這就是變量提升的實際原因
執行階段
在這階段,執行變量賦值、代碼執行
如果 Javascript 引擎在源代碼中聲明的實際位置找不到變量的值,那么將為其分配 undefined 值
回收階段
執行上下文出棧等待虛擬機回收執行上下文
執行棧
執行棧,也叫調用棧,具有 LIFO(后進先出)結構,用于存儲在代碼執行期間創建的所有執行上下文
當Javascript引擎開始執行你第一行腳本代碼的時候,它就會創建一個全局執行上下文然后將它壓到執行棧中
每當引擎碰到一個函數的時候,它就會創建一個函數執行上下文,然后將這個執行上下文壓到執行棧中
引擎會執行位于執行棧棧頂的執行上下文(一般是函數執行上下文),當該函數執行結束后,對應的執行上下文就會被彈出,然后控制流程到達執行棧的下一個執行上下文
舉個例子:
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
轉化成圖的形式
簡單分析一下流程:
- 創建全局上下文請壓入執行棧
- first函數被調用,創建函數執行上下文并壓入棧
- 執行first函數過程遇到second函數,再創建一個函數執行上下文并壓入棧
- second函數執行完畢,對應的函數執行上下文被推出執行棧,執行下一個執行上下文first函數
- first函數執行完畢,對應的函數執行上下文也被推出棧中,然后執行全局上下文
- 所有代碼執行完畢,全局上下文也會被推出棧中,程序結束