JavaScript专题之模拟实现call和apply

栏目: JavaScript · 发布时间: 5年前

内容简介:JS 函数 call 和 apply 用来手动改变 this 的指向,call 和 apply 唯一的区别就在于函数参数的传递方式不同,call 是以逗号的形式,apply 是以数组的形式:本文就尝试用其他方式来模拟实现 call 和 apply。首先观察 call 和 apply 有什么特点?

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 有什么特点?

  1. 被函数调用(函数也是对象),相当于 call 和 apply 是函数的属性
  2. 如果没有传入需要 this 指向对象,那么 this 指向全局对象
  3. 函数执行了
  4. 最后都改变了 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 的原理,旨在深入了解这两个方法的用法和区别,希望你能有所收获。

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

JavaScript专题之模拟实现call和apply

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

XML Hacks

XML Hacks

Michael Fitzgerald / O'Reilly Media, Inc. / 2004-07-27 / USD 24.95

Developers and system administrators alike are uncovering the true power of XML, the Extensible Markup Language that enables data to be sent over the Internet from one computer platform to another or ......一起来看看 《XML Hacks》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具