5.26 Javascript - ES6 features

ES6의 새로운 기능

es6 Logo

1. Block-level scope variable

1.1 let

ES5에서 변수를 선언할 수 있는 유일한 방법은 var 키워드를 사용하는 것이었다. var 키워드를 사용하여 선언한 변수는 중복 선언이 가능하며 호이스팅되며 Function-level scope를 갖게 되는데 이것은 다른 C-family 언어와는 차별되는 특징이다.

기본적으로 JavaScript의 변수는 Function-level scope를 갖는다.

Function-level scope
함수내에서 선언된 변수는 함수 내에서만 유효하며 함수 외부에서는 참조할 수 없다.
Block-level scope
코드 블럭 내에서 선언된 변수는 코드 블럭 내에서만 유효하며 코드 블럭 외부에서는 참조할 수 없다.

아래의 예제를 살펴보자.

console.log(foo); // undefined
var foo = 123;
console.log(foo); // 123
{
  var foo = 456;
}
console.log(foo); // 456

var 키워드를 사용하여 선언한 변수는 중복 선언이 가능하기 때문에 위의 코드는 문법적으로 문제가 없다. 하지만 코드블럭 내의 변수 foo는 전역변수이기 때문에 전역에서 선언된 변수 foo의 값을 대체하는 새로운 값을 재할당한다.

ES6는 Block-level scope를 갖는 변수를 선언하기 위해 let 키워드를 제공한다.

let foo = 123;
{
  let bar = 456;
}
console.log(foo); // 123
console.log(bar); // ReferenceError: bar is not defined

위 코드의 변수 bar는 Block-level scope를 갖는 지역 변수이다.

var는 중복 선언이 가능하였으나 let은 중복 선언 시 에러가 발생한다.

var foo = 123;
var foo = 456;  // OK

let bar = 123;
let bar = 456;  // Error: Identifier 'bar' has already been declared

자바스크립트는 ES6의 let, const를 포함하여 모든 선언(var, let, const, function, function*, class)을 호이스팅(Hoisting)한다.

하지만 var 키워드로 선언된 변수와는 달리 let 키워드로 선언된 변수를 선언문 이전에 참조하면 ReferenceError가 발생한다. 이는 let 키워드로 선언된 변수는 코드블록의 시작에서 변수의 선언까지 일시적 사각지대(Temporal Dead Zone; TDZ)에 빠지게 되기 때문이다.

console.log(foo); // undefined
var foo;

console.log(bar); // Error: Uncaught ReferenceError: bar is not defined
let bar;

Block-level scope를 지원하는 let은 var보다 더욱 직관적이다. 다음 코드를 살펴보자.

var funcs = [];
// create a bunch of functions
for (var i = 0; i < 3; i++) {
  funcs.push(function() {
    console.log(i);
  })
}
// call them
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

위 코드의 실행 결과로 0,1,2를 기대할 수도 있지만 결과는 3이 3번 출력된다. 그 이유는 for문의 var i가 전역 변수이기 때문이다. 0,1,2을 출력시키기 위해서는 아래와 같은 코드가 필요하다.

var funcs = [];
// create a bunch of functions
for (var i = 0; i < 3; i++) {
  (function() {
    var local = i;
    funcs.push(function() {
      console.log(local);
    })
  })();
}
// call them
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

JavaScript의 Function-level scope로 인한 문제를 회피하는 한 수단으로 클로저를 활용한 방법이다.

반복문에서 ES6의 let 키워드를 사용하면 동일한 동작을 한다.

var funcs = [];
// create a bunch of functions
for (let i = 0; i < 3; i++) { // Note the use of let
  funcs.push(function() {
    console.log(i);
  })
}
// call them
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

1.2 const

const는 상수(변하지 않는 값)를 위해 사용한다. 하지만 반드시 상수만을 위해 사용하지는 않는다. 이에 대해서는 후반부에 설명한다.

const는 let과 대부분 동일한 특징을 갖는다. 단 let은 초기화 이후 다른 값으로 재할당이 자유로우나 const는 초기화 이후 재할당이 금지된다.

