资讯专栏INFORMATION COLUMN

JavaScript专题之模拟实现call和apply

ky0ncheng / 2215人阅读

摘要:函数可计算某个字符串,并执行其中的的代码男男成功啦实现了函数参数的传递,那么函数返回值怎么处理呢。而且,如果传入的对象是,又该如何处理所以还需要再做一些工作处理返回值返回返回值男男判断传入对象的类型,如果为就指向对象。

本文共 1320 字,读完只需  5 分钟
概述

JS 函数 call 和 apply 用来手动改变 this 的指向,call 和 apply 唯一的区别就在于函数参数的传递方式不同,call 是以逗号的形式,apply 是以数组的形式:

let person1 = {
    name: "person1",
    say: function(age, sex) {
        console.log(this.name + " age: " + age + " sex: " + sex);
    }
}

let person2 = {
    name: "person"
}

person1.say.call(person2, 20, "男");

person1.say.apply(person2, [20, "男"]);

本文就尝试用其他方式来模拟实现 call 和 apply。

首先观察 call 和 apply 有什么特点?

被函数调用(函数也是对象),相当于 call 和 apply 是函数的属性

如果没有传入需要 this 指向对象,那么 this 指向全局对象

函数执行了

最后都改变了 this 的指向

一、初步实现

基于 call 函数是调用函数的属性的特点,call 的 this 指向调用函数,我们可以尝试把调用函数的作为传入的新对象的一个属性,执行后,再删除这个属性就好了。

Function.prototype.newCall = function (context) {
    context.fn = this;  // this 指的是 say 函数
    context.fn();
    delete context.fn;
}

var person = {
    name: "jayChou"
};

var say = function() {
    console.log(this.name);
}

say.newCall(person);  // jayChou

是不是就初步模拟实现了 call 函数呢,由于 call 还涉及到传参的问题,所以我们进入到下一环节。

二、eval 方式

在给对象临时一个函数,并执行时,传入的参数是除了 context 其余的参数。那么我们可以截取 arguments 参数数组的第一个后,将剩余的参数传入临时数组。

在前面我有讲过函数 arguments 类数组对象的特点,arguments 是不支持数组的大多数方法, 但是支持for 循环来遍历数组。

Function.prototype.newCall = function (context) {
    context.fn = this;
    
    let args = [];
    
    for(let i=1; i< arguments.length; i++) {
        args.push("arguments[" + i + "]");
    }
    // args => [arguments[1], arguments[2], arguments[3], ...]
    
    context.fn(args.join(","));  // ???
    delete context.fn;
}

var person = {
    name: "jayChou"
};

var say = function(age, sex) {
    console.log(`name: ${this.name},age: ${age}, sex: ${sex}`);
}

say.newCall(person);

上面传递参数的方式最后肯定是失败的,我们可以尝试 eval 的方式,将参数添加子函数的作用域中。

eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码
Function.prototype.newCall = function (context) {
    context.fn = this;
    
    let args = [];
    
    for(var i=1; i< arguments.length; i++) {
        args.push("arguments[" + i + "]");
    }

    // args => [arguments[1], arguments[2], arguments[3], ...]
    
    eval("context.fn(" + args + ")");
    delete context.fn;
}

var person = {
    name: "jayChou"
};

function say(age, sex) {
    console.log(`name: ${this.name},age: ${age}, sex: ${sex}`);
}

say.newCall(person, 18, "男");  // name: jayChou,age: 18, sex: 男

成功啦!
实现了函数参数的传递,那么函数返回值怎么处理呢。而且,如果传入的对象是 null,又该如何处理?所以还需要再做一些工作:

Function.prototype.newCall = function (context) {
    if (typeof context === "object") {
        context = context || window
    } else {
        context = Object.create(null);
    }
    
    context.fn = this;
    
    let args = [];
    
    for(var i=1; i< arguments.length; i++) {
        args.push("arguments[" + i + "]");
    }

    // args => [arguments[1], arguments[2], arguments[3], ...]
    
    var result = eval("context.fn(" + args + ")");  // 处理返回值
    delete context.fn;
    return result;  // 返回返回值
}

var person = {
    name: "jayChou"
};

function say(age, sex) {
    console.log(`name: ${this.name},age: ${age}, sex: ${sex}`);
    return age + sex;
}

var check = say.newCall(person, 18, "男");
console.log(check); // 18男

判断传入对象的类型,如果为 null 就指向 window 对象。利用 eval 来执行字符串代码,并返回字符串代码执行的结果,就完成了模拟 call。
大功告成!

三、ES 6 实现

前面我们用的 eval 方式可以用 ES6 的解决还存在的一些问题,有没有注意到,这段代码是有问题的。

context.fn = this;

假如对象在被 call 调用前,已经有 fn 属性怎么办?

