ECMAScript 6 함수

함수 (Function)

함수는 JavaScript 프로그래밍 언어의 중요한 부분이며, JavaScript가 시작된 이후로 ECMAScript 6 이전까지 많이 변경되지 않았습니다. 이로 인해 실수가 많았고, 아주 기본적인 행동을하기 위해 더 많은 코드가 필요했으며, 미묘한 행동의 백 로그(backlog)가 남았습니다.

ECMAScript 6 함수는 JavaScript 개발자의 수년간의 불만과 요청을 고려하여 큰 발전을 이루었습니다. 결과적으로 ECMAScript 5 기능 위에 점진적인 개선이 이루어져 JavaScript에서 오류를 발생시키지 않고 보다 강력하게 프로그래밍할 수있게 되었습니다.

Default 파라미터 값이 있는 함수

JavaScript의 함수는 함수 정의에 선언된 파라미터 수에 관계없이 여러 파라미터를 전달할 수 있다는 점에서 독특합니다. 이렇게하면 파라미터가 제공되지 않을 때 Default 값을 채우기만 하면 다른 개수의 파라미터를 처리할 수있는 함수를 정의할 수 있습니다. 이 절에서는 Default 파라미터가 ECMAScript 6에서 작동하는 방법과 arguments 객체에 대한 몇 가지 중요한 정보, 표현식을 파라미터로 사용하는 방법 및 또다른 TDZ에 대해 설명합니다.

ECMAScript 5에서 Default 파라미터 시뮬레이션

ECMAScript 5 및 이전 버전에서는 다음 패턴을 사용하여 Default 파라미터가 있는 함수를 만들 수 있습니다.

1
2
3
4
5
6
7
8
function makeRequest(url, timeout, callback) {
timeout = timeout || 2000;
callback = callback || function() {};
// 함수의 나머지 부분
}

이 예제에서, timeoutcallback은 실제로 파라미터가 제공되지 않을 때 Default 값이 주어지기 때문에 Optional입니다. 논리합 연산자 (||)는 첫 번째가 거짓 일 때 항상 두 번째 피연산자를 반환합니다. 명시적으로 제공되지 않은 명명된 함수 파라미터는 undefined로 설정되므로 논리 OR 연산자는 누락된 파라미터의 Default값을 제공하는 데 자주 사용됩니다. 그러나 timeout이 유효한 값 0일 수 있지만 0은 false이기 때문에 2000으로 바꿀 수 있다는 점에서 이 접근법에 결함이 있습니다.

이 경우보다 안전한 대안은 다음 예와 같이 typeof를 사용하여 파라미터의 유형을 확인하는 것입니다.

1
2
3
4
5
6
7
8
function makeRequest(url, timeout, callback) {
timeout = (typeof timeout !== "undefined") ? timeout : 2000;
callback = (typeof callback !== "undefined") ? callback : function() {};
// 함수의 나머지 부분
}

이 방법이 더 안전하지만 매우 기본적인 작업을 위해서는 많은 추가 코드가 필요합니다. 인기있는 JavaScript 라이브러리에는 이러한 비슷한 패턴으로 채워집니다. 이는 공통적으로 비슷한 패턴을 사용하기 때문입니다.

ECMAScript 6의 Default 파라미터

ECMAScript 6을 사용하면 파라미터가 전달되지 않을 때 사용되는 Default값을 보다 쉽게 제공할 수 있습니다.

1
2
3
4
5
function makeRequest(url, timeout = 2000, callback = function() {}) {
// 함수의 나머지 부분
}

이 함수는 첫 번째 파라미터가 항상 전달될 것으로 예상합니다. 다른 두 파라미터에는 Default 값이 있으므로 누락된 값을 확인하기 위해 코드를 추가할 필요가 없으므로 함수 본문이 훨씬 단순해집니다.

makeRequest()가 세개의 파라미터 모두로 호출될 때, Default값은 사용되지 않습니다.

1
2
3
4
5
6
7
8
9
10
// uses default timeout and callback
makeRequest("/foo");
// uses default callback
makeRequest("/foo", 500);
// doesn't use defaults
makeRequest("/foo", 500, function(body) {
doSomething(body);
});

ECMAScript 6에서는 url이 필요하다고 생각하기 때문에 makeRequest()를 호출할 때마다 "/foo"가 전달됩니다. Default값이 있는 두 개의 파라미터는 선택적으로 간주됩니다.

Default값은 함수 선언에서 파라미터의 어느 위치나 지정할 수 있습니다. 그렇기 때문에 모든 파라미터에 Default값을 지정할 수 있습니다. 예를 들면 다음과 같습니다.

1
2
3
4
5
function makeRequest(url, timeout = 2000, callback) {
// the rest of the function
}

이 경우 timeoutDefault값은 두 번째 파라미터가 전달되지 않았거나 두 번째 파라미터가 명시 적으로 undefined로 전달된 경우에만 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
// default timeout을 사용합니다.
makeRequest("/foo", undefined, function(body) {
doSomething(body);
});
// default timeout을 사용합니다.
makeRequest("/foo");
// default timeout을 사용하지 않습니다.
makeRequest("/foo", null, function(body) {
doSomething(body);
});

Default 파라미터의 경우, null의 값은 유효하다고 간주됩니다. 즉,makeRequest()에 대한 세 번째 호출에서 timeoutDefault 값은 사용되지 않습니다

Default 파라미터 값이 arguments Object에 미치는 영향

