本文作者:IMWeb Terrance 原文出处:IMWeb社区 未经同意,禁止转载 ECMAScript 6 (or ECMAScript 2015) is the newest version of the ECMAScript standard and has remarkably improved parameter handling in JavaScript. We can now use rest parameters, default values and destructuring, among other new features.
PS:这篇文章主体是根据Faraz Kelhini的文章(见引用1)翻译而来,加入了自己的一些理解。
随着ES6的出现,javascript具备了很多新的特性,很多特性不仅仅在语法上更加清晰简洁,同时也提高了效率和可靠性,便于后期扩展和维护。不过很多新特性普及度并不高,通过学习ES6的一些特性后,可以更好地将其运用到实际项目中,对于浏览器支持度,我觉得可以乐观一些,毕竟ES6是趋势,而且现在也有诸如babel这类工具可以帮助我们将ES6转换为ES5来实现兼容,所以开发中去用ES6,何乐而不为呢?
Arguments和Parameters经常被用于表述函数参数,通常两者并没有进行区分。为了后面讲解更加清晰,这里对二者进行一个区分:Arguments指实际传递给函数的所有参数,这和其他语言里实参的概念很像,同时也和function作用域中的arguments对象所表示的参数吻合;Parameters是指函数定义的时候所声明的变量名,这和其他语言里形参的概念比较像。需要注意的是,在javascript中Arguments和Parameters在参数类型(由于javascript为弱类型语言,所以在参数声明时并没有指定类型)和数量上都可以不同。
function foo(param1, param2) {
// do something
}
foo(10, 20, 30);
比如,在上面这个例子中,10、20、30为Arguments,param1、param2为Parameters。
在ECMAScript 5中我们经常需要使用apply()这类转换工具将数组传递给函数,比如采用Math.max()求数组中最大元素,由于该方法不支持数组作为参数,而apply()方法可以将数组转换为单独的元素,所以通常会像下面这样处理:
var myArray = [5, 10, 50];
Math.max(myArray); // Error: NaN
Math.max.apply(Math, myArray); // 50
幸运的是,在ECMAScript 6中引入了扩展运算符(...),不需要借助apply(),使用...便可以轻松将数组转换为多个参数:
var myArray = [5, 10, 50];
Math.max(...myArray); // 50
扩展运算符除了能够取代apply()来将数组拆分为单个元素外,还提供了更直观的语义和灵活性,比如在一次函数调用中可以多次使用,也能和其他常规Arguments混合使用。
function myFunction() {
for(var i in arguments){
console.log(arguments[i]);
}
}
var params = [10, 15];
myFunction(5, ...params, 20, ...[25]); // 5 10 15 20 25
扩展运算符的另一个优点是可以直接在构造函数中使用,比如:
new Date(...[2016, 5, 6]); // Mon Jun 06 2016 00:00:00 GMT-0700 (Pacific Daylight Time)
上面为ES6的写法,如果用ES5来重写,则需要采用复杂的模式来避免类型报错:
new Date.apply(null, [2016, 4, 24]); // TypeError: Date.apply is not a constructor
new (Function.prototype.bind.apply(Date, [null].concat([2016, 5, 6]))); // Mon Jun 06 2016 00:00:00 GMT-0700 (Pacific Daylight Time)
下面是主流浏览器对于扩展运算符的支持情况:
Chrome | Firefox | Internet Explorer | Microsoft Edge | Opera | Safari |
---|---|---|---|---|---|
46 | 27 | - | Supported | - | 7.1 |
Chrome for Android | Firefox Mobile | Safari Mobile | Opera Mobile | IE Mobile |
---|---|---|---|---|
46 | 27 | 8 | - | - |
剩余运算符和扩展运算符的符号相同,也是...,但剩余运算符用在函数声明中,它是扩展运算符的逆过程,即把未匹配的单个元素收集起来放入一个数组类型的参数中(下面称为剩余参数)。
function myFunction(...options) {
return options;
}
myFunction('a', 'b', 'c'); // ["a", "b", "c"]
在上面例子中,剩余运算符...将参数'a'、'b'、'c'收集起来存到参数options中,当未传入参数时,options则为一个空数组。
当我们需要创建一个参数可变的函数时,使用剩余参数就会非常方便,因为它直接将未匹配的剩余参数转换成了一个数组。而使用arguments对象则包括了所有的参数,而且arguments并非一个真正的数组,无法直接调用数组的一些方法。举例说明一下,对于检查第一个字符串是否包括其他字符串这一功能,在ES5中会这样实现:
function checkSubstrings(string) {
for (var i = 1; i < arguments.length; i++) {
if (string.indexOf(arguments[i]) === -1) {
return false;
}
}
return true;
}
checkSubstrings('this is a string', 'is', 'this'); // true
这种实现存在几个缺点:1.语义上不明确,可读性很差,如果不看函数实现调用者根本不知道需要传递多个参数;2.通过位置来处理参数,如果以后扩展了参数,还需要去修改逻辑代码;3.当对声明地参数变量进行赋值同时又使用了arguments、直接返回arguments对象、对arguments进行赋值等,都会带来一些优化问题,甚至报错。采用ES6的剩余参数,我们可以轻松地避免这些问题,代码如下:
function checkSubstrings(string, ...keys) {
for (var key of keys) {
if (string.indexOf(key) === -1) {
return false;
}
}
return true;
}
checkSubstrings('this is a string', 'is', 'this'); // true
虽然采用缺省参数能够很好地解决上面三个问题,但在使用中也存在着一些限制,如在一次函数声明中最多使用一个缺省参数,且必须位于最后,否则会得到一个语法错误。
下面是主流浏览器对于剩余运算符的支持情况:
Chrome | Firefox | Internet Explorer | Microsoft Edge | Opera | Safari |
---|---|---|---|---|---|
47 | 15 | - | Supported | 34 | - |
Chrome for Android | Firefox Mobile | Safari Mobile | Opera Mobile | IE Mobile |
---|---|---|---|---|
47 | 15 | - | - | - |
ES5中并不支持缺省参数,为了实现这一功能,一般会进行如下处理:
function foo(param1, param2) {
param1 = param1 || 10;
param2 = param2 || 10;
console.log(param1, param2);
}
foo(5, 5); // 5 5
foo(5); // 5 10
foo(); // 10 10
不过由于0和null也会被当作false,导致会误被缺省值覆盖,所以最好修改成如下实现:
function foo(param1, param2) {
if(param1 === undefined){
param1 = 10;
}
if(param2 === undefined){
param2 = 10;
}
console.log(param1, param2);
}
foo(0, null); // 0, null
foo(); // 10, 10
在ES6中,我们可以直接在函数声明中使用缺省参数,只有在未传递实参的情况下才会生效,不会对0和null的情况生效。值得一提的是,可以用函数作为缺省值,只有在参数缺省的时候才会对该函数进行调用。另一个特性是后面的缺省值可以直接使用前面所声明的参数变量。
function getParam() {
alert("getParam was called");
return 3;
}
function myFunction(a = getParam(), b = ++a, c = a*b) {
console.log(c);
}
myFunction(); // 12 (also displays an alert dialog)
multiply(2); // 6
multiply(2, 2); // 4
multiply(2, 2, 2); // 2
下面是主流浏览器对于缺省参数的支持情况:
Feature | Chrome | Firefox | Internet Explorer | Microsoft Edge | Opera | Safari |
---|---|---|---|---|---|---|
基本支持 | 49 | 15 | - | 14 | - | - |
缺省参数可在普通参数后面 | 49 | 26 | - | 14 | -- | - |
Feature | Chrome for Android | Firefox Mobile | Safari Mobile | Opera Mobile | IE Mobile |
---|---|---|---|---|---|
基本支持 | 49 | 15 | - | - | - |
缺省参数可在普通参数后面 | 46 | 26 | - | - | - |
解构是ES6中的一个新特性,它允许我们将一个对象或数组直接映射到一堆变量上,由于语法和对象或数组十分相近,所以可读性很强,使用起来十分简洁高效。另外,解构还可以和普通参数结合使用,可以对整个对象(或数组)提供缺省值,也可以对对象属性(或数组元素)分别提供缺省值。
在ES5中,如果要实现一个配置对象处理的函数,通常会像如下代码片段这样处理:
function initiateTransfer(options) {
var protocol = options.protocol,
port = options.port,
delay = options.delay,
retries = options.retries,
timeout = options.timeout,
log = options.log;
// code to initiate transfer
}
options = {
protocol: 'http',
port: 800,
delay: 150,
retries: 10,
timeout: 500,
log: true
};
initiateTransfer(options);
上面的写法存在几个问题:1.可读性差,但从函数声明并不知道需要传递哪些属性;2.如果某个属性未进行定义,得到的值将是undefined,需要另外去处理缺省值;3.函数内部可能需要另外进行一遍赋值,代码比较繁琐。可以用解构的方式去避免这些问题,代码如下:
function initiateTransfer({
protocol = 'http',
port = 800,
delay = 150,
retries = 10,
timeout = 500,
log = true
}) {
// code to initiate transfer
}
下面是主流浏览器对于参数解构的支持情况:
Feature | Chrome | Firefox | Internet Explorer | Microsoft Edge | Opera | Safari |
---|---|---|---|---|---|---|
基本支持 | 49 | 2.0 | - | 14 | - | 7.1 |
带缺省值的解构参数 | 49 | 47 | - | 14 | -- | - |
Feature | Chrome for Android | Firefox Mobile | Safari Mobile | Opera Mobile | IE Mobile |
---|---|---|---|---|---|
基本支持 | 49 | 1 | 8 | - | - |
带缺省值的解构参数 | 49 | 47 | - | - | - |
对于其他语言来讲,传参分为传值类型和传引用(指针)类型。如果是传值,函数内部对于参数的改变不会影响到外部变量或对象;如果是传引用(指针),在函数内部做的修改则会对外部的变量和对象造成影响。从技术层面来讲,javascript参数的传递方式全部都是传值类型,当我们将一个值传递到函数内部时,一个临时的局部变量会被创建,形成对这个参数的一个拷贝,任何对该值的改变都不会影响原有的外部变量。例如,在下面代码片段中,函数调用前后a的值都为5。
var a = 5;
function increment(a) {
a = ++a;
console.log(a);
}
increment(a); // 6
console.log(a); // 5
但当我们将一个对象(或数组)作为参数传递给函数的时候,虽然还是按值传递,但由于该值实际上映射的是此对象(或数组)在内存中的一片区域,所以当我们修改此对象的属性(或数组的某一个元素)的时候,实际上是操作了公用的一片内存区域,这样便会对外部对象(或数组)造成影响。
function foo(param){
param.bar = 'new value';
}
obj = {
bar : 'value'
}
console.log(obj.bar); // value
foo(obj);
console.log(obj.bar); // new value
在强类型语言中,需要对参数的类型进行声明,但在javascript中缺乏这种机制,我们可以传递任意类型、任意数量的参数给函数,但在函数执行过程中如果不在使用前进行检查往往会报错,通常这不是我们想要看到的。为了避免在函数运行中出现参数为undefined的情况,我们可以在函数调用的时候,就对参数进行检查,对于必须提供的参数可以在一开始就抛出异常,这有利于开发阶段提前解决问题,也有利于函数的健壮性和可测试性。可以采用缺省参数的方式进行解决这一问题:
function throwError() {
throw new Error('Missing parameter');
}
function foo(param1 = throwError(), param2 = throwError()) {
// do something
}
foo(10, 20); // ok
foo(10); // Error: missing parameter
在ES4中本来打算用剩余参数这一特性来取代arguments对象,但因为ES4并未真正实现,新的ES6在实现剩余参数的同时保留了arguments对象。
前面也提到了arguments对象并非一个真正的数组,它拥有length属性,可以用索引来获取所有的参数,但并不支持数组的一些方法(如slice()、foreach()等),可以通过Array.prototype.slice.call(arguments)的方式进行转换,ES6中可以直接使用Array.from(arguments)。
在ES5非严格模式下,arguments对象还有一个callee属性,指向此函数,在匿名函数的回调中使用较多,不过在ES5严格模式和ES6中已经废弃,以后只能通过避免在匿名函数中实现回调。在ES5非严格模式下还存在一个问题,arguments对象会和命名的parameters参数保持同步,这一特性在ES5严格模式和ES6中也被移除。
ES6给javascript带来了上百个大大小小的改进,开发者们也越来越频繁地使用这些新特性,以后这些特性必定会变得不可或缺。本文小结了ES6如何改进参数的处理,但这只触及到了ES6的一点皮毛,更多新的有趣的特性还等待着我们去发掘。