如何寫一個實用的 bind?

前言

這是underscore.js源碼分析的第五篇,若是你對這個系列感興趣,歡迎點擊javascript

underscore-analysis/ watch一下,隨時能夠看到動態更新。java

事情要從js中的this開始提及,你是否是也常常有種沒法掌控和知曉它的感受,對於初學者來講,this簡直如同回調地獄般,神乎其神,讓人沒法捉摸透。可是經過原生js中的bind方法,咱們能夠顯示綁定函數的this做用域,而無需擔憂運行時是否會改變而不符合本身的預期。固然了下劃線中的bind也是模仿它的功能一樣能夠達到相似的效果。git

ctx

bind簡單回顧

咱們從mdn上的介紹來回顧一下bind的使用方法。github

bind方法建立一個新的函數, 當被調用時,它的this關鍵字被設置爲提供的值。瀏覽器

語法app

fun.bind(thisArg[, arg1[, arg2[, ...]]])複製代碼

簡單地看一下這些參數的含義函數

  1. thisArg源碼分析

    當綁定函數被調用時,該參數會做爲原函數運行時的this指向,當使用new 操做符調用綁定函數時,該參數無效。ui

  2. arg1, arg2, ...this

    當綁定函數被調用時,這些參數將置於實參以前傳遞給被綁定的方法。

綁定this做用域示例

window.name = 'windowName'

let obj = {
  name: 'qianlongo',
  showName () {
    console.log(this.name)
  }
}

obj.showName() // qianlongo

let showName = obj.showName
  showName() // windowName

let bindShowName = obj.showName.bind(obj)
  bindShowName() // qianlongo複製代碼

經過以上簡單示例,咱們知道了第一個參數的做用就是綁定函數運行時候的this指向

第二個參數開始起使用示例

let sum = (num1, num2) => {
  console.log(num1 + num2)
}

let bindSum = sum.bind(null, 1)
bindSum(2) // 3複製代碼

bind可使一個函數擁有預設的初始參數。這些參數(若是有的話)做爲bind的第二個參數跟在this(或其餘對象)後面,以後它們會被插入到目標函數的參數列表的開始位置,傳遞給綁定函數的參數會跟在它們的後面。

參數的使用基本上明白了,咱們再來看看使用new去調用bind以後的函數是怎麼回事。

function Person (name, sex) {
  console.log(this) // Person {}
  this.name = name
  this.sex = sex
}
let obj = {
  age: 100
}
let bindPerson = Person.bind(obj, 'qianlongo')

let p = new bindPerson('boy')

console.log(p) // Person {name: "qianlongo", sex: "boy"}複製代碼

有沒有發現bindPerson內部的this再也不是bind的第一個參數obj,此時obj已經再也不起效了。

實際上bind的使用是有必定限制的,在一些低版本瀏覽器下不可用,你想不想看看下劃線中是如何實現一個兼容性好的bind呢!!!come on

下劃線中bind實現

源碼

_.bind = function(func, context) {
  // 若是原生支持bind函數,就用原生的,並將對應的參數傳進去
  if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
  // 若是傳入的func不是一個函數類型 就拋出異常
  if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
  // 把第三個參數之後的值存起來,接下來請看executeBound
  var args = slice.call(arguments, 2);
  var bound = function() {
    return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
  };
  return bound;
};複製代碼

executeBound實現

var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
  // 若是調用方式不是new func的形式就直接調用sourceFunc,而且給到對應的參數便可
  if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 
  // 處理new調用的形式
  var self = baseCreate(sourceFunc.prototype);
  var result = sourceFunc.apply(self, args);
  if (_.isObject(result)) return result;
  return self;
};複製代碼

上面的源碼都作了相應的註釋,咱們着重來看一下executeBound的實現

先看一下這些參數都表明什麼含義

  1. sourceFunc:原函數,待綁定函數
  2. boundFunc: 綁定後函數
  3. context:綁定後函數this指向的上下文
  4. callingContext:綁定後函數的執行上下文,一般就是 this
  5. args:綁定後的函數執行所需參數

ok,咱們來看一下第一句

if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);複製代碼

這句話是爲了判斷綁定後的函數是以new關鍵字被調用仍是普通的函數調用的方式,舉個例子

function Person () {
  if (!(this instanceof Person)) {
    return console.log('非new調用方式')
  }

  console.log('new 調用方式')
}

Person() // 非new調用方式
new Person() // new 調用方式複製代碼

因此若是你但願本身寫的構造函數不管是new仍是沒用new都起效的話能夠用下面的代碼

function Person (name, sex) {
  if (!(this instanceof Person)) {
    return new Person(name, sex)
  }

  this.name = name
  this.sex = sex
}

new Person('qianlongo', 'boy') // Person {name: "qianlongo", sex: "boy"}

Person('qianlongo', 'boy') // Person {name: "qianlongo", sex: "boy"}複製代碼

咱們回到executeBound的解析

if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);複製代碼

callingContext是被綁定後的函數的this做用域,boundFunc就是那個被綁定後的函數,那麼經過這個if判斷,當爲非new調用形式的時候,直接利用apply處理便可。

可是若是是用new調用的呢?咱們看下面這段代碼,別看短短的四行代碼裏面知識點挺多的呢!

// 這裏拿到的是一個空對象,且其繼承於原函數的原型鏈prototype
var self = baseCreate(sourceFunc.prototype);
// 構造函數執行以後的返回值
// 通常狀況下是沒有返回值的,也就是undefined
// 可是有時候寫構造函數的時候會顯示地返回一個obj
var result = sourceFunc.apply(self, args);
// 因此去判斷結果是否是object,若是是那麼返回構造函數返回的object
if (_.isObject(result)) return result;
// 若是沒有顯示返回object,就返回原函數執行結束後的實例
return self;複製代碼

好,到這裏,我有一個疑問,baseCreate是個什麼鬼?

var Ctor = function(){};

var baseCreate = function(prototype) {
  // 若是prototype不是object類型直接返回空對象
  if (!_.isObject(prototype)) return {};
  // 若是原生支持create則用原生的
  if (nativeCreate) return nativeCreate(prototype); 
  // 將prototype賦值爲Ctor構造函數的原型
  Ctor.prototype = prototype; 
  // 建立一個Ctor實例對象
  var result = new Ctor; 
  // 爲了下一次使用,將原型清空
  Ctor.prototype = null; 
  // 最後將實例返回
  return result; 
};複製代碼

是否是好簡單,就是實現了原生的Object.create用來作一些繼承的事情。

結尾

文章很簡短,知道怎麼實現一個原生的bind就行。若是你對apply、call和this感興趣,歡迎查看

js中call、apply、bind那些事

this-想說愛你不容易

相關文章
相關標籤/搜索