JavaScript专题之模拟实现call和apply

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

概述

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

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。es6

首先观察 call 和 apply 有什么特色?数组

  1. 被函数调用(函数也是对象),至关于 call 和 apply 是函数的属性
  2. 若是没有传入须要 this 指向对象,那么 this 指向全局对象
  3. 函数执行了
  4. 最后都改变了 this 的指向

1、初步实现

基于 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 还涉及到传参的问题,因此咱们进入到下一环节。app

2、eval 方式

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

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

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 的方式,将参数添加子函数的做用域中。ui

eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码this

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。 大功告成!

3、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男
复制代码

4、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 系列文章

  1. JavaScript之变量及做用域
  2. JavaScript之声明提高
  3. JavaScript之执行上下文
  4. JavaScript之变量对象
  5. JavaScript原型与原型链
  6. JavaScript之做用域链
  7. JavaScript之闭包
  8. JavaScript之this
  9. JavaScript之arguments
  10. JavaScript之按值传递
  11. JavaScript之例题中完全理解this
  12. JavaScript专题之模拟实现call和apply
相关文章
相关标签/搜索