如何寫一個實用的 bind?

謙龍發表於2017-05-21

前言

這是underscore.js原始碼分析的第五篇,如果你對這個系列感興趣,歡迎點選

underscore-analysis/ watch一下,隨時可以看到動態更新。

事情要從js中的this開始說起,你是不是也經常有種無法掌控和知曉它的感覺,對於初學者來說,this簡直如同回撥地獄般,神乎其神,讓人無法捉摸透。但是通過原生js中的bind方法,我們可以顯示繫結函式的this作用域,而無需擔心執行時是否會改變而不符合自己的預期。當然了下劃線中的bind也是模仿它的功能同樣可以達到類似的效果。

如何寫一個實用的 bind?
ctx

bind簡單回顧

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

bind方法建立一個新的函式, 當被呼叫時,它的this關鍵字被設定為提供的值。

語法

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

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

  1. thisArg

    當繫結函式被呼叫時,該引數會作為原函式執行時的this指向,當使用new 操作符呼叫繫結函式時,該引數無效。

  2. arg1, arg2, ...

    當繫結函式被呼叫時,這些引數將置於實參之前傳遞給被繫結的方法。

繫結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-想說愛你不容易

相關文章