前端基礎(chǔ)進階(七):全方位解讀this

~

我們在學(xué)習(xí)JavaScript的過程中,由于對一些概念理解得不是很清楚轧葛,但是又想要通過一些方式把它記下來禽车,于是就很容易草率的給這些概念定下一些方便自己記憶的有偏差的結(jié)論。

危害比較大的是遣耍,有的不準(zhǔn)確的結(jié)論在網(wǎng)上還廣為流傳闺阱。

比如對于this指向的理解中,有這樣一種說法:誰調(diào)用它舵变,this就指向誰酣溃。在我剛開始學(xué)習(xí)this的時候,我非常相信這句話纪隙。因為在一些情況下赊豌,這樣理解也還算說得通∶嘣郏可是我常常會在開發(fā)中遇到一些不一樣的情況碘饼,一個由于this的錯誤調(diào)用,可以讓我懵逼一整天悲伶。那個時候我也查資料艾恼,在群里問大神,可是我仍然搞不清楚“我特么到底錯哪里了”麸锉。

其實只是因為我的認(rèn)知中有一個不準(zhǔn)確的結(jié)論钠绍。

所以,我認(rèn)為需要有這樣一篇文章花沉,來幫助大家全方位的解讀this五慈。讓大家對this,有一個正確的主穗,全面的認(rèn)知。

在這之前毙芜,我們回顧一下執(zhí)行上下文忽媒。

在前面幾篇文章中,我有好幾個地方都提到執(zhí)行上下文的生命周期腋粥,為了防止大家沒有記住晦雨,再發(fā)一次架曹,如下圖。


執(zhí)行上下文生命周期

執(zhí)行上下文的創(chuàng)建階段闹瞧,會分別生成變量對象绑雄,建立作用域鏈,確定this指向奥邮。其中變量對象與作用域鏈我們都已經(jīng)明白了万牺。本文的關(guān)鍵,就是確定this指向洽腺。

首先脚粟,我們需要得出一個非常重要的,并且一定要牢記于心的結(jié)論蘸朋,this的指向核无,是在函數(shù)被調(diào)用的時候確定的。也就是執(zhí)行上下文被創(chuàng)建時確定的藕坯。

因此团南,一個函數(shù)中的this指向,可以非常靈活炼彪。比如下面的例子中吐根,同一個函數(shù)由于調(diào)用方式的不同,this指向了不一樣的對象霹购。

var a = 10;
var obj = {
  a: 20
}

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

fn(); // 10
fn.call(obj); // 20

除此之外佑惠,在函數(shù)執(zhí)行過程中,this一旦被確定齐疙,就不可更改了膜楷。

var a = 10;
var obj = {
  a: 20
}

function fn() {
  this = obj; // 這句話試圖修改this,運行后會報錯
  console.log(this.a);
}

fn();
一贞奋、全局對象中的this

關(guān)于全局對象的this赌厅,我之前在總結(jié)變量對象的時候提到過,它是一個比較特殊的存在轿塔。全局環(huán)境中的this特愿,指向它本身。因此勾缭,這也相對簡單揍障,沒有那么多復(fù)雜的情況需要考慮。

// 通過this綁定到全局對象
this.a2 = 20;

// 通過聲明綁定到變量對象俩由,但在全局環(huán)境中毒嫡,變量對象就是它自身
var a1 = 10;

// 僅僅只有賦值操作,標(biāo)識符會隱式綁定到全局對象
a3 = 30;

// 輸出結(jié)果會全部符合預(yù)期
console.log(a1);
console.log(a2);
console.log(a3);
二幻梯、函數(shù)中的this

在總結(jié)函數(shù)中this指向之前兜畸,我想我們有必要通過一些奇怪的例子努释,來感受一下函數(shù)中this的捉摸不定。

// demo01
var a = 20;
function fn() {
  console.log(this.a);
}
fn();
// demo02
var a = 20;
function fn() {
  function foo() {
    console.log(this.a);
  }
  foo();
}
fn();
// demo03
var a = 20;
var obj = {
  a: 10,
  c: this.a + 20,
  fn: function () {
    return this.a;
  }
}

console.log(obj.c);
console.log(obj.fn());

這幾個例子需要花點時間仔細感受一下咬摇,如果你暫時沒想明白怎么回事伐蒂,也不用著急,我們一點一點來分析肛鹏。

分析之前逸邦,我們直接了當(dāng)拋出結(jié)論。

在一個函數(shù)上下文中龄坪,this由調(diào)用者提供昭雌,由調(diào)用函數(shù)的方式來決定。如果調(diào)用者函數(shù)健田,被某一個對象所擁有烛卧,那么該函數(shù)在調(diào)用時,內(nèi)部的this指向該對象妓局。如果函數(shù)獨立調(diào)用总放,那么該函數(shù)內(nèi)部的this,則指向undefined好爬。但是在非嚴(yán)格模式中局雄,當(dāng)this指向undefined時,它會被自動指向全局對象存炮。

從結(jié)論中我們可以看出炬搭,想要準(zhǔn)確確定this指向,找到函數(shù)的調(diào)用者以及區(qū)分他是否是獨立調(diào)用十分關(guān)鍵穆桂。

// 為了能夠準(zhǔn)確判斷宫盔,我們在函數(shù)內(nèi)部使用嚴(yán)格模式,因為非嚴(yán)格模式會自動指向全局
function fn() {
  'use strict';
  console.log(this);
}

fn();  // fn是調(diào)用者享完,獨立調(diào)用
window.fn();  // fn是調(diào)用者灼芭,被window所擁有

在上面的簡單例子中,fn()作為獨立調(diào)用者般又,按照定義的理解彼绷,它內(nèi)部的this指向就為undefined。而window.fn()則因為fn被window所擁有茴迁,內(nèi)部的this就指向了window對象寄悯。

掌握了這個規(guī)則,現(xiàn)在回過頭去看看上面的三個例子堕义,通過添加/去除嚴(yán)格模式热某,你就會發(fā)現(xiàn),原來this已經(jīng)變得不那么虛無縹緲,已經(jīng)有跡可循了昔馋。

但是我們需要特別注意的是demo03。在demo03中,對象obj中的c屬性使用this.a + 20來計算。這里我們需要明確的一點是灵莲,單獨的{}不會形成新的作用域鸟整,因此這里的this.a,由于并沒有作用域的限制毯侦,它仍然處于全局作用域之中。所以這里的this其實是指向的window對象。

那么我們修改一下demo03的代碼倦蚪,大家可以思考一下會發(fā)生什么變化。

'use strict';
var a = 20;
function foo() {
  var a = 1;
  var obj = {
    a: 10,
    c: this.a + 20,
    fn: function () {
      return this.a;
    }
  }
  return obj.c;
}
console.log(foo());    // 边苹?
console.log(window.foo());  // ?

實際開發(fā)中陵且,并不推薦這樣使用this;

上面多次提到的嚴(yán)格模式个束,需要大家認(rèn)真對待慕购,因為在實際開發(fā)中,現(xiàn)在基本已經(jīng)全部采用嚴(yán)格模式了茬底,而最新的ES6沪悲,也是默認(rèn)支持嚴(yán)格模式。

再來看一些容易理解錯誤的例子阱表,加深一下對調(diào)用者與是否獨立運行的理解殿如。

var a = 20;
var foo = {
  a: 10,
  getA: function () {
    return this.a;
  }
}
console.log(foo.getA()); // 10

var test = foo.getA;
console.log(test());  // 20

foo.getA()中,getA是調(diào)用者最爬,他不是獨立調(diào)用涉馁,被對象foo所擁有,因此它的this指向了foo烂叔。而test()作為調(diào)用者谨胞,盡管他與foo.getA的引用相同,但是它是獨立調(diào)用的蒜鸡,因此this指向undefined胯努,在非嚴(yán)格模式,自動轉(zhuǎn)向全局window逢防。

稍微修改一下代碼叶沛,大家自行理解。

var a = 20;
function getA() {
  return this.a;
}
var foo = {
  a: 10,
  getA: getA
}
console.log(foo.getA());  // 10

靈機一動忘朝,再來一個灰署。如下例子。

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

function active(fn) {
  fn(); // 真實調(diào)用者,為獨立調(diào)用
}

var a = 20;
var obj = {
  a: 10,
  getA: foo
}

active(obj.getA);
三溉箕、使用call晦墙,apply顯示指定this

JavaScript內(nèi)部提供了一種機制,讓我們可以自行手動設(shè)置this的指向肴茄。它們就是call與apply晌畅。所有的函數(shù)都具有這兩個方法。它們除了參數(shù)略有不同之外寡痰,其功能完全一樣抗楔。它們的第一個參數(shù)都為this將要指向的對象。

如下例子所示拦坠。fn并非屬于對象obj的方法连躏,但是通過call,我們將fn內(nèi)部的this綁定為obj贞滨,因此就可以使用this.a訪問obj的a屬性了入热。這就是call/apply的用法。

function fn() {
  console.log(this.a);
}
var obj = {
  a: 20
}

fn.call(obj);

