1.函數調用棧和調用位置
在函數執行的時候,會有一個活動記錄(也叫執行上下文)來記錄函數的調用順序,這個就是函數調用棧。棧(Stack)是一種先進后出,后出先進的數據結構。
function a(){
console.log('a')
b() //b的調用位置,此時調用棧 a -> b
}
function b(){
console.log('b')
c() //c的調用位置,此時調用棧 a -> b -> c
}
function c(){
console.log('c')
}
a() //a的調用位置
以上代碼中,首先執行a函數,此時a函數被push進調用棧的棧頂;在a函數的執行過程中,調用了b函數,b函數push進棧頂;b函數的執行過程中,調用了c函數,c函數push進棧頂。在瀏覽器環境下,此時便形成了window -> a -> b -> c這樣的調用順序。在chrome中的開發者工具可以更清晰的看清楚這點。
函數的調用位置
要想理解this綁定的過程,首先要弄清楚什么是調用位置。在之前的代碼中,如果是在瀏覽器環境中,window調用了a函數,a的調用位置便是window;b函數調用了c函數,b的調用位置便是a函數;同理c函數的調用位置便是b函數。因此可以理解為棧頂正在的函數的上一個函數便是當前函數的調用位置。
2.this的四種綁定方式
this綁定的方式分為默認綁定,隱式綁定,顯式綁定和new綁定。注意以下代碼均在非嚴格模式下運行。
2.1默認綁定
function foo(){
console.log(this.a)
console.log(this===window)
}
var a = 10
foo() //10 true
以上代碼中foo()是不帶任何修飾被直接調用,因此只能應用默認綁定,此時foo函數內的this對象指向了window(瀏覽器環境)。
function foo(){
function bar(){
console.log(this===window)
}
bar()
}
foo() //true
函數不帶任何修飾被調用,即使是在函數體內,也會應用默認綁定。上面代碼中,運行在foo函數內的bar()綁定的this指向window。
2.2 隱式綁定
在函數的調用位置需要考慮函數是否被某個上下文(或對象)所包含。
function foo(){
console.log(this.a)
}
var obj = {
a: 1,
b: 2,
foo: foo,
bar: function(){
console.log(this.b)
}
}
obj.foo() //1
obj.bar() //2
以上代碼中,無論函數是先聲明再引入(foo),還是在內部定義(bar),這兩種情況都會引用對應的函數。obj.foo()和obj.bar()的調用方式讓調用位置通過obj對象來引用這兩個函數,因此這兩個函數的this會隱式綁定到obj上。
隱式丟失
常見于使用回調函數的時候
function useFoo(fn){
fn() //調用位置
}
var obj = {
a: 1,
foo: function(){
console.log(this.a)
}
}
var a = '我是全局a'
useFoo(obj.foo) //我是全局a
以上代碼中,雖然useFoo傳入參數的是obj.foo的方式,但其實參數傳遞是隱式賦值的方式,因此此時是將obj.foo賦值給參數fn,在調用位置上執行的fn實際上是foo(),因此應用了默認綁定,this指向全局環境。
這種情況內置的函數也不例外
function foo(){
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var a = '全局a'
setTimeout(obj.foo,1000) //全局a
內置的setTimeout()函數和下面的偽代碼類似
function setTimeout(fn, delay){
//等待delay毫秒
fn() //調用位置
}
2.3 顯式綁定
顯式綁定是通過函數的原型方法 apply()、call()和bind()對this進行綁定。
apply(thisArg,[argsArray])
apply方法的第一個參數是一個對象,它會把函數的this綁定到這個對象上;第二個參數是傳入的參數數組。
function foo(){
console.log(this.a)
}
var obj = {
a: 2
}
foo.apply(obj) //2
上面代碼中apply方法首先將foo的this綁定到obj對象上,然后執行foo函數,這樣就實現了this的顯式綁定。
call(thisArg[, arg1[, arg2[, ...]]])
:call方法和apply方法的功能是一樣的,只是call方法接受的是若干個參數列表,apply方法接受的是一個參數數組。
硬綁定
function foo(){
console.log(this.a)
}
var obj = {
a: 1,
foo: foo
}
var a='全局a'
var bar = obj.foo
bar.call(obj) //1
bar() //全局a
以上代碼bar函數先調用call方法將this綁定到了obj對象上,但是再調用bar()函數,由于隱式丟失的原因,仍然是將this綁定到全局。可以通過硬綁定的方式來解決。
bind(thisArg[, arg1[, arg2[, ...]]])
:硬綁定是bind()方法實現的,它的參數和call方法是一樣的,bind方法會返回this綁定了指定對象的原函數拷貝。
function foo(){
console.log(this.a)
}
var obj = {
a: 1
}
var a='全局a'
var bar = foo.bind(obj)
bar() //1
bar.call(window) //1,bind函數綁定的對象this對象不能再修改
上面代碼調用foo函數的bind方法將this綁定到obj上,然后返回函數給bar,此時bar的this已經綁定到obj上。注意,通過bind函數綁定的對象this對象不能再修改,因為bind函數的內部會再將this綁定到obj上。
2.4 new綁定
function Person(name){
this.name = name
}
var bar = new Person('bar')
console.log(bar.name) //bar
1.new Person(bar)首先會創建一個全新對象,這個對象繼承自Person.prototype,然后會將構造函數的this綁定到這個對象上;
2.如果構造函數返回了一個對象,那么它會取代第一步創建的對象,否則會返回new出來的對象(一般來說構造函數不返回任何值),因此bar是一個new創建的對象。
3.this綁定的優先級
優先級:new綁定 > 顯式綁定 > 隱式綁定 > 默認綁定
1.如果函數使用了new關鍵字,那么this綁定到new新創建的對象
var bar = new Person()
2.函數調用了call()、apply()和bind()方法進行顯式綁定,那么this綁定到指定的對象上。
var bar = foo.call(obj)
3.函數調用位置是否被某個對象所包含,如果是則應用隱式綁定,this會綁定到那個對象。
obj.foo()
4.以上都不是,那么應用默認綁定。
4.安全的this
有時候我們使用apply()、call()和bind()方法不是想綁定某個對象的this,而只是傳入某些參數。
function foo(a,b){
console.log(`a:${a} b:${b}`)
}
//使用apply展開數組
foo.apply(null,[1,2]) //1,2
//ES6中可以使用...運算符代替apply
foo(...[1,2]) //1,2
//使用bind進行柯里化
var bar = foo.bind(null,1)
bar(2) //1,2
以上代碼傳遞了null作為第一個參數,以此來忽略this綁定。這樣的做法實際上是會應用默認綁定的,函數的this可能會綁定到全局對象上,因此這可能會存在潛在的危險。
Object.create(null)
更安全的做法是使用Object.create(null)創建一個空對象,這樣做法不會產生Object.prototype這個委托,也不會應用默認綁定。
function foo(a,b){
console.log(this)
console.log(`a:${a} b:${b}`)
}
var _o = Object.create(null)
//使用apply展開數組
foo.apply(_o,[1,2]) //1,2
//使用bind進行柯里化
var bar = foo.bind(_o,1)
bar(2) //1,2
console.dir(_o)
5.箭頭函數的this
在ES6中的箭頭函數,其this不會應用前文的四條規則,而是取決于其外層作用域。
function foo(){
return () => {
//this取決于foo調用時的this
console.log(this.a)
}
}
var obj = {
a: 1
}
var a = '全局a'
var bar = foo.call(obj)
bar() //1
bar.call(window) //1,無法修改綁定的this
foo()內部的箭頭函數會捕獲調用foo()時的this。foo的this綁定到了obj,因此bar也會綁定到obj,而且箭頭函數的綁定無法修改。
箭頭函數最常使用于回調函數中
var handler = {
id: '123456',
init: function() {
document.addEventListener('click',
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log('Handling ' + type + ' for ' + this.id);
}
}
上面init和doSomething方法使用了箭頭函數,是this綁定到了handler對象,如果使用普通函數,this.doSomething中的this則會綁定到document對象。
6.小結
要判斷一個函數綁定的this,需要找到這個函數的直接調用位置。找到之后,就按照四條綁定規則來判斷this綁定的對象。需要特別注意的是,一些調用可能會無意中應用默認綁定規則。
如果是使用箭頭函數,那么箭頭函數會繼承外層函數調用時綁定的this。