프로토타입과 프로토타입 체이닝

JaeungE·2021년 6월 15일
1

JavaScript

목록 보기
4/16
post-thumbnail

프로토타입

JavaScript도 객체 지향 언어이다. 그럼 Java와 같은 클래스 기반의 객체 지향 언어와 다른 점이 무엇일까.

바로, JavaScript는 프로토타입 기반 프로그래밍을 이용해 객체 지향 프로그래밍을 구현했다는 것이다.

물론, ES6에서 class 키워드의 등장으로 클래스 기반의 객체 지향 언어를 학습한 사람들도 문법적으로 이해하기 쉽게 했다고 하지만, ES6의 class 역시 내부적으로는 프로토타입을 이용하고 있다.

그렇다면 프로토타입 기반 프로그래밍은 어떤 건지, 함수 객체와 프로퍼티에서 다루지 못 했던 prototype[[prototype]]프로퍼티를 살펴보며 이해해보자!😉




prototype과 __proto__


prototype

앞서 JavaScript의 함수는 객체이기도 하다고 했다. 그리고 JavaScript의 모든 함수는 함수 객체로서 prototype프로퍼티를 가지고 있는데, 정확히 무엇을 위한 프로퍼티인지 알아보자.

JavaScript는 함수가 생성될 때, constructor 프로퍼티만을 가진 프로토타입 객체도 같이 생성한 뒤, 함수 객체의 prototype 프로퍼티로 해당 객체를 가리키게 된다.
또한, 같이 생성된 프로토타입 객체의 constructor프로퍼티는 자신을 생성한 함수 객체를 가리키게 된다.

잘 이해가 안 갈 땐 역시 눈으로 보는 게 제일이다.😅 아래의 함수를 예로 들어보자!


// func라는 이름을 가진 함수 객체 생성
var func = function(){}; 

// func 함수 객체에 prop 프로퍼티 추가
func.prop = 'this is my function'; 

위의 함수 객체와 prototype 프로퍼티가 가리키는 객체의 관계를 도형으로 표현하자면 다음과 같다.



그렇다면, 위의 설명이 맞는지 직접 확인해보자.


console.log(func === func.prototype.constructor); // true

console.dir(func.prototype)을 이용해 func.prototype이 가리키는 객체의 구조를 확인해 보았더니 constructor 프로퍼티가 있는 것을 확인할 수 있었다. 그렇다면 constructor 프로퍼티는 어떤 함수를 가리킬까?

console.dir(func.prototype.constructor)의 결과를 보면 constructor 프로퍼티가 가리키는 함수의 이름은 func 이고, 기본 프로퍼티 외에 prop : "this is my function"을 가진 함수라는 것을 알 수 있다.

추가로 func === func.prototype.constructor 연산식을 이용해 두 객체가 서로 같은 객체라는 것을 확인 할 수 있었다.


__proto__

사실 __proto__ 는 브라우저에서 이렇게 표현되는 것이고 원래 명칭은 [[prototype]]으로,
Internal slot이라고 해서 JavaScript의 모든 객체의 내부에 은닉되어있다.

하지만 많은 브라우저가 [[prototype]] 프로퍼티를 __proto__ 프로퍼티로 구현해놓아서 거의 표준처럼 사용하고 있고, 해당 프로퍼티에 접근 또한 가능하다!😲

이러한 __proto__([[prototype]]) 프로퍼티는 자신을 생성한 생성자 함수의 prototype을 가리킨다.



myFunc라는 이름의 함수를 만들고, console.dir()을 이용해 프로토타입을 확인해 보았다.

myFunc는 함수 리터럴로 생성한 함수 객체이다. 그리고 함수 리터럴은 내부적으로 new Function() 연산자를 이용해 생성하게 된다.

그렇다면 위에서 설명했듯이 __proto__ 프로퍼티는 자신을 생성한 생성자 함수의 prototype을 가리키게 되고, 따라서 __proto__ 프로퍼티는 Function.prototype을 가리키게 된다.


prototype 객체 변경

그럼 BookShelf 라는 생성자 함수를 이용해 객체를 만든 뒤, prototype 프로퍼티가 함수 생성 시 같이 생성한 프로토타입 객체가 아닌, 다른 객체를 가리키게 되면 __proto__프로퍼티는 어떻게 될까? 다음 예제를 보면서 알아보자!


// BookShelf 생성자 함수 생성
var BookShelf = function(name, genre){
    this.name = name;
    this.genre = genre;
};

// BookShelf 생성자 함수를 이용한 객체 생성
var book = new BookShelf('Blue not`', 'short story collection');

console.dir(book);

book 객체의 구조는 아래와 같다.



book 객체의 __proto__ 프로퍼티가 BookShelf.prototype 객체를 가리키고 있는 것을 확인할 수 있다.


// 일반 객체 생성
var newPrototype = {prop : 'new Prototype!!!!!!'};

// BookShelf.prototype이 newPrototype 객체를 가리키게 함
BookShelf.prototype = newPrototype;

console.dir(book);

그다음, 일반 객체를 생성한 뒤에 BookSelf(생성자 함수)prototype 프로퍼티가 해당 객체를 가리키게 하고, 다시 확인해 보았다.



