3장 새로운 함수 기능

2023.06.16

3.1 화살표 함수와 this, super, 그 외

  • ES2015 자바스크립트에 화살표 함수를 추가했다.
  • 화살표 함수는 특히 콜백을 사용하여 전체 문제를 해결한다.
  • 함수 내부의 this가 함수 외부의 this와 동일한지 보장한다.
  • 또한 function 키워드를 사용하여 만든 기존 함수보다 가볍고 간결하다.

3.1.1 화살표 함수 문법

  • 화살표 함수는 간결한 본문 (종종 간결한 화살표 함수라고 함)과 표준 함수 본문(전통적인 함수만큼 장황하지는 않지만 장황한 화살표 함수라고 함)의 두 가지 형태로 제공된다.
  • 화살표 함수는매개변수가 여러 개인 경우 괄호가 필요하다.
    • 일관성을 유지하려면 매개변수가 하나만 있더라도 괄호를 사용한다.

경고: 콤마 연산자와 화살표 함수

간결한 형태의 화살표 함수는 => 뒤에 표현식을 취하기 때문에 일부 프로그래머는 함수에서 할 일이 두세 가지만 있으면 장황한 양식을 사용하지 않기 위해 쉼표 연산자를 사용했다. 쉼표 연산자는 자바스크립트의 이상한 연산자 중 하나이다. 왼쪽 피연산자를 평가하고 결팟값을 버리고 오른쪽 피연산자를 평가하고 그 값을 결과로 취한다.

1function returnSecond(a, b) {
2  return a, b;
3}
4console.log(returnSecond(1, 2));   // 2
5
6return a, b의 결과는 b의 값이다.

3.1.2 화살표 함수와 this

  • 중요한 비결은 전통적인 함수와 달리 자체 버전이 없다는 것이다.
  • 대신 변수를 감싸는 것처럼 생성된 컨텍스트의 this를 감싼다.
  • 화살표 함수가 this만 감싸는 것은 아니다. 인수와 super와 new.target도 감싼다.

3.1.3 화살표 함수는 생성자가 될 수 없다

  • 화살표 함수는 this를 가지고 있지 않아서 화살표 함수가 생성자 함수가 될 수 없음을 이해했을 것이다.
    • 즉, new와 함께 사용할 수 없다.
  • 함수에 고유한 것이 없으면 새 객체에 속성을 설정할 수 없으며 생성자가 되는 것은 의미가 없다.
  • 생성자로 사용하는 것은 명시적으로 허용되지 않는다.
    • 명시적으로 허용하지 않기 때문에 화살표 함수는 기존 함수보다 가벼워질 수 있다.
    • 객체가 첨부된 프로토타입 속성을 가질 필요가 없기 때문이다.
  • 함수를 생성자로 사용하면 생성된 새 객체의 프로토타입이 함수의 프로토타입에서 할당된다는 것을 기억할 것이다.
1function Thingy() {}
2var t = new Thingy();
3console.log(Object.getPrototypeOf(t) === Thingy.prototype); // true
  • 자바스크립트 엔진은 기존 함수를 생성자로 사용할지 여부를 미리 알 수 없기 때문에 생성하는 모든 기존 함수에 참조할 프로토타입 속성과 객체를 넣어야 한다.
  • 그런데 화살표 함수는 생성자가 될 수 없으므로 prototype 속성을 받지 않는다.
1function traditional() {}
2const arrow = () => {};
3console.log('prototype' in traditional); // true
4console.log('prototype' in arrow); // false

3.2 기본값 매개변수

  • 함수가 호출될 때 매개변수의 값이 undefined면 기본값이 된다.
  • 함수를 호출할 때 인수를 완전히 정의하지 않으면 매개변수의 값은 undefined가 된다.

3.2.1 기본값은 표현식이다

  • 매개변수 기본값은 표현식이다. 리터럴 값일 필요는 없다. 기본값은 함수를 호출할 수도 있다.
  • 기본 표현식은 함수가 정의될 때가 아니라 함수가 호출될 때 평가되며 함수에 대한 특정 호출에 기본값이 필요한 경우에만 평가된다.

3.2.2 기본값은 자체 범위에서 평가된다

  • 기본값은 함수 본문내에 정의된 그 어떤 것도 참조할 수 없으며 호이스트된 것도 참조할 수 없다.

단순하지 않은 매개변수 목록 및 엄격한 모드 변경

기본값이나 배우게 될 다른 ES2015+ 매개변수 기능이 없는 단순한 매개변수 목록인 매개변수 목록을 ‘단순’ 매개변수 목록이라고 한다. 매개변수 목록이 새로운 기능을 사용하는 경우 이를 ‘단순하지 않은’ 매개변수 목록이라고 한다.

놀랍게도 단순하지 않은 매개변수 목록이 있는 함수는 "use strict" 지시문을 가질 수 없다.

1function example(answer = 42) {
2  ‘use strict’; // SyntaxError: Illegal 'use strict' directive<br/>
3                // 단순하지 않은 매개변수 목록이 있는 함수
4  console.log(answer);
5}

구문 분석의 복잡성을 피하기 위해 함수 정의가 이미 엄격한 컨텍스트에 표시되더라도 단순하지 않은 매개변수 목록이 있는 경우 "use strict" 지시문을 가질 수 없다. 정의된 컨텍스트의 엄격함만 상속할 수 있다.

3.2.3 기본값은 함수의 인자 수에 포함되지 않는다

  • 함수의 인자 수는 일반적으로 함수가 가지고 있는 공식 선언 매개변수의 수로 정의되며, 자바스크립트에서는 함수의 length 속성에서 가져올 수 있다.
1function none() {}
2console.log(none.length); // 0
3function one(a) {}
4console.log(one.length); // 1
  • 자바스크립트에서 기본값이 있는 매개변수는 인자 수를 계산할 때 계산되지 않으며 실제로 후속 매개변수도 계산되지 않는다.
1function stillOne(a, b = 42) {}
2console.log(stillOne.length); // 1
3function oneYetAgain(a, b = 42, c) {}
4console.log(oneYetAgain.length); // 1

3.3 "나머지" 연산자

  • 일반적으로 함수를 작성할 때 어떤 매개변수가 필요한지 알고 있으며 각 호출에서 동일한 수의 매개변수가 필요하다.
  • 하지만 일부 함수는 다양한 수의 매개변수를 허용해야 한다.
  • 예를 들어, Object.assign이 ES2015에 추가되기 전에 많은 자바스크립트 프로그래머의 툴킷에는 대상 객체와 하나 이상의 소스 객체를 허용하는 extend 함수가 포함되었다.
1function extend(target, ...sources) {
2  sources.forEach((source) => {
3    Object.keys(source).forEach((key) => {
4      target[key] = source[key];
5    });
6  });
7  return target;
8}
9const obj = extend({}, { a: 1 }, { b: 2 });
10console.log(obj); // {a: 1, b: 2}
  • arguments에는 몇 가지 문제가 있다.
    • 느슨한 모드에서는 양식 매개변수에 연결되는 방식으로 인해 성능 문제가 발생한다.
    • (엄격 모드를 사용하여 해결할 수 있다).
    • 모든 인수가 있다. 즉, 양식 매개변수와 동일한 인수를 넘어서 인덱싱해야 한다. (extend 예 에서 서 n = 0 대신 n = 1로 for 루프를 시작하는 이유다.)
    • 배열과 비슷하지만 배열이 아니므로 forEach이나 map 같은 배열 기능이 없다.
    • 화살표 함수에는 자체 arguments 객체가 없다.
    • 필자의 생각이지만 이러한 인수의 기능별 의미를 포착하지 못하는 어설픈 이름을 가지고 있다.
  • ES2015 이러한 문제에 대한 해결책인 "나머지” 매개변수를 추가했다.
  • 마지막 매개변수 앞에 줄임표(...)를 선언하면 자바스크립트 엔진이 해당 지점(인수의 나머지)에서 모든 실제 인수를 수집하여 다음과 같이 실제 배열로 해당 매개변수에 넣도록 지시한다.
  • 나머지 매개변수를 사용하도록 이전 extend 함수를 업데이트하는 경우 나머지 매개변수의 값이 실제 배열이기 때문에 for 대신 forEach를 사용할 수 있다.
