前端开发系列036-基础篇之call && apply

本文介绍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;
}

posted on 2022-12-10 20:05  文顶顶  阅读(35)  评论(0编辑  收藏  举报

导航