關(guān)于this的一些筆記

this

什么是this,其實(shí)你可以理解為它類似一個(gè)指針

在瀏覽器環(huán)境中,全局作用域下,this指的是windows,在node環(huán)境中,全局下this打印的是一個(gè)空對象{};


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

foo();

上面這段代碼運(yùn)行之后,在瀏覽器環(huán)境中,會打印window對象。再看下面這段代碼。


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

var fn1 = foo;

fn1();

上面的代碼執(zhí)行之后,打印的還是window,我們在看。


function foo(){
    console.log(this)
}

var fn1 = {
    a: 1,
    foo: foo
}

fn1.foo();

這段代碼執(zhí)行之后返回的是fn1對象。接下來再看


function foo(){
    console.log(this)
}

var arr = [foo,2,3]

arr[0]()

上述代碼執(zhí)行完成后,打印的是arr數(shù)組對象。

通過之前的代碼我們可以很明顯的看到,this 是在運(yùn)行時(shí)進(jìn)行綁定的,并不是在編寫時(shí)綁定,當(dāng)我們把函數(shù)賦值到引用對象里調(diào)用的時(shí)候,那this就指向當(dāng)前的調(diào)用環(huán)境,就是調(diào)用的對象本身

什么是執(zhí)行上下文

javascript是一個(gè)單線程語言,這意味著在瀏覽器中同時(shí)只能做一件事情。當(dāng)javascript解釋器初始執(zhí)行代碼,它首先默認(rèn)進(jìn)入全局上下文。每次調(diào)用一個(gè)函數(shù)將會創(chuàng)建一個(gè)新的執(zhí)行上下文。

每次新創(chuàng)建的一個(gè)執(zhí)行上下文會被添加到作用域鏈的頂部,有時(shí)也稱為執(zhí)行或調(diào)用棧。瀏覽器總是運(yùn)行位于作用域鏈頂部的當(dāng)前執(zhí)行上下文。一旦完成,當(dāng)前執(zhí)行上下文將從棧頂被移除并且將控制權(quán)歸還給之前的執(zhí)行上下文。比如我調(diào)用了這個(gè)函數(shù),那么這個(gè)函數(shù)的執(zhí)行上下文 就會被添加到作用域鏈的頂部,然后這個(gè)函數(shù)執(zhí)行完成這個(gè)上下文就會被移除,然后控制權(quán)交給之前的上下文。

執(zhí)行上下文的建立過程

我們現(xiàn)在已經(jīng)知道,每當(dāng)調(diào)用一個(gè)函數(shù)時(shí),一個(gè)新的執(zhí)行上下文就會被創(chuàng)建出來。然而,在javascript引擎內(nèi)部,這個(gè)上下文的創(chuàng)建過程具體分為兩個(gè)階段:

  1. 建立階段(發(fā)生在當(dāng)調(diào)用一個(gè)函數(shù)時(shí),但是在執(zhí)行函數(shù)體內(nèi)的具體代碼以前)

    • 建立變量,函數(shù),arguments對象,參數(shù)
    • 建立作用域鏈
    • 確定this的值
  2. 代碼執(zhí)行階段:

    • 變量賦值,函數(shù)引用,執(zhí)行其它代碼

我們可以看到,當(dāng)一個(gè)函數(shù)調(diào)用的時(shí)候,會建立執(zhí)行上下文,建立的時(shí)候會確定this的值,所以我們現(xiàn)在,就是要探討他的this的值是怎么確定的。

調(diào)用位置


function baz() {
    // 當(dāng)前調(diào)用棧是:baz
    // 因此,當(dāng)前調(diào)用位置是全局作用域
    console.log("baz");
    bar(); // <-- bar 的調(diào)用位置
}

function bar() {
    // 當(dāng)前調(diào)用棧是 baz -> bar
    // 因此,當(dāng)前調(diào)用位置在 baz 中
    console.log("bar");
    foo(); // <-- foo 的調(diào)用位置
}

function foo() {
    // 當(dāng)前調(diào)用棧是 baz -> bar -> foo
    // 因此,當(dāng)前調(diào)用位置在 bar 中
    console.log("foo");
}
baz(); // <-- baz 的調(diào)用位置

