js call、apply、bind的實現

Ace7523發表於2019-01-21

js call、apply、bind的實現
三個函式其實是老生常談了,網上也有太多關於他們的實現。開始只是理解別人的實現,其實是似懂非懂。只有自己實現出來,並且逐行理解,才是真的懂了。 文章從三個函式的使用入手,結合使用場景逐步實現。

call的使用

window.name = 'window'
var obj = {
 name: 'obj'
}
function getName(p1, p2) {
 console.log(p1, p2)
 console.log(this.name)
}
getName('str1', 'str2') 
getName.call(obj, 'str1', 'str2') 

// 函式執行結果可以思考一下。
複製程式碼

怎麼記住call呢,其實就是getName這個方法執行了,不是window去執行,是call括號內的引數去執行。

可以聯想到一個很典型的使用場景 [].prototype.shift.call(arguments),起初很不理解這種寫法,後來一想,其實就是 arguments 不是陣列,它沒有shift方法可以直接來用,就把陣列的shift方法拿來用。

call的實現

1 明確是誰呼叫call,答案,是函式。

2 call接收的引數是什麼?第一個引數是要改變的this指標,也就是上面說到了,是誰去執行這個函式。若無指定,預設為window

3 call接收的第二個,第三個,等等,引數,是用來做什麼的?答,就是作為呼叫call的那個函式所需的引數。

function myCall(context) {
  // 1
  if (typeof this !== 'function'){
	throw new TypeError('error')
  }
  // 2
  context = context || window
  // 3
  context.fn = this
  // 4
  const args = [...arguments].slice(1)
  // 5
  const result = context.fn(...args)
  // 6
  delete context.fn
  return result
}
Function.prototype.myCall = myCall

getName.myCall(obj, 'str1', 'str2')
複製程式碼
  • 1 在myCall方法實現體中的this是什麼? 回想一下,誰呼叫函式,this就是誰。那誰來呼叫call方法呢,是函式,所以這個this,就是呼叫call方法的函式,對於本例子來說,就是getName。如果不是函式,直接報錯。
  • 2 myCall方法如果沒有引數,那麼預設為window
  • 3 本例中的context是傳進來的obj物件,給這個物件新增一個方法,這個方法,就是this,也就是getName
  • 4 獲取引數 ,在本例中,對應的是 'str1', 'str2'
  • 5 結果很明確了,就是 fn是方法,是getName。context是傳進來的obj物件,那麼就是實現了,obj呼叫getName方法,引數為'str1', 'str2'。(誰呼叫方法,this就指向誰)
  • 6 刪除物件上函式,返回結果

apply的使用

apply使用與call大體一致,只是接受引數的方法不同。call可以接收多個引數。apply接收的第一個引數是this,第二個引數是 所需引數所組成的陣列

window.name = 'window'
var obj = {
 name: 'obj'
}
function getName(p1, p2) {
  console.log(p1, p2)
  console.log(this.name)
}
getName('str1', 'str2') 
getName.apply(obj, ['str1', 'str2'])
複製程式碼

apply的實現

Function.prototype.myApply = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  var result
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}
複製程式碼

和上述的call實現基本類似,就引數處理有些不同,不再贅述。

bind的使用

為什麼有call apply後還要有個bind ? 當我們需要繫結一個點選事件的時候,就改變回撥函式的this,怎麼破?因為 call apply都是立即執行了,所以bind登場。看一下下面這個例子吧。

var obj = {
  name: 'obj'
}

document.addEventListener('click',myClick.bind(obj,'p1','p2'),false);

function myClick(p1,p2){
  console.log(this.name, p1, p2)
}
複製程式碼

MDN的解釋是:bind()方法會建立一個新函式,稱為繫結函式,當呼叫這個繫結函式時,繫結函式會以建立它時傳入 bind()方法的第一個引數作為 this,傳入 bind() 方法的第二個以及以後的引數加上繫結函式執行時本身的引數按照順序作為原函式的引數來呼叫原函式。

注意:bind方法的返回值是函式

bind的實現

bind()最簡單的用法是建立一個函式,使這個函式不論怎麼呼叫都有同樣的this值。

因為bind返回值是函式,那麼函式除了直接執行之外,還可以作為建構函式放在new 操作符之後,所以bind的實現就要把這種情況考慮進去。

前置知識點 new

任何函式都可以作為建構函式,放在 new 之後使用,那麼new的過程是怎麼樣的呢?大體分為以下幾步,具體不深究。

  1. 生成空物件
  2. 空物件的原型屬性指向建構函式的原型物件
  3. 給這個空物件新增屬性
  4. 返回這個物件。

上面所說的那個空物件就是建構函式內部的this,並且 對於 new 的情況來說,不會被任何方式改變 this

window.name = 'window'
var obj = {
  name: 'obj'
}
function Fun(p1,p2){
  console.log(this)
  console.log(this.__proto__ === Fun.prototype)
  console.log(this.name)
  this.a = p1
  this.b = p2
  console.log(this)
}
var c = new Fun('str1', 'str2')
console.log(c)
複製程式碼

執行結果如下:

js call、apply、bind的實現

再來看一下,直接執行Fun的返回結果,程式碼不做修改,直接執行 Fun('str1', 'str2')

js call、apply、bind的實現

兩次結果的不同,本文不展開敘述。那麼舉這個例子是想說明什麼呢? 答 : 函式充當建構函式和普通函式,執行時內部的this指向不同。 接下來看bind函式的模擬實現

Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
const _this = this
const args = [...arguments].slice(1)
// 返回函式
return function F() {
  // 1 判斷是否用作建構函式
  if (this instanceof F) {
    return new _this(...args, ...arguments)
  }
  // 2 用作普通函式
  return _this.apply(context, args.concat(...arguments))
 }
}
// 還是用上述舉例子
window.name = 'window'
var obj = {
  name: 'obj'
}
function Fun(p1, p2){
  this.a = p1
  this.b = p2
  console.log(this.name)
  console.log(p1, p2)
}
var f1 = Fun.bind(obj, 'str1')
f1('str2')
複製程式碼

執行結果如下,可見 改變了fn1函式的this指向

js call、apply、bind的實現

再來看一下 ,去掉 f1('str2') ,換成如下語句的執行結果

// f1('str2')
var b = new f1('str2')
console.log(b)
複製程式碼

js call、apply、bind的實現

沒有逐行的說明,不過對於內部實現,有new的前置介紹和註釋,相信開始的call實現程式碼都看懂了的話,這個bind方法也會一目瞭然

相關文章