Part 1 글의 마지막에도 적어둔 것 처럼 이번에는 이어지는 내용을 알아볼 예정이다.
이렇게 3가지 내용을 알아보려고 한다. 레츠 고!
(출처: https://poiemaweb.com/es6-class)
이전의 글에서 생성자 함수로 객체를 생성할 경우 프로토타입이 위 그림처럼 연결된다는 것을 알아보았다.
위 그림의 연결을 한번 글로 이야기해보자
function Person(name) {
this.name = name;
}
const me = new Person('Yoo');
console.log(me.constructor === Person); // true
// me에서 constructor를 찾음 => 없음 => __proto__ 로 상위 객체 접근
// => Person.prototype의 constructor 발견 => 생성자 함수 접근
// 해당 생성자 함수가 Person이니 true가 된 것이다.
위와 같은 연결이 서로 존재하기 때문에 위 코드와 같은 현상이 발생하는 것이다.
그러면 의문이 있다.
과연 리터럴 표기법의 생성자 함수와 프로토타입 연결이란게 있는가? 있다면 어떻게 연결된 것인가?
const obj = {};
// 함수도 객체다
const add = function (a, b) { return a + b; };
// 배열도 객체다
const arr = [1, 2, 3];
// 정규표현식도 객체다
const regexp = /is/ig;
위에 적힌 객체, 함수, 배열, 정규표현식은 모두 리터럴 표기법으로 만드는 객체이다. 이 모든 애들의 생성자 함수와 프로토타입이 있는 것인가?
일단 결론은 프로토타입이 존재한다!
하지만 리터럴 표기법에 의해 생성된 객체의 경우 프로토타입의 constructor 프로퍼티가 가리키는 생성자 함수가 반드시 객체를 생성한 생성자 함수라고 단정할 수는 없다.
이게 무슨 말일까?
// 객체 리터럴 방식
const obj = {};
console.log(obj.constructor === Object); // true
객체 리터럴 방식을 사용했는데 Object 생성자 함수라고 하는 결과를 보면 의아하다.
한번 생각을 해보면 객체 리터럴 방식으로 생성된 객체는 Object.prototype와 연결되어있고, Object.prototype은 Object 생성자 함수와 연결이 되었기 때문에 해당 결과가 나오는 것 같다.
그러면 여기서 합리적 의심을 할 수 있는 것이, 그냥 객체 리터럴 방식을 사용해서 생성하는 것이 내부적으로는 Object 생성자 함수로 생성되는 것이 아닐까?..?
한번 ECMAScript 사양을 살펴보자
Object([value])
When the Object function is called with optional argument value, the following steps are taken:
1. If NewTarget is neither undefined nor active function, then a Return ? OrdinaryCreateFromConstructor(NewTarget,"%Object.prototype%").
2. If value is undefined or null, return OrdinaryObjectCreate("%Object.prototype%").
3. Return ! ToObject(value).
The "length" property of the Object constructor function is 1.
2번을 보면 Object 생성자 함수에 인수를 전달하지 않거나 undefined 또는 null을 인수로 전달하면서 호출하면 내부적으로는 추상연산(ECMAScript 내부 알고리즘이자 의사코드) OrdinaryObjectCreate를 호출하여 Object.prototype을 프로토타입으로 가지는 빈 객체를 생성한다.
객체 리터럴이 평가될 때는 OrdinaryObjectCreate를 호출하여 빈 객체를 생성하고 프로퍼티를 추가하도록 정의되어 있다.
한마디로 Object 생성자 함수 호출과 객체 리터럴의 평가는 추상 연산 OrdinaryObjectCreate를 호출하여 빈 객체를 생성하는 점에서는 동일하나 new.target의 확인이나 프로퍼티를 추가하는 세부 내용이 다르다.
따라서 정확하게는 객체 리터럴에 의해 생성된 객체는 Object 생성자 함수가 생성한 객체가 아니다.
정리를 하자면 다음과 같다.
이처럼 리터럴 방식으로 생성된 객체는 가상적인 생성자 함수를 가지고 프로토타입이 연결된다.
세부적인 차이가 존재하기는 하지만 크게 그냥 두루뭉술하게 보면 연결되어 있는 생성자 함수를 리터럴 표기법으로 생성한 객체를 생성한 생성자 함수로 생각해도 괜찮다.
위에서 잠깐 나왔지만 프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성이 된다.
이는 프로토타입과 생성자 함수가 단독으로 존재할 수 없고 쌍으로 존재하기 때문이다.
그러면 생성자 함수의 종류에 따라서 프로토타입 생성 시점이 다를 것이다.
생성자 함수는 크게 본인이 만든 사용자 정의 생성자 함수와 자바스크립트가 제공하는 빌트인 생성자 함수로 구분할 수 있다. 두 케이스를 한번 확인해보자
생성자 함수로서 호출할 수 있는 함수, 즉 constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다.
console.log(Person.prototype); // {constructor: f}
function Person(name) {
this.name = name;
}
// 이렇게 표현식을 통한 생성자 함수 선언은 console.log가 안나올 것이다.
// 왜냐? 함수 정의가 평가되는 시점이 런타임시 이 변수에 도달했을 때이기 때문이다.
const Person = function (name) {
this.name = name;
}
이 경우가 왜 동작하는 것일까?
함수 선언문은 런타임 이전에 자바스크립트 엔진에 의해서 미리 평가된다. 이때 함수가 평가되면서 함수 객체가 생기고 이 시점에 프로토타입이 같이 생성이 된다.
이때 생성된 프로토타입은 오직 constructor 프로퍼티만을 가지는 객체이다.
여기서 잠시 생각해 볼 것은 프로토타입도 객체인데 프로토타입 객체의 프로토타입도 있는건가?
당연하다. 이때 프로토타입 객체의 프로토타입은 Object.prototype이다.
정리하자면
빌트인 생성자가 아닌 사용자 정의 생성자 함수는 자신이 평가되어 함수 객체로 생성되는 시점에 프로토타입도 더불어 생성되며, 생성된 프로토타입의 프로토타입은 언제나 Object.prototype이다.
빌트인 생성자 함수 종류
Object, String, Number, Function, Array, RegExp, Date, Promise 등이 있다.
전역객체란?
코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 생성되는 특수한 객체다. 전역 객체는 클라이언트 사이드 환경(브라우저)에서는 window, 서버 사이드 환경(Node.js)에서는 global 객체를 의미한다.
빌트인 생성자 함수도 일반 함수와 마찬가지로 빌트인 생성자 함수가 생성되는 시점에 프로토타입이 결정된다.
이 빌트인 생성자 함수는 우리들이 만드는 것도 아닌데 언제 생성이 되는 것인가?
바로 전역 객체가 생성되는 시점에 생성된다. 이렇게 생성된 프로토타입은 빌트인 생성자 함수의 prototype 프로퍼티에 바인딩된다.
프로토타입의 생성 시점은 이처럼 객체가 생성되기 이전에 생성자 함수와 프로토타입은 이미 객체화되어 존재한다.
이후 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면 프로토타입은 생성된 객체의 [[Prototype]] 내부 슬롯에 할당된다.
자바스크립트라는 사랑스러운 언어는 객체를 생성하는 다양한 방법이 있다.
객체 리터럴, Object 생성자 함수, 생성자 함수, Object.create 메서드, 클래스 이렇게 존재한다.
이렇게 생성된 모든 객체는 앞서 살펴본 것 처럼 OrdinaryObjectCreate에 의해 생성된다는 공통점이 있다.
추상 연산 OrdinaryObjectCreate는 필수적으로 자신이 생성할 객체의 프로토타입을 인수로 전달받는다.
그리고 자신이 생성할 객체에 추가할 프로퍼티 목록을 옵션으로 전달할 수 있다.
OrdinaryObjectCreate는 빈 객체를 생성한 후, 객체에 추가할 프로퍼티 목록이 인수로 전달된 경우 프로퍼티를 객체에 추가한다. 그리고 인수로 전달받은 프로토타입을 자신이 생성한 객체의 [[Prototype]] 내부 슬롯에 할당한 다음 생성한 객체를 반환한다.
정리하면
1. OrdinaryObjectCreate + 프로토타입(인수로 전달) + 프로퍼티 목록(옵션)
2. 빈 객체를 생성한다.
3. 프로퍼티 목록이 있으면 객체에 추가한다.
4. 생성한 객체의 [[Prototype]] 내부 슬롯에 인수로 전달받은 프로토타입을 할당한다.
5. 주문하신 객체 나왔습니다!
자 그러면 저 다양한 생성 방법은 어디가 다른 것일까?
바로 인수에서 차이가 난다. 한번 확인해보자
이거는 앞에서 살펴본 방식이다.
자바스크립트 엔진이 객체 리터럴을 평가할때 추상연산 OrdinaryObjectCreate를 호출한다.
이때 인수로 전달되는 프로토타입은 Object.prototype이다.
const obj = { x: 1 };
console.log(obj.constructor === Object);
console.log(obj.hasOwnProperty('x'));
Object 생성자 함수를 인수 없이 호출하면 빈 객체가 생성된다.
Object 생성자 함수를 호출하면 객체 리터럴과 마찬가지로 추상연산 OrdinaryObjectCreate를 호출한다.
이때 인수로 전달되는 프로토타입은 Object.prototype이다.
const obj = new Object();
obj.x = 1;
console.log(obj.constructor === Object);
console.log(obj.hasOwnProperty('x'));
그러면 어떠한 차이가 있는 것일까?
바로 프로퍼티를 추가하는 방식이 다르다.
객체 리터럴 방식은 객체 리터럴 내부에 프로퍼티를 추가하지만 Object 생성자 함수 방식은 빈 객체를 생성한 후 이후 프로퍼티를 추가해야 한다.
new 연산자와 함께 생성자 함수를 호출하여 인스턴스를 생성하면 동일하게 추상연산 OrdinaryObjectCreate가 호출된다.
이때 전달되는 프로토타입이 다른데 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체이다.
즉, 생성자 함수에 의해 생성되는 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체이다.
function Person(name) {
this.name = name;
}
Person.prototype.hi = function () {
console.log(`Hi! I'm ${this.name}`);
}
const yoo = new Person('Yoo');
const ji = new Person('Ji');
yoo.hi();
ji.hi();
프로토타입 체인도 다뤄보려 했는데 양이 너무 많다...
그리고 프로토타입 체인에 대해서 다루면서 상속, 오버라이딩 같은 내용을 한번에 다루면 좋지 않을까? 하는 생각에 파트를 나눠서 적어보기로 했다.
이번 파트의 내용을 한번 정리해보자
다음 글에서는 어떠한 내용을 다뤄볼지 생각해보면