const FOO = 123;
FOO = 456;   // TypeError: Assignment to constant variable.

주의할 것은 const는 반드시 선언과 동시에 초기화가 이루어져야 한다는 것이다.

const FOO; // SyntaxError: Missing initializer in const declaration

또한 const는 let과 마찬가지로 Block-level scope를 갖는다.

{
  const FOO = 10;
  console.log(FOO); //10
}
console.log(FOO); // ReferenceError: FOO is not defined

const는 가독성의 향상과 유지보수의 편의를 위해 적극적으로 사용해야 한다. 예를 들어 아래 코드를 살펴보자.

// Low readability
if (x > 10) {
}

// Better!
const MAXROWS = 10;
if (x > MAXROWS) {
}

조건문 내의 10은 어떤 의미로 사용하였는지 파악하기가 곤란한다. 하지만 네이밍이 적절한 상수로 선언하면 가독성과 유지보수성이 대폭 향상된다.

const는 객체에도 사용할 수 있다. 물론 재할당은 금지된다.

const obj = { foo: 123 };
obj = { bar: 456 }; // TypeError: Assignment to constant variable.

하지만 객체의 프로퍼티는 보호되지 않는다. 다시 말하자면 재할당은 불가능하지만 할당된 객체의 내용은 변경할 수 있다.

const user = {
  name: 'Lee',
  address: {
    city: 'Seoul'
  }
};

user.name = 'Kim'; // 허용된다!

console.log(user); // { name: 'Kim', address: { city: 'Seoul' } }

객체의 프로퍼티까지 보호하여 primitive data와 같이 변경이 불가능한 값(immutable value)으로 만들고 싶다면 Object.freeze() 메서드를 사용한다.

const user = {
  name: 'Lee',
  address: {
    city: 'Seoul'
  }
};

Object.freeze(user);

user.name = 'Kim'; // 무시된다!
console.log(user); // { name: 'Lee', address: { city: 'Seoul' } }

console.log(Object.isFrozen(user)); // true

단 객체 내부의 객체는 변경가능하다.

user.address.city = 'Busan'; // 변경된다!
console.log(user); // { name: 'Lee', address: { city: 'Busan' } }

내부 객체까지 변경 불가능하게 만들려면 Deep freeze를 하여야 한다.

function deepFreeze(obj) {
  const props = Object.getOwnPropertyNames(obj);

  props.forEach((name) => {
    const prop = obj[name];
    if(typeof prop === 'object' && prop !== null) {
      deepFreeze(prop);
    }
  });
  return Object.freeze(obj);
}

const user = {
  name: 'Lee',
  address: {
    city: 'Seoul'
  }
};

deepFreeze(user);

user.name = 'Kim';
user.address.city = 'Busan';

console.log(user); // { name: 'Lee', address: { city: 'Seoul' } }

ES6를 사용한다면 var의 사용은 가급적 지양하고 아래와 같이 경우에 따라 let과 const를 사용하는 것을 추천한다.

  • primitive형 변수에는 let를 사용
  • 변경이 발생하지 않는(재할당이 필요없는) primitive형 변수와 객체형 변수에는 const를 사용

객체형 변수에 const를 사용하는 이유는 객체의 속성값이 변경된다하더라도 객체형 변수에 저장되는 주소값은 변경되지 않기 때문이다. 자바스크립트의 값은 대부분 객체(primitive형 변수를 제외한 모든 값은 객체이다)이므로 결국 대부분의 경우 const를 사용하게 된다.

2. Arrow function (Arrow 함수)

Arrow function은 익명함수를 좀 더 간략하게 표현할 수 있으며 Lexical this를 제공한다.

Arrow function은 항상 익명으로 사용한다. 문법은 아래와 같다.

// 매개변수 지정
    () => { ... } // 매개변수가 없을 경우
     x => { ... } // 매개변수가 한개인 경우는 괄호를 생략할 수 있다.
(x, y) => { ... } // 매개변수가 여러개인 경우

