Part 2 글의 마지막에도 적어둔 것 처럼 이번에는 프로토타입 체인관련 글을 적고 마무리를 해보려고 한다.
이전에 Part 1 글에서 잠시 언급한 적이 있는데 그때의 말을 잠시 가져오면
프로토타입체인은 검색을 순서대로 할 수 있게 만든 단방향 링크드 리스트이다.
어떤 객체에 접근해서 특정 프로퍼티를 찾는데 없다면 JavaScript엔진은 __proto__ 접근자 프로퍼티가 가리키는 참조를 따라서 자신의 부모역할을 하는 프로토타입의 프로퍼티를 순차적으로 접근한다.
대략적으로 단방향 링크드 리스트이고 객체의 프로토타입을 연결해 둔 것이라고 생각할 수 있다.
코드로 한번 알아보자
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () = {
console.log(`Hi my name is ${this.name}`);
};
const me = new Person('Yoo');
console.log(me.hasOwnProperty('name')); // true
우리는 hasOwnProperty라는 메서드를 추가한 적이 없는데 잘 동작하는 것을 볼 수 있다. 이 hasOwnProperty는 Object.prototype의 메서드이다. 언제 이 프로토타입이 연결된 것일까?
위 코드의 구조를 한번 확인해보면 다음과 같다.
(출처: 모던 자바스크립트 Deep Dive 그림 19-18 프로토타입 체인)
앞전까지는 Person 생성자 함수, Person.prototype, 인스턴스 이렇게 3개까지만 있었는데 Function.prototype, Object.prototype 이 두 개는 어느 타임에 생긴 것일까?
우선 Person 생성자 함수가 평가되는 과정에서 Person.prototype이 생성되고 prototype, constructor 프로퍼티에 서로 연결이 됬을 것이다.
Function.prototype은 언제 연결되었을까? 한번 천천히 생각해본다.
위와 같은 과정으로 연결이 되었을 것이다.
그러면 Person.prototype과 Object.prototype은 언제 연결이 되었을까?
마지막으로 볼 것은 Function.prototype과 Object.prototype의 연결인데 이는 어떻게 연결이 되었을지 유추해본다. 이 부분은 정말 유추를 하는 부분으로 오류가 있거나 추가해주고 싶은 내용이 있으면 부탁드린다.
이렇게 정리를 하면 위 그림이 어떻게 연결된 것인지 대략적으로 이해할 수 있다.
그러면 Object.prototype의 프로토타입은 없는 것일까?
정답은 '없다' 이다. (정확하게 말하면 [[Prototype]] 내부 슬롯이 null 이다.)
프로토타입의 최상위에 위치하는 객체는 언제나 Object.prototype이다. 따라서 모든 객체는 Object.prototype을 상속받는다고 생각하면 된다. 이러한 이유 때문에 Object.prototype을 프로토타입 체인의 종점이라고 한다.
구조 설명으로 인해서 조금 말이 내려왔는데 이제 프로토타입 체인에 대해서 알아보자.
자바스크립트는 객체의 프로퍼티, 메서드에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. 이를 프로토타입 체인이라고 한다.
me.hasOwnProperty('name')이 어떠한 과정을 거쳐서 이루어지는 것인지 확인해보자
Object.prototype.hasOwnProperty.call(me, 'name');
오버라이딩이란 무엇일까? 일반적으로 오버라이딩이 무엇인지 알아보자
오버라이딩이란 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의해서 사용하는 방식이다.
코드를 통해서 확인해보자
const Person = (function () {
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () { // 1번
console.log(`Hi my name is ${this.name}`);
};
return Person;
}());
const me = new Person('Yoo');
me.sayHello = function () { // 2번
console.log(`Ha Ha Ha I'm ${this.name}`);
};
me.sayHello(); // Ha Ha Ha I'm Yoo
왜 me.sayHello()의 결과가 Ha Ha Ha 인지 생각해보면 오버라이딩이 어떻게 이루어진 것인지 알 수 있다. 한번 확인해보자
이유는 단순하다. 프로토타입 체인을 올라가지 않아도 이미 메서드가 있어서 해당 메서드를 실행한 것이다.
이처럼 프로토타입을 거슬러 올라가지 않게 하위 객체에서 메서드를 재정의 하는 것을 오버라이딩이라 하며, 상속 관계에 의해 프로퍼티가 가려지는 현상을 프로퍼티 섀도잉이라 한다.
우리는 instanceof 연산자가 있다는 것을 알고있다.
작성자 본인은 그냥 단순하게 우변의 생성자 함수를 통해 좌변의 인스턴스를 생성한건지에 대한 유무를 체크하는 연산자인줄 알았다. 하지만 정의를 보면 뭔가 다르다.
instanceof는 우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인상에 존재하면 true, 아니면 false를 반환한다.
말이 조금 어려운데 정리하면 생성자 함수를 체크하는 것이 아니라 프로토타입을 체크하는 것이다.
생성자 함수와 쌍을 이룬 프로토타입 객체가 인스턴스의 프로토타입 체인상에 존재하는지를 체크한다는 것이다. 코드로 살펴보자
function Person(name) {
this.name = name;
}
const me = new Person('Yoo');
const parent = {};
// me의 prototype을 parent로 바꿔준다.
Object.setPrototypeOf(me, parent);
// 연결이 다 끊어진 모습을 볼 수 있다.
console.log(Person.prototype === parent); // false
console.log(parent.constructor === Person); // false
console.log(me instanceof Person); // false 4번
console.log(me instanceof Object); // true 5번
위 코드는 지금 me 객체의 프로토타입을 교체해주고 결과를 확인한 코드이다.
내가 처음 생각한 instanceof 의 개념으로는 4번 코드의 결과는 true 여야 할 것인데 false가 나왔다.
이는 Person.prototype이 me 객체의 프로토타입 체인 상에서 존재하지 않기 때문이다.
me 객체 입장에서 프로토타입 체인을 생각해보면 me => parent => Object.prototype 이 될 것이다. 이렇기 때문에 5번 코드의 결과가 true가 나온 것이다.
자바스크립트에서 객체를 생성하는 방식이 여러가지가 있던 것을 확인했고 그중 아직 확인하지 못한 방법 중 Object.create 메서드를 한번 알아보자
Object.create 메서드는 명시적으로 프로토타입을 지정하여 새로운 객체를 생성한다. 이 방식도 OrdinaryObjectCreate를 호출해서 내부적으로 생성한다는 것은 동일하다.
Object.create 메서드의 첫 번째 매개변수에는 생성할 객체의 프로토타입으로 지정할 객체를 전달한다. 두 번째 매개변수에는 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터 객체로 이루어진 객체를 전달한다. 두 번째 매개변수는 옵션이니 뭐지 싶어도 걱정하지 않아도 된다. 코드로 알아보자
// 1번
let obj = Object.create(null);
console.log(Object.getPrototype(obj) === null); // true
// Object.prototype을 상속받지 못해서 에러가 발생한다.
console.log(obj.toString()); // TypeError
// 2번
const myProto = { x: 10 };
obj = Object.create(myProto);
// 객체 리터럴을 이용해 생성한 객체도 상속이 가능하다.
console.log(obj.x); // 10
console.log(Object.getPrototypeOf(obj) === myProto); // true
// 옵션
// 두 번째 인자를 사용한다면 다음과 같이 프로퍼티 디스크립터 객체를 사용한 객체로 전달해야한다.
obj = Object.create(Object.prototype, {
x: { value: 1, writalbe: true, enumerable: true, configurable: true }
});
1번 코드는 프로토타입에 null 값을 주었을 경우 생성된 객체는 프로토타입이 없는 것을 보여준다. 이렇게 생성된 객체는 프로토타입 체인의 종점에 위치하게 된다.
2번 코드는 객체 리터럴을 이용해서 생성한 객체를 프로토타입으로 전달해서 객체를 만드는 모습을 보여준다.
이처럼 Object.create 메서드는 첫 번째 매개변수에 전달한 객체의 프로토타입 체인에 속하는 객체를 생성한다. 이러한 방법을 객체를 생성하면서 직접적으로 상속을 구현하는 직접 상속이라고 한다. 이 메서드의 장점은 위에서도 조금 봤지만
이러한 장점들이 있다고 할 수 있다.
하지만 단점들도 있다. 바로 프로퍼티를 정의하는 방법이 너무 귀찮은 것이다. 그러면 간단하게 하는 방법이 있을까?
객체 리터럴 내부에서 __proto__에 의한 직접 상속 방법이 있다.
const myProto = { x: 10 };
const obj = {
y: 20,
__proto__: myProto
};
console.log(obj.x, obj.y); // 10 20
console.log(Object.getPrototypeOf(obj) === myProto); // true
다음과 같이 __proto__ 접근자 프로퍼티를 이용해서 직접 상속을 하는 방법이 있다.
이렇게 길게 글을 적게될 줄 몰랐는데... 3개나 썼다.
더 놀라운 것은 이렇게 3개의 글을 적었지만 내가 모든 프로토타입 관련 지식을 알게된 것도 아니라는 것이다.
또 기회가 된다면 더 깊게 파서 글을 적어보려고 한다.
처음에는 글을 쓰면서 학습을 할 때 '뭔말이여 이게?!?' 하는 내용들이 많기도 했고 내가 가지고 있는 배경지식이 짧아서 해당 내용을 학습하는데에도 시간이 오래 걸렸다.
그렇지만 이렇게 나름은 내 스스로의 기준에 만족스러운 정도로 글을 마무리하니 뿌듯하다.
다음 이렇게 긴 글은 아마 실행 컨텍스트 관련 글을 작성할 때 나오지 않을까 싶다.
여기까지 읽어주시는 분이 계실까도 모르겠지만 만약 있으시다면 보잘 것 없는 글을 읽어줘서 감사하다는 말을 전하고 싶다.