Jello's development blog

Jello's development blog

Javascript ES6의 클래스, 프로토타입 1

Javascript는 프로토타입 기반의 객체지향 언어이다. 프로토타입 기반 언어는 클래스를 사용하지 않고, 프로토타입 체인과 클로저로 상속, 캡슐화 등을 구현할 수 있다. 그러나 클래스에 익숙한 개발자들에게 프로토타입 기반 객체 사용은 어렵고 익숙하지 않을 수 있기 때문에 ES6에서 클래스 문법이 등장했다.

ES6의 클래스는 기존의 프로토타입보다 익숙한 문법을 사용한다. 알아둬야 할 것은 ES6의 클래스가 이전의 프로토타입 기반 프로그래밍과는 다른 모델을 사용하고 있는 것이 아니고, 그저 프로토타입의 Syntatic sugar일 뿐이라는 것이다. 따라서 결국 javascript의 클래스도 함수이다.

이 글에서는 javascript의 프로토타입 기반의 문법이 ES6의 클래스 문법으로 바뀌면서 생긴 새로운 개념(?)을 설명해보겠다.

정의

ES5

var Person = (function () {
  // Constructor
  function Person(name) {
    this._name = name;
  }

  // method
  Person.prototype.sayHi = function () {
    console.log('Hi! ' + this._name);
  };

  // return constructor
  return Person;
}());

var me = new Person('Lee');
me.sayHi(); // Hi! Lee.

console.log(me instanceof Person); // true

프로토타입 문법으로 객체를 정의할 때에는 함수(객체)를 만든 뒤에 그 함수의 이름으로 생성자를 만들고, 프로토타입을 설정하고, 객체의 생성자를 변수에 담았다. sayHi 함수를 하나만 만들어 객체의 인스턴스들이 재활용하기 위해서 프로토타입 안에 정의해 주었다.

ES6

class Person {
  constructor(name) {
    this._name = name;
  }

  sayHi() {
    console.log(`Hi! ${this._name}`);
  }
}

const me = new Person('Lee');
me.sayHi(); // Hi! Lee

console.log(me instanceof Person); // true

클래스 문법에는 constructor라는 생성자 메소드가 있고, 프로토타입 안에 설정되었던 sayHi 함수는 그냥 클래스의 scope에 정의되었다.

인스턴스 생성

class Foo {}
const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'

클래스를 new 키워드 없이 생성하려 할 경우 에러가 난다.

하지만 프로토 타입의 경우 에러는 나지 않지만 문법 오류가 발생하게 된다.

function Person(name) {
    this._name = name;
}

var me = Person('Lee');
// console.log(me._name) // TypeError: Cannot read property '_name' of undefined
console.log(_name); // Lee

Person을 함수로 여기고, this에 전역 객체인 window가 바인딩되어 me._name는 아예 정의되지 않게 된다. 대신에 전역 객체의 _name이 정의가 된다.

생성자 (constructor)

ES5

function Foo(num) {
   this.num = num;
}

console.log(new Foo(1)); // Foo { num: 1 }

프로토타입 문법은 객체로 사용할 함수가 생성자가 된다.

ES6

class Bar {
  constructor(num) {
    this.num = num;
  }
}

console.log(new Bar(1)); // Bar { num: 1 }

클래스 문법은 constructor라는 생성자 메소드를 제공한다. 생성자 메소드는 생략하면 constructor() {}를 포함한 것과 동일하게 동작하지만 객체의 생성과 동시에 초기화는 할 수 없다.

멤버 변수

클래스 바디에는 메소드만을 포함할 수 있다. 클래스 바디에 멤버 변수를 선언하면 SyntaxError가 발생한다.

class Foo {
  let name = ''; // SyntaxError
  
  constructor() {
    ...
  }
}

따라서 멤버 변수의 선언과 초기화는 반드시 constructor 내부에서 해야 한다.

class Foo {
  constructor(name) {
    this.name = name; // OK
  }
}

const foo = new Foo('Lee');
console.log(foo.name); // Lee

constructor 내부에서 선언한 멤버 변수 name은 this(클래스의 인스턴스)에 바인딩되어 있으므로 언제나 public이다. ES6 class는 private, public, protected 키워드와 같은 접근 제한자를 지원하지 않는다.

게터(getter), 세터(setter)

게터(getter)

getter는 어떤 프로퍼티에 접근할 때마다 프로퍼티를 조작하는 행위가 필요할 때 사용한다.

class Foo {
  constructor(arr = []) {
    this._arr = arr;
  }

  // getter: firstElem은 프로퍼티 이름과 같이 사용된다.
  // getter는 반드시 무언가를 반환하여야 한다.
  get firstElem() {
    if (this._arr.length === 0) { return null; }
    return this._arr[0];
  }
}

const foo = new Foo([1, 2]);
// 프로퍼티 firstElem에 접근하면 getter가 호출된다.
console.log(foo.firstElem); // 1

세터(setter)

setter는 어떤 프로퍼티에 값을 할당할 때마다 프로퍼티를 조작하는 행위가 필요할 때 사용한다.

class Foo {
  constructor(arr = []) {
    this._arr = arr;
  }

  // getter: firstElem은 프로퍼티 이름과 같이 사용된다.
  // getter는 반드시 무언가를 반환하여야 한다.
  get firstElem() {
    if (this._arr.length === 0) { return null; }
    return this._arr[0];
  }

  // setter: firstElem은 프로퍼티 이름과 같이 사용된다.
  set firstElem(elem) {
    // ...this._arr은 this._arr를 개별 요소로 분리한다
    this._arr = [elem, ...this._arr];
  }
}

const foo = new Foo([1, 2]);

// 프로퍼티 lastElem에 값을 할당하면 setter가 호출된다.
foo.firstElem = 100;

console.log(foo.firstElem); // 100

정적(static) 메소드

ES5

Javascript에서는 프로토타입이 아닌 그 객체에 직접 정의함으로써 정적 메소드를 만들 수 있다.

var Foo = (function () {
  function Foo(prop) {
    this.prop = prop;
  }
  Foo.staticMethod = function () {
    return 'staticMethod';
  };
  Foo.prototype.prototypeMethod = function () {
    return 'prototypeMethod';
  };
  return Foo;
}());

var foo = new Foo(123);

console.log(Foo.staticMethod());
console.log(foo.staticMethod()); // Uncaught TypeError: foo.staticMethod is not a function

ES6

ES6 에서는 static 키워드로 클래스의 정적 메소드를 정의할 수 있다. 정적 메소드는 클래스의 인스턴스화 없이 호출하며, 클래스의 인스턴스에서 호출할 수 없다.

class Foo {
  constructor(prop) {
    this.prop = prop;      
  }
  static staticMethod() {
    return 'staticMethod';
  }
  prototypeMethod() {
    return 'prototypeMethod';
  }
}

const foo = new Foo(123);

console.log(Foo.staticMethod());
console.log(foo.staticMethod()); // Uncaught TypeError: foo.staticMethod is not a function

참고자료