call 方法使用一個指定的 this 值和單獨給出的一個或多個參數來調用一個函數
舉個例子:git
var value=1; function foo(x,y) { console.log(this.value) } var obj={ value: 2 } foo(); // 1 foo.call(obj,3,4); // 2
上述例子中,當foo函數單獨調用時內部this綁定爲全局對象window。當經過call方法調用時this被綁定爲call方法中的第一個參數。call方法中的除了第一個參數外的剩餘參數爲foo函數的實參。github
特色:面試
上述例子也能夠用apply來改寫:數組
var value=1; function foo(x,y) { console.log(this.value) } var obj={ value: 2 } foo(); // 1 foo.apply(obj,[3,4]); // 2
apply與call的惟一區別就是:調用apply方法時的參數,實參應該是以數組的形式來書寫。閉包
bind 方法建立一個新的函數,也能夠說是當前調用bind方法的函數的一個引用,這個函數的this被綁定爲bind方法的第一個參數,其他參數爲這個新函數的實參。
仍是以上述代碼爲例:app
var value=1; function foo(x,y) { console.log(this.value) } var obj={ value: 2 } var bar=foo.bind(obj,3,4); bar(); // 2
bind與call,apply的區別就是:bind方法不會當即調用函數,它只是改變了新函數的this綁定。函數
當咱們使用bind方法建立一個新函數,這個新函數再使用call或者apply來更改this綁定時,仍是以bing綁定的this爲準。學習
var value=1; function foo(x,y) { console.log(this.value) } var obj={ value: 2 } var o={ value: 3 } var bar=foo.bind(obj,3,4); bar.call(o); // 2
相同點:測試
不一樣點:this
怎樣來實現call呢?先想一想call的特色:
第一個參數爲要綁定的this,剩餘參數爲函數的實參。
那咱們怎樣改更改this的綁定呢?
咱們直到當咱們以 對象 . 方法
調用一個普通函數時,this始終指向當前調用的對象。
var value=1; function foo(x,y) { console.log(this.value) } var obj={ value: 2 } foo.call(obj,3,4); // 2 // 至關於 obj.foo(3,4);
思路:
對象 . 方法
執行這個函數。call的第一個參數還有幾個特色:
/** * @description: 實現call方法 * @param : context this要綁定的值 * @param : args 除第一個參數外的參數集合 * @return: 函數返回值 */ Function.prototype.myCall=function(context,...args) { let handler=Symbol();// 生成一個惟一的值,用來做爲要綁定對象的屬性key,儲存當前調用call方法的函數 if(typeof this!=='function') { //調用者不是函數 throw this+'.myCall is not a function' } // 若是第一個參數爲引用類型或者null if(typeof context==='object'||typeof context==='function') { // 若是爲null 則this爲window context=context||window; } else { // 若是爲undefined 則this綁定爲window if(typeof context==='undefined') { context=window; } else { // 基本類型包裝 1 => Number{1} context=Object(context); } } // this 爲當前調用call方法的函數。 context[handler]=this; // 執行這個函數。這時這個函數內部this綁定爲cxt,儲存函數執行後的返回值。 let result=context[handler](...args); // 刪除對象上的函數 delete context[handler]; // 返回返回值 return result; }
上述call的實現只支持大部分場景,好比要綁定的對象爲凍結對象,則會拋出錯誤。
因爲apply跟call的惟一區別只是除了第一個參數外其他參數的傳遞形式不同。在實現call的基礎上略做修改就能夠了。
call參數的特色:
2.1 若是第二個參數爲null或者undefined,則無效。
2.2 若是第二個參數類型不是Object,則拋出一個異常。若是不是數組,則無效。
/** * @description: 實現apply方法 * @param : context this要綁定的值 * @param : argsArr 要傳遞給調用apply方法的函數的實參集合。數組形式。 * @return: 函數返回值 */ Function.prototype.myApply=function(context,argsArr) { let handler=Symbol();// 生成一個惟一的值,用來做爲要綁定對象的屬性key,儲存當前調用call方法的函數 if(typeof this!=='function') { //調用者不是函數 throw this+'.myBind is not a function' } let args=[]; // 若是傳入的參數是否是數組,則無效 if(typeof argsArr==='object'||typeof context==='function'||typeof argsArr==='undefined') { args=Array.isArray(argsArr)? argsArr:[]; } else { // 若是爲基本類型,若是是undefined,則無效,其它類型則拋出錯誤。 throw 'TypeError: CreateListFromArrayLike called on non-object' } // 若是第一個參數爲引用類型或者null if(typeof context==='object') { // 若是爲null 則this爲window context=context||window; } else { // 若是爲undefined 則this綁定爲window if(typeof context==='undefined') { context=window; } else { // 基本類型包裝 1 => Number{1} context=Object(context); } } // this 爲當前調用call方法的函數。 context[handler]=this; // 執行這個函數。這時這個函數內部this綁定爲cxt,儲存函數執行後的返回值。 let result=context[handler](...args); // 刪除對象上的函數 delete context[handler]; // 返回返回值 return result; }
bind與call和apply區別仍是很大的。
先看一個例子:
var obj={ name: 'erdong' } function foo(name,age) { this.age=age; console.log(this.name+':'+age+'歲'); } var bar=foo.bind(obj,'chen'); bar(18); // erdong:18歲 var b=new bar(27); // undefined:27歲 console.log(b.age); // 27
綜合上述例子,咱們總結一下bind方法特色:
1.調用bind方法會建立一個新函數,咱們成它爲綁定函數(boundF)。
2.當咱們直接調用boundF函數時,內部this被綁定爲bind方法的第一個參數。
3.當咱們把這個boundF函數當作構造函數經過new關鍵詞調用時,函數內部的this綁定爲新建立的對象。(至關於bind提供的this值被忽略)。
4.調用bind方法時,除第一個參數外的其他參數,將做爲boundF的預置參數,在調用boundF函數時默認填充進boundF函數實參列表中。
<!--bind方法中第一個參數的特色:
咱們根據上述的bind方法的特色,一步一步實現bind方法。
// 第一步 返回一個函數 /** * @description: 實現bind方法 * @param : context this要綁定的值 * @param : args 調用bind方法時,除第一個參數外的參數集合,這些參數會被預置在綁定函數的參數列表中 * @return: 返回一個函數 */ Function.prototype.myBind=function(context,...args) { // 這裏的this爲調用bind方法的函數。 let thisFunc=this; let boundF = function() { } return boundF; }
第一步咱們實現了myBind方法返回一個函數。沒錯就是這就是利用了閉包。
// 第二步 /** * @description: 實現bind方法 * @param : context this要綁定的值 * @param : args 調用bind方法時,除第一個參數外的參數集合,這些參數會被預置在綁定函數的參數列表中 * @return: 返回一個函數 */ Function.prototype.myBind=function(context,...args) { // 這裏的this爲調用bind方法的函數。 let thisFunc=this; let boundF=function() { thisFunc.call(context,...args); } return boundF; }
第二步:當調用boundF方法時,原函數內部this綁定爲bind方法的第一個參數,這裏咱們利用了call來實現。
// 第三步 /** * @description: 實現bind方法 * @param : context this要綁定的值 * @param : args 調用bind方法時,除第一個參數外的參數集合,這些參數會被預置在綁定函數的參數列表中 * @return: 返回一個函數 */ Function.prototype.myBind=function(context,...args) { // 這裏的this爲調用bind方法的函數。 let thisFunc=this; let boundF=function() { let isUseNew=this instanceof boundF; thisFunc.call(isUseNew? this:context,...args); } return boundF; }
第三部:先判斷boundF是否經過new調用,也就是判斷boundF內部的this是否爲boundF的一個實例。若是是經過new調用,boundF函數的內部this綁定爲當前新建立的對象,所以調用call方法時把當前新建立的對象當作第一個參數傳遞。
// 第四步 /** * @description: 實現bind方法 * @param : context this要綁定的值 * @param : args 調用bind方法時,除第一個參數外的參數集合,這些參數會被預置在綁定函數的參數列表中 * @return: 返回一個函數 */ Function.prototype.myBind=function(context,...args) { // 這裏的this爲調用bind方法的函數。 let thisFunc=this; let boundF=function() { let boundFAgrs=arguments; let totalAgrs=[...args,...arguments]; let isUseNew=this instanceof boundF; thisFunc.call(isUseNew? this:context,...totalAgrs); } return boundF; }
第四部:經過閉包的特性咱們知道,boundF函數能夠訪問到外部的args變量,將它與boundF函數中的參數合併。而後當作調用原函數的參數。
到此咱們簡易版的bind已經顯示完畢,下面測試:
Function.prototype.myBind=function(context,...args) { // 這裏的this爲調用bind方法的函數。 let thisFunc=this; let boundF=function() { let boundFAgrs=arguments; let totalAgrs=[...args,...arguments]; let isUseNew=this instanceof boundF; thisFunc.call(isUseNew? this:context,...totalAgrs); } return boundF; } var obj={ name: 'erdong' } function foo(name,age) { this.age=age; console.log(this.name+':'+age+'歲'); } var bar=foo.myBind(obj,'chen'); bar(18); // erdong:18歲 var b=new bar(27); // undefined:27歲 console.log(b) console.log(b.age); // 27
咱們發現上述代碼中調用myBind跟bind方法輸出的結果一致。
其實bind方法還有一個特色。
看例子:
var obj={ name: 'erdong' } function foo(name,age) { this.age=age; } foo.prototype.say=function() { console.log(this.age); } var bar=foo.bind(obj,'chen'); var b=new bar(27); b.say();
經過上述例子咱們發現,經過new(新函數)建立的對象 b 。它能夠獲取原函數原型上的方法。由於咱們實現的myBind,b是經過新函數建立的,它跟原函數理論上來講並無什麼關係。
再來看:
var obj={ name: 'erdong' } function foo(name,age) { this.age=age; } var bar=foo.bind(obj,'chen'); var b=new bar(27); console.log(b instanceof foo); // true console.log(b instanceof bar); // true
它的原型鏈上出現了foo.prototype和bar.prototype。按照咱們的常規理解 b 的原型鏈爲:
b.__proto__ => bar.prototype => bar.prototype.__proto__ => Object.prototype
可是跟foo.prototype有什麼關係呢?
我我的的理解:
foo函數調用bind方法產生的新函數bar,這個函數不是一個真正的函數,mdn解釋它爲怪異函數對象
。咱們經過console.log(bar.prototype)
發現
輸出的值爲undefined。咱們暫且把它理解成一個foo函數的一個簡化
版。能夠形象的理解成foo == bar
。
經過咱們上面實現的myBind並不能達到讓新對象b跟原函數和新函數的原型都產生關係。
var obj={ name: 'erdong' } function foo(name,age) { this.age=age; } var bar=foo.myBbind(obj,'chen'); var b=new bar(27); console.log(b instanceof foo); // fasle console.log(b instanceof bar); // true
這是咱們就須要對咱們的myBind進行迭代升級:
// 迭代一 Function.prototype.myBind=function(context,...args) { // 這裏的this爲調用bind方法的函數。 let thisFunc=this; let boundF=function() { let boundFAgrs=arguments; let totalAgrs=[...args,...arguments]; let isUseNew=this instanceof boundF; thisFunc.call(isUseNew? this:context,...totalAgrs); } // 調用myBind方法的函數的prototype賦值給 boundF 的prototype。 boundF.prototype=thisFunc.prototype; return boundF; }
在咱們myBind實現中bar函數其實就是boundF函數,所以把原函數的原型賦值給新函數的原型,這時建立的對象就會跟原函數的原型有關係。
這時b的原型鏈就會變成:
b.__proto__ => bar.prototype => foo.prototype => foo.prototype.__proto__ => Object.prototype
這時b的原型鏈上就會出現 bar.prototype 和 foo.prototype。
var obj={ name: 'erdong' } function foo(name,age) { this.age=age; } var bar=foo.myBbind(obj,'chen'); var b=new bar(27); console.log(b instanceof foo); // true console.log(b instanceof bar); // true
咱們在實現裏把foo的原型直接賦值給bar的原型。因爲引用地址相同,因此改變bar原型的時候foo的原型也會改變。
var obj={ name: 'erdong' } function foo(name,age) { this.age=age; } var bar=foo.myBbind(obj,'chen'); bar.prototype.aaa = 1; console.log(bar.prototype.aaa); // 1 var b=new bar(27); console.log(b instanceof foo); // true console.log(b instanceof bar); // true
這樣是不合理的,咱們繼續迭代:
Function.prototype.myBind=function(context,...args) { // 這裏的this爲調用bind方法的函數。 let thisFunc=this; let boundF=function() { let boundFAgrs=arguments; let totalAgrs=[...args,...arguments]; let isUseNew=this instanceof boundF; thisFunc.call(isUseNew? this:context,...totalAgrs); } var F=function() {}; F.prototype=thisFunc.prototype; boundF.prototype=new F(); return boundF; }
這裏咱們聲明瞭一個函數F,讓它的prototype的值爲foo的prototype。再讓boundF的prototype的值賦值爲F的實例。利用原型鏈繼承,來讓原函數與新函數的原型之間沒有直接關係。 這個時候b的原型鏈爲:
b.__proto__ => bar.prototype => new F() => new F().__proto__ => F.prototype => thisFunc.prototype => thisFunc.prototype.__proto__ => Object.prototype
綜上最終版:
/** * @description: 實現bind方法 * @param : context this要綁定的值 * @param : args 調用bind方法時,除第一個參數外的參數集合,這些參數會被預置在綁定函數的參數列表中 * @return: 返回一個新函數 */ Function.prototype.myBind=function(context,...args) { // 這裏的this爲調用bind方法的函數。 let thisFunc=this; // 若是調用bind的變量不是Function類型,拋出異常。 if(typeof thisFunc!=='function') { throw new TypeError('Function.prototype.bind - '+ 'what is trying to be bound is not callable'); } // 定義一個函數boundF // 下面的」新函數「 均爲函數調用bind方法以後建立的函數。 let boundF=function() { // 這裏的 arguments 爲函數通過bind方法調用以後生成的函數再調用時的實參列表 let boundFAgrs=arguments; // 把調用bind方法時除第一個參數外的參數集合與新函數調用時的參數集合合併。當作參數傳遞給call方法 let totalAgrs=[...args,...arguments]; // 判斷當前新函數是不是經過new關鍵詞調用 let isUseNew=this instanceof boundF; // 若是是->把call方法第一個參數值爲當前的this(這裏的this也就是經過new調用新函數生成的新對象) // 若是否->把調用bind方法時的傳遞的第一個參數當作call的第一個參數傳遞 thisFunc.call(isUseNew? this:context,...totalAgrs); } //經過原型鏈繼承的方式讓原函數的原型和新函數的原型,都在經過new關鍵詞構造的新對象的原型鏈上 // b instanceof 原函數 -> true // b instanceof 新函數 -> true var F=function() {}; F.prototype=thisFunc.prototype; boundF.prototype=new F(); return boundF; }
什麼是軟綁定?咱們知道經過bind能夠更改this綁定爲bind方法的第一個參數(除了new)。綁定以後就沒法改變了。咱們稱bind綁定this爲硬綁定。
// bind var o={ name: 'erdong' } var o1={ name: "chen" } var foo=function() { console.log(this); } var bar=foo.bind(o); var obj={ foo: bar } bar(); // this => o bar.call(o1); // this => o obj.foo(); // this => o
上述例子中,當foo函數經過bind綁定this爲o,再經過call或者對象.方法的形式調用時,this始終被綁定爲o。沒法被改變。固然這裏咱們不考慮new(經過new調用的話,this不綁定爲o)。那麼咱們怎樣再調用bar函數時,還能動態的修改this的綁定呢?
// softBind var o={ name: 'erdong' } var o1={ name: "chen" } var foo=function() { console.log(this); } var bar=foo.softBind(o); var obj={ foo: bar } bar(); // this => o bar.call(o1); // this => o1 obj.foo(); // this => obj
其實這裏的實現softBind的原理跟實現myBind的原理相似。
這裏咱們在myBind源代碼中更改:
Function.prototype.softBind=function(context,...args) { // 這裏的this爲調用bind方法的函數。 let thisFunc=this; // 若是調用bind的變量不是Function類型,拋出異常。 if(typeof thisFunc!=='function') { throw new TypeError('Function.prototype.bind - '+ 'what is trying to be bound is not callable'); } // 定義一個函數boundF // 下面的」新函數「 均爲函數調用bind方法以後建立的函數。 let boundF=function() { // 這裏的 arguments 爲函數通過bind方法調用以後生成的函數再調用時的實參列表 let boundFAgrs=arguments; // 把調用bind方法時除第一個參數外的參數集合與新函數調用時的參數集合合併。當作參數傳遞給call方法 let totalAgrs=[...args,...arguments]; // 若是調用新函數時存在新的this,而且新的this不是全局對象,那麼咱們認爲這裏想要更改新函數this的綁定。所以讓新函數的內部this綁定爲當前新的this。 thisFunc.call(this && this !== window ? this : context,...totalAgrs); } //經過原型鏈繼承的方式讓原函數的原型和新函數的原型,都在經過new關鍵詞構造的新對象的原型鏈上 // b instanceof 原函數 -> true // b instanceof 新函數 -> true var F=function() {}; F.prototype=thisFunc.prototype; boundF.prototype=new F(); return boundF; }
這時咱們用softBind再輸出一下上面的例子:
var o={ name: 'erdong' } var o1={ name: "chen" } var foo=function() { console.log(this); } var bar=foo.softBind(o); var obj={ foo: bar } bar(); // this => o bar.call(o1); // this => o1 這裏若是上面使用bind 這裏的this仍是被綁定爲o bar.call(); // this => o1 這裏若是上面使用bind 這裏的this仍是被綁定爲o obj.foo(); // this => obj 這裏若是上面使用bind 這裏的this仍是被綁定爲o
這時達到了咱們指望的輸出。
重點就在這一句:
thisFunc.call(this && this !== window ? this : context,...totalAgrs);
看下述代碼:
function func(){ console.log(this); } func.call(func); //輸出func func.call.call(func); //輸出window
看到這裏咱們確定對 func.call(func); 輸出什麼很清楚了。
可是 func.call.call(func); 這樣有輸出什麼呢?
咱們一步一步拆解來看
func.call.call(func); // 此時 func.call 內部的this爲 func。 // 這裏是在上一步代碼的基礎上執行的 // 此時func.call的內部this被綁定爲func // 可是此時又執行了func.call(); func.call(); // 因爲call中沒有參數,所以func的內部this被綁定爲window
若是此時把 func.call.call(func)
結合咱們的源碼實現來看,會很容易理解。
若是文中有錯誤,請務必留言指正,萬分感謝。
點個贊哦,讓咱們共同窗習,共同進步。