js 中 this,apply,call,bind 詳解

JavaScript 中關於 this、apply、call、bind 的介紹文章已經有不少了,看的越多,越會一頭霧水。下面從常見的應用場景來講明上述這些概念可能會容易理解一些。面試

this

this 表明函數運行時的環境。注意是函數,是函數,還有就是運行時!數組

function hello(){
    console.log(this) // window
}
hello()
複製代碼

當 hello 函數執行時,在函數內部有個變量 this,表明這個函數運行環境,上述函數在全局環境下且在瀏覽器環境下運行,那麼這個 this 就指向 window,這個比較好理解。可是許多如今許多 js 框架的設計都是使用構造函數方式設計的,例以下面構造函數:瀏覽器

function Dog(name,color){
    this.name = name;
    this.color = color;
    console.log(this)
}
var a = new Dog("阿黃","黃色")
複製代碼

構造函數首先函數名稱首字母都會大寫,其次都會使用一個 new 來構造一個實例,new 的時候 構造函數就會運行一次,這個時候構造函數裏的this就指向這個實例對象bash

上述構造函數只是實現了一些屬性,其實更多的時候構造函數內部應該根據傳入的值來實現一些方法,從而體現良好的封裝性。例如使得上述狗類增長一個方法:閉包

function Dog(name,color){
  this.color = color;
  this.name = name;
  this.say = function(){
    console.log(`I am a ${this.color} dog ,my name is ${this.name}`)
  }
}

let ahuang = new Dog("阿黃","黃色")
ahuang.say() // I am a 黃色 dog ,my name is 阿黃

複製代碼

首先先實例出來一個對象 ahuang ,再調用 ahuang 的 say 方法,say 方法內部能夠獲取到 color 和 name.app

如今稍做改動:框架

function Dog(name,color){
  this.color = color;
  this.name = name;
  this.say = function(){
    console.log(`I am a ${this.color} dog ,my name is ${this.name}`)
  }
}

let ahuang = new Dog("阿黃","黃色")
let a = ahuang.say
a() // I am a undefined dog ,my name is 

複製代碼

a 變量只是獲取了ahuang的 say 方法定義,尚未執行,此時的 a 定於在全局,若是直接這樣執行,那麼say方法的this天然也就指向了全局的 window 了,因爲全局上沒有color和name,因此就成 undefined了。函數

其實個人理解是:函數一個工具和機器,只負責執行,能夠理解爲函數獨立於對象,猶如一個榨汁機,若是放入橙子,運行時天然就會榨出橙汁,若是放入蘋果,天然就會榨出蘋果汁。這裏蘋果和橙子就是運行環境。工具

對於給定什麼環境運行函數,就可能產生不一樣結果,這樣雖然比較自由,但過度自由也會致使一些問題產生,例如上述 Dog 的 say 方法:ui

this.say = function(){
    console.log(`I am a ${this.color} dog ,my name is ${this.name}`)
 }
複製代碼

顯然這個方法只有運行於狗這個對象纔有意義,放在全局雖然也能運行,但卻失去了這個方法的意義。 因此須要一種機制可以始終使得這個方法可以在狗對象中運行。

方案一:採用箭頭函數來定義
箭頭函數綁定了運行環境,即定義在哪一個對象內,則這個箭頭函數內部 this 始終爲這個對象。

function Dog(name,color){
  this.color = color;
  this.name = name;
  this.say = ()=> {
    console.log(`I am a ${this.color} dog ,my name is ${this.name}`)
  }
}

let ahuang = new Dog("阿黃","黃色")
let a = ahuang.say
a() // I am a 黃色 dog ,my name is 阿黃
複製代碼

上述函數,即便a在全局定義,可是 say 是箭頭函數定義的,裏面 this 依然指向 ahuang.

方案二 使用 call 或者 apply 來實現。

call 和 apply

call和 apply 方法功能是同樣的,只是傳入的參數形式不同,做用都是綁定(劫持)一個特定的執行環境:

func.call(this, arg1, arg2,...);
func.apply(this, [arg1, arg2])
複製代碼

call 第一個參數爲執行環境,其他參數爲 func 的參數,能夠有無數個參數,而apply只有兩個參數,第一個爲執行環境,第二個爲其他數組,經過一個數組來傳遞。

