[Javascript] 프로토타입(Prototype)

Gyuhan Park·2023년 3월 4일
0

javascript deepdive

목록 보기
7/11
post-thumbnail

객체(Object)

객체 : 프로퍼티와 메소드를 하나의 논리적인 단위로 묶은 자료구조
프로퍼티(property) : 객체의 상태를 나타내는 데이터
메소드(method) : 상태 데이터를 조작할 수 있는 동작
추상화 : 다양한 속성 중에서 프로그램에 필요한 속성만 간추려 표현하는 것

일급 객체

다음과 같은 조건을 만족하는 객체를 일급 객체라 한다.

  1. 무명의 리터럴로 생성할 수 있다. 즉, 런타임에 생성이 가능하다.
  2. 변수나 자료구조에 저장할 수 있다.
  3. 함수의 매개변수에 전달할 수 있다.
  4. 함수의 반환값으로 사용할 수 있다.

자바스크립트에서의 함수는 위의 조건을 모두 만족하므로 일급 객체다.

// 1. 무명의 리터럴로 생성할 수 있다.
// 2. 변수에 저장할 수 있다.
const increase = funcion (num) {
	return ++num;
}
// 3. 함수의 매개변수에 전달할 수 있다.
// 4. 함수의 반환값으로 사용할 수 있다.
function makeCounter(increase) {
  let num = 0;
  return function() {
    num = increase(num);
    return num;
  };
}

프로토타입 기반 상속

자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복 제거하였다.
같은 동작을 하는 메소드를 인스턴스마다 생성하는 것이 아니라 input값만 달라지고 같은 메소드를 공유하여 사용한다.

// 인스턴스를 생성할 때마다 같은 메소드를 중복해서 생성한다.
function Circle(radius){
	this.radius = radius;
    this.getArea = function() {
    	return Math.PI * this.radius ** 2;
    };
}


// 동일한 메소드는 프로토타입으로 공유하여 불필요한 중복을 제거한다.
function Circle(radius) {
	this.radius = radius;
}
Circle.prototype.getArea = function() {
	return Math.PI * this.radius ** 2;
}

프로퍼티 : 각 인스턴스에 존재
메소드 : 상속을 통해 공유하여 사용

프로토타입 객체

프로토타입 : 어떤 객체의 부모 객체의 역할을 하는 객체

모든 객체는 [[Prototype]] 내부 슬롯을 가짐.
[[Prototype]]은 자신의 프로토타입의 참조값을 가진다.
[[Prototype]]에 저장되는 프로토타입은 객체 생성 방식에 의해 결정

모든 객체는 하나의 프로토타입을 갖는다.
모든 프로토타입은 생성자 함수와 연결되어 있다.

__proto__

__proto__ : 내부 슬롯에 간접적으로 접근 가능한 접근자 프로퍼티
[[Prototype]]는 직접 접근할 수 없고 접근자 프로퍼티를 이용해 간접 접근 가능
__proto__는 접근자 함수([[Get]], [[Set]])를 통해 프로토타입을 얻거나 할당한다.

const obj = {};
const parent = { data: 1 };
obj.__proto__; // getter
obj.__proto__ = parent; // setter
console.log(obj.data); // 1

"[[Prototype]]의 직접 접근을 막는 이유"

참조값을 마음대로 변경하는 경우 다음과 같은 비정상적인 프로토타입 체인이 발생할 수 있다.

const parent = {};
const child = {};
child.__proto__ = parent;
parent.__proto__ = child;

프로토 타입 체인은 단방향으로 이뤄져야 한다.

부모, 자식 관계로 이뤄져야 자신의 부모 역할을 하는 프로토타입의 프로퍼티나 메소드를 순차적으로 검색할 수 있다.

"접근자 프로퍼티는 코드에서 사용하지 않는다"

코드 내에서 접근자 프로퍼티를 직접 사용하는 것은 권장하지 않는다.
직접 상속을 통해 Object.prototype을 상속받지 않는 객체를 생성할 수 있기 때문에 그런 객체는 __proto__ 프로퍼티를 갖고 있지 않아 오류가 발생할 수 있다.

const obj = Object.create(null);

다음 코드로 접근자 함수를 대체할 수 있다.
[[Get]] : Object.getPrototypeOf
[[Set]] : Object.setPrototypeOf

const obj = {};
const parent = { data: 1 };

// obj.__proto__; // getter
// obj.__proto__ = parent; // setter
Object.getPrototypeOf(obj);
Object.setPrototypeOf(obj, parent);
console.log(obj.data); // 1

함수 객체의 prototype