注意我們是如何(從調(diào)用棧中)分析出真正的調(diào)用位置的,因?yàn)樗鼪Q定了 this 的綁定

綁定規(guī)則

我們來看看在函數(shù)的執(zhí)行過程中調(diào)用位置如何決定 this 的綁定對象。

你必須找到調(diào)用位置,然后判斷需要應(yīng)用下面四條規(guī)則中的哪一條。我們首先會分別解釋這四條規(guī)則,然后解釋多條規(guī)則都可用時(shí)它們的優(yōu)先級如何排列。

規(guī)則一 默認(rèn)綁定

獨(dú)立調(diào)用。可以把這條規(guī)則看作是無法應(yīng)用其他規(guī)則時(shí)的默認(rèn)規(guī)則。


function foo() {
    console.log(this.a);
}
var a = 2;
foo(); // 2

在代碼中, foo() 是直接使用不帶任何修飾的函數(shù)引用進(jìn)行調(diào)用的,因此只能使用默認(rèn)綁定,無法應(yīng)用其他規(guī)則。

如果使用嚴(yán)格模式( strict mode ),那么全局對象將無法使用默認(rèn)綁定,因此 this 會綁定到 undefined

規(guī)則二 隱式綁定

另一條需要考慮的規(guī)則是調(diào)用位置是否有上下文對象,或者說是否被某個(gè)對象擁有或者包含,不過這種說法可能會造成一些誤導(dǎo)。


function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2

調(diào)用位置會使用 obj 上下文來引用函數(shù),因此你可以說函數(shù)被調(diào)用時(shí) obj 對象“擁有”或者“包含”它。

