前言
這是underscore.js原始碼分析的第五篇,如果你對這個系列感興趣,歡迎點選
underscore-analysis/ watch一下,隨時可以看到動態更新。
事情要從js中的
this
開始說起,你是不是也經常有種無法掌控和知曉它的感覺,對於初學者來說,this
簡直如同回撥地獄般,神乎其神,讓人無法捉摸透。但是通過原生js中的bind方法,我們可以顯示繫結函式的this
作用域,而無需擔心執行時是否會改變而不符合自己的預期。當然了下劃線中的bind也是模仿它的功能同樣可以達到類似的效果。
bind簡單回顧
我們從mdn上的介紹來回顧一下bind的使用方法。
bind方法建立一個新的函式, 當被呼叫時,它的this關鍵字被設定為提供的值。
語法
fun.bind(thisArg[, arg1[, arg2[, ...]]])複製程式碼
簡單地看一下這些引數的含義
thisArg
當繫結函式被呼叫時,該引數會作為原函式執行時的this指向,當使用new 操作符呼叫繫結函式時,該引數無效。
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
的實現
先看一下這些引數都代表什麼含義
- sourceFunc:原函式,待繫結函式
- boundFunc: 繫結後函式
- context:繫結後函式
this
指向的上下文 - callingContext:繫結後函式的執行上下文,通常就是 this
- 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感興趣,歡迎檢視