티스토리 뷰

개발/JS, TS, React

JS 프로토타입

hahagarden 2023. 3. 16. 12:33

프로토타입

자바스크립트는 명령형, 함수형, 프로토타입 기반 객체지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어이다.
C++나 자바같은 클래스 기반 객체지향 프로그래밍 언어의 특징인 클래스와 상속, 캡슐화를 위한 키워드인 public, private, protected 등이 없어서 자바스크립트는 객체지향 언어가 아니라고 오해하는 경우가 있다.(나도 그랬다) 하지만 자바스크립트는 클래스 기반 객체지향 프로그래밍 언어보다 효율적이며 더 강력한 객체지향 프로그래밍 능력을 지니고 있는  프로토타입 기반의 객체지향 프로그래밍 언어이다.

프로토타입 객체(프로토타입)란 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용된다. 

 

자바스크립트는 함수에 자동으로 객체인 prototype 프로퍼티를 생성해 놓는데, 해당 함수를 생성자 함수로서 사용할 경우, 즉 new 연산자와 함께 함수를 호출할 경우, 그로부터 생성된 인스턴스에는 숨겨진 프로퍼티인 __proto__가 자동으로 생성되며, 이 프로퍼티는 생성자 함수의 prototype 프로퍼티를 참조한다. __proto__ 프로퍼티는 생략 가능하도록 구현돼 있기 때문에 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 접근할 수 있게 된다.

 

 

생성자 함수의 prototype 프로퍼티와 인스턴스의 __proto__ 프로퍼티

생성자함수가 생성되는 시점에 prototype 프로퍼티가 생성되고, new와 함께 인스턴스를 생성하면 인스턴스에 __proto__프로퍼티가 생성되고 prototype프로퍼티를 참조한다.

(__proto__는 사실 인스턴스 객체가 직접 소유하는 프로퍼티가 아니라  Object.prototype의 접근자 프로퍼티이다. Object.prototype은 프로토타입 체인의 최상위 객체로, 이 객체의 프로퍼티와 메서드는 모든 객체에 상속된다. 접근자 프로퍼티를 통해 프로토타입에 접근하는 이유는 프로토타입 체인의 프로퍼티 검색이 한쪽 방향으로만 흘러가도록 단방향 링크드 리스트로 구현하기 위함이다. 자세한 내용은 프로토타입 체인에서 다루겠다.)

prototype 프로퍼티와 __proto__ 프로퍼티는 객체이다. prototype 객체 내부에 인스턴스가 사용할 메서드를 저장하면, 인스턴스에서도 숨겨진 프로퍼티 __proto__를 통해 이 메서드들에 접근할 수 있다.

const Person = function (name) {
  this._name = name;
}
Person.prototype.getName = function () {
  return this._name;
}

let suzi = new Person('Suzi');
suzi.__proto__.getName(); // undefined

suzi.getName() // 'Suzi'

Person이라는 생성자 함수가 있다. suzi라는 인스턴스를 생성하면 __proto__를 통해서 생성자함수의 prototype에 접근 가능하여 prototype객체에 있는 getName()메서드를 호출할 수 있다. 그러나 결과가 기대했던 'Suzi'가 아닌 undefined가 나왔다. 왜일까??

this바인딩때문인다. 여기서 잠시, this바인딩이 상황별로 할당되는 값이 다른데 어떤 함수를 메서드로서 호출하면 메서드명 바로 앞의 객체가 곧 this가 된다. 여기서 suzi.__proto__.getName()은 메서드명 바로 앞의 suzi.__proto__가 this가되는 것이다. __proto__프로퍼티도 객체인데 이 객체에는 _name프로퍼티가 없다. 그래서 undefined가 출력된다.
그런데 지금까지 의문없이 사용해온 바와 같이 우리는 인스턴스의 메서드를 호출할 때에 __proto__를 사용하지 않고 바로 suzi.getName()을 한다. suzi.getName()을 하면 원하는 결과인 'Suzi'가 출력된다. 이것은 왜그럴까?

__proto__가 생략가능한 프로퍼티이기 때문이다. (생략 가능한 프로퍼티라는 개념은 언어를 창시하고 전체 구조를 설계한 브랜든 아이크의 아이디어로, 그냥 그렇구나 하는 수밖에 없다. 어떤 이유이든 __proto__가 생략 가능하다!)
__proto__를 생략함으로서 인스턴스를 this바인딩시키고, __proto__에 있는 프로퍼티와 메서드는 그대로 사용할 수 있다.
인스턴스가 __proto__의 것을 자신의 것처럼 사용할 수 있다. 

 

대표적인 내장 생성자 함수인 Array를 보자. 

let arr = [1, 2];

 

Array를 new연산자와 함께 호출해서 인스턴스를 생성하든, 배열 리터럴을 생성하든 인스턴스인 [1, 2]가 만들어진다. 이 인스턴스의 __proto__는 Array.prototype을 참조하는데 __proto__는 생략 가능하도록 설계돼 있기 때문에 인스턴스가 push, pop, forEach 등의 메서드를 마치 자신의 것처럼 호출할 수 있다.