當(dāng) foo() 被調(diào)用時(shí),它的落腳點(diǎn)確實(shí)指向 obj 對象。當(dāng)函數(shù)引用有上下文對象時(shí),隱式綁定規(guī)則會把函數(shù)調(diào)用中的 this 綁定到這個(gè)上下文對象。因?yàn)檎{(diào)用 foo() 時(shí) this 被綁定到 obj ,因此 this.a 和 obj.a 是一樣的。

對象屬性引用鏈中只有最頂層或者說最后一層會影響調(diào)用位置。舉例來說:


function foo() {
    console.log(this.a);
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};
obj1.obj2.foo(); // 42

隱式丟失


function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; // 函數(shù)別名!
var a = "oops, global"; // a 是全局對象的屬性
bar(); // "oops, global"

雖然 bar 是 obj.foo 的一個(gè)引用,但是實(shí)際上,它引用的是 foo 函數(shù)本身,因此此時(shí)的bar() 其實(shí)是一個(gè)不帶任何修飾的函數(shù)調(diào)用,因此應(yīng)用了默認(rèn)綁定。

一種更微妙、更常見并且更出乎意料的情況發(fā)生在傳入回調(diào)函數(shù)時(shí):


function foo() {
    console.log(this.a);
}

function doFoo(fn) {
    // fn 其實(shí)引用的是 foo
    fn(); // <-- 調(diào)用位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全局對象的屬性
doFoo(obj.foo); // "oops, global"

參數(shù)傳遞其實(shí)就是一種隱式賦值,因此我們傳入函數(shù)時(shí)也會被隱式賦值,所以結(jié)果和上一個(gè)例子一樣

規(guī)則三 顯式綁定

使用 call(..) 和 apply(..) 方法可以強(qiáng)制綁定this的指向。


function foo(){
    console.log(this.a)
}

var obj = {
    a: 2,
}

foo.call(obj)

如果你傳入了一個(gè)原始值(字符串類型、布爾類型或者數(shù)字類型)來當(dāng)作 this 的綁定對象,這個(gè)原始值會被轉(zhuǎn)換成它的對象形式(也就是 new String(..) 、 new Boolean(..) 或者
new Number(..) )。這通常被稱為“裝箱”。

規(guī)則四 new 綁定

在javascript中,使用new 操作符的時(shí)候,其實(shí)和其他大多數(shù)語言中使用new操作符的機(jī)制不太一樣,當(dāng)我們使用了new 操作符調(diào)用函數(shù),這個(gè)函數(shù)就會被當(dāng)做構(gòu)造函數(shù)來調(diào)用。

調(diào)用構(gòu)造函數(shù)的時(shí)候,會發(fā)生四件事情:

  1. 創(chuàng)建一個(gè)空對象。
  2. 將這個(gè)空對象的proto成員指向了構(gòu)造函數(shù)的prototype成員對象
  3. 這個(gè)新對象會綁定到函數(shù)調(diào)用的 this
  4. 如果函數(shù)沒有返回其他對象,那么 new 表達(dá)式中的函數(shù)調(diào)用會自動(dòng)返回這個(gè)新對象

關(guān)于構(gòu)造函數(shù)可查看 http://www.itdecent.cn/p/794672ea66c5

綁定優(yōu)先級

默認(rèn)綁定的優(yōu)先級是最低的。

先看隱式綁定和顯式綁定哪個(gè)優(yōu)先級更高:


function foo() {
  console.log(this.a);
}
var obj1 = {
  a: 2,
  foo: foo
};
var obj2 = {
  a: 3,
  foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2

可以看到,顯式綁定優(yōu)先級更高,也就是說在判斷時(shí)應(yīng)當(dāng)先考慮是否可以應(yīng)用顯式綁定。

現(xiàn)在我們需要搞清楚 new 綁定和隱式綁定的優(yōu)先級誰高誰低:


function foo(val){
  this.a = val;
}

var obj = {
  foo: foo,
}

obj.foo(3);
console.log(obj.a); // 3

var bar = new obj.foo(4);

console.log(bar.a);
console.log(obj.a)

可以看到 new 綁定比隱式綁定優(yōu)先級高。但是 new 綁定和顯式綁定誰的優(yōu)先級更高呢


function foo(something) {
  this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2
var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3

判斷this

現(xiàn)在我們可以根據(jù)優(yōu)先級來判斷函數(shù)在某個(gè)調(diào)用位置應(yīng)用的是哪條規(guī)則

  1. 函數(shù)是否在 new 中調(diào)用( new 綁定)?如果是的話 this 綁定的是新創(chuàng)建的對象。
var bar = new foo()
  1. 函數(shù)是否通過 call 、 apply (顯式綁定)或者硬綁定調(diào)用?如果是的話, this 綁定的是指定的對象
var bar = foo.call(obj2)
  1. 函數(shù)是否在某個(gè)上下文對象中調(diào)用(隱式綁定)?如果是的話, this 綁定的是那個(gè)上下文對象。
var bar = obj1.foo()
  1. 如果都不是的話,使用默認(rèn)綁定。如果在嚴(yán)格模式下,就綁定到 undefined ,否則綁定到全局對象。
var bar = foo()

被忽略的this

如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call 、 apply 或者 bind ,這些值在調(diào)用時(shí)會被忽略,實(shí)際應(yīng)用的是默認(rèn)綁定規(guī)則


function foo() {
  console.log(this.a);
}
var a = 2;
foo.call(null); // 2

一般我們要展開數(shù)組,或者是對參數(shù)進(jìn)行柯里化(預(yù)先設(shè)置一些參數(shù))的時(shí)候,會經(jīng)常傳入一些null值進(jìn)行占位u,es6中展開數(shù)組可以用拓展運(yùn)算符...,但是還沒有參數(shù)柯里化的相關(guān)語法。


function foo(a, b) {

  console.log("a:" + a + ", b:" + b);
}
// 把數(shù)組“展開”成參數(shù)
foo.apply(null, [2, 3]); // a:2, b:3
// 使用 bind(..) 進(jìn)行柯里化
var bar = foo.bind(null, 2);
bar(3); // a:2, b:3

這兩種方法都需要傳入一個(gè)參數(shù)當(dāng)作 this 的綁定對象。如果函數(shù)并不關(guān)心 this 的話,你仍然需要傳入一個(gè)占位值,這時(shí) null 可能是一個(gè)不錯(cuò)的選擇,

更安全的this


function foo(a, b) {
  console.log("a:" + a + ", b:" + b);
}
// 我們的 DMZ 空對象
var ? = Object.create(null);
// 把數(shù)組展開成參數(shù)
foo.apply(?, [2, 3]); // a:2, b:3
// 使用 bind(..) 進(jìn)行柯里化
var bar = foo.bind(?, 2);
bar(3); // a:2, b:3

使用變量名 ? 不僅讓函數(shù)變得更加“安全”,而且可以提高代碼的可讀性,因?yàn)?? 表示“我希望 this 是空”,這比 null 的含義更清楚。

間接引用

另一個(gè)需要注意的是,你有可能(有意或者無意地)創(chuàng)建一個(gè)函數(shù)的“間接引用”,在這種情況下,調(diào)用這個(gè)函數(shù)會應(yīng)用默認(rèn)綁定規(guī)則。


function foo() {
  console.log(this.a);
}

var a = 2;
var o = {
  a: 3,
  foo: foo
};
var p = {
  a: 4
};
o.foo(); // 3
(p.foo = o.foo)(); // 2

賦值表達(dá)式 p.foo = o.foo 的返回值是目標(biāo)函數(shù)的引用,因此調(diào)用位置是 foo() 而不是p.foo() 或者 o.foo() 。根據(jù)我們之前說過的,這里會應(yīng)用默認(rèn)綁定。

箭頭函數(shù)

箭頭函數(shù)不適用this的四種標(biāo)準(zhǔn)規(guī)則。而是根據(jù)外層作用域來決定this。


function foo() {
  return () => {
    console.log(this.a);
  }
}

var obj = {
  a: 2,
  foo: foo
}

var obj2 = {
  a: 4
}

var bar = foo.call(obj2)

bar.call(obj); // 4

foo() 內(nèi)部創(chuàng)建的箭頭函數(shù)會捕獲調(diào)用時(shí) foo() 的 this,由于 foo() 的 this 綁定到 obj2,bar (引用箭頭函數(shù))的 this 也會綁定到 obj2,頭函數(shù)的綁定無法被修改。( new 也不行?。?/p>

總結(jié)

如果要判斷一個(gè)運(yùn)行中函數(shù)的 this 綁定,就需要找到這個(gè)函數(shù)的直接調(diào)用位置。找到之后就可以順序應(yīng)用下面這四條規(guī)則來判斷 this 的綁定對象。

  1. 由 new 調(diào)用?綁定到新創(chuàng)建的對象。
  2. 由 call 或者 apply (或者 bind )調(diào)用?綁定到指定的對象。
  3. 由上下文對象調(diào)用?綁定到那個(gè)上下文對象。
  4. 默認(rèn):在嚴(yán)格模式下綁定到 undefined ,否則綁定到全局對象。

一定要注意,有些調(diào)用可能在無意中使用默認(rèn)綁定規(guī)則。如果想“更安全”地忽略 this 綁定,你可以使用一個(gè) DMZ 對象,比如 ? = Object.create(null) ,以保護(hù)全局對象。

ES6 中的箭頭函數(shù)并不會使用四條標(biāo)準(zhǔn)的綁定規(guī)則,而是根據(jù)當(dāng)前的詞法作用域來決定this ,具體來說,箭頭函數(shù)會繼承外層函數(shù)調(diào)用的 this 綁定(無論 this 綁定到什么)。這其實(shí)和 ES6 之前代碼中的 self = this 機(jī)制一樣。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,569評論 19 139
  • 1.概念 在JavaScript中,this 是指當(dāng)前函數(shù)中正在執(zhí)行的上下文環(huán)境,因?yàn)檫@門語言擁有四種不同的函數(shù)調(diào)...
    BluesCurry閱讀 1,236評論 0 2
  • 1. this之謎 在JavaScript中,this是當(dāng)前執(zhí)行函數(shù)的上下文。因?yàn)镴avaScript有4種不同的...
    百里少龍閱讀 1,094評論 0 3
  • 2018年3月28日 星期三 天氣晴 兩天不寫親子日記了,原因是這兩天沒有和兒子交流。 這幾天我公...
    孫鴻翔閱讀 216評論 0 0
  • Less是一門CSS預(yù)處理語言,擴(kuò)展了CSS,增加了變量、Mixin、函數(shù)等特性。 Less可運(yùn)行在Node或?yàn)g覽...
    JunChow520閱讀 638評論 0 0

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