함수 객체는 일반 객체와 달리 prototype 프로퍼티를 가진다.

함수 객체의 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.

생성자 함수가 인스턴스의 프로토타입을 할당하기 위해 prototype 프로퍼티가 사용한다.

__proto__와 prototype은 프로퍼티를 사용하는 주체가 다르지만 동일한 프로토타입을 가리킨다.
Person 생성자 함수는 prototype 프로퍼티를 가지고 Person.prototype을 가리킴.
Person이 생성한 me 인스턴스의 __proto__ 프로퍼티도 Person.prototype을 가리킴.

function Person(name) {
  this.name = name;
}
const me = new Person('Lee');
console.log(Person.prototype === me.__proto__); // true

모든 프로토타입은 constructor 프로퍼티를 가짐.
constructor는 prototype 프로퍼티로 자신을 참조하는 생성자 함수 가리킴.

인스턴스는 프로토타입의 constructor 프로퍼티에 의해 생성자 함수와 연결된다.
me 인스턴스에는 constructor 프로퍼티가 없지만 프로토타입인 Person.prototype에 있기 때문에 상속받아 사용할 수 있다.

console.log(me.constructor === Person); // true

리터럴 표기법에 의해 생성된 객체

new 연산자와 함께 생성자 함수에 의해 생성된 인스턴스는 프로토타입의 constructor 프로퍼티가 생성자 함수를 가리킴.

const obj = new Object();
console.log(obj.constructor === Object); // true

function Person(name) { this.name = name; }
const me = new Person('Lee');
console.log(me.constructor === Person); // true

그럼 리터럴 표기법에 의해 생성된 객체도 프로토타입의 constructor 프로퍼티가 가리키는 생성자 함수가 객체를 생성한 생성자 함수인가?

const obj = {};
console.log(obj.constructor === Object); // true

아니다!
위 예제는 true를 출력하지만 obj 객체는 Object 생성자 함수로 생성한 객체가 아니라 객체 리터럴에 의해 생성한 객체다.
빈 객체를 생성하는 점에서 동일하나 프로퍼티를 추가하는 처리 등 세부내용이 다르다.
하지만 큰 틀에서 보면 큰 차이는 없기 때문에 같다고 봐도 무방하다.
객체 리터럴로 생성한 객체도 객체로서의 특성을 갖고 함수 리터럴로 생성한 함수도 함수로서의 특성을 갖기 때문이다.

프로토타입의 생성 시점

객체는 크게 리터럴 표기법 또는 생성자 함수 에 의해 생성되므로, 모든 객체는 생성자 함수와 연결되어 있다.
리터럴 표기법에 의해 생성된 객체도 상속을 위해 프로토타입이 필요하다. 따라서 가상적인 생성자 함수를 갖는다. 프로토타입은 생성자 함수와 연결되어 있기 때문이다.

"프로토 타입은 생성자 함수와 언제나 쌍으로 존재하며 prototype, constructor 프로퍼티에 의해 연결되어 있다."

사용자 정의 생성자 함수

함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 같이 생성된다. 즉, 함수 선언문의 경우 런타임 이전에 함수 객체가 생성되므로 런타임 이전에 프로토타입이 생성된다.

console.log(Person.prototype);

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

이때 생성된 프로토타입은 오직 constructor 프로퍼티만을 갖는 객체다.

빌트인 생성자 함수

Object, String, Number 등과 같은 빌트인 생성자 함수도 빌트인 생성자 함수가 생성되는 시점에 프로토타입이 생성된다. 모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점(런타임 이전)에 생성된다.
이처럼, 객체가 생성되기 이전에 생성자 함수와 프로토 타입은 이미 객체화되어 존재한다.
이후 객체를 생성하면 프로토타입은 생성된 객체의 [[Prototype]] 내부 슬롯에 할당된다. 이로써 생성된 객체는 프로토타입을 상속받는다.

객체 생성방식에 따른 프로토타입

[객체 생성 방식]
1. 객체 리터럴
2. Object 생성자 함수
3. 생성자 함수
4. Object.create 메서드
5. 클래스(ES6)

프로토타입은 추상 연산 OrdinaryObjectCreate에 전달되는 인수에 의해 결정된다. 이 인수는 객체 생성 방식에 의해 결정된다.

  • 객체 리터럴의 프로토타입 : Object.prototype
const obj = { x: 1 };
console.log(obj.constructor === Object); // true
  • Object 생성자 함수의 프로토타입 : Object.prototype
    빈 객체를 생성한 후 프로퍼티를 추가해야 한다.
