說(shuō)說(shuō)this

??我們都知道Javascript中的「this」真的是個(gè)頭痛的東西,今兒我們就來(lái)好好總結(jié)下這個(gè)「this」。

??我記得之前這塊內(nèi)容,我直接先背了個(gè)「口訣」。

function fn() {
  console.log(this);
}

// 1. fn(); // 指向window
// 2. fn(); // undefined(嚴(yán)格模式下)
// 3. a.b.c.fn() // 指向a.b.c
// 4. new fn() // 指向new的實(shí)例對(duì)象
// 5. () => { fn(); } // 箭頭函數(shù)調(diào)用fn,this指向「外層代碼庫(kù)的this」

之前了解的不多,今天來(lái)詳細(xì)解析一波。

首先我們來(lái)說(shuō)說(shuō)this的綁定規(guī)則。
(1)默認(rèn)綁定
(2)隱式綁定
(3)顯式綁定
(4)new綁定

以下全部在「瀏覽器環(huán)境」中。

(1)默認(rèn)綁定
??在不能應(yīng)用「其他綁定規(guī)則」的時(shí)候,使用「默認(rèn)綁定」,通常是作為「獨(dú)立函數(shù)」進(jìn)行調(diào)用。

function foo() {
    console.log(this.name); // 'Jason'
}

var name = 'Jason';

foo();

??在調(diào)用「foo()」的時(shí)候,應(yīng)用了「默認(rèn)綁定」,this指向了全局對(duì)象window(非嚴(yán)格模式下),嚴(yán)格模式下,指向「undefined」。

(2)隱式綁定
??函數(shù)的調(diào)用是在「某個(gè)對(duì)象」上觸發(fā)的,即調(diào)用位置上存在「執(zhí)行上下文」。典型的形式如「xxx.fn()」。

var name = 'Jack';

function fn() {
    console.log(this.name);
}

var obj = {

    name: 'Jason',

    foo: fn

};

obj.foo(); // 'Jason';

??函數(shù)fn的聲明在對(duì)象obj的外部,看起來(lái)是不屬于obj對(duì)象的,但是在調(diào)用foo的時(shí)候,隱式綁定會(huì)把函數(shù)調(diào)用中的「this」(foo函數(shù)中的this)綁定到對(duì)應(yīng)的「執(zhí)行上下文」(此例中的obj)。注意:對(duì)象屬性鏈只有最后一層會(huì)影響調(diào)用位置。

function fn() {
    console.log(this.name);
}

let b = {
    name: 'Jack',
    foo: fn
}

let a = {
    name: 'Jason',
    friend: b
}

a.friend.foo();

??上面代碼中,foo函數(shù)的執(zhí)行環(huán)境不是a,而是「a.friend」,即是「b」。

??但是隱式綁定存在一個(gè)問(wèn)題:綁定丟失。我們來(lái)看下邊一段代碼:

function fn() {
    console.log(this.name);
}

var name = 'Jack';

let obj = {
    name: 'Jason',
    sayName: fn,
}

let say = obj.sayName;

say(); // 'Jack'

??如果我們單看「obj.sayName」,執(zhí)行上下文是對(duì)象obj,但是我們把「obj.sayName」賦值給了變量say后,調(diào)用say()后,函數(shù)fn的執(zhí)行上下文就變?yōu)榱巳肿兞浚╳indow)中。
??針對(duì)這類(lèi)問(wèn)題,我們只要記住,形式為「xxx.fn()」才是隱式綁定,如果格式為「fn()」,前面什么都沒(méi)有,那肯定不是隱式綁定,但是也不一定是「默認(rèn)綁定」,下文中會(huì)解釋。
??除了上述的「綁定丟失」,還有一種綁定丟失的情況,就是發(fā)生在「回調(diào)函數(shù)」中,我們?cè)倏匆粋€(gè)例子。

function fn() {
    console.log(this.name);
}

var person1 = {
    name: 'Jason',
    sayName: function() {
        setTimeout(function() {
            console.log('Hello!', this.name);
        })
    }
};

