解析javascript中的this

陌上寒發表於2019-01-23

我們幾乎每天都在和this打交道,先不去管他的概念,看一個小栗子,預熱一下

this初探

⚠️約定:
因為使用let和const宣告的變數沒有掛載到全域性變數下,所以在全域性下宣告的變數我們使用var

var heroName = '黃蓉'
function hero() {
  const heroName = "黃藥師"
  console.log(this)//window
  console.log(this.heroName);//=>黃蓉
}

hero()
複製程式碼

我們執行hero(),通過列印我們發現

this指向的是window全域性,所以this.heroName,應當在全域性下查詢heroName,所以輸出=>黃蓉
當全域性下沒有heroName呢?this指向的是全域性,全域性沒有就是沒有,那就是undefined

function hero() {
  const heroName = "黃藥師"
  console.log(this)//window
  console.log(this.heroName);//=>undefined
}

hero()
複製程式碼

我們繼續看一個栗子

var heroName = '黃蓉'
function hero() {
  const heroName = "黃藥師"
  console.log(this)//oHero
  console.log(this.heroName);//=>歐陽鋒
}
const oHero={
  heroName:'歐陽鋒',
  hero
}
oHero.hero()
複製程式碼

提示:在宣告oHero中的hero中,使用了ES6標準,當key和value同名是,可以像上面這樣簡寫
上面的栗子中,呼叫hero()的環境變了 ,根據輸出我們發現,this的指向變了
this的指向是oHero物件,所以this.heroName=>歐陽鋒,同樣的當oHero下沒有heroName,毫無懸念,就會輸出=>undefined
如果前面兩個小栗子搞懂了,我們就繼續
一個栗子

var heroName = '黃蓉'

function hero() {
  console.log(this.heroName);
}
const oHero1 = {
  heroName:'郭靖',
  hero
};

hero()//=>黃蓉
oHero1.hero()//=>郭靖
複製程式碼

現在我們嘗試著給this下一個定義:

this指的是函式執行時所在的環境,如果一個函式內部有this,this就會指向一個物件,指向哪個物件呢?取決於這個函式的執行環境。 補充(如果在全域性下呼叫這個函式,則this指向全域性,如果在某個物件下呼叫this,則this指向這個物件)

再看一個栗子

var heroName = '黃蓉'
function hero1() {
  const heroName = "黃藥師"
  this.hero2()
}
function hero2() {
  console.log(this.heroName);
}
hero1()
複製程式碼

先不關心輸出 我們在hero2中的console.log前加一個debugger,分別看下兩個this的指向

解析javascript中的this

解析javascript中的this

可以發現。兩個this均指向window 在腦中跑一遍這段程式碼,希望你還清醒 我們一起捋一下: 全域性下執行hero1() 在hero1中,this指向的是全域性=>window, 全域性下存在hero2(), 在hero2中,this指向哪裡?取決於是哪裡呼叫的hero2(),是在hero1()中呼叫的hero2(), 所以hero2中的this指向hero1?輸出=>黃藥師? 但是我們通過debugger得出,hero2中的this指向的是window,所以輸出必然是=>黃蓉 為什麼呢? 我們再看一個栗子

function hero(){
  console.log(this.heroName);
}
var HERO1 = {
  heroName : '黃蓉',
  hero
}
var HERO2 = {
  heroName : '郭靖',
  HERO1
}
var HERO3 = {
  heroName : '郭靖',
  HERO2
}
HERO3.HERO2.HERO1.hero()//=>黃蓉
複製程式碼

我相信不會有人寫出這樣的程式碼,除了我! 這依然是個呼叫鏈 我們在輸出前打一個斷點

解析javascript中的this
this指向了誰?不是最初的呼叫方HERO3,而是上一次,或者所最後一次=>HERO1 出現上面這種情況,是由函式呼叫方式決定的

  1. 作為一個函式進行的呼叫
  2. 作為一個物件的方法進行的呼叫
  3. 作為構造器進行的呼叫
  4. 通過apply()、call()、bind()函式進行的呼叫

作為一個函式呼叫this指向window,作為一個物件的方法呼叫,this指向當前呼叫的物件

作為一個函式進行呼叫

回顧之前的栗子

var heroName = '黃蓉'

function hero() {
  console.log(this.heroName);
}
const oHero1 = {
  heroName:'郭靖',
  hero
};

hero()//=>黃蓉
oHero1.hero()//=>郭靖
複製程式碼

hero(),是作為一個函式進行呼叫的,所以this指向window oHero1.hero(), 是作為一個函式的方法呼叫的,所以this指向oHero1物件

作為一個物件的方法進行呼叫

繼續看一個栗子

