JavaScrip - 프로토타입01

김민재·2021년 8월 10일
1

TIL, Deep Javascript

목록 보기
22/22
post-thumbnail

*🔐Study Keyword :

🔑프로토타입 기반의 객체지향 프로그래밍 자바스크립트를 이해해보자.

-객체지향 프로그래밍
-상속과 프로토타입
-프로토타입 객체(__proto__ 접근자 프로퍼티 & 함수 객체의 prototype 접근자 프로퍼티 & 프로토타입의 constructor 프로퍼티와 생성자 함수)

- 프로토타입

  • 자바스크립트는 명령형, 함수형, 프로토 타입 기반 객체 지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어이다.
  • 자바스크립트는 객체 기반의 언어이며 자바스크립트를 이루고 있는 거의 '모든 것'이 객체이다.
    원시타입의 값을 제외한 나머지 값들(함수,배열,정규표현식 등)은 모두 객체다.

🤷‍♂️그렇다면 객체지향 프로그래밍이 그럼 뭘까?


1. 객체지향 프로그래밍

-WHAT IS❓

  • 객체 지향 프로그래밍프로그램을 명령어 또 함수 목록으로 보는 전통적인 명령형 프로그래밍의 절차 지향적 관점에는서 벗어나 여러 개의 독립적인 단위, 즉 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임을 말한다.
  • 객체 지향 프로그래밍실세계의 '🕵️‍♀️실체(사물이나 개념)를 인식하는 철학적 사고'를 '프로그래밍에 접목'하려는 시도에서 시작했다.
  • 🕵️‍♀️'실체'는 특징이나 성질을 나타내는 '속성' 을 가지고 있고 이를 통해 실체를 인식하거나 구별할 수 있다.
    🔹예시1) 사람은 이름, 주소, 성별, 나이 등 다양한 속성을 가진다.
    '이름은 김민재이며 주소는 서울인 사람'과 같이 속성을 구체적으로 표현하여 특정한 사람을 다른 사람과 구별하며 인식할 수 있다.
<script>
const person = {
    name: 'minjae',
    address: 'seoul'
}
</script>
  • 프로그래머는 이 방식을 통해 프로그래밍에 접목시켜 사람의 '이름'과 '주소'라는 속성에만 관심있는 객체 person을 구현해 다른 객체와 구별하여 인식할 수 있게 된다.
  • 이처럼 다양한 속성 중에 프로그램에 필요한 속성만 간추려 내어 표현하는 것추상화라고 한다.
  • 이렇게 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료구조를 '객체'라 하며 객체 지향 프로그래밍독립적인 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임이다.
    🔹예시2) 원이라는 개념을 객체로 만들면 원에는 반지름이라는 속성이 있고 반지름을 가지고 원의 지름, 둘레, 넓이를 구할 수 있는데 이때 반지름은 원의 1)'상태'를 나타내는 데이터이며 2)원의 지름, 둘레, 넓이를 구하는 것은 '동작'을 의미한다.
<script>
const circle = {
  radius: 5, // 반지름, '상태 데이터'인 프로퍼티
  getDiameter() { // 원의 지름 2r, 상태 데이터를 조작하는 '동작'인 메서드   		  return 2 * this.raidus;
  },
  getPerimeter() { // 원의 둘레 2πr
      return 2 * Math.PI * this.radius;
  },
  getArea() { // 원의 넓이 πrr
      return Math.PI * this.radius ** 2;
  }
}
console.log(circle) // {radius : 5, getDiameter: f, getPerimeter: f, getArea: f }
console.log(circle.getDiameter()) // 10
console.log(circle.getPerimeter()) // 31.41592653589793
console.log(circle.getArea()) // 78.53981633974483
</script>

=> 이처럼 객체 지향 프로그래밍은 객체의 1)상태를 나타내는 데이터2)상태 데이터를 조작할 수 있는 동작하나의 논리적 단위로 묶어 생각한다.

  • 객체1)상태 데이터와 2)동작을 하나의 논리적 단위로 묶은 복합적인 자료구조라고 할 수 있다.
  • 이때 상태 데이터를 프로퍼티라고 하며 동작을 메서드라고 부른다.
  • 객체는 고유의 기능을 갖는 독립적인 부품으로도 볼 수 있지만 자신의 고유한 기능을 수행하면서 다른 객체와의 관계성을 가질 수도 있다.
    🔹예시3) 3-1.다른 객체와 메시지를 주고 받거나 3-2.데이터를 처리하거나 3-3.다른 객체의 프로퍼티(상태 데이터)나 메서드(동작)💎상속받아 쓰는 경우도 있다.

2. 상속과 프로토타입

-WHAT IS❓

  • 💎상속객체지향 프로그래밍의 핵심 개념으로 어떤 객체의 1) 프로퍼티 또는 2) 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 의미한다.
  • 상속을 하면 기존의 1>코드를 적극 재사용하여 2>불필요한 중복을 제거하고 이러한 코드 재사용은 3>개발 비용을 현저히 줄여준다.

-PROBLME❓

  • 생성자 함수동일한 프로퍼티(메서드 포함) 구조를 갖는 객체를 생성할 때는 유용하지만 아래의 코드의 생성자 함수는 문제가 있다.

🔹예시4) Circle 생성자 함수가 생성하는 모든 객체
1) radius 프로퍼티와 2) getArea 메서드를 갖는다.

<script>
// 생성자 함수
function Circle(radius) {
  this.radius = radius;
  this.getArea = function() { 
  		return Math.PI * this.radius ** 2;
  	};
}
const circle1 = new Circle(1); // 반지름이 1인 인스턴스 생성
const circle2 = new Circle(2); // 반지름이 2인 인스턴스 생성
// Circle 생성자 함수는 인스턴스를 생성할 때마다
// 동일한 동작을 하는 getArea 메서드를 중복 생성해 몯모든 인스턴스가 중복 소유
// getArea 메서드는 하나만 생성하여 모든 인스턴스가 공유해 사용하는 것이 바람직
console.log(circle1.getArea===circle2.getArea) // false
console.log(circle1.getArea()) // 3.141592653589793
console.log(circle2.getArea()) // 12.566370614359172	
</script>
  • radius 프로퍼티 값은 인스턴스마다 다르지만 getArea 메서드는 모든 인스턴스가 동일한 내용의 메서드를 사용해 하나만 생성하여 모든 인스턴스를 공유하는게 더 바람직하다.

-BADS THINGS❓

  • 현재의 생성자 함수는 인스턴스를 생성할 때마다 getArea 메서드를 중복 생성해 모든 인스턴스가 중복으로 소유하므로 1) 메모리를 불필요하게 낭비하고 인스턴스를 생성할 때마다 2) 메서드를 생성하여 퍼포먼스에도 악영향을 준다.

-HOW TO SOLVE❔❕

이러한 문제를 상속을 통해 불필요한 중복을 제거할 수 있는데 자바스크립트는 '프로토타입'기반으로 상속을 구현한다.

  • Circle 생성자 함수가 생성한 인스턴스는 자신의 '프로토타입'인 상위(부모) 객체 역할을 하는 Circle.prototype의 모든 프로퍼티와 메서드를 상속받는다.
  • 이때 getArea 메서드는 하나만 생성되어 Circle.prototype의 메서드로 할당되고 Circle 생성자 함수가 생성하는 모든 인스턴스는 getArea 메서드를 상속받아 사용할 수 있다.
<script>
function Circle(radius) {
  this.radius = radius;
}
// Circle 생성자 함수가 생성한 인스턴스가 
// getArea 메서드를 공유해서 사용할 수 있도록 프로토 타입 추가
// 프로토타입은 Circle 생성자 함수의 prototype 프로퍼티에 바인딩되어 있다.
Circle.prototype.getArea = function() {
  		return Math.PI * this.radius ** 2;
}
// 인스턴스 생성
const circle1 = new Circle(1); // 반지름이 1인 인스턴스 생성
const circle2 = new Circle(2); // 반지름이 2인 인스턴스 생성
console.log(circle1.getArea===circle2.getArea) // true
console.log(circle1.getArea()) // 3.141592653589793
console.log(circle2.getArea()) // 12.566370614359172
</script>

  • 따라서 생성자 함수는 자신의 상태를 나타내는 radius 1)프로퍼티만 개별적으로 소유하고 내용이 동일한 2) 메서드는 상속을 통해 공유하여 사용한다.
  • 상속은 이처럼 코드의 '재사용' 관점에서 매우 유용하다.
    => 생성자 함수가 생성할 모든 인스턴스가 '공통적'을 사용할
    1) 프로퍼티나 2) 메서드를 프로토타입에 미리 구현
    하면 생성자 함수가 생성할 모든 인스턴스는 별도의 구현 없이 상위 객체인 프로토타입의 자산(프로퍼티나 메서드)을 공유하며 사용할 수 있다.

3. 프로토타입 객체 (3)

-WHAT IS❓

  • 프로토타입 객체(프로토타입)객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용된다.
  • 프로토타입어떤 객체의 상위(부모) 객체의 역할 하는 객체로서 다른 객체에 공유 프로퍼티(메서드 포함)를 제공한다.
  • 프로토타입상속받은 하위(자식) 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용할 수 있다.
  • 모든 객체는 [[prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조다.
  • [[prototype]]에 저장되는 '프로토타입'은 객체 생성 방식에 의해 결정된다.
    => 즉 객체가 생성될 때 객체 생성 방식에 따라 '프로토타입'이 결정되고 [[prototype]]에 저장된다.
    🔹예시5) 1>객체 리터럴에 의해 생성된 객체의 '프로토타입'은 Object.prototype이고
    2> 생성자 함수에 의해 생성된 객체의 '프로토타입'은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체다.

-객체와 프로토타입과 생성자함수의 관계

  • 모든 객체는 하나의 프로토타입을 갖고 모든 프로토타입은 생성자 함수와 연결되어 있는데 객체와 프로토타입과 생성자 함수는 서로 연결되어있음을 의미한다.
    - [[prototype]] 내부 슬롯에 직접 접근할 수는 없지만
    1__proto__접근자 프로퍼티를 통해 자신의 [[prototype]] 내부슬롯이 가리키는 프로토타입에 간접적으로 접근할 수 있다. '프로토타입'자신의 constructor 프로퍼티를 통해 생성자 함수에 접근할 수 있고 '생성자 함수'자신의 prototype 프로퍼티를 통해 프로토타입에 접근할 수 있다.

3_1. __proto__ 접근자 프로퍼티

▶ __proto__접근자 프로퍼티

  • 모든 객체는 __proto__접근자 프로퍼티를 통해 자신의 프로토타입, [[prototype]] 내부 슬롯에 간접적으로 접근할 수 있다
  • 위 그림 속 빨간 박스로 표시된 것이 person 객체의 프로토타입인 Object.prototype으로 person 객체는 __proto__접근자 프로퍼티를 통해 person 객체의 [[prototype]] 내부 슬롯이 가리키는 객체인 Object.prototype에 접근할 수 있다.

1> __proto__접근자 프로퍼티이다.

  • 내부슬롯은 프로퍼티가 아니여서 직접적으로 접근, 호출하지 못하고 간접적으로 접근할 수 있는 수단을 이용해야하는데 그 예시가 바로 __proto__ 접근자 프로퍼티를 통해 [[prototype]] 내부슬롯의 값인 프로토타입에 접근하는 것이다.
  • __proto__접근자 프로퍼티 자체적인 값(프로퍼티 어트리뷰트)를 갖지 않고 다른 데이터 프로퍼티의 1>값을 읽거나 2>저장할 때 사용하는 접근자 함수인 1> [[Get]]과 2>[[Set]] 프로퍼티 어트리뷰트로 구성된 프로터디이다.
  • Object.prototype의 접근자 프로퍼티인 __proto__는 1>getter/2>setter 함수인 접근자 함수(1>[[Get]],2>[[Set]] 프로퍼티 어트리뷰트에 할당된 함수)를 통해 [[prototype]] 내부 슬롯의 값인 프로토타입을 1>취득하거나 2>할당한다.
    1> __proto__접근자 프로퍼티를 통해 프로토타입에 1>접근하면 내부적으로 __proto__접근자 프로퍼티의 getter 함수인 [[Get]]이 호출되고
    2> __proto__접근자 프로퍼티를 통해 새로운 프로토타입을 2>할당하면 __proto__접근자 프로퍼티의 setter 함수인 [[Set]]이 호출된다.
<script>
const obj = {};
const parent = {x:1};
// getter 함수인 get __proto__ 호출되 obj 객체의 프로토타입 취득
obj.__proto__;
// setter 함수인 set __proto__ 호출되 obj 객체의 프로토타입 교체
obj.__proto__ = parent;
console.log(obj.x) // 1
</script>

2> __proto__ 접근자 프로퍼티는 상속을 통해 사용된다

  • __proto__ 접근자 프로퍼티 는 객체가 직접 소유한 프로퍼티가 아닌 📜Object.prototype의 프로퍼티모든 객체는 상속을 통해 Object.prototype.__proto__ 접근자 프로퍼티를 사용할 수 있다.
<script>
const person = {name:'kim'};
// person 객체는 __proto__ 프로퍼티를 소유하지 않는다.
console.log(person.hasOwnProperty('__proto__')) // false
// __proto__ 프로퍼티는 모든 객체의 프로토타입 객체인 Object.prototype의 접근자 프로퍼티이다.
console.log(Object.getOwnPropertyDescriptor(Object.prototype,'__proto__'))// { configurable: true, enumerable: false, get: f, set: f }
// 모든 객체는 Object.prototype의 접근자 프로퍼티인 __proto__ 를 상속받아 쓸수있다
console.log({}.__proto__ === Object.prototype); // true
</script>

📜Object.prototype

  • 모든 객체는 프로토타입의 계층 구조인 프로토타입 체인에 묶여있다.
    -자바스크립트 엔진은 객체의 프로퍼티(메소드 포함)에 접근하려할 때 해당 객체에 접근하려는 프로퍼티가 없다면 __proto__ 접근자 프로퍼티가 가리키는 참조를 따라 자신의 부모 역할을 하는 '프로토타입'프로퍼티를 순차적으로 검색한다.
  • 프로토타입 체인의 종점, 최상위 객체가 바로 Object.prototype이 객체의 프로퍼티와 메서드를 모든 객체가 상속받는다.

3> __proto__ 접근자 프로퍼티를 통해 프로퍼티에 접근하는 이유

  • [[prototype]] 내부 슬롯의 값인 프로토타입에 접근하기 위해 __proto__접근자 프로퍼티를 사용하는 이유는 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서이다.
  • 🔹예시6) parent 객체를 child 객체의 프로토타입으로 설정한 후 child 객체를 parent 객체의 프로토타입으로 설정하면 서로가 자신의 프로토타입이 되는 비정상적인 프로토타입 체인이 만들어져 에러가 발생한다
<script>
const parent = {x:1};
const child = {};
// child의 프로토타입을 parent로 설정
child.__proto__ = parent;
// parent의 프로토타입을 chlid로 설정
parent.__proto__ = child; // TypeError: Cyclic __proto__ value
</script>

=> 위 코드 처럼 순환 참조를 하는 프로토타입 체인이 만들어지면 프로토타입 체인 종점이 없어 프로퍼티 검색 시 무한 루프에 빠진다

-__proto__를 통해 프로퍼티에 접근하는 이유

  • 프로토타입 체인'단반향 링크드 리스트'로 구현되어 프로퍼티 검색 방향은 항상 '한쪽 방향'으로 흘러가야한다.
    => 아무런 확인없이 무조건 포로토타입을 교체할 수 없도록(방지차원) __proto__접근자 프로퍼티 통해 프로토타입에 접근하고 교체하도록 구현되어있다.

4> __proto__ 접근자 프로퍼티를 코드 내에서 직접 사용하는 건 권장 ❌

  • 접근자 프로퍼티 __proto__ 는 ES5까진 비표준, ES6에서 표준으로 채택되었지만 접근자 프로퍼티 __proto__ 를 코드 내에서 직접 사용하는 건 권장 ❌
  • 모든 객체가 접근자 프로퍼티 __proto__ 를 사용할 수 있는 것이 아니기 때문에 Object.prototype상속받지 않는 객체를 생성하는 경우 접근자 프로퍼티 __proto__ 사용할 수 없는 경우가 생긴다.
<script>
const obj = Object.create(null);
// obj는 프로토타입 체인의 종점으로 Object.__proto__ 상속 X
console.log(obj.__proto__) // undefined
</script>

-__proto__ 코드의 대안

=> 접근자 프로퍼티 __proto__ 대신 1> 프로토타입 '참조를 취득'할 경우 Object.getPrototypeOf 메서드를 사용하고 2> '교체'하고 싶을 경우엔 Object.setPrototypeOf 메서드 사용을 권장 ⭕

<script>
const obj = Object.create(null);
// __proto__ 보다 Object.getPrototypeOf 메서드 사용 권장 O
console.log(Object.getPrototypeOf(obj)) // null
</script>

3_2. 함수 객체의 prototype 접근자 프로퍼티

  • 함수 객체 만이 소유하는 prototype 프로퍼티생성자 함수가 생성할 객체(인스턴스)의 프로토타입을 가리킨다.
<script>
// 함수 객체는 prototype 프로퍼티를 소유한다.
((function (){}).hasOwnProperty('prototype')); // true
// 일반 객체는 prototype 프로퍼티를 소유하지 않는다.
({}.hasOwnProperty('prototype')) // false  
</script>

-1>생성자 함수로 호출 할 수 없는 함수2>정의하지 않은 일반함수

  • 1> 생성자 함수로서 호출할 수 없는 함수인,
    1.1>non- constructor인 화살표 함수와 1.2>ES6 메서드 축약 표현으로 정의한 메서드prototype 프로퍼티를 소유하지 않고 프로토타입도 생성하지 않는다.
  • 생성자 함수로 호출하기 위해 2> 정의하지 않은 일반 함수(함수 선언문 ,표현식)prototype 프로퍼티를 소유하지만 생성하지 않는 일반 함수의 prototype 프로퍼티는 아무 의미가 없다.
<script>
// 1. 화살표 함수는 non-constructor
const Person  = name => {
  this.name = name;
};
// 1. non-constructor는 prototype 프로퍼티를 소유하지 않음.
console.log(Person.hasOwnProperty('prototype')); // false
// => 따라서 non-constructor는 프로토타입을 생성하지 않음
console.log(Person.prototype); // undefiend
// 2. ES6 메서드 축약 표현으로 정의한 메서드도  non-constructor
const obj = {
  foo(){}
};
// 2. non-constructor는 prototype 프로퍼티를 소유하지 않음.
console.log(obj.foo.hasOwnProperty('prototype')); // false  
// => 따라서 non-constructor는 프로토타입을 생성하지 않음
console.log(obj.foo.prototype); // undefiend
</script>

- __proto__접근자 프로퍼티 함수 객체 만이 소유하는 prototype 프로퍼티 공통점과 차이점

  • 모든 객체가 가진(Object.prototype으로 상속받은) __proto__접근자 프로퍼티 함수 객체 만이 소유하는 prototype 프로퍼티는 결국 🎆'프로토타입'을 참조하지만 프로퍼티를 🎇사용하는 주체는 다르다.
<script>
// 생성자 함수
function Person(name) {
  this.name = name;
};
const me = new Person('kim')
// 생성자 함수의 Person.prototype과 객체의 me.__proto__는 같은 프로토타입을 참조 
console.log(me.__proto__ === Person.prototype); // true
</script>

- 생성자 함수(new 연산자 활용)로 객체(me)를 생성한 후 '__proto__접근자 프로퍼티'함수 객체 만이 소유하는 'prototype 프로퍼티'로 객체에 접근하면 같은 '프로토타입'을 가리킨다

3_3. 프로토타입의 constructor 프로퍼티와 생성자 함수

  • 모든 프로토타입은 'constructor 프로퍼티'를 갖는데 'constructor 프로퍼티'prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다. 이 연결은 생성자 함수가 생성 될 때, 즉 함수 객체가 생성될 때 이뤄진다.
<script>
// 생성자 함수
function Person(name) {
  this.name = name;
};
// 1. 생성자 함수가 객체, 인스턴스 생성
// 2. me 객체 Person의 prototpye의 constructor 프로퍼티를 통해 생성자와 연결
const me = new Person('kim')
// 3. me 객체는 Person의 prototpye로 부터 constructor 상속받아 사용
// me 객체의 생성자 함수는 Person이다. 
console.log(me.constructor === Person); // true
</script>
  • 1>Person 생성자 함수me 객체를 생성했고 이때 2>me 객체는 프로토타입의 constructor 프로퍼티를 통해 생성자 함수와 연결된다.
    => me 객체에는 'constructor 프로퍼티'가 없지만 me 객체의 프로토타입인 Person.prototype에는 constructor 프로퍼티가 있어 이를 상속받아 사용한다.

*💡conclusion

  • 프로토타입 기반의 객체지향 언어인 자바스크립트는 자바스크립트를 잘 설명할 수 있는 키워드이므로 사용시 그림과 함께 구조적인 관계를 항상 떠올리자!

#📑Study Source

  1. 책 - 모던 자바스크립트 Deep Dive (258p-272p)
profile
자기 신뢰의 힘을 믿고 실천하는 개발자가 되고자합니다.

0개의 댓글