bind 函數掛在 Function 的原型上面試
Function.prototype.bind
建立的函數均可以直接調用 bind,使用:瀏覽器
function func(){ console.log(this) } func.bind(); // 用函數來調用
bind 的做用:app
bind() 方法調用後會建立一個新函數。當這個新函數被調用時,bind() 的第一個參數將做爲新函數運行時的 this的值,以後的序列參數將會在傳遞的實參前傳入做爲新函數的參數。<MDN>
bind 接收的參數函數
func.bind(thisArg[,arg1,arg2...argN])
bind 返回值佈局
返回一個新函數
注意:這和函數調用 call/apply 改變this指向有所不一樣。調用call/apply 會把原函數直接執行了。學習
舉個例子說明:測試
function func(){ console.log(this) } // 用call func.call({a:1}); // func函數被執行了,打印:{a:1} // 用bind let newFunc = func.bind({}); // 返回新函數 newFunc(); // 只有當返回的新函數執行,func函數纔會被執行
從以上獲得以下信息:this
以上知道了 bind 函數的做用以及使用方式,接下深刻到 bind 函數的使用中,具體介紹三個方面的使用,這也是以後模擬實現 bind 函數的要點。prototype
當調用 bind 函數後,bind 函數的第一個參數就是原函數做用域中 this 指向的值。rest
function func(){ console.log(this); } let newFunc = func.bind({a:1}); newFunc(); // 打印:{a:1} let newFunc2 = func.bind([1,2,3]); newFunc2(); // 打印:[1,2,3] let newFunc3 = func.bind(1); newFunc3(); // 打印:Number:{1} let newFunc4 = func.bind(undefined/null); newFunc4(); // 打印:window
以上要注意,當傳入爲 null 或者 undefined 時,在非嚴格模式下,this 指向爲 window。
當傳入爲簡單值時,內部會將簡單的值包裝成對應類型的對象,數字就調用 Number 方法包裝;字符串就調用 String 方法包裝;true/false 就調用 Boolean 方法包裝。要想取到原始值,能夠調用 valueOf 方法。
Number(1).valueOf(); // 1 String("hello").valueOf(); // hello Boolean(true).valueOf(); // true
當屢次調用 bind 函數時,以第一次調用 bind 函數的改變 this 指向的值爲準。
function func(){ console.log(this); } let newFunc = func.bind({a:1}).bind(1).bind(['a','b','c']); newFunc(); // 打印:{a: 1}
從 bind 的第二個參數開始,是向原函數傳遞的實參。bind 返回的新函數調用時也能夠向原函數傳遞實參,這裏就涉及順序問題。
function func(a,b,c){ console.log(a,b,c); // 打印傳入的實參 } let newFunc = func.bind({},1,2); newFunc(3)
打印結果爲1,2,3。
能夠看到,在 bind 中傳遞的參數要先傳入到原函數中。
調用 bind 函數後返回的新函數,也能夠被當作構造函數。經過新函數建立的實例,能夠找到原函數的原型上。
// 原函數 function func(name){ console.log(this); // 打印:經過{name:'wy'} this.name = name; } func.prototype.hello = function(){ console.log(this.name) } let obj = {a:1} // 調用bind,返回新函數 let newFunc = func.bind(obj); // 把新函數做爲構造函數,建立實例 let o = new newFunc('seven'); console.log(o.hello()); // 打印:'seven' console.log(obj); // 打印:{a:1}
新函數被當成了構造函數,原函數func 中的 this 再也不指向傳入給 bind 的第一個參數,而是指向用 new 建立的實例。在經過實例 o 找原型上的方法 hello 時,可以找到原函數 func 原型上的方法。
在模擬實現 bind 特別要注意這一塊的實現,這也是面試的重點,會涉及到繼承。
以上只是說了 bind 函數時如何使用的,學會了使用,要把它放在業務場景中來解決一些現實問題。
先來一個佈局:
<ul id="list"> <li>1</li> <li>1</li> <li>1</li> </ul>
需求:點擊每個 li 元素,延遲1000ms後,改變 li 元素的顏色,
let lis = document.querySelectorAll('#list li'); for(var i = 0; i < lis.length; i++){ lis[i].onclick = function(){ setTimeout(function(){ this.style.color = 'red' },1000) } }
以上代碼點擊每個 li,並不會改變顏色,由於定時器回調函數的 this 指向的不是點擊的 li,而是window,(固然你也可使用箭頭函數,let之類來解決,這裏討論的主要是用bind來解決)。此時就須要改變回調函數的 this 指向。能改變函數 this 指向的有:call、apply、bind。那麼選擇哪個呢?根據場景來定,這裏的場景是在1000ms以後才執行回調函數,因此不能選擇使用call、apply,由於它們會當即執行函數,因此這個場景應該選擇使用 bind解決。
setTimeout(function(){ this.style.color = 'red' }.bind(this),1000)
有時會使用面向對象的方式來組織代碼,涉及到把事件處理函數拆分在原型上,而後把這些掛在原型上的方法賦值給事件,此時的函數在事件觸發時this都指向了元素,進而須要在函數中訪問實例上的屬性時,便不能找到成。
function Modal(options){ this.options = options; } Modal.prototype.init = function(){ this.el.onclick = this.clickHandler; // 此方法掛載原型上 } Modal.prototype.clickHandler = function(){ console.log(this.left); // 此時點擊元素執行該函數,this指向元素,不能找到left } let m = new Modal({ el: document.querySelector('#list'), left: 300 }) m.init(); // 啓動應用
以上代碼,在 init 函數中,給元素綁定事件,事件處理函數掛在原型上,使用 this 來訪問。當點擊元素時,在 clickHandler 函數中須要拿到實例的 left 屬性,但此時 clickHandler 函數中的 this 指向的是元素,而不是實例,因此拿不到。要改變 clickHandler 函數 this 的指向,此時就須要用到 bind。
Modal.prototype.init = function(){ this.el.onclick = this.clickHandler.bind(this) }
以上場景只是 bind 使用的冰山一角,它本質要作的事情是改變 this 的指向,達到預期目的。掌握了 bind 的做用以及應用的場景,在腦海中就會樹立一個印象:當須要改變this指向,並不當即執行函數時,就能想到 bind。
爲何要本身去實現一個bind函數呢?
bind()函數在 ECMA-262 第五版才被加入;它可能沒法在全部瀏覽器上運行(ie8如下)。
面試用,讓面試官找不到拒絕你的理由
抓住 bind 使用的幾個特徵,把這些點一一實現就OK,具體的點:
被函數調用,能夠直接掛在Function的原型上,爲了補缺那些不支持的瀏覽器,不用再爲支持的瀏覽器添加,能夠作以下判斷:
if(!Function.prototype.bind) { Function.prototype.bind = function(){ } }
這種行爲也叫做 polyfill,爲不支持的瀏覽器添加某項功能,以達到抹平瀏覽器之間的差距。
注意:若是瀏覽器支持,方便本身測試,能夠把 if 條件去掉,或者把 bind 改一個名字。在下文準備更名字爲 bind2,方便測試。
調用 bind 後會返回一個新的函數,當新函數被調用,原函數隨之也被調用。
Function.prototype.bind2 = function(thisArg,...args){ let funcThis = this; // 函數調用bind,this指向原函數 // 返回新函數 return function (...rest) { return funcThis.apply(thisArg,[...args,...rest]/*bind2傳遞的實參優先於新函數的實參*/) } } // 測試 function func(a,b,c){ console.log(this) console.log(a,b,c) } let newFunc = func.bind2({a:1},1,2); newFunc(3); // 打印:{a: 1} // 打印:1 2 3
以上這個函數已經可以改變原函數 this 的指向,並傳遞正確順序的參數。接下來就是比較難理解的地方,當新函數被當作構造函數的狀況。
須要做出兩個地方的改變:
先作繼承,讓新函數繼承原函數的原型,維持原來的原型關係。匿名函數沒辦法引用,因此給新函數起一個名字。
Function.prototype.bind2 = function(thisArg,...args){ let funcThis = this; // 函數調用bind,this指向原函數 // 要返回的新函數 let fBound = function (...rest) { return funcThis.apply(thisArg,[...args,...rest]/*bind2傳遞的實參優先於新函數的實參*/) } // 不是全部函數都有prototype屬性,好比 Function.prototype就沒有。 if(funcThis.prototype){ // 使用Object.create,以原函數prototype做爲新對象的原型建立對象 fBound.prototype = Object.create(funcThis.prototype); } return fBound; } // 測試 function func(name){ console.log(this); // {a: 1} this.name = name; } func.prototype.hello = function(){ console.log(this.name); // undefined } let newFunc = func.bind2({a:1}); let o = new newFunc('seven') o.hello(); // 打印:{a: 1} // 打印:undefined
以上代碼,新建的實例 o 可以調用到 hello 這個方法,說明繼承已經實現,可以訪問新函數上原型方法。
接下來是關於 this 指向問題,上面例子中,使用了 new 運算符調用函數,那麼原函數中,this 應該指向實例纔對。因此須要在改變 this 指向的 apply 那裏對是不是使用 new 操做符調用的作判斷。
用到的操做符是 instanceof,做用是判斷一個函數的原型是否在一個對象的原型鏈上,是的話返回true,不然返回false。測試以下:
function Person(){} let p = new Person(); console.log(p instanceof Person); // true console.log(p instanceof Object); // true console.log(p instanceof Array); // fasle
也能夠用 instanceof 在構造函數中判斷是不是經過 new 來調用的。若是是用 new 來調用,說明函數中 this 對象的原型鏈上存在函數的原型,會返回true。
function Person(){ console.log(this instanceof Person); // true } new Person();
回到咱們的 bind2 函數上,當調用 bind2 後返回了新函數 fBound,當使用 new 調用構造函數時,實際上調用的就是 fBound 這個函數,因此只須要在 fBound 函數中利用 instanceof 來判斷是不是用 new 來調用便可。
Function.prototype.bind2 = function(thisArg,...args){ let funcThis = this; // 函數調用bind,this指向原函數 // 要返回的新函數 let fBound = function (...rest) { // 若是是new調用的,原函數this指向新函數中建立的實例對象 // 不是new調用,依然是調用bind2傳遞的第一個參數 thisArg = this instanceof fBound ? this : thisArg; return funcThis.apply(thisArg,[...args,...rest]/*bind2傳遞的實參優先於新函數的實參*/) } // 不是全部函數都有prototype屬性,好比 Function.prototype就沒有。 if(funcThis.prototype){ // 使用Object.create,以原函數prototype做爲新對象的原型建立對象 fBound.prototype = Object.create(funcThis.prototype); } return fBound; } // 測試 function func(name){ console.log(this); // {a: 1} this.name = name; } func.prototype.hello = function(){ console.log(this.name); // undefined } let newFunc = func.bind2({a:1}); let o = new newFunc('seven') o.hello(); // 打印:{name:'seven'} // 打印:'seven'
bind 函數源碼已實現完成,但願對你有幫助。
若有誤差歡迎指正學習,謝謝。