diff --git a/book/docs/chapter_19.md b/book/docs/chapter_19.md new file mode 100644 index 0000000..4ab4af5 --- /dev/null +++ b/book/docs/chapter_19.md @@ -0,0 +1,1095 @@ +### Chapter 19 - 프로토타입 + +자바스크립트는 명령형(imperative), 함수형(functional), 프로토타입 기반(prototype-based), +객체 지향 프로그램이(OOP; Object Oriented Programming)을 지원하는 멀티 페러다임 프로그래밍 언어입니다. + +자바스크립트를 이루고 있는 거의 "모든 것"이 객체입니다. +원시 타입(primitive type)을 제외한 나머지 값들(함수, 배열, 정규 표현식 등)은 모두 객체입니다. + +

+ +### 객체 지향 프로그래밍 +객체 지향 프로그래밍은 실세계의 실체(사물이나 개념)을 인식하는 철학적 사고를 프로그래밍에 접목하려는 시도에서 시작합니다. +실체는 특징이나 성질을 나타내는 **속성(attribute/property)**을 가지고 있고, +이를 통해 실체를 인식하거나 구별할 수 있습니다. + +사람의 "이름"과 "주소"라는 속성을 갖는 `person`이라는 객체를 자바스크립트로 표현하면 아래와 같을 것입니다. + +``` js +const person = { + name: 'Lee', + address: 'Seoul' +}; + +console.log(person); +``` + +위와 같이 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료구조를 객체라고 합니다. + +

+ +조금 더 나아가서 원(Circle)이라는 개념을 객체로 구현해보도록 합시다. +원에는 반지름이라는 속성이 있고, 이 반지름을 이용해 지름, 둘레, 넓이 등을 구할 수 있습니다. +이때 반지름은 원의 상태를 나타내는 데이터이고, 원의 지름, 둘레, 넓이를 구하는 것은 동작으로 정의내릴 수 있습니다. + +``` js +const circle = { + radius: 5, + + // 원의 지름: 2r + getDiameter() { + return 2 * this.radius; + }, + + // 원의 둘레: 2πr + getPerimeter() { + return 2 * Math.PI * this.radius; + }, + + // 원의 넓이: πrr + getArea() { + return Math.PI * this.radius ** 2; + } +}; +``` + +이처럼 객체 지향 프로그래밍은 객체의 상태(state)를 나타내는 데이터와 +상태 데이터를 조작할 수 있는 동작(behavior)를 하나의 논리적인 단위로 묶어 생각할 수 있습니다. + +각 객체는 고유의 기능을 갖는 독립적인 부품으로 볼 수도 있지만 +자신의 고유한 기능을 수행하면서 다른 객체와 관계성을 가질 수도 있다. + +

+ +### 상속과 프로토타입 +상속은 객체지향 프로그래밍의 핵심 개념이며, +어떤 객체의 프로퍼티 또는 메소드를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 말합니다. + +자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거합니다. +중복을 제거하는 방법은 기존의 코드를 적극적으로 재사용하는 것입니다. + +``` js +// 생성자 함수 +function Circle(radius) { + this.radius = radius; + this.getArea = function () { + return Math.PI * this.radius ** 2; + }; +} + +// 반지름이 1인 인스턴스 생성 +const circle1 = new Circle(1); + +// 반지름이 2인 인스턴스 생성 +const circle2 = new Circle(2); + +// Circle 생성자 함수는 인스턴스를 생성할 때마다 동일한 동작을 하는 +// getArea 메소드를 중복 생성하고 모든 인스턴스가 중복 소유합니다. +// getArea 메소드는 하나만 생성하여 모든 인스턴스가 공유해서 사용하는 것이 바람직합니다. +console.log(circle1.getArea === circle2.getArea); // false + +console.log(circle1.getArea()); +console.log(circle2.getArea()); +``` + +
+ +`Circle` 생성자 함수가 생성하는 모든 객체(인스턴스)는 `radius` 프로퍼티와 `getArea` 메소드를 갖습니다. +`radius` 프로퍼티 값은 일반적으로 인스턴스마다 다르지만 `getArea` 메소드는 모든 인스턴스가 동일한 내용의 메소드를 사용하므로 +단 하나만 생성하여 모든 인스턴스가 공유해서 사용하는 것이 바람직합니다. + +그런데 `Circle` 생성자 함수는 인스턴스를 생성할 때마다 `getArea` 메소드를 중복해서 생성하고 모든 인스턴스가 중복으로 소유하고 있습니다. + +image + +
+ +이처럼 동일한 생성자 함수에 의해 생성된 모든 인스턴스가 동일한 메소드를 중복 소유하는 것은 메모리를 불필요하게 낭비합니다. +또한 인스턴스를 생성할 때마다 메소드를 생성하므로 퍼포먼스에도 악영향을 줍니다. +만약 10개의 인스턴스를 생성하면 내용이 동일한 메소드로 10개 생성됩니다. + +
+ +상속을 통해 불필요한 중복을 제거할 수 있습니다. +**자바스크립트는 프로토타입(prototype)을 기반으로 상속을 구현합니다.** + +``` js +// 생성자 함수 +function Circle(radius) { + this.radius = radius; +} + +// Circle 생성자 함수가 생성한 모든 인스턴스가 getArea 메소드를 +// 공유해서 사용할 수 있도록 프로토타입에 추가합니다. +// 프로토타입은 Circle 생성자 함수의 prototype 프로토타입에 바인딩되어 있습니다. +Circle.prototype.getArea = function() { + return Math.PI * this.radius ** 2; +}; + +// 반지름이 1인 인스턴스 생성 +const circle1 = new Circle(1); + +// 반지름이 2인 인스턴스 생성 +const circle2 = new Circle(2); + +// Circle 생성자 함수는 인스턴스를 생성할 때마다 동일한 동작을 하는 +// getArea 메소드를 중복 생성하고 모든 인스턴스가 중복 소유합니다. +// getArea 메소드는 하나만 생성하여 모든 인스턴스가 공유해서 사용하는 것이 바람직합니다. +console.log(circle1.getArea === circle2.getArea); // true + +console.log(circle1.getArea()); +console.log(circle2.getArea()); +``` + +image + +
+ +`Circle` 생성자 함수가 생성한 모든 인스턴스는 자신의 프로토타입, +즉 상위(부모) 객체 역할을 하는 `Circle.prototype`의 모든 프로퍼티와 메소드를 상속받습니다. + +`getArea` 메소드는 단 하나만 생성되어 프로토타입인 `Circle.prototype`의 메소드로 할당되어 있습니다. +따라서 `Circle` 생성자 함수가 생성되는 모든 인스턴스는 `getArea` 메소드를 상속받아 사용할 수 있습니다. + +즉, 자신의 상태를 나타내는 `radius` 프로퍼티만 개별적으로 소유하고 내용이 동일한 메소드는 상속을 통해 공유하여 사용하는 것입니다. +상속은 코드의 재사용이란 관점에서 매우 유용합니다. +메소드를 프로토타입에 미리 구현해두면 모든 인스턴스는 별도의 구현 없이 상위(부모) 객체인 프로토타입의 자산을 공유하여 사용할 수 있습니다. + +

