- 자바스크립트는 프로토타입을 기반으로 한 객체지향 프로그래밍 언어(prototype-based oop)이다.
- 자바스크립트에서 원시 값을 제외한 모든 것이 객체이다.
이 문서에서는 Class
로 생성된 객체는 다루지 않는다.
- 동일한 생성자 함수에 의해 생성된 모든 인스턴스가 동일한 프로퍼티를 중복하여 가지는 것은 메모리와 성능 면에 비효율적이다. 객체지향 프로그래밍 언어는 상속(inheritance)을 통하여 불필요한 중복을 제거한다.
- 자바스크립트는 프로토타입을 기반으로 상속을 구현한다. 모든 인스턴스는 프로토타입의 프로퍼티를 상속을 통해 공유한다.
function Champion(name) {
this.name = name;
}
Champion.prototype.printName = function () {
console.log(this.name);
};
const c1 = new Champion('lux');
const c2 = new Champion('azir');
c1 === c2; // false
c1.printName === c2.printName; // true
Champion
의 인스턴스 c1
과 c2
는 다른 객체이지만 같은 메서드를 공유한다.
- 프로토타입(prototype; 프로토타입 객체)은 상위 객체로서 다른 객체에 공유 프로퍼티를 제공한다. 즉, 프로퍼티를 상속한다. 프로퍼티를 상속받은 객체는 하위 객체로서 상위 객체의 프로퍼티를 사용할 수 있다.
- 모든 객체는 단 하나의 프로토타입을 가지며, 모든 프로토타입은 생성자 함수와 연결되어 있다.
모든 객체는 프로토타입을 가지며, 이 프로토타입은 내부 슬롯 [[Prototype]]
에 저장된다.
- 모든 객체는 내부 슬롯 **
[[Prototype]]
**을 가지며,[[Prototype]]
은 프로토타입의 참조를 값으로 가진다. 이때 저장되는 프로토타입은 객체를 생성하는 방식에 따라 달라진다. - 모든 객체는
Object.prototype.__proto__
접근자 프로퍼티를 통해[[Prototype]]
이 가리키는 프로토타입에 간접적으로 접근할 수 있다.
- 모든 객체는
__proto__
접근자 프로퍼티를 통해 자신의 프로토타입에 간접적으로 접근할 수 있다.
function Champion(name) {
this.name = name;
}
const champion = new Champion('unknown');
champion.__proto__; // { constructor: ƒ }
__proto__
접근자 프로퍼티를 통해 프로토타입에 접근하면 내부적으로 __proto__
접근자 프로퍼티의 getter 함수인 [[Get]]
이 호출된다. __proto__
접근자 프로퍼티를 통해 새로운 프로퍼티를 할당하면 __proto__
접근자 프로퍼티의 setter 함수인 [[Set]]
이 호출된다.
- 모든 객체는 상속을 통하여
Object.prototype.__proto__
접근자 프로퍼티를 사용할 수 있다.
- 프로토타입 체인은 단방향 연결 리스트로 구현되어 프로퍼티의 검색 방향은 한쪽 방향으로만 흐른다. 두 개의 서로 다른 프로토타입이 서로를 참조하면 순환 참조(circular reference)하는 프로토타입 체인이 만들어져 프로토타입 검색이 무한 루프에 빠지게 되기 때문이다.
- 따라서
__proto__
접근자 프로퍼티를 사용하여 상호 참조하는 경우TypeError
가 발생하도록 한다.
const parent = {};
const child = {};
child.__proto__ = parent;
parent.__proto__ = child; // TypeError: Cyclic __proto__ value
__proto__
프로퍼티는 브라우저 호환성을 고려하여 ES6부터 표준으로 채택된 사양이다. 또한, 객체가 Object.prototype
을 상속받지 않는다면 __proto__
접근자 프로퍼티를 사용할 수 없다. 예를 들어 프로토타입의 종점은 종점으로서 어떤 프로퍼티도 상속받지 않는다.
// obj는 프로토타입의 종점으로 Object.__proto__를 상속받지 않는다.
const obj = Object.create(null);
console.log(obj.__proto__); // undefined
이러한 이유로 __proto__
접근자 프로퍼티 대신 다음 메서드를 사용한다.
Object.getPrototypeOf
(ES5): 프로토타입의 참조를 반환한다.get Object.prototype.__proto__
와 동작이 일치한다.Object.setPrototypeOf
(ES6): 프로토타입을 교체한다.set Object.prototype.__proto__
와 동작이 일치한다.
const obj = Object.create(null);
console.log(Object.getPrototypeOf(obj)); // null
constructor 함수 객체(함수 선언문, 함수 표현식, 클래스로 선언된 함수)만 소유하는 prototype
프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다. 즉, constructor인 함수만이 프로토타입을 생성할 수 있다.
/* constructor 함수 객체는 prototype 프로퍼티를 가진다 */
(function() {}).hasOwnProperty('prototype'); // true
(function() {}).prototype; // {constructor: ƒ}
/* non-constructor 함수 객체는 prototype 프로퍼티를 가지지 않는다 */
(() => {}).hasOwnProperty('prototype'); // false
(() => {}).prototype; // undefined
임의의 생성자 함수에 대하여, 생성자 함수의 prototype
프로퍼티와 생성자 함수로 생성한 인스턴스의 __proto__
는 같은 프로토타입 객체를 참조한다.
function Champion(name) {
this.name = name;
}
const champion = new Champion('rammus');
console.log(Champion.prototype === champion.__proto__); // true
그러나 두 프로퍼티를 사용하는 주체가 다르다.
구분 | 소유 | 값 | 사용 주체 | 사용 목적 |
---|---|---|---|---|
__proto__ 접근자 프로퍼티 |
모든 객체(Object.prototype 으로부터 상속받는다) |
프로토타입의 참조 | 모든 객체 | 객체가 자신의 프로토타입에 접근하기 위해 사용한다. |
prototype 프로퍼티 |
constructor인 함수 객체 | 프로토타입의 참조 | 생성자 함수 | 생성자 함수가 자신이 생성할 인스턴스의 프로토타입을 할당하기 위해 사용한다. |
모든 프로토타입 객체는 constructor
프로퍼티를 가진다. constructor
프로퍼티는 prototype
프로퍼티로 자신을 참조하는 생성자 함수를 참조한다. 자바스크립트 엔진은 생성자 함수가 생성될 때 constructor
프로퍼티와 prototype
프로퍼티에 참조를 할당한다.
function Champion(name) {
this.name = name;
}
const rammus = new Champion('rammus');
console.log(rammus.constructor === Champion); // true
rammus
에는 constructor
프로퍼티가 없지만 rammus
의 프로토타입인 Champion.prototype
에 있는 constructor
프로퍼티를 상속받아 사용할 수 있다.
구분 | 참조 | 소유 |
---|---|---|
Object.prototype.__proto__ |
객체의 프로토타입을 가리킨다 | 모든 객체 |
Object.prototype |
생성자 함수에 바인딩된 프로토타입을 가리킨다 | 생성자 함수 객체 |
Object.prototype.constructor |
프로토타입에 바인딩된 생성자 함수를 가리킨다 | 프로토타입 객체 |
Object === Object.prototype.constructor; // true
Object.prototype === new Object().__proto__; // true
Object.prototype.__proto__ === null; // true
Object.__proto__ === Function.prototype; // true
function Foo () {}
Foo === Foo.prototype.constructor; // true
Foo.prototype === new Foo().__proto__; // true
Foo.prototype.__proto__ === Object.prototype; // true
Foo.__proto__ === Function.prototype; // true
- 생성자 함수는
prototype
프로퍼티를 통해 바인딩된 프로토타입(자신이 생성할 인스턴스의 프로토타입)에 접근할 수 있다. - 프로토타입 객체는
constructor
프로퍼티를 통해 바인딩된 생성자 함수에 접근할 수 있다. - 모든 객체는
__proto__
프로퍼티를 통해 프로토타입에 접근할 수 있다.
- 프로토타입은 생성자가 생성되는 시점에 함께 생성된다. 즉, 프로토타입과 생성자는 단독으로 존재할 수 없고 항상 같이 존재한다.
- 생성자와 생성자에 연결된 프로토타입의 생성 시점은 런타임 이전이다.
- 빌트인 생성자 함수와 그 프로토타입의 생성 시점: 전역 객체가 생성되는 시점(런타임 이전)
- 사용자 정의 생성자 함수와 그 프로토타입의 생성 시점: 함수 정의가 평가되는 시점(런타임 이전)
- 빌트인 생성자 함수 객체와 프로토타입 객체의 생성: 빌트인 생성자 함수는 전역 객체가 생성되는 시점 곧 런타인 이전에 생성된다. 이때 프로토타입도 함께 생성된다.
- 프로퍼티 바인딩
- 생성자의
prototype
프로퍼티에 프로토타입이 바인딩된다. - 프로토타입의
constructor
프로퍼티에 생성자가 바인딩된다.
- 생성자의
- 생성자 함수 객체와 프로토타입 객체의 생성: 함수 호이스팅에 의해 함수 정의가 평가되어 함수 객체가 생성되는 시점은 런타임 이전이다. 따라서 constructor 역시 함수 정의가 평가되어 함수 객체가 생성되는 시점에 생성된다. 이때 프로토타입도 함께 생성된다.
- 프로퍼티 바인딩
- 생성자의
prototype
프로퍼티에 프로토타입이 바인딩된다. - 프로토타입의
constructor
프로퍼티에 생성자가 바인딩된다.
- 생성자의
console.log(Champion.prototype); // {constructor: ƒ}
function Champion(name) {
this.name = name;
}
한편, 프로토타입 역시 객체이므로 자신의 프로토타입에 대한 참조를 값으로 가지는 [[Prototype]]
내부 슬롯을 가진다. 프로토타입의 프로토타입은 Object.prototype
이므로, Champion.prototype
의 프로토타입(Champion.prototype.__proto__
) 역시 Object.prototype
이 된다.
모든 객체가 생성되기 전에 사용자 정의 생성자 함수 객체와 빌트인 생성자 함수 객체의 생성, 그리고 바인딩된 프로토타입의 생성이 완료된다. 생성된 프로토타입은 이후 생성자 함수나 리터럴 표기법으로 생성된 객체의 [[Prototype]]
내부 슬롯에 할당되어 상속된다.
객체가 생성될 때 객체의 __proto__
프로퍼티에 프로토타입 객체가 할당된다. 이때 할당되는 프로토타입은 객체를 생성하는 방식에 따라 달라진다.
- 객체 리터럴
- 생성자 함수
Object.create
메서드- 클래스(ES6)
세부적인 객체 생성 방식에 차이가 있으나 이들 모두 추상 연산 OrdinaryObjectCreate
에 의해 생성된다.
- 추상 연산은 ECMAScript 사양에서 내부 동작의 구현 알고리즘을 표현하기 위해 사용한다.
- 모든 객체는 생성될 때 추상 연산
OrdinaryObjectCreate
가 호출된다.
우선 객체의 생성 방식에 따라 프로토타입이 결정된다. 그다음 OrdinaryObjectCreate
가 호출되어 다음과 같은 순서로 동작한다.
- 호출되며 다음과 같은 인자를 전달받는다.
- (필수) 객체에 대하여 결정된 프로토타입
- (옵션) 객체에 추가할 프로토타입 목록
- 빈 객체를 생성한다. 프로토타입 목록을 전달받았다면 해당 프로퍼티를 생성한 객체에 추가한다.
- 전달받은 프로토타입을 생성한 객체의
[[Prototype]]
내부 슬롯에 할당한다. - 생성한 객체를 반환한다.
다음은 리터럴 표기법으로 객체를 생성한 경우 OrdinaryObjectCreate
에 전달되는 프로토타입이다.
리터럴 | 생성자 함수 | 프로토타입 |
---|---|---|
객체 리터럴({} ) |
Object |
Object.prototype |
함수 리터럴(function () {} ) |
Function |
Function.prototype |
배열 리터럴([] ) |
Array |
Array.prototype |
정규 표현식 리터럴(// ) |
RegExp |
RegExp.prototype |
-
리터럴 표기법으로 생성한 객체와 생성자 함수로 생성한 객체는 세부 처리가 다를 수 있다.
-
그러나 프로토타입과 생성자 함수는 더불어 생성되고
prototype
프로퍼티와constructor
프로퍼티에 의해 연결되어있다. 리터럴로 생성한 객체도 상속을 위해 프로토타입이 필요하므로 가상의 생성자 함수를 가져야 한다. -
따라서 리터럴로 생성된 객체와 생성자 함수로 생성된 객체는 같은 프로토타입을 가진다고 볼 수 있다.
Object
생성자 함수에 인수를 전달하지 않거나 nullish 값을 전달하면 내부적으로 추상 연산OrdinaryObjectCreate
를 호출하여Object.prototype
을 프로토타입으로 갖는 빈 객체를 생성한다.- 객체 리터럴의 평가는
OrderinaryObjectaCreate
를 호출하여 빈 객체를 생성하고 프로퍼티를 추가하여 이루어진다. OrdinaryObjectCreate
를 호출한다는 점에서는 같지만 세부 처리가 다르므로 객체 리터럴로 생성한 객체는Object
생성자 함수로 생성한 객체와 같지 않다.
({}).__proto__ === Object.prototype; // true
Object.getPrototypeOf({}) === Object.prototype; // true
객체 리터럴로 생성한 객체는constructor
프로퍼티나 hasOwnProperty
메서드 등을 가지지 않지만, Object.prototype
의 constructor
프로퍼티나 hasOwnProperty
메서드 등을 상속받아 자신의 것처럼 사용할 수 있다.
Function
생성자 함수로 생성한 함수는 함수 리터럴(함수 선언문, 함수 표현식)을 평가하여 생성한 함수와 달리 스코프와 클로저를 만들지 않는 등 세부 내용이 다르다.
생성자 함수로 객체를 생성하는 경우 OrdinaryObjectCreate
에는 생성자 함수의 prototype
프로퍼티에 바인딩된 프로토타입이 전달된다.
Object.create(프로토타입, {
프로퍼티키1: 프로퍼티 디스크립터1,
프로퍼티키2: 프로퍼티 디스크립터2,
});
Object.create
로 생성한 객체의 프로토타입은 첫번째 인수로 전달한 객체가 된다. 15.8 직접 상속 - Object.create
를 참고한다.
Unless otherwise specified every built-in function and every built-in constructor has the Function prototype object, which is the initial value of the expression
Function.prototype
(20.2.3), as the value of its [[Prototype]] internal slot.Unless otherwise specified every built-in prototype object has the Object prototype object, which is the initial value of the expression
Object.prototype
(20.1.3), as the value of its [[Prototype]] internal slot, except the Object prototype object itself. https://262.ecma-international.org/13.0/#sec-ecmascript-standard-built-in-objects
위와 같은 방법으로 생성하지 않은 객체, 즉 생성자 함수 객체와 이것에 바인딩된 프로토타입 객체의 프로토타입은 어떻게 결정되는가? 기본적으로 함수 객체의 프로토타입은 Function.prototype
이므로, 생성자 함수 객체의 프로토타입 역시 Function.prototype
이다. 이에 따라 Function
생성자는 다른 생성자와 조금 다르다.
Function
생성자는 함수 객체를 생성하므로,Function.prototype
은 함수다. 이와 달리, 다른 생성자는 일반 객체를 생성한다.Function
생성자는 함수 객체이므로, 다른 함수 객체와 마찬가지로 프로토타입으로Function.prototype
을 가진다. 이와 달리, 다른 생성자는 바인딩된 프로토타입을 자신의 프로토타입으로 가지지 않는다.
// Function 생성자의 프로토타입은 Function 생성자에 바인딩된 프로토타입이다.
Function.__proto__ == Function.prototype; // true
/* Function 생성자와 달리, 다른 생성자 함수의 프로토타입은 해당 생성자에 바인딩된 프로토타입이 아니라 Function 생성자에 바인딩된 프로토타입이다.
*/
Object.__proto__ === Object.prototype; // false
Object.__proto__ === Function.prototype; // true
따라서 모든 객체의 프로토타입을 정리하면 다음과 같다.
Object.prototype
의 프로토타입은null
이다.Object.prototype
을 제외한 모든 프로토타입의 프로토타입은Object.prototype
이다.Function.prototype
을 제외한 모든 함수의 프로토타입은Function.prototype
이다.Function.prototype
의 프로토타입은Object.prototype
이다.
따라서 생성자 함수의 프로토타입은 Function.prototype
이다. 한편 생성자 함수에 바인딩된 프로토타입의 프로토타입은 아래와 같다.
Object.prototype
을 제외한 생성자 함수에 바인딩된 프로토타입의 프로토타입은Object.prototype
이다.Object.prototype
의 프로토타입은null
이다.
자바스크립트는 프로토타입 체인으로 객체지향 프로그래밍의 상속 개념을 구현한다. 프로토타입 체인(prototype chain)이란 프로퍼티를 검색하는 방법으로, 객체의 프로퍼티에 접근할 때 해당 객체가 프로퍼티를 가지고 있지 않다면 [[Prototype]]
내부 슬롯이 참조하는 객체를 따라올라가며 프로토타입의 프로퍼티를 순차적으로 검색하는 것이다.
Object.prototype
는 프로토타입 체인의 종점(end of prototype chain)으로서, 프로토타입 체인의 최상위에 위치한다.Object.prototype
에서도 프로퍼티를 찾지 못하는 경우undefined
를 반환한다.Object.prototype
의[[Prototype]]
내부 슬롯의 값은null
이다.
- 현재 객체에서 프로퍼티를 검색한다. 없다면 현재 객체의
[[Prototype]]
내부 슬롯이 바인딩하는 프로토타입 객체로 이동한다. - 프로퍼티를 찾을 때까지
1번
을 반복한다. 찾는다면 해당 프로퍼티를 반환하거나 메서드를 호출한다. 이때 메서드는 자신의this
에 최초의 탐색 객체를 바인딩한다.
생성자 함수에서 프로퍼티를 정의하는 것이 일반적인데 왜 메서드는 프로토타입에 정의하는 걸까?
/* 생성자 함수에서 메서드 정의하기 */
function Foo (name) {
this.name = name;
this.func = function () {
console.log(this.name);
};
}
const foo = new Foo('foo');
foo.func(); // foo
/* 프로토타입에 메서드 정의하기 */
function Bar (name) {
this.name = name;
}
Bar.prototype.func = function () {
console.log(this.name);
}
bar.func(); // bar
위 코드에서 foo.func()
과 bar.func()
모두 객체의 name
프로퍼티를 잘 출력하고 있다. 그래서 후자는 전자의 syntax sugar처럼 보이지만 실제로는 그렇지 않다.
-
프로토타입 체인은 객체가 참조하는 프로토타입에서 프로퍼티를 검색하는 것이다. 즉 생성자 함수에서 정의한 프로퍼티는 프로토타입 체인에 존재하지 않는다.
/* func는 생성자 함수에서 정의되었으므로 프로토타입 체인에 존재하지 않는다 */ console.log(Foo.prototype.hasOwnProperty('func')); // false /* func는 프로토타입에 정의되었으므로 프로토타입에 존재한다 */ console.log(new Bar().hasOwnProperty('func')); // false
-
생성자 함수에서 정의한 프로퍼티는 인스턴스마다 새로이 생성된다. 프로퍼티의 값이 객체라면, 동일한 내용이어도 각 인스턴스마다 새로운 객체가 생성되는 것이다.
/* Foo의 인스턴스는 각자의 func을 가진다 */ console.log(new Foo().hasOwnProperty('func')); // true new Foo().func === new Foo().func; // false /* Bar의 인스턴스는 func을 공유한다 */ console.log(Bar.prototype.hasOwnProperty('func')); // true new Bar().func === new Bar().func; // true
-
이는 비효율적이므로
this
의 특성(메서드로 호출하면this
의 값이 메서드를 호출한 객체가 된다)을 사용하여 메서드를 프로토타입의 프로퍼티로 정의하는 것이 적절하다.
생성자 함수에서 정의한 프로퍼티가 상속되지 않는 것은 Object.create
로 확실히 알 수 있다.
function Foo(name) {
this.name = 'foo';
}
const foo = new Foo();
const bar = Object.create(Foo.prototype);
console.log(foo.__proto__ === bar.__proto__); // true
console.log(foo.name); // foo
console.log(bar.name); // undefined
foo
와 bar
은 같은 프로토타입(Foo.prototype
)을 가지지만 생성자 함수에 정의된 프로퍼티는 프로토타입 체인에 존재하지 않으므로, bar.name
은 undefined
이다.
ES6의 클래스로 살펴보자.
class Foo {
f1 = function() {}
f2() {}
}
class Bar extends Foo {}
console.log(Foo.prototype.hasOwnProperty('f1')); // false
console.log(Foo.prototype.hasOwnProperty('f2')); // true
f1
은 인스턴스마다 생성되는 프로퍼티이지만 f2
는 프로토타입의 프로퍼티이다. 따라서 Foo
의 인스턴스들은 f2
는 같은 함수 객체를 참조하지만 f1
은 각자 가지게 된다.
const b1 = new Bar();
const b2 = new Bar();
console.log(b1.f1 === b2.f1); // false
console.log(b2.f2 === b2.f2); // true
객체가 자신의 프로토타입 체인에 존재하지 않는 메서드를 호출하도록 하고 싶을 수 있다. 가령 인수의 합을 반환하는 함수 sum
을 작성한다고 하자. sum
은 정수의 배열 nums
을 전달받는다.
function sum(nums) {
return nums.reduce((acc, cur) => acc + cur, 0);
}
console.log(sum([1, 2, 3])); // 6
nums
는 Array
의 인스턴스이다. 즉, nums
의 내부 슬롯 [[Prototype]]
은 Array.prototype
을 바인딩한다. 따라서 Array.prototype.reduce
를 사용할 수 있다. 그러나 유사 배열 객체 arguments
는 Array
의 인스턴스가 아니므로 Array.prototype
의 메서드를 사용할 수 없다.
function sum() {
return arguments.reduce((acc, cur) => acc + cur, 0);
}
console.log(sum(1, 2, 3)); // TypeError: arguments.reduce is not a function
이때 Function.prototype.call
이나 Function.prototype.apply
로 Array.prototype.reduce
를 간접 호출할 수 있다.
function sum() {
return Array.prototype.reduce.call(arguments, (acc, cur) => acc + cur, 0);
}
console.log(sum(1, 2, 3)); // 6
Function.prototype.apply/call
를 참고한다.
자바스크립트에서 객체지향 프로그래밍의 오버라이딩(overriding) 개념은 프로퍼티 쉐도잉이라고 한다. 오버라이딩이란 하위 클래스가 상위 클래스의 메서드를 재정의하여 사용하는 것이다. 프로퍼티 쉐도잉(property shadowing)은 계층 관계를 가진 두 객체가 동일한 이름의 프로퍼티를 가질 경우 하위 객체의 프로퍼티가 상위 객체의 프로퍼티를 가리는 것을 의미한다.
프로토타입은 동적으로 교체할 수 있다. 두 가지 방법이 있다.
- 생성자 함수의
prototype
프로퍼티로 모든 인스턴스의 프로토타입 교체하기 - 인스턴스 객체의
__proto__
접근자 프로퍼티로 특정 인스턴스의 프로토타입 교체하기
생성자 함수의 prototype
프로퍼티에 접근하여 프로토타입을 교체할 수 있다. 이 작업은 생성자의 모든 인스턴스 객체에 반영된다.
function Champion(name) {
this.name = name;
}
Champion.prototype = {
sayHi() {
console.log('안녕!');
}
};
const champion = new Champion();
자바스크립트 엔진은 프로토타입을 생성할 때 constructor
프로퍼티에 생성자 함수를 바인딩한다. 이 경우 객체 리터럴에 constructor
프로퍼티를 명시하지 않았으므로 기존의 프로토타입 객체 Champion.prototype
의 constructor
프로퍼티와 생성자 함수 Champion
과의 바인딩이 파괴된다.
console.log(champion.constructor === Champion); // false
console.log(champion.constructor === Object); // true
따라서 프로토타입 체인에 의해 constructor
프로퍼티는 Object
를 반환한다.
Champion.prototype.constructor === Champion; // false
Champion.prototype.constructor === Object; // true
생성자 함수의 prototype
프로퍼티를 직접 할당하면 생성자 함수와 기존의 프로토타입 객체의 constructor
프로퍼티 간의 바인딩이 파괴된다. 여기서는 Champion.prototype
과 Champion
간의 바인딩이 파괴되었다.
인스턴스 객체의 __proto__
접근자 프로퍼티 혹은 Object.setPrototypeOf
메서드를 사용하여 프로토타입을 교체할 수 있다. 이 작업은 해당 인스턴스 객체에만 반영된다.
function Champion(name) {
this.name = name;
}
const champion = new Champion('lux');
const parent = {
displayName() {
console.log(`${this.name}`);
}
}
Object.setPrototypeOf(champion, parent);
이 경우 parent
에 constructor
프로퍼티를 명시하지 않았으므로 인스턴스 객체 champion
의 내부 슬롯 [[Prototype]]
이 바인딩하는 객체의 constructor
프로퍼티는 생성자 함수 Champion
를 바인딩하지 않는다.
console.log(champion.constructor === Champion); // false
console.log(champion.constructor === Object); // true
따라서 인스턴스 객체 chamion
의 constructor
프로퍼티에 접근하면 프로토타입 체인에 의해 constructor
프로퍼티는 Object
를 반환하게 된다.
Champion.prototype.constructor === Champion; // true
Champion.prototype.constructor === Object; // false
인스턴스 객체의 __proto__
접근자 프로퍼티로 직접 할당하면 인스턴스 객체의 내부 슬롯 [[Prototype]]
이 가리키는 객체의 constructor
프로퍼티가 생성자 함수를 바인딩하지 않을 뿐, 기존의 프로토타입 객체의 constructor
프로퍼티와 생성자 함수의 바인딩이 끊어진 것은 아니다. 따라서 Champion.prototype
과 Champion
간의 바인딩이 파괴되지는 않았다.
객체에 명시적으로 프로토타입을 지정하여 상속받도록 할 수 있다. 세 가지 방법이 있다.
Object.create
로 직접 상속하기- 객체 리터럴에서
__proto__
로 직접 상속하기 - ES6 클래스 사용하기
Object.create
로 생성할 객체의 프로토타입을 지정할 수 있다.
Object.create(프로토타입, {
프로퍼티키1: 프로퍼티 디스크립터1,
프로퍼티키2: 프로퍼티 디스크립터2,
});
ESLint는 Object.create(null)
로 프로토타입 체인의 종점을 생성할 수 있으므로 사용을 권장하지 않는다. nullish나 아무 값도 전달하지 않는다면 Object.prototype
의 프로퍼티들을 사용할 수 없다. 이때 Object.prototype
의 빌트인 메서드를 사용하려면 다음과 같이 간접적으로 호출한다.
const obj = Object.create(null);
obj.a = 1;
console.log(Object.prototype.hasOwnProperty.call(obj, 'a'));
객체 리터럴 내부에 __proto__
프로퍼티를 명시하여 객체의 프로토타입을 지정할 수 있다.
const obj = {
__proto__: Object.prototype;
};
- 정적(static) 프로퍼티와 메서드는 인스턴스를 생성하지 않아도 참조하고 호출할 수 있는 프로퍼티와 메서드이다.
- 프로토타입 프로퍼티와 메서드는 생성한 인스턴스에서 참조하고 호출할 수 있는 프로퍼티와 메서드이다.
function Champion() {}
/* 정적 메서드 생성 */
Champion.sayHello = function() {
console.log('Hello world');
};
const champion = new Champion();
Champion.sayHello(); // Hello world
champion.sayHello(); // TypeError: champion.sayHello is not a function
champion
의 프로토타입 체인에 sayHello
가 존재하지 않기 때문에 오류가 발생한다.
프로토타입 메서드 내에서 인스턴스 혹은 프로퍼티를 참조하지 않는다면(this
를 사용하지 않는다면) 정적 메서드로 변경할 수 있다.
function Champion() {}
/* 정적 메서드 생성 */
Champion.prototype.sayHello = function() {
console.log('Hello world');
};
const champion = new Champion();
champion.sayHello(); // Hello world
Champion.sayHello(); // Champion.sayHello is not a function
프로토타입의 sayHello
를 호출하려면 인스턴스를 생성해야 한다.
instaceof
로 객체가 어느 타입의 인스턴스인지 알 수 있다.
객체 instanceof 생성자함수;
객체
의 프로토타입 체인에서 생성자함수
의 prototype
에 바인딩된 객체가 있다면 true
, 그렇지 않다면 false
로 평가한다.
프로퍼티키 in 객체;
프로퍼티키
가 객체
의 프로토타입 체인 상에 존재한다면 true
, 그렇지 않다면 false
로 평가한다.
Reflect.has(객체, 프로퍼티키);
프로퍼티키
가 객체
의 프로토타입 체인 상에 존재한다면 true
, 그렇지 않다면 false
로 평가한다. in
연산자와 동일하게 동작한다.
객체.hasOwnProperty(프로퍼티키);
객체
가 고유하게 프로퍼티키
를 가지면 true
, 그렇지 않다면(상속받았다면) false
로 평가한다.
for (변수선언문 in 객체) {}
객체
의 프로토타입 체인에 존재하는 프로토타입 객체의 모든 열거가능한 프로퍼티를 순회하며 열거한다. 열거가능한 프로퍼티는 내부 슬롯 [[Enumerable]]
의 값이 true
이다. Symbol
형의 프로퍼티는 기본적으로 열거하지 않는다. 대부분의 모던 브라우저는 정수 프로퍼티에 대해서는 정렬하고 그 외의 프로퍼티는 정의된 순서로 나열하는 것을 보장한다.
배열의 경우 프로퍼티를 모두 열거하고 싶은 것이 아니라면 for
문, for ... of
문 또는 Array.prototype.forEach
를 사용하는 것이 적절하다.
Object.keys(객체);
객체
의 열거가능한 프로퍼티를 배열로 반환한다.
Object.values(객체);
객체
의 열거가능한 프로퍼티의 값을 배열로 반환한다.
Object.entries(객체)
객체
의 열거가능한 프로퍼티의 키와 값을 담은 배열들의 배열로 반환한다.
- 클래스 기반 언어에서 클래스(class)는 추상적이며 객체는 클래스를 인스턴스화한 인스턴스(instance)이다.
- 프로토타입 기반 언어는 프로토타입(prototypical object)를 가진다.
- 새로운 객체가 초기 프로퍼티를 가지도록 템플릿으로서 사용된다.
- 모든 객체는 다른 객체에 대한 프로토타입이 될 수 있다.
- 모든 객체는 생성될 때 혹은 런타임에 프로퍼티를 생성할 수 있다.
클래스 기반 | 프로토타입 기반 | |
---|---|---|
클래스와 인스턴스 | 클래스와 인스턴스는 별개의 개체이다. | 모든 객체는 다른 객체로부터 상속한다. |
정의 | 클래스 정의로 클래스를 정의한다. 생성자 함수로 클래스를 인스턴스화한다. |
생성자 함수로 일련의 객체들을 정의하고 생성한다. |
새로운 객체 생성하기 | new 연산자로 단일한 객체 생성 |
동일함. |
객체 계층의 구성 | 존재하는 클래스(superclass)로부터 서브클래스(subclass)를 정의한 클래스 정의를 사용하여 객체 계층 구성 | 객체를 생성자 함수와 연관된 프로토타입으로 할당하여 객체 계층 구성 |
상속 모델 | 클래스 체인을 통한 프로퍼티 상속 | 프로토타입 체인을 통한 프로퍼티 상속 |
프로퍼티 동적 생성 | 클래스 정의는 클래스의 모든 인스턴스의 모든 프로퍼티를 정의. 런타임에 동적으로 프로퍼티 수정 불가능 |
생성자 함수 혹은 프로토타입이 일련의 초기 프로퍼티들을 명시 일련의 객체들과 개별적인 객체에 동적으로 프로퍼티 추가 혹은 삭제 가능 |
- MDN Object Model
- MDN 상속과 프로토타입 체인
- 모던 자바스크립트 Deep Dive 19장: 프로토타입