1. 程式人生 > 其它 >前端開發系列036-基礎篇之call && apply

前端開發系列036-基礎篇之call && apply

title: '前端開發系列036-基礎篇之call && apply'
tags:
  - javaScript系列
categories: []
date: 2017-09-18 20:21:13
本文介紹JavaScript 中的 call 、apply 和 bind 方法的基本使用,使用注意點以及常見的使用場景等,並簡單介紹這些方法的實現原理提供對應的原始碼。 call && apply 方法

call 和 apply 是 JavaScript 中兩個重要的常用方法,這兩個方法的功能 (作用) 基本上是一樣的,都是修改函式內部的 this,並且執行當前函式,如果這個函式是其它物件的成員,那麼也可以把它們的功能理解為借用物件的方法並繫結為 this

我們先通過程式碼來看下call 和 apply的基本使用情況。

// call && apply基本使用
// (1) 修改函式中的 this 
// (2) 執行修改了this後的函式

/* 演示程式碼-01 */
function f1() {
    console.log("f1-1-this->", this)
}

/* 001-直接呼叫函式 */
f1();
/* 列印結果:f1-1-this->window */

/* 002-通過 call 和 apply 呼叫函式 */
f1.call({ name: "zs" });
f1.apply({ name: "zs" });
/* 列印結果:f1-1-this->{ name: "zs" } */
/* 列印結果:f1-1-this->{ name: "zs" } */

/* 演示程式碼-02 */
function a() {
    console.log("a-1-this->", this)
}

function b() {
    console.log("b-1-this->", this)
}

a();                  /* a-1-this->window  */
b();                  /* b-1-this->window  */
a.call(b);            /* a-1-this->function b */
a.call.call.call(b);  /* b-1-this->window */

/* 演示程式碼-03 */
let o1 = { name: "Yong", showName() { console.log("姓名:" + this.name) } };
let o2 = { name: "Xia" };

// o1.showName(); /* 姓名:Yong */
// o2.showName(); /* 報錯:Uncaught TypeError: o2.showName is not a function */

/* 相當於是 o2.showName()  */
o1.showName.call(o2);  /* 姓名:Xia */
o1.showName.apply(o2); /* 姓名:Xia */

call 和 apply的基本功能一樣,但使用時也存在一些差異,體現在兩個方面。

  • 引數的傳遞方式不同,call通過引數列表方式傳遞,apply則通過陣列的方式傳遞
  • 形參(期望傳遞的引數)的個數不同,call方法的形參個數為0,而 apply方法的形參個數為1
let o1 = {
    name: "Yong",
    show() {
        console.log("姓名:" + this.name + " Other:", arguments);
    }
};
let o2 = { name: "Xia" };

/* (1) 引數的傳遞方式不同 */
o1.show();                          /* 姓名:Yong Other: */
o1.show.call(o2);                   /* 姓名:Xia Other: */
o1.show.call(o2, 100, 200, "abc");  /* 姓名: Xia Other: Arguments(3)[100, 200, "abc"] */
o1.show.apply(o2, [10, 20, "abc"]); /* 姓名: Xia Other: Arguments(3)[100, 200, "abc"] */

/* (2) 形參個數不同 */
console.log(Function.prototype.call === o1.show.call);                      /* true */
console.log(Function.prototype.call.length, Function.prototype.apply.length)/* 1,2 */

基於 call 方法和 apply 方法的基本功能和它們的差異,下面試著給出這兩個方法的實現原理( 原始碼 ),因為所有的函式都能夠呼叫這兩個方法,因此這兩個方法自然應該被實現在Function.prototype上面,內部的實現主要處理兩個工作,即修改 this 和 執行函式,在呼叫並執行函式的時候需要考慮到引數的傳遞以及它們傳遞方式的不同。

/* call 原理 */
Function.prototype.call = function(context) {
    /* 01-上下文環境的容錯處理,如果context是原始型別那麼就先包裝 */
    context = context ? Object(context) : window;

    /* 02-獲取方法並把該方法新增到當前的物件上 */
    context.f = this;

    /* 03-拿到引數列表(剔除了繫結 this的第一個引數) */
    let args = [];
    for (let i = 1; i < arguments.length; i++) {
        args.push(arguments[i]);
    }

    /* 04-呼叫並執行函式,利用了陣列的 toString來處理引數 */
    return eval("context.f(" + args + ")");
}