例如上述狗的構造函數也可使用call來實現箭頭函數效果:

function Dog(name,color){
  this.color = color;
  this.name = name;
  this.say = function(){
    console.log(`I am a ${this.color} dog ,my name is ${this.name}`)
  }
}
let ahuang = new Dog("阿黃","黃色")
let b = ahuang.say
b.call(ah
複製代碼

使用call 和 apply 還有一些經常使用的應用。

1.求數組最大值

let arr = [1,2,3]
// 方法一:
Math.max(...arr)
// 方法二
Math.max.apply(Math,arr) // 巧妙地利用了第二個參數爲數組特徵
複製代碼

2.判斷數組

function isArray(obj){ 
    return Object.prototype.toString.call(obj) === '[object Array]' ;
}
複製代碼

bind

bind 和 call,apply 也有類似之處,bind()方法會建立一個新函數,稱爲綁定函數,當調用這個綁定函數時,綁定函數會以建立它時傳入 bind()方法的第一個參數做爲 this,傳入 bind() 方法的第二個以及之後的參數加上綁定函數運行時自己的參數按照順序做爲原函數的參數來調用原函數。

function Dog(name,color){
  this.color = color;
  this.name = name;
  this.say = function(){
    console.log(`I am a ${this.color} dog ,my name is ${this.name}`)
  }
}
let ahuang = new Dog("阿黃","黃色")
let b = ahuang.say.bind(ahuang) // 使用bind後b返回的是一個函數,這個函數內的this永遠是ahuang了
b() // I am a 黃色 dog ,my name is 阿黃

複製代碼

可見 bind 綁定執行環境是靜態的,關鍵是在定義時就能夠綁定,而不是像 call 那樣須要調用時去綁定.有點相似於箭頭函數。

bind 通常有幾個經典的應用場景:

[1,2,3].forEach(function(){
    console.log(this) // window
})
複製代碼

匿名的回調函數裏面 this 通常爲指向 window 假如咱們要在這個函數內用到一些其餘對象的值,則能夠經過bind來改變回調函數的值,以Vue框架爲例:

let vm = new Vue({
  data(){
    return {
      height:768
    }
  },
  mounted(){
    window.addEventListener("resize",(function(){
      this.height = document.body.clientHeight
    }).bind(this))
  }
})
複製代碼

若是不綁定this,則回調函數內this爲 window ,顯然讀取不到this.height,固然還可使用箭頭函數或者聲明一箇中間變量來解決:

mounted(){
    window.addEventListener("resize",()=>{
      this.height = document.body.clientHeight
    })
    // 或者
    let That =this
     window.addEventListener("resize", function(){
      That.height = document.body.clientHeight
    })
  }
複製代碼

因此在 Vue 框架內建議多用箭頭函數來定義,forEach,map等方法也是如此!

實現原理

不少面試時候可能會問到 call 和 bind 實現原理,並手寫一個。其實這並不難

call

以上述榨汁機的解釋,例若有個榨汁機如今在榨橙汁,如今咱們想讓榨汁機在蘋果的環境中運行。

榨汁機.call("蘋果")
複製代碼

調用 call 時會傳入蘋果對象,咱們須要在蘋果對象上增長榨汁機的方法,再執行一次,執行完畢後再把原屬於榨汁機的方法給刪掉便可!雖然有點牽強,但實際就是這麼幹的。

Function.prototype.myCall = function(){
  let args = [...arguments]
  let ctx = args.length>0 ? args.shift() : window
  let s = Symbol() // 生成一個惟一值
  // 在被劫持者對象屬性中加入這個屬性
  ctx[s] = this
  let result = ctx[s](...args)
  delete ctx[s]
  return result
}
複製代碼

bind

bind 返回的是一個函數,或者說閉包

Function.prototype.myBind = function(){
  let args = [...arguments]
  let ctx = args.length>0 ? args.shift() : window
  let s = Symbol() // 生成一個惟一值
  // 在被劫持者對象屬性中加入這個屬性
  ctx[s] = this
  return function(){
    let res = ctx[s](...args) // 定義了在特定的上下文運行的結果,當執行時就能獲得這個特定上下文的結果。
    delete ctx[s]
    return res
  }
}
複製代碼

完!

相關文章
相關標籤/搜索