嘮一嘮call、apply和bind以及手動實現(拒絕晦澀難懂)

對我來說,博客首先是一種知識管理工具,其次纔是傳播工具。我的技術文章,主要用來整理我還不懂的知識。我只寫那些我還沒有完全掌握的東西,那些我精通的東西,往往沒有動力寫。炫耀從來不是我的動機,好奇才是。--阮一峯

最近突然想在弄弄基礎的東西了,就盯上了這個,callapplybind的區別、原理到底是什麼,怎麼手動實現了;經過自己的收集總結了這篇文章;

文章分爲理解和實現兩部分,如果你理解這三個方法,可以直接跳到實現的部分;

理解 call、apply、bind

共同點和區別

javascript中,call、apply、bind都是Function對象自帶的方法;
call、apply、bind方法的的共同點和區別:

三者都是用來改變函數的this對象的指向的;

三者的第一個參數都是this要指向的對象,也就是上下文(函數的每次調用都會擁有一個特殊值--本次調用的上下文(context)-- 這就是this的關鍵字的值);

三者都可以利用後續傳參:

call:call([thisObj,arg1,arg2,...);

apply:apply(thisObj,[arg1,arg2,...]);

bind:bind(thisObj,arg1,arg2,...);

bind 是返回對應函數,便於稍後調用,apply、call則是立即調用

call

定義: 調用一個對象的調用一個對象的一個方法,以另一個對象替換當前對象。

說明: call 方法可以用來代替另一個對象調用一個方法。

thisObj的取值有以下4種情況:

  • 1 不傳,或者傳null,undefined, 函數中的this指向window對象;
  • 2 傳遞另一個函數的函數名,函數中的this指向這個函數的引用;
  • 3 傳遞字符串、數值或布爾類型等基礎類型,函數中的this指向其對應的包裝對象,如 String、Number、Boolean;
  • 4 傳遞一個對象,函數中的this指向這個對象;

再來看一下w3school上的解釋

是不是不太好理解!

代碼試驗一下可能會更加的直觀:

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的所有屬性和方法。


apply

定義:應用某一對象的一個方法,用另一個對象替換當前對象。

說明:如果 argArray 不是一個有效的數組或者不是 arguments 對象,那麼將導致一個 TypeError。

如果沒有提供 argArray 和 thisObj 任何一個參數,那麼 Global 對象將被用作 thisObj, 並且無法被傳遞任何參數。

對於 apply、call 二者而言,作用完全一樣,只是接受參數的方式不太一樣。這裏就不多做解釋了;直接看call的就可以了;

call 需要把參數按順序傳遞進去,而 apply 則是把參數放在數組裏。

既然兩者功能一樣,那該用哪個呢?

在JavaScript 中,某個函數的參數數量是不固定的,因此要說適用條件的話,當你的參數是明確知道數量時用 call;而不確定的時候用apply,然後把參數push進數組傳遞進去。當參數數量不確定時,函數內部也可以通過 arguments 這個數組來遍歷所有的參數。


bind

注意: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了。不然實現也沒有什麼意義;

call(obj,arg,arg....)

目標函數的this指向傳入的第一個對象,參數爲不定長,且立即執行;

實現思路

  • 改變this指向:可以將目標函數作爲這個對象的屬性
  • 利用arguments類數組對象實現參數不定長
  • 不能增加對象的屬性,所以在結尾需要delete
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_;
}

aplly(obj,[...arg])

其實知道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

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))
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章