/* apply 原理 */
Function.prototype.apply = function(context, args) {
    /* 01-上下文環境的容錯處理,如果context是原始型別那麼就先包裝 */
    context = context ? Object(context) : window;

    /* 02-獲取方法並把該方法新增到當前的物件上 */
    context.f = this;

    /* 03-如果沒有以陣列傳遞引數那麼就直接呼叫並返回*/
    if (!args) {
        return context.f();
    }

    /* 04-如果以陣列傳遞了引數那麼就利用 eval 來執行函式並返回結果 */
    return eval("context.f(" + args + ")");
}


/* 測試程式碼 */
let o1 = {
    name: "Yong",
    show() {
        console.log("姓名:" + this.name + " Other:", arguments);
    }
};

let o2 = { name: "Xia" };
o1.show.call(o2, 10, 20, 30);       /* 姓名:Xia Other: Arguments(3) [10, 20, 30] */
o1.show.apply(o2, [100, 200, 300]); /* 姓名:Xia Other: Arguments(3) [100, 200, 300] */

console.log(Function.prototype.call === o1.show.call);                       /* true */
console.log(Function.prototype.call.length, Function.prototype.apply.length) /* 1,2  */
bind 方法

在 JavaScript 中,其實現在bind方法用的已經比較少了,我個人的感覺是因為這個方法使用起來相對於 call 或者是 apply 來說會比較麻煩,而且可讀性不好,bind方法的功能和 call 很像,它也能過繫結函式中的 this,區別在於該方法並不執行函式,而是把綁定了(修改了) this後的函式返回。

在下面通過一段程式碼來簡單演示bind方法的基本使用。

/* bind 方法的基本使用                */
/* (1) 繫結函式中的 this              */
/* (2) 把繫結 this 後的函式返回        */
/* (3) 允許多種傳參的方式              */
/* (4) 可以通過 new 來呼叫目標函式      */
/* (5) 例項化物件能找到原類的原型物件    */

/* 演示程式碼-01 */
let o1 = {
    name: "Yong",
    show() {
        console.log("姓名:" + this.name + " Other:", arguments);
    }
};

let o2 = { name: "Xia" };
let fnc = o1.show.bind(o2);

fnc(10, 20, 30); /* 姓名:Xia Other: Arguments(3) [10, 20, 30] */


/* 演示程式碼02 */
f1.prototype.say = function() { console.log("say ...") }

function f1(a, b, c) {
    console.log("f1-this->", this, a, b, c);
}

function f2() {
    console.log("f2-this->", this);
}

/* [1] 允許兩種傳參方式: */
/* 方式1 */
// let F = f1.bind(f2,10,20,30);
// F();                   /* f1-this-> ƒ f2()  10,20,30 */

/* 方式2 */
let F = f1.bind(f2, 10);
F(20, 30); /* f1-this-> ƒ f2()  10,20,30 */


/* [2] 通過 new 來呼叫目標函式 */
/* 註解:例項化的物件 f 建構函式為原先的函式 f1 */
let f = new F(200, 300); /* f1-this-> f1 {} 10 200 300 */
console.log(f); /* f1 {} */

/* [3] 例項化的物件可以找到原先建構函式的原型物件 */
f.say(); /* say ... */

如果僅僅是處理修改函式中的 this 並把函式返回,那麼bind方法在實現上會簡單很多,似乎只需要像下面這樣來在 Function.prototype上面新增一個 bind函式就可以了。

Function.prototype.bind = function(context) {
    let that = this;
    return function() {
        that.call(context);
    }
}

/* 測試程式碼 */
function fn1() {
    console.log("fn1-", this)
}

function fn2() {
    console.log("fn2-", this)
}

let fn = fn1.bind(fn2);
fn(); /* fn1- ƒ fn2() */

但是如果需要把引數的傳遞以及建構函式的呼叫等因素都考慮進去,那麼bind方法內部的實現可能就會稍微複雜點,特別是它允許兩種方式來傳遞引數,下面給出最終版本的程式碼供參考。

/* bind 方法的實現原理 */
Function.prototype.bind = function(context) {
    let that = this;

    /* 獲取第一部分引數 : ex 獲取 let F = f1.bind(f2, 10); 中的10*/
    let argsA = [].slice.call(arguments, 1); /* [10] */

    function bindFunc() {
        /* 獲取第二部分的引數:ex 獲取 F(20, 30); 中的 20 和 30 */
        let argsB = [].slice.call(arguments); /* [20,30] */
        let args = [...argsA, ...argsB];
        return that.apply(this instanceof bindFunc ? this : context, args);
    }

    /* 原型處理 */
    function Fn() {};
    Fn.prototype = this.prototype;
    bindFunc.prototype = new Fn();

    /* 返回函式 */
    return bindFunc;
}