ECMAScript 6(ECMAScript 2015라고도 함)은 ECMAScript 표준의 최신 버전으로, JS에서 매개변수를 처리하는 방식을 크게 개선합니다. 다른 새로운 기능 외에도 나머지 매개변수, 기본값, 구조 분해 할당 등을 사용할 수도 있습니다.
이 튜토리얼에서는 인수와 매개변수를 자세히 살펴보고 ES6가 이를 어떻게 개선하는지 살펴보겠습니다.
인수와 매개변수 비교 링크
보통 인수와 매개변수를 언급할 때는 서로 바꿔서 사용하는 것으로 간주합니다. 그러나 이 튜토리얼에서는 명확하게 구분합니다. 대부분의 표준에서 매개변수(형식 매개변수)는 함수 이름과 함수 본문을 선언할 때 사용하는 매개변수를 의미하고, 인수(실제 매개변수)는 함수가 실제로 호출될 때 전달되는 특정 값을 의미한다. 다음 함수를 생각해 보세요.
function foo(param1, param2) { // do something } foo(10, 20);
이 함수에서 param1과 param2는 함수의 형식 매개변수(형식 매개변수)이고, foo 함수가 호출되면 전달된 (10 및 20) 실제 매개변수(실제 매개변수)입니다.
확장 연산자 링크
ES5에서는 apply() 메서드를 사용하여 쉽게 배열을 함수에 전달할 수 있습니다. 예를 들어 배열의 최대값을 얻기 위해 Math.max()와 함께 사용하는 경우가 많습니다. 다음 코드를 살펴보십시오.
var myArray = [5, 10, 50]; Math.max(myArray); // Error: NaN Math.max.apply(Math, myArray); // 50
Math.max() 메서드는 배열 전달을 지원하지 않으며 숫자만 허용합니다. 따라서 배열을 매개변수로 전달하면 오류가 발생합니다. 그러나 apply() 메서드를 사용하면 배열이 Math.max()에서 처리할 수 있는 개별 숫자로 변환됩니다.
다행히 ES6에는 스프레드 연산자가 도입되어 더 이상 apply() 메서드를 사용할 필요가 없습니다. 확장 연산자를 통해 표현식에 대한 여러 매개변수를 쉽게 전달할 수 있습니다.
var myArray = [5, 10, 50]; Math.max(...myArray); // 50
여기서 확장 연산자는 myArray를 독립적인 값으로 확장하여 함수에 전달합니다. 연산자를 모방하기 위해 ES5에서 apply()를 사용하면 목표를 달성할 수 있지만 구문이 혼란스럽고 확장 연산자의 유연성이 부족합니다. 스프레드 연산자는 사용하기 쉬울 뿐만 아니라 다른 많은 기능도 포함합니다. 예를 들어 함수를 호출할 때 여러 번 사용할 수 있고 다른 매개변수와 혼합할 수 있습니다.
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)`
물론 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)
브라우저는 확장 기능에서 작동합니다. 함수 Link에서 호출되는 기호 지원
데스크톱 브라우저;
모바일 브라우저:
Rest 매개변수 링크
The 나머지 매개변수와 확장 연산자는 동일한 구문을 사용하지만 나머지 매개변수는 모든 매개변수를 수집하여 배열로 변환하는 반면 확장 연산자는 배열을 별도의 매개변수로 확장한다는 점에서 다릅니다.
function myFunction(...options) { return options; } myFunction('a', 'b', 'c'); // ["a", "b", "c"]
함수 호출 시 실제 매개변수가 전달되지 않으면 나머지 매개변수는 다음과 같이 빈 배열을 출력합니다.
function myFunction(...options) { return options; }myFunction(); // []
생성 시 나머지 매개변수가 사용됩니다. 가변 함수(즉, 가변 개수의 매개변수가 있는 함수에 특히 유용합니다). 나머지 매개변수는 배열의 고유한 장점을 갖고 있으며 인수 객체를 빠르게 대체할 수 있습니다(이 용어는 아래에서 설명합니다). 다음 함수는 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
이 함수는 문자열( this is a string )에 이러한 하위 문자열( is , this )이 포함되어 있는지 확인합니다. 이 함수의 첫 번째 문제점은 함수 본문에 여러 매개변수가 있는지 확인해야 한다는 것입니다. 두 번째 문제는 인수[0]이 첫 번째 매개변수(문자열)를 가리키기 때문에 루프가 0이 아닌 1부터 시작해야 한다는 것입니다. 나중에 이 문자열 앞이나 뒤에 다른 매개변수를 추가하려는 경우 루프 본문을 업데이트하는 것을 잊을 수 있습니다. 나머지 매개변수를 사용하면 이러한 문제를 쉽게 피할 수 있습니다.
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
이 함수의 출력은 이전 함수의 출력과 동일합니다. 매개변수 문자열이 이 함수의 인수에 포함되어 첫 번째 매개변수가 전달되고 나머지 매개변수는 배열에 배치되어 키라는 변수에 할당된다는 점을 여기서 다시 언급하겠습니다.
코드 가독성을 높이고 일부 js 최적화 문제를 방지하려면 인수 객체 대신 나머지 매개변수를 사용하세요. 그러나 나머지 매개변수에도 단점이 없는 것은 아닙니다. 예를 들어 마지막 매개변수여야 합니다. 그렇지 않으면 다음 함수에 표시된 것처럼 오류가 보고됩니다.
function logArguments(a, ...params, b) { console.log(a, params, b); } logArguments(5, 10, 15); // SyntaxError: parameter after rest parameter
또 다른 단점은 함수 선언에서 나머지 매개변수를 하나만 허용할 수 있다는 것입니다.
function logArguments(...param1, ...param2) { } logArguments(5, 10, 15); // SyntaxError: parameter after rest parameter
Rest 매개변수 링크에 대한 브라우저 지원
데스크톱 브라우저:
移动端浏览器:
默认参数 Link
ES5 默认参数 Link
在ES5中,JS 并不支持默认参数, 但是,我们也有一种变通的方案,那就是在函数中使用 OR 操作符( || )。我们简单地模仿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
该函数预期传入两个参数,但如果在调用该函数时,没有传入实参,则它会用默认值。在函数体内,如果没有传入实际参数,则会被自动设为 undefined, 所以,我们可以检测这些参数,并且声明他们的默认值。我们可以使用 OR 操作符( || )来检测是否有传入实际参数,并且设定他们的默认值。 OR 操作符会检测它的第一个参数,如果有 实际值 2 ,则用第一个,如果没有,则用它的第二个参数。
这种方法在函数中普遍使用,但它有一个瑕疵,那就是传入 0 或者 null 也会触发默认值,因为 0 和 null 都被认为是false. 所以,如果我们需要给函数传入 0 和 null 时,我们需要另一种方式去检测这个参数是否缺失:
function foo(param1, param2) { if(param1 === undefined){ param1 = 10; } if(param2 === undefined){ param2 = 10; } console.log(param1, param2); } foo(0, null); // 0, nullfoo(); // 10, 10
在上面这个函数中,只有当所传的参数全等于 undefined 时,才会使用默认值。这种方式需要用到的代码稍微多点,但是安全度更高,我们可以给函数传入 0 和 null。
ES6 默认参数 Link
有了ES6,我们不需要再去检测哪些值为undefined并且给它们设定默认值了。现在我们可以直接在函数声明中放置默认值:
function foo(a = 10, b = 10) { console.log(a, b); } foo(5); // 5 10 foo(0, null); // 0 null
如你所见,省略一个参数,就会触发一个默认值,但是传入 0 或者 null 时,则不会。 我们甚至可以使用函数去找回默认参数的值:
function getParam() { alert("getParam was called"); return 3; } function multiply(param1, param2 = getParam()) { return param1 * param2; } multiply(2, 5); // 10 multiply(2); // 6 (also displays an alert dialog)
注意 getParam 这个函数只有在第二个参数省略时才会被调用。所以当我们给 multiply 传入两个参数并调用它时,alert是不会出现的。
默认参数还有一个有趣的特性,那就是我们可以在函数声明中指定其它参数和变量的值:
function myFunction(a=10, b=a) { console.log('a = ' + a + '; b = ' + b); } myFunction(); // a=10; b=10 myFunction(22); // a=22; b=22 myFunction(2, 4); // a=2; b=4
你甚至可以在函数声明中做运算:
function myFunction(a, b = ++a, c = a*b) { console.log(c); } myFunction(5); // 36
请注意,JavaScript 和其它语言不同, 它是在函数被调用时,才去求参数的默认值。
function add(value, array = []) { array.push(value); return array; } add(5); // [5] add(6); // [6], not [5, 6]
浏览器对默认参数的支持情况 Link
桌面浏览器:
移动端浏览器:
解构赋值 Link
解构赋值是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);
这种方式实现起来很好,已经被许多JS开发者所采用。 只是我们必须看函数内部,才知道函数预期需要哪些参数。结合解构赋值,我们就可以在函数声明中清晰地表示这些参数:
function initiateTransfer({protocol, port, delay, retries, timeout, log}) { // code to initiate transfer }; var options = { protocol: 'http', port: 800, delay: 150, retries: 10, timeout: 500, log: true } initiateTransfer(options);
在该函数中,我们没有传入一个配置对象,而是以对象解构赋值的方式,给它传参数。这样做不仅使这个函数更加简明,可读性也更高。
我们也可以把解构赋值传参和其它规则的参数一起使用:
function initiateTransfer(param1, {protocol, port, delay, retries, timeout, log}) { // code to initiate transfer } initiateTransfer('some value', options);
注意如果函数调用时,参数被省略掉,则会抛出错误,如下:
function initiateTransfer({protocol, port, delay, retries, timeout, log}) { // code to initiate transfer } initiateTransfer(); // TypeError: Cannot match against 'undefined' or 'null'
当我们需要让参数都是必填时,这种方法能够得到我们想要的结果,但如果我们希望参数是可选的时候呢?想要让参数缺失时不会报错,我们就需要给默认参数设定一个默认值:
function initiateTransfer({protocol, port, delay, retries, timeout, log} = {}) { // code to initiate transfer } initiateTransfer(); // no error
上面这个函数中,需要解构赋值的参数有了一个默认值,这个默认值就是一个空对象。这时候,函数被调用时,即使没有传入参数,也不会报错了。
我们也可以给每个被解构的参数设定默认值,如下:
function initiateTransfer({ protocol = 'http', port = 800, delay = 150, retries = 10, timeout = 500, log = true }) { // code to initiate transfer }
在这个例子中,每个属性都有一个默认值,我们不需要手动去检查哪个参数值是 undefined ,然后在函数中给它设定默认值了。
浏览器对解构赋值的支持情况 Link
桌面浏览器:
移动端浏览器:
参数传递 Link
参数能通过 引用 或 值 传递给函数。修改按引用传递的参数,一般反映在全局中,而修改按值传递的参数,则只是反映在函数内部。
在像 Visual Basic 和 PowerShell 这样的语言中,我们可以选择是按引用还是按值来传递参数,但是在 JavaScript 中则不行。
按值传递参数 Link
从技术上来讲,JavaScript 只允许按值传参。当我们给函数按值传递一个参数时,该函数的作用域内就已经复制了这个值。因此,这个值的改变,只会在函数内部反映出来。请思考下面这个列子:
var a = 5;function increment(a) { a = ++a; console.log(a); } increment(a); // 6 console.log(a); // 5
这里,修改函数里面的参数 a = ++a ,是不会影响到原来 a 的值。 所以在函数外面打印 a 的值,输出仍然是 5 。
按引用传递参数 Link
在JavaScript中,一切都是按值传递的。但当我们给函数传一个变量,而这个变量所指向的是一个对象(包括数组)时,这个 变量 就是对象的一个引用。通过这个变量来改变对象的属性值,是会从根本上改变这个对象的。
来看下面这个例子:
function foo(param){ param.bar = 'new value'; } obj = { bar : 'value' } console.log(obj.bar); // value foo(obj); console.log(obj.bar); // new value
如你所见,对象的属性值在函数内部被修改了,被修改的值在函数外面也是可见的。
当我们传递一个没有初始值的参数时,如数组或对象,会隐形地创建了一个变量,这个变量指向记忆中原对象所在的位置。这个变量随后被传递给了函数,在函数内部对这个变量进行修改将会影响到原对象。
参数类型检测、参数缺失或参数多余 Link
在强类型语言中,我们必须在函数声明中明确参数的类型,但是 JavaScript 没有这种特性。在JavaScript中,我们传递给函数的参数个数不限,也可以是任何类型的数据。
假设现在有一个函数,这个函数只接受一个参数。但是当函数被调用时,它本身没有限制传入的参数只能是一个,我们可以随意地传入一个、两个、甚至是更多。我们也可以什么都不传,这样都不会报错。
形参( arguments )和 实参( parameters )的个数不同有两种情况:
实参少于形参
缺失的参数都会等同于 undefined 。
实参多于形参
多余的参数都将被忽略,但它们会以数组的形式保存于变量 arguments 中(下文会讨论到)。
必填参数 Link
如果一个参数在函数调用时缺失了,它将被设为 undefined 。基于这一点,我们可以在参数缺失时抛出一个错误:
function foo(mandatory, optional) { if (mandatory === undefined) { throw new Error('Missing parameter: mandatory'); } }
在 ES6 中,我们可以更好地利用这个特性,使用默认参数来设定必填参数:
function throwError() { throw new Error('Missing parameter'); } function foo(param1 = throwError(), param2 = throwError()) { // do something } foo(10, 20); // ok foo(10); // Error: missing parameter
参数对象 Link
为了取代参数对象,rest参数在 ECMAScript 4 中就已经得到支持,但是 ECMAScript 4 没有落实。随着 ECMAScript 6 版本的发布,JS 正式支持rest参数。它也拟定计划,准备 抛弃 对参数对象 arguments object 的支持。
参数对象是一个类数组对象,可在一切函数内使用。它允许通过数字而不是名称,来找回被传递给函数的参数值。这个对象使得我们可以给函数传递任何参数。思考以下代码段:
function checkParams(param1) { console.log(param1); // 2 console.log(arguments[0], arguments[1]); // 2 3 console.log(param1 + arguments[0]); // 2 + 2 } checkParams(2, 3);
该函数预期接收一个参数。但是当我们给它传入两个参数并且调用它时,第一个参数通过名为 param1 的形参或者参数对象 arguments[0] 被函数所接受,而第二个参数只能被放在 argument[1] 里面。此外,请注意,参数对象可以与命名参数一起使用。
参数对象给每个被传递给函数的参数提供了一个入口,并且第一个入口的下标从 0开始。如果我们要给上面这个函数传递更多的参数,我们可以写 arguments[2] , arguments[3] 等等。
我们甚至可以跳过设定命名参数这一步,直接使用参数对象:
function checkParams() { console.log(arguments[1], arguments[0], arguments[2]); } checkParams(2, 4, 6); // 4 2 6
事实上,命名参数只是为了方便使用,并不是必须的。类似地,rest参数也可用于反映被传递的参数:
function checkParams(...params) { console.log(params[1], params[0], params[2]); // 4 2 6 console.log(arguments[1], arguments[0], arguments[2]); // 4 2 6 } checkParams(2, 4, 6);
参数对象是一个类数组的对象,只是它没有数组本身具备的方法,如 slice() 和 foreach() 。 如果要在参数对象中使用数组的方法,首先要把它转换成一个真正的数组。
function sort() { var a = Array.prototype.slice.call(arguments); return a.sort(); } sort(40, 20, 50, 30); // [20, 30, 40, 50]
在该函数中,采用了 Array.prototype.slice.call() 来快速地把参数对象转换成一个数组。接着,在 sort() 方法中,为这个数组排序并且把它返回。
ES6 新增了更直接的方法,用 Array.from() 把任何类数组对象转换成数组:
function sort() { var a = Array.from(arguments); return a.sort(); } sort(40, 20, 50, 30); // [20, 30, 40, 50]
长度属性 Link
尽管参数对象从技术上来讲,不算是一个数组,但仍有一个长度属性,来检测传递给函数的参数个数:
function countArguments() { console.log(arguments.length); } countArguments(); // 0 countArguments(10, null, "string"); // 3
通过 length 属性,我们可以更好地控制传递给函数的参数个数。举个例子,如果一个函数只要求两个参数,那么我们就可以使用 length 属性来检测所传入的参数个数,如果少于要求的个数,则抛出错误:
function foo(param1, param2) { if (arguments.length < 2) { throw new Error("This function expects at least two arguments"); } else if (arguments.length === 2) { // do something } }
rest参数是数组,所以他们都有 length 属性。 所以上面的代码,在ES6里面可以用rest参数写成下面这样:
function foo(...params) { if (params.length < 2) { throw new Error("This function expects at least two arguments"); } else if (params.length === 2) { // do something } }
被调用属性与调用属性 Link
被调用 属性指向当前正在执行的函数,而 调用 属性则指向那个调用了 当前正在执行的函数 的函数。 在ES5的严格模式下,这些属性是不被支持的,如果尝试使用它们,则会报错。
arguments.callee 这个属性在递归函数中很有用,尤其在匿名函数中。因为匿名函数没有名称,只能通过 arguments.callee 来指向它。
var result = (function(n) { if (n
严格模式和非严格模式下的参数对象 Link
在ES5非严格模式下, 参数对象 有个不一般的特性:它能使 自身的值 跟 与之相对应的命名参数的值 保持同步。
请看下面这个例子:
function foo(param) { console.log(param === arguments[0]); // true arguments[0] = 500; console.log(param === arguments[0]); // true return param } foo(200); // 500
在这个函数里面, arguments[0] 被重新赋值为 500 。由于 arguments 的值总是和对应的命名参数保持同步,所以改变了 arguments[0] 的值,也就相应的改变了param 的值。实际上,他们就像是同一个变量,拥有两个不同的名字而已。而在 ES5严格模式 下,参数对象的这种特性则被移除了。
"use strict"; function foo(param) { console.log(param === arguments[0]); // true arguments[0] = 500; console.log(param === arguments[0]); // false return param } foo(200); // 200
加上 严格模式 , 现在改变 arguments[0] 的值是不会影响到 param 的值了,打印出来的值也跟预期的一致。 在 ES6 中 该函数的输出跟在 ES5 严格模式下是一样的。需要记住的是,当函数声明中使用了默认值时,参数对象是不会受到影响的:
function foo(param1, param2 = 10, param3 = 20) { console.log(param1 === arguments[0]); // true console.log(param2 === arguments[1]); // true console.log(param3 === arguments[2]); // false console.log(arguments[2]); // undefined console.log(param3); // 20 } foo('string1', 'string2');
在这个函数中,尽管 param3 有默认值 20 ,但是 arguments[2] 仍然是undefined , 因为函数调用时只传了两个值。换言之,设定默认值对参数对象是没有任何影响的。
总结 Link
ES6 给 JS 带来了上百个大大小小的改进。 越来越多的开发者正使用ES6的新特性, 所以我们都需要去了解它们。在本教程中,我们学习了ES6是如何改善JS的参数处理的,但我们仍只是知晓了ES6的皮毛。更多新的、有趣的特性值得我们去探讨。