Skip to content

从underscore源码看如何实现map函数(二) #14

@webproblem

Description

@webproblem

在上篇文章中,遗留了两个问题:

  • arguments 存在性能问题
  • call 比 apply 速度更快

本篇文章将会对这两个问题进行详细的分析。

arguments 存在性能问题

arguments 是存在于函数内部用于存储传递给函数的参数的类数组对象,在函数被调用时创建。arguments 是类数组对象,只拥有 length 属性,但是可以在函数内部转换为数组。

function test() {
    var args = Array.prototype.slice.call(arguments);
    console.log(args); // ["白展堂", "吕秀才"]
    // var args2 = [].slice.call(arguments);
    // console.log(args2);
}
test('白展堂', '吕秀才');

但是对参数使用 slice 会阻止某些 JavaScript 引擎中的优化,引发性能问题。所以将 arguments 对象转换为数组应该采用如下方法:

var args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments));
var args = Array.from(arguments);
var args = [...arguments];

我们来测试下,性能差距究竟有多大。

function sum() {
	var args = Array.prototype.slice.call(arguments);
	return args;
}
function sum1() {  
    var args = Array.apply(null, arguments); 
	return args;
}
function leaksArguments(number) {
	var t = new Date;
	for(var i=0; i<100000; i++) {
		if(!number) {
			sum(1,2,3,4,5,6);
		}else {
			sum1(1,2,3,4,5,6);
		}
	}
	console.log(`耗时:${new Date - t}`);
}

简单写了一个测试用例,分别得出两种将 arguments 对象转换成数组的方法的耗时情况,执行 leaksArguments 函数时不传参数使用的是 Array.prototype.slice.call 方式转换,传参使用的是 Array.apply 方式转换。测试结果如下:

image

可以看到,性能提升了2倍多,且随着传递给函数的参数越多,性能耗时差距就越大。这也说明,JavaScript 引擎在访问 arguments 时会消耗性能。

那么如何安全地使用 arguments 呢?

  • 只使用 arguments.length 属性。
  • 只是用 arguments[i] ,需要始终为 arguments 的合法整型索引,且不允许越界 。
  • 除了 .length[i],不要直接使用 arguments
  • 严格来说用 fn.apply(y, arguments) 是没问题的,但除此之外都不行(例如 .slice)。 Function#apply 是特别的存在。
  • 请注意,给函数添加属性值(例如 fn.$inject = ...)和绑定函数(即 Function#bind 的结果)会生成隐藏类,因此此时使用 #apply 不安全。

如果按照上面的方式正确使用 arguments 对象,就不必担心使用 arguments 导致性能消耗问题。

call 比 apply 速度更快

call 和 apply 实现的功能都是一样的,都是为了改变函数在运行时的上下文,只是接收的参数方式不同,call 方法从第二个参数开始是一系列参数列表,而 apply 方法则是把参数放在数组里。

为了证明 call 的速度比 apply 更快,来写一个简单的测试。

var foo = {
    color: 'blue'
}
function bar(name) {
    return name + this.color;
}
function test(number) {
	var t = new Date;
	for(var i=0; i< 100000; i++) {
		if(!number) {
			bar.call(foo, '天空的颜色');
		}else {
			bar.apply(foo, ['天空的颜色']);
		}
	}
	console.log(`${!number?'call':'apply'}耗时:${new Date - t}`);
}

测试结果如下:

image

当然,可以推荐在一个性能测试网站 jsperf 上进行性能测试。下面是几个简单的测试结果。

image

image

想要知道 call 和 apply 在性能上为什么会有差异,就得知道 call 和 apply 在执行过程中发生了什么。简单来说就是 call 和 apply,最终都是调用一个叫做 [[Call]] 的内部函数 ,apply 方法执行过程中对参数的处理更为复杂,需要进行检测数组参数和格式化等步骤,而 call 方式的参数原本就是按照顺序排列的参数列表,处理步骤更为简洁。 关于具体的执行步骤可参见 stackoverflow 的回答

需要知道的是,关于 call 和 apply 性能差异问题也只是存在于 ES6 之前,随着 ECMAScript 语言和 JavaScript 解释器性能不断增强,call 和 apply 性能大致一样了。call 和 apply 是存在于不同场景下的,我们应该更加注重两个函数在实际应用场景中如何选择合适的方式来实现需求的效果。

参考

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions