JS식 객체지향 프로그래밍(OOJS)을 알아보기 전에, 먼저 객체지향 프로그래밍에 대해 알아보자.
인간 중심적 프로그래밍 패러다임. 즉, 현실 세계를 프로그래밍으로 옮겨와 프로그래밍하는 것을 말한다. 현실 세계의 사물들을 객체라고 보고, 그 객체로부터 개발하고자 하는 애플리케이션에 필요한 특징들을 뽑아와 프로그래밍하는 것이다.
*객체 - 데이터와 기능이 class로 '캡슐화'된 컴퓨터 자원의 묶음.
추상화
: 개별 기능의 목적에 맞는 이름을 임의로 붙일 수 있다. 프로그래머의 의도에 맞추어 가장 중요한 것들만을 뽑아서 복잡한 것들을 보다 단순한 모델로 변환하는 작업.
캡슐화 / 은닉성
: 내부 구조는 private으로 감춰놓고, 외부에서 조작할 수 있는 명령어만 public으로 공개한다.
바깥의 간섭으로 발생할 수 있는 오류들을 방지할 수 있으며, 남이 만든 객체들도 내부를 뜯어보지 않고 사용 가능하다. (남이 쓴 코드 이해하기 힘들수도)
상속
: 기존 부모 클래스(비교적 추상적)를 상속받아 새로운 자식 클래스(비교적 구체적)를 추가할 수 있다. 부모 생성자의 기능을 물려 받으면서, 새로운 기능을 추가.
다형성
: 여러 객체 타입에 같은 기능을 정의할 수 있는 능력. 상속을 받은 기능을 변경/확장하는 것. 부모 클래스에서 정의된 메소드의 작업이 자식 클래스에서 다른 것으로 override(대체)될 수 있다.
: 자바스크립트는 Class 대신, 기존의 객체를 복사하여 새로운 객체를 생성하는 prototype 언어이다.
(ES2015부터 class 문법을 지원하기 시작했지만 그냥 syntax sugar일 뿐, 자바스크립트는 여전히 prototype 기반 언어이다.)
자바스크립트에서 객체가 정의될 때,
const obj = {a: 1}
=> Object.prototype을 가짐
const arr = ['object', 'too']
=> Array.prototype을 상속받음
function f() {return 2}
=> Function.prototype을 상속받음
객체를 생성하는 함수를 생성자 함수라고 부르며, 객체와 그 기능을 정의한다. 대문자로 시작하는게 규칙이다.
실제 쿠키(객체)를 만들 수 있는 쿠키틀(생성자 함수)이라고 생각하면 된다. 생성자로부터 새로운 객체 인스턴스가 생성되면, 객체의 핵심 기능이 프로토타입 체인에 의해 연결된다.
function Cookie(flavor, deco) {
this.flavor = flavor;
this.deco = deco;
this.description = function() {
console.log(this.flavor + ' cookie with ' + this.deco);
}
}
Cookie라는 생성자를 만들고, 이 쿠키틀이 가져야 할 속성을 this
으로 정의해준다. this
는 생성자 함수 자신을 가리키며, 여기에 저장된 것들은 new를 통해 생성된 객체에 그대로 적용된다.
이제 이 Cookie 생성자를 바탕으로 실제 쿠키 객체를 만들 수 있다! 실제 쿠키를 만들 때는
new를 사용해 생성자 함수를 호출하면 된다.
const 객체명 = new 생성자(인자)
const choco = new Cookie('chocolate', 'buttons');
const berry = new Cookie('strawberry', 'white cream');
choco.description(); // chocolate cookie with buttons
berry.description(); // strawberry cookie with white cream
Cookie 생성자로 choco와 berry라는 두 개의 쿠키 객체를 만들어보았다. 내부 속성과 메서드는 그대로 적용된다.
두 가지 관점에서 프로토타입을 설명하자면,
1) 생성자 함수 관점
: 생성자 함수로부터 생성된 모든 객체가 공유할 원형. 말 그대로 원형이라는 뜻으로, 같은 생성자를 통해 만들어진 객체들은 모두 이 원형 객체를 공유한다.
2) 객체 관점
: 각각의 객체가 가지는 은닉(private)속성 [[prototype]]. 자신의 프로토타입(원형, 상위단계)이 되는 다른 객체를 가리킨다. 그 객체의 프로토타입 또한 프로토타입을 가지고 있고 이것이 반복되다, 결국 null을 프로토타입으로 가지는 오브젝트에서 끝난다. null은 더 이상의 프로토타입이 없다고 정의되며, 프로토타입 체인의 종점 역할을 한다.
: JS 객체는 속성을 저장할 때, 자기만의 속성과 숨은링크(프로토타입 객체에 대한)를 가진다. 객체의 속성에 접근하려할 때, 객체 내 자체 속성뿐만 아니라, 객체의 프로토타입, 그 프로토타입의 프로토타입 체인의 종단에 이를 때까지 속성을 탐색한다.
기존의 Cookie 생성자의 description을 프로토타입을 사용하여 정의(생성자명.prototype.정의할것
)하고, 객체의 속성에 접근해보자.
function Cookie(flavor, deco) {
this.flavor = flavor;
this.deco = deco;
Cookie.prototype.description = function() {
console.log(this.flavor + ' cookie with ' + this.deco);
}
}
const choco = new Cookie('chocolate', 'buttons');
const berry = new Cookie('strawberry', 'white cream');
console.log(choco.flavor); // chocolate
// choco는 flavor라는 속성을 가진다. 프로토타입 역시 flavor속성을 가지고 있지만, 이 값은 쓰이지 않는다. (속성의 가려짐)
console.log(choco.deco); // buttons
// choco는 deco라는 속성을 가진다. 프로토타입 역시 deco속성을 가지고 있지만, 이 값은 쓰이지 않는다. (속성의 가려짐)
console.log(choco.description);
// choco는 description이라는 속성을 가지지 않음.
// choco.[[prototype]]을 확인해보니 description이라는 속성가짐. 값인 chocolate cookie with buttons 반환
console.log(choco.price);
// choco는 price이라는 속성을 가지지 않음.
// choco.[[prototype]]도 price이라는 속성을 가지지 않음.
// choco.[[prototype]].[[prototype]]은 Object.prototype
// choco.[[prototype]].[[prototype]].[[prototype]]은 null
// 찾는 것을 그만두고 undefined 반환
this.description
대신 Cookie.prototype.description
를 사용한 것인데,
this
보다 프로토타입을 사용하는게 더 효율적이다. 프로토타입은 모든 객체가 공유하고 있어서 한 번만 만들어지지만(재사용 가능), this에 넣은 속성은 객체를 만들 때마다 함수도 하나씩 만들어져서 메모리가 낭비되기 때문이다.
프로토타입 체인을 살펴보면,
{flavor: choco, deco: buttons} ---> {flavor: flavor, deco: deco} ---> Object.prototype --> null
: JS에 메소드라는 것은 없지만, 객체의 속성으로 함수를 지정할 수 있고, 속성 값을 사용하듯 쓸 수 있다. (method overriding)
const 객체명 = Object.create(생성자)
Object.create()
를 호출해 새로운 객체를 만들고, 속성을 overriding해보자.
const calculate1 = {
a: 2,
square: function(b) {
return this.a ** 2;
}
};
console.log(calculate1.square()); // 4
// this는 calculate1을 가리킴
const calculate2 = Object.create(calculate1);
// calculate2는 프로토타입을 calculate1으로 가지는 객체이다.
calculate2.a = 4; // calcultate2에 'a'라는 새로운 속성을 만듦. (method overriding)
console.log(calculate2.square()); // 16
// calculate2가 호출될 때, 'this'는 'calculate2'를 가리킨다.
// calculate1의 함수 square를 상속받으며, 'this.a'는 calculate2.a를 나타내고 이는 calculate2의 개인 속성 'a'가 된다.
__proto__
)실제 객체를 만들 때, 생성자의 프로토타입이 참조된 모습. (프로토타입이 제대로 구현되었는지 확인할 때 사용한다.) 사용자가 프로토타입을 작성하고, proto는 new를 호출할 때 프로토타입을 참조하여 자동으로 만들어짐.
function Cookie(flavor, deco) {
this.flavor = flavor;
this.deco = deco;
Cookie.prototype.description = function() {
console.log(this.flavor + ' cookie with ' + this.deco);
}
}
const choco = new Cookie('chocolate', 'buttons');
console.log(choco);
// {flavor: 'chocolate', deco: 'buttons',
// __proto__ : object}
console.log(choco.__proto__);
// {constructor: function Cookie(flavor, deco),
// description: function() {},
// __proto__: object}
__proto__
와 프로토타입의 내용은 같다고 생각하면 된다.
ES6부터 추가된 문법. 객체 생성자로 구현했던 코드를 깔끔하고, 쉽게 구현할 수 있다. class는 대문자로 시작, strict모드에서 실행됨.
프로토타입 기반 상속을 사용해 주어진 이름의 새로운 클래스를 만듦.
class 생성자명
class 내에서 객체를 생성하고, 초기화하기 위한 특별한 메서드.
class 생성자명 {constructor(){}}
'use strict';
class Polygon {
constructor(height, width) {
this.name = 'Polygon';
this.height = height;
this.width = width;
}
}
class를 다른 class의 자식으로 만들기 위해 사용.
class 자식클래스명 extends 부모클래스명 {}
부모 객체의 함수를 호출할 때 사용됨. (상속받고 싶은 것만 써주고, 나머지는 this로 추가)생성자에서 this가 사용되기 전에 호출되어야 한다.
super(...arg)
class Square extends Polygon {
constructor(sideLength) {
// sideLength와 함께 부모 클래스의 생성자를 호출
// Polygon의 height, width가 제공됨.
super(sideLength, sideLength);
// *super()는 반드시 this 사용 전에 호출해야 함.
this.name = 'Square';
}
get area() {
return this.height * this.width;
}
set sideLength(newLength) {
this.height = newLength;
this.width = newLength;
}
}
const square = new Square(2);
console.log(square);
// Square {height: 2, width: 2}
// area: 4,
// __proto__: Polygon
// area: 4
// constructor: class Square
// name: 'Square'
console.log(square.area); // 4
*쿠키 예제
// 부모 클래스 Cookie 생성
class Cookie {
constructor(flavor, deco) {
this.flavor = flavor;
this.deco = deco;
}
description() {
console.log(this.flavor + ' cookie with ' + this.deco);
}
}
// Cookie를 상속받을 자식 클래스 Choco 생성
class Choco extends Cookie {
// 밑의 arg와 함께 부모 클래스 생성자 호출
constructor(flavor, deco, taste) {
// super() 안의 arg만 상속받겠다
super(flavor, deco);
// 새로운 속성 생성
this.taste = taste;
}
// 기존 함수 overwriting
description() {
console.log(this.flavor + ' cookie is ' + this.taste);
}
}
// // Cookie를 상속받을 자식 클래스 Berry 생성
class Berry extends Cookie {
constructor(flavor, deco, price) {
super(flavor, deco);
this.price = price;
}
description() {
console.log(this.flavor + ' cookie is ' + this.price);
}
}
const choco = new Choco('chocolate', 'buttons', 'good');
const berry = new Berry('strawberry', 'white cream', '$2');
choco.description(); // chocolate cookie is good
berry.description(); // strawberry cookie is $2
참조 문서: