1. 知识点补充:

首先在模拟实现前,先Mark一些我之前不知道的知识:

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

其中,string是必需传入的待计算或待执行的语句,并且必须是原始字符串的形式!

eval(string)相当于<script> string </script>

b. 类数组对象(Array-like Object)

类数组对象是一个对象,比如:arguments、DOM API返回的NodeList对象都属于类数组对象,具有指向对象元素的数组index下标和length属性,但是它们不能使用push/pop/shift/unshift等数组方法!

但是如何能将类数组转换为真正的数组呢?有如下方法:

  1. Array.prototype.slice.call( arguments )  // 在低版本IE下不支持
  2. [].slice.call( arguments )      // 等同于1
  3. let arr = Array.from( arguments )  // ES6,可将类数组对象和可遍历对象转为真正的数组
  4. let arr = [ ...arguments ]

以下例为例演示:

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(this.value);
   console.log(name)
   console.log(age) } bar.call(foo, 'ning', 20);

这里可以考虑将bar这个函数作为foo的一个方法,然后在外层执行这个函数,然后再删掉该函数即可!

2. call的模拟实现

    Function.prototype.call2 = function (context) {
        context.fn = this;  // context是foo,this是bar也就是调用call的那个函数
        context.fn();
        delete context.fn;
    }

    // 使用下例来验证call2是否可行
    var foo = {
        value: 1
    }
    function bar() {
        console.log(this.value);
    }

    bar.call2(foo);

content.fn = this;这句首先获取到了调用call的函数,本例这里也就是bar;

context.fn();即执行bar这个函数;

delete删掉该函数。

 

但是现在的模拟中有几个问题:

  1. 不能传入参数,因此我们将利用arguments,从Arguments对象中从第二个参数(因为第一个参数是this)开始取值,放到一个数组里,再把这个数组放到要执行的函数的参数里
  2. this参数传入null或者undefined时,我们需要将this指向window
  3. 当call2()内传的不是一个对象,而是一个基本数据类型时,如何处理?(在call实现时会自动调用Object()转换)
  4. 函数可以有返回值

所以我们得到以下call2()代码:

    Function.prototype.call2 = function (context) {
        context = context ? Object(context) : window;  
        context.fn = this;

        var arr = [];
        for (var i = 1, len = arguments.length; i < len; i++) {
            arr.push('arguments[' + i + ']');
        }

        var result = eval('context.fn(' + arr + ')');
        delete context.fn;
        return result;
    }

下面我们测试一下:

    var value = 'global';
    var foo = {
        value: 1
    }
    function bar(name, age) {
        console.log(this.value)
        return {
            value: this.value,
            name: name,
            age: age
        }
    }

    bar.call2(null)  // global

    console.log(bar.call2(foo, 'ning', 20))
    // 1
    // {value: 1, name: "ning", age: 20}

说明两点:

  1. arr.push('arguments['+ i +']');这句得到的是(2) ["arguments[1]", "arguments[2]"]一个新数组,是我们想要的
  2. eval('context.fn('+ arr +')');这句中arr会自动调用arr.toString()得到一个字符串:arguments[1],arguments[2],然后进行字符串拼接

下面给出ES6版本的:

    Function.prototype.call2 = function (context) {
        context = context ? Object(context) : window; 
        context.fn = this;

        let arr = [...arguments].slice(1);
        let result = context.fn(' + arr + ');

        delete context.fn;
        return result;
    }

 

3. apply的模拟实现:

    Function.prototype.apply2 = function (context, arr) {
        context = context ? Object(context) : window;
        context.fn = this;

        var result = [];
        // 没有arr参数直接执行
        if (!arr) {
            result = context.fn();
            // 有arr参数则将参数拼接后执行
        } 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.apply2 = function (context, arr) {
        context = context ? Object(context) : window;
        context.fn = this;

        let result = [];
        if (!arr) {
            result = context.fn();
        } else {
            // ...arr的使用
            result = context.fn(...arr)
        }

        delete context.fn;
        return result;
    }