Default 파라미터가 존재할 때 arguments 객체의 동작이 다르다는 것을 기억 해야 합니다. ECMAScript 5 nonstrict mode에서arguments 객체는 함수의 명명된 파라미터의 변경을 반영합니다. 이러한 작동 방식을 보여주는 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
function mixArgs(first, second) {
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a", "b");

출력 결과 :

1
2
3
4
true
true
true
true

arguments 객체는 nonstrict mode에서 명명된 파라미터의 변경은 항상 업데이트 합니다. 따라서 첫 번째와 두 번째가 새로운 값으로 할당될 때 arguments[0]arguments[1]는 그에 따라 업데이트되어 모든 === 비교가 true로 해석됩니다.

ECMAScript 5의 strict 모드는 arguments 객체의 혼란스러운면을 제거합니다. strict 모드에서, arguments 객체는 명명된 파라미터에 대한 변경을 반영하지 않습니다. 아래의 예제는 strict 모드에서 mixArgs() 함수를 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
function mixArgs(first, second) {
"use strict";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d"
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a", "b");

mixArgs()의 실행 결과는 다음과 같습니다.

1
2
3
4
true
true
false
false

이번에는 firstsecond를 변경해도 arguments에 영향을 미치지 않으므로 예상대로 실행됩니다.

그러나 ECMAScript 6의 Default 파라미터를 사용하는 함수의 arguments 객체는 함수가 명시적으로 strict 모드로 실행되는지 여부에 관계없이 ECMAScript 5 strict 모드와 동일한 방식으로 동작합니다. Default 파라미터가 있으면 arguments 객체가 명명된 파라미터에서 분리된 상태로 유지됩니다. 이는 arguments객체가 어떻게 사용되는 지에 대한 미묘하지만 중요한 세부 변경사항입니다. 다음 코드를
살펴 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
// strict mode가 아님
function mixArgs(first, second = "b") {
console.log(arguments.length);
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d"
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a");

실행 결과 :

1
2
3
4
5
1
true
false
false
false

이 예제에서 arguments.length는 1입니다. 왜냐하면 오직 하나의 파라미터가 mixArgs()에 전달 되었기 때문입니다. 이는 또한 arguments[1]undefined라는 것을 의미합니다. 이는 하나의 인수만 함수에 전달될 때 예상되는 동작입니다. 즉, firstarguments[0]과 같습니다. firstsecond를 변경해도 arguments에는 아무런 영향을 미치지 않습니다. 이러한 현상은 nonstrictstrict 모드 모두에서 발생하기 때문에 arguments를 사용하여 항상 초기 호출 상태를 확인할 수 있습니다.

Default 파라미터 표현식

아마도 Default 파라미터의 가장 흥미로운 특징은 Default 값이 Primitive 값일 필요는 없다는 것입니다. 예를 들어, 다음과 같이 함수를 실행하여 Default 파라미터를 검색할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
function getValue() {
return 5;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6

여기에서 마지막 파라미터가 제공되지 않으면 getValue() 함수가 호출되어 올바른 Default 값을 검색합니다. getValue()는 두 번째 파라미터 없이 add()가 호출될 때만 호출되며 함수 선언이 처음 구문 분석될 때 호출되지 않는다는 점에 유의하십시오. 즉, getValue()가 다르게 작성된 경우 잠재적으로 다른 값을 반환할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
let value = 5;
function getValue() {
return value++;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
console.log(add(1)); // 7

이 예제에서, value는 5로 시작하고 getValue()가 호출될 때마다 증가합니다. add(1)에 대한 첫 번째 호출은 6을 반환하고, 두 번째 호출은 value가 증가했기 때문에 add(1)에 대한 호출은 7을 반환합니다. second에 대한 Default 값은 함수가 호출될 때만 평가되기 때문에 언제든지 그 값을 변경할 수 있습니다.

함수 호출을 Default 파라미터로 사용할 때는 주의하십시오. 마지막 예제에서 second = getValue와 같이 괄호를 잊어 버린 경우 함수 호출 결과가 아닌 함수에 대한 참조를 전달합니다.

이 동작은 또 다른 흥미로운 기능을 제공합니다. 이전 파라미터를 이후 파라미터의 Default로 사용할 수 있습니다. 다음은 그 예입니다.

1
2
3
4
5
6
function add(first, second = first) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 2

이 코드에서 파라미터 secondfirstDefault 값으로 받습니다. 즉, 하나의 파라미터만 전달하면 두 인수가 같은 값으로 남게됩니다. add(1)은 2를 반환하고 add(1, 1)도 2를 반환합니다. 이 단계를 더 진행하면, firstsecond의 값을 얻기위한 함수로 다음과 같이 전달할 수 있습니다 :

1
2
3
4
5
6
7
8
9
10
function getValue(value) {
return value + 5;
}
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7

이 예제는 secondgetValue(first)에 의해 반환된 값과 동일하게 설정하므로 add(1, 1)은 여전히 2를 반환하지만 add(1)은 7 (1 + 6)을 반환합니다.

Default 파라미터 지정에서 파라미터를 참조하는 기능은 앞의 arguments 에 대해서만 작동하므로 앞서는 arguments는 나중 arguments를 액세스할 수 없습니다.

1
2
3
4
5
6
function add(first = second, second) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // throws error

secondfirst 다음에 정의되고 따라서 Default 값으로 사용할 수 없기 때문에 add(undefined, 1)에 대한 호출은 에러를 던집니다. 그런 일이 일어나는 이유를 이해하려면 Temporal dead zone을 다시 이해해야 합니다.

Default 파라미터 Temporal Dead Zone

1 장에서는 letconst에 관련된 TDZ (Temporary Dead Zone)를 소개했고, Default 파라미터 중에 파라미터에 접근할 수 없는 TDZ도 있습니다. let 선언과 유사하게, 각 파라미터는 오류를 던지지 않고 초기화 전에 참조할 수 없는 새로운 식별자 바인딩을 생성합니다. 파라미터 초기화는 함수에 대한 값을 전달하거나 Default 파라미터를 사용하여 함수를 호출할 때 발생합니다.

Default 파라미터 TDZ를 알아 보기위해 아래 예제와 같이 “Default 파라미터 표현식”에 대해 생각해 보겠습니다..

1
2
3
4
5
6
7
8
9
10
function getValue(value) {
return value + 5;
}
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7

add(1, 1)add(1)에 대한 호출은 효과적으로 firstsecond 파라미터 값을 생성하기 위해 다음 코드를 실행합니다 :

1
2
3
4
5
6
7
// add(1, 1)에 대한 JavaScript 표현
let first = 1;
let second = 1;
// add(1)에 대한 JavaScript 표현
let first = 1;
let second = getValue(first);

add()함수가 처음 실행될 때, firstsecond 바인딩은 특정한 파라미터의 TDZ에 추가됩니다 (let의 동작과 유사합니다). 따라서 secondfirst의 값으로 초기화될 수 있습니다. 왜냐하면 first는 항상 그때 초기화 되기 때문이고, 그 반대는 성립하지 않습니다. 이제, 다시 작성된 add()함수를 살펴 보겠습니다.

1
2
3
4
5
6
function add(first = second, second) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // throws error

이 예제에서 add(1, 1)add(undefined, 1)에 대한 호출은 이제 아래 코드와 매핑됩니다.

1
2
3
4
5
6
7
// add(1, 1) 호출의 JavaScript 표현
let first = 1;
let second = 1;
// add(undefined, 1) 호출의 JavaScript 표현
let first = second;
let second = 1;

이 예제에서 add(undefined, 1) 호출은 first가 초기화될 때 second가 아직 초기화되지 않았기 때문에 에러를 던집니다. 이 시점에서 second는 TDZ에 있으므로 second에 대한 참조는 오류를 발생시킵니다. 이것은 1 장에서 논의한 let 바인딩의 동작이 반영됩니다.

함수의 파라미터는 자신의 scope와 함수 몸체 scope와는 별도의 TDZ를 가지고 있습니다. 즉, 파라미터의 Default값은 함수 본문 내에서 선언된 변수에 액세스할 수 없습니다.

Unnamed 파라미터로 작업하기

지금까지 이 장의 예제는 함수 정의에서 명명된 파라미터만을 다뤘습니다. 그러나 JavaScript 함수는 정의된 이름이 부여된 파라미터의 개수로, 전달할 수있는 파라미터의 수를 제한하지 않습니다. 공식적으로 지정된 것보다 더 적은 또는 더 많은 파라미터를 항상 전달할 수 있습니다. Default 파라미터 값은 함수가 더 적은 수의 파라미터를 받아 들일 수 있을 때 명확하게하고, ECMAScript 6는 정의된 것보다 더 많은 파라미터를 전달하는 문제를 만들려고했습니다.

ECMAScript 5의 Unnamed 파라미터

초기에 JavaScript는 각 파라미터를 개별적으로 정의하지 않고 전달된 모든 함수 파라미터를 검사하는 방법으로 arguments 객체를 제공했습니다. 대부분의 경우 arguments의 검사는 잘 작동하지만, 이 객체는 작업하기가 약간 번거로울 수 있습니다. 예를 들어, arguments 객체를 검사하는 이 코드를 생각해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function pick(object) {
let result = Object.create(null);
// 두 번째 파라미터에서 시작
for (let i = 1, len = arguments.length; i < len; i++) {
result[arguments[i]] = object[arguments[i]];
}
return result;
}
let book = {
title: "Understanding ECMAScript 6",
author: "Nicholas C. Zakas",
year: 2015
};
let bookData = pick(book, "author", "year");
console.log(bookData.author); // "Nicholas C. Zakas"
console.log(bookData.year); // 2015

이 함수는 Underscore.js 라이브러리의 pick () 메서드를 모방한 것입니다. 이 라이브러리는 원본 객체의 프로퍼티 중 일부가 복사된 객체를 반환합니다. 이 예에서는 하나의 파라미터만 정의하고 첫 번째 파라미터는 프로퍼티를 복사할 대상이될 것으로 예상합니다. 전달된 다른 모든 파라미터는 결과에 복사해야하는 프로퍼티의 이름입니다.

pick ()함수에 대해 주의해야 할 몇 가지 점이 있습니다. 첫째, 함수가 둘 이상의 파라미터를 처리할 수 있다는 것이 전혀 분명하지 않습니다. 몇 가지 파라미터를 더 정의할 수는 있지만 이 함수가 여러 파라미터를 사용할 수 있음을 나타낼 수는 없습니다. 둘째, 첫 번째 파라미터의 이름이 지정되어 직접 사용되므로 복사할 속성을 찾을 때 인덱스 0 대신 인덱스 1의 arguments 객체에서 시작해야합니다. 적절한 파라미터를 arguments와 함께 사용하는 것을 기억하는 것은 반드시 어렵다고는 할 수 없지만, 추적이 용이한 것이 더 중요합니다.

ECMAScript 6에서는 이러한 문제를 해결할 수있는 Rest 파라미터를 도입했습니다.

Rest 파라미터

Rest 파라미터는 명명된 파라미터 앞에 세 개의 점 (...)으로 표시됩니다. 이 명명된 파라미터는 함수에 전달된 나머지 파라미터를 포함하는 Array가 되며, 이것이 “Rest“ 파라미터 이름의 출처입니다. 예를 들어 pick()은 다음과 같이 Rest 파라미터를 사용하여 다시 작성할 수 있습니다 :

1
2
3
4
5
6
7
8
9
function pick(object, ...keys) {
let result = Object.create(null);
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}

이 버전의 함수에서 keysobject 다음에 전달된 모든 파라미터를 포함하는 Rest 파라미터입니다 (첫 번째 파라미터를 포함하여 모든 파라미터를 포함하는 arguments와는 다릅니다). 즉, keys를 처음부터 끝까지 반복할 수 있다는 뜻입니다. 이 함수를 보면 보너스로 임의의 여러 파라미터를 처리할 수있다는 것을 알수 있습니다.

Rest 파라미터는 함수에서 이름이 지정된 파라미터의 수를 나타내는 length 프로퍼티에 영향을주지 않습니다. 이 예제에서 pick()에 대한length 값은object 만이 값으로 계산되기 때문에 1입니다.

Rest 파라미터 제약사항

Rest 파라미터에는 두 가지 제약이 있습니다. 첫 번째 제약 사항은 하나의 Rest 파라미터만 있을 수 있으며 Rest 파라미터가 마지막이어야 한다는 것입니다. 예를 들어 다음 코드는 작동하지 않습니다.

1
2
3
4
5
6
7
8
9
10
// Syntax error: rest 파라미터 다음에 명명된 파라미터를 가질 수 없습니다.
function pick(object, ...keys, last) {
let result = Object.create(null);
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}

여기서 파라미터 lastRest 파라미터 keys 다음에 오기 때문에 구문오류가 발생합니다.

두 번째 제약은 Rest 파라미터를 객체 리터럴 Setter에서 사용할 수 없다는 것입니다. 즉, 아래 코드도 구문 오류를 발생시킵니다.

1
2
3
4
5
6
7
let object = {
// Syntax error: Can't use rest param in setter
set name(...value) {
// do something
}
};

이 제약은 개체 리터럴 setter가 단일 파라미터로 제한되기 때문에 발생합니다. Rest 파라미터는 정의상 무한한 수의 파라미터이므로 이 문맥에서는 허용되지 않습니다.

Rest 파라미터arguments 객체에 미치는 영향

Rest 파라미터는 ECMAScript의 arguments를 대체하도록 설계되었습니다. 원래 ECMAScript 4는 arguments를 없애고 Rest 파라미터를 추가하여 무제한의 파라미터를 함수에 전달할 수 있었습니다. 하지만 ECMAScript 4는 발표 되지 않았고, 이러한 내용은 ECMAScript 6에서 계속 유지되었습니다.

arguments 객체는 이 프로그램에서 처럼 호출될 때 함수에 전달된 arguments를 반영하여 Rest 파라미터와 함께 작동합니다.

1
2
3
4
5
6
7
8
function checkArgs(...args) {
console.log(args.length);
console.log(arguments.length);
console.log(args[0], arguments[0]);
console.log(args[1], arguments[1]);
}
checkArgs("a", "b");

checkArgs() 실행은 다음을 출력합니다 :

1
2
3
4
2
2
a a
b b

arguments 객체는 Rest 파라미터 사용에 관계없이 함수에 전달된 파라미터를 항상 정확하게 반영합니다.

이러한 내용이 Rest 파라미터를 사용하기 위해 알아야 할 전부입니다.

함수 Constructor의 향상된 기능

Function의 Constructor는 새로운 함수를 동적으로 생성할 수 있도록 하지만 JavaScript에서 자주 사용하지는 않습니다. Constructor에 대한 arguments는 함수 및 함수 본문에 대한 파라미터이며 모두 문자열입니다. 다음은 그 예입니다.

1
2
3
var add = new Function("first", "second", "return first + second");
console.log(add(1, 1)); // 2

ECMAScript 6는 Default 파라미터Rest 파라미터를 허용하기 위해 Function Constructor 함수의 기능을 보강했습니다. Default
파라미터는 다음과 같이 파라미터 이름에 등호와 값을 추가하기 만하면됩니다.

1
2
3
4
5
var add = new Function("first", "second = first",
"return first + second");
console.log(add(1, 1)); // 2
console.log(add(1)); // 2

위의 예제에서 하나의 파라미터만 전달되면 second 파라미터에 first값이 할당됩니다. 이 구문은 Function을 사용하지 않는 함수 선언과 같습니다.

Rest 파라미터의 경우, 마지막 파라미터 앞에 ...를 다음과 같이 추가할 수 있습니다.

1
2
3
var pickFirst = new Function("...args", "return args[0]");
console.log(pickFirst(1, 2)); // 1

위 코드는 하나의 Rest 파라미터만을 사용하고 전달된 첫 번째 파라미터를 반환하는 함수를 만듭니다.

DefaultRest 파라미터가 추가되어 Function이 함수의 선언적 형식과 동일한 기능을 모두 갖췄습니다.

Spread 연산자

Rest 파라미터와 밀접하게 관련된 것은 Spread 연산자입니다. Rest 파라미터를 사용하면 여러 독립 파라미터를 Array에 결합해야 한다고 지정할 수 있지만 Spread 연산자를 사용하면 분할해야 하는 Array을 지정하고 해당 항목을 함수에 대한 별도 파라미터로 전달할 수 있습니다. Math.max() 메서드를 생각해 보겠습니다. 이 메서드는 파라미터를 받아들이고 값중 가장 큰 값을 반환합니다. 다음은 이 메서드의 간단한 사용 예입니다.

1
2
3
4
let value1 = 25,
value2 = 50;
console.log(Math.max(value1, value2)); // 50

이 예제에서와 같이 단지 두 개의 값을 다룰 때, Math.max()는 매우 사용하기 쉽습니다. 두 값이 전달되고 더 큰 값이 반환됩니다. 그러나 Array에서 값을 추적하고 있고 이제 가장 높은 값을 찾고 싶다면 어떻게해야 할까요? Math.max() 메서드는 Array을 전달할 수 없기 때문에, ECMAScript 5와 그 이전 버전에서는 Array을 직접 찾거나 다음과 같이 apply()를 사용했습니다.

1
2
3
let values = [25, 50, 75, 100]
console.log(Math.max.apply(Math, values)); // 100

이 방법으로 실행은 되지만, apply()를 이런 방식으로 사용하는 것은 다소 혼란스러울 수 있습니다. 실제로 추가 구문이 코드의 진정한 의미를 모호하게 만드는 것으로 보입니다.

ECMAScript 6 Spread 연산자는 이 경우를 매우 간단하게 만듭니다. apply()를 호출하는 대신 ArrayMath.max()에 직접 전달하고 Rest 파라미터와 함께 사용되는 ...패턴의 접두사를 붙일 수 있습니다. 그러면 JavaScript 엔진이 Array을 개별 파라미터로 분리하여 다음과 같이 전달합니다.

1
2
3
4
5
let values = [25, 50, 75, 100]
// console.log(Math.max(25, 50, 75, 100));
// 위 코드와 동일합니다.
console.log(Math.max(...values)); // 100

이제 Math.max()에 대한 호출은 좀 더 보편적으로 보이며 간단한 수학 연산을 위해 this-binding(앞의 예제에서 Math.max.apply()의 첫 번째 파라미터)을 지정하는 복잡성을 피할 수 있습니다.

Spread 연산자를 다른 파라미터와 혼합하여 사용할 수도 있습니다. Math.max()에서 반환되는 가장 작은 숫자가 0이 되길 원한다고 가정하십시오. (음수가 Array에 들어가는 경우를 대비해서). 그 파라미터를 개별적으로 전달할 수 있으며 다음과 같이 다른 파라미터에 대해서는 여전히 Spread 연산자를 사용합니다.

1
2
3
let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0)); // 0

이 예제에서,Math.max()에 전달된 마지막 파라미터는 0이며, 다른 파라미터들은 Spread 연산자를 사용하여 전달됩니다.

파라미터 전달을 위한 Spread 연산자는 함수 파라미터에 대한 Array 사용을 훨씬 쉽게 만듭니다. 대부분의 상황에서 apply() 메서드를 적절하게 대체할 수있을 것입니다.

ECMAScript 6에서는 지금까지 DefaultRest 파라미터에서 사용한 용도 외에도 JavaScript의 Function 생성자에 두 파라미터 유형을 모두 적용할 수 있습니다.

ECMAScript 6의 name 프로퍼티

JavaScript에서 다양한 방법으로 함수를 정의할 수 있어면 함수 식별이 어려울 수 있습니다. 또한, 익명 함수식이 널리 보급됨에 따라 디버깅이 조금 더 어려워지고, 종종 스택 추적의 읽기 및 해독이 어려워질수 있습니다. 이러한 이유로 ECMAScript 6은 모든 함수에 name 프로퍼티을 추가했습니다.

적절한 이름 선택하기

ECMAScript 6 프로그램의 모든 함수는 name 프로퍼티에 적절한 이름이 부여됩니다. 함수와 함수 표현식을 보여주는 다음 예제의 name 프로퍼티를 출력해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
function doSomething() {
// ...
}
var doAnotherThing = function() {
// ...
};
console.log(doSomething.name); // "doSomething"
console.log(doAnotherThing.name); // "doAnotherThing"

이 코드에서 doSomething()은 함수 선언이기 때문에 "doSomething"과 같은 name 프로퍼티를 가지고 있습니다. 익명 함수 표현 인 doAnotherThing()"doAnotherThing"name을 가지고 있습니다. 왜냐하면 그 이름이 할당된 변수의 이름이기 때문입니다.

name 프로퍼티의 특별한 Case

함수 선언 및 함수 표현식에 대한 이름은 쉽게 찾을 수 있습니다. 그리고 ECMAScript 6은 모든 함수가 적절한 이름을 갖도록합니다. 이것을 설명하기 위해 다음 프로그램을 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var doSomething = function doSomethingElse() {
// ...
};
var person = {
get firstName() {
return "Nicholas"
},
sayName: function() {
console.log(this.name);
}
}
console.log(doSomething.name); // "doSomethingElse"
console.log(person.sayName.name); // "sayName"
var descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor.get.name); // "get firstName"

이 예제에서,doSomething.name"doSomethingElse"입니다. 왜냐하면 함수 표현식 자체가 이름을 갖고, 그 이름이 함수가 할당된 변수보다 우선합니다. person.sayName()name 프로퍼티는 값이 객체 리터럴로부터 해석 되었기 때문에 "sayName"입니다. 비슷하게, person.firstName은 실제로 Getter 함수이므로 이 차이를 나타 내기 위해 이름은 "get firstName"입니다. Setter 함수 앞에는 "set"이 붙습니다.(GetterSetter 함수는 모두 Object.getOwnPropertyDescriptor()를 사용하여 검색해야 합니다.)

함수 이름에도 몇 가지 특별한 경우가 있습니다. bind()를 사용하여 생성된 함수의 이름은 "bound"로 시작하고, Function 생성자를 사용하여 생성된 함수의 이름은 "anonymous"입니다.

1
2
3
4
5
6
7
var doSomething = function() {
// ...
};
console.log(doSomething.bind().name); // "bound doSomething"
console.log((new Function()).name); // "anonymous"

Bound 함수의 name은 바운드되는 함수의 name에 문자열 "bound"로 접두사가 붙을 것이기 때문에 doSomething()의 바운드 버전은 "bound doSomething"입니다.

함수의 name 값이 반드시 같은 name 변수를 참조하는 것은 아닙니다. name 프로퍼티는 정보를 얻는 수단이되고 디버깅에 유익하지만, name의 값을 사용하여 함수에 대한 참조를 얻는 방법은 없습니다.

함수의 이중 목적을 명확히 하기

ECMAScript 5 및 이전 버전에서는 함수가 new 유무에 관계없이 호출할 수있는 이중 목적을 제공합니다. new와 함께 사용하면, 함수 안에있는 이 값은 새로운 객체이며, 다음 예제와 같이 새로운 객체가 반환됩니다 :

1
2
3
4
5
6
7
8
9
function Person(name) {
this.name = name;
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas");
console.log(person); // "[Object object]"
console.log(notAPerson); // "undefined"

notAPerson을 만들 때 new가 없이 Person ()을 호출하면 undefined가됩니다.(nonstrict 모드에서 전역 객체에 name 프로퍼티를 설정합니다). Person의 대문자 사용은 일반적인 JavaScript 프로그램에서 new를 사용하여 함수를 호출할 수 있다는 유일한 지표입니다. 함수의 이중 역할에 대한 혼란으로 인해 ECMAScript 6은 일부 변경되었습니다.

JavaScript는 함수를 위한 두 가지 내부 전용 메서드를 가지고있습니다. :[[Call]][[Construct]]. new 없이 함수가 호출되면 [[Call]]메서드가 실행되고 코드의 함수 본문이 실행됩니다. new로 함수를 호출하면, [[Construct]]메서드가 호출됩니다. [[Construct]]메서드는 new.target이라는 새로운 객체를 생성 한 후 thisnew.target으로 설정하여 함수 본문을 실행합니다. [[Construct]]메서드를 가진 함수를 Constructor라고 부릅니다.

모든 함수가 [[Construct]]를 가지고있는 것은 아니므로 new로 모든 함수를 호출할 수있는 것은 아닙니다. “Arrow Function” 섹션에서 논의된 Arrow 함수에는 [[Construct]] 메서드가 없습니다.

ECMAScript 5에서 함수 호출 방법 결정

ECMAScript 5에서 함수가 new와 함께 (그리고 Constructor와 함께) 호출되었는지 결정하는 가장 보편적 인 방법은 instanceof를 사용하는 것입니다.

1
2
3
4
5
6
7
8
9
10
function Person(name) {
if (this instanceof Person) {
this.name = name; // using new
} else {
throw new Error("You must use new with Person.")
}
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas"); // throws error

이 코드에서 this 값은 그것이 Constructor의 인스턴스인지를 검사하고, 만약 그렇다면 실행은 정상적으로 계속됩니다. this
Person의 인스턴스가 아니라면 에러가 발생합니다. 이것은[[Construct]] 메서드가 Person의 새로운 인스턴스를 생성하고 이것을 this에 할당하기 때문에 작동합니다. 불행히도, 이 접근법은 완전히 신뢰할 만하지 않습니다. 왜냐하면 아래의 예제에서와 같이 new를 사용하지 않고 Person의 인스턴스가될 수 있기 때문입니다 :

1
2
3
4
5
6
7
8
9
10
function Person(name) {
if (this instanceof Person) {
this.name = name; // using new
} else {
throw new Error("You must use new with Person.")
}
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // works!

Person.call()을 호출하면 person 변수가 첫 번째 인수로 전달됩니다. 이는 Person 함수 안에서 person으로 설정되었음을 의미합니다. 이 함수에는 new로 불리는 것과 구별할 방법이 없습니다.

new.target MetaProperty

이 문제를 해결하기 위해 ECMAScript 6는 new.target metaproperty를 도입했습니다. 메타 속성은 대상과 관련된 추가 정보 (예 : new)를 제공하는 비 객체의 프로퍼티입니다. 함수의 [[Construct]] 메서드가 호출되면 new.targetnew 연산자의 대상으로 채워집니다. 이 타겟은 일반적으로 새로 생성된 객체 인스턴스의 생성자이며, 이 객체 인스턴스는 함수 본문에서 this가 됩니다. [[Call]]이 실행되면 new.targetundefined가됩니다.

이 새로운 메타 속성을 사용하면 new.target이 다음과 같이 정의되어 있는지 여부를 확인하여 new 함수가 호출되었는지를 안전하게 감지할 수 있습니다 :

1
2
3
4
5
6
7
8
9
10
function Person(name) {
if (typeof new.target !== "undefined") {
this.name = name; // using new
} else {
throw new Error("You must use new with Person.")
}
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // error!

instanceof Person 대신 new.target을 사용함으로써, Person 생성자는 new없이 사용될 때 정확하게 에러를 던집니다.

특정 생성자를 사용하여 new.target이 호출되었는지 확인할 수 있습니다. 예를 들어 다음 예제를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name) {
if (new.target === Person) {
this.name = name; // using new
} else {
throw new Error("You must use new with Person.")
}
}
function AnotherPerson(name) {
Person.call(this, name);
}
var person = new Person("Nicholas");
var anotherPerson = new AnotherPerson("Nicholas"); // error!

이 코드를 보면 제대로 작동하기 위해서는 new.targetPerson이어야합니다. new AnotherPerson( "Nicholas")가 호출될 때, Person.call(this, name)에 대한 후속 호출은 new.targetPerson 생성자 내부에서 undefined이기 때문에 오류를 던질 것입니다. (`new ‘없이 호출 되었다).

경고 : 함수 밖에서 new.target을 사용하는 것은 구문 오류입니다.

new.target을 추가함으로써, ECMAScript 6은 함수 호출에 대한 모호함을 명확히하는 데 도움이되었습니다. 이 주제에 따라, ECMAScript 6은 언어의 또 다른 모호한 부분 인 Block-level 함수를 선언합니다.

Block-Level 함수

ECMAScript 3 및 이전 버전에서는 블록 내부에서 발생하는 함수 선언 (Block-level 함수)은 기술적으로 구문 오류 였지만 모든 브라우저가 여전히 이를 지원했습니다. 불행히도 구문을 허용하는 각 브라우저는 약간 다른 방식으로 동작하므로 블록 내부에서 함수 선언을 피하는 것이 가장 좋습니다 (함수 표현식을 사용하는 것이 가장 좋습니다).

이 호환되지 않는 동작을 억제하기 위해 ECMAScript 5 strict 모드에서는 함수 선언이 다음과 같이 블록 내부에서 사용될 때마다 오류가 발생했습니다.

1
2
3
4
5
6
7
8
9
"use strict";
if (true) {
// ES5에서는 구문 오류가 발생하지만 ES6에서는 발생하지 않습니다.
function doSomething() {
// ...
}
}

ECMAScript 5에서 이 코드는 구문 오류를 발생시킵니다. ECMAScript 6에서 doSomething() 함수는 Block-level선언으로 간주되며, 정의된 동일한 블록 내에서 액세스하고 호출할 수 있습니다. 예 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"use strict";
if (true) {
console.log(typeof doSomething); // "function"
function doSomething() {
// ...
}
doSomething();
}
console.log(typeof doSomething); // "undefined"

Block-level 함수는 정의된 블럭의 맨 위로 올라가므로 typeof doSomething은 코드의 함수 선언 앞에 나타나도, “function”을 반환합니다. if 블록의 실행이 끝나면 doSomething()은 더 이상 존재하지 않습니다.

Block-level 함수 사용시기 결정

Block-level 함수let 함수 표현식과 유사합니다. 함수 정의는 함수가 정의된 블록에서 실행된 후 제거됩니다. 중요한 차이점은 Block-level 함수는 포함 블록의 상단에 hoisting 된다는 것입니다. let을 사용하는 함수 표현식은 다음 예제와 같이hoisting되지 않습니다 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"use strict";
if (true) {
console.log(typeof doSomething); // throws error
let doSomething = function () {
// ...
}
doSomething();
}
console.log(typeof doSomething);

여기서 typeof doSomething이 실행되면 let 문이 아직 실행되지 않았기 때문에 코드 실행이 멈추고 TDZ에 doSomething()이 남게됩니다. 이 차이점을 안다면, Block-level 함수를 사용할 것인지, hoisting동작을 원하는지 여부에 따라 let 표현식을 사용할 것인지 선택할 수 있습니다.

Nonstrict 모드의 Block-level 함수

ECMAScript 6은 nonstrict 모드에서 Block-level 함수를 허용하지만, 동작은 약간 다릅니다. 함수 선언문을 블록의 상단으로 hoisting 하는 대신에, 함수를 포함하는 함수, 또는 전역 환경으로 계속 끌어 올립니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// ECMAScript 6 behavior
if (true) {
console.log(typeof doSomething); // "function"
function doSomething() {
// ...
}
doSomething();
}
console.log(typeof doSomething); // "function"

위 예제에서, doSomething()은 전역 Scope로 끌어 올려 지므로 if 블록의 바깥에 존재합니다. ECMAScript 6은 이 동작을 표준화하여 이전에는 존재하지 않는 호환되지 않는 브라우저 동작을 제거하므로 모든 ECMAScript 6 런타임은 동일한 방식으로 동작해야합니다.

Block-level 함수는 JavaScript에서 함수를 선언하는 기능이 향상시켰지만 ECMAScript 6에서는 함수를 선언하는 완전히 새로운 방법을 도입했습니다.

화살표 함수 (Arrow Function)

ECMAScript 6의 가장 흥미로운 부분 중 하나는 Arrow 함수입니다. Arrow 함수는 이름에서 알 수 있듯이 “Arrow”(=>)를 사용하는 새로운 구문으로 정의된 함수입니다. 하지만 Arrow 함수는 여러 가지 중요한 부분이 기존의 JavaScript 함수와 다르게 작동합니다.

  • this, super, arguments 및 new.target 바인딩은 없습니다 - 함수 내부의 this, super, arguments 및 new.target의 값은 가장 인접한 nonarrow 함수를 나타냅니다. (super는 4 장에서 다룬다.)
  • new로 호출할 수 없습니다 - Arrow 함수[[Construct]] 메서드를 가지지 않으므로 생성자를 사용할 수 없습니다. Arrow 함수new와 함께 사용될 때 에러를 던집니다.
  • prototype이 없습니다 - Arrow 함수new를 사용할 수 없으므로 prototype이 필요 없습니다. Arrow 함수는 prototype 프로퍼티가 존재하지 않습니다.
  • this를 바꿀 수 없습니다 - 함수 안의 this 값은 변경할 수 없습니다. 함수의 전체 라이프 사이클 동안 동일하게 유지됩니다.
  • arguments 객체가 없습니다 - Arrow 함수에는 arguments 바인딩이 없으므로 함수 파라미터에 액세스하려면 명명된 파라미터와 rest 파라미터에 의존해야합니다.
  • 이름이 중복된 파라미터가 없습니다 - Arrow 함수nonstrict 모드에서 중복된 이름의 파라미터를 가질수 있는 nonarrow 함수와는 달리 strict 또는 nonstrict 모드에서 중복된 이름의 파라미터를 가질수 없습니다.

이러한 차이에는 몇 가지 이유가 있습니다. 무엇보다 먼저, this 바인딩은 JavaScript의 일반적인 오류 원인입니다. 의도하지 않은 프로그램 동작을 초래할 수있는 함수 내에서 this값을 추적하는 것을 잃어 버리기가 매우 쉽습니다. Arrow 함수는 이러한 혼란을 없애줍니다. 둘째, Arrow 함수를 단일 this값으로 코드를 실행하는 것으로 제한함으로써 JavaScript 엔진은 생성자로 사용되거나 수정될 수있는 일반 함수와 달리 연산을 보다 쉽게 최적화할 수 있습니다.

나머지 차이점은 Arrow 함수 내부의 오류와 모호성을 줄이는 데에도 초점을 맞추고 있습니다. 그렇게함으로써, JavaScript 엔진은 Arrow
함수
실행을 더 잘 최적화할 수 있습니다.

참고 : Arrow 함수에는 다른 함수와 동일한 규칙을 따르는 name 프로퍼티는 있습니다.

Arrow 함수 Syntax

Arrow 함수의 구문은 여러분이 원하는 모양에 따라 다양한 형태로 가능합니다. 하지만 모든 Arrow 함수는 파라미터와 화살표, 함수 본문 순으로 시작됩니다. 파라미터와 본문 모두 사용법에 따라 다른 형태를 취할 수 있습니다. 예를 들어, 다음 Arrow 함수는 단일 파라미터를 받고 이를 단순히 반환합니다.

1
2
3
4
5
6
7
var reflect = value => value;
// 다음과 동일한 효과를 나타냅니다.
var reflect = function(value) {
return value;
};

Arrow 함수에 대해 파라미터가 하나만 있을 경우, 그 파라미터는 더 이상의 구문없이 직접 사용될 수 있습니다. 다음에 화살표가오고 화살표 오른쪽에 있는 표현식이 계산되어 반환됩니다. 명시적 return 문이 없더라도 이 Arrow 함수는 전달된 첫 번째 파라미터를 반환합니다.

둘 이상의 파라미터를 전달하는 경우 다음과 같이 파라미터를 괄호로 묶어야합니다.

1
2
3
4
5
6
7
var sum = (num1, num2) => num1 + num2;
// 다음과 동일한 효과를 나타냅니다.
var sum = function(num1, num2) {
return num1 + num2;
};

sum() 함수는 두개의 파라미터를 더하고 그 결과를 반환합니다. 이 Arrow 함수reflect() 함수의 유일한 차이점은 파라미터가 괄호 안에 쉼표로 구분되어 있습니다 (기존 함수처럼).

함수에 대한 파라미터가가 없으면 다음과 같이 빈 괄호 세트를 선언에 포함해야합니다.

1
2
3
4
5
6
7
var getName = () => "Nicholas";
// 다음과 동일한 효과를 나타냅니다.
var getName = function() {
return "Nicholas";
};

두 개 이상의 표현식으로 구성되는 더 전통적인 함수 본문을 제공하려면 이 버전의 sum ()에서와 같이 함수 본문을 중괄호로 묶고 반환 값을 명시적으로 정의해야합니다.

1
2
3
4
5
6
7
8
9
var sum = (num1, num2) => {
return num1 + num2;
};
// 다음과 동일한 효과를 나타냅니다.
var sum = function(num1, num2) {
return num1 + num2;
};

중괄호 내부는 arguments를 사용할 수 없다는 점을 제외하면 전통적인 함수와 동일합니다.

아무 것도하지 않는 함수를 만들려면 다음과 같이 중괄호를 포함해야합니다.

1
2
3
4
5
var doNothing = () => {};
// 다음과 동일한 효과를 나타냅니다.
var doNothing = function() {};

중괄호는 함수의 몸체를 나타 내기 위해 사용되며, 지금까지 보았던 경우에는 잘 작동합니다. 그러나 객체 리터럴을 반환하고자하는 화살표 함수는 괄호 안에 리터럴을 래핑해야합니다.

1
2
3
4
5
6
7
8
9
10
11
var getTempItem = id => ({ id: id, name: "Temp" });
// 다음과 동일한 효과를 나타냅니다.
var getTempItem = function(id) {
return {
id: id,
name: "Temp"
};
};

괄호 안에 객체 리터럴을 래핑하면 중괄호가 함수 본문 대신 객체 리터럴임을 알립니다.

즉시 호출 함수 표현식 만들기 (Immediately-Invoked Function Expression)

JavaScript 함수의 인기있는 사용은 즉시 호출 함수 표현식 (Immediately-Invoked Function Expression - IIFE)을 작성하는 것입니다. IIFE를 사용하면 익명의 함수를 정의하여 참조를 저장하지 않고 즉시 호출할 수 있습니다. 이 패턴은 프로그램의 나머지 부분으로부터 보호되는 Scope를 만들 때 유용합니다. 예 :

1
2
3
4
5
6
7
8
9
10
11
let person = function(name) {
return {
getName: function() {
return name;
}
};
}("Nicholas");
console.log(person.getName()); // "Nicholas"

이 코드에서 IIFE는 getName() 메서드를 사용하여 객체를 만드는 데 사용됩니다. 이 메서드는 name 파라미터를 리턴 값으로 사용하여 반환된 객체의 private 멤버를 만듭니다.

괄호를 이용하여 래핑하면 동일한 작업을 수행 하는 Arrow 함수를 만들수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
let person = ((name) => {
return {
getName: function() {
return name;
}
};
})("Nicholas");
console.log(person.getName()); // "Nicholas"

괄호는 Arrow 함수 정의 주위에서만 있고 ("Nicholas")는 포함하지 않습니다. 이것이 기존의 함수와 다른데, 괄호는 입력된 파라미터를 포함하여 함수 정의 주변만 배치 할 수 있습니다.

this 바인딩이 없음

JavaScript에서 가장 일반적인 오류 중 하나는 함수 내부에 this를 바인딩하는 것입니다. this의 값은 함수가 호출되는 문맥에 따라 하나의 함수 안에서 바뀔 수 있기 때문에, 의도하지 않지만 실수로 다른 객체에 영향을 미칠 수 있습니다. 다음 예제를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", function(event) {
this.doSomething(event.type); // error
}, false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};

위 코드에서 객체 PageHandler는 페이지와 상호 작용을 처리하도록 설계되었습니다. 상호 작용을 설정하기 위해 init() 메서드가 호출되고, 그 메서드는 차례대로 this.doSomething()을 호출하는 이벤트 핸들러를 할당합니다. 그러나 이 코드는 의도 한대로 정확하게 작동하지 않습니다.

this.doSomething()에 대한 호출은 thisPageHandler에 바인딩되는 대신 이벤트 대상 (이 경우 document)에 대한 참조이기 때문에 끊어집니다. 이 코드를 실행하려고하면 this.doSomething()이 대상 document 객체에 없기 때문에 이벤트 핸들러가 실행될 때 오류가 발생합니다.

bind() 메서드를 이용해 명시적으로 PageHandler에 바인딩함으로써 이 문제를 해결할 수 있습니다 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", (function(event) {
this.doSomething(event.type); // no error
}).bind(this), false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};

이제 코드는 예상대로 작동하지만 약간 이상하게 보일 수 있습니다. bind(this)를 호출함으로써 실제로 this가 현재 this에 바인드된 새로운 함수,PageHandler를 생성하고 있습니다. 이 코드를 수정하는 더 좋은 방법은 Arrow 함수를 사용하는 것입니다.

Arrow 함수에는 this 바인딩이 없습니다. 즉, Arrow 함수 안에있는 this의 값은 Scope 체인을 찾는 것으로만 결정될 수 있습니다. Arrow 함수nonarrow 함수 내에 포함되어 있다면, this는 포함 함수와 같습니다. 그렇지 않으면 this는 전역 Scope에서 this의 값과 같습니다. Arrow 함수를 사용하여 이 코드를 작성할 수있는 한 가지 방법은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click",
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};

이 예제의 이벤트 핸들러는 this.doSomething()을 호출하는 Arrow 함수입니다. this의 값은 init() 안에있는 것과 같기 때문에, 이 버전의 코드는 bind (this)를 사용하는 것과 비슷하게 동작합니다. doSomething() 메서드는 값을 반환하지 않지만, 여전히 함수 본문 내에서 실행되는 유일한 명령문이므로, 중괄호를 포함할 필요가 없습니다.

Arrow 함수는 “제거 가능한(throwaway)” 함수로 설계되었으므로 새 타입을 정의하는 데 사용할 수 없습니다. 이것은 일반 함수에 있는 prototype 속성이 빠져 있음을 알 수 있습니다. 화살표 함수로 new 연산자를 사용하려고하면 다음 예제와 같이 오류가 발생합니다.

1
2
var MyType = () => {},
object = new MyType(); // error - you can't use arrow functions with 'new'

이 코드에서, MyTypeArrow 함수이므로 [[Construct]] 행위를 하지 않기 때문에 새로운 MyType()에 대한 호출은 실패합니다. JavaScript 엔진은 Arrow 함수new와 함께 사용할 수 없다는 사실을 알게 되면 자신의 행동을 더욱 최적화할 수 있습니다.

또한, this값은 Arrow 함수가 정의된 포함 함수에 의해 결정되므로 call(), apply() 또는 bind()를 사용하여 this의 값을 변경할 수 없습니다.

Arrow 함수와 Array

Arrow 함수의 간결한 구문은 Array 처리와 함께 사용하기에 이상적입니다. 예를 들어, 사용자 정의 Comparator를 사용하여Array을 정렬하려면 일반적으로 다음과 같이 작성합니다.

1
2
3
var result = values.sort(function(a, b) {
return a - b;
});

이것은 매우 간단한 기능을 위해 많은 구문이 필요합니다. 더 간결한 Arrow 함수 버전과 비교해보십시오.

1
var result = values.sort((a, b) => a - b);

sort(), map(), reduce()와 같은 콜백 함수를 받아들이는 Array 메서드는 단순한 코드로 복잡한 프로세스를 변경하는 Arrow 함수 구문의 이점을 모두 누릴 수 있습니다.

arguments 바인딩 없음

Arrow 함수가 자신의 arguments 객체를 가지고 있지는 않지만 포함 함수의 arguments 객체에 접근할 수 있습니다. Arrow
함수
가 어디에서 실행 되든 관계없이 포함 함수의 arguments객체를 사용할 수 있습니다.

1
2
3
4
5
6
7
function createArrowFunctionReturningFirstArg() {
return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction()); // 5

createArrowFunctionReturningFirstArg() 내부에서, arguments[0] 요소는 생성된 Arrow 함수에 의해 참조됩니다. 그 참조는createArrowFunctionReturningFirstArg() 함수에 전달 된 첫 번째 파라미터를 나타냅니다. Arrow 함수가 나중에 실행되면 5를 반환하는데, 이것은 createArrowFunctionReturningFirstArg()에 전달된 첫 번째 인파라미터입니다. Arrow 함수가 더 이상 자신를 생성 한 Scope에 없더라도 arguments 식별자의 Scope chain resolution 때문에 arguments는 여전히 접근 가능합니다.

Arrow 함수 식별하기

다른 구문에도 불구하고 Arrow 함수는 여전히 함수이며 식별 가능합니다. 다음 코드를 생각해 보겠습니다.

1
2
3
4
var comparator = (a, b) => a - b;
console.log(typeof comparator); // "function"
console.log(comparator instanceof Function); // true

console.log() 결과는, typeofinstanceof가 다른 함수와 마찬가지로 Arrow 함수가 똑같이 행동한다는 것을 보여줍니다.

함수의 this-binding은 영향을 받지 않지만, 다른 함수와 마찬가지로 Arrow 함수는 여전히 call(), apply()bind()를 사용할 수 있습니다. 여기 몇 가지 예가 있습니다.

1
2
3
4
5
6
7
8
var sum = (num1, num2) => num1 + num2;
console.log(sum.call(null, 1, 2)); // 3
console.log(sum.apply(null, [1, 2])); // 3
var boundSum = sum.bind(null, 1, 2);
console.log(boundSum()); // 3

sum() 함수는 다른 함수와 마찬가지로 call()apply()를 사용하여 파라미터를 전달합니다. bind()메서드는 boundSum()을 생성하는데 사용되며, 두개의 파라미터는 1과 2에 연결되어 직접 전달할 필요가 없습니다.

Arrow 함수는 콜백과 같이 현재의 익명 함수 표현식을 사용하고있는 곳이면 어디서든 사용하기에 적합합니다. 다음 섹션은 또 다른 주요한 ECMAScript 6을 다루지만, 이것은 모두 내부적이며 new 구문을 가지고 있지 않습니다.

Tail Call 최적화

아마도 ECMAScript 6의 기능 중 가장 흥미로운 변화는 Tail call 시스템을 변경하는 엔진 최적화입니다. Tail call은 함수가 다음과 같이 함수의 마지막 명령문으로 다른 함수를 호출하는 패턴입니다.

1
2
3
function doSomething() {
return doSomethingElse(); // tail call
}

ECMAScript 5 엔진에서 구현된 Tail call은 다른 함수 호출과 마찬가지로 처리됩니다. 새 스택 프레임이 만들어지고 Call 스택에 푸시되어 함수 호출을 나타냅니다. 이는 모든 이전 스택 프레임이 메모리에 유지된다는 것을 의미하며, 호출 스택이 너무 커지면 문제가됩니다.

무엇이 다른가?

ECMAScript 6는 strict 모드에서 특정 Tail call에 대한 호출 스택의 크기를 줄이려고 시도합니다 (nonstrict 모드 Tail call은 그대로 유지됩니다). 이 최적화를 통해 Tail call에 대한 새 스택 프레임을 만드는 대신 다음 조건이 충족되는 한 현재 스택 프레임이 지워지고 다시 사용됩니다.

  1. Tail call은 현재 스택 프레임의 변수에 액세스할 필요가 없습니다 (함수가 클로저가 아님을 의미)
  2. Tail call을 수행하는 함수는 Tail call이 리턴 한 후에 더 이상 수행할 작업이 없습니다.
  3. Tail call의 결과는 함수 값으로 반환됩니다.

예를 들어 다음 코드는 세 가지 기준 모두에 적합하기 때문에 이 코드를 쉽게 최적화할 수 있습니다.

1
2
3
4
5
6
"use strict";
function doSomething() {
// optimized
return doSomethingElse();
}

위의 함수는 doSomethingElse()에 대해 Tail call을 하고 결과를 즉시 반환하며 로컬 범위의 변수에 액세스하지 않습니다. 하지만 아래의 함수는 결과를 반환하지 않기 때문에 최적화되지 않습니다.

1
2
3
4
5
6
"use strict";
function doSomething() {
// not optimized - no return
doSomethingElse();
}

마찬가지로 Tail call에서 복귀 한 후 작업을 수행하는 경우 함수를 최적화할 수 없습니다.

1
2
3
4
5
6
"use strict";
function doSomething() {
// not optimized - must add after returning
return 1 + doSomethingElse();
}

위 예제는 값을 반환하기 전에 doSomethingElse()에 단지 1을 더하지만 최적화를 끄기에 충분합니다.

실수로 최적화를 끄는 또 다른 일반적인 실수는 함수 호출 결과를 변수에 저장 한 다음 결과를 반환하는 것입니다. 예를 들면 다음과 같습니다.

1
2
3
4
5
6
7
"use strict";
function doSomething() {
// not optimized - call isn't in tail position
var result = doSomethingElse();
return result;
}

위 예제는 doSomethingElse()의 값이 즉시 리턴되지 않기 때문에 최적화될 수 없습니다.

아마도 가장 어려운 상황은 클로저 사용에 있습니다. 클로저가 포함된 Scope의 변수에 액세스할 수 있으므로 Tail call 최적화가 해제될 수 있습니다.

1
2
3
4
5
6
7
8
9
"use strict";
function doSomething() {
var num = 1,
func = () => num;
// not optimized - 함수가 closure 입니다.
return func();
}

클로저 func()는 이 예제에서 지역 변수 num에 접근할 수 있습니다. func()를 호출하면 즉시 결과가 반환되지만 변수 num을 참조하기 때문에 최적화할 수 없습니다.

Tail call 최적화를 활용하는 방법

실제로 Tail call 최적화는 배후에서 발생하기 때문에 함수를 최적화하려고 시도하지 않는 한 생각할 필요가 없습니다. Tail call 최적화의 주요 사용 사례는 재귀 함수입니다. 최적화가 가장 큰 효과를 발휘하기 때문입니다. 팩토리얼을 계산하는 이 함수를 생각해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
function factorial(n) {
if (n <= 1) {
return 1;
} else {
// not optimized - must multiply after returning
return n * factorial(n - 1);
}
}

곱셈이 factorial()을 재귀 호출 한 후에 일어나야하기 때문에 이 버전의 함수는 최적화될 수 없습니다. n이 매우 큰 경우 호출 스택 크기가 커지고 잠재적으로 스택 오버플로가 발생할 수 있습니다.

함수를 최적화 하려면 마지막 함수 호출 후에 곱셈이 발생하지 않도록 해야합니다. 이를 위해 Default 파라미터를 사용하여 곱하기 연산을 return 문 외부로 옮길 수 있습니다. 결과 함수는 임시 결과를 따라 다음 반복으로 전달되어 동일하게 동작하지만 ECMAScript 6 엔진에서 최적화할 수있는 함수를 만듭니다. 다음은 새로운 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
function factorial(n, p = 1) {
if (n <= 1) {
return 1 * p;
} else {
let result = n * p;
// optimized
return factorial(n - 1, result);
}
}

이 다시 작성된 factorial() 버전에서 두 번째 파라미터 p가 추가되었고 Default 값이 1입니다. p 파라미터는 이전 곱셈 결과를 유지하므로 다른 함수 호출없이 다음 결과를 계산할 수 있습니다. n이 1보다 크면 곱셈이 먼저 수행된 후 두 번째 인수로 factorial()으로 전달됩니다. 이를 통해 ECMAScript 6 엔진이 재귀 호출을 최적화할 수 있습니다.

Tail call 최적화는 재귀 함수를 작성할 때, 특히 연산이 많은 함수에 적용할 때 상당한 성능 향상을 제공할 수 있으므로 생각해야할 부분입니다.

요약

함수는 ECMAScript 6에서 큰 변화를 겪지는 않았지만 일련의 점진적 변경으로 인해 작업이 더 쉬워졌습니다.

함수의 Default 파라미터를 사용하면 특정 파라미터가 전달되지 않을 때 사용할 값을 쉽게 지정할 수 있습니다. ECMAScript 6 이전에는 함수 내부에 몇 가지 추가 코드가 필요 했으므로 파라미터가 있는지 확인하고 다른 값을 할당할 수 있었습니다.

Rest 파라미터를 사용하면 나머지 모든 파라미터를 배치할 Array을 지정할 수 있습니다. 실제 Array을 사용하고 포함시킬 파라미터를 지정하게하면 Rest 파라미터arguments보다 훨씬 더 유연한 해결책이됩니다.

Spread 연산자Rest 파라미터와 함께 사용되므로 함수를 호출할 때 Array을 개별 파라미터로 분해할 수 있습니다. ECMAScript 6 이전에는 Array에 포함된 개별 파라미터를 전달하는 두 가지 방법이 있었습니다. 수동으로 각 파라미터를 지정하거나 apply()를 사용하는 것입니다. Spread 연산자를 사용하면 함수의 ‘this’ 바인딩에 대한 걱정없이 모든 함수에 Array을 쉽게 전달할 수 있습니다.

name 프로퍼티을 추가하면 디버깅 및 평가 목적을 쉽게 식별할 수 있습니다. 또한 ECMAScript 6는 Block-level 함수의 동작을 공식적으로 정의하므로 더 이상 strict 모드에서는 구문 오류가 아닙니다.

ECMAScript 6에서 함수의 동작은 [[Call]], 일반 함수 실행, [[Construct]]에 의해 정의되며, new로 호출이 됩니다. new.target 메타 속성은 new를 사용하여 함수가 호출되었는지 아닌지를 결정할 수 있게합니다.

ECMAScript 6에서 가장 많이 변경된 기능은 Arrow 함수가 추가된 것입니다. Arrow 함수는 익명의 함수 표현식 대신 사용되도록 설계되었습니다. Arrow 함수는 보다 간결한 문법, 어휘, this 바인딩 없음, arguments 객체를 가지고 있지 않습니다. 또한, Arrow 함수this 바인딩을 변경할 수 없으므로 Constructor로 사용할 수 없습니다.

Tail call 최적화는 더 작은 호출 스택을 유지하고 더 적은 메모리를 사용하며 스택 오버플로 오류를 방지하기 위해 일부 함수 호출을 최적화할 수 있습니다. 이 최적화는 엔진에서 자동으로 적용되므로 안전하지만 이 최적화를 이용하려면 재귀 함수를 다시 작성하기로 결정할 수도 있습니다.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://leanpub.com/understandinges6/read#leanpub-auto-functions]

참고

공유하기