var person2 = {
    name: 'Jack',
    sayName: fn
};


var name = 'Tom';

person1.sayName(); // 'Hello! Tom'

setTimeout(person2.sayName, 100); // 'Tom'

setTimeout(function() {
    person2.sayName();  // 'Jack'
}, 200);

我們依次說(shuō)說(shuō)每次輸出的原因。

  • (1)setTimeout的回調(diào)函數(shù)中,this是「默認(rèn)綁定」,在非嚴(yán)格模式下,指向全局變量「window」,所以輸出「'Hello! Tom'」
  • (2)第二條可能有些迷惑,不是說(shuō)格式為「xxx.fn()」就是隱式綁定嗎?然后執(zhí)行上下文就是對(duì)象xxx?其實(shí)是這樣的,對(duì)于setTimeout(fn, delay),第一個(gè)參數(shù)「fn」是「person2.sayName」,也就是說(shuō)我們把「person2.sayName」賦值給了fn,然后執(zhí)行了fn(),這樣就跟person2無(wú)關(guān)系。
  • (3)第三條雖然也是在setTimeout函數(shù)中,但是我們可以看到執(zhí)行的是「 person2.sayName()」,所以是一個(gè)隱式綁定,因此函數(shù)的執(zhí)行上下文是person2,跟當(dāng)前的作用域無(wú)關(guān)系。

(3)顯式綁定
??顯式綁定主要是通過(guò)call,apply和bind來(lái)顯式的綁定this,call,apply和bind的第一個(gè)參數(shù)就是對(duì)應(yīng)的this對(duì)象,call和apply的作用一樣,只是call從第二個(gè)參數(shù)開(kāi)始依次傳入?yún)?shù),而apply是直接把所有參數(shù)集成為一個(gè)數(shù)組放到第二個(gè)參數(shù),bind會(huì)返回一個(gè)函數(shù),在正式執(zhí)行函數(shù)的時(shí)候,優(yōu)先調(diào)bind第二個(gè)后的參數(shù)。來(lái)看看代碼:

function fn() {
    console.log(this.name);
}

var person = {
    name: 'Jason',
    sayName: fn
};


var name = 'Jack';

var sayName = person.sayName;

sayName.call(person); // 'Jason'

??上述代碼中,如果先不看最后一行,看倒數(shù)第二行「var sayName = person.sayName」,通過(guò)上述的講述,可以認(rèn)定到這一行,如果直接調(diào)用,所處的執(zhí)行上下文是全局變量window,但是最后一行的call函數(shù),指定了this的對(duì)象為person,則輸出‘Jason’。

??那么顯式綁定是不是會(huì)出現(xiàn)綁定丟失呢?看下面的代碼。

function fn() {
    console.log(this.name);
}

var person = {
    name: 'Jason',
    sayName: fn
}

var name = 'Jack';

var Hi = function(fn) {
    fn(); // 'Jack'
};

Hi.call(person, person.sayName);

??乍一看最后一行的顯式綁定,確實(shí)Hi函數(shù)的this綁定到了對(duì)象person上,Hi函數(shù)會(huì)接受一個(gè)參數(shù)fn,然后執(zhí)行fn();此刻這個(gè)參數(shù)fn是call的第二個(gè)參數(shù)「person.sayName」,但是在執(zhí)行fn的時(shí)候,相當(dāng)于直接調(diào)用了sayName函數(shù)(person.sayName賦值給了參數(shù)fn,隱式綁定也丟了,),對(duì)應(yīng)的是默認(rèn)綁定。

??那我們能不能繼續(xù)還是想要綁定到person上?

function fn() {
    console.log(this.name);
}

var person = {
    name: 'Jason',
    sayName: fn
}

var name = 'Jack';

var Hi = function(fn) {
    fn.call(this); // 'Jason'
};

Hi.call(person, person.sayName);

