javascript에서 class가 어떻게 동작하는지에 대해 정리해보겠습니다.
TL;DR
- javascript에서는 class 역시 함수.
- class 키워드는 클래스를 직관적으로 표현하기 위한 새로운 문법일 뿐 ES6 이전과 동작 방식은 같음.
- 인스턴스는 클래스의 prototype을 물려받음.
- 클래스의 메서드는 클래스의 prototype에 저장됨.
- 인스턴스에서는 프로토타입 체인을 통해 클래스의 메서드에 접근할 수 있음.
- 정적 메서드는 클래스의 prototype이 아닌 클래스 자체에 저장됨.
- 그렇기 때문에 정적 메서드는 인스턴스에서 접근할 수 없고 클래스만 접근할 수 있음.
class는 함수다
ES6에서 class 키워드가 등장하기 전까지 javascript에서는 생성자로 사용할 함수를 정의하고 new 키워드를 통해 인스턴스를 생성하는 방식을 사용했습니다.
function Person(name, age) {
this.name = name;
this.age = age;
}
const jony = new Person("jony", 7);
console.log(jony);
아래는 위 코드의 실행 결과입니다.
여기서 주목해야 할 점은 콘솔에 찍힌 출력 결과입니다. Person 함수에는 분명히 아무 값도 return 하지 않는데 jony 변수에는 name, age 프로퍼티를 가지는 객체가 저장되었습니다. 그 이유는 바로 new 키워드 때문입니다.
new 키워드는 인스턴스를 생성할 때 사용됩니다.
MDN 문서에 따르면 new 키워드는
new constructor[([arguments])]
형태로 사용합니다.
new 키워드를 사용하여 constructor 함수를 호출하면 constructor 함수의 prototype을 물려받는 객체가 생성됩니다. prototype에 대해서는 아래에서 설명하겠습니다.
그 후 constructor 함수가 실행되는데 이때 constructor 함수의 this는 new 키워드로 생성된 객체가 됩니다. constructor 함수가 종료되면 새로 생성된 객체가 리턴됩니다. 이 객체가 클래스의 인스턴스로 사용되는 것입니다.
그렇기 때문에 Person 함수에서 아무것도 리턴하지 않았지만 Person 함수 내부에서 정의한 프로퍼티가 포함된 객체가 jony 변수에 저장된 것입니다.
이번엔 ES6에서 도입된 class 키워드를 이용해 위 예제와 똑같은 인스턴스를 생성해보겠습니다.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const jony = new Person('jony', 7);
console.log(jony);
아래는 위 코드의 실행 결과입니다.
위 예제와 정확히 같은 결과를 출력하고 있습니다.
그 이유는 class 키워드도 예전에 사용하던 방식을 그대로 사용하고 있기 때문입니다. class 키워드로 선언한 Person도 function 키워드로 선언한 Person과 같은 함수입니다. class 키워드는 class를 조금 더 직관적이고 편리하게 표현하기 위한 새로운 문법일 뿐입니다. function 키워드로 선언한 Person 내부의 코드가 class 키워드로 선언한 Person의 constructor() 함수 내부로 들어갔을 뿐 두 표현은 완전히 동일합니다.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
console.log(typeof Person); // function
function Person(name, age) {
this.name = name;
this.age = age;
}
console.log(typeof Person); // function
프로토타입(prototype)
이번엔 prototype에 대해 알아보겠습니다.
모든 함수에는 prototype이라는 특별한 프로퍼티가 존재합니다. 일반 함수에서는 prototype 프로퍼티가 의미가 없지만 new 키워드와 함께 constructor 함수로 사용된 함수에서는 매우 중요한 역할을 합니다.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
console.log(Person.prototype);
// 브라우저에서 { constructor: f() }
// node에서 Person{}
Person 클래스의 prototype을 출력해봤습니다. javascript에서는 클래스 역시 함수이기 때문에 prototype 프로퍼티를 가지고 있습니다. 위 결과에서 확인할 수 있듯이 prototype은 객체 타입의 프로퍼티입니다.
위에서 new 키워드로 생성된 객체는 constructor 함수의 prototype을 물려받는다고 설명했습니다. prototype을 물려받는다는 의미는 constructor 함수의 prototype이 새로 생성된 객체의 prototype이 된다는 뜻입니다. constructor 함수의 prototype은 새로 생성된 객체의 __proto__ 프로퍼티에 저장됩니다.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const jony = new Person('jony', 7);
console.log(jony.__proto__);
// 브라우저에서 { constructor: f() }
// node에서 Person{}
console.log(jony.__proto__ === Person.prototype); // true
jony 객체의 __proto__ 프로퍼티를 출력해보니 위에서 본 Person.prototype과 같은 출력 결과가 나옵니다. === 연산자를 통해 두 객체가 완전히 같은 객체임을 확인할 수 있습니다.
prototype의 중요한 점은 클래스의 메서드가 이 prototype 객체에 저장된다는 점입니다.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
hello() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const jony = new Person('jony', 7);
console.log(jony); // Person { name: 'jony', age: 7 }
console.log(jony.__proto__);
// 브라우저에서 { constructor: f(), hello: f() }
// node에서는 console 출력 시 prototype의 프로퍼티가 보이지 않음.
jony.hello(); // Hello, my name is jony.
Person 클래스에 hello() 메서드를 정의했지만 jony 변수에는 name과 age 프로퍼티만 들어있습니다. 그 이유는 클래스의 메서드가 prototype 객체에 저장되기 때문입니다.
클래스의 메서드가 prototype 객체에 저장되는 이유는 모든 인스턴스에서 클래스의 메서드를 공유하기 위해서입니다. new 키워드로 생성한 인스턴스들은 constructor 함수의 prototype 객체를 물려받기 때문에 모든 인스턴스가 prototype에 저장된 클래스의 메서드를 사용할 수 있습니다.
여기서 한 가지 이상한 점이 있습니다.
hello 메서드는 jony 객체에 들어있지 않고 jony.__proto__ 객체에 들어있는데 jony.__proto__. hello()가 아닌 jony.hello()로 hello 메서드를 호출할 수 있습니다. 이것이 가능한 이유는 javascript에서 prototype 객체가 동작하는 방식 때문입니다.
javascript에서는 객체의 프로퍼티에 접근하려고 할 때 해당 프로퍼티가 그 객체에 들어있는지 확인합니다. 만약 그 객체에 해당 프로퍼티가 없다면 그 객체의 prototype에서 해당 프로퍼티를 찾고 그 안에도 없다면 prototype의 prototype에서 해당 프로퍼티를 찾아갑니다. 최상위 prototype에서 해당 프로퍼티가 없을 때 javascript에서는 그 객체가 해당 프로퍼티에 접근할 수 없다고 판단하고 객체의 프로퍼티 값은 undefined가 됩니다.
이렇게 프로토타입이 연결되어 있는 것을 javascript의 프로토타입 체인(prototype chain)이라고 부릅니다. javascript에서는 프로토타입 체인을 이용해 상속을 구현하고 있습니다. 상속에 대해서는 다음 글에서 자세히 정리하겠습니다.
정적 메서드
이번에는 정적 메서드에 대해 알아보겠습니다. 클래스의 메서드 앞에 static 키워드가 붙으면 정적 메서드가 됩니다. 정적 메서드는 일반적인 클래스의 메서드와 다르게 this가 클래스 그 자체가 됩니다.
정적 메서드는 클래스와는 관련이 있지만 인스턴스와는 관련이 없는 범용적인 작업에 사용됩니다.
예를 들어, Car 클래스가 있고 자동차 식별 번호인 VIN을 생성하는 메서드를 생각해보겠습니다. 개별 인스턴스에서 VIN을 생성한다는 것은 불가능한 일입니다. 개별 인스턴스에서는 다른 인스턴스의 VIN을 알 수 없기 때문입니다. 따라서 이 메서드는 정적 메서드로 만들어야 합니다.
class Car {
static getNextVin() {
return this.nextVin++;
// this가 클래스 그 자체가 되기 때문에 정적 메소드라는 것을 알기쉽게 하기위해
// return Car.nextVin++; 과 같이 사용하는 것이 좋습니다.
}
constructor(make, model) {
this.make = make;
this.model = model;
this.vin = Car.getNextVin();
}
}
Car.nextVin = 0;
const car1 = new Car('KIA', 'K3');
const car2 = new Car('KIA', 'K5');
const car3 = new Car('Hyundai Motors', 'Sonata');
car1.vin; // 0
car2.vin; // 1
car3.vin; // 2
Car.nextVin; // 2
정적 메서드인 getNextVin() 메서드를 만들고 인스턴스가 생성될 때마다 호출하여 인스턴스의 vin을 부여하도록 했습니다. 정적 메서드에서는 this가 클래스 그 자체가 되기 때문에 getNextVin() 메서드 내부의 this.nextVin 프로퍼티는 Car.nextVin 프로퍼티가 됩니다.
정적 메서드에서 중요한 점은 정적 메서드가 클래스의 prototype 객체가 아닌 클래스 그 자체에 저장되기 때문에 인스턴스에서는 호출할 수 없다는 점입니다. 그렇기 때문에 정적 메서드는 인스턴스가 아닌 클래스에서 호출해야 합니다. 그래서 정적 메서드를 클래스 메서드라고도 부릅니다. static 키워드가 붙어있지 않은 일반적인 클래스의 메서드는 인스턴스 메서드라고도 부릅니다.
class Car {
static getNextVin() {
return Car.nextVin++;
}
constructor(make, model) {
this.make = make;
this.model = model;
this.vin = Car.getNextVin();
}
}
Car.nextVin = 0;
const car1 = new Car('KIA', 'K3');
Car.getNextVin(); // 1
car1.getNextVin(); // Uncaught TypeError: car1.getNextVin is not a function
이 글에 쓰인 코드들은 O'REILLY의 '러닝 자바스크립트' 책에 있는 코드를 참고했습니다.
'Javascript' 카테고리의 다른 글
Javascript의 실행환경과 동작방식 (0) | 2021.04.25 |
---|---|
Javascript의 iterable, iterator, generator (0) | 2021.04.06 |
프로토타입 체인을 이용한 Javascript의 상속 (0) | 2021.02.21 |
Javascript의 "this" (0) | 2021.01.21 |
클로저(closure)와 즉시실행함수(IIFE) (0) | 2020.01.26 |