앞서 함수의 종류는 2가지가 있다는 것을 알아보았다.
함수를 호출하면, [[Call]], [[Construct]] 내부적으로 두가지 내부 메소드 중 하나가 실행된다.
new
로 호출할 때 [[Construct]], 그냥 호출할 때 [[Call]]이 호출된다고 하였다.
다른 언어에서는 대부분 객체를 만들기 위해 Class를 사용하는데, JS는 특이하게 함수를 2가지 용도로 나누어 [[Construct]]를 호출하면 객체를 생성하도록 구현한 것이다.
생성자 함수는 prototype
객체를 갖고, 동일한 생성자에 의해 만들어진 객체는 prototype
객체를 공유한다.
직전에 봤던 내용이므로 아래 코드를 보면 쉽게 이해할 수 있을 것이다.
function Computer(name) {
this.name = name;
this.notShared() {}
}
Computer.prototype.turnOn = function() {
console.log("Computer turned on");
}
Computer.prototype.turnOff = function() {
console.log("Computer turned off");
}
생성자 함수 내에서 this
로 선언된 속성은 각 객체마다 개별적으로 갖는 값이다.(this.notShared
는 this.name
)
그리고 같은 생성자 함수로 생성된 모든 객체는 동일한 prototype
객체를 공유한다.
Computer
생성자 함수의 prototype
객체 내에 정의된 메소드들은 Computer
생성자로 만들어진 모든 객체가 하나의 함수를 공유하는 형태이기 때문에 메모리 공간을 훨씬 절약할 수 있다.
(Computer.prototype.turnOn
, Computer.prototype.turnOff
)
const com1 = new Computer("com1");
com1.turnOn(); // "Computer turned on"
com1.__proto__.turnOn(); // "Computer turned on"
prototype
객체 내에 정의된 메소드는 일반 속성과 똑같이 접근할 수 있으며,
생성자 함수에서 접근할 때는 Computer.prototype.turnOn
과 같이 접근하지만, 객체에서 접근할 때는 .prototype
에 바로 접근할 수는 없고, 간접적으로 __proto__
를 통해 접근할 수 있다.
그래서 com1.__proto__.turnOn
과 같이 사용한 것이다.
그러나 .__proto__
를 빼도 알아서 접근이 되니, 굳이 .__proto__
를 추가로 쓰면서 사용할 필요는 없을 것이다.
모든 객체는 프로토타입의 계층 구조인 프로토타입 체인에 묶여있다.
JS 엔진은 객체의 속성, 메소드에 접근할 때, 해당 속성, 메소드가 존재하지 않는다면 __proto__
접근자 프로퍼티가 가리키는 생성자 함수의 .prototype
거기에도 없다면 연결된 부모 생성자 함수의 .prototype
을 타고 올라가면서 검색한다.
프로토타입 체인의 종점인 Object.prototype
에 없을 때까지.
function A() {}
A.prototype; // {constructor, [[Prototype]]}
const a = new A();
a.__proto__; // // {constructor, [[Prototype]]}
크롬 콘솔창에서 위 코드를 실행해 보면, 생성자 함수는 .prototype
을, instance는 .__proto__
를 갖고, 그 안에 [[Prototype]] 내부 슬롯이 위치한 것을 알 수 있다.
이 때, instance가 .__proto__
접근자 프로퍼티를 통해 생성자 함수의 .prototype
에 접근하는 이유는 오류를 방지하기 위해서다.
const parent = {};
const child = {};
child.__proto__ = parent;
parent.__proto__ = child; // TypeError: Cyclic __proto__ value
위와 같이 서로가 서로의 프로토타입이 된다면, child -> parent -> child -> parent ...
로 무한 참조가 발생하게 될 것이다.
최종 목적지인 Object.prototype
에 도달할 수 없게 되는 것이다.(무한 루프)
__.proto__
를 통해 이러한 구조로 설정한 경우 오류를 발생시켜 무한 루프를 예방한다.
간접적으로 값을 설정하게 하면서 무한 순환 참조 여부에 대한 검사 로직을 추가하는 듯 하다.
그러나 __proto__
를 코드 내에서 직접 사용하는 것도 비권장 사항이라고 한다.
모든 객체가 __proto__
를 사용할 수 있는 것이 아니기 때문.
const obj = Object.create(null);
obj.__proto__; // undefined
Object.getPrototypeOf(obj); // null
위와 같이 직접 상속을 통해 생성한 객체는 Object.prototype
을 상속받지 않는다.
프로토타입 접근 시, .__proto__
대신 Object.getPrototypeOf
를,
프로토타입 교체 시, Object.setPrototypeOf
를 사용할 것이 권장된다.
const obj = {};
const parent = { x: 1 };
// obj 객체의 프로토타입을 취득
Object.getPrototypeOf(obj); // obj.__proto__;
// obj 객체의 프로토타입을 교체
Object.setPrototypeOf(obj, parent); // obj.__proto__ = parent;
console.log(obj.x); // 1
함수 객체만이 소유하는 .prototype
속성은 생성자 함수가 생성할 객체의 프로토타입을 가리킨다.
일반 객체는 prototype
속성이 없다.
(function() {}).hasOwnProperty("prototype"); // true
{}.hasOwnProperty("prototype"); // false
앞서 봤듯이, 화살표 함수나 메소드 축약 표현으로 선언된 함수는 [[Construct]] 내부 메소드 자체가 없기 때문에 생성자 함수가 될 수 없다.
그래서 당연히 prototype
속성도 없다.
생성자로 사용되지 않는 일반 함수도 prototype
속성을 가지고 있지만 객체를 생성하지 않을 것이기 때문에 의미는 없다.
모든 객체가 가진 Object.prototype
으로부터 상속받은 __proto__
접근자 프로퍼티와 함수 객체만 가지고 있는 prototype
프로퍼티는 결국 동일한 프로토타입을 가리킨다.
사용 주체가 다르니 위 도표를 통해 파악하자.
function Person(name) {
this.name = name;
}
const p1 = new Person('a');
console.log(Person.prototype === p1.__proto__); // true
Person
생성자 함수의 .prototype
과 Person
instance
의 .__proto__
프로퍼티는 결국 동일한 프로토타입(Person.[[Prototype]])을 가리킨다.
모든 프로토타입은 .constructor
프로퍼티를 갖는다.
생성자 함수는 .prototype
을, 프로토타입은 .constructor
로 서로를 참조한다.
그리고 둘 다 .__proto__
를 가지고 있다.
생성자 함수의 .__proto__
는 Function.prototype
을, 프로토타입의 .__proto__
는 부모 프로토타입(최상위는 Object.prototype
)을 가진다.
그리고 Object.prototype
은 최상위 프로토타입이므로, 부모가 존재하지 않기 때문에Object.prototype.__proto__
은 null
이다.
/ 생성자 함수
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// me 객체의 생성자 함수는 Person이다.
console.log(me.constructor === Person); // true
객체는 .constructor
속성이 없지만, 자동으로 .__proto__
를 참조하여 자신을 생성할 때 사용한 프로토타입의 .constructor
을 참조하여 생성자 함수를 반환한다.
이번에는 리터럴로 생성된 객체를 살펴보자.
// obj 객체는 Object 생성자 함수로 생성한 객체가 아니라 객체 리터럴로 생성했다.
const obj = {};
// 하지만 obj 객체의 생성자 함수는 Object 생성자 함수다.
console.log(obj.constructor === Object); // true
모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성됨.
자바스크립트는 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없
다면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으
로 검색한다. 이를 프로토타입 체인이라 한다. 프로토타입 체인은 자바스크립트가 객체지향 프로그래밍의 상
속을 구현하는 메커니즘이다.
function Person(name) {
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};
const me = new Person('Lee');
// hasOwnProperty는 Object.prototype의 메서드다.
console.log(me.hasOwnProperty('name')); // true