[JS] 객체지향 자바스크립트 -상속과 프로토타입

돗개·2021년 1월 16일
3

crack JavaScript

목록 보기
16/18
post-thumbnail

JS식 객체지향 프로그래밍(OOJS)을 알아보기 전에, 먼저 객체지향 프로그래밍에 대해 알아보자.

객체지향 프로그래밍(OOP)

인간 중심적 프로그래밍 패러다임. 즉, 현실 세계를 프로그래밍으로 옮겨와 프로그래밍하는 것을 말한다. 현실 세계의 사물들을 객체라고 보고, 그 객체로부터 개발하고자 하는 애플리케이션에 필요한 특징들을 뽑아와 프로그래밍하는 것이다.

*객체 - 데이터와 기능이 class로 '캡슐화'된 컴퓨터 자원의 묶음.


특징

  • 추상화
    : 개별 기능의 목적에 맞는 이름을 임의로 붙일 수 있다. 프로그래머의 의도에 맞추어 가장 중요한 것들만을 뽑아서 복잡한 것들을 보다 단순한 모델로 변환하는 작업.

  • 캡슐화 / 은닉성
    : 내부 구조는 private으로 감춰놓고, 외부에서 조작할 수 있는 명령어만 public으로 공개한다.
    바깥의 간섭으로 발생할 수 있는 오류들을 방지할 수 있으며, 남이 만든 객체들도 내부를 뜯어보지 않고 사용 가능하다. (남이 쓴 코드 이해하기 힘들수도)

  • 상속
    : 기존 부모 클래스(비교적 추상적)를 상속받아 새로운 자식 클래스(비교적 구체적)를 추가할 수 있다. 부모 생성자의 기능을 물려 받으면서, 새로운 기능을 추가.

  • 다형성
    : 여러 객체 타입에 같은 기능을 정의할 수 있는 능력. 상속을 받은 기능을 변경/확장하는 것. 부모 클래스에서 정의된 메소드의 작업이 자식 클래스에서 다른 것으로 override(대체)될 수 있다.


장점

  • 재사용성 높아짐 (생산성↑)
  • 신뢰성 확보
  • 컴파일 단계에서 에러를 잡아낼 수 있어 버그발생이 줄어듦
  • 객체 단위로 코드가 나눠져 작성 => 디버깅이 쉽고 유지보수에 용이
  • 객체와 매핑하기 수월 => 요구사항을 명확히 파악 가능

단점

  • 메시지 교환을 통한 객체간 정보 교환 => overhead 발생
  • 객체가 상태를 가짐 => 변수가 존재 => 객체가 예측할 수 없는 상태를 갖게 됨 => 앱 내부에서 버그를 발생시킴 => 함수형 패러다임 등장

객체지향 자바스크립트(OOJS)

: 자바스크립트는 Class 대신, 기존의 객체를 복사하여 새로운 객체를 생성하는 prototype 언어이다.
(ES2015부터 class 문법을 지원하기 시작했지만 그냥 syntax sugar일 뿐, 자바스크립트는 여전히 prototype 기반 언어이다.)

자바스크립트에서 객체가 정의될 때,

  • 1) 생성자(constructor) 자격이 부여된다.
  • 2) 해당 객체의 prototype object 생성 및 연결
    - 1) prototype 속성 (Prototype Object) : 자신을 원형으로 하위로 물려줄 연결에 대한 속성. (자기만의 속성)
    - 2) 숨은 링크 (Prototype Link) : 상위에서 물려받은 객체의 prototype에 대한 링크.

*객체 생성법 (0) 문법 생성자

const obj = {a: 1} => Object.prototype을 가짐
const arr = ['object', 'too'] => Array.prototype을 상속받음
function f() {return 2} => Function.prototype을 상속받음


1) 생성자(constructor)

객체를 생성하는 함수를 생성자 함수라고 부르며, 객체와 그 기능을 정의한다. 대문자로 시작하는게 규칙이다.
실제 쿠키(객체)를 만들 수 있는 쿠키틀(생성자 함수)이라고 생각하면 된다. 생성자로부터 새로운 객체 인스턴스가 생성되면, 객체의 핵심 기능이 프로토타입 체인에 의해 연결된다.

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 생성자를 바탕으로 실제 쿠키 객체를 만들 수 있다! 실제 쿠키를 만들 때는

*객체 생성법 (1) 생성자 & new 연산자

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라는 두 개의 쿠키 객체를 만들어보았다. 내부 속성과 메서드는 그대로 적용된다.


2) 프로토타입

두 가지 관점에서 프로토타입을 설명하자면,

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)

*객체 생성법 (2) Object.create

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__)

실제 객체를 만들 때, 생성자의 프로토타입이 참조된 모습. (프로토타입이 제대로 구현되었는지 확인할 때 사용한다.) 사용자가 프로토타입을 작성하고, 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__와 프로토타입의 내용은 같다고 생각하면 된다.


3) class 문법

*객체 생성법 (3) class 키워드

ES6부터 추가된 문법. 객체 생성자로 구현했던 코드를 깔끔하고, 쉽게 구현할 수 있다. class는 대문자로 시작, strict모드에서 실행됨.


-class 선언

프로토타입 기반 상속을 사용해 주어진 이름의 새로운 클래스를 만듦.
class 생성자명

-constructor 메서드

class 내에서 객체를 생성하고, 초기화하기 위한 특별한 메서드.
class 생성자명 {constructor(){}}

'use strict';

class Polygon {
  constructor(height, width) {
    this.name = 'Polygon';
    this.height = height;
    this.width = width;
  }
}

-extends

class를 다른 class의 자식으로 만들기 위해 사용.
class 자식클래스명 extends 부모클래스명 {}

-super

부모 객체의 함수를 호출할 때 사용됨. (상속받고 싶은 것만 써주고, 나머지는 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

참조 문서:

profile
울보 개발자(멍.. 하고 울어요)

0개의 댓글