[모던 자바스크립트 Deep Dive]26장 ES6 함수의 추가 기능
26.1 함수의 구분
ES6 이전
ES6 이전의 함수 호출 방식
1 2 3 4 5 6 7 8 9 10 11 12 13
var foo = function () { return 1; }; // 일반적인 함수로서 호출 foo(); // -> 1 // 생성자 함수로서 호출 new foo(); // -> foo {} // 메서드로서 호출 var obj = { foo: foo }; obj.foo(); // -> 1
- 일반 함수로서 호출
new
연산자와 함께 생성자 함수로서 호출- 객체에 바인딩되어 메서드로서 호출
ES6 이전의 모든 함수는 callable이며 contructor이다. 즉 모든 함수를 일반 함수로서도, 생성자 함수로서도 호출할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// 프로퍼티 f에 바인딩된 함수는 callable이며 constructor다. var obj = { x: 10, f: function () { return this.x; } }; // 프로퍼티 f에 바인딩된 함수를 메서드로서 호출 console.log(obj.f()); // 10 // 프로퍼티 f에 바인딩된 함수를 일반 함수로서 호출 var bar = obj.f; console.log(bar()); // undefined // 프로퍼티 f에 바인딩된 함수를 생성자 함수로서 호출 console.log(new obj.f()); // f {}
객체의 바인딩된 함수, 콜백 함수도 contructor이기 때문에 호출 시 불필요한 프로토타입 객체를 생성한다.
1 2 3 4 5
// 콜백 함수를 사용하는 고차 함수 map. // 콜백 함수도 constructor이며 프로토타입을 생성한다. [1, 2, 3].map(function (item) { return item * 2; }); // -> [ 2, 4, 6 ]
ES6 이후
구분 | contructor | prototype | super | arguments |
---|---|---|---|---|
일반 함수 | O | O | X | O |
메서드 | X | X | O | O |
화살표 함수 | X | X | X | X |
- 함수를 사용 목적에 따라 명확히 구분한다.
- 일반 함수만 ES6 이전과 차이가 없다.
26.2 메서드
- 메서드: 메서드 축약 표현으로 정의된 함수
- ES6에서 명확하게 정의가 규정됨
1 2 3 4 5 6 7 8 9 10
const obj = { x: 1, // foo는 메서드이다. foo() { return this.x; }, // bar에 바인딩된 함수는 메서드가 아닌 일반 함수이다. bar: function() { return this.x; } }; console.log(obj.foo()); // 1 console.log(obj.bar()); // 1
ES6 메서드는 non-contructor ⇒ 생성자 함수로서 호출 불가
1 2
new obj.foo(); // -> TypeError: obj.foo is not a constructor new obj.bar(); // -> bar {}
ES6 메서드는 인스턴스 생성 불가 ⇒
prototype
프로퍼티가 없고 프로토타입도 생성 안 함1 2 3 4 5
// obj.foo는 constructor가 아닌 ES6 메서드이므로 prototype 프로퍼티가 없다. obj.foo.hasOwnProperty('prototype'); // -> false // obj.bar는 constructor인 일반 함수이므로 prototype 프로퍼티가 있다. obj.bar.hasOwnProperty('prototype'); // -> true
표준 빌트인 객체가 제공하는 프로토타입 메서드, 정적 메서드도 모두 non-contructor
1 2 3 4 5 6 7 8
String.prototype.toUpperCase.prototype; // -> undefined String.fromCharCode.prototype // -> undefined Number.prototype.toFixed.prototype; // -> undefined Number.isFinite.prototype; // -> undefined Array.prototype.map.prototype; // -> undefined Array.from.prototype; // -> undefined
- ES6 메서드는 내부 슬롯
[[HomeObject]]
를 가짐 ⇒super
참조 사용 가능[[HomeObject]]
: 자신을 바인딩한 객체를 가리키며,super
참조 시 수퍼클래스 메서드 참조를 위해 사용
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
const base = { name: 'Lee', sayHi() { return `Hi! ${this.name}`; } }; const derived = { __proto__: base, // sayHi는 ES6 메서드다. ES6 메서드는 [[HomeObject]]를 갖는다. // sayHi의 [[HomeObject]]는 sayHi가 바인딩된 객체인 derived를 가리키고 // super는 sayHi의 [[HomeObject]]의 프로토타입인 base를 가리킨다. sayHi() { return `${super.sayHi()}. how are you doing?`; } }; console.log(derived.sayHi()); // Hi! Lee. how are you doing?
- ES6 메서드가 아닌 함수는
super
키워드 사용 불가
1 2 3 4 5 6 7 8 9
const derived = { __proto__: base, // sayHi는 ES6 메서드가 아니다. // 따라서 sayHi는 [[HomeObject]]를 갖지 않으므로 super 키워드를 사용할 수 없다. sayHi: function () { // SyntaxError: 'super' keyword unexpected here return `${super.sayHi()}. how are you doing?`; } };
26.3 화살표 함수
26.3.1 화살표 함수 정의
함수 정의
1
2
const multiply = (x, y) => x * y;
multiply(2, 3); // -> 6
- 함수 표현식으로 정의(함수 선언문 불가)
매개변수 선언
1
2
3
4
5
const arrow = (x, y) => { ... };
const arrow = x => { ... };
const arrow = () => { ... };
- 소괄호 안에 매개변수 선언
- 매개변수가 한 개인 경우 소괄호 생략 가능
- 매개변수가 없는 경우 소괄호 생략 불가
함수 몸체 정의
- 함수 몸체 내부가 표현식인 하나의 문이라면 중괄호 생략 가능
- 함수 몸체 내부의 문이 값으로 평가될 수 있는 표현식인 문이라면 암묵적으로 반환
1 2 3 4 5 6 7
// concise body const power = x => x ** 2; power(2); // -> 4 // 위 표현은 다음과 동일하다. // block body const power = x => { return x ** 2; };
- 함수 몸체 내부가 하나의 문이라도 표현식이 아니면 중괄호 생략 불가
- 함수 몸체 내부의 문이 표현식이 아닌 문이라면 반환이 불가해 에러 발생
1 2 3 4
const arrow = () => const x = 1; // SyntaxError: Unexpected token 'const' // 위 표현은 다음과 같이 해석된다. const arrow = () => { return const x = 1; };
객체 리터럴을 반환하는 경우 객체 리터럴을 소괄호로 감싸야 한다.
1 2 3 4 5
const create = (id, content) => ({ id, content }); create(1, 'JavaScript'); // -> {id: 1, content: "JavaScript"} // 위 표현은 다음과 동일하다. const create = (id, content) => { return { id, content }; };
감싸지 않으면 객체 리터럴 중괄호를 함수 몸체의 중괄호로 해석
1 2 3
// { id, content }를 함수 몸체 내의 쉼표 연산자문으로 해석한다. const create = (id, content) => { id, content }; create(1, 'JavaScript'); // -> undefined
함수 몸체가 여러 문으로 구성된 경우 중괄호 생략 불가
1 2 3 4
const sum = (a, b) => { const result = a + b; return result; };
즉시 실행 함수로 사용 가능
1 2 3 4 5
const person = (name => ({ sayHi() { return `Hi? My name is ${name}.`; } }))('Lee'); console.log(person.sayHi()); // Hi? My name is Lee.
- 일급 객체이므로 고차 함수에 인수로 전달 가능
- 가독성도 좋다.
1 2 3 4 5 6 7
// ES5 [1, 2, 3].map(function (v) { return v * 2; }); // ES6 [1, 2, 3].map(v => v * 2); // -> [ 2, 4, 6 ]
26.3.2 화살표 함수와 일반 함수의 차이
- 화살표 함수는 인스턴스를 생성할 수 없는 non-contructor
prototype
프로퍼티도 없고 프로토타입도 생성 불가
1 2 3 4 5 6 7
const Foo = () => {}; // 화살표 함수는 생성자 함수로서 호출할 수 없다. new Foo(); // TypeError: Foo is not a constructor // 화살표 함수는 prototype 프로퍼티가 없다. Foo.hasOwnProperty('prototype'); // -> false
중복된 매개변수 이름 선언 불가
1 2
const arrow = (a, a) => a + a; // SyntaxError: Duplicate parameter name not allowed in this context
일반 함수는 strict mode가 아니라면 중복된 매개변수 이름 선언이 가능하다.
1 2
function normal(a, a) { return a + a; } // 일반 함수 console.log(normal(1, 2)); // 4
1 2 3 4
'use strict'; function normal(a, a) { return a + a; } // strict mode에서 일반 함수 // SyntaxError: Duplicate parameter name not allowed in this context
- 화살표 함수 자체의
this
,super
,new.target
,arguments
바인딩을 갖지 않음- 화살표 함수 내부에서
this
,super
,new.target
,arguments
참조 시 스코프 체인을 통해 상위 스코프의this
,super
,new.target
,arguments
을 참조한다.
- 화살표 함수 내부에서
26.3.3 this
콜백 함수 내부에 일반 함수를 사용할 경우 다음과 같은 “콜백 함수 내부의 this
문제”가 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Prefixer {
constructor(prefix) {
this.prefix = prefix;
}
add(arr) {
// add 메서드는 인수로 전달된 배열 arr을 순회하며 배열의 모든 요소에 prefix를 추가한다.
return arr.map(function (item) {
return this.prefix + item;
// -> TypeError: Cannot read property 'prefix' of undefined
});
}
}
const prefixer = new Prefixer('-webkit-');
console.log(prefixer.add(['transition', 'user-select']));
add
메서드에서this
는 메서드를 호출한prefixer
객체를 가리킨다.Array.prototype.map
의 인수로 전달된 콜백 함수에서this
는undefined
를 가리킨다.- 클래스 내부에는 암묵적으로 strict mode가 적용된다.
- strict mode에서 일반 함수로 호출된 함수 내부의
this
에는 전역 객체가 아니라undefined
가 바인딩된다.
- 에러 발생
콜백 함수 내부의 this
문제 해결 방법
this
를 회피시킨다.1 2 3 4 5 6 7 8
add(arr) { // this를 일단 회피시킨다. const that = this; return arr.map(function (item) { // this 대신 that을 참조한다. return that.prefix + ' ' + item; }); }
Array.prototype.map
은 이러한 문제를 해결하기 위해 두 번째 인수로this
에 사용할 객체를 받을 수 있다.1 2 3 4 5
add(arr) { return arr.map(function (item) { return this.prefix + ' ' + item; }, this); // this에 바인딩된 값이 콜백 함수 내부의 this에 바인딩된다. }
Function.prototype.bind
메서드를 사용한다.1 2 3 4 5
add(arr) { return arr.map(function (item) { return this.prefix + ' ' + item; }.bind(this)); // this에 바인딩된 값이 콜백 함수 내부의 this에 바인딩된다. }
ES6 이후에는, 화살표 함수를 사용한다.
1 2 3 4 5 6 7 8 9 10 11 12 13
class Prefixer { constructor(prefix) { this.prefix = prefix; } add(arr) { return arr.map(item => this.prefix + item); } } const prefixer = new Prefixer('-webkit-'); console.log(prefixer.add(['transition', 'user-select'])); // ['-webkit-transition', '-webkit-user-select']
화살표 함수의 this
특징
- 화살표 함수는 함수 자체의
this
바인딩을 갖지 않음 ⇒ 상위 스코프의this
를 참조- 이처럼 함수가 정의된 위치에 따라
this
가 결정되는 것을 lexical this라 한다.
1 2 3 4 5
// 화살표 함수는 상위 스코프의 this를 참조한다. () => this.x; // 익명 함수에 상위 스코프의 this를 주입한다. 위 화살표 함수와 동일하게 동작한다. (function () { return this.x; }).bind(this);
- 이처럼 함수가 정의된 위치에 따라
여러 화살표 함수가 중첩된 경우 가장 가까운 상위 함수 중 화살표 함수가 아닌 함수의
this
참조1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 중첩 함수 foo의 상위 스코프는 즉시 실행 함수다. // 따라서 화살표 함수 foo의 this는 상위 스코프인 즉시 실행 함수의 this를 가리킨다. (function () { const foo = () => console.log(this); foo(); }).call({ a: 1 }); // { a: 1 } // bar 함수는 화살표 함수를 반환한다. // bar 함수가 반환한 화살표 함수의 상위 스코프는 화살표 함수 bar다. // 하지만 화살표 함수는 함수 자체의 this 바인딩을 갖지 않으므로 bar 함수가 반환한 // 화살표 함수 내부에서 참조하는 this는 화살표 함수가 아닌 // 즉시 실행 함수의 this를 가리킨다. (function () { const bar = () => () => console.log(this); bar()(); }).call({ a: 1 }); // { a: 1 }
화살표 함수가 전역 함수라면
this
는 전역 객체를 가리킴1 2 3 4
// 전역 함수 foo의 상위 스코프는 전역이므로 // 화살표 함수 foo의 this는 전역 객체를 가리킨다. const foo = () => console.log(this); foo(); // window
프로퍼티에 할당한 화살표 함수도 같은 규칙을 따음
1 2 3 4 5 6 7 8
// increase 프로퍼티에 할당한 화살표 함수의 상위 스코프는 전역이다. // 따라서 increase 프로퍼티에 할당한 화살표 함수의 this는 전역 객체를 가리킨다. const counter = { num: 1, increase: () => ++this.num }; console.log(counter.increase()); // NaN
자체의
this
바인딩이 없기 때문에Function.prototype.apply/call/bind
메서드를 사용해도this
교체 불가1 2 3 4 5 6 7
window.x = 1; const normal = function () { return this.x; }; const arrow = () => this.x; console.log(normal.call({ x: 10 })); // 10 console.log(arrow.call({ x: 10 })); // 1
호출 자체는 가능하지만
this
에 영향 없음1 2 3 4 5
const add = (a, b) => a + b; console.log(add.call(null, 1, 2)); // 3 console.log(add.apply(null, [1, 2])); // 3 console.log(add.bind(null, 1, 2)()); // 3
화살표 함수 주의 사항
- 메서드에 화살표 함수 사용은 권장하지 않음
- 메서드에서
this
는 메서드를 호출한 객체를 가리켜야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// Bad const person = { name: 'Lee', sayHi: () => console.log(`Hi ${this.name}`) }; // sayHi 프로퍼티에 할당된 화살표 함수 내부의 this는 // 상위 스코프인 전역의 this가 가리키는 전역 객체를 가리키므로 // 이 예제를 브라우저에서 실행하면 this.name은 빈 문자열을 갖는 // window.name과 같다. 전역 객체 window에는 빌트인 프로퍼티 name이 존재한다. person.sayHi(); // Hi // Good const person = { name: 'Lee', sayHi() { console.log(`Hi ${this.name}`); } }; person.sayHi(); // Hi Lee
- 메서드에서
프로토타입 객체에 화살표 함수 사용은 권장하지 않음
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// Bad function Person(name) { this.name = name; } Person.prototype.sayHi = () => console.log(`Hi ${this.name}`); const person = new Person('Lee'); // 이 예제를 브라우저에서 실행하면 this.name은 빈 문자열을 갖는 window.name과 같다. person.sayHi(); // Hi // Good function Person(name) { this.name = name; } Person.prototype.sayHi = function () { console.log(`Hi ${this.name}`); }; const person = new Person('Lee'); person.sayHi(); // Hi Lee
일반 함수가 아닌 ES6 메서드를 동적 추가하기 위해선 객체 리터럴 바인딩 및 프로토타입의
contructor
프로퍼티, 생성자 함수 간 연결 필요1 2 3 4 5 6 7 8 9 10 11 12
function Person(name) { this.name = name; } Person.prototype = { // constructor 프로퍼티와 생성자 함수 간의 연결을 재설정 constructor: Person, sayHi() { console.log(`Hi ${this.name}`); } }; const person = new Person('Lee'); person.sayHi(); // Hi Lee
화살표 함수와 클래스 필드
클래스 필드 정의 제안을 사용해 클래스 필드에 할당 가능
1 2 3 4 5 6 7 8 9
// Bad class Person { // 클래스 필드 정의 제안 name = 'Lee'; sayHi = () => console.log(`Hi ${this.name}`); } const person = new Person(); person.sayHi(); // Hi Lee
- 클래스 필드에 할당한 화살표 함수의
this
를 참조하면,constructor
내부의this
바인딩을 참조constructor
내부의this
바인딩은 클래스가 생성할 인스턴스를 가리킴- 화살표 함수의
this
도 클래스가 생성할 인스턴스를 가리킴
1 2 3 4 5 6 7 8
class Person { constructor() { this.name = 'Lee'; // 클래스가 생성한 인스턴스(this)의 sayHi 프로퍼티에 화살표 함수를 할당한다. // sayHi 프로퍼티는 인스턴스 프로퍼티이다. this.sayHi = () => console.log(`Hi ${this.name}`); } }
클래스 필드에 할당한 화살표 함수는 프로토타입 메서드가 아닌 인스턴스 메서드가 되므로 ES6 메서드 사용을 권장
1 2 3 4 5 6 7 8 9
// Good class Person { // 클래스 필드 정의 name = 'Lee'; sayHi() { console.log(`Hi ${this.name}`); } } const person = new Person(); person.sayHi(); // Hi Lee
26.3.4 super
- 화살표 함수는 함수 자체의
super
바인딩을 갖지 않음 ⇒ 상위 스코프의super
를 참조 클래스 필드에 할당한 화살표 함수가
super
를 참조하면, ⇒constructor
내부의super
바인딩을 참조1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
class Base { constructor(name) { this.name = name; } sayHi() { return `Hi! ${this.name}`; } } class Derived extends Base { // 화살표 함수의 super는 상위 스코프인 constructor의 super를 가리킨다. sayHi = () => `${super.sayHi()} how are you doing?`; } const derived = new Derived('Lee'); console.log(derived.sayHi()); // Hi! Lee how are you doing?
26.3.5 arguments
화살표 함수는 함수 자체의
arguments
바인딩을 갖지 않음 ⇒ 상위 스코프의arguments
를 참조1 2 3 4 5 6 7 8 9 10 11
(function () { // 화살표 함수 foo의 arguments는 상위 스코프인 즉시 실행 함수의 arguments를 가리킴 const foo = () => console.log(arguments); // [Arguments] { '0': 1, '1': 2 } foo(3, 4); }(1, 2)); // 화살표 함수 foo의 arguments는 상위 스코프인 전역의 arguments를 가리킨다. // 하지만 전역에는 arguments 객체가 존재하지 않는다. // arguments 객체는 함수 내부에서만 유효하다. const foo = () => console.log(arguments); foo(1, 2); // ReferenceError: arguments is not defined
가변 인자 함수를 구현할 때 화살표 함수는 좋지 못함 ⇒ Rest 파라미터 사용 권장
26.4 Rest 파라미터
- Rest 파라미터: 이름 앞에 세 개의 점을 붙여 정의한 매개변수
26.4.1 기본 문법
함수에 전달된 인수의 목록을 배열로 전달받음
1 2 3 4 5 6
function foo(...rest) { // 매개변수 rest는 인수들의 목록을 배열로 전달받는 Rest 파라미터다. console.log(rest); // [ 1, 2, 3, 4, 5 ] } foo(1, 2, 3, 4, 5);
일반 매개변수와 함께 사용 가능
1 2 3 4 5 6 7 8 9 10 11 12 13 14
function foo(param, ...rest) { console.log(param); // 1 console.log(rest); // [ 2, 3, 4, 5 ] } foo(1, 2, 3, 4, 5); function bar(param1, param2, ...rest) { console.log(param1); // 1 console.log(param2); // 2 console.log(rest); // [ 3, 4, 5 ] } bar(1, 2, 3, 4, 5);
Rest 파라미터는 매개변수에 할당되고 남은 나머지 인수로 구성 ⇒ Rest 파라미터는 반드시 마지막 파라미터여야 한다.
1 2 3 4
function foo(...rest, param1, param2) { } foo(1, 2, 3, 4, 5); // SyntaxError: Rest parameter must be last formal parameter
단 하나만 선언 가능
1 2 3 4
function foo(...rest1, ...rest2) { } foo(1, 2, 3, 4, 5); // SyntaxError: Rest parameter must be last formal parameter
함수 객체의
length
프로퍼티에 영향을 주지 않음1 2 3 4 5 6 7 8
function foo(...rest) {} console.log(foo.length); // 0 function bar(x, ...rest) {} console.log(bar.length); // 1 function baz(x, y, ...rest) {} console.log(baz.length); // 2
26.4.2 Rest 파라미터와 arguments
객체
ES5, arguments
객체
가변 인자 함수는
arguments
객체를 통해 인수를 전달 받음1 2 3 4 5 6 7
// 매개변수의 개수를 사전에 알 수 없는 가변 인자 함수 function sum() { // 가변 인자 함수는 arguments 객체를 통해 인수를 전달받는다. console.log(arguments); } sum(1, 2); // {length: 2, '0': 1, '1': 2}
arguments
객체는 유사 배열 객체이기 때문에 배열로 변환해야 배열 메서드 사용 가능1 2 3 4 5 6 7 8 9 10
function sum() { // 유사 배열 객체인 arguments 객체를 배열로 변환한다. var array = Array.prototype.slice.call(arguments); return array.reduce(function (pre, cur) { return pre + cur; }, 0); } console.log(sum(1, 2, 3, 4, 5)); // 15
ES6, Rest 파라미터
인수 목록을 배열로 직접 전달 가능
1 2 3 4 5
function sum(...args) { // Rest 파라미터 args에는 배열 [1, 2, 3, 4, 5]가 할당된다. return args.reduce((pre, cur) => pre + cur, 0); } console.log(sum(1, 2, 3, 4, 5)); // 15
함수와 ES6 메서드는 Rest 파라미터와
arguments
객체 모두 사용할 수 있지만, 화살표 함수는 Rest 파라미터만 사용 가능
26.5 매개변수 기본값
- 인수가 전달되지 않은 매개변수의 값 ⇒
undefined
- 따라서 인수를 확인하고 기본값을 설정하는 과정 필요
1 2 3 4 5 6 7 8 9 10
function sum(x, y) { // 인수가 전달되지 않아 매개변수의 값이 undefined인 경우 기본값을 할당한다. x = x || 0; y = y || 0; return x + y; } console.log(sum(1, 2)); // 3 console.log(sum(1)); // 1
ES6에서 매개변수 기본값 도입 ⇒ 간편하게 인수 체크 및 초기화 가능
1 2 3 4 5 6
function sum(x = 0, y = 0) { return x + y; } console.log(sum(1, 2)); // 3 console.log(sum(1)); // 1
매개변수에 인수를 전달하지 않은 경우,
undefined
를 전달한 경우에만 기본값 유효1 2 3 4 5 6 7
function logName(name = 'Lee') { console.log(name); } logName(); // Lee logName(undefined); // Lee logName(null); // null
Rest 파라미터에는 기본값 지정 불가
1 2 3 4
function foo(...rest = []) { console.log(rest); } // SyntaxError: Rest parameter may not have a default initializer
기본값은
length
프로퍼티와arguments
객체에 영향을 주지 않음1 2 3 4 5 6 7 8
function sum(x, y = 0) { console.log(arguments); } console.log(sum.length); // 1 sum(1); // Arguments { '0': 1 } sum(1, 2); // Arguments { '0': 1, '1': 2 }