// 함수 몸체 지정
x => { return x * x }  // block
x => x * x             // 위 표현과 동일하다.

Arrow function의 장점은 크게 2가지로 이야기할 수 있다.

첫번째 일반적인 함수 표현식보다 표현이 간단하다.

// ES5
var arr = [1, 2, 3];
var pow = arr.map(function(x) {
  // x는 요소값
  return x * x;
});

console.log(pow); // [ 1, 4, 9 ]
// ES6
const arr = [1, 2, 3];
const pow = arr.map(x => x * x);

console.log(pow); // [ 1, 4, 9 ]

두번째 장점으로 직관적인 this를 사용할 수 있다.

JavaScript this는 해당 함수 호출 패턴에 따라 this에 바인딩되는 객체가 달라진다. 콜백함수 내부의 this는 전역 객체 window를 가리킨다.

Arrow function은 위의 규칙을 따르지 않고 언제나 자신을 포함하는 외부 scope에서 this를 계승 받는다. 이를 Lexical this라 한다.

function Prefixer(prefix) {
  this.prefix = prefix;
}

Prefixer.prototype.prefixArray = function (arr) {
  // (B)
  return arr.map(function (x) {
    return this.prefix + x; // (A)
  });
};

var pre = new Prefixer('Hi ');
console.log(pre.prefixArray(['Lee', 'Kim']));

(A)에서 사용한 this는 아마도 생성자 함수 Prefixer가 생성한 객체(위 예제의 경우 pre)일 것으로 기대하였겠지만 이곳에서 this는 전역 객체 window를 가리키므로 기대한 대로 동작하지 않는다. (B)에서의 this는 생성자 함수 Prefixer가 생성한 객체(위 예제의 경우 pre)이다.

위 설명이 잘 이해되지 않는다면 this를 참조하기 바란다.

콜백함수 내부의 this가 메서드를 호출한 객체를 가리키게 하기 위해서는 아래의 4가지 방법이 있다.

// Solution 1: that = this
Prefixer.prototype.prefixArray = function (arr) {
  var that = this;  // (A)
  return arr.map(function (x) {
    return that.prefix + x;
  });
};
// Solution 2: map(func, this)
Prefixer.prototype.prefixArray = function (arr) {
  return arr.map(function (x) {
    return this.prefix + x;
  }, this); // (A)
};

ES5에 추가된 Function.prototype.bind()로 this를 바인딩한다. call(), apply()도 사용 가능하다.

// Solution 3: bind(this)
Prefixer.prototype.prefixArray = function (arr) {
  return arr.map(function (x) {
    return this.prefix + x;
  }.bind(this)); // (A)
};

Arrow function은 Solution 3의 Syntactic sugar이다.

Prefixer.prototype.prefixArray = function (arr) {
  return arr.map((x) => this.prefix + x);
};

이것을 class로 표현하면 아래와 같다.

class Prefixer {
  constructor(prefix) {
    this.prefix = prefix;
  }
  prefixArray(arr) {
    return arr.map(x => this.prefix + x); // (A)
  }
}

3. Template Strings (템플릿 문자열)

ES6는 템플릿 문자열(template string)이라고 불리는 새로운 종류의 문자열 표기법을 도입하였다. 템플릿 문자열은 일반 문자열과 비슷해 보이지만, ‘ 또는 “ 같은 통상적인 따옴표 문자 대신 백틱(backtick) 문자 `를 사용한다.

let template = `Template strings can include 'single quotes' and "double quotes" inline.`;

console.log(template);

일반적인 문자열과 달리 템플릿 문자열은 여러 줄에 걸쳐 표현할 수 있으며 줄바꿈과 들여쓰기 등 템플릿 문자열 속의 모든 white-space는 있는 그대로 적용된다.

let template =`<ul class="nav-items">
  <li><a href="#home">Home</a></li>
  <li><a href="#news">News</a></li>
  <li><a href="#contact">Contact</a></li>
  <li><a href="#about">About</a></li>