+ +#### 프로토타입 객체 +프로토타입 객체(또는 줄여서 프로토타입)란 객체지향 프로그래밍의 근간을 이루는 객체 간 상속 을 구현하기 위해 사용됩니다. +프로토타입은 어떤 객체의 상위(부모) 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티(메서드 포함)를 제공합니다. +프로토타입을 상속받은 하위(자식) 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용할 수 있습니다. + +모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가지며, [[Prototype]]에 저장되는 프로토타입은 객체 생성 방식에 의해 결정됩니다. +즉, 객체가 생성될 때 객체 생성 방식에 따라 프로토타입이 결정되고 [[Prototype]]에 저장됩니다. + +> 예를 들어, 객체 리터럴에 의해 생성된 객체의 프로토타입은 `Object.prototype`이고 +> 생성자 함수에 의해 생 성된 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체다. +모든 객체는 하나의 프로토타입을 갖습니다. 그리고 모든 프로토타입은 생성자 함수와 연결되어 있습니다. +즉, 객체와 프로토타입과 생성자 함수는 다음 그림과 같이 서로 연결되어 있습니다. + +

+ +image + +[[Prototype]] 내부 슬롯에는 직접 접근할 수 없지만, 위와 같이 `__proto__` 접근자 프로퍼티를 통해 +자신의 프로토타입, 즉 자신의 [[Prototype]] 내부 슬롯이 가리키는 프로토타입에 간접적으로 접근할 수 있습니다. + +그리고 프로토타입은 자신의 constructor 프로퍼티를 통해 생성자 함수에 접근할 수 있고, +생성자 함수는 자신의 prototype 프로퍼티를 통해 프로토타입에 접근할 수 있습니다. + +

+ +##### __proto__ 접근자 프로퍼티 +모든 객체는 `__proto__` 접근자 프로퍼티를 통해 자신의 프로토타입, +즉 [[Prototype]] 내부 슬롯이 가리키는 프로토타입에 **간접적으로 접근할 수 있습니다.** + +image + + +

+ +`__proto__`는 접근자 프로퍼티이다. + +16.1절 "내부 슬롯과 내부 메서드"에서 살펴보았듯이 내부 슬롯은 프로퍼티가 아닙니다. +따라서 자바스크립트는 원칙적으로 내부 슬롯과 내부 메서드에 직접적으로 접근하거나 호출할 수 있는 방법을 제공하지 않습니다. + +단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단을 제공합니다. + +[[Prototype]] 내부 슬롯이 _proto__ 접근자 프로퍼티를 통해 간접적으로 [[Prototype]] 내부 슬롯의 값, 즉 프로토타입에 접근할 수 있습니다. + +16.3.2절 "접근자 프로퍼티"에서 살펴본 것처럼 +접근자 프로퍼티는 자체적으로는 값([[Value]] 프로퍼티 어트리뷰트)을 갖지 않고 +다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수(accessor function), +즉 [[Get]], [[Set]] 프로퍼티 어트리뷰트로 구성된 프로퍼티입니다. + +
+ +image + + +``` js +const obj = {}; +const parent = { x: 1 }; + +// getter 함수인 get __proto__ 가 호출되어 obj 객체의 프로토타입을 취득 +obj.__proto__; + +// setter 함수인 set __proto__ 가 호출되어 obj 객체의 프로토타입을 교체 +obj.__proto__ = parent; + +console.log(obj.x); // 1 +``` + +