function hero1(){
  this.hero2()
}
function hero2(){
  console.log(this.heroName);//Uncaught TypeError: this.hero2 is not a function
}
var HERO1 = {
  heroName : '黃蓉',
  hero1
}
var HERO2 = {
  heroName : '郭靖',
  HERO1
}
HERO2.HERO1.hero1()
複製程式碼

報錯了

HERO1物件下呼叫的hero1(),所以hero1()中this指向的是HERO1,但是HERO1中不存在方法hero2(),所以報錯 稍作修改

function hero1(){
  HERO3.hero2()
}
function hero2(){
  console.log(this.heroName);//=>郭靖
}
var HERO1 = {
  heroName : '黃蓉',
  hero1
}
var HERO2 = {
  heroName : '郭靖',
  HERO1
}
var HERO3 = {
  heroName : '郭靖',
  hero2
}
HERO2.HERO1.hero1()
複製程式碼

我們似乎總結出來一點小竅門,我們從最後的輸出反推執行順序 是誰呼叫的hero2(),是HERO3,所以hero2,中的this指向HERO3,所以輸出是=>郭靖

上面說了前兩種情況,其實(1)是(2)的一種特殊情況, 即當作為一個函式呼叫的時候,就是作為window物件的一個方法進行呼叫

一個小栗子進行簡單回顧

var heroName = "郭靖";

function hero1() {
  console.log(this.heroName);
}
var HERO1 = {
  heroName: "黃蓉",
  hero1:hero1
};
HERO1.hero1();//=>黃蓉
hero1();//=>郭靖
複製程式碼

都是執行hero1()方法,同樣都是輸出this.heroName,卻得到了不同的結果

this的指向是在函式執行的時候定義的,而不是在函式建立時定義的

基於上面的栗子,我們再看一個栗子

var heroObj = {
  heroName: "郭靖",
  heroFoo: {
    heroName: "黃蓉",
    hero: function() {
      console.log(this.heroName); 
    }
  }
};

heroObj.heroFoo.hero();//=>黃蓉

var h = heroObj.heroFoo.hero;
h();//=>undefined
複製程式碼

為了直觀表達我們做一下改動

//部分程式碼省略
window.heroObj.heroFoo.hero();//=>黃蓉

var h = heroObj.heroFoo.hero;
window.h();//=>undefined
複製程式碼

輸出的結果是相同的,我要表達的內容也很就很直觀明顯了, 這些物件和方法,最終都會掛載到window物件下,我們看這一句

window.heroObj.heroFoo.hero();//=>黃蓉
複製程式碼

hero()中的this是指向呼叫它的物件,那是哪個物件呼叫的hero()呢?

這是個問題

這是個問題?

這不是個問題

this指向的是最後呼叫它的物件

上面的栗子

window.heroObj.heroFoo.hero();//=>黃蓉
複製程式碼

最後呼叫hero()的是heroFoo,所以hero()中this指向了heroFoo物件

var h = heroObj.heroFoo.hero;
window.h();//=>undefined
複製程式碼

最後呼叫h()的是window物件,所以hero()中this指向了window物件

綜上

this的指向是在函式執行的時候定義的,而不是在函式建立時定義的,this指向的是最後呼叫它的物件

下面討論剩下的兩種情況

作為構造器進行呼叫

之前在介紹物件導向的時候,談一談javascript物件導向,討論過,使用建構函式來建立物件

function Hero(name, nickname, skill) {
  this.name = name;
  this.nickname = nickname;
  this.doSth = function() {
   return skill
  };
}
const hero = new Hero("黃藥師", "東邪", "碧海潮生曲");
console.log(hero);
複製程式碼

使用自定義構造器構造一個物件需要四步

  1. 建立一個新物件
const hero = new Hero("黃藥師", "東邪", "碧海潮生曲");
複製程式碼
  1. 將建構函式的作用域賦給新物件,設定原型鏈(因此this就指向了這個新物件hero)
hero.__proto__=Hero.prototype;
複製程式碼
  1. 執行建構函式Hero中的程式碼(為這個新物件hero新增屬性和方法)
  2. 返回新物件hero

說this指向hero,Hero內所有針對this的操作,都會發生在hero上面

console.log(hero.name);//=>黃藥師
console.log(hero.nickname);//=>東邪
console.log(hero.doSth());//=>碧海潮生曲
複製程式碼

建構函式是不需要return的, return在普通函式中也不是必須存在的,我們知道,在普通函式中,如果沒有手動return ,會預設return undefined 但是如果在建構函式中使用了return,會存在一些坑 我們一起來填坑 我們把上面的栗子進行簡化

function Hero(name) {
  this.heroName = name;
  return 123
}
const hero = new Hero("郭靖");
console.log(hero.heroName);//=>郭靖
複製程式碼
function Hero(name) {
  this.heroName = name;
  return null
}
const hero = new Hero("郭靖");
console.log(hero.heroName);//=>郭靖
複製程式碼
function Hero(name) {
  this.heroName = name;
  return undefined
}
const hero = new Hero("郭靖");
console.log(hero.heroName);//=>郭靖
複製程式碼