😱😱😱

분명히 __proto__ 프로퍼티는 자신을 생성한 생성자 함수의 prototype 프로퍼티를 가리키는데, BookShelf.prototype 프로퍼티가 newPrototype을 가리키도록 변경했음에도 book 객체의 __proto__ 프로퍼티는 그대로인 것을 확인할 수 있다.

이것은 생성자 함수의 prototype 프로퍼티를 변경해도, 변경하기 이전에 생성한 객체의 __proto__([[prototype]]) 프로퍼티는 그대로 유지하기 때문이다. 그렇다면 새 객체를 한번 생성해보자.



// 새로운 객체 생성
var newBook = new BookShelf('redshift', 'short story collection');

console.dir(newBook);

위에서 BookShelfprototype 객체를 변경한 뒤, 그다음엔 같은 생성자 함수의 새 객체인 newBook 객체를 생성했다. newBook 객체의 __proto__ 프로퍼티를 확인해보자!



새로 만든 객체인 newBook__proto__ 프로퍼티는 constructor 프로퍼티가 없는 객체, 즉 일반 객체를 가리키고 있다.

프로토타입의 prop 프로퍼티를 확인해보니, newBook 객체가 변경된 프로토타입 객체인 newPrototype 객체를 정상적으로 가리키고 있는 것을 알 수 있다.


console.log(book.__proto__ === newBook.__proto__); // false

일치 연산자를 이용해 확인해 보아도 역시나 false를 반환하는 것을 확인할 수 있다.





프로토타입 체이닝

앞서 설명한 프로퍼티들은 모두 프로토타입 체이닝을 이해하기 위한 내용이었다.

프로토타입 체이닝은 객체가 자신의 프로퍼티나 메서드 뿐만 아니라, 부모 역할을 하는 객체의 프로퍼티와 메서드에도 접근을 가능하게 한다.

이때, 부모 역할을 하는 객체의 기준은 생성자 함수의 prototype 프로퍼티가 가리키는 객체 즉, 객체의 __proto__([[prototype]]) 프로퍼티를 기준으로 프로토타입 체이닝을 한다.

사실 다른 객체 지향 언어를 다뤄 본 경험이 있고, 위에서 배운 내용을 정확히 알고 있으면 이해하기 어렵지 않을 것으로 생각한다.

간단한 예제를 보며 이해해보자!😊


// Shapes 생성자 함수 생성
var Shapes = function(shape){
    this.shape = shape;
}

// 생성자 함수를 이용해 객체 생성
var a = new Shapes('rectangle');

// a 객체의 color 프로퍼티 출력
console.log(a.color); // undefined

// Shapes 생성자 함수의 프로토타입 객체에 color 프로퍼티 추가
Shapes.prototype.color = 'red';

// 프로토타입 체이닝이 일어남
console.log(a.color); // red

이렇게만 보면 무슨 이유로 프로토타입 체이닝이 일어나는지 모르겠다...😅 코드를 나눠서 살펴보자.


// Shapes 생성자 함수 생성
var Shapes = function(shape){
    this.shape = shape;
}

// 생성자 함수를 이용해 객체 생성
var a = new Shapes('rectangle');

// a 객체의 color 프로퍼티 출력
console.log(a.color); // undefined

일단 Shapes 생성자 함수를 이용해 a 객체를 만들었다. a 객체의 구조는 다음과 같다.



shape 프로퍼티가 존재하며, __proto__ 프로퍼티는 Shapes의 프로토타입 객체를 가리키고 있다.

이후 a 객체의 color 프로퍼티에 접근을 시도했지만, Shapes 생성자 함수에 color 프로퍼티는 선언되어 있지 않으므로 undefined 를 반환하였다.

다음 코드를 보자.


// Shapes 생성자 함수의 프로토타입 객체에 color 프로퍼티 추가
Shapes.prototype.color = 'red';

Shapes 생성자 함수의 prototype 객체에 color 프로퍼티를 추가한 뒤 'red'로 값을 초기화 해줬다.

확인해보면 다음과 같이 a 객체의 __proto__ 프로퍼티가 가리키는 객체에 color : 'red'가 추가된 것이 보인다.



JavaScript는 객체의 프로퍼티에 접근하려고 할 때, 해당 프로퍼티를 찾을 수 없으면 프로토타입 체인을 통해 __proto__ 프로퍼티가 가리키는 부모 객체를 순회하면서 해당 프로퍼티가 존재하는지 찾는다. 이것이 프로토타입 체이닝 이다.

위의 예제에서는 a 객체에 color가 없으므로, 프로토타입 체이닝이 일어나 Shapes.prototype에 존재하는 color 프로퍼티의 값을 읽어왔으므로 'red'를 출력하게 됐다.

이 예제에서 알 수 있는 사실은 동적으로 추가된 프로퍼티도 프로토타입 체이닝에 반영된다는 것이다.


또한 hasOwnProperty() 혹은 toString() 같은 메서드는 모두 Object.prototype의 메서드인데, JavaScript의 모든 객체에서 해당 메서드가 사용 가능한 이유는 Object.prototype 객체가 프로토타입 체이닝의 종착지이기 때문이다.

0개의 댓글