對我來講,博客首先是一種知識管理工具,其次纔是傳播工具。個人技術文章,主要用來整理我還不懂的知識。我只寫那些我尚未徹底掌握的東西,那些我精通的東西,每每沒有動力寫。炫耀歷來不是個人動機,好奇才是。--阮一峯javascript
最近忽然想在弄弄基礎的東西了,就盯上了這個,call
、apply
和bind
的區別、原理究竟是什麼,怎麼手動實現了;通過本身的收集總結了這篇文章;java
文章分爲理解和實現兩部分,若是你理解這三個方法,能夠直接跳到實現的部分;數組
在javascript中,call、apply、bind都是Function對象自帶的方法; call、apply、bind方法的的共同點和區別:bash
三者都是用來改變函數的this對象的指向的;app
三者的第一個參數都是this要指向的對象,也就是上下文(函數的每次調用都會擁有一個特殊值--本次調用的上下文(context)-- 這就是this的關鍵字的值);函數
三者均可以利用後續傳參:工具
call:call([thisObj,arg1,arg2,...);ui
apply:apply(thisObj,[arg1,arg2,...]);this
bind:bind(thisObj,arg1,arg2,...);spa
bind 是返回對應函數,便於稍後調用,apply、call則是當即調用
;
定義: 調用一個對象的調用一個對象的一個方法,以另外一個對象替換當前對象。
說明: call 方法能夠用來代替另外一個對象調用一個方法。
thisObj的取值有如下4種狀況:
1 不傳,或者傳null,undefined, 函數中的this指向window對象;
2 傳遞另外一個函數的函數名,函數中的this指向這個函數的引用;
3 傳遞字符串、數值或布爾類型等基礎類型,函數中的this指向其對應的包裝對象,如 String、Number、Boolean;
4 傳遞一個對象,函數中的this指向這個對象;
是否是不太好理解!
代碼試驗一下可能會更加的直觀:
function fn1() {
console.log(this); //輸出函數fn1中的this對象
}
function fn2() {}
let obj = {name:"call"}; //定義對象obj
fn1.call(); //window
fn1.call(null); //window
fn1.call(undefined); //window
fn1.call(1); //Number
fn1.call(''); //String
fn1.call(true); //Boolean
fn1.call(fn2); //function fn2(){}
fn1.call(c); //Object
複製代碼
若是還不理解上面的,不要緊,咱們再來看一個栗子:
function class1(){
this.name = function(){
console.log("我是class1內的方法", this);
}
}
function class2() {
class1.call(this);
}
var f = new class2();
f.name(); //調用的是class1內的方法,將class1的name方法交給class2使用, 在class1中輸出this, 能夠看到指向的是class2
複製代碼
函數class1調用call方法,並傳入this(this爲class2構造後的的對象),傳入的this對象替換class1的this對象,並執行class1函數體實現了class1的上下文(確切地說算僞繼承,原型鏈纔算得上真繼承)。也就是修改了class1內部的this指向,你看懂了嗎?
再來看幾個經常使用的栗子,增強一下印象。
function eat(x,y){
console.log(x+y);
console.log(this);
}
function drink(x,y){
console.log(x-y);
console.log(this);
}
eat.call(drink,3,2);
輸出:5
那麼這個this呢? 是drink;
複製代碼
這個栗子中的意思就是用eat臨時調用了(或說實現了)一下drink函數,eat.call(drink,3,2) == eat(3,2) ,因此運行結果爲:console.log(5);直白點就是用drink,代替了eat中的this,咱們能夠在eat中拿到drink的實例;
注意:js 中的函數實際上是對象,函數名是對 Function 對象的引用。
看懂了嗎? 看看下邊這段代碼中輸出的是什麼?
function eat(x,y){
console.log(x+y);
const func = this;
const a = new func(x, y);
console.log(a.names());
}
function drink(x,y){
console.log(x-y);
this.names = function () {
console.log("你好");
}
}
eat.call(drink,3,2); // 5 1 '你好'
複製代碼
繼承(僞繼承)
function Animal(name){
this.name=name;
this.showName=function(){
console.log(this.name);
}
}
function Dog(name){
Animal.call(this,name);
}
var dog=new Dog("Crazy dog");
dog.showName(); // 'Crazy dog'
複製代碼
Animal.call(this) 的意思就是使用Animal對象代替this對象,那麼Dog就能直接調用Animal的全部屬性和方法。
定義:應用某一對象的一個方法,用另外一個對象替換當前對象。
說明:若是 argArray 不是一個有效的數組或者不是 arguments 對象,那麼將致使一個 TypeError。
若是沒有提供 argArray 和 thisObj 任何一個參數,那麼 Global 對象將被用做 thisObj, 而且沒法被傳遞任何參數。
對於 apply、call 兩者而言,做用徹底同樣,只是接受參數的方式不太同樣。這裏就很少作解釋了;直接看call的就能夠了;
call 須要把參數按順序傳遞進去,而 apply 則是把參數放在數組裏。
既然二者功能同樣,那該用哪一個呢?
在JavaScript 中,某個函數的參數數量是不固定的,所以要說適用條件的話,當你的參數是明確知道數量時用 call;而不肯定的時候用apply,而後把參數push進數組傳遞進去。當參數數量不肯定時,函數內部也能夠經過 arguments 這個數組來遍歷全部的參數。
注意:bind是在EcmaScript5中擴展的方法(IE6,7,8不支持),bind() 方法與 apply 和 call 很類似,也是能夠改變函數體內this的指向,可是bind方法的返回值是函數。
MDN的解釋是:bind()方法會建立一個新函數,稱爲綁定函數,當調用這個綁定函數時,綁定函數會以建立它時傳入bind()方法的第一個參數做爲this,傳入bind()方法的第二個以及之後的參數加上綁定函數運行時自己的參數按照順序做爲原函數的參數來調用原函數。
也就是說,區別是,當你但願改變上下文環境以後並不是當即執行,而是回調執行的時候,使用 bind() 方法。而 apply/call 則會當即執行函數。
var bar=function(){
console.log(this.x);
}
var foo={
x:3
}
bar();
bar.bind(foo)();
/*或*/
var func=bar.bind(foo);
func();
輸出:
undefined
3
複製代碼
有個有趣的問題,若是連續 bind() 兩次,亦或者是連續 bind() 三次那麼輸出的值是什麼呢?像這樣:
var bar = function(){
console.log(this.x);
}
var foo = {
x:3
}
var sed = {
x:4
}
var func = bar.bind(foo).bind(sed);
func(); //?
var fiv = {
x:5
}
var func = bar.bind(foo).bind(sed).bind(fiv);
func(); //?
複製代碼
答案是,兩次都仍將輸出 3 ,而非期待中的 4 和 5 。 緣由是,在Javascript中,屢次 bind() 是無效的。更深層次的緣由, bind() 的實現,至關於使用函數在內部包了一個 call / apply ,第二次 bind() 至關於再包住第一次 bind() ,故第二次之後的 bind 是沒法生效的
既然談到實現其原理,那就最好不要在實現代碼裏使用到call、aplly了。否則實現也沒有什麼意義;
目標函數的this指向傳入的第一個對象,參數爲不定長,且當即執行;
實現思路:
Function.prototype.myCall = function (object, ...arg) {
if (this === Function.prototype) {
return undefined; // 用於防止 Function.prototype.myCall() 直接調用
}
let obj = Object(object) || window; // 加入這裏沒有參數,this則要指向window;
obj.fn = this; // 將this的指向函數自己;
obj.fn(...arg); // 對象上的方法,在調用時,this是指向對象的。
delete obj.fn; // 再刪除obj的_fn_屬性,去除影響.
}
複製代碼
在驗證下沒什麼問題(不要在細節):
這是ES6實現的,不使用ES6實現,相對就比較麻煩了,這裏就順便貼一下吧
Function.prototype.myCall = function(obj){
let arg = [];
for(let i = 1 ; i<arguments.length ; i++){
arg.push( 'arguments[' + i + ']' ) ;
// 這裏要push 這行字符串 而不是直接push 值
// 由於直接push值會致使一些問題
// 例如: push一個數組 [1,2,3]
// 在下面👇 eval調用時,進行字符串拼接,JS爲了將數組轉換爲字符串 ,
// 會去調用數組的toString()方法,變爲 '1,2,3' 就不是一個數組了,至關因而3個參數.
// 而push這行字符串,eval方法,運行代碼會自動去arguments裏獲取值
}
obj._fn_ = this;
eval( 'obj._fn_(' + arg + ')' ) // 字符串拼接,JS會調用arg數組的toString()方法,這樣就傳入了全部參數
delete obj._fn_;
}
複製代碼
其實知道call和apply之間的差異,就會發現,它們的實現原理只有一點點差異,那就是後面的參數不同,apply的第二個參數是一個數組,因此能夠拿call的實現方法稍微改動一下就能夠了,以下:
Function.prototype.myApply = function (object, arg) {
let obj = Object(object) || window; // 若是沒有傳this參數,this將指向window
obj.fn = this; // 獲取函數自己,此時調用call方法的函數已是傳進來的對象的一個屬性,也就是說函數的this已經指向傳進來的對象
獲取第二個及後面的全部參數(arg是一個數組)
delete obj.fn(arg); // 這裏不要將數組打散,而是將整個數組傳進去
}
複製代碼
bind方法被調用的時候,會返回一個新的函數,這個新函數的this會指向bind的第一個參數,bind方法的其他參數將做爲新函數的參數。
爲返回的新函數也可使用new操做符,因此在新函數內部須要判斷是否使用了new操做符,須要注意的是怎麼去判斷是否使用了new操做符呢?在解決這個問題以前,咱們先看使用new操做符時具體幹了些什麼,下面是new操做符的簡單實現過程:
function newFun(constructor){
// 第一步:建立一個空對象;
let obj = {};
// 第二步:將構造函數的constructor的原型對象賦值給obj原型;
obj.__proto__ = constructor.prototype;
// 第三步:將構造函數的constructor中的this指向obj,並當即執行構造函數的操做;
constructor.apply(obj);
// 第四步:返回這個對象;
}
複製代碼
new操做符的一個過程至關於繼承,新建立的構造函數的實例能夠訪問構造函數的原型鏈;
在new操做符實現過程的第三步中,會將構造函數constructor中的this指向obj,並當即執行構造函數內部的操做,那麼,當在執行函數內部的操做時,若是不進行判斷是否使用了new,就會致使 " 將構造函數 constructor中的this指向obj " 這一過程失效;
Function.prototype.myBind = function (context, ...args1) {
if (this === Function.prototype) {
throw new TypeError('Error')
}
const _this = this
return function F(...args2) {
// 判斷是否用於構造函數
if (this instanceof F) {
return new _this(...args1, ...args2)
}
return _this.apply(context, args1.concat(args2))
}
}
複製代碼