프로토타입 기반 상속

HyunHo Lee·2022년 1월 21일
1

JavaScript

목록 보기
17/20
post-thumbnail

Property란?

속성 이란 뜻으로, JavaScript에서는 객체의 내부 속성을 의미한다.

let obj = {name: "이현호"}

객체를 생성하며 프로퍼티를 할당하고있다. 이제 프로퍼티에 접근해보자.

obj["name"];
obj.name;

대괄호나 점을 이용하여 프로퍼티에 접근할 수 있다. 보통은 점을 이용한 접근을 더 많이 사용하는 것 같다. 출력은 둘다 이현호로 동일하다.

let EngName = "Hyun Ho";

EngName.length를 콘솔 로그 찍어보면 7이 출력된다. 우리는 문자열에 length라는 프로퍼티가 있기 때문에 편리한 기능인 length를 사용할 수 있다.


플래그

프로퍼티에는 value외에도 flag라고 불리는 세 가지 속성이 있다.

  • writable : true면 값을 수정할 수 있고, false면 읽기만 가능하다.
  • enumberable : true면 반복문으로 나열이 가능하고, false면 불가능하다.
  • configurable : true면 프로퍼티의 삭제나 flag수정이 가능하고, false면 불가능하다.

flag의 기본값은 모두 true이고, 특별한 경우에는 설정하지 않으므로 궁금한 부분은 프로퍼티 Flag와 설명자 에서 확인하자.


프로퍼티 getter와 setter

객체의 프로퍼티는 두 가지 종류가 있다.

  1. 데이터 프로퍼티(data property)
    보통 우리가 사용하는 프로퍼티는 모두 데이터 프로퍼티이다.

  2. 접근자 프로퍼티(accessor property)
    접근자 프로퍼티의 본질은 함수다. 이 함수는 값을 획득(get)하고 설정(set)하는 역할을 담당한다. 하지만 외부에서 사용할때는 일반적인 프로퍼티처럼 사용한다.


getter

let user = {
    name: "이",
    surname: "현호",
  
    get fullName() {
      return `${this.name} ${this.surname}`;
    }
  };
  
  console.log(user.fullName); // 이 현호

getter는 객체를 어떤 값으로 불러오기위해 사용한다. 현재 user.fullName으로 이 현호를 불러오고 있다.

접근자 프로퍼티를 사용하면 함수처럼 호출하는것이 아니라 일반 프로퍼티에서 값에 접근하는 것처럼 'user.fullName'을 사용한다. user.fullName() 이 아니다.


setter

let user = {
  name: "이",
  surname: "현호",

  get fullName() {
    return `${this.name} ${this.surname}`;
  },

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  }
};

user.fullName = "HyunHo Lee";

console.log(user.name);
console.log(user.surname); 

이번에는 값을 할당해보자. fullName의 값을 할당하기 위해서는 set이 있어야 한다. (없으면 에러난다.)

위의user.fullName = "HyunHo Lee"; 는 setter로 값을 설정해주고 있는 것이다.

콘솔 로그를 getter로 찍어보면 HyunHoLee가 정상적으로 출력된다.


접근자 프로퍼티(getter, setter) 특징

이와 같이 getter와 setter 메서드를 구현하면 객체에는 읽고 쓰기가 가능하지만 실제로 존재하지 않는 fullName이란 가상의 프로퍼티가 생성된다.

데이터 프로퍼티는 value와 writable, enumberable, configurable라는 3개의 flag가 있었다.(설명자)
접근자 프로퍼티에는 get, set, enumberable, configurable 라는 설명자를 갖고있다.


getter, setter 활용

let user = {
  get name() {
    return this._name;
  },

  set name(value) {
    if (value.length < 2) {
      console.log("입력하신 이름이 2글자보다 적습니다.");
      return;
    }
    this._name = value;
  }
};

user.name = "이현호";
console.log(user.name);

user.name = "이";

getter와 setter을 실제 프로퍼티 값을 감싸는 wrapper처럼 활용할 수 있다. 전체적인 코드 동작은 짧은 이름을 잡아내는 것이다.

user.name = "이현호"; 부분을 보자. 먼저 get으로 인해 this._name"이현호" 가 될 것이다. 그리고 설정에 맞게 리턴하기 위해 set으로 가서 if문을 확인한다. 2글자 미만이 아니므로 this._name = value; 이 된다. 그러므로 콘솔 로그를 찍어보면 이현호가 출력되는 것이다. 하지만 user.name = "이";의 경우에는 set 의 if문에 걸리게 되고 입력하신 이름이 2글자보다 적습니다. 이 출력되며 빈 return값을 가지게 된다.