하지만 Array.prototype에 있지 않은 from, isArray 등의 메서드들은 인스턴스가 직접 호출할 수 없다. 생성자 함수의 정적 메서드이기 때문에 생성자 함수에서 직접 접근해야 실행 가능하다. 그래서 우리가 그동안 생성자함수에서 호출한 Array.isArray()를 사용하고, __proto__를 생략한 arr.push()를 사용한 것이다.

 

 

prototype 객체의 constructor 프로퍼티

생성자 함수의 프로퍼티인 prototype 객체 내부에는 constructor 프로퍼티가 있다. 인스턴스의 __proto__ 객체 내부에도 있을 것이다. 이 프로퍼티는 생성자 함수 자기 자신을 참조한다. 이는 인스턴스로 하여금 그 원형이 무엇인지를 알 수 있는 수단이 된다. 

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

const p1 = new Person('사람1');
const p1Proto = Object.getPrototypeOf(p1);
const p2 = new Person.prototype.constructor('사람2');
const p3 = new p1Proto.constructor('사람3');
const p4 = new p1.__proto__.constructor('사람4');
const p5 = new p1.constructor('사람5');

p1부터 p5까지 모두 Person의 인스턴스이다.  

// 다음 다섯 줄은 모두 동일한 대상을 가리킨다
[Constructor]
[instance].__proto__.constructor
[instance].constructor
Object.getPrototypeOf([instance]).constructor
[Constructor].prototype.constructor

// 다음 네 줄은 모두 동일한 객체(prototype)에 접근할 수 있다
[Constructor].prototype
[instance].__proto__
[instance]
Object.getPrototypeOf([instance])

 

 

메서드 오버라이드

const Person = function (name) {
  this.name = name;
}
Person.prototype.getName = function () {
  return this.name;
}

let iu = new Person('지금');
iu.getName() = function () {
  return '바로' + this.name;
}

iu.getName() // '바로 지금'

인스턴스에 __proto__를 생략해도 prototype에 정의된 프로퍼티나 메서드를 자신의 것처럼 사용할 수 있었다. 만약 인스턴스가 동일한 이름의 프로퍼티 또는 메서드를 가지고 있는 상황이라면 어떨까?

마지막 줄의 결과 '바로 지금'을 보면 iu.__proto__.getName이 아닌 iu객체에 있는 getName 메서드가 호출됐다. 메서드 오버라이드가 일어난 것이다. 자바스크립트 엔진이 getName이라는 메서드를 찾는 방식은 가장 가까운 대상인 자신의 프로퍼티를 검색하고, 없으면 그 다음으로 가까운 대상인 __proto__를 검색한다. 이 상황에서는 자기 자신 객체에 getName메서드가 있기 때문에 우선시되어 실행한 것이다. 오버라이딩을 해도 __proto__의 getName메서드가 교체되는 것은 아니다. 제 자리에 그대로 있는데 실행할 getName을 우선순위에서 먼저 찾은 것 뿐이다.

iu.__proto__.getName.call(iu); // '지금'

위 코드로 iu를 this바인딩하여 __proto__의 getName메서드를 호출하고 iu인스턴스의 name프로퍼티인 '지금'을 출력할 수 있다.

 

프로토타입 체인

자바스크립트는 객체의 프로퍼티와 메서드에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 __proto__를 찾고 없다면 또다시 그 안에 있는 __proto__를 찾는다. 어떤 데이터의 __proto__ 프로퍼티 내부에 다시 __proto__ 프로퍼티가 연쇄적으로 이어진 것을 프로토타입 체인이라고 하고, 이 체인을 따라가며 검색하는 것을 프로토타입 체이닝이라고 한다. 최종적으로는 __proto__를 계속 따라가다 보면 Object.prototype에 당도한다.

 

function Person (name) {
  this.name = name;
}
Person.prototype.sayHello = function () {
  console.log(`Hi! My name is ${this.name}`);
}

const me = new Person('Lee');
me.hasOwnProperty('name');

스코프 체인과 비슷한데, 프로토타입 체인은 상속과 프로퍼티 검색을 위한 메커니즘이라고 할 수 있고, 스코프 체인은 식별자 검색을 위한 메커니즘이라고 할 수 있다. 위 코드의 경우 먼저 스코프 체인에서 me 식별자를 검색한다. me 식별자를 검색한 다음 me객체의 프로토타입 체인에서 hasOwnProperty 메서드를 검색한다. 이처럼 스코프 체인과 프로토타입 체인은 서로 연관없이 별도로 동작하는 것이 아니라 서로 협력하여 식별자와 프로퍼티를 검색하는데 사용된다.

 

Reference

코어자바스크립트
모던자바스크립트 딥다이브

 

반응형

'개발 > JS, TS, React' 카테고리의 다른 글

JS 프로미스 체이닝과 Promise.all(), Fetch API  (0) 2023.03.21
JS 동기와 비동기, Callback, Promise, async/await  (0) 2023.03.17
JS 고차함수  (0) 2023.03.15
JS var, let, const  (0) 2023.03.07
JS 스코프, 그 규칙과 종류  (0) 2023.03.07
댓글