call與applay后面的參數(shù)疲迂,都是向?qū)⒁獔?zhí)行的函數(shù)傳遞參數(shù)才顿。其中call以一個一個的形式傳遞,apply以數(shù)組的形式傳遞尤蒿。這是他們唯一的不同郑气。

function fn(num1, num2) {
  console.log(this.a + num1 + num2);
}
var obj = {
  a: 20
}

fn.call(obj, 100, 10); // 130
fn.apply(obj, [20, 10]); // 50

因為call/apply的存在,JavaScript變得更加靈活腰池。
也因此他們的使用場景就多種多樣尾组。簡單總結(jié)幾點,也歡迎大家補充示弓。

將類數(shù)組對象轉(zhuǎn)換為數(shù)組

function exam(a, b, c, d, e) {

  // 先看看函數(shù)的自帶屬性 arguments 什么是樣子的
  console.log(arguments);

  // 使用call/apply將arguments轉(zhuǎn)換為數(shù)組, 返回結(jié)果為數(shù)組讳侨,arguments自身不會改變
  var arg = [].slice.call(arguments);

  console.log(arg);
}

exam(2, 8, 9, 10, 3);

// result:
// { '0': 2, '1': 8, '2': 9, '3': 10, '4': 3 }
// [ 2, 8, 9, 10, 3 ]
//
// 也常常使用該方法將DOM中的nodelist轉(zhuǎn)換為數(shù)組
// [].slice.call( document.getElementsByTagName('li') );

根據(jù)自己的需要靈活修改this指向

var foo = {
  name: 'joker',
  showName: function () {
    console.log(this.name);
  }
}
var bar = {
  name: 'rose'
}
foo.showName.call(bar);

實現(xiàn)繼承

// 定義父級的構(gòu)造函數(shù)
var Person = function (name, age) {
  this.name = name;
  this.age = age;
  this.gender = ['man', 'woman'];
}

// 定義子類的構(gòu)造函數(shù)
var Student = function (name, age, high) {

  // use call
  Person.call(this, name, age);
  this.high = high;
}
Student.prototype.message = function () {
  console.log('name:' + this.name + ', age:' + this.age + ', high:' + this.high + ', gender:' + this.gender[0] + ';');
}

new Student('xiaom', 12, '150cm').message();

// result
// ----------
// name:xiaom, age:12, high:150cm, gender:man;

簡單給有面向?qū)ο蠡A(chǔ)的朋友解釋一下。在Student的構(gòu)造函數(shù)中奏属,借助call方法跨跨,將父級的構(gòu)造函數(shù)執(zhí)行了一次,相當(dāng)于將Person中的代碼囱皿,在Sudent中復(fù)制了一份勇婴,其中的this指向為從Student中new出來的實例對象。call方法保證了this的指向正確嘱腥,因此就相當(dāng)于實現(xiàn)了繼承耕渴。Student的構(gòu)造函數(shù)等同于下。

var Student = function (name, age, high) {
  this.name = name;
  this.age = age;
  this.gender = ['man', 'woman'];
  // Person.call(this, name, age); 這一句話齿兔,相當(dāng)于上面三句話橱脸,因此實現(xiàn)了繼承
  this.high = high;
}

在向其他執(zhí)行上下文的傳遞中础米,確保this的指向保持不變

如下面的例子中,我們期待的是getA被obj調(diào)用時添诉,this指向obj屁桑,但是由于匿名函數(shù)的存在導(dǎo)致了this指向的丟失,在這個匿名函數(shù)中this指向了全局吻商,因此我們需要想一些辦法找回正確的this指向掏颊。

var obj = {
  a: 20,
  getA: function () {
    setTimeout(function () {
      console.log(this.a)
    }, 1000)
  }
}

obj.getA();

常規(guī)的解決辦法很簡單,就是使用一個變量艾帐,將this的引用保存起來。我們常常會用到這方法盆偿,但是我們也要借助上面講到過的知識柒爸,來判斷this是否在傳遞中被修改了,如果沒有被修改事扭,就沒有必要這樣使用了捎稚。

var obj = {
  a: 20,
  getA: function () {
    var self = this;
    setTimeout(function () {
      console.log(self.a)
    }, 1000)
  }
}

另外就是借助閉包與apply方法,封裝一個bind方法求橄。

function bind(fn, obj) {
  return function () {
    return fn.apply(obj, arguments);
  }
}

var obj = {
  a: 20,
  getA: function () {
    setTimeout(bind(function () {
      console.log(this.a)
    }, this), 1000)
  }
}

obj.getA();