+ +`__proto__` 접근자 프로퍼티는 상속을 통해 사용된다. + +`__proto__` 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라 `Object.prototype`의 프로퍼티입니다. +모든 객체는 상속을 통해 `Object.prototype.__proto__` 접근자 프로퍼티를 사용할 수 있습니다. + +``` js +const person = { name: 'Lee' }; + +// person 객체는 __proto__ 프로퍼티를 소유하지 않습니다. +console.log(person.hasOwnProperty('__proto__')); // false + +// __proto__ 프로퍼티는 모든 객체의 프로토타입 객체인 Object.prototype의 접근자 프로퍼티입니다. +console.log(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__')); +// {get: f, set: f, enumerable: false, configurable: true} + +// 모든 객체는 Object.prototype의 접근자 프로퍼티 __proto__를 상속받아 사용할 수 있습니다. +console.log({}.__proto__ === Object.prototype); // true +``` + +
+ +> ※ `Object.prototype` +> 모든 객체는 프로토타입의 계층 구조인 프로토타입 체인에 묶여 있다. +> 자바스크립트 엔진은 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때 +> 해당 객체에 접근하려는 프로퍼티가 없다면 `__proto__` 접근자 프로퍼티가 가리키는 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. +> +> 프로토타입 체인의 종점, 즉 프로토타입 체인의 최상위 객체는 `Object.prototype`이며, +> 이 객체의 프로퍼티와 메서드는 모든 객체에 상속된다. +

+ +`__proto__` 접근자 프로퍼티를 통해 프로토타입에 접근하는 이유 +[[Prototype]] 내부 슬롯의 값, 즉 프로토타입에 접근하기 위해 접근자 프로퍼티를 사용하는 이유는 +상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서입니다. + +``` js +const parent = {}; +const child = {}; + +// child의 프로토타입을 parent로 설정 +child.__proto__ == parent; + +// parent의 프로토타입을 child로 설정 +parent.__proto__ = child; // TypeError: Cyclic __proto__ value +``` + +
+ +위 예제에서는 `parent` 객체를 `child` 객체의 프로토타입으로 설정한 후, +`child` 객체를 `parent` 객체의 프로토타입으로 설정했습니다. + +이러한 코드가 에러 없이 정상적으로 처리되면 +서로가 자신의 프로토타입이 되는 비정상적인 프로토타입 체인이 만들어지기 때문에 +`__proto__` 접근자 프로퍼티는 에러를 발생시킵니다. + +image + +> 서로가 자신의 프로토타입이 되는 비정상적인 프로토타입 체인 +

+ +이런 문제 때문에 프로토타입 체인은 단방향 Linked List로 구현되어야 합니다. +따라서 아무런 체크 없이 무조건적으로 프로토타입을 교체하는 로직이 발생하지 않도록 +`__proto__` 접근자 프로퍼티를 통해 프로토타입에 접근하고 교체하도록 구현되어 있습니다. + + + +

+ +`__proto__` 접근자 프로퍼티를 코드 내에서 직접 사용하는 것은 권장하지 않는다. +코드 내에서 `__proto__` 접근자 프로퍼티를 직접 사용하는 것은 권장하지 않습니다. +모든 객체가 `__proto__` 접근자 프로퍼티를 사용할 수 있는 것은 아니기 때문입니다. + +직접 상속을 통해 아래와 같이 `Object.prototype`을 상속받지 않는 객체를 생성할 수도 있기 때문에 +`__proto__` 접근자 프로퍼티를 사용할 수 없는 경우가 있습니다. + +``` js +// obj는 프로토타입 체인의 종점이다. 따라서 Object.__proto__를 상속받을 수 없습니다. +const obj = Object.create(null); + +// obj는 Object.__proto__를 상속받을 수 없습니다. +console.log(obj.__proto__); // undefined + +// 따라서 __proto__보다 Object.getPrototypeOf 메소드를 사용하는 것이 바람직하다. +console.log(Object.getPrototypeOf(obj)); // null +``` + +따라서 프로토타입의 참조를 얻고 싶은 경우에는, `Obejct.getPrototypeOf` 메소드를 사용하고, +프로토타입을 교체하고 싶은 경우에는 `Object.setPrototypeOf` 메소드를 사용할 것을 권장합니다. + +``` js +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` 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킵니다. + +``` js +// 함수 객체는 prototype 프로퍼티를 소유한다. +(function() {}).hasOwnProperty('prototype'); // true + +// 일반 객체는 prototype 프로퍼티를 소유하지 않는다. +({}).hasOwnProperty('prototype'); // false +``` + +
+ +`prototype` 프로퍼티는 생성자 함수가 생성할 객체(인스턴스)의 프로토타입을 가리킵니다. + +따라서 생성자 함수로서 호출할 수 없는 함수, 즉 non-constructor인 화살표 함수와 ES6 메소드 축약 표현으로 정의한 메소드는 +`prototype` 프로퍼티를 소유하지 않으며 프로토타입도 생성하지 않습니다. + +``` js +// 화살표 함수는 non-constructor 입니다. +const Person = name => { + this.name = name; +}; + +// non-constructor는 prototype 프로퍼티를 소유하지 않는다. +console.log(Person.hasOwnProperty('prototype')); // false + +// ES6의 메소드 축약 표현으로 정의한 메소드는 non-constructor다. +const obj = { + foo() {} +}; + +// non-constructor는 prototype 프로퍼티를 소유하지 않는다. +console.log(obj.foo.hasOwnProperty('prototype')); // false + +// non-constructor는 프로토타입을 생성하지 않는다. +console.log(obj.foo.prototype); // undefined +``` + +
+ +모든 객체가 가지고 있는 `__proto__` 접근자 프로퍼티와 +함수 객체만이 가지고 있는 `prototype` 프로퍼티는 결국 동일한 프로토타입을 가리킵니다. + +|구분|소유|값|사용 주체|사용 목적| +|--|--|--|--|--| +|`__proto__` 접근자 프로퍼티|모든 객체|프로토타입의 참조|모든 객체|객체가 자신의 프로토타입에 접근 또는 교체하기 위해 사용| +|`prototype` 프로퍼티|constructor|프로토타입의 참조|생성자 함수|생성자 함수가 자신이 생성할 객체(인스턴스)의 프로토타입을 할당하기 위해 사용| + +

+ +``` js +// 생성자 함수 +function Person(name) { + this.name = name; +} + +const me = new Person('Lee'); + +// Person.prototype와 me.__proto__는 동일한 프로토타입을 가리킨다. +console.log(Person.prototype === me.__proto__); // true +``` + +
+ +image + +
+ +모든 인스턴스는 결국 같은 prototype을 사용한다. + + +

+ +#### 프로토타입의 constructor 프로퍼티와 생성자 함수 +모든 프로토타입은 `constructor` 프로퍼티를 갖습니다. +이 `constructor` 프로퍼티는 `prototype` 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킵니다. + +``` js +// 생성자 함수 +function Person(name) { + this.name = name; +} + +const me = new Person('Lee'); + +// me 객체의 생성자 함수는 Person +console.log(me.constructor === Person); // true +``` + +image + + + +

+ +### 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입 +생성자 함수에 의해 생성된 인스턴스는 프로토타입의 constructor 프로퍼티에 의해 생성자 함수와 연결됩니다. +> constructor 프로퍼티가 가리키는 생성자 함수는 인스턴스를 생성한 생성자 함수다. +``` js +// obj 객체를 생성한 생성자 함수는 Object이다. +const obj = new Object(); +console.log(obj.constructor === Object); // true + +// add 함수 객체를 생성한 생성자 함수는 Function이다. +const add = new Function('a', 'b', 'return a + b'); +console.log(add.constructor === Function); // true + +// 생성자 함수 +function Person(name) { + this.name = name; +} + +// me 객체를 생성한 생성자 함수는 Person이다. +const me = new Person('Lee'); +console.log(me.constructor === Person); // true +``` + +
+ +하지만 리터럴 표기법에 의한 객체 생성 방식과 같이 명시적으로 `new` 연산자와 함께 생성자 함수를 호출하여 +인스턴스를 생성하지 않는 객체 생성 방식도 있습니다. + +``` js +// 객체 리터럴 +const obj = {}; + +// 함수 리터럴 +const add = function (a, b) { + return a + b; +} + +// 배열 리터럴 +const arr = [1, 2, 3]; + +// 정규 표현식 리터럴 +const regexp = /is/ig; +``` + +
+ +리터럴 표기법에 의해 생성된 객체도 프로토타입이 존재합니다. + +하지만 리터럴 표기법에 의해 생성된 객체의 경우 +프로토타입의 constructor 프로퍼티가 가리키는 생성자 함수가 반드시 객체를 생성한 생성자 함수라고 단정할 수는 없습니다. + +``` js +// obj 객체는 Object 생성자 함수로 생성한 객체가 아니라 객체 리터럴로 생성했다. +const obj = {}; + +// 하지만 obj 객체의 생성자 함수는 Object 생성자 함수이다. +console.log(obj.constructor === Obejct); // true +``` + +위 예제의 obj 객체는 Object 생성자 함수로 생성한 객체가 아니라 객체 리터럴에 의해 생성된 객체입니다. +하지만 obj 객체는 Object 생성자 함수와 constructor 프로퍼티로 연결되어 있습니다. + +그렇다면 객체 리터럴에 의해 생성된 객체는 사실 Object 생성자 함수로 생성되는 것은 아닐까? +ECMAScript 사양을 살펴봅시다. + +> Object 생성자 함수에 인수를 전달하지 않거나 `undefined` 또는 `null`을 인수로 전달하면서 호출하면 +> 내부적으로는 추상 연산 `OrdinaryObjectCreate`를 호출하여 `Object.prototype`을 프로토타입으로 갖는 빈 객체를 생성합니다. +이는 "빈 객체를 생성할 때, 전역 prototype (`Object.prototype`)을 가지는 객체가 생성된다."와 같습니다. + +

+ +※ 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입 +|리터럴 표기법|생성자 함수|프로토타입| +|---|---|---| +|객체 리터럴|`Object`|`Object.prototype`| +|함수 리터럴|`Function`|`Function.prototype`| +|배열 리터럴|`Array`|`Array.prototype`| +|정규표현식 리터럴|`RegExp`|`ExgExp.prototype`| + + +

+ +### 프로토타입의 생성 시점 +객체는 리터럴 표기법 또는 생성자 함수에 의해 생성되므로 결국 모든 객체는 생성자 함수와 연결되어 있습니다. + +프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성됩니다. +프로토타입과 생성자 함수는 단독으로 존재하지 않고 언제나 쌍으로 존재하기 때문입니다. + +생성자 함수는 사용자가 직접 정의한 사용자 정의 생성자 함수와 자바스크립트가 기본 제공하는 빌트인 생성자 함수로 구분할 수 있습니다. +사용자 정의 생성자 함수와 빌트인 생성자 함수를 구분하여 프로토타입 생성 시점에 대해 살펴볼 것입니다. + +

+ +#### 사용자 정의 생성자 함수와 프로토타입 생성 시점 +내부 메소드 `[[Construct]]`를 갖는 함수 객체, +즉 화살표 함수나 ES6의 메소드 축약 표현으로 정의하지 않고 +일반 함수(함수 선언문, 함수 표현식)로 정의한 함수 객체는 `new` 연산자와 함께 생성자 함수로서 호출할 수 있습니다. + +``` js +// 함수 정의(constructor)가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성됩니다. +console.log(Person.prototype); // {constructor: f} + +// 생성자 함수 +function Person(name) { + this.name = name; +} +``` + +생성자 함수로서 호출할 수 없는 함수인 `non-constructor`는 프로토타입이 생성되지 않습니다. + +``` js +// 화살표 함수는 non-constructor 입니다. +const Person = name => { + this.name = name; +}; + +// non-constructor는 프로토타입이 생성되지 않습니다. +console.log(Person.prototype); // undefined + + +// 메소드 축약 표현 객체 리터럴은 non-constructor 입니다. +const person = { + name: 'Lee', + setName(name) { + this.name = name; + } +} + +// non-constructor는 프로토타입이 생성되지 않습니다. +console.log(person.setName.prototype); // undefined +``` + +
+ +"12장 함수 생성 시점과 함수 호이스팅"에서 보았듯이, +함수 선언문은 런타임 이전에 자바스크립트 엔진에 의해 먼저 실행됩니다. + +따라서 함수 선언문으로 정의된 `Person` 생성자 함수는 어떤 코드보다 먼저 평가되어 함수 객체가 됩니다. +이때 프로토타입도 더불어 생성됩니다. + +생성된 프로토타입은 `Person` 생성자 함수의 `prototype` 프로퍼티에 바인딩됩니다. + +image + +생성된 프로토타입은 오직 `constructor` 프로퍼티만 갖는 객체로 보입니다. +프로토타입도 객체이고, 모든 객체는 프로토타입을 가지므로 프로토타입도 자신의 프로토타입을 가집니다. + +> 생성된 프로토타입의 프로토타입은 `Object.prototype` 입니다. +image + +
+ +정리하자면, **사용자 정의 생성자 함수는 자신이 평가되어 함수 객체로 생성되는 시점에 프로토타입도 더불어 생성되며,** +**생성된 프로토타입의 프로토타입은 언제나 `Object.prototype`입니다.** + +

+ +#### 빌트인 생성자 함수와 프로토타입 생성 시점 +`Object`, `String`, `Number`, `Function`, `Array`, `RegExp`, `Date`, `Promise` 등과 같은 빌트인 생성자 함수도 일반 함수와 마찬가지로 빌트인 생성자 함수가 생성되는 시점에 프로토타입이 생성됩니다. + +모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성됩니다. + +생성된 프로토타입은 빌트인 생성자 함수의 `prototype` 프로퍼티에 바인딩됩니다. + +image + +
+ +이처럼 객체가 생성되기 이전에 생성자 함수와 프로토타입은 이미 객체화되어 존재합니다. +이후 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면 +프로토타입은 생성된 객체의 `[[Prototype]]` 내부 슬롯에 할당됩니다. 이후 생성된 객체는 프로토타입을 상속받습니다. + +


+ +### 객체 생성 방식과 프로토타입의 결정 +객체는 아래와 같이 다양한 생성 방법이 있습니다. +- 객체 리터럴 +- `Object` 생성자 함수 +- 생성자 함수 +- `Object.create` 메소드 +- 클래스(ES6) + +

+ +이처럼 다양한 방식으로 생성된 모든 객체는 각 방식마다 세부적인 객체 생성 방식의 차이는 있으나 +추상 연산 `ordinaryObjectCreate`에 의해 생성된다는 공통점이 있습니다. + +이는 즉, 프로토타입은 추상 연산 `OrdinaryObjectCreate`에 전달되는 인수에 의해 결정됩니다. +이 인수는 객체가 생성되는 시점에 객체 생성 방식에 의해 결정됩니다. + +