??其實(shí)只用在Hi函數(shù)中對(duì)fn使用call調(diào)用,因?yàn)閯倓偽覀冋f(shuō)了最后一句話,HI的this對(duì)象綁定到了person對(duì)象上,那么我們?cè)谡{(diào)用fn函數(shù)的時(shí)候再次顯示綁定一次this,此時(shí)「fn.call(this)」中的this就是Hi函數(shù)的this對(duì)象(person)。

(4)new綁定
??關(guān)于new會(huì)發(fā)生什么,可以具體看我之前的一篇文章(http://www.itdecent.cn/p/6ea91eb41283
)。

??簡(jiǎn)單來(lái)說(shuō):
(1)創(chuàng)建一個(gè)空對(duì)象,作為將要返回的對(duì)象實(shí)例。
(2)將這個(gè)空對(duì)象的原型,指向構(gòu)造函數(shù)的「prototype」屬性。
(3)將這個(gè)空對(duì)象賦值給構(gòu)造函數(shù)內(nèi)的「this」關(guān)鍵字。
(4)開(kāi)始執(zhí)行構(gòu)造函數(shù)內(nèi)的代碼。

function Person(name) {
    this.name = name;
}

var x = new Person('Jason');

console.log(x.name) // 'Jason'

(5)綁定例外
??上述(1)~(4)已經(jīng)基本參數(shù)了this綁定的規(guī)則,但是我們還是要補(bǔ)充點(diǎn)可能存在的問(wèn)題。比如「綁定例外」。

??如果我們?cè)谑褂谩革@示綁定」的時(shí)候,第一個(gè)參數(shù)傳了「null」或者「undefined」,這些值是會(huì)被忽略的,實(shí)際上應(yīng)用的是「默認(rèn)綁定」。

(6)綁定優(yōu)先級(jí)
new綁定 > 顯式綁定 > 隱式綁定 > 默認(rèn)綁定

(7)箭頭函數(shù)
??箭頭函數(shù)是ES6中的語(yǔ)法,帶來(lái)了很多的便利,但是我們也有幾個(gè)重要的注意點(diǎn):
(1)函數(shù)體內(nèi)的this對(duì)象,就是定義時(shí)所在的對(duì)象,而不是使用時(shí)所在的對(duì)象。

(2)不可以當(dāng)作構(gòu)造函數(shù),也就是說(shuō),不可以使用new命令,否則會(huì)拋出一個(gè)錯(cuò)誤。

(3)不可以使用arguments對(duì)象,該對(duì)象在函數(shù)體內(nèi)不存在。如果要用,可以用 rest 參數(shù)代替。

(4)不可以使用yield命令,因此箭頭函數(shù)不能用作 Generator 函數(shù)。
??其中第一點(diǎn)就跟本文的主題this密切相關(guān),我們來(lái)看兩個(gè)例子。

var name = 'Jason';

var obj = {
    name: 'Jack',
    sayName: function() {
        console.log(this.name); // 'Jack'
    }
};

obj.sayName();
var name = 'Jason';

var obj = {
    name: 'Jack',
    sayName: () => {
        console.log(this.name); // 'Jason'
    }
};

obj.sayName();

??上述兩個(gè)例子非常相似,唯一不同點(diǎn)在于obj的sayName函數(shù)一個(gè)使用了普通函數(shù)形式(第一個(gè)例子),另一個(gè)使用了箭頭函數(shù)(第二個(gè)例子),然后輸出結(jié)果就不同了。

??總的來(lái)說(shuō):普通函數(shù)的this是函數(shù)「運(yùn)行時(shí)」綁定的,而箭頭函數(shù)的this是函數(shù)「定義時(shí)」綁定的。

??這里我們?cè)趺蠢斫?strong>「定義時(shí)」?我們可以說(shuō)箭頭函數(shù)的this和其外層代碼庫(kù)的this一樣,或者說(shuō)指向其父級(jí)執(zhí)行上下文中的this。

上述第二個(gè)例子中,箭頭函數(shù)本身跟sayName平級(jí)以key:value的形式,也就是說(shuō)箭頭函數(shù)本身存在于obj對(duì)象上,而obj對(duì)象所處的環(huán)境在全局變量window上,所以此例中的this.name其實(shí)是window.name。我們?cè)倏磧蓚€(gè)例子。

