前言
白的不能再白的小白,通過各種渠道學習到了this,和大家分享,有什麼錯誤的話還望指出,共同學習進步。覺得好的話還望點個小贊,為繼續堅持寫文章增加動力。
正文
我在想寫這篇文章的時候,我是無從下手的。因為我自己也同樣說不清在JavaScript中this到底是什麼?我們都知道在一些強型別語言中(像java)對this有一個很好的定義:this就是一個指標,指向當前物件。在JavaScript中如果用上面這種方法來表示this的話,是不完全的。對於我來說,this就像JavaScript中的魔術師,千變萬化中又有著那麼一絲絲小套路。
何為this?
this是JavaScript語法中很難捉摸透的一個東西。我們都知道JavaScript執行機制分為四個步驟:詞法分析
、語法分析
、預編譯
、解釋執行(執行時)
。我們在執行程式碼的時候,通常是在全域性中執行一個函式或者通過物件方法呼叫執行函式,在函式呼叫過程中得到this。也就是說this這個東西就生在執行時,即this是在執行時繫結的,而非編譯時。
JavaScript中函式呼叫的幾種方式
預設呼叫
在JavaScript中呼叫函式的最常見的一種方法就是預設呼叫。
function test(){
console.log(this);
}
test()
複製程式碼
正如上面程式碼這樣呼叫test函式,就是預設呼叫。
預設呼叫的情況下,函式的this指向全域性物件。
我們都知道在ES5出現了嚴格模式
這個概念。在嚴格模式下全域性物件的this
指向undefined
,非嚴格模式下全域性物件的this
指向Window
物件。
//嚴格模式
"use strict";
function test(){
console.log(this); //undefined
}
test()
複製程式碼
//非嚴格模式下
function test(){
console.log(this); //window
}
test();
複製程式碼
IIFE
立即執行函式在執行的時候會形成一個閉包,其中的this
永遠指向window
。
(function(){
console.log(this); //window
}())
複製程式碼
隱式繫結
隱式繫結也可以說成作為物件的方法來呼叫。看下面程式碼
function test(){
console.log(this.a);
}
var obj = {
a : 2,
test:test,
}
obj.test(); //2
複製程式碼
這裡可以看出obj中有一個屬性為test,它的屬性名為test函式。當test這個屬性被建立的時候,會在記憶體中開闢一小段空間,而記憶體中所存的值就是test函式所在記憶體的地址。此時就把test函式隱式的繫結到了obj上面,當使用obj呼叫test的時候,test中的this就指向了呼叫test的物件。
這裡就可以總結出一個套路:誰呼叫函式,函式的this就指向誰。
另外一種情況:
function foo(){
console.log(this.a);
}
var obj2 = {
a:1,
foo :foo,
}
var obj1 = {
a : 2,
obj2 : obj2
}
obj1.obj2.foo() //1
複製程式碼
當物件方法呼叫鏈很長的時候,只有方法引用鏈的上一層或者說是最後一層在呼叫位置中起作用。上面程式碼中只有obj2會起作用。
隱式繫結中還有一個現象是隱式丟失,也就是說當把一個物件的方法暴露在外部的時候(也就是說把物件的方法賦於一個全域性的變數),再通過預設呼叫的方式呼叫這個函式,此時的this就不會再指向物件,而是指向window。
var obj ={
foo : function(){
console.log(this);
}
}
obj.foo() //obj
var fun = obj.foo; //把物件的方法暴露在外部
fun(); //window
複製程式碼
顯示繫結
顯示繫結也可以稱之為硬繫結。所謂硬繫結就是使用一種手段強制的改變函式的this執行,這種強制的手段通過call、apply、bind等函式的API來實現。這裡由於說的是this機制,就先不提call、apply、bind等原理。
Function.prototype.call
call的作用就是改變函式呼叫中的this指向,call方法第一個引數就是繫結的this到哪個物件上,後面的引數是傳遞函式的實參。
function test(name,age){
console.log(name);
console.log(age);
console.log(this);
}
var obj ={}
test.call(obj,'xiaoming',18); // xiaohong 18 obj
複製程式碼
Function.prototype.apply
apply的作用和call的作用相同,同樣是為了改變this指向,唯一不同的區別是call在傳遞函式實參的時候是通過分來傳參,apply函式傳參是通過傳遞一個陣列。
function test(name,age){
console.log(name);
console.log(age);
console.log(this);
}
var obj ={}
test.call(obj,['xiaoming',18]); // xiaohong 18 obj
複製程式碼
Function.prototype.bind
bind是ES5出現的一個API,同樣也是可以改變this指向。bind和上面兩個API有很大的不同之處,call和apply是立即執行所呼叫的函式。bind是返回一個新函式,並且支援柯里化(分佈傳參)。
function test(name,age){
console.log(name);
console.log(age);
console.log(this);
}
var obj ={}
var fun = test.bind(obj,'xiaoming'); //呼叫bind方法,第一個引數傳入所要繫結的物件,第二個引數傳入name,並且返回一個新函式。
fun(18); //傳遞剩餘的引數。
//輸出 xiaohong 18 obj
複製程式碼
建構函式呼叫
JavaScript中的物件導向程式設計是很模糊的,起初這個語言並不是為物件導向程式設計而設計的,JavaScript語言的特性更類似於函數語言程式設計。我們為了使用JavaScript模擬物件導向程式設計,從而有了建構函式的概念:使用new呼叫的函式稱為構造呼叫,而這個函式被稱為建構函式。
發生建構函式呼叫會自動執行以下操作:
- 建立一個全新的物件。
- 這個新物件會被執行[[Prototype]連線
- 這個新物件會繫結到函式呼叫的this
- 如果函式沒有返回其他物件(物件、陣列、函式),那麼new表示式中的函式呼叫會自動返回這個新物件。
function Test(name,age){
this.name = name;
this.age = age;
}
var test = new Test('xiaoming',18);
console.log(test) //Test{}
複製程式碼
函式呼叫的優先順序
那麼多函式呼叫方式,如果同時出現的話究竟哪個優先順序高,哪個優先順序低呢?
預設呼叫和隱式繫結
先來看一下最基礎的兩種
function test(){
console.log(this);
}
//預設呼叫
test(); //window
var obj = {
test : test,
}
//隱式繫結
obj.test(); //obj
複製程式碼
可以明顯的看出,同一個函式預設呼叫的情況下this為window,隱式繫結的情況下this指向obj,很明顯隱式繫結改變了預設呼叫的this,所以隱式繫結的優先順序比預設呼叫的優先順序高。
隱式繫結和顯示繫結
再來看一下隱式繫結
var temp = {
a : 1,
}
var obj = {
a : 2
foo : function(){
console.log(this.a);
}
}
//隱式繫結
obj.foo() //2
//顯示繫結
obj.foo.call(temp); //1
複製程式碼
我們可以發現顯示繫結改變了隱式繫結的this,即顯示繫結的優先順序大於隱式繫結
顯示繫結和構造呼叫
如果讓顯示繫結和構造呼叫相比的話,就需要bind了,因為call和apply都是直接呼叫,沒辦法和構造呼叫相比,而bing則是返回一個新函式,可以使用構造呼叫。
var obj = {
a : 1,
}
function Test(){
console.log(this.a);
}
//顯示繫結
var fun = Test.bind(obj); //此時函式已經繫結了obj物件。
fun(); //執行fun,輸出1
//構造呼叫
new fun(); //unfeinde
複製程式碼
我們發現,fun此時已經繫結了obj,輸出1,對的沒有關係,使用new fun怎麼就輸出undefined了呢,其實這裡輸出的是fun建構函式返回的物件。新建立的物件上沒有a屬性,我們來修改一行程式碼再測試下。
function Test(){
console.log(this)
}
複製程式碼
重新呼叫
得出結論建構函式呼叫比顯示繫結的優先順序高,這裡涉及到bind原理的東西,目前沒有寫這類文章,喜歡探索的可以自行google下。所以最後得出的結論是:建構函式呼叫>顯示繫結>隱式繫結>預設呼叫。
判斷this規則
- 判斷是否是立即執行函式,如果是則this為window,如果不是移步第2步驟。
- 判斷this是否是通過建構函式呼叫,如果是this就繫結新建立的物件,如果不是移步第3步驟。
- 判斷是否是通過call、apply、bind等繫結,如果是this繫結指定物件,如果不是移步第4步驟。
- 判斷函式是夠是在某個上下文物件中呼叫,如果是this就繫結在上下文物件上,如果不是請移步第5步驟。
- 如果上面都不是的話,那麼函式使用預設呼叫,如果是嚴格模式,則this為undefined,如果是非嚴格模式,則this為window。
箭頭函式的this
起這個小標題說實話不太想起,因為箭頭函式中是沒有this的,怎麼說呢,請移步軟大大的箭頭函式。在其他函式中this是可變的,在箭頭函式中this是固定的,同樣也是因為箭頭函式中沒有this。在你不知道的JavaScript(上)
中有那麼一句話:箭頭函式在涉及this繫結的行為和普通的行為完全不一致。它放棄了所有普通this繫結的規則,取而代之的是用當前的詞法作用域覆蓋了this本來的值。
這句話和上面其實是不矛盾的,所謂詞法通俗的將就是書寫程式碼的位置。箭頭函式書寫在什麼地方,它的this就是所處環境的執行上下文物件的this。
var a = 2;
var obj = {
a : 1,
foo : ()=> {
console.log(this.a);
}
}
//此時obj定義在全域性內,箭頭函式中的this為window,故:輸出2
obj.foo(); //2
複製程式碼
var a = 2;
var obj = {
a : 1,
fun : function(){
var foo = ()=>{
console.log(this.a);
}
foo();
}
}
//此時foo定義在fun函式中,執行fun函式,fun函式中的this為obj,所以箭頭函式中的this為obj。故:輸出1
obj.fun(); //1
複製程式碼
總結
- 如果是箭頭函式,則就判斷箭頭函式定義在什麼地方,this就是什麼。
- 如果是立即執行函式,則this就是windows
- 其他形式則按照判斷this的規則來。
文章總結自:你不知道的JavaScript(上),安利一波,值得一讀。
原文發自:探索JavaScript中this機制