+ +#### 객체 리터럴에 의해 생성된 객체의 프로토타입 +자바스크립트 엔진은 리터럴을 평가하여 객체를 생성할 때 추상 연산 `OrdinaryObjectCreate`를 호출합니다. + +이때 추상 연산 `OrdinaryObjectCreate`에 전달되는 프로토타입은 `Object.prototype`입니다. +즉, 객체 리터럴에 의해 생성되는 객체의 프로토타입은 `Object.prototype`입니다. + +``` js +const obj = { + x: 1 +}; +``` + +위 객체 리터럴이 평가되면 추상 연산 `OrdinaryObjectCreate`에 의해 +다음과 같이 `Object` 생성자 함수와 `Object.prototype`과 생성된 객체 사이에 연결이 만들어집니다. + +image + + +
+ +이와 같이 객체 리터럴에 의해 생성된 `obj` 객체는 `Object.prototype`을 프로토타입으로 갖게 되며, +`Object.prototype`을 상속받습니다. + +`obj` 객체는 `constructor` 프로퍼티와 `hasOwnProperty` 메소드 등을 소유하지는 않지만 +자신의 프로토타입인 `Object.prototype`의 `constructor` 프로퍼티와 `hasOwnProperty` 메소드를 +자신의 자산인 것처럼 사용 가능합니다. + +> `obj` 객체가 자신의 프로토타입인 `Object.prototype` 객체를 상속받았기 때문이다. +``` js +const obj = { + x: 1 +}; + + +// 객체 리터럴에 의해 생성된 obj 객체는 Object.prototype을 상속받습니다. +console.log(obj.constructor === Object); // true +console.log(obj.hasOwnProperty('x')); // true +``` + +

+ +#### Object 생성자 함수에 의해 생성된 객체의 프로토타입 +`Object` 생성자 함수를 인수 없이 호출하면 빈 객체가 생성됩니다. +`Object` 생성자 함수를 호출하면 객체 리터럴과 마찬가지로 추상 연산 `OrdinaryObjectCreate`가 호출됩니다. + +이때 추상 연산 `OrdinaryObjectCreate`에 전달되는 프로토타입은 `Object.prototype`입니다. +즉, `Object` 생성자 함수에 의해 생성되는 객체의 프로토타입은 `Object.prototype`입니다. + +``` js +const obj = new Object(); +obj.x = 1; +``` + +

+ +위 코드가 실행되면 추상 연산 `OrdinaryObjectCreate`에 의해 +아래와 같이 `Object` 생성자 함수와 `Object.prototype`과 생성된 객체 사이에 연결이 만들어집니다. +> 객체 리터럴에 의해 생성된 객체와 동일한 구조를 갖게 된다. +image + +
+ +``` js +const obj = new Object(); +obj.x = 1; + +// Object 생성자 함수에 의해 생성된 obj 객체는 Object.prototype을 상속받습니다. +console.log(obj.constructor === Object); // true +console.log(obj.hasOwnProperty('x')); // true +``` + +

+ +#### 생성자 함수에 의해 생성된 객체의 프로토타입 +`new` 연산자와 함께 생성자 함수를 호출하여 인스턴스를 생성하면 +다른 객체 생성 방식과 마찬가지로 추상 연산 `OrdinaryObjectCreate`가 호출됩니다. + +즉, 생성자 함수에 의해 생성되는 객체의 프로토타입은 +생성자 함수의 `prototype` 프로퍼티에 바인딩되어 있는 객체입니다. + + +``` js +function Person(name) { + this.name = name; +} + +const me = new Person('Lee'); +``` + +위 코드가 실행되면 추상 연산 `OrdinaryObjectCreate`에 의해 +다음과 같이 생성자 함수와 생성자 함수의 `prototype` 프로퍼티에 바인딩되어 있는 객체와 생성된 객체 사이에 연결이 만들어집니다. + +image + +
+ +표준 빌트인 객체인 `Object` 생성자 함수와 더불어 생성된 프로토타입 `Object.prototype`은 +다양한 빌트인 메소드(`hasOwnProperty`, `propertyIsEnumerable` 등)를 가지고 있습니다. + +하지만 사용자 정의 생성자 함수와 더불어 생성된 프로토타입 `Person.prototype`의 프로퍼티는 `constructor` 뿐입니다. + +
+ +``` js +function Person(name) { + this.name = name; +} + +// 프로토타입 메소드 +Person.prototype.sayHello = function() { + console.log(`Hi! my name is ${this.name}`); +}; + +const me = new Person('Lee'); +const you = new Person('Kim'); + +me.sayHello(); +you.sayHello(); +``` + +`Person` 생성자 함수를 통해 생성된 모든 객체는 프로토타입에 추가된 `sayHello` 메소드를 상속받아 +자신의 메소드처럼 사용할 수 있습니다. + +image + +

+ +또한 자바스크립트는 프로토타입 체인을 통해 빌트인 메소드를 사용할 수 있도록 지원합니다. + +

+ +### 프로토타입 체인 + +``` js +function Person(name) { + this.name = name; +} + +// 프로토타입 메소드 +Person.prototype.sayHello = function() { + console.log(`Hi! my name is ${this.name}`); +}; + +const me = new Person('Lee'); + +// hasOwnPeropery는 Object.prototype의 메소드입니다. +console.log(me.hasOwnProperty('name')); // true +``` + +`Person` 생성자 함수에 의해 생성된 me 객체는 `Object.prototype`의 메서드인 hasownProperty를 호출할 수 있습니다. +이것은 me 객체가 `Person.prototype`뿐만 아니라 `Object.prototype`도 상속받았다는 것을 의미합니다. + +
+ +``` js +Object.getPrototypeOf(me) === Person.prototype; // true +``` + +`Person.prototype`의 프로토타입은 `Object.prototype`입니다. +프로토타입의 프로토타입은 언제나 `Object.prototype`인 것입니다. + +``` js +Object.getPrototypeOf(Person.prototype) === Object.prototype; // true +``` +image + +
+ +자바스크립트는 객체의 프로퍼티(메소드 포함)에 접근하려 할 때 +해당 객체에 접근하려는 프로퍼티가 없다면 `[[Prototype]]` 내부 슬롯의 참조를 따라 +자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색하는데, 이것을 프로토타입 체인이라고 합니다. + + +``` js +// me 객체는 프로토타입 체인을 따라 hasOwnProperty 메소드를 검색하여 사용합니다. +me.hasOwnProperty('name'); +``` + +


