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

本文介绍JavaScript 中的 call 、apply 和 bind 方法的基本使用,使用注意点以及常见的使用场景等,并简单介绍这些方法的实现原理提供对应的源码。
call && apply 方法

call 和 apply 是 JavaScript 中两个重要的常用方法,这两个方法的功能 (作用) 基本上是一样的,都是修改函数内部的 this,并且执行当前函数,如果这个函数是其它对象的成员,那么也可以把它们的功能理解为借用对象的方法并绑定为 this

我们先通过代码来看下call 和 apply的基本使用情况。

javascript 复制代码
// 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
javascript 复制代码
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 和 执行函数,在调用并执行函数的时候需要考虑到参数的传递以及它们传递方式的不同。

javascript 复制代码
/* 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方法的基本使用。

javascript 复制代码
/* 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函数就可以了。

javascript 复制代码
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方法内部的实现可能就会稍微复杂点,特别是它允许两种方式来传递参数,下面给出最终版本的代码供参考。

javascript 复制代码
/* 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;
}
相关推荐
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte7 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc