JavaScript夯實基礎系列(三):this
??在JavaScript中,函數的每次調用都會擁有一個執行上下文,通過this關鍵字指向該上下文。函數中的代碼在函數定義時不會執行,只有在函數被調用時才執行。函數調用的方式有四種:作為函數調用、作為方法調用、作為構造函數調用以及間接調用,判定this指向的規則跟函數調用的方式有關。
一、作為函數的調用
??作為函數調用是指函數獨立執行,函數沒有人為指定的執行上下文。在有些情況下,作為函數調用的形式具有迷惑性,不僅僅是簡單的函數名後面加括號來執行。
1、明確的作為函數調用
??明確的作為函數調用是指形如func(para)形式的函數調用。作為函數調用的情況下this在嚴格模式下為undefined,在非嚴格模式下指向全局對象(在瀏覽器環境下為Window對象)如下代碼所示:
var a = 1;
function test1 () {
var a = 2
return this.a
}
test1() // 1
‘use strict‘
var a = 1;
function test1 () {
var a = 2
return this.a
}
test1() // Uncaught TypeError
??以函數調用形式的函數通常不使用this,但是可以根據this來判斷當前是否是嚴格模式。如下代碼所示,在嚴格模式下,this為undefined,strict為true;在非嚴格模式下,this為全局對象,strict為false。
var strict = (function () {
return !this
})()
2、對象作為橋梁找到方法
??通過對象調用的函數稱為方法,但是通過對象找到方法並不執行屬於作為函數調用的情況。如下代碼所示:
var a = 1;
function test() {
console.log( this.a );
}
var obj = {
a: 2,
test: test
};
var func = obj.test;
func(); // 1
??上述代碼中,obj.test是通過obj對象找到函數test,並未執行,找到函數之後將變量func指向該函數。obj對象在這個過程中只是起到一個找到test地址的橋梁作用,並不固定為函數test的執行上下文。因此var func = obj.test;執行的結果僅僅是變量func和變量test指向共同的函數體而已,因此func()仍然是作為函數調用
??當傳遞回調函數時,本質也是作為函數調用。如下代碼所示:
var a = 1
function func() {
console.log( this.a );
}
function test(fn) {
fn();
}
var obj = {
a: 2,
func: func
};
test( obj.func ); // 1
??函數參數是以值傳遞的形式進行的,obj.func作為參數傳遞進test函數時會被復制,復制的僅僅是指向函數func的地址,obj在這個過程中起到找到函數func的橋梁作用,因此test函數執行時,裏面的fn是作為函數調用的。
??接收回調的函數是自己寫的還是語言內建的沒有什麽區別,比如:
var a = 1;
function test() {
console.log( this.a );
}
var obj = {
a: 2,
test: test
};
setTimeout( obj.test, 1000 ); // 1
??setTimeout的第一個參數是通過obj對象找到的函數test,本質上obj依然是起到找到test函數的橋梁作用,因此test依然是作為函數調用的。
3、間接調用傳遞null或undefined作為執行上下文
??函數的間接調用是指通過call、apply或bind函數明確指定函數的執行上下文,當我們指定null或者undefined作為間接調用的上下文時,函數實際是作為函數調用的。但是有一點需要註意:call()和apply()在嚴格模式下傳入空值則上下文為空值,並不是因為遵循作為函數調用在嚴格模式下執行上下文為全局對象的規則,而是因為在嚴格模式下call()和apply()的第一個實參都會變成this的值,哪怕傳入的實參是原始值甚至是null或undefined。
var a = 1;
function test() {
console.log( this.a );
}
test.call( null ); // 1
??間接調用的目的是為了指定函數的執行上下文,那麽為什麽要傳null或undefined使其作為函數調用呢?這是因為我們會用到這些方法的其他性質:函數call中一般不傳入空值(null或undefined);函數apply傳入空值可以起到將數組散開作為函數參數的效果;函數bind可以用來進行函數柯裏化。在ES6中,新增了擴展運算符‘...’,將一個數組轉為用逗號分隔的參數序列,可以替代往apply函數傳空值的情況。但是ES6中沒有增加函數柯裏化的方法,因此往函數bind中傳空值的情況將繼續使用。
??在使用apply或bind傳入空值的情況,一般是不關心this值。但是如果函數中使用了this,在非嚴格模式下能夠訪問到全局變量,有時會違背代碼編寫的本意。因此,使用一個真正空的值傳入其中能夠避免這類情況,如下代碼所示:
var empty = Object.create( null );
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
foo.apply( empty, [1, 2] ); // a:1, b:2
二、作為方法調用
??當函數掛載到一個對象上,作為對象的屬性,則稱該函數為對象的方法。如果通過對象來調用函數時,該對象就是本次調用的上下文,被調用函數的this也就是該對象。如下代碼所示:
var obj = {
a: 1,
test: test
};
function test() {
console.log( this.a );
}
obj.test(); // 1
??在JavaScript中,對象可以擁有對象屬性,對象屬性有又可以擁有對象或者方法。函數作為方法調用時,this指向直接調用該方法的對象,其他對象僅僅是為了找到this指向的這個對象而已。如下代碼所示:
function test() {
console.log( this.a );
}
var obj2 = {
a: 2,
test: test
};
var obj1 = {
a: 1,
obj2: obj2
};
obj1.obj2.test(); // 2
??當方法的返回值時一個對象時,這個對象還可以再調用它的方法。當方法不需要返回值時,最好直接返回this,如果一個對象中的所有方法都返回this,就可以采用鏈式調用對象中的方法。如下代碼所示:
function add () {
this.a++;
return this;
}
function minus () {
this.a--;
return this;
}
function print() {
console.log( this.a );
return this;
}
var obj = {
a: 1,
print: print,
minus: minus,
add: add
};
obj.add().minus().add().print(); // 2
三、作為構造函數調用
??在JavaScript中,構造函數沒有任何特殊的地方,任何函數只要是被new關鍵字調用該函數就是構造函數,任何不被new關鍵字調用的都不是構造函數。
??當使用new關鍵字來調用函數時,會經歷以下四步:
1、創建一個新的空對象。
2、這個空對象繼承構造函數的prototype屬性。
3、構造函數將新創建的對象作為執行上下文來進行初始化。
4、如果構造函數有返回值並且是對象,則返回構造函數的返回值,否則返回新創建的對象。
??約定俗成的是:在編寫構造函數時函數名首字母大寫,且構造函數不寫返回值。因此一般來說,new關鍵字調用構造函數創建的新對象作為構造函數的this。如下代碼所示:
function foo() {
this.a = 1;
}
var bar = new foo();
console.log( bar.a ); // 1
四、間接調用
??在JavaScript中,對象中的方法屬性僅僅存儲的是一個函數的地址,函數與對象的耦合度沒有想象中的高。通過對象來調用函數,函數的執行上下文(this指向)就是該對象。如果通過對象來找到函數的地址,就能指定函數的執行上下文,可以使用call()、apply()和bind()方法來實現。換而言之,任何函數可以作為任何對象的方法來調用,哪怕函數並不是那個對象的方法。
1、call()和apply()
??每個函數都call()和apply()方法,函數調用這兩個方法是可以明確指定執行上下文。從綁定上下文的角度來說這兩個方法是一樣的,第一個參數傳遞的都是指定的執行上下文。所不同的在於call()方法剩余的參數將會作為函數的實參來使用,可以有多個;apply()則最多只接收兩個參數,第一個是執行上下文,第二個是一個數組,數組中的每個元素都將作為函數的實參。如下代碼所示:
var a = 1
function test(b,c) {
console.log(`a:${this.a},b:${b},c:${c}`)
}
var obj = {
a:2
}
test.call(obj,3,4) // a:2,b:3,c:4
var d = 11
function test2(b,c) {
console.log(`b:${b},c:${c},d:${this.d}`)
}
var obj2 = {
d:12
}
test2.apply(obj2,[13,14]) // b:13,c:14,d:12
??在非嚴格模式下,call()、apply()的第一個參數傳入null或者undefined時,函數的執行上下文被替代為全局對象,如果傳入的是基礎類型,則為替代為相應的包裝對象。在嚴格模式下,遵循的規則是傳入的值即為執行上下文,不替換,不自動裝箱。如下代碼所示:
var a = 1
function test1 () {
console.log(this.a)
}
test1.call(null) // 1
test1.call(undefined) // 1
test1.apply(null) // 1
test1.apply(undefined) // 1
‘use strict‘
function test1 () {
console.log(this)
}
test1.call(null) // null
test1.call(undefined) // undefined
test1.call(1) // 1
test1.apply(null) // null
test1.apply(undefined) // undefined
test1.apply(1) // 1
??apply()有一個較為常見的用法:將數組轉化成函數的參數序列。ES6中增加了擴展運算符“...”來實現該功能。如下代碼所示:
var arr = [1,19,4,54,69,9]
var a = Math.max.apply(null,arr)
console.log(a) // 69
var b = Math.max(...arr)
console.log(b) // 69
2、bind()
??bind()函數可以接收多個參數,返回一個功能相同、執行上下文確定、參數經過初始化的函數。其中第一個參數為要綁定的執行上下文,剩余參數為返回函數的預定義值。bind()函數的作用有兩點:1、為函數綁定執行上下文;2、進行函數柯裏化。如下代碼所示:
var a = 1
function func(b,c) {
console.log(`a:${this.a},b:${b},c:${c}`)
}
var obj = {
a: 2
}
var test = func.bind(obj,3)
test(4) // a:2,b:3,c:4
??bind()方法是ES5加入的,但是我們可以很輕易的在ES3中通過apply()模擬出來,下面代碼是MDN上的bind()的polyfill。
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== "function") {
// 可能的與 ECMAScript 5 內部的 IsCallable 函數最接近的東西,
throw new TypeError( "Function.prototype.bind - what " +
"is trying to be bound is not callable"
);
}
var aArgs = Array.prototype.slice.call( arguments, 1 ),
fToBind = this,
fNOP = function(){},
fBound = function(){
return fToBind.apply(
(this instanceof fNOP &&oThis ? this : oThis),
aArgs.concat( Array.prototype.slice.call( arguments ) )
);
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
五、規則的優先級
??函數的調用有時不只一種,那麽不同調用方式的規則的優先級就最終決定了this的指向。那就讓我們來比較不同調用方式的規則優先級。如下代碼所示,當函數作為方法調用的時候,this指向調用方法的對象,當作為函數調用時,this指向在非嚴格模式下指向全局對象,在嚴格模式下指向undefined。因此,方法調用的優先級高於函數調用。
var a = 1
var obj = {
a:2,
test:test
}
function test () {
console.log(this.a)
}
var b = obj.test
obj.test() // 2
b() // 1
??如下代碼所示是函數作為方法調用分別和間接調用、構造函數調用作對比。由代碼可知:函數作為方法調用優先級分別小於間接調用和構造函數調用。
function test(para) {
this.a = para
}
var obj1 = {
test: test
}
var obj2 = {}
obj1.test( 2 )
console.log( obj1.a ) // 2
obj1.test.call( obj2, 3 )
console.log( obj2.a ) // 3
var bar = new obj1.test( 4 )
console.log( obj1.a ) // 2
console.log( bar.a ) // 4
??new關鍵字後面是一個函數,而call()和apply()並不是返回一個函數,而是依照傳入參數來執行函數,因此形如new foo.call(obj)的代碼是不被允許的。ES5中的bind()返回的是一個函數,可以與new關鍵字同時使用。如下代碼所示,bind()返回的函數用作構造函數,將忽略傳入bind()的this值,原始函數會以構造函數的形式調用,傳入的參數也會原封不動的傳入原始函數。
function test(something) {
this.a = something;
}
var obj = {};
var bar = test.bind( obj );
bar( 2 );
console.log( obj.a ); // 2
var baz = new bar( 3 );
console.log( obj.a ); // 2
console.log( baz.a ); // 3
??總之,構造函數的優先級大於間接調用,間接調用的優先級大於方法調用,方法調用的優先級大於函數調用。
六、詞法this
??this關鍵字沒有作用域限制,函數的this指向調用該函數的對象,在嵌套函數匯中,如果想訪問外層函數的this值,可以將外層函數的this賦值給一個變量,用詞法作用域來代替傳統的this機制。如下代碼所示:
function foo() {
var self = this // 詞法上捕獲`this`
setTimeout( function(){
console.log( self.a )
}, 1000 )
}
var obj = {
a: 2
};
foo.call( obj ) // 2
??ES6新增了箭頭函數,箭頭函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。如下代碼所示,箭頭函數能夠將this固化,箭頭函數內部沒有綁定this的機制,其內部的this就是外層代碼塊的this。傳統的this機制讓很多人與詞法作用域混淆,因此有了將this賦值給變量的行為,ES6只是將這種行為加以標準化而已。
var a = 21
function test() {
setTimeout(() => {
console.log(‘a:‘, this.a)
}, 1000)
}
test.call({ a: 42 }) // 2
七、總結
??JavaScript中的this機制跟詞法作用域沒有關系,根據函數調用的方式不同,確定this指向的規則也不相同。在確定this指向時可以遵循以下步驟:
1、函數是否為構造函數調用,即函數跟在new關鍵字後面,如果是,this就是新構建的對象。
2、函數是否為間接調用,即通過call()、apply()或者bind()調用,如果是,this就是明確指定的對象。
3、函數是否為作為方法調用,即通過對象來調用函數,如果是,this就是該對象。
4、否則,即為作為函數的調用,在非嚴格模式下,this指向全局對象,在嚴格模式下,this為undefined。
??可以將外層函數的this賦值給一個變量,使得內層函數以詞法作用域的規則來訪問該this。ES6新增的箭頭函數便是使用詞法作用域來決定this綁定的。
如需轉載,煩請註明出處:https://www.cnblogs.com/lidengfeng/p/9198569.html
JavaScript夯實基礎系列(三):this