執行上下文
js在執行代碼之前,需要經過一系列的“準備”,這被稱為執行上下文 ,其包含詞法環境 和this 。所有的 js 代碼在運行時都是在執行上下文中進行的,每創建一個執行上下文,就會將當前執行上下文放到一個棧頂,這就就是我們常説的執行棧 。
執行上下文的創建
何時創建執行上下文
JavaScript 中有三種情形會創建新的執行上下文:
- 全局執行上下文 ,進入去全局代碼的時候。任何不在函數內部的都是全局執行上下文,它首先會創建一個全局的window對象,並且設置this的值等於這個全局對象,一個程序中只有一個 全局執行上下文。
- 函數執行上下文 ,進入
function函數體代碼。當一個函數被調用時,就會為該函數創建一個新的執行上下文,函數的上下文可以有很多個 。 - Eval執行上下文 ,eval 函數參數指定的代碼,執行在eval函數中的代碼會有屬於它自己的執行上下文。
eval函數執行上下文用的不多,我們只介紹全局執行上下文和函數執行上下文:
- 在全局執行上下文中 ,this 是指向 window 對象的;
- 在函數執行上下文中 ,默認情況下調用一個函數,其執行上下文的 this 也是指向 window 的,(理論上誰最後調用它,它指向誰 )。
2. 函數的 this 指向
this是JavaScript的一個關鍵字,多數情況下this指向調用它的對象。
這句話有兩個意思,首先 this 指向的應該是一個對象 (函數執行上下文對象)。其次,這個對象指向的是調用它的對象,如果調用它的不是對象或對象不存在,則會指向全局對象(嚴格模式下為 undefined)。
其實,this 是在函數被調用時 確定的,它的指向取決於函數調用的地方,而不是它被聲明的地 方(除箭頭函數外)。
當函數被調用時,會創建一個執行上下文,它包含函數在哪裏被調用(調用棧)、函數的調用方式、傳入的參數等信息,this 就是這個記錄的一個屬性,它會在函數執行的過程中被用到。
this 在函數的指向綁定形式有四種:默認綁定、隱式綁定、顯式綁定、new綁定 。
(1)默認綁定(全局環境)
函數在瀏覽器全局環境中直接使用不帶任何修飾的函數引用進行調用,非嚴格模式下 this 指向 window;在 use strict 指明嚴格模式的情況下就是 undefined(嚴格模式不允許 this 指向全局對象)。
注意: 在瀏覽器環境下,全局對象是window;在Node.js環境下,全局對象是global。
function func1 () {
console.log(this)
}
function func2 () {
'use strict'
console.log(this)
}
func1() // window
func2() // undefined
需要注意一種情況,來看代碼:
var num = 1
var foo = {
num: 10,
func: function() {
console.log(this) // window
console.log(this.num) // 1
}
}
var func1 = foo.fn
func1()
這裏會輸出 Window 和 1。
這裏 this 仍然指向 window。雖然 func 函數在 foo 對象中作為方法被引用,但是在賦值給 func1 之後,func1 的執行仍然是在 window 全局環境中。因此輸出 window 和 1。因為最後是window調用的func1,所以this指向window
它們相當於:
console.log(window)
console.log(window.num)
(2)隱式綁定(上下文對象)
如果函數在某個上下文對象中調用,那麼 this 綁定的是那個上下文對象。來看一個例子:
var a = 'hello'
var obj = {
a: 'world',
fn: function() {
console.log(this.a)
}
}
obj.fn()
在上述代碼中,最後會輸出"world"。這裏fn方法是作為對象的屬性調用的,此時fn方法執行時,this會指向obj對象。也就是説,此時this指向的是調用這個方法的對象。
那如果嵌套了多層對象,this的指向又是怎樣的呢?下面來看一個例子:
const obj1 = {
text: 1,
fn: function() {
return this.text
}
}
const obj2 = {
text: 2,
fn: function() {
return obj1.fn()
}
}
const obj3 = {
text: 3,
fn: function() {
var fn = obj1.fn
return fn()
}
}
console.log(obj1.fn())
console.log(obj2.fn())
console.log(obj3.fn())
對於這段代碼,最終的三個輸出結果:
- 第一個
console輸出1,這時 this 指向了調用 fn 方法的對象 obj1,所以會輸出obj1中的屬性text的值1; - 第二個
console輸出1,這裏調用了obj2.fn(),最終還是調用o1.fn(),因此仍然會輸出1。 - 第二個
console輸出undefined,在進行var fn = o1.fn賦值之後,是直接調用的,因此這裏的this指向window,答案是undefined。
根據上面例子就可以得出結論:如果嵌套了多個對象,那麼指向最後一個 調用這個方法的對象。
那如何讓 console.log(obj2.fn()) 輸出 2 呢?可以這樣:
const obj1 = {
text: 1,
fn: function() {
return this.text
}
}
const obj2 = {
text: 2,
fn: o1.fn
}
console.log(obj2.fn())
還是上面的結論:this 指向最後 調用它的對象,在 fn 執行時,掛到 obj2 對象上即可,就相當於提前進行了賦值操作。
(3)顯示綁定(apply、call、bind)
顯式綁定是指需要引用一個對象時進行強制綁定調用,顯式綁定可以使用apply、call、bind方法來綁定this值,使其指向我們指定的對象。
call、apply 和 bind三個方法都可以改變函數 this 指向,但是 call 和 apply 是直接進行函數調用;bind 不會執行函數,而是返回一個新的函數,這個新的函數已經自動綁定了新的 this 指向,需要我們手動調用。call 和 apply 的區別: call 方法接受的是參數列表,而 apply 方法接受的是一個參數數組。
這三個方法的使用形式如下:
const target = {}
fn.call(target, 'arg1', 'arg2')
fn.apply(target, ['arg1', 'arg2'])
fn.bind(target, 'arg1', 'arg2')()
需要注意,如果把 null 或 undefined 作為 this 的綁定對象傳入 call、apply、bind,這些值在調用時會被忽略,實際應用的是默認綁定規則:
var a = 'hello'
function fn() {
console.log(this.a)
}
fn.call(null)
這裏會輸出 hello,因為將 null 作為 this 傳給了 call 方法,這時 this 會使用默認的綁定規則,this指向了全局對象 window,所以輸出 window 中 a 的值 hello。
再來看一個例子:
const foo = {
name: 'hello',
logName: function() {
console.log(this.name)
}
}
const bar = {
name: 'world'
}
console.log(foo.logName.call(bar))
這裏將會輸出:world。
那如果對一個函數進行多次 bind,那麼上下文會是什麼呢?
let a = {}
let fn = function () {
console.log(this)
}
fn.bind().bind(a)()
這裏會輸出 a嗎?可以把上述代碼轉換成另一種形式:
// fn.bind().bind(a) 等於
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2()
可以發現,不管給函數 bind 幾次,fn 中的 this 永遠由第一次 bind 決定,所以結果永遠是 window。
let a = {
name: 'kobe'
}
function fn() {
console.log(this.name)
}
fn.bind(a)() // kobe
(4)new綁定(構造函數)
函數作為構造函數使用 new 調用時, this 綁定的是新創建的構造函數的實例:
function Person(name,age){
this.name = name;
this.age = age;
this.say = function(){
console.log(this.name + ":" + this.age);
}
}
var person = new Person("kobe",24);
console.log(person.name); // kobe
console.log(person.age); // 24
person.say(); // kobe:24
可以看到,在上面代碼中,this 就指向了構造函數 Person 的新對象person,所以使用 this 可以獲取到 person 對象的屬性和方法。
實際上,在使用 new 調用構造函數時,會執行以下操作:
- 創建一個新對象;
- 構造函數的 prototype 被賦值給這個新對象的 proto ;
- 將新對象賦給當前的 this;
- 執行構造函數;
如果函數沒有返回其他對象,那麼 new 表達式中的函數調用會自動返回這個新對象,如果返回的不是對象將被忽略。
總結:
- this 是在函數被調用時確定的,它的指向取決於函數調用的地方,而不是它被聲明的地方(除箭頭函數外)
- this 在函數的指向綁定形式有四種:默認綁定、隱式綁定、顯式綁定、new綁定 。
- this優先級:new綁定>顯示綁定>隱式綁定>默認綁定