</ul>`;

console.log(template);

템플릿 문자열은 + 연산자를 사용하지 않아도 간단한 방법으로 문자열에 새로운 문자열을 삽입할 수 있는 기능을 제공한다. 이를 String Interpolation(문자열 삽입)이라 한다.

const first = 'Ung-mo';
const last = 'Lee';
console.log('My name is ' + first + ' ' + last + '.');
console.log(`My name is ${first} ${last}.`);

console.log(`1 and 1 make ${1 + 1}`);

위 코드의 ${text}, ${1 + 1}를 템플릿 대입문(template substitution)이라 한다. 템플릿 대입문에는 문자열뿐만아니라 모든 JavaScript 표현식이 사용될 수 있다.

function authorize(user, action) {
  if (!user.hasPrivilege(action)) {
    throw new Error(
      `User ${user.name} is not authorized to do ${action}.`);
  }
}

4. Extended Parameter Handling (함수 파라미터 확장)

4.1 Default Parameter value (기본 파라미터 초기값)

파라미터에 초기값을 설정하여 함수 내에서 수행하던 파라미터 체크 및 초기화를 간편화할 수 있다.

// ES5
function plus(x, y) {
  x = x || 0;
  y = y || 0;
  return x + y;
}

console.log(plus());     // 0
console.log(plus(1, 2)); // 3
// ES6
function plus(x = 0, y = 0) {
  return x + y;
}

console.log(plus());     // 0
console.log(plus(1, 2)); // 3

4.2 Rest Parameter (Rest 파라미터)

인자의 갯수를 사전에 알 수 없는 가변 인자 함수의 경우, 함수를 호출할 때 인수들과 함께 암묵적으로 arguments 객체가 함수 내부로 전달되는 함수 객체의 arguments 속성을 사용하여 인자값을 확인하여야 한다.

// ES5
function sum() {
  var array = Array.prototype.slice.call(arguments);
  return array.reduce(function(pre, cur) {
    return pre + cur;
  });
}

console.log(sum(1, 2, 3, 4, 5));

ES6의 Rest 파라미터는 가변인자를 함수 내부에 배열로 전달한다. 따라서 유사 배열인 arguments 객체를 배열로 변환하는 등의 번거로움을 피할 수 있다.

// ES6
function sum(...args) {
  console.log(Array.isArray(args)); // true
  return args.reduce((pre, cur) => pre + cur);
}
console.log(sum(1, 2, 3, 4, 5));

4.3 Spread Operator (Spread 연산자)

Spread 연산자(…)는 배열을 다른 배열의 내부에 삽입시킨다.

// ES5
var arr = [ 1, 2, 3 ];
console.log(arr.concat([ 4, 5, 6 ])); // [ 1, 2, 3, 4, 5, 6 ]
// ES6
var arr = [ 1, 2, 3 ];
console.log([ ...arr, 4, 5, 6 ]); // [ 1, 2, 3, 4, 5, 6 ]

배열을 함수의 인수로 사용하고 싶은 경우, Function.prototype.apply를 사용하는 것이 일반적이다. 하지만 Spread 연산자를 사용하면 Function.prototype.apply를 사용할 필요가 없다.

// ES5
function sum() {
  var array = Array.prototype.slice.call(arguments);
  return array.reduce(function(pre, cur) {
    return pre + cur;
  });
}

var arr = [1, 2, 3, 4, 5];

console.log(sum.apply(null, arr));
// ES6
function sum(...args) {
  return args.reduce((pre, cur) => pre + cur);
}

var arr = [1, 2, 3, 4, 5];

console.log(sum(...arr));

5. Destructuring (디스트럭처링)

Destructuring은 객체 또는 배열에 저장되어 있는 여러 값을 추출해내는 매우 편리한 방법이다.

5.1 Object destructuring (객체 디스트럭처링)

ES5의 경우, 객체의 값에 접근 또는 할당하기 위해서는 속성명(키)를 사용하여야 한다.

var obj = { first: 'Jane', last: 'Doe' };
var name = {};

name.first = obj.first;
name.last  = obj.last;

console.log(name); // { first: 'Jane', last: 'Doe' }

ES6에서는 destructuring을 사용할 수 있다.

const obj = { first: 'Jane', last: 'Doe' };
const {first: f, last: l} = obj;
// f = 'Jane', l = 'Doe'

console.log({first: f, last: l});
// { first: 'Jane', last: 'Doe' }

// {prop} is short for {prop: prop}
const {first, last} = obj;
// first = 'Jane'; last = 'Doe'

console.log({first, last});
// { first: 'Jane', last: 'Doe' }

속성명을 지정하여 여러 값이 저장되어 있는 객체에서 원하는 값만을 추출할 수 있다.

function margin() {
  const left = 1, right = 2, top = 3, bottom = 4;
  return { left, right, top, bottom };
}
const { left, bottom } = margin();
console.log(left, bottom); // 1 4

중첩 객체의 경우는 아래와 같이 사용한다.

function settings() {
  return { display: { color: 'red' }, keyboard: { layout: 'qwerty'} };
}
const { display: { color: displayColor }, keyboard: { layout: keyboardLayout }} = settings();

console.log(displayColor, keyboardLayout); // red qwerty

5.2 Array destructuring (배열 디스트럭처링)

배열의 경우도 객체의 경우와 유사하다.

const iterable = ['a', 'b'];
const [x, y] = iterable; // x = 'a', y = 'b'

console.log([x, y]); // [ 'a', 'b' ]

객체의 경우 속성명이 일치하는 값을 가지고 오지만 배열의 경우, 순차적으로 값이 저장된다.

const [all, year, month, day] = /^(\d\d\d\d)-(\d\d)-(\d\d)$/.exec('1999-12-31');

console.log([all, year, month, day]); // [ '1999-12-31', '1999', '12', '31' ]

필요한 값만을 추출할 수 있다.

const array = [1, 2, 3, 4];
const [first, ,third] = array;
console.log(first, third); // 1 3

6. Class

Javascript는 프로토타입 기반(prototype-based) 객체지향형 언어다. 비록 다른 객체지향 언어들과의 차이점에 대한 논쟁들이 있긴 하지만, Javascript는 강력한 객체지향 프로그래밍 능력들을 지니고 있다.

프로토타입 기반 프로그래밍은 클래스가 필요없는(class-free) 객체지향 프로그래밍 스타일로 프로토타입 체인과 클로저 등으로 객체 지향 언어의 상속, 캡슐화(정보 은닉) 등의 개념을 구현할 수 있다.

var Person = function(name) {
  this.name = name;
}
Person.prototype.walk = function() {
  console.log(this.name + ' is walking.');
}

var me = new Person('Lee');

console.log(me instanceof Person); // true
me.walk(); // Lee is walking.

var you = new Person('Kim');
you.walk(); // Kim is walking.

prototype

프로토타입 객체에 의한 속성의 상속

하지만 클래스 기반 언어에 익숙한 프로그래머들은 혼란을 일으킬 수 있으며 JavaScript를 어렵게 느끼게하는 하나의 장벽처럼 인식되었다.

ES6의 클래스는 기존 prototype 기반 객체지향 프로그래밍보다 클래스 기반 언어에 익숙한 프로그래머가 보다 빠르게 학습할 수 있는 단순명료한 새로운 문법을 제시하고 있다. ES6의 클래스가 새로운 객체지향 모델을 제공하는 것이 아니며 사실 클래스도 함수이고 기존 prototype 기반 패턴의 Syntactic sugar일 뿐이다.

6.1 Class Definition (클래스 정의)

ES6 클래스를 정의하기 위해서는 class 키워드를 사용한다. name 속성과 walk 메서드를 갖는 Person 클래스를 정의해 보자.

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

  walk() {
    console.log(`${this._name} is walking.`);
  }
}

let me = new Person('Lee');

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

me.walk(); // Lee is walking.

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

class Foo {
  let name = ''; // SyntaxError

  constructor() {}
}

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

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

console.log(new Foo('Lee')); // Foo { name: 'Lee' }

constructor 내부에서 선언한 멤버 변수 name은 this(클래스 Foo의 인스턴스)에 바인딩되어 있으므로 언제나 public이다.

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

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

ES6 class 사양은 private, public, protected 키워드를 지원하지 않는다. Symbol 또는 WeakMap을 사용하여 private 멤버 변수를 정의할 수 있다.

constructor 메서드는 객체를 생성하고 초기화하기 위한 특수한 메서드이다. constructor 메서드는 클래스 내에 한 개만 존재할 수 있으며 만약 클래스가 2개 이상의 constructor 메서드를 포함하면 SyntaxError가 발생한다.

constructor 메서드는 생략할 수 있다. constructor를 생략하면 constructor() {}를 포함한 것과 동일하게 동작하지만 객체의 생성과 동시에 초기화는 할 수 없다.

class Foo {}

const foo = new Foo();
console.log(foo); // Foo {}

foo.num = 1;      // 동적 속성 추가
console.log(foo); // Foo { num: 1 }

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

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

new 연산자를 사용하지 않고 객체를 생성하면 에러가 발생한다.

class Foo {}

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

ES6 Class는 함수이다.

class Foo {}

console.log(typeof Foo);       // function
console.log(typeof new Foo()); // object

자바스크립트는 ES6의 let, const를 포함하여 모든 선언(var, let, const, function, function*, class)을 호이스팅(Hoisting)한다.

하지만 class 선언문 이전에 class를 참조하면 참조하면 ReferenceError가 발생한다. 이는 코드블록의 시작에서 class의 선언까지 일시적 사각지대(Temporal Dead Zone; TDZ)에 빠지게 되기 때문이다.

new Foo(); // ReferenceError

class Foo {}

클래스 표현식으로도 클래스를 정의할 수 있다. 함수와 마찬가지로 클래스는 이름을 가질 수도 갖지 않을 수도 있다. 이때 클래스가 할당된 변수를 사용해 클래스를 생성하지 않고 기명 클래스의 클래스명을 사용해 클래스를 생성하면 에러가 발생한다. 이는 함수와 마찬가지로 클래스 표현식에서 사용한 클래스명은 외부 코드에서 접근 불가능하기 때문이다. 자세한 내용은 함수표현식(Function expression)을 참조하기 바란다.

const Foo = class {}

const foo = new Foo();
console.log(foo); // Foo {}

const Bar = class MyClass {}

const bar = new Bar();
console.log(bar);  // MyClass {}

new MyClass(); // ReferenceError: MyClass is not defined

Class body에는 method(Static method, Prototype method)나 constructor와 같은 class member를 정의한다.

6.2 Static method (정적 메서드)

static 키워드는 클래스의 정적(static) 메서드를 정의한다. 정적 메서드는 클래스의 인스턴스화(instantiating)없이 호출하며 클래스의 인스턴스로 호출할 수 없다. 정적 메서드는 어플리케이션을 위한 유틸리티(utility) 함수를 생성하는데 주로 사용된다.

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

위에서도 언급했지만 사실 Class도 함수이고 기존 prototype 기반 패턴의 Syntactic sugar일 뿐이다.

위 예제를 ES5로 표현해보면 아래와 같다.

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

ES5로 표현한 위 코드는 ES6 Class로 표현한 코드와 정확히 동일하게 동작한다.

정적 메서드는 클래스의 인스턴스화(instantiating)없이 호출하며 클래스의 인스턴스로 호출할 수 없는 이유에 대해 알아보자. prototypeJavaScript OOP에 대한 사전 지식이 필요하므로 아직 이에 대한 학습이 안되어 있으면 skip하기 바란다.

우선 FOO는 함수이다. Class도 사실 함수라고 위에서 언급하였다.

class Foo {
  constructor() {}
}

console.log(typeof Foo); // function

함수 객체는 prototype 프로퍼티를 갖는데 일반 객체의 [[Prototype]] 프로퍼티와는 다른 것이며 일반 객체는 prototype 프로퍼티를 가지지 않는다.

함수 객체만이 가지고 있는 prototype 프로퍼티는 함수 객체가 생성자로 사용될 때 이 함수를 통해 생성된 객체의 부모 역할을 하는 객체를 가리킨다. 즉 Foo는 함수이고 생성자 함수로 사용되므로 함수 Foo의 prototype은 함수 Foo로 생성되는 객체 foo의 부모 역할을 한다.

console.log(Foo.prototype === foo.__proto__); // true

그리고 prototype이 가지고 있는 constructor 프로퍼티는 함수 객체 자신을 가리킨다.

console.log(Foo.prototype.constructor === Foo); // true

정적 메서드인 staticMethod는 함수 객체 Foo의 member, 프로토타입 메서드인 prototypeMethod는 Foo.prototype의 member가 되므로 staticMethod는 foo에서 호출할 수 없게 된다.

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

console.log(typeof Foo.staticMethod); // function
console.log(Foo.staticMethod());      // staticMethod

console.log(typeof Foo.prototype.prototypeMethod); // function
console.log(foo.prototypeMethod());                // prototypeMethod

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

class prototype

6.3 Class Inheritance (클래스 상속)

상속(또는 확장)은 코드 재사용의 관점에서 매우 유용하다. 새롭게 정의할 클래스가 기존에 있는 클래스와 매우 유사하다면, 상속을 통해 다른 점만 구현하면 된다. 코드 재사용은 개발 비용을 현저히 줄일 수 있는 잠재력이 있기 때문에 매우 중요하다.

extends 키워드는 부모 Class(Base class)를 상속하는 자식 Class(Sub class)의 생성을 위해 class 선언에 사용된다.

// Base class
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toString() {
    return `(${this.x}, ${this.y})`;
  }
}

// Sub class
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // (A)
    this.color = color;
  }
  toString() {
    return `${super.toString()} in ${this.color}`; // (B)
  }
}

const cp = new ColorPoint(25, 8, 'green');
console.log(cp.toString()); // (25, 8) in green

console.log(cp instanceof ColorPoint); // true
console.log(cp instanceof Point);      // true

ColorPoint는 Point를 상속받은(파생된 또는 확장한) 자식 class이다.

super 키워드는 부모 Class(Base Class)의 참조(Reference)이다. 위 예제의 경우 super는 ColorPoint의 부모 class인 Point를 가리키며, 부모 class의 프로퍼티를 참조하기 위해 사용한다. (B)

(A)의 super 메서드는 자식 class의 constructor 내부에서 부모 class의 constructor(super-constructor)를 호출한다. 자식 class는 constructor를 생략하지 않는 경우, 자신의 constructor내에서 반드시 super()를 호출하여야 한다.

자식class의 constructor에서 super()를 호출하지 않으면 ReferenceError가 발생한다.

class Foo {}

class Bar extends Foo {
  constructor() { // ReferenceError: this is not defined
  }
}

new Bar();

자식class에서 this를 사용하기 위해서는 반드시 super()를 호출하여야 한다.

class Foo {}

class Bar extends Foo {
  constructor(num) {
    // console.log(this); // ReferenceError: this is not defined
    super();
    this.num = num;    // OK
    console.log(this); // Bar { num: 5 }
  }
}

new Bar(5);

prototype 관점에서 바라보면 자식 class의 [[prototype]]은 부모 class이다.

class Foo {}

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

console.log(Bar.__proto__ === Foo); // true
console.log(Bar.prototype.__proto__ === Foo.prototype); // true

class-prototype-relation

자식 class의 [[prototype]]은 부모 class이다

이것은 Prototype chain에 의해 부모class의 정적 메서드도 상속됨을 의미한다.

class Foo {
  static staticMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

console.log(Bar.staticMethod()); // 'hello'

자식class의 정적 메서드 내부에서도 super를 사용하여 정적 메서드를 호출할 수 있다.

class Foo {
  static staticMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static staticMethod() {
    return `${super.staticMethod()}, too`;
 }
}

console.log(Bar.staticMethod()); // 'hello, too'

class-prototype-chain

prototype chain에 의한 메서드의 상속

Reference

Back to top
Close