1const extend = (target, ...sources) => {
2  sources.forEach((source) => {
3    Object.keys(source).forEach((key) => {
4      target[key] = source[key];
5    });
6  });
7  return target;
8};
9const obj = extend({}, { a: 1 }, { b: 2 });
10console.log(obj); // {a: 1, b: 2}
  • …은 연산자가 아니지만 사람들이 “나머자 연산자”라고 부르는 것을 들을 것이다. 이것은 단지 “구문”이다.

3.4 매개변수 목록에서 후행 쉼표와 함수 호출

  • ES2017을 사용하면 매개변수 목록과 인수 목록 끝에 쉼표를 붙일 수 있다.
  • 나중에 세 번째 매개변수를 추가할 때 새 줄만 추가하면 된다. 기존 엔트리를 변경할 필요가 없다.
1function example(
2  question, // (string) The question, must end with a question mark
3  answer, // (string) The answer, must end with appropriate punctuation
4) {
5  console.log('question: ' + question);
6  console.log('answer: ' + answer);
7}
8example('Do you like building software?', 'Big time!');

3.5 함수 name 속성

  • ES2015는 마침내 함수 name 속성을 표준화했으며 (몇 년 동안 일부 자바스크립트 엔진에서 비표준 화장이었다) 매우 흥미롭고 강력한 방식으로 이를 수행하여 무엇보다도 익명이었던 많은 함수를 더 이상 익명이 아닌 것으로 만든다.
  • 함수가 이름을 얻는 명백한 방법은 함수 선언 또는 명명 된 함수 표현식을 사용하는 것이다.
1// Declaration
2function foo() {}
3console.log(foo.name); // "foo"
4
5// Named function expression
6const f = function bar() {};
7console.log(f.name); // "bar"
  • 이름을 알게 된다면 목적을 파악하는 데 편리하며 특히 오류에 대한 호출 스택에서 유용하다.
  • 이름이 설정되는 시기와 위치는 사양에 명확하고 신중하게 정의되어 있다.
  • 함수를 할당할 때 변수를 선언하든 나중에 함수를 할당하든 상관없다. 중요한 것은 함수를 만들 때다.
1let foo;
2foo = function () {};
3console.log(foo.name); // "foo"
  • 화살표 함수 표현식은 익명 함수 표현식과 동일하게 작동한다.
1let foo = () => {};
2console.log(foo.name); // "foo"
  • 객체 표현식에서 익명 함수 표현식의 결과를 속성에 할당하면 함수는 속성의 이름을 가져온다.
1const obj = {
2  foo: function () {},
3};
4console.log(obj.foo.name); // "foo"
  • 함수를 기본 매개변수 값으로 사용할 때도 작동한다.
1(function (callback = function () {}) {
2  console.log(callback.name); // "callback"
3})();
  • 함수의 이름을 설정하지 않는 작업이 하나 있다. 기존 객체의 객체 속성에 할당하는 것이다.
1const obj = {};
2obj.foo = function () {};
3console.log(obj.foo.name); // "" - there's no name
  • TC39는 이와 같은 특별한 사용을 너무 많은 정보 유출로 간주했다.
