JavaScript之call和apply原理解析及模拟实现
call() 和 apply()的区别在于,call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组
var func = function(arg1, arg2) {
...
};
func.call(this, arg1, arg2); // 使用 call,参数列表
func.apply(this, [arg1, arg2]) // 使用 apply,参数数组
call() 和 apply()的常用用法
1.合并数组
var a = ['apple', 'banana'];
var b = ['orange', 'garde'];
// 将第二个数组融合进第一个数组
// 相当于 a.push('orange', 'garde');
Array.prototype.push.apply(a, b);
// 4
vegetables;
// ['apple', 'banana', 'orange', 'garde']
当第二个数组太大时,不要使用这个方法来合并数组,因为一个函数能够接受的参数个数是有限制的。不同的引擎有不同的限制,JS核心限制在 65535,有些引擎会抛出异常,有些不抛出异常但丢失多余参数。
我们可以用将参数数组切块后循环传入目标方法,代码如下:
function concatOfArray(arr1, arr2) {
var QUANTUM = 32768;
// 利用循环切割数组,进行合并
for (var i = 0, len = arr2.length; i < len; i += QUANTUM) {
Array.prototype.push.apply(
arr1,
arr2.slice(i, Math.min(i + QUANTUM, len) )
);
}
return arr1;
}
// 验证
var arr1 = [-3, -2, -1];
var arr2 = [];
for(var i = 0; i < 1000000; i++) {
arr2.push(i);
}
//Array.prototype.push.apply(arr1, arr2);
//报错:Uncaught RangeError: Maximum call stack size exceeded
concatOfArray(arr1, arr2);
// 结果:(1000003) [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
2、获取数组中的最大值,最小值
var numbers = [5, 458 , 120 , -215 ];
Math.max.apply(Math, numbers); //458
Math.max.call(Math, 5, 458 , 120 , -215); //458
// ES6中写法
Math.max.call(Math, ...numbers); // 458
数组 numbers 本身没有 max方法,这里借助 call / apply 使用 Math.max 方法。
3.验证是否是数组
function isArray(obj){
return Object.prototype.toString.call(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true
// 直接使用 toString()
[1, 2, 3].toString(); // "1,2,3"
"123".toString(); // "123"
123.toString(); // SyntaxError: Invalid or unexpected token
Number(123).toString(); // "123"
Object(123).toString(); // "123"
可以通过toString()
来获取每个对象的类型,但是不同对象的 toString()有不同的实现,所以通过 Object.prototype.toString()
来检测,需要以 call() / apply()
的形式来调用,传递要检查的对象作为第一个参数。
var toStr = Function.prototype.call.bind(Object.prototype.toString);
function isArray(obj){
return toStr(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true
// 使用改造后的 toStr
toStr([1, 2, 3]); // "[object Array]"
toStr("123"); // "[object String]"
toStr(123); // "[object Number]"
toStr(Object(123)); // "[object Number]"
上面方法首先使用 Function.prototype.call
函数指定一个 this
值,然后 .bind
返回一个新的函数,始终将 Object.prototype.toString
设置为传入参数。其实等价于 Object.prototype.toString.call()
。
注意:前提是toString()方法没有被覆盖
Object.prototype.toString = function() {
return '';
}
isArray([1, 2, 3]);
// false
4.调用父构造函数实现继承
function SuperType(){
this.color=["red", "green", "blue"];
}
function SubType(){
// 核心代码,继承自SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color);
// ["red", "green", "blue", "black"]
var instance2 = new SubType();
console.log(instance2.color);
// ["red", "green", "blue"]
在子构造函数中,通过调用父构造函数的call方法来实现继承。
缺点:
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现复用,每个子类都有父类实例函数的副本,影响性能
call的模拟实现
var value = 1;
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
call()主要有以下两点
- call()改变了this的指向
- 函数 bar 执行了
模拟实现第一步
如果在调用call()的时候把函数 bar()添加到foo()对象中,即如下
var foo = {
value: 1,
bar: function() {
console.log(this.value);
}
};
foo.bar(); // 1
上面的代码改变了this的指向并且执行了函数bar。
这种方法给foo额外添加了一个属性
解决方法很简单,用 delete 删掉就好了。
实现下面3步就可以模拟实现了。
- 将函数设置为对象的属性:foo.fn = bar
- 执行函数:foo.fn()
- 删除函数:delete foo.fn
// 第一版
Function.prototype.call2 = function(context) {
// 首先要获取调用call的函数,用this可以获取
context.fn = this; // foo.fn = bar
context.fn(); // foo.fn()
delete context.fn; // delete foo.fn
}
// 测试一下
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call2(foo); // 1
模拟实现第二步
第一版代码函数 bar 不能接收参数,我们可以从 arguments中获取参数,取出第二个到最后一个参数放到数组中。
抛弃第一个数组是因为第一个参数是 this。
//第二版
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
参数数组搞定了,接下来要做的就是执行函数 context.fn()。
context.fn( args.join(',') );
上面直接调用肯定不行,args.join(’,’)会返回一个字符串,并不会执行。
这边采用 eval方法来实现,拼成一个函数。
eval('context.fn(' + args +')')
上面代码中args 会自动调用 args.toString() 方法,因为'context.fn(' + args +')'
本质上是字符串拼接,会自动调用toString()方法,如下代码:
var args = ["a1", "b2", "c3"];
console.log(args);
// ["a1", "b2", "c3"]
console.log(args.toString());
// a1,b2,c3
console.log("" + args);
// a1,b2,c3
第二版的代码如下:
// 第二版
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
模拟实现第三步
还需要注意的几点如下:
- this 参数可以传 null 或者 undefined,此时 this 指向 window
- this 参数可以传基本类型数据,原生的 call 会自动用 Object() 转换
- 函数是可以有返回值的
第三版代码如下
// 第三版
Function.prototype.call2 = function (context) {
context = context ? Object(context) : window; // 实现细节 1 和 2
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; // 实现细节 2
}
// 测试一下
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
function foo() {
console.log(this);
}
bar.call2(null); // 2
foo.call2(123); // Number {123, fn: ƒ}
bar.call2(obj, 'kevin', 18);
// 1
// {
// value: 1,
// name: 'kevin',
// age: 18
// }
call和apply模拟实现汇总
call的模拟实现
Function.prototype.call = function (context) {
context = context ? Object(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;
}
ES6:
Function.prototype.call = function (context) {
context = context ? Object(context) : window;
context.fn = this;
let args = [...arguments].slice(1);
let result = context.fn(...args);
delete context.fn
return result;
}
apply的模拟实现
Function.prototype.apply = function (context, arr) {
context = 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;
}
ES6:
Function.prototype.apply = function (context, arr) {
context = context ? Object(context) : window;
context.fn = this;
let result;
if (!arr) {
result = context.fn();
} else {
result = context.fn(...arr);
}
delete context.fn
return result;
}