call和aply原理

作者: 羽晞yose | 来源:发表于2020-06-09 16:39 被阅读0次

要实现一个方法,就必须知道这个方法做了什么事,这样我们才能一步步实现出来

call有三个特点:

  1. 改变函数this指向
  2. 执行该函数
  3. 不限制参数个数,第一个参数为this指向,从第二个参数开始,全都存在arguments里

Q:以下各输出什么?

function fn () {
    console.log(this, arguments);
}

fn(); // window,因为this指向调用对象,也就是window
fn.call(); // window,因为call的第二个特点是执行该函数,所以这里相当于fn();
fn.call('hello'); // String {"hello"},注意不是字符串,而是一个 Object('hello')
fn.call('hello', 1, 2); // String {"hello"}, Arguments(2) [1, 2]

Q:以下各输出什么?

function fn1() {
    console.log(1);
}

function fn2() {
    console.log(2, arguments);
}

fn1.call(fn2); // 1,改变this指向,但执行的fn1里的输出跟指向没半毛钱关系
fn1.call.call(fn2); // 2,具体往下继续看

模拟call

  1. 接收一个上下文环境context,可以不传,不传则指向window(null也会指向window)
  2. 改变函数this执行,因为不能直接this = context(语法错误),但我们可以在包装后的context上挂载一个fn的属性,该属性等于this
  3. call从第二个参数开始都会归纳arguments里,所以我们需要从1的位置开始取arguments
  4. call会执行,所以最后再将context.fn执行即可
  5. 删除挂载的fn,释放内存
// ES6语法,但ES6有词法作用域,至少我现在都不需要bind或call这些了,所以只是作为了解即可
Function.prototype.callModel = function (context) {
    context = context ? Object(context) : window;
    context.fn = this;

    let args = [...arguments].slice(1);
    context.fn(...args);

    delete context.fn;
}

// ES5语法
Function.prototype.callModel = function (context) {
    context = context ? Object(context) : window;
    context.fn = this;

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

    // 利用数组的toString()特性
    var r = eval('context.fn(' + args +')');
    delete context.fn;
    return r;
}

模拟apply

apply跟call的特点基本是一致的,不同的有以下两点:

  1. 第二个参数接收的是一个数组
  2. 当第二个参数不是数组是,会抛出错误

因此只需要将callModel稍微更改一下便可以得到applyModel。代码如下,就不写ES5语法的了

Function.prototype.applyModel = function (context, args) {
    if (!Array.isArray(args)) throw Error('CreateListFromArrayLike called on non-object');

    context = context ? Object(context) : window;
    context.fn = this;

    // 利用数组的toString()特性
    context.fn(...args);
    delete context.fn;
}

回到上面的问题,为什么fn1.call.call(fn2)就输出2了?
首先,fn1.call,其实就是上面去找原型上的call方法,对应到callModel,其实就是如下代码块

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

    let args = [...arguments].slice(1);
    context.fn(...args);

    delete context.fn;
}

如果这里理解了,那么也就是说,不管有多少个call,实际到调用之前,它都是同一个函数代码块(也就是上面的function)
然后这条函数再去调用call()方法,那么该函数里的context.fn则变成了fn2,最后call会执行,所以context.fn()则变成了fn2(),因此输出变成了2。像这种问题如果理解不了,最好在控制台里调试看一下执行情况。


Q:可以无限.call.call,那么可以apply.apply吗?
A:不能,因为函数会执行两遍。拿callModel来说,第一次执行的是callModel方法,但callModel方法里面会再次执行context.fn(),也就是继续callModel(),这一次context变为fn2,arguments是剩下的参数。
这也是.call.call()后,上述arguments只剩2,3的原因。.apply.apply(),第二次会拿数组第一个参数包装成object对象,再将剩余参与传入,这个时候就出现入参非数组的报错了。

相关文章

网友评论

    本文标题:call和aply原理

    本文链接:https://www.haomeiwen.com/subject/qvcrtktx.html