當(dāng)然今野,也可以使用ES5中已經(jīng)自帶的bind方法。它與我上面封裝的bind方法是一樣的效果罐农。

var obj = {
  a: 20,
  getA: function () {
    setTimeout(function () {
      console.log(this.a)
    }.bind(this), 1000)
  }
}

ES6中也常常使用箭頭函數(shù)的方式來替代這種方案

四条霜、構(gòu)造函數(shù)與原型方法上的this

在封裝對象的時候,我們幾乎都會用到this涵亏,但是宰睡,只有少數(shù)人搞明白了在這個過程中的this指向,就算我們理解了原型气筋,也不一定理解到了this拆内。所以這一部分,我認(rèn)為將會為這篇文章最重要最核心的部分宠默。理解了這里麸恍,將會對你學(xué)習(xí)JS面向?qū)ο螽a(chǎn)生巨大的幫助。

結(jié)合下面的例子搀矫,我拋出幾個問題大家思考一下抹沪。

function Person(name, age) {

    // 這里的this指向了誰?
    this.name = name;
    this.age = age;   
}

Person.prototype.getName = function() {

    // 這里的this又指向了誰?
    return this.name;
}

// 上面的2個this艾君,是同一個嗎采够,他們是否指向了原型對象?

var p1 = new Person('Nick', 20);
p1.getName();

我們已經(jīng)知道冰垄,this蹬癌,是在函數(shù)調(diào)用過程中確定权她,因此,搞明白new的過程中到底發(fā)生了什么就變得十分重要逝薪。

通過new操作符調(diào)用構(gòu)造函數(shù)隅要,會經(jīng)歷以下4個階段。

  • 創(chuàng)建一個新的對象董济;
  • 將構(gòu)造函數(shù)的this指向這個新對象步清;
  • 指向構(gòu)造函數(shù)的代碼,為這個對象添加屬性虏肾,方法等廓啊;
  • 返回新對象。

因此封豪,當(dāng)new操作符調(diào)用構(gòu)造函數(shù)時谴轮,this其實指向的是這個新創(chuàng)建的對象,最后又將新的對象返回出來吹埠,被實例對象p1接收第步。因此,我們可以說缘琅,這個時候粘都,構(gòu)造函數(shù)的this,指向了新的實例對象:p1刷袍。

而原型方法上的this就好理解多了翩隧,根據(jù)上邊對函數(shù)中this的定義,p1.getName()中的getName為調(diào)用者做个,他被p1所擁有鸽心,因此getName中的this,也是指向了p1居暖。

好啦顽频,我所知道的,關(guān)于this的一切太闺,已經(jīng)總結(jié)完了糯景,希望大家在閱讀之后,能夠真正學(xué)到東西省骂,然后給我點個贊蟀淮!

下一篇:前端基礎(chǔ)進階(八):在chrome開發(fā)者工具中觀察函數(shù)調(diào)用棧、作用域鏈與閉包
上一篇:前端基礎(chǔ)進階(六):setTimeout與循環(huán)閉包面試題詳解
前端基礎(chǔ)進階目錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钞澳,一起剝皮案震驚了整個濱河市怠惶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌轧粟,老刑警劉巖策治,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脓魏,死亡現(xiàn)場離奇詭異,居然都是意外死亡通惫,警方通過查閱死者的電腦和手機茂翔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來履腋,“玉大人珊燎,你說我怎么就攤上這事∽窈” “怎么了悔政?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長延旧。 經(jīng)常有香客問我卓箫,道長,這世上最難降的妖魔是什么垄潮? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮闷盔,結(jié)果婚禮上弯洗,老公的妹妹穿的比我還像新娘。我一直安慰自己逢勾,他們只是感情好牡整,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著溺拱,像睡著了一般逃贝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上迫摔,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天沐扳,我揣著相機與錄音,去河邊找鬼句占。 笑死沪摄,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的纱烘。 我是一名探鬼主播杨拐,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼擂啥!你這毒婦竟也來了哄陶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤哺壶,失蹤者是張志新(化名)和其女友劉穎屋吨,沒想到半個月后蜒谤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡离赫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年芭逝,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渊胸。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡旬盯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出翎猛,到底是詐尸還是另有隱情胖翰,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布切厘,位于F島的核電站萨咳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏疫稿。R本人自食惡果不足惜培他,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望遗座。 院中可真熱鬧舀凛,春花似錦、人聲如沸途蒋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽号坡。三九已至懊烤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宽堆,已是汗流浹背腌紧。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留日麸,地道東北人寄啼。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像代箭,于是被迫代替她去往敵國和親墩划。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容