Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript深入之call和apply的模拟实现 #11

Open
mqyqingfeng opened this issue May 2, 2017 · 221 comments
Open

JavaScript深入之call和apply的模拟实现 #11

mqyqingfeng opened this issue May 2, 2017 · 221 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented May 2, 2017

call

一句话介绍 call:

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

举个例子:

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

注意两点:

  1. call 改变了 this 的指向,指向到 foo
  2. bar 函数执行了

模拟实现第一步

那么我们该怎么模拟实现这两个效果呢?

试想当调用 call 的时候,把 foo 对象改造成如下:

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};

foo.bar(); // 1

这个时候 this 就指向了 foo,是不是很简单呢?

但是这样却给 foo 对象本身添加了一个属性,这可不行呐!

不过也不用担心,我们用 delete 再删除它不就好了~

所以我们模拟的步骤可以分为:

  1. 将函数设为对象的属性
  2. 执行该函数
  3. 删除该函数

以上个例子为例,就是:

// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn

fn 是对象的属性名,反正最后也要删除它,所以起成什么都无所谓。

根据这个思路,我们可以尝试着去写第一版的 call2 函数:

// 第一版
Function.prototype.call2 = function(context) {
    // 首先要获取调用call的函数,用this可以获取
    context.fn = this;
    context.fn();
    delete context.fn;
}

// 测试一下
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call2(foo); // 1

正好可以打印 1 哎!是不是很开心!(~ ̄▽ ̄)~

模拟实现第二步

最一开始也讲了,call 函数还能给定参数执行函数。举个例子:

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.call(foo, 'kevin', 18);
// kevin
// 18
// 1

注意:传入的参数并不确定,这可咋办?

不急,我们可以从 Arguments 对象中取值,取出第二个到最后一个参数,然后放到一个数组里。

比如这样:

// 以上个例子为例,此时的arguments为:
// arguments = {
//      0: foo,
//      1: 'kevin',
//      2: 18,
//      length: 3
// }
// 因为arguments是类数组对象,所以可以用for循环
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
}

// 执行后 args为 ["arguments[1]", "arguments[2]", "arguments[3]"]

不定长的参数问题解决了,我们接着要把这个参数数组放到要执行的函数的参数里面去。

// 将数组里的元素作为多个参数放进函数的形参里
context.fn(args.join(','))
// (O_o)??
// 这个方法肯定是不行的啦!!!

也许有人想到用 ES6 的方法,不过 call 是 ES3 的方法,我们为了模拟实现一个 ES3 的方法,要用到ES6的方法,好像……,嗯,也可以啦。但是我们这次用 eval 方法拼成一个函数,类似于这样:

eval('context.fn(' + args +')')

这里 args 会自动调用 Array.toString() 这个方法。

所以我们的第二版克服了两个大问题,代码如下:

// 第二版
Function.prototype.call2 = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    eval('context.fn(' + args +')');
    delete context.fn;
}

// 测试一下
var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.call2(foo, 'kevin', 18); 
// kevin
// 18
// 1

(๑•̀ㅂ•́)و✧

模拟实现第三步

模拟代码已经完成 80%,还有两个小点要注意:

1.this 参数可以传 null,当为 null 的时候,视为指向 window

举个例子:

var value = 1;

function bar() {
    console.log(this.value);
}

bar.call(null); // 1

虽然这个例子本身不使用 call,结果依然一样。

2.函数是可以有返回值的!

举个例子:

var obj = {
    value: 1
}

function bar(name, age) {
    return {
        value: this.value,
        name: name,
        age: age
    }
}

console.log(bar.call(obj, 'kevin', 18));
// Object {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

不过都很好解决,让我们直接看第三版也就是最后一版的代码:

