bind函式作用、應用場景以及模擬實現
bind函式
bind 函式掛在 Function 的原型上
Function.prototype.bind
建立的函式都可以直接呼叫 bind,使用:
function func(){
console.log(this)
}
func.bind(); // 用函式來呼叫
bind 的作用:
bind() 方法呼叫後會建立一個新函式。當這個新函式被呼叫時,bind() 的第一個引數將作為新函式執行時的 this的值,之後的序列引數將會在傳遞的實參前傳入作為新函式的引數。<MDN>
bind
func.bind(thisArg[,arg1,arg2...argN])
- 第一個引數thisArg,當 func 函式被呼叫時,該引數會作為 func 函式執行時的 this 指向。當使用 new 操作符呼叫繫結函式時,該引數無效。
- [,arg1,arg2...argN] 作為實參傳遞給 func 函式。
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函式才會被執行
從以上得到如下資訊:
- bind被函式呼叫
- 返回一個新函式
- 能改變函式this指向
- 可以傳入引數
深入bind 使用
以上知道了 bind 函式的作用以及使用方式,接下深入到 bind 函式的使用中,具體介紹三個方面的使用,這也是之後模擬實現 bind 函式的要點。
- 改變函式執行時this指向
- 傳遞引數
- 返回的新函式被當成建構函式
改變函式執行時this指向
當呼叫 bind 函式後,bind 函式的第一個引數就是原函式作用域中 this 指向的值。
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函式應用場景
以上只是說了 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,具體的點:
- 被函式呼叫
- 返回新函式
- 傳遞引數
- 改變函式執行時this指向
- 新函式被當做建構函式時處理
被函式呼叫,可以直接掛在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 的指向,並傳遞正確順序的引數。接下來就是比較難理解的地方,當新函式被當做建構函式的情況。
需要作出兩個地方的改變:
- 新返回的函式要繼承原函式原型上的屬性
- 原函式改變this問題。如果用new呼叫,則原函式this指向應該是新函式中this的值;否則為傳遞的thisArg的值。
先做繼承,讓新函式繼承原函式的原型,維持原來的原型關係。匿名函式沒辦法引用,所以給新函式起一個名字。
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 函式原始碼已實現完成,希望對你有幫助。
如有偏差歡迎指正學習,謝謝。