저번 프로토타입의 기본 개념에 이어서 포스팅을 이어가보자
19-5. 프로토타입의 생성 시점
프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성된다. 계속해서 이어지는 내용인데, 결국 생성된 객체와 생성자 함수, 그리고 프로토타입 객체는 모두 유기적으로 연결되어 있기 때문이다.
생성자 함수가 이미 빌트인으로 주어지는 것이 있고(예를 들면 Object, Array....) 우리가 직접 생성자 함수를 만드는 경우가 있다. 이 경우 프로토타입은 언제 생성이 될까? 차이점이 있을 것 같다.
우선 우리가 직접 생성자 함수를 만드는 경우를 예를 들어보자
console.log(Person.prototype); // { constructor: f }
// 생성자 함수
function Person(name){
this.name = name;
};
생성자 함수를 선언한것보다 먼저 콘솔로그를 통해 프로토타입 객체여부를 확인하였다. 확인 결과 프로토타입 객체는 이미 생성되어 있었다. 얼추 눈치 챌만한 것은 이전에 배웠던 호이스팅과 연관이 있다. 즉 생성자 함수를 만들 수 있는 함수 선언문의 경우 선언과 초기화가 동시에 일어나고, 런타임으로 코드가 실행되기 전에 전역객체의 프로퍼티로 추가되게 된다. 이 내용까지는 이미 알고있던 부분이고, 지금 사실로 알 수 있는 점은 우리가 생성한 생성자 함수가 선언이 될 때, 그러면서 함수 객체가 되었을 때, 이때 프로토타입도 더불어 생성이 된다.
이러한 점에서 생성자 함수를 만들지 못하는 화살표 함수의 경우 프로토타입 객체 역시 생성되지 않는다.
const Person = name => {
this.name = name;
};
console.log(Person.prototype) // undefined
생성된 프로토타입은 오로지 constructor 프로퍼티만을 가진다. 여기에 생성자 함수로서 추가로 메서드를 상속시켜줄 수 있는 것이다. 근데 이렇게 생성한 메서드 뿐 아니라 Object 의 메서드를 그대로 사용할 수도 있다. 즉 Object 의 메서드도 상속이 되었다는 것인데, 이는 간단한 이유다. 프로토타입 객체 역시 객체이기 때문에 Object.prototype 객체를 가지게 되고, 이로서 상속을 받을 수가 있다.
그럼 우리가 생성한 생성자 함수가 아닌, 빌트인 함수들은 어떠할까? 일단 위와 동일하게 생성자 함수가 생성되는 시점에 프로토타입 객체가 생성된다는 점은 동일하다. 직접 선언한 생성자함수야 개발자의 의도라고 친다면, 빌트인 함수는 어떠할까?
빌트인 함수는 전역 객체가 생성되는 시점에서 생성이 된다. 이때 빌트인 함수와 더불어 프로토타입 객체가 생성되고 빌트인 함수의 prototype 프로퍼티에 바인딩 된다. (연결된다는 의미다)
전역객체라는 말이 자주 나오게 되는데, 전역객체는 자바스크립트의 런타임 이전 단계에서 특수하게 생성되는 객체다. 클라이언트 환경에서는 window, 서버 환경에서는(node.js) global 객체를 의미한다. 전역객체는 위에서 언급한 표준 빌트인 객체들과 호스트 객체(web API or node.js API), 선언된 변수와 함수가 프로퍼티로 자리잡는다.
이처럼 전역객체는 가장 초기에 생성된다고 생각하면 편할것같다. 이렇게 초기에 생성이되니 당연히 빌트인 함수에 대한 프로토타입 역시 초기에 생성이 되어있고, 이후 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면 프로토타입은 생성된 객체 [[Prototype]] 내부 슬롯에 할당된다.
19-6. 객체 생성 방식과 프로토타입 결정
객체의 생성법에는 다음과 같이 여러가지가 존재한다.
- 객체 리터럴
- Object 생성자 함수
- 생성자 함수
- Object.create 메서드
- 클래스(ES6)
이전에 객체 리터럴에 의해 생성되는 객체의 경우, 추상 연산 OrdinaryObjectCreate 에 의해 프로토타입을 가지게 된다고 설명한 적이 있다. 여기서 좀 더 보충 설명을 하자면, 우선 위에 있는 모든 객체 생성 방식은 모두 추상 연산을 이용한다. 그리고 추상 연산은 인수를 가지게 되는데 이때 인수가 바로 프로토타입이다. 그렇다면 생성된 객체의 프로토타입은 인수에 따라 달라질 수 있다는 의미일까? 한번 살펴보자
추상 연산은 기본적으로 이러한 과정을 거치는데
- 빈 객체를 생성한다
- 객체에 추가할 프로퍼티가 있으면 객체에 넣어준다
- 다 넣었다면 인수로 전달받은 프로토타입(주소) 를 생성 객체 [[Prototype]] 내부 슬롯에 할당한다
- 객체를 반환한다.
객체 리터럴에 의해 객체를 생성할 경우 추상연산에 전달되는 인수 프로토타입은 Object.prototype 이다. 예를 들어서
const obj = { x : 1};
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty('x')); //true
이렇게 객체 리터럴에 의해 객체를 생성하였다 할 때, 프로토타입은
위 그림처럼 Object.prototype 을 가지게 된다. 그래서 obj 객체는 위 프로토타입의 메서드를 사용할 수 있는것이다. 그림에서 나타나였듯이, 추상 연산은 객체가 생성될 때 객체의 [[Prototype]] 내부 슬롯에 Object.prototype 주소를 넣어둔다. 그래서 상속이 가능한것이고 이 부분은 지금까지 설명했던 부분과 같다.
Object 연산자를 사용해서 객체를 생성할때 역시 리터럴과 같다. Object 연산자를 사용하는 방법은 다음과 같다.
const obj = new Object();
obj.x = 1;
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty('x')) // true
근데, 생성자 함수에 의해 생성된 객체의 프로토타입은 차이가 있다. 추상 연산을 하는것은 동일 한데, 이때 인수로 전달되는 프로토타입이 기존처럼 Object.prototype 이 아니라, 생성자 함수의 prototype 프로퍼티에 바인딩 되어있는 프로토타입 객체다.
function Person(name){
this.name = name;
}
const me = new Person('Lee');
위 코드에서 사용자가 새로운 생성자 함수 Person 을 선언하였다. 선언과 더불어 프로토타입 객체가 Person 과 바인딩 되는데, 이후에 객체 me 를 생성할 때 추상 연산의 인수에 들어오는 프로토타입 객체가 바로 위에서 바인딩 된 객체다.
프로토타입에서 차이가 발생하는데, 기존 Object.prototype 과 달리 프로퍼티가 하나밖에 없음이 차이가 있다. 물론 이건 생성자 함수에 따라 상속시켜줄 메서드나 프로퍼티를 추가시켜줄 수 있다.
Person.prototype.sayHello = function(){
console.log(`Hi! ${this.name}`);
}
me.sayHello(); // Hi! Lee
우린 위에서 생성된 객체 모두 Object.prototype 의 메서드를 사용할 수 있다는 점을 알고 있었다. 위 그림상으로만 보면 상속되어지는 것들은 Object.prototype 에서 오는 것이 아닌데, 어떻게 사용이 가능한 것인가? 연결되어있음을 유추할 수 있지만, 이를 따로 부르는 개념이 있다.
19-7. 프로토타입 체인
이전 예제를 다시 가져와보자
function Person(name){
this.name = name;
}
Person.prototype.sayHello(){
console.log(`Hi! ${this,name}`);
}
const me = new Person('Lee');
// name 프로퍼티를 가지고 있다. 동시에 이 메서드를 사용 가능하다
console.log(me.hasOwnProperty('name'); // true
// 객체 me 의 프로토타입은 Person.prototype 이다.
Object.getPrototypeOf(me) === Person.prototype; // true
// 프로토타입의 프로토타입은 언제나 Object.prototype 이다.
Object.getPrototypeOf(Person.prototype) === Object.prototype; // true
위 코드처럼 생성자 함수로 객체 생성시 프로토타입은 Person.prototype 이지만 Object.prototype 의 메서드인 hasOwnProperty() 를 사용하는 것을 볼 수 있다. 이것이 가능한것은 결국 객체 me 는 프로토타입을 타고 올라가 Object.prototype 을 참조할 수있고 상속받을 수 있기 때문이다.
위 과정을 그림으로 표현하자면
위 그림은 객체지향 프로그래밍에서 의미하는 바가 크다. 여태까지 복잡하게 프로토타입에 대해 공부하였던 이유이기도 하고, 자바스크립트의 코드 메커니즘을 확인할 수 있는 부분이기도 하다.
자바스크립트는 객체의 프로퍼티(메서드 포함)에 접근하려 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부 슬롯의 참조를 따라서 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. 이를 프로토타입 체인이라고 한다. 프로토타입 체인은 자바스크립트가 객체지향 프로그래밍의 상속을 구현하는 메커니즘이다.
순차적인 검색이라는 부분을 좀 더 쉽게 이해해보자
me.hasOwnProperty('name'); // true
예제로 작성한 코드에서 우린 객체 me를 생성하였다. 이제 바로 위 코드 한줄이 어떠한 메커니즘으로 실행되는 지 살펴보자
- 우선 자바스크립트는 스코프의 계층적 구조를 참고하여 식별자 me가 어디에 있는지 부터 파악한다.
- 이후 식별자를 찾았다면, 이 식별자가 객체임을 확인하고 객체의 프로퍼티 중 hasOwnProperty 가 있는지 확인한다.
- 만약 없다면 내부 슬롯을 참조하여 프로토타입으로 체인을 타고 이동한다 (Person.prototype)
- 타고 올라간 프로토타입에서 메서드 hasOwnProperty 가 있는지 확인한다.
- 만약 없다면 Person.prototype 의 내부 슬롯을 참조하여 Object.prototype 으로 체인을 타고 이동한다
- 타고 올라간 프로토타입에서 메서드 hasOwnProperty 가 있느지 확인한다. 그리고 존재함을 확인하였다.
- 그렇기에 자바스크립트는 Object.prototype.hasOwnProperty('name') 을 발동시킨다. 여기서 메서드의 this 를 me로 바인딩한다.
뭔가 길어보이는 과정이지만, 쉽게 생각하여 그냥 스코프 체인을 생각하면 된다. 우선 객체에서 확인하고 없으면 프로토타입으로 가고, 또 없으면 체인의 종점인 Object.prototype 까지 가는 것이다. 이를 통해 유추할 것은 만약 종점에도 없는 프로퍼티라면 당연히 찾을 수 없음을 알려줄 것이라는 것이다.
허나, 한가지 다른점은 만약 찾는 프로퍼티가 존재하지 않느다면 오류가 아니라 undefined 를 반환하게 된다. 이 점을 유의해야 한다.
위에서의 내용을 정리해보면, 자바스크립트는 식별자를 스코프 체인의 계층적인 구조를 통해 검색하고(식별자 검색을 위한 메커니즘), 이후 프로퍼티나 메서드를 프로토타입 체인의 계층적인 구조를 통해 검색한다(프로퍼티 검색을 위한 메커니즘).
결국 스코프체인과 프로토타입 체인은 서로 연관없이 별도로 동작하는 것이 아니라 서로 협력하여 식별자와 프로퍼티를 검색하는데 사용된다.
19-8. 오버라이딩과 프로퍼티 쉐도잉
const Person = (function(){
function Person(name){
this.name = name;
}
Person.prototype.sayHello = function(){
console.log(`Hi! My name is ${this.name}`);
};
return Person;
}());
const me = new Person('Lee');
// 인스턴스 메서드
me.sayHello = function(){
console.log(`Hey! My name is ${this.name}`);
};
// 인스턴스 메서드가 호출된다. 프로토타입 메서드는 인스턴스 메서드에 의해 가려진다
me.sayHello(); // Hey! My name is Lee
// 인스턴스 메서드를 삭제한다
delete me.sayHello;
// 인스턴스 메서드가 사려젔으니 프로토타입 메서드가 등장한다
me.sayHello(); // Hi! My name is Lee
// 한번 더 지우면
delete me.sayHello;
// 프로토타입의 경우 직접 접근하는경우가 아님 지울 수 없다
me.sayHello(); // Hi! My name is Lee
코드를 우선 살펴보고, 설명을 해보겠다.
생성자 함수가 생성되고, 그에 따라 객체 me 를 생성하였다. 객체 me 는 상속에 따라 메서드 sayHello 를 사용할 수 있다. 근데, 상속받는 메서드가 아닌, 인스턴스 자체에 같은 이름을 가지는 메서드를 생성하였다. 그렇다면 만일 이 sayHello 를 호출했을 때 어떤 메서드가 작동을 할 것인가? 인스턴스인가 아님 프로토타입인가
결과적으로는 인스턴스 메서드가 실행되었다. 이를 오버라이딩(overriding) 했다고 표현하는데 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의해서 사용하는 방식이라고 알면 된다. 즉 상위 프로토타입의 메서드를 하위 인스턴스 메서드가 대체 해버린것이다. 이를통해 상위 프로토타입 메서드는 가려지게 되는데 이를 프로퍼티 쉐도잉(Property Shadowing) 이라고 한다.
코드 마지막 부분을 참고하며 알겠지만, 인스턴스 메서드를 삭제하면 이후에는 상속에 따라 프로토타입 프로퍼티 메서드를 불러오게 된다. 그렇다고 한번 더 삭제한다고 프로토타입의 프로퍼티에 접근할 수 있는 것은 아니다.
19-9. 프로토타입 교체
놀랍게도 프로토타입은 임의의 다른 객체로 변경할 수 있다. 다만 이 방법은 추천하지 않는 방법이다. 왜냐하면 기존 생성자 함수와 프로토타입 객체의 관계도 망가질 수 있고, 자바스크립트가 프로토타입을 만들 때 자동적으로 생성하는 프로퍼티 constructor 역시 형성되지 않는다. 물론 이것들을 다시 재 정의하는 방법은 있지만 직접 상속이나 클래스를 활용하는 방법이 비해 효율성이 많이 떨어진다. 그래서 일단 가능하다는 점만 알고있자.
프로토타입의 교체는 생성자 함수 내에서 할 수 있으며, 인스턴스에서 __proto__ 를 통해 변경할 수도 있다. 그림을 통해 보면 모두 바뀐 프로토타입에는 constructor 프로퍼티가 없음을 확인할 수 있고, 당연하게도 이는 바뀐 프로토타입 객체에서 직접 생성해주어야 한다.
const parent = {
constructor : Person,
sayHello(){
console.log(`Hi!, ${this.name}`);
}
}
위 코드처럼 직접 설정해야한다.
그 다음 차이라면 인스턴스에 의한 프로토타입의 경우 기존 생성자 함수 Person 과의 연결관계가 끊키게 된다. Person은 Person.prototype 과 연결되어있었으니, 이를 참고하여 다음과 같이 변경해주면 역시 관계를 회복할 수 있다.
Person.prototype = parent
이정도만 알고 있어도 될것 같다..

이번 포스팅에서 가장 핵심은 스코프 체인과 더불어 프로토타입의 체인을 학습하면서 자바스크립트의 메커니즘에 대한 이해가 아닐까 싶다. 아직 객체 지향에 대해 많이 공부하지 않아, 이러한 메커니즘이 객체 지향 프로그래밍의 핵심 메커니즘이라는 점에서는 감이 잘 오질 않는다.. 추후에 다루게 되면 좀 더 이해도가 높아지지 않을까 기대한다
다음에는 추가적인 메서드나 프로퍼티 열거 방식에 대해 살펴보겠다.
'Programing > Javascript' 카테고리의 다른 글
[Deep Dive] This (0) | 2022.11.26 |
---|---|
[Deep Dive] 프로토타입 - instanceof ~ (1) | 2022.11.22 |
[Deep Dive] 프로토 타입 - 프로토타입이란? (0) | 2022.11.16 |
[Deep Dive] 전역 변수의 문제점 (0) | 2022.11.08 |
[Deep Dive] 스코프 (0) | 2022.11.07 |