日期:2019 年 9 月 5 日
this 指向問(wèn)題
介紹
this 指向問(wèn)題一直是 js 中一個(gè)令人頭疼的問(wèn)題,這幾天得空,復(fù)習(xí)了一下以前的知識(shí),順便整理了有關(guān) js 中 this 指向的知識(shí)點(diǎn)。
this 綁定
很多人對(duì) this 指向一直都存在一個(gè)誤區(qū):this 寫(xiě)在誰(shuí)里面就指向誰(shuí)。但其實(shí)這是不對(duì)的,this 既不指向函數(shù)自身,也不指函數(shù)的詞法作用域,它實(shí)際是在函數(shù)被調(diào)用時(shí)才發(fā)生的綁定,也就是說(shuō)this具體指向什么,取決于你是怎么調(diào)用的函數(shù)。
this 存在4種綁定規(guī)則:
- 默認(rèn)綁定
- 隱式綁定
- 顯示綁定
- new綁定
這 4 種綁定規(guī)則的優(yōu)先級(jí)是從低到高的
1、默認(rèn)綁定
function foo(){
console.log(this.a);
}
var a = 2;
foo(); // 打印結(jié)果?
打印結(jié)果:2
因?yàn)閒oo()是直接調(diào)用的(獨(dú)立函數(shù)調(diào)用),沒(méi)有應(yīng)用其他的綁定規(guī)則,這里進(jìn)行了默認(rèn)綁定,將全局對(duì)象( window )綁定this上,所以this.a 就解析成了全局變量中的a,即 2
注意:在嚴(yán)格模式下(strict mode),全局對(duì)象將無(wú)法使用默認(rèn)綁定( this 指向 undefined ),即執(zhí)行會(huì)報(bào) undefined 的錯(cuò)誤:
function foo(){
“use strict”;
console.log(this.a);
}
var a = 2;
foo(); // Uncaught TypeError : Cannot read property 'a' of undefined
另外,這幾天正在學(xué) Typescript,剛好也遇到了一些 this 指向的問(wèn)題,這里也說(shuō)道說(shuō)道,看一看下面的幾種情況:
例一:
let a = 233;
let obj1 = {
a: 666,
fn: function(){
console.log(this.a);
}
}
obj1.fn(); // 輸出: 666
例二:
let a = 233;
let obj2 = {
a: 888,
fn: function(){
console.log("外層:", this.a);
return function(){
console.log("內(nèi)層:", this.a);
}
}
}
let ha = obj2.fn(); // 外層: 888
ha(); // 內(nèi)層: undefined
例三:
var a = 233;
let obj2 = {
a: 888,
fn: function(){
console.log("外層:", this.a);
return function(){
console.log("內(nèi)層:", this.a);
}
}
}
let ha = obj2.fn(); // 外層: 888
ha(); // 內(nèi)層: 233
比較之下我們可以看到,對(duì)象的 fn 屬性是一個(gè)函數(shù),對(duì)象調(diào)用這個(gè)函數(shù),函數(shù)的執(zhí)行上下文就是這個(gè)對(duì)象,所以外層的 this 指向?qū)ο鬀](méi)有問(wèn)題;
然后 fn 還返回了一個(gè)函數(shù),這個(gè)函數(shù)里面的 this (內(nèi)層 this)指向的是 window 而不是對(duì)象 obj2;
有人就要問(wèn)了既然指向 window,為什么例二中內(nèi)層的輸出不是 233 呢?這個(gè)問(wèn)題問(wèn)得好,這就涉及到了 ES6 與 ES5 中變量聲明方面的區(qū)別了:
- ES5聲明變量只有兩種方式:var 和 function
- ES6有 let、const、import、class 再加上 ES5 的 var、function 共有六種聲明變量的方式
- 還需要了解頂層對(duì)象:瀏覽器環(huán)境中頂層對(duì)象是window,Node中是global對(duì)象
- ES5中,頂層對(duì)象的屬性等價(jià)于全局變量 (敲黑板了啊)
- ES6中,有所改變:var、function聲明的全局變量,依然是頂層對(duì)象的屬性;let、const、class聲明的全局變量不屬于頂層對(duì)象的屬性,也就是說(shuō)ES6開(kāi)始,全局變量和頂層對(duì)象的屬性開(kāi)始分離、脫鉤
所以ES6非嚴(yán)格模式下,與var聲明的全局變量都會(huì)成為window的屬性:
a = 1;
console.log(window.a); // 1
var b = 2;
console.log(window.b); // 2
而使用let聲明的全局變量,不會(huì)成為window的屬性:
let c = 3;
console.log(window.c); // undefined
關(guān)于這方面問(wèn)題,更多請(qǐng)移步原文:
關(guān)于let聲明的變量在window下無(wú)法獲取的問(wèn)題
2、隱式綁定
除了直接對(duì)函數(shù)進(jìn)行調(diào)用外,有些情況是,函數(shù)的調(diào)用是在某個(gè)對(duì)象上觸發(fā)的,即調(diào)用位置上存在上下文對(duì)象
function foo(){
console.log(this.a);
}
var a = 2;
var obj = {
a : 3,
foo : foo
}
obj.foo(); // ?
輸出結(jié)果: 3
這里foo函數(shù)被當(dāng)做引用屬性,被添加到obj對(duì)象上。這里的調(diào)用過(guò)程是這樣的:
獲取obj.foo屬性 -> 根據(jù)引用關(guān)系找到foo函數(shù),執(zhí)行調(diào)用,所以這里對(duì)foo的調(diào)用存在上下文對(duì)象obj,this進(jìn)行了隱式綁定,即this綁定到了obj上,所以this.a被解析成了obj.a,即 3
當(dāng)存在多層調(diào)用鏈時(shí):
function foo(){
console.log(this.a);
}
var a = 2;
var obj1 = {
a : 4,
foo : foo
}
var obj2 = {
a : 3,
obj1 : obj1
}
obj2.obj1.foo(); // ?
輸出結(jié)果:4
同樣,我們看下函數(shù)的調(diào)用過(guò)程:
先獲取obj2.obj1 -> 通過(guò)引用獲取到obj1對(duì)象,再訪問(wèn) obj1.foo -> 最后執(zhí)行foo函數(shù)調(diào)用
這里調(diào)用鏈不只一層,存在obj1、obj2兩個(gè)對(duì)象,那么隱式綁定具體會(huì)綁哪個(gè)對(duì)象。這里原則是獲取最后一層調(diào)用的上下文對(duì)象,即obj1,所以結(jié)果顯然是 4
3、顯示綁定
對(duì)于隱式綁定,this值在調(diào)用過(guò)程中會(huì)動(dòng)態(tài)變化,可是我們就想綁定指定的對(duì)象,這時(shí)就用到了顯示綁定
顯示綁定主要是通過(guò)改變對(duì)象的prototype關(guān)聯(lián)對(duì)象,這里不展開(kāi)講。具體使用上,可以通過(guò)這兩個(gè)方法 call() 或 apply() 來(lái)實(shí)現(xiàn)(大多數(shù)函數(shù)及自己創(chuàng)建的函數(shù)默認(rèn)都提供這兩個(gè)方法)
call 與apply 是同樣的作用,區(qū)別只是其他參數(shù)的設(shè)置上:
/*apply()方法*/
function.apply(thisObj[, argArray])
/*call()方法*/
function.call(thisObj[, arg1[, arg2[, [,...argN]]]]);
它們各自的定義:
apply:調(diào)用一個(gè)對(duì)象的一個(gè)方法,用另一個(gè)對(duì)象替換當(dāng)前對(duì)象。例如:B.apply(A, arguments);即A對(duì)象應(yīng)用B對(duì)象的方法。
call:調(diào)用一個(gè)對(duì)象的一個(gè)方法,用另一個(gè)對(duì)象替換當(dāng)前對(duì)象。例如:B.call(A, args1,args2);即A對(duì)象調(diào)用B對(duì)象的方法。
它們的共同之處:
都“可以用來(lái)代替另一個(gè)對(duì)象調(diào)用一個(gè)方法,將一個(gè)函數(shù)的對(duì)象上下文從初始的上下文改變?yōu)橛蓆hisObj指定的新對(duì)象”。
它們的不同之處:
apply:最多只能有兩個(gè)參數(shù)——新this對(duì)象和一個(gè)數(shù)組argArray。如果給該方法傳遞多個(gè)參數(shù),則把參數(shù)都寫(xiě)進(jìn)這個(gè)數(shù)組里面,當(dāng)然,即使只有一個(gè)參數(shù),也要寫(xiě)進(jìn)數(shù)組里。如果argArray不是一個(gè)有效的數(shù)組或arguments對(duì)象,那么將導(dǎo)致一個(gè)TypeError。如果沒(méi)有提供argArray和thisObj任何一個(gè)參數(shù),那么Global對(duì)象將被用作thisObj,并且無(wú)法被傳遞任何參數(shù)。
call:它可以接受多個(gè)參數(shù),第一個(gè)參數(shù)與apply一樣,后面則是一串參數(shù)列表。這個(gè)方法主要用在js對(duì)象各方法相互調(diào)用的時(shí)候,使當(dāng)前this實(shí)例指針保持一致,或者在特殊情況下需要改變this指針。如果沒(méi)有提供thisObj參數(shù),那么 Global 對(duì)象被用作thisObj。 實(shí)際上,apply和call的功能是一樣的,只是傳入的參數(shù)列表形式不同。
有關(guān) apply 與 call 的比較,詳情請(qǐng)移步原文:
apply() 與 call() 的區(qū)別
OK,回歸正題,我們來(lái)看看顯示綁定:
function foo(){
console.log(this.a);
}
var a = 2;
var obj1 = {
a : 3,
foo : foo
}
var obj2 = {
a : 4,
obj1 : obj1
}
foo.call(obj1); // ?
foo.call(obj2); // ?
結(jié)果:
3
4
這里因?yàn)轱@示的申明了要綁定的對(duì)象,所以this就被綁定到了 obj 上,打印的結(jié)果自然就是obj1.a 和 obj2.a
4、new 綁定
var a = 2;
function foo(a){
this.a = a;
}
let bar1 = new foo(3);
console.log(bar1.a); // ?
let bar2 = new foo(4);
console.log(bar2.a); // ?
結(jié)果:
3
4
因?yàn)槊看握{(diào)用生成的是全新的對(duì)象,該對(duì)象又會(huì)自動(dòng)綁定到this上,所以答案顯而易見(jiàn)
綁定規(guī)則優(yōu)先級(jí)
上面也說(shuō)過(guò),這里在重復(fù)一下。優(yōu)先級(jí)是這樣的,以按照下面的順序來(lái)進(jìn)行判斷:
函數(shù)是否在new中調(diào)用(new綁定)?如果是的話this綁定的是新創(chuàng)建的對(duì)象;
函數(shù)是否通過(guò)call、apply(顯式綁定)或者硬綁定調(diào)用?如果是的話,this綁定的是 指定的對(duì)象;
函數(shù)是否在某個(gè)上下文對(duì)象中調(diào)用(隱式綁定)?如果是的話,this綁定的是那個(gè)上下文對(duì)象;
如果都不是的話,使用默認(rèn)綁定。如果在嚴(yán)格模式下,就綁定undefined,否則綁定到全局對(duì)象。
規(guī)則例外:
在顯示綁定中,對(duì)于 null 和 undefined 的綁定將不會(huì)生效
function foo(){
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
foo.call(undefined); // 2
這種情況主要是用在不關(guān)心this的具體綁定對(duì)象(用來(lái)忽略this),而傳入null實(shí)際上會(huì)進(jìn)行默認(rèn)綁定,導(dǎo)致函數(shù)中可能會(huì)使用到全局變量,與預(yù)期不符
所以對(duì)于要忽略this的情況,可以傳入一個(gè)空對(duì)象?,該對(duì)象通過(guò)Object.create(null)創(chuàng)建。這里不用{}的原因是,?是真正意義上的空對(duì)象,它不創(chuàng)建Object.prototype委托,{}和普通對(duì)象一樣,有原型鏈委托關(guān)系
箭頭函數(shù)中的 this
箭頭函數(shù)的this指向:
- 箭頭函數(shù)不會(huì)創(chuàng)建自己的this,它只會(huì)從自己的作用域鏈的上一層繼承this;
- 箭頭函數(shù)里的this指向無(wú)法被call,apply,bind 改變;
var obj = {
foo(){
console.log(this);
},
bar : ()=>{
console.log(this);
}
}
obj.foo(); // { foo: foo(), bar: bar() }
obj.bar(); // window