bind方法簡介
關于bind() 方法的介紹,可以參照這里;
bind() 方法創(chuàng)建一個新的函數(shù)获三,在 bind() 被調用時勇劣,這個新函數(shù)的 this 被指定為 bind() 的第一個參數(shù),而其余參數(shù)將作為新函數(shù)的參數(shù),供調用時使用屎慢。
語法
function.bind(thisArg[, arg1[, arg2[, ...]]])
參數(shù)
- thisArg
調用綁定函數(shù)時作為this
參數(shù)傳遞給目標函數(shù)的值亚享。
如果使用new
運算符構造綁定函數(shù)咽块,則忽略該值。當使用bind
在setTimeout
中創(chuàng)建一個函數(shù)(作為回調提供)時欺税,作為thisArg
傳遞的任何原始值都將轉換為object
侈沪。如果bind
函數(shù)的參數(shù)列表為空,或者thisArg
是null
或undefined
魄衅,執(zhí)行作用域的this
將被視為新函數(shù)的 thisArg
峭竣。
- arg1, arg2, ...
當目標函數(shù)被調用時,被預置入綁定函數(shù)的參數(shù)列表中的參數(shù)晃虫。
返回值
返回一個原函數(shù)的拷貝皆撩,并擁有指定的 this 值和初始參數(shù)。
實現(xiàn)bind方法1.0
- 首先哲银,我們定義一個函數(shù)扛吞,這個函數(shù)的第一個參數(shù)作為
thisArg
,后面的參數(shù)作為返回的新方法的參數(shù):
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift(); // 一箭雙雕的寫法
}
也可以利用剩余參數(shù):
Function.prototype.mybind = function (thisArg, ...args) {
// ...
}
剩余參數(shù)是ES6的新特性荆责,一般說來滥比,不支持bind的情況一般也不支持剩余參數(shù),所以做院,不推薦這種寫法盲泛。
- 然后,
mybind
方法會返回一個新函數(shù)键耕,該函數(shù)將外層函數(shù)的參數(shù)與內層函數(shù)的參數(shù)連接起來一起作為參數(shù):
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift();
return function () {
newArgs = args.concat(Array.from(arguments));
// ...
}
}
- 我們可以使用
apply
來完成this
指向變更寺滚,在那之前可以使用變量thisFunc
先保存原函數(shù):
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift();
let thisFunc = this;
return function () {
newArgs = args.concat(Array.from(arguments));
return thisFunc.apply(thisArg, newArgs);
}
}
在調用apply必須先檢測
thisFunc
是不是Function
,這里沒寫是因為我懶屈雄。
低版本瀏覽器在不支持bind的情況下會支持apply嗎村视?還真會。
當然酒奶,你也可以手動實現(xiàn)apply
:
Function.prototype.mybind = function () {
let args = Array.from(arguments);
// 手動實現(xiàn)需要在第一個參數(shù)為null時
let thisArg = args.shift() || window;
let thisFunc = this;
return function () {
newArgs = args.concat(Array.from(arguments));
let fn = Symbol('thisFunc');
thisArg[fn] = _Func;
let res = thisArg[fn](...newArgs);
delete thisArg[fn];
return res;
}
}
數(shù)組解構也是個ES6的語法蚁孔,感覺又把自己繞進去了。
這樣惋嚎,一個類似于bind
的方法就寫好了杠氢,下面我們調用一下它。
使用mybind 1.0
- 情景1:普通函數(shù)
var age = 18;
let myfn = function (a, b, c) {
console.log(this.age, a, b, c);
}
myfn(); // 18 undefined undefined undefined
let xiaohuang = {
age: 12
}
let myfn1 = myfn.mybind(xiaohuang, 1);
myfn1(3); // 12 1 3 undefined
let myfn2 = myfn.bind({ age: 114514 });
myfn2(19, 19, 810); // 114514 19 19 810
myfn2(19, 19, 810); // 114514 19 19 810
沒有問題另伍,一切正常修然。
- 情景2: 構造函數(shù)
let Animal = function (name) {
this.name = name;
}
let buly = {
name: 'buly'
}
let Cat = Animal.mybind(buly);
let tom = new Cat('tom');
console.log(tom, buly); // {} {name: "tom"}
// expected output: {name: "tom"} {name: "buly"}
哦吼,出問題了。
實現(xiàn)bind方法2.0
看來我們根據具體的情況采取不同的策略愕宋,當傳入的函數(shù)是一個構造函數(shù)時玻靡,我們不需要更改this
的指向。
如何判斷是否為構造函數(shù)呢中贝?只需要判斷this instanceof 構造方法
的值就可以了囤捻。
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift();
let thisFunc = this;
// 因為需要構造函數(shù),所以不能是匿名函數(shù)了
let fBound = function () {
newArgs = args.concat(Array.from(arguments));
// 判斷是否為構造函數(shù)
thisArg = this instanceof fBound ? this : thisArg;
return thisFunc.apply(thisArg, newArgs);
}
return fBound;
}
使用bind2.0
- 情景2: 構造函數(shù)
let Animal = function (name) {
this.name = name;
}
let buly = {
name: 'buly'
}
let Cat = Animal.mybind(buly);
let tom = new Cat('tom');
console.log(tom, buly); // {name: "tom"} {name: "buly"}
— 我很高興你完成了手寫bind的全部內容邻寿。
— 不好意思蝎土,你高興的太早了。
- 情景3: 帶原型對象(
prototype
绣否,下同)的構造函數(shù)
let Animal = function (name) {
this.name = name;
}
// 箭頭函數(shù)中的this會穿透作用域誊涯,所以不要用箭頭函數(shù)哦
Animal.prototype.say = function() {
console.log('hello, my name is ' + this.name);
}
let buly = {
name: 'buly'
}
let Cat = Animal.mybind(buly);
let tom = new Cat('tom');
tom.say(); // Error: tom.say is not a function
新返回的函數(shù)與原函數(shù)的原型對象并沒有建立聯(lián)系,所以new
出來的對象不能訪問到原函數(shù)的原型對象上的方法蒜撮。
實現(xiàn)bind方法3.0
讓我們簡單粗暴一點:
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift();
let thisFunc = this;
// 因為需要構造函數(shù)暴构,所以不能是匿名函數(shù)了
let fBound = function () {
newArgs = args.concat(Array.from(arguments));
// 判斷是否為構造函數(shù)
thisArg = this instanceof fBound ? this : thisArg;
return thisFunc.apply(thisArg, newArgs);
}
// 直接將原函數(shù)的prototype賦值給綁定函數(shù)
fBound.prototype = this.prototype;
return fBound;
}
當然,我們不推薦這種粗暴的繼承方式段磨,這種情況下取逾,若更改新函數(shù)的原型對象,則原函數(shù)的原型對象也會被改變苹支。
- 推薦方法1: 原型式繼承
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift();
let thisFunc = this;
// 中間函數(shù)
let fNop = function () { };
// 因為需要構造函數(shù)砾隅,所以不能是匿名函數(shù)了
let fBound = function () {
newArgs = args.concat(Array.from(arguments));
// 判斷是否為構造函數(shù)
thisArg = this instanceof fBound ? this : thisArg;
return thisFunc.apply(thisArg, newArgs);
}
fNop.prototype = this.prototype;
// 原型式繼承
fBound.prototype = new fNop();
return fBound;
}
你可能不太能理解這段東西,我試著簡單的解釋一下:
如果我們采用ES6中的class
的話债蜜,是這樣的:
class Animal {
constructor(name) {
this.name = name;
}
}
class Cat extends Animal {
constructor(name) {
super(name);
}
}
let tom = new Cat('tom');
這里晴埂,tom
的__proto__
指向Cat的原型對象,tom
的__proto__
的__proto__
指向Animal的原型對象
但ES5只支持構造函數(shù)寻定,不支持class
儒洛,所以們先創(chuàng)建一個空函數(shù)fNop
,然后使其原型對象指向原函數(shù)的原型對象特姐。在使得fBound
的原型對象指向fNop
的實例,這也變相實現(xiàn)了fBound extends fNop
黍氮。
以上純屬個人理解唐含,僅供參考。
- 推薦方法2:
Object.create
方便書寫又方便理解沫浆,其實就是淺拷貝捷枯。
Function.prototype.mybind = function () {
let args = Array.from(arguments);
let thisArg = args.shift();
let thisFunc = this;
// 因為需要構造函數(shù),所以不能是匿名函數(shù)了
let fBound = function () {
newArgs = args.concat(Array.from(arguments));
// 判斷是否為構造函數(shù)
thisArg = this instanceof fBound ? this : thisArg;
return thisFunc.apply(thisArg, newArgs);
}
// Object.create拷貝原型對象
fBound.prototype = Object.create(this.prototype);
return fBound;
}
使用bind3.0
- 情景3: 帶原型對象(
prototype
专执,下同)的構造函數(shù)
let Animal = function (name) {
this.name = name;
}
// 箭頭函數(shù)中的this會穿透作用域淮捆,所以不要用箭頭函數(shù)哦
Animal.prototype.say = function() {
console.log('hello, my name is ' + this.name);
}
let buly = {
name: 'buly'
}
let Cat = Animal.mybind(buly);
let tom = new Cat('tom');
tom.say(); // hello, my name is tom
和真正的bind不同,使用我們手動實現(xiàn)的bind,最后實例化的對象的構造函數(shù)是
fBound
而不是原構造函數(shù)案站。
總結
實現(xiàn)bind的技術要點如下:
將剩余的參數(shù)和傳入的參數(shù)拼接,作為新的參數(shù)石挂。知識點有:解構賦值、Array.prototype.concat里伯、Array.prototype.apply;
判斷是否為構造函數(shù),使用instanceof操作符肩碟;
實現(xiàn)原型繼承,參考JS中的繼承與原型鏈。
其他的技術要點吨拍,可以參考我之前的文章《如何手寫一個call方法》。
MDN提供的polyFill代碼
polyfill:補丁。意思是在瀏覽器不支持bind時燥撞,采用該代碼以解決此問題:
全部代碼如下:
// Yes, it does work with `new (funcA.bind(thisArg, args))`
if (!Function.prototype.bind) (function(){
var ArrayPrototypeSlice = Array.prototype.slice;
Function.prototype.bind = function(otherThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var baseArgs= ArrayPrototypeSlice.call(arguments, 1),
baseArgsLength = baseArgs.length,
fToBind = this,
fNOP = function() {},
fBound = function() {
baseArgs.length = baseArgsLength; // reset to default base arguments
baseArgs.push.apply(baseArgs, arguments);
return fToBind.apply(
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
);
};
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
})();
這里提出2條解讀:
- 最外層是匿名函數(shù)自調用冠胯,是防止浪費存儲資源的做法。
-
baseArgs.length = baseArgsLength;
這一步的原因有二。一是閉包導致了所有函數(shù)共用一個baseArgs
;二是因為push是一種會修改原數(shù)組的API,所以不可避免的會改動baseArgs
。使用concat
或者數(shù)組解構可以更好地解決此問題沃于。