本文共 1320 字,读完只需 5 分钟
概述 JS 函数 call 和 apply 用来手动改变 this 的指向,call 和 apply 唯一的区别就在于函数参数的传递方式不同,call 是以逗号的形式,apply 是以数组的形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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。
首先观察 call 和 apply 有什么特点?
被函数调用(函数也是对象),相当于 call 和 apply 是函数的属性
如果没有传入需要 this 指向对象,那么 this 指向全局对象
函数执行了
最后都改变了 this 的指向
一、初步实现 基于 call 函数是调用函数的属性的特点,call 的 this 指向调用函数,我们可以尝试把调用函数的作为传入的新对象的一个属性,执行后,再删除这个属性就好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Function .prototype.newCall = function (context ) { context.fn = this ; context.fn(); delete context.fn; }; var person = { name: "jayChou" }; var say = function ( ) { console .log(this .name); }; say.newCall(person);
是不是就初步模拟实现了 call 函数呢,由于 call 还涉及到传参的问题,所以我们进入到下一环节。
二、eval 方式 在给对象临时一个函数,并执行时,传入的参数是除了 context 其余的参数。那么我们可以截取 arguments 参数数组的第一个后,将剩余的参数传入临时数组。
在前面我有讲过函数 arguments 类数组对象的特点,arguments 是不支持数组的大多数方法, 但是支持 for 循环来遍历数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Function .prototype.newCall = function (context ) { context.fn = this ; let args = []; for (let i = 1 ; i < arguments .length; i++) { args.push("arguments[" + i + "]" ); } 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 的方式,将参数添加子函数的作用域中。
eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Function .prototype.newCall = function (context ) { context.fn = this ; let args = []; for (var i = 1 ; i < arguments .length; i++) { args.push("arguments[" + i + "]" ); } 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 , "男" );
成功啦! 实现了函数参数的传递,那么函数返回值怎么处理呢。而且,如果传入的对象是 null,又该如何处理?所以还需要再做一些工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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 + "]" ); } 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);
判断传入对象的类型,如果为 null 就指向 window 对象。利用 eval 来执行字符串代码,并返回字符串代码执行的结果,就完成了模拟 call。 大功告成!
三、ES 6 实现 前面我们用的 eval 方式可以用 ES6 的解决还存在的一些问题,有没有注意到,这段代码是有问题的。
假如对象在被 call 调用前,已经有 fn 属性怎么办?
ES6 中提供了一种新的基本数据类型,Symbol,表示独一无二的值,另外,Symbol 作为属性的时候,不能使用点运算符。所以再加上 ES 的 rest 剩余参数替代 arguments 遍历的工作就有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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);
四、apply apply 和 call 的实现原理,基本类似,区别在于 apply 的参数是以数组的形式传入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 实现
1 2 3 4 5 6 7 8 9 10 11 12 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 的原理,旨在深入了解这两个方法的用法和区别,希望你能有所收获。