🔑프로토타입 기반의 객체지향 프로그래밍 자바스크립트를 이해해보자.
-객체지향 프로그래밍
-상속과 프로토타입
-프로토타입 객체(__proto__ 접근자 프로퍼티 & 함수 객체의 prototype 접근자 프로퍼티 & 프로토타입의 constructor 프로퍼티와 생성자 함수)
🤷♂️그렇다면 객체지향 프로그래밍이 그럼 뭘까?
-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.다른 객체의 프로퍼티(상태 데이터)나 메서드(동작)을 💎상속받아 쓰는 경우도 있다.
-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) 메서드를 프로토타입에 미리 구현하면 생성자 함수가 생성할 모든 인스턴스는 별도의 구현 없이 상위 객체인 프로토타입의 자산(프로퍼티나 메서드)을 공유하며 사용할 수 있다.
-WHAT IS❓
- 프로토타입 객체(프로토타입)란 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용된다.
- 프로토타입은 어떤 객체의 상위(부모) 객체의 역할 하는 객체로서 다른 객체에 공유 프로퍼티(메서드 포함)를 제공한다.
- 프로토타입을 상속받은 하위(자식) 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용할 수 있다.
- 모든 객체는 [[prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조다.
- [[prototype]]에 저장되는 '프로토타입'은
객체 생성 방식
에 의해 결정된다.
=> 즉 객체가 생성될 때객체 생성 방식
에 따라 '프로토타입'이 결정되고 [[prototype]]에 저장된다.
🔹예시5) 1>객체 리터럴에 의해 생성된 객체의 '프로토타입'은 Object.prototype이고
2> 생성자 함수에 의해 생성된 객체의 '프로토타입'은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체다.
- 모든 객체는 하나의 프로토타입을 갖고 모든 프로토타입은 생성자 함수와 연결되어 있는데 객체와 프로토타입과 생성자 함수는 서로 연결되어있음을 의미한다.
- [[prototype]] 내부 슬롯에 직접 접근할 수는 없지만
1__proto__접근자 프로퍼티를 통해 자신의 [[prototype]] 내부슬롯이 가리키는 프로토타입에 간접적으로 접근할 수 있다. '프로토타입'은 자신의 constructor 프로퍼티를 통해 생성자 함수에 접근할 수 있고 '생성자 함수'는 자신의 prototype 프로퍼티를 통해 프로토타입에 접근할 수 있다.
- 모든 객체는 __proto__접근자 프로퍼티를 통해 자신의 프로토타입, [[prototype]] 내부 슬롯에 간접적으로 접근할 수 있다
- 위 그림 속 빨간 박스로 표시된 것이 person 객체의 프로토타입인 Object.prototype으로 person 객체는 __proto__접근자 프로퍼티를 통해 person 객체의 [[prototype]] 내부 슬롯이 가리키는 객체인 Object.prototype에 접근할 수 있다.
접근자 프로퍼티
이다.
- 내부슬롯은 프로퍼티가 아니여서 직접적으로 접근, 호출하지 못하고 간접적으로 접근할 수 있는 수단을 이용해야하는데 그 예시가 바로 __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>
상속
을 통해 사용된다
- __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
로 이 객체의 프로퍼티와 메서드를 모든 객체가상속
받는다.
프로퍼티에 접근하는 이유
- [[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__접근자 프로퍼티 통해 프로토타입에 접근하고 교체하도록 구현되어있다.
직접 사용하는 건 권장 ❌
- 접근자 프로퍼티 __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>
- 함수 객체 만이 소유하는 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 프로퍼티'로 객체에 접근하면 같은 '프로토타입'을 가리킨다
- 모든 프로토타입은 '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 프로퍼티가 있어 이를상속
받아 사용한다.
- 프로토타입 기반의 객체지향 언어인 자바스크립트는 자바스크립트를 잘 설명할 수 있는 키워드이므로 사용시 그림과 함께 구조적인 관계를 항상 떠올리자!
- 책 - 모던 자바스크립트 Deep Dive (258p-272p)