const obj = new Object();
console.log(obj.constructor === Object); // true
obj.x = 1;
  • 생성자 함수 : 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체
function Person(name) {
  this.name = name;
}
const me = new Person('Park');
console.log(me.constructor === Person); // true

// Person.prototype에 프로퍼티 추가하기
Person.prototype.sayHello = function() {
  console.log(`My name is ${this.name}`);
};

const me = new Person('Lee');
const you = new Person('Kim');
me.sayHello(); // My name is Lee
you.sayHello(); // My name is Kim

프로토타입 체인

[[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다.
다음과 같이 메소드를 호출했다고 가정해보자.

me.hasOwnProperty('name');

그럼 프로토타입 체인을 따라 순차적으로 검색한다.

me 객체에 hasOwnProperty 메서드가 있는 지 확인한다.
없으면 프로토타입인 Person.prototype 객체에 메서드가 있는 지 확인한다.
없으면 프로토타입인 Object.prototype 객체에 메서드가 있는 지 확인한다.
Object.prototype에도 없는 경우 undefined를 반환한다. (에러 X)

프로토타입 체인을 따라 객체 간의 상속 관계로 이루어진 프로토타입의 계층적인 구조에서 프로퍼티/메서드 검색.
프로토타입 체인은 상속과 프로퍼티 검색을 위한 메커니즘
스코프 체인을 따라 함수의 중첩 관계로 이루어진 스코프의 계층적 구조에서 식별자 검색.
스코프 체인은 식별자 검색을 위한 메커니즘

인스턴스 메서드

프로토타입 프로퍼티와 같은 프로퍼티를 인스턴스에 추가하면 인스턴스 프로퍼티로 추가
인스턴스 메서드가 프로토타입 메서드를 overriding
상속 관계에 의해 프로퍼티가 가려지는 현상 : property shadowing

하위 객체를 통해 프로토타입에 get 액세스는 허용되나 set 액세스는 허용되지 않는다

프로토타입 교체

프로토타입은 임의의 다른 객체로 변경할 수 있다.
프로토타입을 동적으로 변경할 수 있다.
객체 간의 상속 관계를 동적으로 변경할 수 있다.
프로토타입은 생성자 함수 또는 인스턴스에 의해 교체할 수 있다.

생성자 함수로 프로토타입 교체

Person.prototype에 객체 리터럴을 할당했다. Person 생성자 함수가 생성할 인스턴스의 프로토타입을 객체 리터럴로 교체했다.

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

Person.prototype = {
  sayHello() {
    console.log(`Hi! I'm ${this.name}`);
  }
};

const me = new Person('Lee');

객체 리터럴로 생성한 프로토타입은 constructor가 없다.
자바스크립트 엔진이 프로토타입을 생성할 때 암묵적으로 constructor를 추가하는데 임의로 교체한 프로토타입에는 추가되지 않는다.

console.log(me.constructor === Person) // false
console.log(me.constructor === Object) // true

그럼 우리가 constructor 프로퍼티를 추가하자.

Person.prototype = {
  constructor: Person,
  sayHello() {
    console.log(`Hi! I'm ${this.name}`);
  }
};

console.log(me.constructor === Person) // true
console.log(me.constructor === Object) // false

인스턴스로 프로토타입 교체

프로토타입은 생성자 함수의 prototype 프로퍼티와 인스턴스의 __proto__ 접근자 프로퍼티로 접근할 수 있다.

생성자 함수의 prototype 프로퍼티 : 생성될 인스턴스의 프로토타입 교체
__proto__ : 이미 생성된 객체의 프로토타입 교체

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

const parent = {
  sayHello() {
    console.log(`Hi! I'm ${this.name}`);
  }
};

const me = new Person('Lee');
Object.setPrototypeOf(me, parent);

생성자 함수에 의한 프로토타입 교체 : 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입을 가리킴.
인스턴스에 의한 프로토타입의 교체 : 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입을 가리키지 않음.

다음과 같이 Person 생성자 함수의 prototype 프로퍼티와 프로토타입을 연결해주면 된다.

Person.prototype = parent;
Object.setPrototypeOf(me, parent);

프로토타입 교체를 통해 객체 간의 상속관계를 동적으로 변경하는 것은 번거롭기 때문에 프로토타입은 직접 교체하지 않는 것이 좋다.
상속관계를 인위적으로 설정하려면 직접 상속 또는 클래스를 사용하는 게 더 편리하고 안전하다.

모던자바스크립트 deepdive

profile
단단한 프론트엔드 개발자가 되고 싶은

0개의 댓글