프로퍼티 나열

객체를 객체답게 만드는 프로퍼티는 (문자열 또는 심볼)과 으로 구성된다. 객체의 특징은 키를 통해 프로퍼티에 접근할 수 있다는 점이다.

객체도 컨테이너이며 프로퍼티 나열을 지원하고 순서가 보장되지 않는다는 점을 기억해야한다.

for...in

문자열 프로퍼티가 몇개 있고 심볼 프로퍼티가 하나 있는 객체가 있다고 가정.

const SYM = Symbol();

const o = { a: 1, b: 2, c: 3, [SYM]: 4};

for(let prop in o) {
    if(!o.hasOwnProperty(prop)) continue;
    console.log(`${prop} : ${o[prop]}`);
}

hasOwnProperty는 이 예제에서 생략해도 차이가 없지만, 다른 타입의 객체, 특히 다른 사람이 만든 객체의 프로퍼티를 나열하다 보면 예상치 못한 상황이 생길 수 있으므로 hasOwnProperty를 쓰는 습관을 기르는게 좋다.

Object.keys

Object.keys는 객체에서 나열 가능한 문자열 프로퍼티를 배열로 반환한다.

const SYM = Symbol();

const o = { a: 1, b: 2, c: 3, [SYM]: 4};

Object.keys(o).forEach(prop => console.log(`${prop} : ${o[prop]}`)

객체지향 프로그래밍 (OOP)

Object Oriented Programming은 컴퓨터 과학에서 전통적인 패러다임이다.
OOP의 기본 아이디어는 단순하고 직관적이고, 객체는 데이터와 기능을 논리적으로 묶어놓은 것이다.

만약 자동차가 객체라면,

데이터

  • 제조사
  • 모델
  • 도어 숫자
  • 차량번호

기능

  • 가속
  • 변속
  • 문 열기
  • 헤드라이트

등이 있고 사물에 관해 추상적으로(어떤 자동차), 구체적으로(특정 자동차) 생각할 수 있게 합니다.

OOP의 기본 용어

클래스는 어떤 자동차 처럼 추상적이고 범용적인 것이다.
인스턴스는 특정 자동차 처럼 구체적이고 한정적인 것이다.
메서드는 기능을 메서드라 부른다. 클래스에 속하지만 특정 인스턴스에 묶이지 않은 기능을 클레스 메서드라고 한다. 예를 들어 '시동을 거는' 기능은 클래스 메서드라 할 수 있다.
인스턴스를 처음 만들 때는 생성자(constructor)가 실행된다. 생성자는 객체 인스턴스를 초기화한다.

클래스와 인스턴스 생성

자동차를 예제로 새 클래스 Car을 생성한다.

class Car {
    constructor() {
    }
}

아직 인스턴스(특정 자동차)는 만들어지지 않았지만 언제든 만들 수 있다.

인스턴스를 만들 때는 new 키워드를 사용한다.

const car1 = new Car();
const car2 = new Car();

이제 Car 클래스의 인스턴스가 두 개 생겼다. 객체가 클래스의 인스턴스인지 확인하는 instanceof 연산자를 사용해보자.

car1 instanceof Car    // true
car1 instanceof Array    // false

Car 클레스에 제조사와 모델 데이터, 변속기능을 추가해보자.

class Car {
    constructor(make, model) {
        this.make = make;
        this.model = model;
        this.userGears = ['P', 'N', 'R', 'D'];
        this.userGear = this.userGears[0];
    }
    shift(gear) {
        if(this.userGears.indexOf(gear) < 0)
            throw new Error(`Invalid gear: ${gear}`);
        this.userGear = gear;
    }
}

this 키워드는 메서드를 호출한 인스턴스를 가리키는 목적으로 쓰였다. 메서드를 호출하는 시점에서 this가 무엇인지 알 수 있게 된다.
클래스로 사용해보자.

const car1 = new Car("Tesla", "Model S");
const car2 = new Car("Mazda", "3i");
car1.shift('D');
car2.shift('R');

car1.shift('D')를 호출하면 this는 car1에 묶이고 car2.shift('R')를 호출하면 this는 car2에 묶인다.

프로토타입

클래스의 인스턴스에서 사용할 수 있는 메서드라면 그건 프로토타입 메서드이다. Car의 인스턴스에서 사용할 수 있는 shift 메서드가 프로토타입 메서드이며, 프로토타입 메서드는 Car.prototype.shift 처럼 표기할 때가 많습니다.

모든 함수에는 prototype이라는 특별한 프로퍼티가 있다. 일반적인 함수에서는 프로토타입을 사용할 일이 없지만, 객체 생성자로 동작하는 함수에서는 프로토타입이 대단히 중요하다.

new 키워드로 생성자를 만들었을 때, 새 객체는 생성자의 prototype 프로퍼티에 접근할 수 있고 객체 인스턴스는 생성자의 prototype 프로퍼티를 proto 프로퍼티에 저장한다.

인스턴스에서 메서드나 프로퍼티를 정의하면 프로토타입에 있는 것을 가리는 효과가 있다. 자바스크립트는 먼저 인스턴스를 체크 후 거기에 없다면 프로토타입을 체크하기 때문이다.

const car1 = new Car;
const car2 = new Car;

car1.shift === Car.prototype.shift; // true
car1.shift('D');
car1.shift('d');    // error
car1.userGear;  // 'D'
car1.shift === car2.shift;   // true

정적 메서드

지금까지 사용한 인스턴스 메서드 외에도 정적 메서드(클래스 메서드)가 있다. 특정 인스턴스에 적용되지 않으며, 정적 메서드에는 this는 인스턴스가 아니라 클래스 자체에 묶인다. 일반적으로 정적 메서드에는 this 대신 클래스 이름을 사용한다.

자동차에 식별 번호(VIN)을 붙이는 메서드 생성 시 VIN을 할당한다는 것을 자동차 전체를 대상으로 하는 추상적인 개념이므로 정적 매서드를 사용하는 것이 맞다.

Car.vin

상속

클래스의 인스턴스는 클래스의 기능을 모두 상속한다.
한 단계로 끝나지 않으며, 객체의 프로토타입에서 메서드를 찾지 못하면 자바스크립트는 프로토타입의 프로토타입을 검색한다. 이런 방식이 프로토타입 체인이다. 조건에 맞는 프로토타입을 찾지 못하면 에러가 발생한다.

let f = function () {
    this.a = 1;
    this.b = 2;
}
let o = new f(); // {a: 1, b: 2}

// f 함수의 prototype 속성 값들을 추가
f.prototype.b = 3;
f.prototype.c = 4;
// {a: 1, b: 2} ---> {b: 3, c: 4} ---> Object.prototype ---> null

console.log(o.a); // 1
console.log(o.b); // 2
console.log(o.c); // 4
console.log(o.d); // undefined

다중 상속, 믹스인, 인터페이스

다중 상속 (multiple inheritance)이란 기능은 클래스가 슈퍼클래스 두 개를 가지는 기능이며, 슈퍼클래스의 슈퍼클래스가 존재하는 일반적인 상속과는 다르고 다중 상속에는 충돌에 위험이 있다.
자바스크립트가 단일 상속이 필요한 문제에 대한 해답으로 내놓은 개념이 mixin이다. 믹스인의 기능은 필요한 만큼 섞어 놓은 것으로 자바스크립트는 느슨한 타입의 언어이므로 어떤 기능이라도 언제든, 어떤 객체에 추가할 수 있다.

자동차에 적용할 수 있는 보험 가입 믹스인을 만들어보자.

class InsurancePolicy {}
function makeInsurable(o) {
    o.addInsurancePolicy = function(p) {this.insurancePolicy = p;}
    o.getInsurancePolicy = function() {return this.insurancePolicy;}
    o.isInsured = function() {return !!this.insurancePolicy;}
}

이제 어떤 객체든 보험에 가입할 수 있다.


makeInsurable(Car.prototype);
const car1 = new Car();
car1.addInsurancePolicy(new InsurancePolicy());