Object-Oriented Programming

배열과 객체의 차이점

배열은 값을 가지며 각 값에는 숫자형 인덱스가 있다. 객체는 프로퍼티를 가지며 각 프로퍼티에는 문자열이나 심볼 인덱스가 있다.

배열에는 순서가 있다. 즉, arr[0]은 항상 arr[1]보다 앞에있다. 객체에는 순서가 보장되지 않는다.

프로퍼티 나열

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]}`)

  // a: 1
  // b: 2
  // c: 3
}

WARNING

for...in 을 배열에 사용할 수도 있지만, 배열에서는 일반적인 for 루프나 forEach 를 사용하는 것이 좋다.

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]}`))

// a: 1
// b: 2
// c: 3

객체의 프로퍼티 키를 배열로 가져와야 할 경우에는 Object.keys 가 편리하다.

// x로 시작하는 프로퍼티를 모두 가져오기
const o = { apple: 1, xochitl: 2, ballon: 3, guitar: 4, xylophone: 5 }

Object.keys(o)
  .filter(prop => prop.match(/^x/))
  .forEach(prop => console.log(`${prop}: ${o[prop]}`))

// xochitl: 2
// xylophone: 5

객체지향 프로그래밍

클래스와 인스턴스 생성

class Car {
  constructor() {}
}

위의 코드는 새 클래서 Car 를 만든다. 인스턴스를 만들때는 new 키워드를 사용한다.

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

객체가 클래스의 인스턴스인지 아닌지 확인할 때는 instanceof 연산자를 사용하면 된다.

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

여기서의 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')를 호출하면 t4 his 는 car2 에 묶이게 된다.

car1.userGear // "D"
car2.userGear // "E"

Car 클래스에 shift 메서드를 사용하면 잘못된 기어를 선택하는 실수를 방지할 수 있는것 처럼 보이지만, 완벽하게 보호된 것은 아니다. (예: 직접 car1.userGear = 'X'로 설정하면 막을 수 없다.) Car 클래스를 다음과 같이 수정하면 실수로 기어 프로퍼티를 고치지 않도록 어느 정도 막을 수 있다.

class Car {
  constructor(make, model) {
    this.make = make
    this.model = model
    this._userGears = ['P', 'N', 'R', 'D']
    this._userGear = this._userGears[0]
  }

  get userGear() {
    return this._userGear
  }
  set userGear(value) {
    if (this._userGears.indexOf(value) < 0) {
      throw new Error(`Invalid gear: ${value}`)
    }
    this._userGear = value
  }

  shift(gear) {
    this.userGear = gear
  }
}

프로퍼티를 꼭 보호해야 하는 경우 스코프를 이용해 보호하는 WeakMap 인스턴스를 사용할 수 있습니다. Car 클래스를 다음과 같이 고치면 기어 프로퍼티를 완벽하게 보호할 수 있습니다.

const Car = (function() {
  const carProps = new WeakMap()

  class Car {
    constructor(make, model) {
      this.make = make
      this.model = model
      this._userGears = ['P', 'N', 'R', 'D']
      carProps.set(this, { userGear: this._userGears[0] })
    }
    get userGear() {
      return carProps.get(this).userGear
    }
    set userGear(value) {
      if (this._userGears.indexOf(value) < 0) {
        throw new Error(`Invalid gear: ${value}`)
      }
      carProps.get(this).userGear = value
    }

    shift(gear) {
      this.userGear = gear
    }
  }

  return Car
})()

위 예제에서는 즉시 호출하는 함수 표현식을 사용하여 WeakMap 을 클로저로 감싸고 바깥에서 접근할 수 없도록 하였다. WeakMap 은 클래스 외부에서 접근하면 안 되는 프로퍼티를 안전하게 저장한다.

클래스는 함수다

// ES5
function Car(make, model) {
  this.make = make
  this.model = model
  this._userGears = ['P', 'N', 'R', 'D']
  this._userGear = this.userGears[0]
}
// ES6
class Es6Car{}
function Es5Car{}
> typeof Es6Car // "function"
> typeof Es5Car // "function"

ES6 에서 클래스가 바뀐것이 아니라 단지 간편한 새 문법이 생겼을 뿐이다.

프로토타입