+ +### 오버라이딩과 프로퍼티 섀도잉 +``` js +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 +``` + +생성자 함수로 객체(인스턴스)를 생성한 다음, 인스턴스에 동일 이름의 메소드를 새로 선언하면 +새로이 생성한 메소드로 **오버라이딩** 됩니다. + +그리고 이 현상에 의해 가려진 원래의 메소드가 사라지는 현상을 **프로퍼티 섀도잉**이라 부릅니다. + +

+ +### 직접 상속 +#### Object.create에 의한 직접 상속 +`Object.create` 메서드는 명시적으로 프로토타입을 지정하여 새로운 객체를 생성합니다. +`Object.create` 메서드도 다른 객체 생성 방식과 마찬가지로 추상 연산 `OrdinaryObjectCreate`를 호출합니다. + +`Object.create` 메서드의 **첫 번째 매개변수에는 생성할 객체의 프로토타입으로 지정할 객체를 전달합니다.** +**두 번째 매개변수에는 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터 객체로 이뤄진 객체를 전달합니다.** + +이 객체의 형식은 `Object.defineProperties` 메서드의 두 번째 인수와 동일합니다. +두 번째 인수는 옵션이므로 생략 가능합니다. + +``` js +/** + * 지정된 프로토타입 및 프로퍼티를 갖는 새로운 객체를 생성하여 반환 + * @param {Object} prototype - 생성할 객체의 프로토타입으로 지정할 객체 + * @param {Object} [propertiesObject] - 생성할 객체의 프로퍼티를 갖는 객체 + * @returns {Object} 지정된 프로토타입 및 프로퍼티를 갖는 새로운 객체 + */ +Object.create(prototype[, propertiesObject]) +``` + +
+ +``` js +// 프로토타입이 null인 객체를 생성합니다. 생성된 객체는 프로토타입 체인 종점에 위치합니다. +// obj → null +let obj = Object.create(null); +console.log(Object.getPrototypeOf(obj) === null); // true + +// Object.prototype을 상속받지 못합니다. +console.log(obj.toString()); // TypeError: obj.toString is not a function + +// obj → Object.prototype → null +// obj = {};와 동일합니다. +obj = Object.create(Object.prototype); +console.log(Object.getPrototypeOf(obj) === Object.prototype); // true + +// obj → Object.prototype → null +// obj = { x: 1 };와 동일합니다. +obj = Object.create(Object.prototype, { + x: { value: 1, writable: true, enumerable: true, configurable: true} +}); + +console.log(obj.x); // 1 +console.log(Object.getPrototypeOf(obj) === Object.prototype); // true + +const myProto = { x: 10 }; +// 임의의 객체를 직접 상속받는다. +// obj → myProto → Object.prototype → null +obj = Object.create(myProto); +console.log(obj.x); // 10 +console.log(Object.getPrototypeOf(obj) === myProto); // true + +// 생성자 함수 +function Person(name) { + this.name = name; +} + +// obj → Person.prototype → Object.prototype → null +// obj = new Person('Lee')와 동일합니다. +obj = Object.create(Person.prototype); +obj.name = 'Lee'; +console.log(obj.name); +console.log(Object.getPrototypeOf(obj) === Person.prototype); // true +``` + +위와 같이 `Object.create` 메소드는 첫 번째 매개변수에 전달한 객체의 프로토타입 체인에 속하는 객체를 생성합니다. +즉, 객체를 생성하면서 직접적으로 상속을 구현하는 것입니다. + +장점은 아래와 같습니다. +- `new` 연산자 없이도 객체 생성이 가능합니다. +- 프로토타입을 지정하면서 객체를 생성할 수 있습니다. +- 객체 리터럴에 의해 생성된 객체도 상속받을 수 있습니다. + + +
+ +단 빌트인 메소드를 객체가 직접 호출하는 것은 권장하지 않습니다. +``` js +// 프로토타입이 null인 객체, 즉 프로토타입 체인의 종점에 위치하는 객체를 생성합니다. +const obj = Object.create(null); +obj.a = 1; + +console.log(Object.getPrototypeOf(obj) === null); // true + +// obj는 Object.prototype의 빌트인 메소드를 사용할 수 없다. +console.log(obj.hasOwnProperty('a')); // TypeError: obj.hasOwnProperty is not a function +``` + +
+ +따라서 Object.prototype의 빌트인 메소드는 아래와 같이 간접적으로 호출하는 것이 좋습니다. + +``` js +// 프로토타입이 null인 객체를 생성 +const obj = Object.create(null); +obj.a = 1; + +// Object.prototype의 빌트인 메소드는 객체로 직접 호출하지 않습니다. +console.log(Object.prototype.hasOwnProperty.call(obj, 'a')); // true +``` + +

