一文了解this指向

this的中文意思是這,在javascript中指的是當前執行代碼的環境對象.在非嚴格模式下,總是指向一個對象,在嚴格模式下可以是任意值.相信很多同學在看到這個this的時候,肯定是有點腦殼疼的.所以今天我就寫了一篇有關this的小文章,來梳理梳理有關this的幾種用法,希望對大家都能有所幫助

事件調用環境

誰觸發事件,函數裏面的this就指向誰

<button id="btn1">click me</button>
let btn1 = document.querySelector('#btn1')
function fn(){
  console.log(this)
}
btn1.onclick = fn

上面的代碼打印出button按鈕這個對象

全局環境

首先我們看下在全局環境下this指向誰?

console.log(this)

我們把上面的代碼放到瀏覽器環境下執行,結果是Window對象
再打開終端Terminal,鍵入node指令,進入node執行環境,結果是global
新建一個index.js文件,使用node index.js運行腳本,這裏的this指的是node的默認導出對象,我們執行如下代碼

console.log(this)  // {}
console.log(module.exports)  // {}
console.log(module.exports === this)  // true

函數環境

單純函數調用

function fn(){
  console.log(this)
}
fn()  // Window

但是在嚴格模式下,this指向的就是undefined

'use strict'
function fn(){
  console.log(this)
}
fn()  // undefined

可以看出單純的調用函數時,this指向的就是全局對象

對象方法調用

let obj = {
  a:1,
  fn:function(){
    console.log(this)
  }
}
obj.fn()  // {a: 1, fn: ƒ}

直接通過對象.方法的形式調用函數的時候,this指向的是調用這個方法的對象,即上面的obj對象
我們稍微改動一下上面的代碼,讓這個函數在對象中再深入一層,通過對象.屬性.方法的形式去調用函數

let obj = {
  a:1,
  b:{
    fn:function(){
      console.log(this)
    }
  }
}
obj.b.fn()  // {fn: ƒ}

可以看到輸出的結果是{fn: f},即b,可以得出結論: this指向的是最終調用它的對象.當函數被多層對象所包含,且函數被最外層對象調用,this指向的也只是它的上一級對象
我們再來修改下代碼,如下:

let obj = {
  a:1,
  fn:function(){
    console.log(this)
  }
}
let fn2 = obj.fn

fn2()  // Window

可以看出執行結果是Window對象,是不是覺得奇怪,這是爲什麼呢?
這是因爲當我們進行let fn2 = obj.fn這步賦值操作的時候,我們將obj.fn這個函數的內存地址賦值給了fn2這個變量,而obj.fn這個函數乾的事情就是console.log(this),現在我們執行的是fn2(),調用這個fn2函數的是Window對象.那你說,window調用了一個函數,這個函數的任務就是打印出誰調用了它,那答案不是顯而易見嘛

這次,我們還要來修改下代碼,如下

function fn2(){
  console.log(this)
}
let obj = {
  a:1,
  fn:fn2
}
obj.fn()  // {a: 1, fn: ƒ}

要解析這段代碼的思路,其實和上面那段代碼是一樣的.obj調用fn函數,而fn函數指向的是fn2,我們可以理解其實就是obj調用fn2函數,所以執行的結果就是打印出obj

這次,我們還要來修改下代碼,如下

let obj = {
  a:1,
  fn:function(){
    setTimeout(function(){
      console.log(this)
    })
  }
}
obj.fn()  // Window

那麼,這又是爲何呢?我們知道setTimeout是定時器,作用是在一段時間之後執行,setTimeout()這個函數的()中的是它的參數,也就是說function(){console.log(this)}這個函數其實是被當作一個參數傳入setTimeout當中的.這個其實有個隱式的操作就是將function(){console.log(this)}賦值給一個假想函數f,到這裏的時候,其實已經和obj這個對象無關了.然後等待定時器的時間到了以後,執行的就是f這個函數.這個f是被當作普通函數直接調用的,所以this指向了Window對象.我們再這麼一想,function(){console.log(this)}這是個匿名函數啊,它都沒有名字,是不能被普通對象調用的,但是Window可以調用啊.
那我們現在要是想要用console.log(this)打印出obj這個對象該怎麼辦呢?有兩個方案:

  • 保存this變量
  • 箭頭函數

