[SEB_FE_45] 2023.05.12 / 프로토타입

Kay·2023년 5월 12일
0

📖 [강의 내용 및 개념 정리]
목차

  • 프로토타입? 어디서 온 단어일까?
  • 자바스크립트에서의 프로토타입
  • 함수의 prototype 프로퍼티
  • 프로토타입 체인

프로토타입? 어디서 온 단어일까?

medium - 자바스크립트는 왜 프로토타입을 선택했을까

이 글에서 저자는 자바스크립트의 "프로토타입 기반의 객체지향", "렉시컬스코프", "this", "스코프"와 같은 개념에 대해 한 논문을 참고하여 이야기를 풀어 설명해주십니다!

(*제 부족한 실력으로 논문을 요약하자면 다음과 같습니다.)

  • "클래스 기반의 객체지향 모델"과 "프로토타입 기반의 객체지향 모델"은 기원이 되는 학자의 이론이 다르다.
  • 그로 인해 각 모델의 특징과 언어적 특징이 다르다.

프로토타입에 대해 정리하기에 앞서 클로져에 대한 정리글의 시작 부분을 긁어왔다.

"클래스 기반의 객체지향 모델"과 "프로토타입 기반의 객체지향 모델"은 기원이 되는 학자의 이론이 다르며, 그로 인해 언어적 특징이 다르다고 하였다.
우선 각 이론에 대해 간략하게 이해를 하기 위해 간단한 예시를 들어보려고 한다.


아리스토텔레스의 분류학을 기반으로 동물을 분류한다면...?

  1. 동물을 분류할 기준을 정한다
  • 예를 들어, (1차 기준) 척추 뼈의 유무, (2차 기준) 새끼를 어떻게 낳는지 ...
  1. 각 분류에 맞추어 동물들을 분류한다.

Rosch의 프로토타입 이론을 기반으로 동물을 분류한다면...?

  1. 각 동물들의 특징들을 먼저 살펴본다.
  • 예를 들어, 참새는 날개가 있는데 강아지는 날개가 없네 ...
  1. 원형(prototype)을 찾아간다.

  2. 원형은 시대적 맥락에 따라 달라질 수 있다.


정확하지는 않은 예시이겠지만 아마 이런 느낌일 것 같다.

원형(prototype)이라는 것은 완전한 것이 아닌 상황과 맥락에 따라 달라질 수 있으며 많은 경험을 축적하여 최고의 원형을 찾는 것이 목표인 것이다.

두 이론을 다르게 느껴지게 하는 가장 큰 차이점은 "프로토타입"이며, 자바스크립트가 유연한 언어라는 특성을 가지게 된 이유 중 하나일 것이다.

자바스크립트에서의 프로토타입

자바스크립트에서 프로토타입이라는 단어가 왜 쓰이는지 그리고 어떤 특성을 가지고 있는지에 대한 대충의 감을 잡았다.

그렇다면 자바스크립트에서 프로토타입은 무슨 의미일까?

mdn - Object Prototypes
JavaScript는 흔히 프로토타입 기반 언어(prototype-based language)라 불립니다.— 모든 객체들이 메소드와 속성들을 상속 받기 위한 템플릿으로써 프로토타입 객체(prototype object)를 가진다는 의미입니다.
프로토타입 객체도 또 다시 상위 프로토타입 객체로부터 메소드와 속성을 상속 받을 수도 있고 그 상위 프로토타입 객체도 마찬가지입니다.
이를 프로토타입 체인(prototype chain)이라 부르며 다른 객체에 정의된 메소드와 속성을 한 객체에서 사용할 수 있도록 하는 근간입니다.

정확히 말하자면 상속되는 속성과 메소드들은 각 객체가 아니라 객체의 생성자의 prototype이라는 속성에 정의되어 있습니다.

객체에 정의된 것과 객체의 생성자의 prototype 속성에 정의된 것의 차이

mdn을 보면 많은 내장함수들 중 예를들어 Array.from()Array.prototype.filter()가 있다.

Array.from()

유사 배열 객체나 반복 가능한 객체를 얕게 복사해 새로운 Array 객체를 만드는 함수

Array.from('foo'); // ['f', 'o', 'o']

Array.prototype.filter()

const arr = [1, 2, 3, 4, 5];
// const arr = new Array(1, 2, 3, 4, 5)
const newArr = arr.filter(v => v % 2  === 0);
console.log(newArr); // [2, 4]

Array.from()객체에 정의된 것이기 때문에 반드시 Array 객체의 from()이라는 내장 함수를 호출해야 한다.
즉, 생성자에서만 사용할 수 있으며, 인스턴스 객체에 from()이라는 함수를 사용할 수 없다.

하지만, prototype 속성에 정의된 것은 인스턴스 객체에서 접근이 가능하다.

프로토타입

javascript info - 프로토타입 상속

자바스크립트의 객체는 명세서에서 명명한 [[Prototype]]이라는 숨김 프로퍼티를 갖습니다. 이 숨김 프로퍼티 값은 null이거나 다른 객체에 대한 참조가 되는데, 다른 객체를 참조하는 경우 참조 대상을 '프로토타입(prototype)'이라 부릅니다.

[[Prototype]] 프로퍼티는 내부 프로퍼티이면서 숨김 프로퍼티이지만 다양한 방법을 사용해 개발자가 값을 설정할 수 있습니다.
아래 예시처럼 특별한 이름인 __proto__을 사용하면 값을 설정할 수 있습니다.

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal;

자바스크립트가 만들어졌을 당시엔 프로토타입 기반 상속이 자바스크립트의 주요 기능 중 하나였습니다.
그런데 과거엔 프로토타입에 직접 접근할 수 있는 방법이 없었습니다. 그나마 믿고 사용할 수 있었던 방법은 이번 챕터에서 설명할 생성자 함수의 "prototype" 프로퍼티를 이용하는 방법뿐이었죠. 많은 스크립트가 아직 이 방법을 사용하는 이유가 여기에 있습니다.

(__proto__를 사용하여 부모 클래스의 속성과 메서드를 변경할 수 없으며, prototype을 사용하여 접근해야 함)

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal; // (*)

// 프로퍼티 eats과 jumps를 rabbit에서도 사용할 수 있게 되었습니다.
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true

(*)로 표시한 줄에선 animal이 rabbit의 프로토타입이 되도록 설정하였습니다.

(**)로 표시한 줄에서 alert 함수가 rabbit.eats 프로퍼티를 읽으려 했는데, rabbit엔 eats라는 프로퍼티가 없습니다. 이때 자바스크립트는 [[Prototype]]이 참조하고 있는 객체인 animal에서 eats를 얻어냅니다.

프로토타입은 읽기 전용

프로토타입은 프로퍼티를 읽을 때만 사용합니다.
프로퍼티를 추가, 수정하거나 지우는 연산은 객체에 직접 해야 합니다.
메서드는 공유되지만, 객체의 상태는 공유되지 않습니다.

let user = {
  name: "John",
  surname: "Smith",

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

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

let admin = {
  __proto__: user,
  isAdmin: true
};

alert(admin.fullName); // John Smith (*)

// setter 함수가 실행됩니다!
admin.fullName = "Alice Cooper"; // (**)

alert(admin.fullName); // Alice Cooper, setter에 의해 추가된 admin의 프로퍼티(name, surname)에서 값을 가져옴
alert(user.fullName); // John Smith, 본래 user에 있었던 프로퍼티 값

this가 나타내는 것

this는 프로토타입에 영향을 받지 않기 때문에 "암시적 바인딩"에 의해 해당 함수를 호출한 객체, 즉 콘텍스트 객체에 바인딩된다.

프로퍼티 순회하기

for ... in

for..in은 객체 자신의 키와 상속 프로퍼티의 키 모두를 순회합니다.

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};


for(let prop in rabbit) alert(prop); // jumps, eats

Object.keys()

Object.keys는 객체 자신의 키만 반환합니다.

alert(Object.keys(rabbit)); // jumps

Object.prototype.hasOwnProperty()

Object.prototype.hasOwnProperty()는 상속 프로퍼티가 아니고 해당 객체에 직접 구현되어 있는 프로퍼티일 때만 true를 반환합니다.

for(let prop in rabbit) {
  let isOwn = rabbit.hasOwnProperty(prop);

  if (isOwn) {
    alert(`객체 자신의 프로퍼티: ${prop}`); // 객체 자신의 프로퍼티: jumps
  } else {
    alert(`상속 프로퍼티: ${prop}`); // 상속 프로퍼티: eats
  }
}

참고) 상속 프로퍼티에 Object.prototype.hasOwnProperty()는 보이지 않는 이유

  • Object.prototype에 있는 모든 메서드의 열거가능 여부를 의미하는 enumerable 플래그는 false

함수의 prototype 프로퍼티

let animal = {
  eats: true
};

function Rabbit(name) {
  this.name = name;
}

Rabbit.prototype = animal;

let rabbit = new Rabbit("흰 토끼"); //  rabbit.__proto__ == animal

alert( rabbit.eats ); // true

참고로 일반 객체엔 "prototype" 프로퍼티를 추가해도 아무런 일이 일어나지 않습니다.
모든 함수는 기본적으로 F.prototype = { constructor : F }를 가지고 있으므로 "constructor" 프로퍼티를 사용하면 객체의 생성자를 얻을 수 있습니다.

let user = {
  name: "John",
  prototype: "Bla-bla" // 마술은 일어나지 않습니다.
};

프로토타입 체인

let arr = [1, 2, 3];

// arr은 Array.prototype을 상속받았나요?
alert( arr.__proto__ === Array.prototype ); // true

// arr은 Object.prototype을 상속받았나요?
alert( arr.__proto__.__proto__ === Object.prototype ); // true

// 체인 맨 위엔 null이 있습니다.
alert( arr.__proto__.__proto__.__proto__ ); // null

프로토타입 체이닝의 제약사항

  • 순환 참조(circular reference)는 허용되지 않는다.
  • __proto__를 이용해 닫힌 형태로 다른 객체를 참조하면 에러가 발생한다.
  • __proto__의 값은 객체나 null만 가능하며, 다른 자료형은 무시된다.
  • 객체는 두 개의 객체를 상속받지 못한다.

원시값

문자열과 숫자, 불린값은 객체가 아닙니다. 그런데 이런 원시 타입 값의 프로퍼티에 접근하려고 하면 내장 생성자 String, Number, Boolean을 사용하는 임시 래퍼(wrapper) 객체가 생성됩니다. 임시 래퍼 객체는 이런 메서드를 제공하고 난 후에 사라집니다.

래퍼 객체는 보이지 않는 곳에서 만들어지는데 엔진에 의해 최적화가 이뤄집니다. 그런데 명세서엔 각 자료형에 해당하는 래퍼 객체의 메서드를 프로토타입 안에 구현해 놓고 String.prototype, Number.prototype, Boolean.prototype을 사용해 쓰도록 규정합니다.

0개의 댓글