ES6 中提供了一种新的基本数据类型,Symbol,表示独一无二的值,另外,Symbol 作为属性的时候,不能使用点运算符。所以再加上 ES 的 rest 剩余参数替代 arguments 遍历的工作就有:

Function.prototype.newCall = function (context,...params) {
    if (typeof context === "object") {
        context = context || window
    } else {
        context = Object.create(null);
    }
    let fn = Symbol();
    context[fn] = this
    var result = context[fn](...params);
    
    delete context.fn;
    return result;
}

var person = {
    name: "jayChou"
};

function say(age, sex) {
    console.log(`name: ${this.name},age: ${age}, sex: ${sex}`);
    return age + sex;
}

var check = say.newCall(person, 18, "男");
console.log(check); // 18男
四、apply

apply 和 call 的实现原理,基本类似,区别在于 apply 的参数是以数组的形式传入。

Function.prototype.newApply = function (context, arr) {
    if (typeof context === "object") {
        context = context || window
    } else {
        context = Object.create(null);
    }
    context.fn = this;

    var result;
    if (!arr) {  // 判断函数参数是否为空
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0; i < arr.length; i++) {
            args.push("arr[" + i + "]");
        }
        result = eval("context.fn(" + args + ")");
    }

    delete context.fn;
    return result;
}

es6 实现

Function.prototype.newApply = function(context, parameter) {
  if (typeof context === "object") {
    context = context || window
  } else {
    context = Object.create(null)
  }
  let fn = Symbol()
  context[fn] = this;
  var result = context[fn](...parameter);
  delete context[fn];
  return result;
}
总结

本文通过原生 JS 的 ES5 的方法和 ES 6 的方法模拟实现了 call 和 apply 的原理,旨在深入了解这两个方法的用法和区别,希望你能有所收获。

欢迎关注我的个人公众号“谢南波”,专注分享原创文章。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98832.html

相关文章

  • JavaScript专题模拟实现bind

    摘要:但是三作为构造函数时函数其实还有一个非常重要的特点返回的函数如果作为构造函数,搭配关键字出现的话,我们的绑定就需要被忽略。其次,当返回的函数作为构造函数时,之前绑定的会失效。 本文共 1100 字,读完只需 4 分钟 概述 前一篇文章我们尝试模拟实现了 call 和 apply 方法,其实 bind 函数也可以用来改变 this 的指向。bind 和 call和 apply 两者的区别...

    刘明 评论0 收藏0
  • JavaScript专题模拟实现new

    摘要:模拟实现操作符构造函数返回结果创建一个空对象取传入的第一个参数,即构造函数,并删除第一个参数。二处理返回值构造函数也是函数,有不同类型返回值。有时候构造函数会返回指定的对象内容,所以要对这部分进行处理。 本文共 1230 字,读完只需 5 分钟 写在前面 最近工作太忙,快接近两周没更新博客,总感觉有一些事情等着自己去做,虽然工作内容对自己提升挺大,但我总觉得,一直埋着头走路,偶尔也...

    pingink 评论0 收藏0
  • JavaScript深入系列15篇正式完结!

    摘要:写在前面深入系列共计篇已经正式完结,这是一个旨在帮助大家,其实也是帮助自己捋顺底层知识的系列。深入系列自月日发布第一篇文章,到月日发布最后一篇,感谢各位朋友的收藏点赞,鼓励指正。 写在前面 JavaScript 深入系列共计 15 篇已经正式完结,这是一个旨在帮助大家,其实也是帮助自己捋顺 JavaScript 底层知识的系列。重点讲解了如原型、作用域、执行上下文、变量对象、this、...

    fxp 评论0 收藏0
  • JS专题数组去重

    摘要:将元素作为对象的键,默认键对应的值为如果对象中没有这个键,则将这个元素放入结果数组中去。 前言 数组去重在日常开发中的使用频率还是较高的,也是网上随便一抓一大把的话题,所以,我写这篇文章目的在于归纳和总结,既然很多人都在提的数组去重,自己到底了解多少呢。又或者是如果自己在开发中遇到了去重的需求,自己能想到更好的解决方案吗。 这次我们来理一理怎么做数组去重才能做得最合适,既要考虑兼容性,...

    only_do 评论0 收藏0
  • JavaScript专题递归

    摘要:专题系列第十八篇,讲解递归和尾递归定义程序调用自身的编程技巧称为递归。然而非尾调用函数,就会创建多个执行上下文压入执行上下文栈。所以我们只用把阶乘函数改造成一个尾递归形式,就可以避免创建那么多的执行上下文。 JavaScript 专题系列第十八篇,讲解递归和尾递归 定义 程序调用自身的编程技巧称为递归(recursion)。 阶乘 以阶乘为例: function factorial(n...

    asoren 评论0 收藏0

发表评论

0条评论

ky0ncheng

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<