tip) user._name으로 외부에서 이름에 바로 접근할 수 있지만 _ 으로 시작하는 프로퍼티는 객체 내부에서만 활용하고, 외부에서는 건드리지 않는 것이 관습이다.


프로토타입

Java는 Class 기반 객체 지향 프로그래밍 언어이다. Class는 상속을 위해 사용하는데, 객체 지향 프로그래밍의 꽃이라고도 볼 수 있다.

JavaScript도 객체 지향 프로그래밍 언어이다. 하지만 JavaScript는 Class라는 개념이 없다. ES6에서 Class 문법이 추가되었지만 JavaScript는 프로토타입 기반 객체 지향 프로그래밍이므로 동작 방식이 약간 다르다.

다른 언어에서 Class를 상속을 위해 사용하듯이 프로토타입도 상속을 구현하기 위해 사용하고 있다.

자바스크립트에는 Prototype Object와 Prototype Link가 존재하고, 이 둘을 합쳐 프로토타입이라고 부른다.

Prototype Object

function Animal() {}
let animalObject = new Animal();

객체 생성은 함수를 통해 이루어진다. animalObject는 Animal 함수에 new 키워드를 사용하여 생성된 객체이다.

우리가 자주 사용하는 let obj = {}도 사실은 let obj = new Object{}이고, let arr = []let arr = new Array() 와 같다. 객체도 배열도 사실은 함수로 정의되어 있다는 것이다.


함수를 정의하면 Constructor 자격이 부여가 되어 new 키워드를 사용할 수 있게 되면서 해당 함수의 프로토타입 객체도 생성이 된다.

생성된 함수에는 prototype이라는 속성이 존재하는데, 이 속성으로 함수의 프로토타입 객체인 Animal Object에 접근할 수 있게 된다. 확인한 결과 함수의 프로토타입 객체는 constructor__proto__를 가지고 있다.

위의 사진에서는 __proto__가 아닌 [[Prototype]] 가 있는데, __proto__는 ES6에서 더이상 사용되지 않기 때문이다.

function Animal() {}

Animal.prototype.dogs = 2;
Animal.prototype.cats = 0;

Animal함수의 프로토타입 객체에 속성을 추가해주고 있다.

Animal.prototype으로 Animal 함수의 프로토타입 객체를 조회해본 결과 cats와 dogs 속성이 생성된 것을 볼 수 있다.


function Animal() {}

Animal.prototype.dogs = 2;
Animal.prototype.cats = 0;

let animalObject = new Animal();

[[Prototype]]은 모든 객체가 빠짐없이 가지고 있는 속성이고, 객체가 생성될 때 조상이었던 함수의 Prototype Object를 가리킨다.

animalObject은 Animal 함수로 생성되었으므로 조상이었던 함수의 프로퍼티 객체인 Animal Prototype Object를 가리키게 되는 것이다.

ES6 부터는 __proto__를 사용하지 않는다고 했으므로 Object.getPrototypeOf(변수명) 로 접근해서 확인해봐도 된다.

animalObject가 dogs를 사용하고 싶다고 가정해보자. dogs를 직접 갖고 있는것이 아니므로 해당 속성을 찾을때 까지 상위 프로토타입을 탐색한다. 최상위인 객체의 prototype object 까지도 찾지 못한다면 undefined를 리턴하고, 찾는다면 값을 리턴한다. 이런 형태를 프로토타입 체인이라고 부른다.

프로토타입 체인 구조 때문에 모든 객체는 Object의 자식이라고 불린다. 그리고 Object의 Prototype Object에 있는 모든 속성을 사용할 수 있다. 이 예시와 [[Prototype]]에 대해 더 알아보고 싶으면 밑의 글을 더 읽어보자.


프로토타입 깨알 요약

자바스크립트는 객체지향 프로그래밍 언어이지만 객체지항 프로그래밍 에서 사용하는 클래스가 없다. 클래스는 상속을 위해 사용하는데, 자바스크립트는 클래스 대신 프로토타입을 사용한다. 자바스크립트의 객체는 [[Prototype]]이라는 숨김 프로퍼티를 갖는다. 이 숨김 프로퍼티는 null값이거나 다른 객체에 대한 참조가 된다. 다른 객체를 참조하는 경우 참조하는 대상을 프로토타입이라고 부른다. 객체에서 특정 속성을 호출하면 프로퍼티에서 찾는데, 만약에 없다면 프로토타입에서 프로퍼티를 찾는 특징이 있다. 이것을 프로토타입 상속이라고 하며, [[Prototype]] 프로퍼티를 통해 상위 프로토타입과 연결되어있는 형태는 프로토타입 체인이라고 부른다.