var name = 'Jason';

function Test() {
    this.name = 'Jack';

    let fn = function() {
        console.log(this.name); // 'Jason'
    };

    fn();

}

var x = new Test();
var name = 'Jason';

function Test() {
    this.name = 'Jack';

    let fn = () => {
        console.log(this.name); // 'Jack'
    };

    fn();

}

var x = new Test();

??第一個(gè)例子中,一個(gè)普通函數(shù)賦值給了變量fn,然后直接調(diào)用fn(),是默認(rèn)綁定,此時(shí)的this指向全局變量window,所以this.name就是window.name,輸出‘Jason’。
??第二個(gè)例子中,我們把箭頭函數(shù)賦值為了變量fn,箭頭函數(shù)中的this指向了父級(jí)執(zhí)行上下文中的this,父級(jí)執(zhí)行上下文中的this是通過(guò)new指向了其構(gòu)造函數(shù)的實(shí)例(本例中的x),然后Test函數(shù)中第一句的「this.name = 'Jack'」,使得x.name為Jack。

??我們?cè)賮?lái)一個(gè)例子練練手。

var obj = {
    hi: function(){
        console.log('1', this); // obj
        return ()=>{
            console.log('2',this); // obj
        }
    },
    sayHi: function(){
        return function() {
            console.log('3', this); // window
            return ()=>{
                console.log('4', this); // window
            }
        }
    },
    say: ()=>{
        console.log('5', this); // window
    }
}

let hi = obj.hi();

hi();

let sayHi = obj.sayHi();

let fun1 = sayHi();

fun1();

obj.say();

??我們來(lái)依次解釋一下:
(1)第一條(1處)是由于「obj.hi()」調(diào)用函數(shù),此時(shí)是隱式綁定,固然1處的this指向了obj。
(2)第2處,由于調(diào)用「hi()」,obj.hi()返回的是一個(gè)箭頭函數(shù),這個(gè)箭頭函數(shù)中的this與外層代碼庫(kù)的this一樣,外層代碼庫(kù)就是obj對(duì)象中的fn屬性(這是一個(gè)函數(shù)),于是乎,跟1處的this是一樣的,都指向obj。
(3)由于「obj.sayHi()」賦值給了變量sayHi,「sayHi」執(zhí)行的時(shí)候,相當(dāng)于原本是隱式綁定然后變?yōu)槟J(rèn)綁定,固然3處輸出的是window。
(4)由于sayHi()賦值給了fun1,在「fun1()」執(zhí)行的時(shí)候,執(zhí)行的是一個(gè)箭頭函數(shù),然后我們就找這個(gè)箭頭函數(shù)的外層代碼庫(kù)的this,就與3處的this相同,即輸出window。
(5)由于「obj.say()」的執(zhí)行,乍一看是一個(gè)隱式綁定,但是看到函數(shù)是箭頭函數(shù),obj的外層代碼庫(kù)所在的環(huán)境就是全局變量window,輸出window。

那么箭頭函數(shù)一定是靜態(tài)的嗎?

var obj = {
    hi: function(){
        console.log('1', this);
        return ()=>{
            console.log('2', this);
        }
    },
    sayHi: function(){
        return function() {
            console.log('3', this);
            return ()=>{
                console.log('4', this);
            }
        }
    },
    say: ()=>{
        console.log('5', this);
    }
}

let sayHi = obj.sayHi();

let fun1 = sayHi(); // window
fun1(); // window

let fun2 = sayHi.bind(obj)(); // obj
fun2(); // obj