1// 앱에 사용자와 관련된 일부 비밀 정보로 키가 지정된 핸들러 캐시가 있고
2// 핸드러를 일부 외부 코드에 전달해야 한다고 가정해 보자.
3
4cache[getUserSecret(user)] = function () {};
5
6// 함수 이름은 해당 작업에 의해 설정되었으며
7// 핸들러 함수를 타사 코드에 제공하면 getUserSecret(user)의 값이 제공된다.
  • 따라서 위원회는 함수 이름 설정에서 특정 작업을 의도적으로 생략했다.

3.6 블록 내 함수 선언

  • 수년 동안 함수 선언을 블록 안에 넣는 것은 사양에 포함되지 않았지만 허용되지 않은 것은 아니었다.
  • 자바스크립트 엔진은 이를 "허용 가능한 확장"으로 처리할 수 있다. ES2015부터 블록의 함수 선언은 사양의 일부이다.
  • TC39가 블록의 함수 선언에 대한 의미를 정의하려고 했을 때, 이 세상의 중요한 코드를 무효화하지 않고 합리적이고 일관된 것을 지정하는 매우 어려운 작업을 수행했다.
    • ES2015의 나머지 부분과 일치하는 표준 의미를 정의했다.
    • 웹 브라우저의 느슨한 모드 코드에 대해서만 “레거시 웹 의미론”을 정의했다.

3.6.1 블록 내 함수 선언: 표준 의미론

  • 표준 의미 체계를 사용하면 함수 선언이 let 변수에 할당된 함수 표현식으로 효과적으로 변환되고, 블록 맨위에 올라간다.
1'use strict';
2function branching(num) {
3  console.log(num);
4  if (num < 0.5) {
5    console.log('true branch, typeof doSomething = ' + typeof doSomething);
6    function doSomething() {
7      console.log('true');
8    }
9  } else {
10    console.log('false branch, typeof doSomething = ' + typeof doSomething);
11    function doSomething() {
12      console.log('false');
13    }
14  }
15  doSomething();
16}
17branching(Math.random());
  • 엄격 모드에서 자바스크립트 엔진은 위의 코드를 다음과 같이 처리한다.
1'use strict';
2function branching(num) {
3  console.log(num);
4  if (num < 0.5) {
5    let doSomething = function doSomething() {
6      console.log('true');
7    };
8    console.log('true branch, typeof doSomething = ' + typeof doSomething);
9  } else {
10    let doSomething = function doSomething() {
11      console.log('false');
12    };
13    console.log('false branch, typeof doSomething = ' + typeof doSomething);
14  }
15  doSomething();
16}
17branching(Math.random());
  • 당연히 실행하면 실패한다. 각 블록의 doSomething이 블록 스코프이기 때문에 맨 끝에 있는 doSomething은 함수의 최상위 범위에서 선언된 식별자가 아니기 때문이다.
  • 아래와 같이 실행되도록 변경해보자.
1'use strict';
2function branching(num) {
3  let f;
4  console.log(num);
5  if (num < 0.5) {
6    console.log('true branch, typeof doSomething = ' + typeof doSomething);
7    f = doSomething;
8    function doSomething() {
9      console.log('true');
10    }
11  } else {
12    console.log('false branch, typeof doSomething = ' + typeof doSomething);
13    f = doSomething;
14    function doSomething() {
15      console.log('false');
16    }
17  }
18  f();
19}
20branching(Math.random());
  • f는 한 함수 또는 다른 함수를 참조하고 true 또는 false를 기록한다. 호이스팅 때문에 doSomething은 해당 할당이 선언 위에 있더라도 f에 할당될 때 함수를 참조한다.

3.6.2 블록의 함수 선언: 레거시 웹 의미 체계

  • 웹 브라우저의 느슨한 모드에서는 사양의 부속서 B에 정의된 레거시 웹 의미 체계가 적용된다. (일부 엔진은 이를 웹 브라우저 외부에서도 적용한다).
  • 사양은 블록의 함수 선언을 구문 오류로 처리하지 않을 때 엔진이 이를 처리하는 다른 방식이 해당 자바스크립트 엔진의 교차점에 의해 신뢰할 수 있는 동일한 방식으로 처리된 시나리오만 의미한다는 것을 명시한다.
  • 이 세 가지 시나리오는 다음과 같다.
    1. 함수는 단일 블록 내에서만 선언되고 참조된다.
    2. 함수는 단일 블록 내에서 선언되고 사용 가능하지만 동일한 블록 내에 포함되지 않은 내부 함수 정의에 의해 참조된다.
    3. 함수는 단일 블록 내에서 선언되고 사용 가능하지만 후속 블록 내에서도 참조된다.
1function branching(num) {
2  console.log('num = ' + num + ', typeof doSomething = ' + typeof doSomething);
3  if (num < 0.5) {
4    console.log('true branch, typeof doSomething = ' + typeof doSomething);
5    function doSomething() {
6      console.log('true');
7    }
8    console.log('end of true block');
9  } else {
10    console.log('false branch, typeof doSomething = ' + typeof doSomething);
11    function doSomething() {
12      console.log('false');
13    }
14    console.log('end of false block');
15  }
16  doSomething();
17}
18branching(Math.random());
  • 느슨한 모드에서 자바스크립트 엔진은 위의 코드를 아래와 같이 효과적으로 처리한다.
1function branching(num) {
2  var varDoSomething;
3  console.log('num = ' + num + ', typeof doSomething = ' + typeof doSomething);
4  if (num < 0.5) {
5    let letDoSomething = function doSomething() {
6      console.log('true');
7    };
8    console.log('true branch, typeof doSomething = ' + typeof doSomething);
9    varDoSomething = letDoSomething; // 선언이 있던 곳
10    console.log('end of true block');
11  } else {
12    let letDoSomething = function doSomething() {
13      console.log('false');
14    };
15    console.log('false branch, typeof doSomething = ' + typeof doSomething);
16    varDoSomething = letDoSomething; // 선언이 있던 곳
17    console.log('end of false block');
18  }
19  varDoSomething();
20}
21branching(Math.random());
  • 함수는 함수 범위 var 변수에 할당되므로 블록 외부에서 접근할 수 있다. 그러나 선언은 여전히 블록 내에 들어있다.
  • 이러한 레거시 의미 체계에 의존하는 새 코드를 작성하지 않는 것이 가장 좋다.

3.7 과거 습관을 새롭게

3.7.1 다양한 값 대신 화살표 함수 사용 해결 방법

  • 화살표 함수를 사용하면 아래와 같이 과거에 콜백에서 호출 컨텍스트에 this를 접근할 수 있는 방법을 사용하지 않아도 된다.
1var self = this;
2this.entires.forEach(function (entry) {
3  //...
4});

3.7.2 this 또는 인수를 사용하지 않을 때 콜백에 화살표 함수 사용

  • 콜백이 this 또는 인수를 사용하지 않는 경우 화살표 함수를 사용하자.
1someArray.sort((a, b) => a - b);

3.7.3 다른 곳에서도 화살표 함수 고려

  • 콜백이 아니더라도 화살표 함수를 사용하는 것이 타당한지 고려하자.
  • 이것은 주로 스타일의 문제이다.

3.7.4 호출자가 이 값을 제어해야 할 때 화살표 함수를 사용하지 말라

  • addEventListener로 연결한 DOM 이벤트에 응답할 때
  • 호출될 때 this를 설정하도록 허용하는 것이 중요해서 객체 간에 공유되는 객체 메서드를 정의할 때(예를 들면 프로토타입에 있기 때문에)
  • 화살표 함수로 전환하는 것이 어떤 경우에는 유용하지만 때로는 오래된 전통적인 함수가 좋다.

3.7.5 기본값을 제공하는 코드 대신 기본 매개변수 값 사용

  • 가능하면 기본 매개변수 값을 대신 사용하자.
1function doSomething(delay = 300) {}

3.7.6 인수 키워드 대신 나머지 매개변수 사용

  • 나머지 매개변수를 사용하자