Deepdive 공부 정리용 글입니다.
JS는 프로토타입 기반 객체지향 언어
프로토타입 기반 객체지향 언어는 클래스가 필요 없는 객체지향 언어!
ES5의 함수와 프로토타입을 통한 객체지향 언어의 상속 구현
// ES5 생성자 함수
var Person = (function () {
// 생성자 함수
function Person(name) {
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
console.log('Hi! My name is ' + this.name);
};
// 생성자 함수 반환
return Person;
})();
// 인스턴스 생성
var me = new Person('Lee');
me.sayHi(); // Hi! My name is Lee
console.log(me);
// Person {name: 'Lee'}
클래스와 생성자함수의 차이점
25.2_ 클래스 정의
class Person {}
const Person = class{};
const Person = class Myclass{};
즉 클래스는 표현식으로 정의 가능하므로 일급 객체라는 의미.
클래스는 컨스트럭터, 프로토타입 메서드, 정적 메서드 정의 가능.
생성자 함수와 클래스의 비교
//생성자 함수
var Person = (function(){
function Person(name){
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHi = function(){
console.log('Hi, My name is' + this.name);
}
// 정적 메서드
Person.sayHello = function () {
console.log('Hello!');
}
//생성자 함수 반환
return Person;
}());
------------------
// 클래스 선언문
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name; // name 프로퍼티는 public하다.
}
// 프로토타입 메서드
sayHi() {
console.log(`Hi! My name is ${this.name}`);
}
// 정적 메서드
static sayHello() {
console.log('Hello!');
}
}
// 인스턴스 생성
const me = new Person('Lee');
// 인스턴스의 프로퍼티 참조
console.log(me.name); // Lee
// 프로토타입 메서드 호출
me.sayHi(); // Hi! My name is Lee
// 정적 메서드 호출
Person.sayHello(); // Hello!
class Article {
constructor(title, date) {
this.title = title;
this.date = date;
}
static createTodays() {
// this는 Article입니다.
return new this("Today's digest", new Date());
}
}
let article = Article.createTodays();
alert( article.title ); // Today's digest
const Person = '';
{
// 호이스팅이 발생하지 않는다면 ''이 출력되어야 한다.
console.log(Person);
// ReferenceError: Cannot access 'Person' before initialization
// 클래스 선언문
class Person {}
}
class Person {}
const me = new Person{}
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
}
// 클래스는 함수다.
console.log(typeof Person); // function
console.dir(Person);
// 인스턴스 생성
const me = new Person('Lee');
console.log(me);//Person {name: 'Lee'}
// 클래스
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
}
// 생성자 함수
function Person(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
프로토타입의 constructor와 class의 constructor차이
관련 없음.
프로토타입의 컨스트럭터는 모든 프로토타입이 가지고 있는 프로퍼티로 생성자 함수를 가리킴.
class Person {
constructor(name, address) {
// 인수로 인스턴스 초기화
this.name = name;
this.address = address;
}
}
// 인수로 초기값을 전달한다. 초기값은 constructor에 전달된다.
const me = new Person('Lee', 'Seoul');
console.log(me); // Person {name: "Lee", address: "Seoul"}
// 생성자 함수
function Person(name) {
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
console.log(`Hi! My name is ${this.name}`);
};
// 프로토타입 지정 안해도 기본적으로 프로토타입 메서드가 됨.
sayBye = function(){
console.log(`Bye, ${this.name}`);
const me = new Person('Lee');
me.sayHi(); // Hi! My name is Lee
// me 객체의 프로토타입은 Person.prototype이다.
Object.getPrototypeOf(me) === Person.prototype; // -> true
me instanceof Person; // -> true
// Person.prototype의 프로토타입은 Object.prototype이다.
Object.getPrototypeOf(Person.prototype) === Object.prototype; // -> true
me instanceof Object; // -> true
// me 객체의 constructor는 Person 클래스다.
me.constructor === Person; // -> true
정적 메서드는 인스턴스를 생성하지 않아도 호출 가능한 메서드
생성자 함수의 정적 메서드 생성 방법
// 생성자 함수
function Person(name){
this.name = name;
}
// 정적 메서드
Person.sayHi = function () {
console.log('Hi!');
};
Person.sayHi(); //Hi!
class Person {
//생성자
constructor(name) {
//인스턴스 생성 및 초기화
this.name = name;
}
static sayHi() {
console.log('Hi');
}
}
// 정적 메서드는 클래스로 호출한다.
// 정적 메서드는 인스턴스 없이도 호출할 수 있다.
Person.sayHi(); // Hi!
// 인스턴스 생성
const me = new Person('Lee');
me.sayHi(); // TypeError: me.sayHi is not a function
- 정적 메서드와 프로토타입 메서드는 속해있는 프로토타입 체인이 다름
- 정적 메서드는 클래스로 호출, 프로토타입 메서드는 인스턴스로 호출
- 정적 메서드는 인스턴스 프로퍼티를 참조 불가 / 프로토타입 메서드는 인스턴스 프로퍼티를 참조 가능
- 사용 예)
// 표준 빌트인 객체의 정적 메서드 Math.max(1, 2, 3); // -> 3 Number.isNaN(NaN); // -> true JSON.stringify({ a: 1 }); // -> "{"a":1}" Object.is({}, {}); // -> false Reflect.has({ a: 1 }, 'a'); // -> true
클래스에서 정의한 메서드의 특징
1. function 키워드를 생략한 메서드 축약 표현 사용
2. 객체 리터럴과 다르게 클래스에 메서드 정의 시 콤마 불필요
3. 암묵적으로 strict mode로 실행
4. for...in 문이나 Objcet.keys 메서드 등으로 열거 불가
5. 내부 메서드 [[Construct]]를 갖지 않는 non-constructor.
(즉, new 연산자와 함께 호출 불가)
class Person {
constructor(name) {
// 인스턴스 프로퍼티
this.name = name;
}
}
const me = new Person('Lee');
console.log(me); // Person {name: "Lee"}
constructor 내부 코드 실행되기 이전에 constructor 내부 this에는 이미 클래스가 암묵적으로 생성한 인스턴스인 빈 객체가 바인딩 됨
-> 이로써 클래스가 암묵적 생성한 빈 객체(인스턴스)에 프로퍼티가 추가되어 인스턴스 초기화
접근자 프로퍼티는 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티
const person = {
// 데이터 프로퍼티
firstName: 'Ungmo',
lastName: 'Lee',
// fullName은 접근자 함수로 구성된 접근자 프로퍼티다.
// getter 함수
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
// setter 함수
set fullName(name) {
// 배열 디스트럭처링 할당: "36.1. 배열 디스트럭처링 할당" 참고
[this.firstName, this.lastName] = name.split(' ');
}
};
// 데이터 프로퍼티를 통한 프로퍼티 값의 참조.
console.log(`${person.firstName} ${person.lastName}`); // Ungmo Lee
// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
// 접근자 프로퍼티 fullName에 값을 저장하면 setter 함수가 호출된다.
person.fullName = 'Heegun Lee';
console.log(person); // {firstName: "Heegun", lastName: "Lee"}
// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
// 접근자 프로퍼티 fullName에 접근하면 getter 함수가 호출된다.
console.log(person.fullName); // Heegun Lee
// fullName은 접근자 프로퍼티다.
// 접근자 프로퍼티는 get, set, enumerable, configurable 프로퍼티 어트리뷰트를 갖는다.
console.log(Object.getOwnPropertyDescriptor(person, 'fullName'));
// {get: ƒ, set: ƒ, enumerable: true, configurable: true}
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// fullName은 접근자 함수로 구성된 접근자 프로퍼티다.
// getter 함수
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
// setter 함수
set fullName(name) {
[this.firstName, this.lastName] = name.split(' ');
}
}
const me = new Person('Ungmo', 'Lee');
// 데이터 프로퍼티를 통한 프로퍼티 값의 참조.
console.log(`${me.firstName} ${me.lastName}`); // Ungmo Lee
// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
// 접근자 프로퍼티 fullName에 값을 저장하면 setter 함수가 호출된다.
me.fullName = 'Heegun Lee';
console.log(me); // {firstName: "Heegun", lastName: "Lee"}
// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
// 접근자 프로퍼티 fullName에 접근하면 getter 함수가 호출된다.
console.log(me.fullName); // Heegun Lee
// fullName은 접근자 프로퍼티다.
// 접근자 프로퍼티는 get, set, enumerable, configurable 프로퍼티 어트리뷰트를 갖는다.
console.log(Object.getOwnPropertyDescriptor(Person.prototype, 'fullName'));
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}
클래스 필드란 클래스 기반 객체지향 언어에서 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 용어.
JS도 클래스 기반처럼 작동하도록 바뀜
class Person{
//클래스 필드 정의
name = 'Lee';
// this에 클래스 필드 바인딩 X
this.age = 12; (X)
//참조하는 경우에는 this 필요
constructor() {
console.log(name); //refer error
}
gender;// 할당안하면 undefined
}
const me = new Person('Lee');
// 버전 12 이후 node.js에서는 정상 작동.
class Person {
//자동으로 추가되기 때문에 클래스 필드 정의할 필요 없다.
constructor(name) {
this.name = name;
}
}
const me = new Person('Lee');
console.log(me); // Person {name: "Lee"}
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
위처럼 클래스 필드에 함수를 할당하면 이 함수는 프로토타입 메서드가 아닌 인스턴스 메서드가 됨.
(모든 클래스 필드는 인스턴스 프로퍼티가 되므로 클래스필드에 함수 할당은 권장 X)
인스턴스 프로퍼티는 언제나 외부 참조 가능한 public
TC39 프로세스 stage3에서 제안 : private 필드 정의는 선두에 '#'
Typescript는 private, public, protected 모두 지원
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
접근 가능성
접근 여부-> 클래스내부/자식클래스내부/클래스인스턴스통한접근
public / O / O / O
private / O / X / X
private 필드 정의하는 방법
class Person {
// private 필드 정의
// 반드시 클래스 몸체에 정의!
#name = '';
// 만약 몸체에 정의 없이 아래의 constructor로만 정의하면 에러가 발생할 것이다.
constructor(name) {
this.#name = name;
}
// name은 접근자 프로퍼티다.
get name() {
// private 필드를 참조하여 trim한 다음 반환한다.
return this.#name.trim();
}
}
const me = new Person(' Lee ');
console.log(me.name); // Lee
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)
p449 animal 예시 참고!
class Animal {
constructor(age, weight) {
this.age = age;
this.weight = weight;
}
eat() { return 'eat'; }
move() { return 'move'; }
}
// 상속을 통해 Animal 클래스를 확장한 Bird 클래스
class Bird extends Animal {
fly() { return 'fly'; }
}
const bird = new Bird(1, 5);
console.log(bird); // Bird {age: 1, weight: 5}
console.log(bird instanceof Bird); // true
console.log(bird instanceof Animal); // true
console.log(bird.eat()); // eat
console.log(bird.move()); // move
console.log(bird.fly()); // fly
class Base {}
class Derived extends Base{}
// 생성자 함수
function Base(a) {
this.a = a;
}
// 생성자 함수를 상속받는 서브클래스
class Derived extends Base {}
const derived = new Derived(1);
console.log(derived); // Derived {a: 1}
function Base1() {}
class Base2 {}
let condition = true;
// 조건에 따라 동적으로 상속 대상을 결정하는 서브클래스
class Derived extends (condition ? Base1 : Base2) {}
const derived = new Derived();
console.log(derived); // Derived {}
console.log(derived instanceof Base1); // true
console.log(derived instanceof Base2); // false
constructor(...args){super(...args);
//super은 부모 클래스의 constructor를 호출하여 인스턴스 생산
// 수퍼클래스
class Base {}
// 서브클래스
class Derived extends Base {}
----- constructor 암묵적 생산 ---
// 수퍼클래스
class Base {
constructor() {}
}
// 서브클래스
class Derived extends Base {
constructor() { super(); }
}
const derived = new Derived();
console.log(derived); // Derived {}
// 수퍼클래스
class Base {
constructor(a, b) { // ④
this.a = a;
this.b = b;
}
}
// 서브클래스
class Derived extends Base {
constructor(a, b, c) { // ② 인자 받음
super(a, b); // ③ 부모의 cons 호출
this.c = c; // 따로 c 생성
}
}
const derived = new Derived(1, 2, 3); // ①
console.log(derived); // Derived {a: 1, b: 2, c: 3}
주의!
1. constructor 생략 안하는 경우 반드시 super호출하기.class Base {} class Derived extends Base { constructor() { // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor console.log('constructor call'); } } const derived = new Derived();
- 자식클래스의 constructor에서 super 호출 전 this 참조 불가
class Base {} class Derived extends Base { constructor() { // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor this.a = 1; super(); } } const derived = new Derived(1);
- super는 반드시 자식 클래스의 constructor에서만 호출!
- super.sayHi의 참조
// 수퍼클래스 class Base { constructor(name) { this.name = name; } sayHi() { return `Hi! ${this.name}`; } } // 서브클래스 class Derived extends Base { sayHi() { // super.sayHi는 수퍼클래스의 프로토타입 메서드를 가리킨다. return `${super.sayHi()}. how are you doing?`; } } const derived = new Derived('Lee'); console.log(derived.sayHi()); // Hi! Lee. how are you doing?
- 자식 클래스의 정적 메서드 내에서 suer.sayHi는 부모 클래스의 정적 메서드 sayHi 가리킴
// 수퍼클래스 class Base { static sayHi() { return 'Hi!'; } } // 서브클래스 class Derived extends Base { static sayHi() { // super.sayHi는 수퍼클래스의 정적 메서드를 가리킨다. return `${super.sayHi()} how are you doing?`; } } console.log(Derived.sayHi()); // Hi! how are you doing?
// 수퍼클래스
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
>
getArea() {
return this.width * this.height;
}
>
toString() {
return `width = ${this.width}, height = ${this.height}`;
}
}
>
// 서브클래스
class ColorRectangle extends Rectangle {
constructor(width, height, color) {
super(width, height);
this.color = color;
}
// 메서드 오버라이딩
toString() {
return super.toString() + `, color = ${this.color}`;
}
}
>
const colorRectangle = new ColorRectangle(2, 4, 'red');
console.log(colorRectangle); // ColorRectangle {width: 2, height: 4, color: "red"}
>
// 상속을 통해 getArea 메서드를 호출
console.log(colorRectangle.getArea()); // 8
// 오버라이딩된 toString 메서드를 호출
console.log(colorRectangle.toString()); // width = 2, height = 4, color = red
- 자식 클래스의 super 호출
부모는 내부슬롯 값이 "base" 자식은 "derived"로 설정됨.
이 때문에 new 연산자 호출 시 둘은 구별된다.
부모는 new 연산자 호출 시 암묵적으로 빈 객체를 생성하여 이를 this에 바인딩
하지만 자식은 직접 인스턴스를 생성하지 않고 부모에게 인스턴스 생성을 위임하며 이것이 super를 호출해야하는 이유!
따라서 자식이 super 호출 안하면 에러가 나는 이유는 인스턴스 생성의 주체가 부모클래스이기 때문이다.
2. 부모클래스의 인스턴스 생성과 this 바인딩
constructor 내부 코드가 실행되기 이전 빈 객체(인스턴스)가 생성되고 이 객체는 this에 바인딩 된다. 따라서 constructor 내부의 this는 인스턴스를 가리킨다.// 수퍼클래스 class Rectangle { constructor(width, height) { // 암묵적으로 빈 객체, 즉 인스턴스가 생성되고 this에 바인딩된다. console.log(this); // ColorRectangle {} // new 연산자와 함께 호출된 함수, 즉 new.target은 ColorRectangle이다. console.log(new.target); // ColorRectangle ...
이 때 new.target은 ColorRectangle을 가리키는 것을 통해 new 연산자를 통해 생성된 인스턴스는 new target이 가리키는 자식 클래스가 생성한 것으로 처리됨. 따라서 생성된 인스턴스의 프로토타입은 부모의 프로토타입(Rectagle.prototype)이 아닌 자식의 ColorRectangle.prototype 이다.
// 수퍼클래스
class Rectangle {
constructor(width, height) {
// 암묵적으로 빈 객체, 즉 인스턴스가 생성되고 this에 바인딩된다.
console.log(this); // ColorRectangle {}
// new 연산자와 함께 호출된 함수, 즉 new.target은 ColorRectangle이다.
console.log(new.target); // ColorRectangle
>
// 생성된 인스턴스의 프로토타입으로 ColorRectangle.prototype이 설정된다.
console.log(Object.getPrototypeOf(this) === ColorRectangle.prototype); // true
console.log(this instanceof ColorRectangle); // true
console.log(this instanceof Rectangle); // true
...
- 수퍼클래스의 인스턴스 초기화
// 수퍼클래스 class Rectangle { constructor(width, height) { // 암묵적으로 빈 객체, 즉 인스턴스가 생성되고 this에 바인딩된다. console.log(this); // ColorRectangle {} // new 연산자와 함께 호출된 함수, 즉 new.target은 ColorRectangle이다. console.log(new.target); // ColorRectangle
// 생성된 인스턴스의 프로토타입으로 ColorRectangle.prototype이 설정된다.
console.log(Object.getPrototypeOf(this) === ColorRectangle.prototype); // true
console.log(this instanceof ColorRectangle); // true
console.log(this instanceof Rectangle); // true
// 인스턴스 초기화 this.width = width; this.height = height;
console.log(this); // ColorRectangle {width: 2, height: 4}
}
...
~468 까지 책 읽어보는 게 낫겠다.