??還是用上面的例子,我們看看fun1和fun2,第一次執(zhí)行「sayHi()」賦值給fun1的時(shí)候,是由隱式綁定轉(zhuǎn)換為了默認(rèn)綁定,this為window,執(zhí)行「fun1」的時(shí)候,是執(zhí)行箭頭函數(shù),所以箭頭函數(shù)里的this也是window。
??但是對(duì)于fun2, 「sayHi.bind(obj)()」中使用bind顯示綁定了sayHi的this對(duì)象為obj,本來(lái)沒(méi)有bind綁定的時(shí)候,是由隱式綁定轉(zhuǎn)變?yōu)槟J(rèn)綁定,然后此處我們又強(qiáng)行使用bind綁定回來(lái)了。所以「sayHi.bind(obj)()」執(zhí)行的時(shí)候,3處的this為obj,然后「fun2()」執(zhí)行的時(shí)候,4處的this和3處的this一樣都是obj。

我們來(lái)一個(gè)終極題目來(lái)綜合一下。

var number = 5;
var obj = {
    number: 3,
    fn: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}

var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);

??我們來(lái)分析下這段代碼,在obj對(duì)象的fn屬性定義的時(shí)候,就是一個(gè)「立即執(zhí)行函數(shù)」,并且?guī)в小搁]包」,我們看看這個(gè)「立即執(zhí)行函數(shù)」中的this指向了誰(shuí)?沒(méi)有new綁定,沒(méi)有顯式綁定,沒(méi)有隱式綁定,自然就是默認(rèn)綁定了,this指向了全局變量window。所以「立即執(zhí)行函數(shù)」中的代碼可以這樣理解。

var number; // number是undefined

window.number *= 2; // 此時(shí)全局變量中的number變?yōu)?10

number = number * 2; // 由于number是undefined, Number(undefined)為NaN,此處number變?yōu)镹aN。
number = 3; // 然后又使變量number變?yōu)?

執(zhí)行完fn的立即執(zhí)行函數(shù)后。obj的fn屬性是下列這樣的:

fn: function() {
  var num = this.number;
  this.number *= 2;
  console.log(num);
  number *= 3;
  console.log(number);
}

然后我們執(zhí)行「myFun.call(null)」,這種顯式綁定我們?cè)谏鲜鑫恼轮姓f(shuō)過(guò),如果第一個(gè)參數(shù)為null,就是轉(zhuǎn)為默認(rèn)綁定。本例中就是執(zhí)行obj的fn函數(shù),然后我們?cè)賮?lái)每一行分析一波:我們首先想想obj的fn函數(shù)里面的this是誰(shuí)?由于是默認(rèn)綁定所以是window,于是乎。

var num = this.number; // 由于this指向了window,此時(shí)window.number為10,則num被賦值為10

this.number *= 2; // 使得全局變量的「number」,變?yōu)?0

console.log(num); // 輸出10

number *= 3; 這個(gè)number是obj.fn中的閉包函數(shù)中的「number」

console.log(number) ; // 同時(shí)輸出這個(gè)number
改變閉包中的number變量

接著我們執(zhí)行「obj.fn()」,還是優(yōu)先執(zhí)行了obj.fn的立即執(zhí)行函數(shù)。立即執(zhí)行函數(shù)的this還是指向全局變量window,然后依次分析每行代碼。立即執(zhí)行函數(shù)中:

var number;

this.number *= 2; // 等價(jià)于 window.number *= 2; 使得全局變量的number變?yōu)?0

number = number * 2; // NaN

number = 3;

然后我們?cè)趫?zhí)行「立即執(zhí)行函數(shù)」返回的函數(shù)。此時(shí)由于是「obj.fn()」這樣調(diào)用,所以this指向了obj對(duì)象。

var number = this.number; // 3,即obj.number

this.number *= 2; // 使得obj.number變?yōu)?

console.log(number); // 輸出3

number *= 3; // 此時(shí)number還是指閉包中的那個(gè)「number」,剛剛number是9,現(xiàn)在就是27了

console.log(number); // 就是輸出閉包那個(gè)「number」,是27

最后執(zhí)行「console.log(window.number)」,此時(shí)全局變量的number是20,輸出20.

參考:

  1. https://www.cnblogs.com/gaoht/p/10694967.html
  2. https://zhuanlan.zhihu.com/p/26475137
  3. http://es6.ruanyifeng.com/#docs/function
  4. http://www.ruanyifeng.com/blog/2018/06/javascript-this.html
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容