티스토리 뷰
클래스
ES6의 클래스가 기존의 프로토타입 기반 객체지향 모델을 폐지하고 새롭게 클래스 기반 객체지향 모델을 제공하는 것은 아니다. 사실 클래스는 함수이며 기존 프로토타입 기반 패턴을 클래스 기반 패턴처럼 사용할 수 있도록 하는 문법적 설탕이다. 아니 새로운 객체 생성 메커니즘이다.
클래스는 생성자 함수와 매우 유사하게 동작하지만 몇가지 차이가 있다.
1. 클래스를 new 연산자 없이 호출하면 에러가 발생한다.
2. 클래스는 상속을 지원하는 extends와 super 키워드를 제공한다. 하지만 생성자 함수는 extends와 super 키워드를 지원하지 않는다.
3. 클래스는 호이스팅이 발생하지 않는 것처럼 동작한다. 하지만 함수 선언문으로 정의된 생성자 함수는 함수 호이스팅이 함수 표현식으로 정의한 생성자 함수는 변수 호이스팅이 발생한다.
4. 클래스 내의 모든 코드에는 암묵적으로 strict mode가 지정되어 실행되며 strict mode를 해제할 수 없다. 하지만 생성자 함수는 암묵적으로 strict mode가 지정되지 않는다.
5. 클래스의 constructor, 프로토타입 메서드, 정적 메서드는 모두 프로퍼티 어트리뷰트 [[Enumerable]]의 값이 false다. 다시말해, 열거되지 않는다.
클래스는 let, const와 같이 호이스팅이 일어난다.
클래스는 생성자 함수이며 new 연산자와 함께 호출되어 인스턴스를 생성한다.
클래스 몸체에서 정의할 수 있는 메서드는 constructor, 프로토타입 메서드, 정적 메서드(static) 세 가지가 있다.
constructor는 인스턴스를 생성하고 초기화하기 위한 특수한 메서드다.
constructor 내부에서는 return을 사용하면 return 문에 명시한 객체가 반환되기 때문에 생략하자.
*클래스의 constructor 메서드와 프로토타입의 constructor 프로퍼티는 이름과 같이 혼동하기 쉽지만 직접적인 관련이 없다. 프로토타입의 constructor 프로퍼티는 모든 프로토타입이 가지고 있는 프로퍼티이며, 생성자 함수를 가리킨다.
클래스 몸체에서 정의한 메서드는 기본적으로 프로토타입 메서드가 된다.
메서드에 static 키워드를 붙이면 정적 메서드(클래스 메서드)가 된다.
정적 메서드와 프로토타입 메서드의 차이
1. 정적 메서드와 프로토타입 메서드는 자신이 속해 있는 프로토타입 체인이 다르다.
2. 정적 메서드는 클래스로 호출하고 프로토타입 메서드는 인스턴스로 호출한다.
3. 정적 메서드는 인스턴스 프로퍼티를 참조할 수 없지만 프로토타입 메서드는 인스턴스 프로퍼티를 참조할 수 있다.
클래스의 인스턴스 생성 과정
1. 인스턴스 생성과 this 바인딩
new 연산자와 함께 클래스를 호출하면 construcotr의 내부 코드가 실행되기에 앞서 암묵적으로 빈 객체가 생성된다. 이 객체, 즉 인스턴스는 this에 바인딩된다. 따라서 constructor 내부의 this는 클래스가 생성한 인스턴스를 가리킨다.
2. 인스턴스 초기화
constructor 내부 코드로 this에 바인딩되어 있는 인스턴스를 초기화한다.
3. 인스턴스 반환
클래스의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
class Person {
// 생성자
constructor(name) {
// 1. 암묵적으로 인스턴스가 생성되고 this에 바인딩된다.
console.log(this); // Person {}
console.log(Object.getPrototypeOf(this) === Person.prototype); // true
// 2. this에 바인딩되어 있는 인스턴스를 초기화한다.
this.name = name;
// 3. 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
}
}
프로퍼티
1. 인스턴스 프로퍼티는 constructor 내부에서 정의
2. 접근자 프로퍼티는 그대로 사용(set,get) 기본적으로 프로토타입 메서드가 된다.
3. 클래스 필드 정의 제안
클래스 몸체에서 클래스 필드를 정의하는 경우 this에 클래스 필드를 바인딩해서는 안된다. this는 클래스의 constructor와 메서드 내에서만 유효하다.
class Person {
// 클래스 필드에 문자열을 할당
name = 'Lee';
// 클래스 필드에 함수를 할당
getName = function () {
return this.name;
}
// 화살표 함수로 정의할 수도 있다.
// getName = () => this.name;
}
const me = new Person();
console.log(me); // Person {name: "Lee", getName: ƒ}
console.log(me.getName()); // Lee
4. private 필드 정의 제안
private 필드의 선두에는 #을 붙여준다. 참조할 때도 #을 붙여주어야 한다.
private 필드를 직접 constructor에 정의하면 에러가 발생한다.
class Person {
// private 필드 정의
#name = '';
constructor(name) {
// private 필드 참조
this.#name = name;
}
}
const me = new Person('Lee');
// private 필드 #name은 클래스 외부에서 참조할 수 없다.
console.log(me.#name);
// SyntaxError: Private field '#name' must be declared in an enclosing class
5. static 필드 정의 제안
class MyMath {
// static public 필드 정의
static PI = 22 / 7;
// static private 필드 정의
static #num = 10;
// static 메서드
static increment() {
return ++MyMath.#num;
}
}
console.log(MyMath.PI); // 3.142857142857143
console.log(MyMath.increment()); // 11
상속에 의한 클래스 확장
상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것이다.
extends 키워드를 사용하여 상속받을 클래스를 정의한다. 클래스도 프로토타입을 통해 상속 관계를 구현한다.
수퍼 클래스와 서브 클래스는 인스턴스의 프로토타입 체인뿐 아니라 클래스 간의 프로토타입 체인도 생성한다.
서브 클래스에 constructor를 생략하면 클래스에
constructor(...args){ super(...args); } 가 암묵적으로 정의된다.
super 키워드
super 키워드는 함수처럼 호출할 수도 있고 this와 같이 식별자처럼 참조할 수 있는 특수한 키워드다.
super를 호출하면 수퍼클래스의 constructor(super-constructor)를 호출한다.
super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.
// 수퍼클래스
class Base {
constructor(a, b) { // ④
this.a = a;
this.b = b;
}
}
// 서브클래스
class Derived extends Base {
constructor(a, b, c) { // ②
super(a, b); // ③
this.c = c;
}
}
const derived = new Derived(1, 2, 3); // ①
console.log(derived); // Derived {a: 1, b: 2, c: 3}
super를 호출할 때 주의할 사항
1. 서브클래스에서 constructor를 생략하지 않는 경우 서브클래스의 constructor에서는 반드시 super를 호출해야 한다.
2. 서브클래스의 construcotr에서 super를 호출하기 전에는 this를 참조할 수 없다.
3. super는 반드시 서브클래스의 constructor에서만 호출한다. 서브클래스가 아닌 클래스의 constructor나 함수에서 super를 호출하면 에러가 발생한다.
메서드 내에서 super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.
1. 서브클래스의 프로토타입 메서드 내에서 super.sayHi는 수퍼클래스의 프로토타입 메서드 sayHi를 가리킨다.
super 참조가 동작하기 위해서는 super를 참조하고 있는 메서드가 바인딩되어 있는 객체의 프로토타입을 찾을 수 있어야 한다. 이를 위해 메서드는 내부 슬롯 [[HomeObject]]를 가지며, 자신을 바인딩하고 있는 객체를 가리킨다.
주의할 것은 ES6의 메서드 축약 표현으로 정의된 함수만이 [[HomeObject]]를 갖는다.
==> 그냥 a: function(){} 이렇게 옛날방식 안쓰면 돼
super는 객체에서도 가능해. 클래스에서만 쓰는게아니야
const base = {
name: 'Lee',
sayHi() {
return `Hi! ${this.name}`;
}
};
const derived = {
__proto__: base,
// ES6 메서드 축약 표현으로 정의한 메서드다. 따라서 [[HomeObject]]를 갖는다.
sayHi() {
return `${super.sayHi()}. how are you doing?`;
}
};
console.log(derived.sayHi()); // Hi! Lee. how are you doing?
2. 서브클래스의 정적 메서드 내에서 super.sayHi는 수퍼클래스의 정적 메서드 sayHi를 가리킨다.
상속 클래스의 인스턴스 생성 과정
1 서브클래스의 super 호출
2. 수퍼클래스의 인스턴스 생성과 this 바인딩
3. 수퍼클래스의 인스턴스 초기화
4. 서브클래스 constructor로의 복귀와 this 바인딩
5. 서브클래스의 인스턴스 초기화
6. 인스턴스 반환
ES6 함수의 추가 기능
ES6이전의 모든 함수는 일반 함수로서 호출할 수 있는 것은 물론 생성자 함수로서 호출할 수 있었다. 즉 callable이면서 constructor이다. 사용 목적에 따라 명확한 구분이 없으므로 호출 방식에 특별한 제약이 없고 생성자 함수로 호출되지 않아도 프로토타입 객체를 생성한다. 이는 혼란스러우며 실수를 유발할 가능성이 있고 성능에도 좋지 않다.
이를 해결하기 위해 ES6에서는 함수를 사용 목적에 따라 세 가지 종류로 구분했다.
ES6 함수의 구분 | constructor | prototype | super | arguments |
일반 함수 | O | O | X | O |
메서드 | X | X | O | O |
화살표 함수 | X | X | X | X |
메서드
ES6사양에서 메서드는 메서드 축약 표현으로 정의된 함수만을 의미한다. ES6 메서드는 자신을 바인딩한 객체를 가리키는 내부 슬롯 [[HomeObject]]를 갖는다. super 참조는 내부 슬롯 [[HomeObject]]를 사용하여 수퍼클래스의 메서드를 참조한다.
ex
const base = {
name: 'Lee',
sayHi() {
return `Hi! ${this.name}`;
}
};
const derived = {
__proto__: base,
// sayHi는 ES6 메서드다. ES6 메서드는 [[HomeObject]]를 갖는다.
// sayHi의 [[HomeObject]]는 derived.prototype을 가리키고
// super는 sayHi의 [[HomeObject]]의 프로토타입인 base.prototype을 가리킨다.
sayHi() {
return `${super.sayHi()}. how are you doing?`;
}
};
console.log(derived.sayHi()); // Hi! Lee. how are you doing?
화살표 함수
콜백 함수 내부에서 this가 전역 객체를 가리키는 문제를 해결하기 위한 대안으로 유용
즉시실행함수 ex
const person = (name => ({
sayHi() { return `Hi? My name is ${name}.`; }
}))('Lee');
console.log(person.sayHi()); // Hi? My name is Lee.
화살표 함수와 일반 함수의 차이
1. 화살표 함수는 인스턴스를 생성할 수 없는 non-constructor이다.
2. 중복된 매개변수 이름을 선언할 수 없다.
3. 화살표 함수는 함수 자체의 this, arguments, super, new.target 바인딩을 갖지 않는다.
화살표 함수의 가장 큰 특징은 this 바인딩이다.
클래스나 생성자 함수에서 Array.prototype.map 메서드에 함수를 넣게 되면 콜백 함수의 내부에서 this는 undefined를 가리키게 된다. 이런경우에 const that=this; 라던가 map의 두번째 인수로 this 객체를 전달하거나, bind 메서드를 사용할 수 있었다. 지금은 화살표함수로 편하게 콜백함수내부의 this문제를 해결할 수 있다. lexical this
객체의 메서드(es6말고)를 화살표 함수로 정의하는 것은 피하자. this바인딩 문제. es6 메서드를 사용
프로토타입 객체의 프로퍼티에 화살표 함수를 할당하면 동일한 문제
일반 함수가 아닌 ES6 메서드를 동적 추가하고 싶다면 객체 ㅔ리터럴을 바인딩하고 프로토타입의 constructor 프로퍼티와 생성자 함수 간의 연결을 재설정한다.
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
클래스 필드 정의 제안을 사용하여 클래스 필드에 화살표 함수를 할당할 수도 있다.
// Bad
class Person {
// 클래스 필드 정의 제안
name = 'Lee';
sayHi = () => console.log(`Hi ${this.name}`);
}
const person = new Person();
person.sayHi(); // Hi Lee
이거는 이 뜻이므로 좋지 않다.
class Person {
constructor() {
this.name = 'Lee';
// 클래스가 생성한 인스턴스(this)의 sayHi 프로퍼티에 화살표 함수를 할당한다.
// sayHi 프로퍼티는 인스턴스 프로퍼티이다.
this.sayHi = () => console.log(`Hi ${this.name}`);
}
}
es6 축약 표현으로 정의한 es6 메서드를 사용하자
// Good
class Person {
// 클래스 필드 정의
name = 'Lee';
sayHi() { console.log(`Hi ${this.name}`); }
}
const person = new Person();
person.sayHi(); // Hi Lee
```
super
화살표 함수는 함수 자체의 super 바인딩을 갖지 않는다. 따라서 화살표 함수 내부에서 super를 참조하면 this와 마찬가지로 상위 스코프의 super를 참조한다.
arguments도 마찬가지. 화살표 함수로 가변 인자 함수를 구현해야 할 때는 반드시 Rest 피라미터를 사용해야 한다.
rest 파라미터
기본 문법(rest 파라미터는 반드시 마지막 파라미터, 단 하나만 선언, 함수 객체의 length 프로퍼티 영향x)
rest 파라미터(나머지 매개변수)는 매개변수 이름 앞에 세개의 점 ...을 붙여서 정의한 매개변수를 의미한다. rest 파라미터는 함수에 전달된 인수들의 목록을 배열로 전달 받는다.
function foo(...rest) {
// 매개변수 rest는 인수들의 목록을 배열로 전달받는 Rest 파라미터다.
console.log(rest); // [ 1, 2, 3, 4, 5 ]
}
foo(1, 2, 3, 4, 5);
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);
arguments 객체는 배열이 아닌 유사 배열 객체이므로 배열 메서드를 사용하려면 Function.prototype.call, apply 메서드를 사용해 arguments 객체를 배열로 변환해야 한다.
but rest 피라미터는 가변 인자 함수의 인수 목록을 배열로 직접 전달받을 수 있다.
함수를 호출할 때 매개변수의 개수만큼 인수를 전달하는 것이 바람직하지만 그렇지 않아도 에러가 발생하지 않는다. 그래서 function sum(x=0,y=0) 처럼 기본 값을 사용할 수 있다.(es6이전에는 x=x||0 방법)
rest 피라미터에는 기본값 지정 x, length, arguments에도 영향x
'책 > 모던 자바스크립트 Deep Dive' 카테고리의 다른 글
모던자바스크립트 8(28장~33장) (0) | 2021.05.11 |
---|---|
모던자바스크립트 7(27장) 배열 (0) | 2021.05.11 |
모던자바스크립트 5(23장~24장)(실행 컨텍스트, 클로저) (0) | 2021.04.21 |
모던자바스크립트 4(20장~22장)(strict, 빌트인, this) (0) | 2021.04.17 |
모던자바스크립트 3(19장) 프로토타입 (0) | 2021.04.13 |