+ +#### 객체 리터럴 내부에서 __proto__에 의한 직접 상속 +`Object.create` 메소드에 의한 직접 상속은 여러 장점들이 있지만, +**두 번째 인자로 프로퍼티를 정의하는 것이 번거롭습니다.** + +객체를 일단 생성한 다음에 프로퍼티를 추가하는 방법도 있지만, 이 또한 깔끔하지는 않습니다. +ES6에서는 객체 리터럴 내부에서 `__proto__` 접근자 프로퍼티를 사용하여 직접 상속을 구현할 수 있습니다. + +``` js +const myProto = { x: 10 }; + +// 객체 리터럴에 의해 객체를 생성하면서 프로토타입을 지정하여 직접 상속받을 수 있습니다. +const obj = { + y: 20, + // 객체를 직접 상속받는다. + // obj → myProto → Object.prototype → null + __proto__: myProto +}; + +/* 위 코드는 아래와 동일합니다. +const obj = Object.create(myProto, { + y: { value: 20, writable: true, enumerable: true, configurable: true } +}); +*/ + +console.log(obj.x, obj.y); // 10, 20 +console.log(Object.getPrototypeOf(obj) === myProto); // true +``` + +

+ +### 프로퍼티 존재 확인 + +#### in 연산자 +`in` 연산자는 객체 내 특정 프로퍼티가 존재하는지 여부를 확인합니다. +``` js +/** + * key: 프로퍼티 키를 나타내는 문자열 + * object: 객체로 평가되는 표현식 + */ +key in object +``` + +``` js +const person = { + name: 'Lee', + address: 'Seoul' +}; + +// person 객체에 name 프로퍼티가 존재합니다. +console.log('name' in person); // true + +// person 객체에 age 프로퍼티가 존재하지 않습니다. +console.log('age' in person); // false +``` + +`in` 연산자는 확인 대상 객체의 프로퍼티 뿐만 아니라 +확인 대상 객체가 상속받은 모든 프로토타입의 프로퍼티를 확인하므로 주의가 필요합니다. + +``` js +console.log('toString' in person); // true +``` + +`person` 객체에는 `toString`이라는 프로퍼티가 없지만 아래의 실행결과는 true 입니다. +이는 `in` 연산자가 `person` 객체가 속한 프로토타입 체인 상 존재하는 +모든 프로토타입에서 `toString` 프로퍼티를 검색했기 때문입니다. + +

+ +`in` 연산자 대신 ES6에서 도입된 `Reflict.has` 메소드를 사용할 수 있습니다. + +``` js +const person = { + name: 'Lee' +}; + +console.log(Reflict.has(person, 'name')); // true +console.log(Reflict.has(person, 'toString')); // false +``` + +

+ +#### Object.prototype.hasOwnProperty 메소드 +`Object.prototype.hasOwnProperty` 메소드를 사용해도 객체에 특정 프로퍼티가 존재하는지 확인할 수 있습니다. + +```js +console.log(person.hasOwnProperty('name')); // true +console.log(person.hasOwnProperty('age')); // false +``` + +
+ +`Object.prototype.hasOwnProperty` 메소드는 이름에서 알 수 있듯이 +인수로 전달받은 프로퍼티 키가 객체 고유의 프로퍼티 키인 경우에만 `true`를 반환하고 +상속받은 프로토타입의 프로퍼티 키인 경우 `false`를 반환합니다. + +``` js +console.log(person.hasOwnProperty('toString')); // false +``` + +

+ + +### 프로토타입 열거 + +#### for ... in 문 +객체의 모든 프로퍼티를 순회하며 열거하려면 `for ... in` 문을 사용하면 됩니다. + +``` js +const perosn = { + name: 'Lee', + address: 'Seoul' +}; + +// for ... in 문의 변수 prop에 person 객체의 프로퍼티 키가 할당됩니다. +for (const key in person) { + console.log(key + ': ' + person[key]); +} +``` + +

+ +#### Object.keys/values/entries 메소드 +`for ... in` 문은 상속받은 프로퍼티 까지 열거합니다. + +객체 자신의 고유 프로퍼티만 열거하기 위해서는 +**`for ... in` 보다는 `Object.keys/values/entries` 메소드를 사용하는 것이 권장됩니다.** + +``` js +const person = { + name: 'Lee', + address: 'Seoul', + __proto__: { age: 20 } +}; + +console.log(Object.keys(person)); // ["name", "address"] +``` + +
+ +ES8에서 도입된 `Object.values` 메소드는 객체 자신의 열거 가능한 프로퍼티 값을 배열로 반환합니다. + +``` js +console.log(Object.values(person)); // ["Lee", "Seoul"] +``` + +
+ +ES8에서 도입된 `Object.entries` 메소드는 객체 자신의 열거 가능한 프로퍼티 키와 값의 쌍의 배열을 배열에 담아 반환합니다. + +``` js +console.log(Object.entries(person)); // [["name", "Lee"], ["address", "Seoul"]] + +Object.entires(person).forEach(([key, value]) => console.log(key, value)) +/* +name Lee +address Seoul +*/ +``` \ No newline at end of file