[[Prototype]] 좀 더 쉽게..

let a = [1, 2, 3, 4];
a.sort((a, b) => a - b);
a;

배열 a에 내장함수 sort를 사용하여 정렬했다. 출력을 상세하게 분석해보자.

a에는 정렬이 적용되어 [1, 2, 3, 4] 으로 저장되어있다. 그런데 저장되어 있는 값 외에 [[Prototype]]라고 되어있는게 보인다.

토글을 눌러보니 수 많은 내장함수들이 나온다. 그 중에 sort도 있다. 배열안에는 [[Prototype]]이 존재하는데, 이곳에 sort 내장함수가 존재하기 때문에 a.sort((a, b) => a - b);와 같이 sort함수를 사용할 수 있는것이다.


a.toString()

이번에는 toString()을 사용해보자. 출력은 '1,2,3,4'이다. 하지만 위에서 [[Prototype]]안에 함수들을 보면 toString()은 눈을씻고 찾아봐도 없다. 어떻게 사용하는 걸까?


배열 안에 [[Prototype]] 안에있는 [[Prototype]]에 toString()이 있다. 그래서 사용가능한 것이다.

이것이 바로 프로토타입 체인 구조를 가지고 있는 자바스크립트의 특징때문에 가능한 일이다. 모든 객체는 Object의 자식이라고 불리고, 그러므로 Object의 Prototype Object에 있는 모든 속성을 사용할 수 있는 것이다.


클래스

클래스 : 공장
인스턴스 : 공장에서 나오는 생산품

class User {

  constructor(name) {
    this.name = name;
  }

  sayHi() {
    console.log(this.name);
  }

}

let user = new User("Hyunho");
user.sayHi();

Hyunhoconstructor의 파라미터로 들어간다. this.name = 'Hyunho'가 된 것이다. 그래서 sayHi()을 호출하면 Hyunho가 출력될 것이다.


typeof User
// 'function'

User의 타입은 'function'이다. class는 function인 것이다.


class User {
  name = "Hyunho";
  #age = 26;

  sayHi() {
    console.log(`Hello, ${this.name}!`);
  }
}

let me = new User()
console.log(me) // User {name: 'Hyunho', #age: 26}

class는 #을 사용하기도 한다. 위 사진에서 보여지는 것처럼 age에는 접근할 수 없다.


  • 상속
class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed = speed;
    console.log(`${this.name} 은/는 속도 ${this.speed}로 달립니다.`);
  }
  stop() {
    this.speed = 0;
    console.log(`${this.name} 이/가 멈췄습니다.`);
  }
}

let animal = new Animal("동물");

class Rabbit extends Animal {
  hide() {
    console.log(`${this.name} 이/가 숨었습니다!`);
  }
}

let rabbit = new Rabbit("흰 토끼");

rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다.
rabbit.hide(); // 흰 토끼 이/가 숨었습니다!

rabbit은 이제 hide(), run(), stop()를 가지고 있다.


  • 메서드 오버라이딩
class Rabbit extends Animal {
  stop() {
    // rabbit.stop()을 호출할 때
    // Animal의 stop()이 아닌, 이 메서드가 사용됩니다.
  }
}

rabbit에서 가장 가까운 [[Prototype]]의 stop() 함수는 rabbit의 stop()이다. 그 밑에 Animal의 stop() 함수가 있기때문에, 가장 가까운 rabbit의 stop()함수가 실행되는 것이다.

function add(x, y){
	return x + y;
}

function add(x, y, z){
	return x + y + z;
}

console.log(add(1, 2)); // NaN
console.log(add(1, 2, 3)); // 6

오버로딩은 Javascript에 존재하지 않는다.


  • 접근
class CoffeeMachine {
  #waterLimit = 200;

  #checkWater(value) {
    if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
    if (value > this.#waterLimit) throw new Error("물이 용량을 초과합니다.");
  }

}

let coffeeMachine = new CoffeeMachine();

// 클래스 외부에서 private에 접근할 수 없음
coffeeMachine.#checkWater(); // Error
coffeeMachine.#waterLimit = 1000; // Error






참고 : JavaScript 200제, 모던 자바스크립트 튜토리얼, MDN, 오승환님

나중에 읽어 볼 글 : https://www.howdy-mj.me/javascript/prototype-and-proto/

profile
함께 일하고 싶은 개발자가 되기 위해 달려나가고 있습니다.

0개의 댓글