// 第三版
Function.prototype.call2 = function (context) {
    var context = context || window;
    context.fn = this;

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

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

// 测试一下
var value = 2;

var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call2(null); // 2

console.log(bar.call2(obj, 'kevin', 18));
// 1
// Object {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

到此,我们完成了 call 的模拟实现,给自己一个赞 b( ̄▽ ̄)d

apply的模拟实现

apply 的实现跟 call 类似,在这里直接给代码,代码来自于知乎 @郑航的实现:

Function.prototype.apply = function (context, arr) {
    var context = Object(context) || window;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

下一篇文章

JavaScript深入之bind的模拟实现

重要参考

知乎问题 不能使用call、apply、bind,如何用 js 实现 call 或者 apply 的功能?

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

@mqyqingfeng mqyqingfeng changed the title avaScript深入之call和apply的模拟实现 JavaScript深入之call和apply的模拟实现 May 2, 2017
@JuniorTour
Copy link

受益匪浅!学到了很多,谢谢前辈!
有一个小问题:call2第三版和apply的函数内,是不是不必要 var context =...,直接context=...即可?

@mqyqingfeng
Copy link
Owner Author

哈哈,确实可以,没有注意到这点,感谢指出,(๑•̀ㅂ•́)و✧

@fantasy123
Copy link

arr.push('arguments['+i+']');
请问这里为什么是一个拼接操作呢?

@jawil
Copy link

jawil commented May 17, 2017

eval函数接收参数是个字符串

定义和用法

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

语法:
eval(string)

string必需。要计算的字符串,其中含有要计算的 JavaScript 表达式或要执行的语句。该方法只接受原始字符串作为参数,如果 string 参数不是原始字符串,那么该方法将不作任何改变地返回。因此请不要为 eval() 函数传递 String 对象来作为参数。

简单来说吧,就是用JavaScript的解析引擎来解析这一堆字符串里面的内容,这么说吧,你可以这么理解,你把eval看成是<script>标签。

eval('function Test(a,b,c,d){console.log(a,b,c,d)};Test(1,2,3,4)')
@fantasy123

@mqyqingfeng
Copy link
Owner Author

@jawil 感谢回答哈~
@fantasy123 最终目的是为了拼出一个参数字符串,我们一步一步看:

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

最终的数组为:

var args = [arguments[1], arguments[2], ...]

然后

 var result = eval('context.fn(' + args +')');

在eval中,args 自动调用 args.toString()方法,eval的效果如 jawil所说,最终的效果相当于:

 var result = context.fn(arguments[1], arguments[2], ...);

这样就做到了把传给call的参数传递给了context.fn函数

@lz-lee
Copy link

lz-lee commented May 29, 2017

apply郑航的实现,循环是不是应该从 i = 1 开始?

@mqyqingfeng
Copy link
Owner Author

@lz-lee call 的实现中,是通过 arguments 取各个参数,所以从 1 开始,省略掉为 0 的 context,而apply的实现中,arr 直接就表示参数的数组,循环这个参数数组,直接就从 0 开始。

@lz-lee
Copy link

lz-lee commented May 29, 2017

粗心大意了。感谢提醒。@mqyqingfeng

@lynn1824
Copy link

分析的很透彻,点个赞!

@qianlongo
Copy link

赞赞赞

@mqyqingfeng
Copy link
Owner Author

@lynn1824 @qianlongo 感谢夸奖,写的时候我就觉得模拟实现一遍 call 和 apply 最能让大家明白 call 和 apply 的原理 (~ ̄▽ ̄)~

@lzr900515
Copy link

var context = Object(context) || window; 这里有问题吗?context为null时Object(null)返回空对象,不会被赋值为window

@mqyqingfeng
Copy link
Owner Author

@lzr900515 没有什么问题哈,非严格模式下,指定为 null 或 undefined 时会自动指向全局对象,郑航写的是严格模式下的,我写的是非严格模式下的,实际上现在的模拟代码有一点没有覆盖,就是当值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。

@hujiulong
Copy link

hujiulong commented Jun 13, 2017

context.fn = this;这里似乎漏掉了一个很关键的问题,如果context本来就有fn这个成员怎么办。这里只能给一个原来不存在的名字

var id = 0;
while ( context[ id ] ) {
    id ++;
}
context[ id ] = this;

不过这个方法似乎有点傻

@mqyqingfeng
Copy link
Owner Author

@hujiulong 哈哈,有道理哈~ 确实会覆盖之前对象的方法,还好模拟实现 call 和 apply 的目的在于让大家通过模拟实现了解 call 和 apply 的原理,实际开发的时候还是要直接使用 call 和 apply 的~

@libin1991
Copy link

实在是佩服!

@jasperchou
Copy link

// 以上个例子为例,此时的arguments为:
// arguments = {
//      0: foo,
//      1: 'kevin',
//      2: 18,
//      length: 3
// }
// 因为arguments是类数组对象,所以可以用for循环
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
}

// 执行后 args为 [foo, 'kevin', 18]

// 执行后 args为 [foo, 'kevin', 18]

这一句可能造成误导。结果为:["arguments[1]", "arguments[2]"]
虽然后面确实会用eval执行,但是此处还没有。

@TTTJH
Copy link

TTTJH commented Apr 5, 2022

我问疑问 // 第一版 Function.prototype.call2 = function(context) { // 首先要获取调用call的函数,用this可以获取 context.fn = this; context.fn(); delete context.fn; }

这里的这个怎么指向的直接就是函数

作者在这篇文章中讲的去理解,这里的这个不应该是 Function.prototype 吗?

有没有大佬解答一下~~~

编写的call2函数是作为目标函数(需要被修改this的函数)的方法调用的,我们知道当函数作为方法调用时,该函数内部this指向就是其调用者,在这里call2的调用者就是 那个需要被修改this的函数,个人理解🐻‍❄️

@crystalYY
Copy link

crystalYY commented Apr 5, 2022 via email

@wenwen1995
Copy link

wenwen1995 commented Apr 5, 2022 via email

@shenghuitian
Copy link

shenghuitian commented Apr 5, 2022 via email

@haifeng511
Copy link

haifeng511 commented Apr 19, 2022

// 采用博主思路,完整的的版本,主要是不是用eval函数

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

    let args = [];
    for (let i = 1, len = arguments.length; i < len; i++) {
        args.push(arguments[i])
    }
    // 采用扩展运算符传递参数
    let result = context.fn(...args);

    delete context.fn
    return result;
}
Function.prototype.apply2 = function (context, arr) {
    context = context === null ? window : Object(context);
    context.fn = this;

    let result;
    if (!arr) {
        result = context.fn();
    }
    else {
        let args = [];
        for (let i = 0, len = arr.length; i < len; i++) {
            args.push(arr[i])
        }
        result = context.fn(...args);
    }

    delete context.fn
    return result;
}
// 测试结果正确
var value = 2;