方法1:

let obj = {
  a:1,
  fn:function(){
    let _self = this // 在這裏保存好this,免得它到時候跑了
    setTimeout(function(){
      console.log(_self)
    })
  }
}
obj.fn()  // {a: 1, fn: ƒ}

方法2:

let obj = {
  a:1,
  fn:function(){
    setTimeout(() => {
      console.log(this)
    })
  }
}
obj.fn()  // {a: 1, fn: ƒ}

我們使用箭頭函數也是可以達到同樣的效果的,至於原因我們後面會再提到,這裏就先跳過去了
但是setTimeout其實還有一個點容易忽略,我們之前說過在嚴格模式下直接調用一個函數,它的this指向undefined.但是在setTimeout方法中傳入函數的時候,如果這個函數沒有指定this的話,它會有自動注入全局上下文,類似於xxx.call(window)這樣的操作.看下面代碼

function fn(){
  'use strict'
  console.log(this)  
}
setTimeout(fn)  // Window

當然,如果我們在setTimeout中傳入函數的時候綁定了this的話,那就不會被注入全局對象

function fn(){
  'use strict'
  console.log(this)  
}
setTimeout(fn.bind({}))  // {}

構造函數調用

構造函數大家都應該比較熟悉了吧,我們new一下,就new出一個新對象的那種.其實構造函數就是普通的函數,只不過在調用的時候前面加了new運算符.關於new運算符是幹嘛的,可以看我的另外一篇文章:JS new的時候幹了啥

function Person(){
  console.log(this)
}
let p = new Person()  // Person {}

當我們使用構造函數調用的時候,this指向了這個實例化出來的對象,我們將上面的代碼和下面的代碼進行一個比對就能看出不同了

function Person(){
  console.log(this)
}
let p = Person()  // Window

兩段代碼的唯一區別就是後面的代碼沒有new它,從而指向了Window對象.由此可見,使用了new之後,這個構造函數的this被綁定到了正在構造的新對象上.這個在 JS new的時候幹了啥 也是有講到的,不太清楚的童鞋可以跳過去看一看
我們再來一個例子鞏固下

function Person(){
  return this
}
let p = Person()
console.log(p)  // Window
console.log(p === window)  // true

不使用new關鍵字,Person普通函數返回this,函數又是被Window調用的,所以就是返回Window對象,那麼p === window返回的結果自然就是true
但是在構造函數中,如果顯式的返回了一個新的對象(非null),那麼this就會指向那個對象

function Person(name){
  this.name = name
  return {
    name:'lisi'
  }
}
let p = new Person('zhangsan')
console.log(p.name)  // lisi

call,apply,bind 三兄弟

這三兄弟都是Function這個對象原型上的方法.它們可以更改函數中的this指向.

let obj = {}
function fn(){
  console.log(this)
}
fn()  // Window
fn.call(obj)  // {}
fn.apply(obj)  // {}
fn.bind(obj)()  // {}

這裏可以看出它們三兄弟確實都可以改變this的指向,那麼它們的區別在哪裏呢?
callapply的作用基本一致,區別在於傳參的方式不太一樣

let obj = {}
function fn(){
  console.log(arguments)
}
fn.call(obj,1,2)  // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
fn.apply(obj,[1,2])  // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]

call的參數是一個個傳進去的,apply的參數是直接傳了一個數組進去
那麼callbind又有什麼區別呢?

let obj = {}
function fn(){
  console.log(arguments)
}
fn.bind(obj)(1,2)  // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
fn.bind(obj,1,2)()  // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]

