微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

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

2017-05-25 22:36 前端大全

(点击上方公众号,可快速关注)

作者:冴羽 

github.com/mqyqingfeng/Blog/issues/11

如有好文章投稿,请点击 → 这里了解详情


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为 [foo, 'kevin', 18]


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


// 将数组里的元素作为多个参数放进函数的形参里

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.call(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;

}


重要参考


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


深入系列


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


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


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


本系列:


  1. JavaScirpt 深入之从原型到原型链

  2. JavaScript 深入之词法作用域和动态作用域

  3. JavaScript 深入之执行上下文栈

  4. JavaScript 深入之变量对象

  5. JavaScript 深入之作用域链

  6. JavaScript 深入之从 ECMAScript 规范解读 this

  7. JavaScript 深入之执行上下文

  8. JavaScript 深入之闭包

  9. JavaScript 深入之参数按值传递



觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能

 
前端大全 更多文章 前端跨域请求原理及实践 Javascript 深拷贝 我对Flexbox布局模式的理解 怎么找工作,怎么找到工作,怎么找到满意工作? vuejs 初体验— Chrome 插件开发实录
猜您喜欢 ​第3章 对于所有对象都通用的方法 嵌入式企鹅圈邀请到重量级工程师加盟 暨 加速度传感器的原理和应用 怎么用最短时间高效而踏实的学习Python? HUAWEI EMUI用户体验设计的一些事儿 2016阿里巴巴技术论坛邀你共享技术盛宴