var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call2(null); // 2

console.log(bar.call2(obj, 'kevin', 18));
// 1
// Object {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

console.log('-----------');
bar.apply2(null) // 2
console.log(bar.apply2(obj, ['kevin1', 19]));
// 1
// Object {
//    value: 1,
//    name: 'kevin1',
//    age: 19
// }

@shenghuitian
Copy link

shenghuitian commented Apr 19, 2022 via email

@gongyansheng
Copy link

我发现call 如果传入的是基本数据类型,会自动提升为包装对象,是否也需要处理一下

@crystalYY
Copy link

crystalYY commented May 16, 2022 via email

@shenghuitian
Copy link

shenghuitian commented May 16, 2022 via email

@shenghuitian
Copy link

shenghuitian commented Aug 3, 2022 via email

@wenwen1995
Copy link

wenwen1995 commented Aug 3, 2022 via email

@crystalYY
Copy link

crystalYY commented Aug 3, 2022 via email

@shenghuitian
Copy link

shenghuitian commented Oct 11, 2022 via email

@wenwen1995
Copy link

wenwen1995 commented Oct 11, 2022 via email

@kingOfSoySauce
Copy link

context.fn = this;这里似乎漏掉了一个很关键的问题,如果context本来就有fn这个成员怎么办。这里只能给一个原来不存在的名字

var id = 0;
while ( context[ id ] ) {
    id ++;
}
context[ id ] = this;

不过这个方法似乎有点傻

我也觉得有这个问题,然后问了gpt,发现这里可以用Symbol,很巧妙

  // 将函数本身作为属性添加到 context 上
  const fn = Symbol('fn');
  context[fn] = this;

  // 执行函数并保存结果
  const result = context[fn](...args);

@shenghuitian
Copy link

shenghuitian commented Apr 13, 2023 via email

@crystalYY
Copy link

crystalYY commented Apr 13, 2023 via email

@bosens-China
Copy link

bosens-China commented Oct 6, 2023

Function.prototype.apply = function (context, arr) {
    var context = Object(context) || window;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

var context = Object(context) || window;

这里是错误的,Obejct始终返回Object类型,所以千万不要这样写,而是 context || window;

@shenghuitian
Copy link

shenghuitian commented Oct 6, 2023 via email

@fangyinghua
Copy link

fangyinghua commented Oct 6, 2023 via email

@crystalYY
Copy link

crystalYY commented Oct 6, 2023 via email

@yuu2lee4
Copy link

yuu2lee4 commented Jan 7, 2024

context.fn = this;这里似乎漏掉了一个很关键的问题,如果context本来就有fn这个成员怎么办。这里只能给一个原来不存在的名字

var id = 0;
while ( context[ id ] ) {
    id ++;
}
context[ id ] = this;

不过这个方法似乎有点傻

我也觉得有这个问题,然后问了gpt,发现这里可以用Symbol,很巧妙

  // 将函数本身作为属性添加到 context 上
  const fn = Symbol('fn');
  context[fn] = this;

  // 执行函数并保存结果
  const result = context[fn](...args);

symbol都是es6的东西了

@shenghuitian
Copy link

shenghuitian commented Jan 7, 2024 via email

@crystalYY
Copy link

crystalYY commented Jan 7, 2024 via email

@fangyinghua
Copy link

fangyinghua commented Jan 7, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests