[JavaScript] 12. Prototype

100tick·2022년 12월 31일
0

JavaScript Deep Dive

목록 보기
12/16
post-thumbnail

작성중

앞서 함수의 종류는 2가지가 있다는 것을 알아보았다.
함수를 호출하면, [[Call]], [[Construct]] 내부적으로 두가지 내부 메소드 중 하나가 실행된다.

new로 호출할 때 [[Construct]], 그냥 호출할 때 [[Call]]이 호출된다고 하였다.

다른 언어에서는 대부분 객체를 만들기 위해 Class를 사용하는데, JS는 특이하게 함수를 2가지 용도로 나누어 [[Construct]]를 호출하면 객체를 생성하도록 구현한 것이다.

생성자 함수는 prototype 객체를 갖고, 동일한 생성자에 의해 만들어진 객체는 prototype 객체를 공유한다.

직전에 봤던 내용이므로 아래 코드를 보면 쉽게 이해할 수 있을 것이다.

function Computer(name) {
	this.name = name;
  	this.notShared() {}
}

Computer.prototype.turnOn = function() {
	console.log("Computer turned on");
}

Computer.prototype.turnOff = function() {
  	console.log("Computer turned off");
}

생성자 함수 내에서 this로 선언된 속성은 각 객체마다 개별적으로 갖는 값이다.(this.notSharedthis.name)

그리고 같은 생성자 함수로 생성된 모든 객체는 동일한 prototype 객체를 공유한다.

Computer 생성자 함수의 prototype 객체 내에 정의된 메소드들은 Computer 생성자로 만들어진 모든 객체가 하나의 함수를 공유하는 형태이기 때문에 메모리 공간을 훨씬 절약할 수 있다.
(Computer.prototype.turnOn, Computer.prototype.turnOff)

const com1 = new Computer("com1");

com1.turnOn(); // "Computer turned on"
com1.__proto__.turnOn(); // "Computer turned on"

prototype 객체 내에 정의된 메소드는 일반 속성과 똑같이 접근할 수 있으며,
생성자 함수에서 접근할 때는 Computer.prototype.turnOn과 같이 접근하지만, 객체에서 접근할 때는 .prototype에 바로 접근할 수는 없고, 간접적으로 __proto__를 통해 접근할 수 있다.
그래서 com1.__proto__.turnOn과 같이 사용한 것이다.

그러나 .__proto__를 빼도 알아서 접근이 되니, 굳이 .__proto__를 추가로 쓰면서 사용할 필요는 없을 것이다.

1. [[Prototype]], Object.prototype, proto

모든 객체는 프로토타입의 계층 구조인 프로토타입 체인에 묶여있다.
JS 엔진은 객체의 속성, 메소드에 접근할 때, 해당 속성, 메소드가 존재하지 않는다면 __proto__ 접근자 프로퍼티가 가리키는 생성자 함수의 .prototype 거기에도 없다면 연결된 부모 생성자 함수의 .prototype을 타고 올라가면서 검색한다.
프로토타입 체인의 종점인 Object.prototype에 없을 때까지.

function A() {}
A.prototype; // {constructor, [[Prototype]]}

const a = new A();
a.__proto__; // // {constructor, [[Prototype]]}

크롬 콘솔창에서 위 코드를 실행해 보면, 생성자 함수는 .prototype을, instance.__proto__를 갖고, 그 안에 [[Prototype]] 내부 슬롯이 위치한 것을 알 수 있다.

이 때, instance.__proto__ 접근자 프로퍼티를 통해 생성자 함수의 .prototype에 접근하는 이유는 오류를 방지하기 위해서다.

const parent = {};
const child = {};

child.__proto__ = parent;
parent.__proto__ = child; // TypeError: Cyclic __proto__ value

위와 같이 서로가 서로의 프로토타입이 된다면, child -> parent -> child -> parent ...로 무한 참조가 발생하게 될 것이다.
최종 목적지인 Object.prototype에 도달할 수 없게 되는 것이다.(무한 루프)
__.proto__를 통해 이러한 구조로 설정한 경우 오류를 발생시켜 무한 루프를 예방한다.
간접적으로 값을 설정하게 하면서 무한 순환 참조 여부에 대한 검사 로직을 추가하는 듯 하다.

그러나 __proto__를 코드 내에서 직접 사용하는 것도 비권장 사항이라고 한다.
모든 객체가 __proto__를 사용할 수 있는 것이 아니기 때문.

const obj = Object.create(null);

obj.__proto__; // undefined
Object.getPrototypeOf(obj); // null

위와 같이 직접 상속을 통해 생성한 객체는 Object.prototype을 상속받지 않는다.

프로토타입 접근 시, .__proto__ 대신 Object.getPrototypeOf를,
프로토타입 교체 시, Object.setPrototypeOf를 사용할 것이 권장된다.

const obj = {};
const parent = { x: 1 };
// obj 객체의 프로토타입을 취득
Object.getPrototypeOf(obj); // obj.__proto__;
// obj 객체의 프로토타입을 교체
Object.setPrototypeOf(obj, parent); // obj.__proto__ = parent;
console.log(obj.x); // 1

1.1 prototype Property of Function Object

함수 객체만이 소유하는 .prototype 속성은 생성자 함수가 생성할 객체의 프로토타입을 가리킨다.
일반 객체는 prototype 속성이 없다.

(function() {}).hasOwnProperty("prototype"); // true
{}.hasOwnProperty("prototype"); // false

앞서 봤듯이, 화살표 함수나 메소드 축약 표현으로 선언된 함수는 [[Construct]] 내부 메소드 자체가 없기 때문에 생성자 함수가 될 수 없다.
그래서 당연히 prototype 속성도 없다.

생성자로 사용되지 않는 일반 함수도 prototype 속성을 가지고 있지만 객체를 생성하지 않을 것이기 때문에 의미는 없다.

모든 객체가 가진 Object.prototype으로부터 상속받은 __proto__ 접근자 프로퍼티와 함수 객체만 가지고 있는 prototype 프로퍼티는 결국 동일한 프로토타입을 가리킨다.

사용 주체가 다르니 위 도표를 통해 파악하자.

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

const p1 = new Person('a');

console.log(Person.prototype === p1.__proto__); // true

Person 생성자 함수의 .prototypePerson instance.__proto__ 프로퍼티는 결국 동일한 프로토타입(Person.[[Prototype]])을 가리킨다.

constructor Property

모든 프로토타입은 .constructor 프로퍼티를 갖는다.

생성자 함수는 .prototype을, 프로토타입은 .constructor로 서로를 참조한다.
그리고 둘 다 .__proto__를 가지고 있다.
생성자 함수의 .__proto__Function.prototype을, 프로토타입의 .__proto__는 부모 프로토타입(최상위는 Object.prototype)을 가진다.
그리고 Object.prototype은 최상위 프로토타입이므로, 부모가 존재하지 않기 때문에Object.prototype.__proto__null이다.

/ 생성자 함수
function Person(name) {
 this.name = name;
}
const me = new Person('Lee');

// me 객체의 생성자 함수는 Person이다.
console.log(me.constructor === Person); // true

객체는 .constructor 속성이 없지만, 자동으로 .__proto__를 참조하여 자신을 생성할 때 사용한 프로토타입의 .constructor을 참조하여 생성자 함수를 반환한다.

이번에는 리터럴로 생성된 객체를 살펴보자.

// obj 객체는 Object 생성자 함수로 생성한 객체가 아니라 객체 리터럴로 생성했다.
const obj = {};
// 하지만 obj 객체의 생성자 함수는 Object 생성자 함수다.
console.log(obj.constructor === Object); // true

When Prototype is created

모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성됨.

Prototype Chain

자바스크립트는 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없
다면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으
로 검색한다. 이를 프로토타입 체인이라 한다. 프로토타입 체인은 자바스크립트가 객체지향 프로그래밍의 상
속을 구현하는 메커니즘이다.

function Person(name) {
 this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHello = function () {
 console.log(`Hi! My name is ${this.name}`);
};
const me = new Person('Lee');
// hasOwnProperty는 Object.prototype의 메서드다.
console.log(me.hasOwnProperty('name')); // true

0개의 댓글