可以看出來,綁定bind的時候是不會直接調用函數的,它會返回一個新的函數,我們需要主動去()一下,它纔會執行.相對比之下,callapply都是會立即執行函數的.從第二個參數開始傳入的都是執行函數時需要傳入的參數,callbind傳參的格式一致.另外,在構造函數調用的時候,內部其實也是有通過call來變更this指向的.這個前面我們也說到了,指向了創建出來的對象.
使用bind方法創建的上下文爲永久性的上下文環境,沒法更改,callapply也不行.

var a = 0
let obj = {
  a:1
}
function fn(){
  console.log(this.a)
}
fn()  // 0
fn.call({})  // undefined  # 通過call改變了this
fn.bind(obj)()  // 1  # 通過bind改變了this
fn.bind(obj).call({})  // 1  # 通過bind改變了this, call再想要來更改就沒門了

上面代碼中,我們是給一個普通函數指定this,下面我們來看看爲對象中的方法指定this

let obj = {
  fn:function(){
    console.log(this)
  }
}
obj.fn.call({})  // {}

換成構造函數,再來看看

function Person(){
  console.log(this)
}
let p = new Person.call({})  // Person.call is not a constructor

錯誤提示是Person.call不是一個構造函數,這是因爲此時我們去new的是Person.call而不是Person,這當然不是一個構造函數了.我們換bind再來試一試,看看結果

function Person(){
  console.log(this)
}
let P = Person.bind({a:1})
let p = new P()  // Person {}

發現結果是Person {},這說明我們的綁定沒有成功,否則結果就應該是{a:1}了.因此,我們也可以得到結論,在構造函數中,我們去new的時候,bind綁定的this是不會起效果的

箭頭函數

箭頭函數是在ES6的時候纔有的,它的語法比普通的函數表達式要簡潔,並且沒有自己的thisarguments.它並不創建自身的上下文,其上下文在定義的時候就已經確定了,即一次綁定,便不可更改.

let obj = {
  fn:() => console.log(this)
}
obj.fn()  //Window

這裏爲啥是Window而不是obj呢,箭頭函數的this在定義的時候就已經確定了.在箭頭函數中引用this,實際上調用的是定義時的上一層作用域的this.那麼上面的代碼中調用的就是Window對象了,因爲obj對象是不能形成作用域的.
我們再來看下面的代碼,結果也是Window,因爲即使fn在函數中的位置深了一層,但是仍然沒有形成作用域,箭頭函數定義的時候還是指向全局對象了

let obj = {
  a:{
    fn:() => console.log(this)
  }
}
obj.a.fn()  //Window

我們先看如下代碼:

let obj = {
  fn:function(){
    return function(){
      console.log(this)
    }
  }
}
obj.fn()()  // Window

這裏函數作爲對象的一個方法使用,裏面有個閉包.當我們通過對象.方法去調用的時候,實際上就相當於是函數直接調用.此時的this指向Window
我們改動代碼如下,將閉包的形式變成箭頭函數

let obj = {
  fn:function(){
    return () => console.log(this)
  }
}
obj.fn()()  // {fn: ƒ}

此時的輸出結果變成obj這個對象了.因爲箭頭函數在定義的時候是在fn這個函數中,所以箭頭函數中的this指向了fnthis,也就是obj.大家是否還記得前面有個地方我們留了一個懸念給大家.對了,就是定時器那裏.我們說過,使用箭頭函數也可以達到同樣的效果,現在童鞋們是否都明白了.箭頭函數的this在定義的時候就已經確定了,call,apply,bind等都無法改變它.

由於箭頭函數的外部決定了上下文以及靜態上下文等特性,因此最好不要在全局環境下使用箭頭函數來定義方法,我們建議使用函數表達式來定義函數,可確保正確的上下文環境

總結: 有關this的學習到這裏基本就結束了.有時間的同學建議將我文章中的代碼都敲一遍,加深對this的理解,達到事半功倍的效果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章