在模擬bind之前,我們先了解bind的概念,這裡引入MDN解釋:
bind() 方法創建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定為 bind() 的第一個參數,而其餘參數將作為新函數的參數,供調用時使用。
說的通俗一點,bind與apply/call一樣都能改變函數this指向,但bind並不會立即執行函數,而是返回一個綁定了this的新函數,你需要再次調用此函數才能達到最終執行。
我們來看一個簡單的例子:
var obj = { z: 1};var obj1 = { z: 2};
function fn(x, y) { console.log(x + y + this.z);};fn.call(obj, 2, 3); fn.apply(obj, [2, 3]);
var bound = fn.bind(obj, 2);bound(3); bound.call(obj1, 3);可以到bind並不是立即執行,而是返回一個新函數,且新函數的this無法再次被修改,我們總結bind的特點:
可以修改函數this指向。
bind返回一個綁定了this的新函數boundFcuntion,例子中我們用bound表示。
支持函數柯裡化,我們在返回bound函數時已傳遞了部分參數2,在調用時bound補全了剩餘參數。
boundFunction的this無法再被修改,使用call、apply也不行。
考慮到有同學對於柯裡化的陌生,這裡簡單解釋,所謂函數柯裡化其實就是在函數調用時只傳遞一部分參數進行調用,函數會返回一個新函數去處理剩下的參數,一個經典簡單的例子:
function fn(x, y) { return function (y) { console.log(x + y); };};var fn_ = fn(1);fn_(1);
fn(1)(1)不難發現函數柯裡化使用了閉包,在執行內層函數時,它使用了外層函數的局部形參x,從而構成了閉包,扯遠了點。
我們來嘗試實現bind方法,先從簡單的改變this和返回函數開始。
實現bind之前已經有了模擬call/apply的經驗,這裡直接給出版本一:
Function.prototype.bind_ = function (obj) { var fn = this; return function () { fn.apply(obj); };};
var obj = { z: 1};
function fn() { console.log(this.z);};
var bound = fn.bind_(obj);bound();唯一需要留意的就是var fn = this這一行,如果不提前保存,在執行bound時內部this會指向window。
版本一以滿足了this修改與函數返回,馬上有同學就想到了,版本一不支持函數傳參,那麼我們進行簡單修改讓其支持傳參:
Function.prototype.bind_ = function (obj) { var args = Array.prototype.slice.call(arguments, 1); var fn = this; return function () { fn.apply(obj, args); };};完美了嗎?並不完美,別忘了我們前面說bind支持函數柯裡化,在調用bind時可以先傳遞部分參數,在調用返回的bound時可以補全剩餘參數,所以還得進一步處理,來看看bind_第二版:
Function.prototype.bind_ = function (obj) { var args = Array.prototype.slice.call(arguments, 1); var fn = this; return function () { var params = Array.prototype.slice.call(arguments); fn.apply(obj, args.concat(params)); };};
var obj = { z: 1};
function fn(x, y) { console.log(x + y + this.z);};
var bound = fn.bind_(obj, 1);bound(2);看,改變this,返回函數,函數柯裡化均已實現。這段代碼需要注意的是args.concat(params)的順序,args在前,因為只有這樣才能讓先傳遞的參數和fn的形參按順序對應。
至少走到這一步都挺順序,需要注意的是,bind方法還有一個少見的特性,這裡引用MDN的描述
綁定函數也可以使用 new 運算符構造,它會表現為目標函數已經被構建完畢了似的。提供的 this 值會被忽略,但前置參數仍會提供給模擬函數。
說通俗點,通過bind返回的boundFunction函數也能通過new運算符構造,只是在構造過程中,boundFunction已經確定的this會被忽略,且返回的實例還是會繼承構造函數的構造器屬性與原型屬性,並且能正常接收參數。
有點繞口,我們來看個簡單的例子:
var z = 0;var obj = { z: 1};
function fn(x, y) { this.name = '聽風是風'; console.log(this.z); console.log(x); console.log(y);};fn.prototype.age = 26;
var bound = fn.bind(obj, 2);var person = new bound(3);
console.log(person.name);在此例子中,我們先是將函數fn的this指向了對象obj,從而得到了bound函數。緊接著使用new操作符構造了bound函數,得到了實例person。不難發現,除了先前綁定好的this丟失了(後面會解釋原因),構造器屬性this.name,以及原型屬性fn.prototype.age都有順利繼承,除此之外,兩個形參也成功傳遞進了函數。
難點來了,至少在ES6之前,JavaScript並沒有class類的概念,所謂構造函數其實只是對於類的模擬;而這就造成了一個問題,所有的構造函數除了可以使用new構造調用以外,它還能被普通調用,比如上面例子中的bound我們也可以普通調用:
有同學在這可能就有疑惑,bound()等同於window.bound(),此時this不是應該指向window從而輸出0嗎?我們在前面說bind屬於硬綁定,一次綁定終生受益,上面的調用本質上等同於:
函數fn存在this默認綁定window與顯示綁定bind,而顯示綁定優先級高於默認綁定,所以this還是指向obj。
當構造函數被new構造調用時,本質上構造函數中會創建一個實例對象,函數內部的this指向此實例,當執行到console.log(this.z)這一行時,this上並未被賦予屬性z,所以輸出undefined,這也解釋了為什麼bound函數被new構造時會丟失原本綁定的this。
是不是覺得ES5構造函數特別混亂,不同調用方式函數內部this指向還不同,也正因如此在ES6中隆重推出了class類,凡是通過class創建的類均只能使用new調用,普通調用一律報錯處理:
class Fn { constructor(name, age) { this.name = name; this.age = age; }; sayName() { console.log(this.name); };};const person = new Fn('聽風是風', 26);person.sayName(); const person1 = Fn();扯遠了,讓我們回到上面的例子,說了這麼多無非是為了強調一點,我們在模擬bind方法時,返回的bound函數在調用時得考慮new調用與普通調用,畢竟兩者this指向不同。
再說直白一點,如果是new調用,bound函數中的this指向實例自身,而如果是普通調用this指向obj,怎麼區分呢?
不難,我們知道(強行讓你們知道)構造函數實例的constructor屬性永遠指向構造函數本身(這句話其實有歧義,具體我會在原型的文章中解釋),比如:
function Fn(){};var o = new Fn();console.log(o.constructor === Fn);而構造函數在運行時,函數內部this指向實例,所以this的constructor也指向構造函數:
function Fn() { console.log(this.constructor === Fn); };var o = new Fn();console.log(o.constructor === Fn);所以我就用constructor屬性來判斷當前bound方法調用方式,畢竟只要是new調用,this.constructor === Fn一定為true。
讓我們簡單改寫bind_方法,為bound方法新增this判斷以及原型繼承:
Function.prototype.bind_ = function (obj) { var args = Array.prototype.slice.call(arguments, 1); var fn = this; var bound = function () { var params = Array.prototype.slice.call(arguments); fn.apply(this.constructor === fn ? this : obj, args.concat(params)); }; bound.prototype = fn.prototype; return bound;};有同學就問了,難道不應該是this.constructor===bound嗎?並不是,雖然new的是bound方法,本質上執行的還是fn,畢竟bound自身並沒有構造器屬性,這點關係還是需要理清。
其次還有個缺陷。雖然構造函數產生的實例都是獨立的存在,實例繼承而來的構造器屬性隨便你怎麼修改都不會影響構造函數本身:
function Fn() { this.name = '聽風是風'; this.sayAge = function () { console.log(this.age); };};Fn.prototype.age = 26;
var o = new Fn();o.sayAge(); o.name = 'echo';var o1 = new Fn();console.log(o1.name)但是如果我們直接修改實例原型,這就會對構造函數Fn產生影響,來看個例子:
function Fn() { this.name = '聽風是風'; this.sayAge = function () { console.log(this.age); };};Fn.prototype.age = 26;
var o = new Fn();o.sayAge(); o.__proto__.age = 18;var o1 = new Fn();console.log(o1.age)不難理解,構造器屬性(this.name,this.sayAge)在創建實例時,我們可以抽象的理解成實例深拷貝了一份,這是屬於實例自身的屬性,後面再改都與構造函數不相關。而實例要用prototype屬性時都是順著原型鏈往上找,構造函數有便借給實例用了,一共就這一份,誰要是改了那就都得變。
我們可以輸出實例o,觀察它的屬性,可以看到age屬性確實是綁原型__proto__上(注意,prototype是函數特有,普通對象只有__proto__,兩者指向相同)。
怎麼做才保險呢,這裡就可以藉助一個空白函數作為中介,直接看個例子:
function Fn() { this.name = '聽風是風'; this.sayAge = function () { console.log(this.age); };};Fn.prototype.age = 26;var Fn1 = function () {};Fn1.prototype = Fn.prototype;var Fn2 = function () {};Fn2.prototype = new Fn1();var o = new Fn2();console.log(o.age); o.__proto__.age = 18;var o1 = new Fn();console.log(o1.age);說到底,我們就是借用空白函數,讓Fn2的實例多了一層__proto__,達到修改原型不會影響Fn原型的目的,當然你如果通過__proto__.__proto__還是一樣能修改,差不多就是這個意思:
o.__proto__.__proto__.age = 18;var o1 = new Fn();console.log(o1.age);所以綜上,我們再次修改bind_方法,拿出第四版:
Function.prototype.bind_ = function (obj) { var args = Array.prototype.slice.call(arguments, 1); var fn = this; var fn_ = function () {}; var bound = function () { var params = Array.prototype.slice.call(arguments); fn.apply(this.constructor === fn ? this : obj, args.concat(params)); console.log(this); }; fn_.prototype = fn.prototype; bound.prototype = new fn_(); return bound;};最後,bind方法如果被非函數調用時會拋出錯誤,所以我們要在第一次執行bind_時做一次調用判斷,加個條件判斷,我們來一個完整的最終版:
Function.prototype.bind_ = function (obj) { if (typeof this !== "function") { throw new Error("Function.prototype.bind - what is trying to be bound is not callable"); }; var args = Array.prototype.slice.call(arguments, 1); var fn = this; var fn_ = function () {}; var bound = function () { var params = Array.prototype.slice.call(arguments); fn.apply(this.constructor === fn ? this : obj, args.concat(params)); console.log(this); }; fn_.prototype = fn.prototype; bound.prototype = new fn_(); return bound;};
var z = 0;var obj = { z: 1};
function fn(x, y) { this.name = '聽風是風'; console.log(this.z); console.log(x); console.log(y);};fn.prototype.age = 26;
var bound = fn.bind_(obj, 2);var person = new bound(3);
console.log(person.name); console.log(person.age); person.__proto__.age = 18;var person = new fn();console.log(person.age);如果文章對你有幫助,歡迎關注+轉發,讓更多同學看到。
原文:https://www.cnblogs.com/echolun/p/12178655.html