不再進行一一列舉,上面return的都是基本資料型別 繼續看 陣列型別

function Hero(name) {
  this.heroName = name;
  return []
}
const hero = new Hero("郭靖");
console.log(hero.heroName);//=>undefined
複製程式碼

object型別

function Hero(name) {
  this.heroName = name;
   return {
          heroName: "黃蓉"
        };
}
const hero = new Hero("郭靖");
console.log(hero.heroName);//=>黃蓉
複製程式碼

funciton型別

function Hero(name) {
  this.heroName = name;
   return  function(){}
}
const hero = new Hero("郭靖");
console.log(hero.heroName);//=>undefined
複製程式碼

funciton升級

function Hero(name) {
  this.heroName = name;
  return (function() {
    return  {heroName:'黃蓉'}
  })()
}
const hero = new Hero("郭靖");
console.log(hero.heroName);//=>黃蓉
複製程式碼

總結一下 建構函式會改版this的指向,指向通過new例項化出來的物件 建構函式中不需要return,當存在return時,以下幾點需要注意

  1. 當return的是基本資料型別時,返回不變
  2. 當return的沒有返回時,預設返回undefined,返回不變
  3. 當return的是引用型別時,返回的是這引用型別

通過apply()、call()、bind()進行呼叫

之前在講物件導向的時候,對call進行過討論,javascript 物件導向之一篇文章搞定call()方法 它們兩個也是用來改變this的指向的,

為什麼要改變this的指向呢?

call

看一個栗子

const hero1 = {
    name: "歐陽鋒",
    doSth: function (skill) {
        console.log(`${this.name}學習${skill}`);
    }
};

const hero2 = {
    name: "洪七公"
};
hero1.doSth('九陰真經')//=>歐陽鋒學習九陰真經
複製程式碼

執行了hero1物件下面的doSth方法,並傳入引數“九陰真經”,最後輸出=>歐陽鋒學習九陰真經 結合上面的介紹講解,我們知道,doSth中的this是指向的hero1,這個沒問題吧

好了,現在hero2下面有一個“洪七公”,“洪七公”也想呼叫hero1下面的doSth方法,怎麼辦呢?

const hero1 = {
    name: "歐陽鋒",
    doSth: function (skill) {
        console.log(`${this.name}學習${skill}`);
    }
};

const hero2 = {
    name: "洪七公"
};
hero1.doSth.call(hero2, "降龍十八掌");//=>洪七公學習降龍十八掌
複製程式碼

doSth是由hero1呼叫的,預設情況下doSth中的this指向的是hero1,但是使用了call,所以,this的指向變了,指向了call方法中的第一個引數hero2,

apply

apply呢?apply和call很是類似

const hero1 = {
  name: "歐陽鋒",
  doSth: function(skill, favourite) {
    console.log(`${this.name}學習${skill}`);
    console.log(`${this.name}喜歡${favourite}`);
  }
};

const hero2 = {
  name: "洪七公"
};
hero1.doSth.apply(hero2, ["降龍十八掌", "吃雞"]);
hero1.doSth.call(hero2, "降龍十八掌", "吃雞");
複製程式碼

這兩種寫法等效,只是傳入的引數格式略有不同

hero1.doSth.apply(hero2, ["降龍十八掌", "吃雞"]);
hero1.doSth.call(hero2, "降龍十八掌", "吃雞");
複製程式碼

bind

bind也可以改變this的指向,在用法上和call和apply略有不同,bind的使用更加靈活

const hero1 = {
	name: "歐陽鋒",
	doSth: function(skill, favourite) {
		console.log(`${this.name}學習${skill}`);
		console.log(`${this.name}喜歡${favourite}`);
	}
};

const hero2 = {
	name: "洪七公"
};
const foo = hero1.doSth.bind(hero2, "降龍十八掌", "吃雞");
foo();
複製程式碼

之前說的call和apply都是立即執行,而bind不會立即執行,需要手動執行,所以bind的使用更加靈活 不止於此 上面的foo方法在呼叫的時候可以額外的傳入引數

const hero1 = {
  name: "歐陽鋒",
  doSth: function(skill, favourite, q, n, b) {
    console.log(`${this.name}學習${skill}`);//=>洪七公學習降龍十八掌
    console.log(`${this.name}喜歡${favourite}`);//=>洪七公喜歡吃雞
    console.log(q, n, b);//=>1 2 3
  }
};

const hero2 = {
  name: "洪七公"
};
const foo = hero1.doSth.bind(hero2, "降龍十八掌", "吃雞");
foo(1, 2, 3);
複製程式碼

我是陌上寒,我們一起學前端

解析javascript中的this

END

相關文章