19장 프로토타입
19.1 객체지향 프로그래밍(19-01)
-
객체는 상태 데이터와 동작을 하나의 논리적인 단위로 묶은 복합적인 자료구조
-
상태(프로퍼티)
// 이름과 주소 속성을 갖는 객체
const person = {
name: 'Lee',
address: 'Seoul'
};
console.log(person); // {name: "Lee", address: "Seoul"}
19-02
- 동작(메서드)
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;
}
};
console.log(circle);
// {radius: 5, getDiameter: ƒ, getPerimeter: ƒ, getArea: ƒ}
console.log(circle.getDiameter()); // 10
console.log(circle.getPerimeter()); // 31.41592653589793
console.log(circle.getArea()); // 78.53981633974483
19.2 상속과 프로토타입(19-03)
-
자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거한다(코드 재사용은 비용을 현저히 줄이는 좋은점)
-
아래 예시는 getArea메서드를 보면 코드 재사용 하면 훨씬 좋은데, 중복으로 생성한다
// 생성자 함수
function Circle(radius) {
this.radius = radius;
this.getArea = function () {
// Math.PI는 원주율을 나타내는 상수다.
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()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172
19-04
-
상속(프로토타입 기반)으로 구현하면 불필요한 중복을 제거한다
-
생성자 함수는 앞서 배운 prototype 프로퍼티 가지니까 이를 이용해 프로토타입을 이용해서 상속까지 할 수 있는것!
// 생성자 함수
function Circle(radius) {
this.radius = radius;
}
// Circle 생성자 함수가 생성한 모든 인스턴스가 getArea 메서드를
// 공유해서 사용할 수 있도록 프로토타입에 추가한다.
// 프로토타입은 Circle 생성자 함수의 prototype 프로퍼티에 바인딩되어 있다.
Circle.prototype.getArea = function () {
return Math.PI * this.radius ** 2;
};
// 인스턴스 생성
const circle1 = new Circle(1);
const circle2 = new Circle(2);
// Circle 생성자 함수가 생성한 모든 인스턴스는 부모 객체의 역할을 하는
// 프로토타입 Circle.prototype으로부터 getArea 메서드를 상속받는다.
// 즉, Circle 생성자 함수가 생성하는 모든 인스턴스는 하나의 getArea 메서드를 공유한다.
console.log(circle1.getArea === circle2.getArea); // true
console.log(circle1.getArea()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172
19.3 프로토타입 객체
-
프로토타입 객체를 프로토타입이라고도 부름(상속에 주 사용)
-
모든 객체는 하나의 프로토타입을 갖고, 모든 프로토타입은 생성자 함수와 연결
19.3.1 __proto__ 접근자 프로퍼티(19-05)
- 브라우저 콘솔에 출력해서 보면 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입을 들여다 볼 수 있다
const person = { name: 'Lee' };
person
19-06
- __proto__는 접근자 프로퍼티다. 이는 앞에서 [[Get]], [[Set]]을 가진다고 했다
const obj = {};
const parent = { x: 1 };
// getter 함수인 get __proto__가 호출되어 obj 객체의 프로토타입을 취득
obj.__proto__;
// setter함수인 set __proto__가 호출되어 obj 객체의 프로토타입을 교체
obj.__proto__ = parent;
console.log(obj.x); // 1
19-07
- __proto__접근자 프로퍼티는 상속을 통해 사용. 즉, 객체 자신이 가지는 프로퍼티가 아니라 Object.prototype의 프로퍼티다(이를 상속을 통해 바로 사용)
const person = { name: 'Lee' };
// person 객체는 __proto__ 프로퍼티를 소유하지 않는다.
console.log(person.hasOwnProperty('__proto__')); // false
// __proto__ 프로퍼티는 모든 객체의 프로토타입 객체인 Object.prototype의 접근자 프로퍼티다.
console.log(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__')); // 즉, Objcet.prototype은 저것을 프로퍼티로 가짐.
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}
// 모든 객체는 Object.prototype의 접근자 프로퍼티 __proto__를 상속받아 사용할 수 있다.
console.log({}.__proto__ === Object.prototype); // true(상속받아서 사용하니까 동일한것을 사용하는거임. 그래서 true)
19-08
- __proto__접근자 프로퍼티를 통해 프로토타입에 접근하는 이유? 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해
// 프로토타입 체인은 단방향 링크드 리스트로 구현되어야 함
const parent = {};
const child = {};
// child의 프로토타입을 parent로 설정
child.__proto__ = parent;
// parent의 프로토타입을 child로 설정
parent.__proto__ = child; // TypeError: Cyclic __proto__ value
19-09
-
__proto__접근자 프로퍼티를 코드 내에서 직접 사용은 권장X 왜냐? __proto__접근자 프로퍼티를 못 사용하는 객체도 존재
-
참고로 프로토타입 종점을 Object.prototype로 생각할것. 맨아래 단계인 obj같은 객체로 생각하면 혼동이 올 수 있음
// obj는 프로토타입 체인의 종점이다. 따라서 Object.__proto__를 상속받을 수 없다. 즉, obj가 null로 프로토타입 종점으로 가졌는데, 원래 종점은? Object.prototype이였다. 따라서 상속 못 받는것이다.
const obj = Object.create(null);
// obj는 Object.__proto__를 상속받을 수 없다.
console.log(obj.__proto__); // undefined
// 따라서 Object.getPrototypeOf 메서드를 사용하는 편이 좋다.
console.log(Object.getPrototypeOf(obj)); // null
19-10
- Object.getPrototypeOf, Object.setPrototypeOf 메서드를 사용하는것을 추천(위의 proto상속을 못받는 경우를 애초에 차단)
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
19.3.2 함수 객체의 prototype 프로퍼티(19-11)
- 함수 객체는 prototype 프로퍼티 소유, 일반 객체는 소유X
// 함수 객체는 prototype 프로퍼티를 소유한다.
(function () {}).hasOwnProperty('prototype'); // -> true
// 일반 객체는 prototype 프로퍼티를 소유하지 않는다.
({}).hasOwnProperty('prototype'); // -> false
19-12
-
함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킴
-
따라서 생성자 함수를 호출할 수 없는 함수. 즉, non-constructor인 화살표함수과 ES6메서드 축약 표현으로 정의한 메서드는 소유X
// 화살표 함수는 non-constructor다.
const Person = name => {
this.name = name;
};
// non-constructor는 prototype 프로퍼티를 소유하지 않는다.
console.log(Person.hasOwnProperty('prototype')); // false
// non-constructor는 프로토타입을 생성하지 않는다.
console.log(Person.prototype); // undefined
// ES6의 메서드 축약 표현으로 정의한 메서드는 non-constructor다.
const obj = {
foo() {}
};
// non-constructor는 prototype 프로퍼티를 소유하지 않는다.
console.log(obj.foo.hasOwnProperty('prototype')); // false
// non-constructor는 프로토타입을 생성하지 않는다.
console.log(obj.foo.prototype); // undefined
19-13
- 사용주체만 다를뿐 모든 객체가 가지는 __proto__ 접근자 프로퍼티와 함수 객체만이 가지는 prototype 프로퍼티는 동일한 프로토타입을 가리킴
// 생성자 함수
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// 결국 Person.prototype과 me.__proto__는 결국 동일한 프로토타입을 가리킨다. (혼동하지 말 것!!)
console.log(Person.prototype === me.__proto__); // true
19.3.3 프로토타입의 constructor 프로퍼티와 생성자 함수(19-14)
-
예로 Person.prototype의 constructor 프로퍼티가 Person(생성자 함수)를 가리킨다는 것!
-
이것은 생성자 함수를통해 생성된 me객체의 __proto__ 프로퍼티가 Person.protytpe과 같다고 위에서 설명했듯이 Person.prototype을 가리키기 때문에 이것의 constructor 프로퍼티를 상속받아 사용할 수 있다(책의 그림을 보는게 더욱 이해하기 편함)
// 생성자 함수
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// me 객체의 생성자 함수는 Person이다.
console.log(me.constructor === Person); // true
19.4 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입(19-15)
- 아래 예시는 위에서 설명한 내용
// 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
19-16
- 우리가 알듯이 기존 리터럴 방식으로 인스턴스를 생성하지 않는 객체 생성 방식도 있다(new 연산자 이용한 생성자 함수 호출방식X)
// 객체 리터럴
const obj = {};
// 함수 리터럴
const add = function (a, b) { return a + b; };
// 배열 리터럴
const arr = [1, 2, 3];
// 정규표현식 리터럴
const regexp = /is/ig;
19-17
- 프로토타입을 가질까?
// obj 객체는 Object 생성자 함수로 생성한 객체가 아니라 객체 리터럴로 생성했다.
const obj = {};
// 하지만 obj 객체의 생성자 함수는 Object 생성자 함수다.
console.log(obj.constructor === Object); // true
19-18
- ECMAScript 사양을보면 생성자 함수에 인수 전달안하거나 undefined 또는 null을 전달하면 내부적으론 추상연산 OrdinaryObjectCreate를 호출하여 Object.prototype을 프로토타입으로 갖는 빈 객체를 생성
// 2. Object 생성자 함수에 의한 객체 생성
// Object 생성자 함수는 new 연산자와 함께 호출하지 않아도 new 연산자와 함께 호출한 것과 동일하게 동작한다.(미리 그렇게 설계함)
// 인수가 전달되지 않았을 때 추상 연산 OrdinaryObjectCreate를 호출하여 빈 객체를 생성한다.
let obj = new Object();
console.log(obj); // {}
// 1. new.target이 undefined나 Object가 아닌 경우
// 인스턴스 -> Foo.prototype -> Object.prototype 순으로 프로토타입 체인이 생성된다.
class Foo extends Object {} // 이런식으로 내부적으로 Object상속중
new Foo(); // Foo {}
// 3. 인수가 전달된 경우에는 인수를 객체로 변환한다.
// Number 객체 생성
obj = new Object(123);
console.log(obj); // Number {123}
// String 객체 생성
obj = new Object('123');
console.log(obj); // String {"123"}
19-19
-
결론은 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재
-
그리고 리터럴 표기법에 의해 생성된 객체도 전부 프로토타입을 가지며 객체 리터럴 : Object.prototype 가짐 함수 리터럴 : Function.prototype 가짐 배열 리터럴 : Array.prototype 가짐 정규 표현식 리터럴 : RegExp.prototype 가짐
// foo 함수는 Function 생성자 함수로 생성한 함수 객체가 아니라 함수 선언문으로 생성했다.
function foo() {}
// 하지만 constructor 프로퍼티를 통해 확인해보면 함수 foo의 생성자 함수는 Function 생성자 함수다.
console.log(foo.constructor === Function); // true
19.5 프로토타입의 생성 시점
- 지금까지 결론은 리터럴 표기법으로 생성한 객체도 생성자 함수와 연결된다는 것을 확인(생성자 함수로 생성했다는건 아님)
19.5.1 사용자 정의 생성자 함수와 프로토타입 생성 시점(19-20)
-
constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성
-
즉, 앞서 배운 개념을 적용하자면 함수 선언문은 런타임 이전에 엔진이 실행하고, 이때 먼저 함수 객체가 생성되는데 프로토타입도 더불어 생성!
// 함수 정의(constructor)가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다.
console.log(Person.prototype); // {constructor: ƒ}
// 생성자 함수
function Person(name) {
this.name = name;
}
19-21
- 화살표 함수는 X
// 화살표 함수는 non-constructor다.
const Person = name => {
this.name = name;
};
// non-constructor는 프로토타입이 생성되지 않는다.
console.log(Person.prototype); // undefined
19.5.2 빌트인 생성자 함수와 프로토타입 생성 시점(19-22)
-
빌트인 생성자 함수 : Object, String,,, 앞서 얘기한 것
-
결론은 객체 생성 이전에 생성자 함수와 프로토타입은 이미 객체화되어 존재, 이후 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면 프로토타입은 생성된 객체의 [[Prototype]] 내부 슬롯에 할당
-
빌트인 객체는 21장에서 자세히 살펴보겠다
// 전역 객체 window는 브라우저에 종속적이므로 아래 코드는 브라우저 환경에서 실행해야 한다.
// 빌트인 객체인 Object는 전역 객체 window의 프로퍼티다.
window.Object === Object // true
19.6 객체 생성 방식과 프로토타입의 결정
19.6.1 객체 리터럴에 의해 생성된 객체의 프로토타입(19-23)
- 책의 그림보는걸 추천
const obj = { x: 1 };
19-24
const obj = { x: 1 };
// 객체 리터럴에 의해 생성된 obj 객체는 Object.prototype을 상속받는다. 아래 메서드 사용은 Object.prototype이 가진 메서드들
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty('x')); // true
19.6.2 Object 생성자 함수에 의해 생성된 객체의 프로토타입(19-25)
-
책의 그림보길 추천
-
결론은 리터럴과 생성자 함수 이용한 객체 생성 둘다 내부적으로 구조 비슷하게 프로토타입 가진다는 의미
const obj = new Object();
obj.x = 1;
19-26
const obj = new Object();
obj.x = 1;
// Object 생성자 함수에 의해 생성된 obj 객체는 Object.prototype을 상속받는다.
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty('x')); // true
19.6.3 생성자 함수에 의해 생성된 객체의 프로토타입(19-27)
- Object같은 빌트인 생성자 함수의 프토토타입은 다양한 메서드들이 이미 존재하는데, 사용자가 만든 생성자 함수의 프로토타입은 constructor 메서드만 존재
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
19-28
- 프로토타입 메서드 추가
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(); // Hi! My name is Lee
you.sayHello(); // Hi! My name is Kim
19.7 프로토타입 체인(19-29)
- 아래 예제의 의미는 Person.prototype뿐만 아니라 Object.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
19-30
Object.getPrototypeOf(me) === Person.prototype; // -> true
19-31
Object.getPrototypeOf(Person.prototype) === Object.prototype; // -> true
19-32
-
이런 구조가 프로토타입 체인(상속을 위한)
-
스코프 체인은? 식별자 검색을 위한
// hasOwnProperty는 Object.prototype의 메서드다.
// me 객체는 프로토타입 체인을 따라 hasOwnProperty 메서드를 검색하여 사용한다.
me.hasOwnProperty('name'); // -> true
19-33
Object.prototype.hasOwnProperty.call(me, 'name');
19-34
console.log(me.foo); // undefined
19-35
me.hasOwnProperty('name');
19.8 오버라이딩과 프로퍼티 섀도잉(19-36)
-
책의 그림이 잘 나와있음
-
설명하자면, 프로퍼티 섀도잉이란 me객체가 Person.prototype을 상속하고 있는 상태에서 아래 코드를 보면 sayHello가 Person.prototype에 메서드로 등록되었는데 me객체가 외부에서 오버라이딩을해서 me객체 안에 sayHello를 등록
-
따라서 Person.prototype에 있던 sayHello는 상속 관계에 의해 가려짐 이 현상을 의미
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
19-37
- 인스턴스 메서드 삭제시? 기존에 존재한 프로토타입 메서드가 호출
// 인스턴스 메서드를 삭제한다.
delete me.sayHello;
// 인스턴스에는 sayHello 메서드가 없으므로 프로토타입 메서드가 호출된다.
me.sayHello(); // Hi! My name is Lee
19-38
- 하위 객체를 통해 프로토타입에 get엑세스 허용, set엑세스 불가
// 프로토타입 체인을 통해 프로토타입 메서드가 삭제되지 않는다.
delete me.sayHello;
// 프로토타입 메서드가 호출된다.
me.sayHello(); // Hi! My name is Lee
19-39
- 즉, 프로토타입에 직접 접근해야 삭제 가능
// 프로토타입 메서드 변경
Person.prototype.sayHello = function () {
console.log(`Hey! My name is ${this.name}`);
};
me.sayHello(); // Hey! My name is Lee
// 프로토타입 메서드 삭제
delete Person.prototype.sayHello;
me.sayHello(); // TypeError: me.sayHello is not a function
19.9 프로토타입의 교체
19.9.1 생성자 함수에 의한 프로토타입의 교체(19-40)
- 객체 리터럴엔 constructor 프로퍼티가 없다. 책의 그림 참고
const Person = (function () {
function Person(name) {
this.name = name;
}
// ① 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
Person.prototype = {
sayHello() {
console.log(`Hi! My name is ${this.name}`);
}
};
return Person;
}());
const me = new Person('Lee');
19-41
- 위 코드의 연장선
// 프로토타입을 교체하면 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴된다.
console.log(me.constructor === Person); // false
// 프로토타입 체인을 따라 Object.prototype의 constructor 프로퍼티가 검색된다.
console.log(me.constructor === Object); // true
19-42
- 직접 추가해서 살릴 수가 있음
const Person = (function () {
function Person(name) {
this.name = name;
}
// 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
Person.prototype = {
// constructor 프로퍼티와 생성자 함수 간의 연결을 설정
constructor: Person,
sayHello() {
console.log(`Hi! My name is ${this.name}`);
}
};
return Person;
}());
const me = new Person('Lee');
// constructor 프로퍼티가 생성자 함수를 가리킨다.
console.log(me.constructor === Person); // true
console.log(me.constructor === Object); // false
19.9.2 인스턴스에 의한 프로토타입의 교체(19-43)
- 이 또한 책의 그림이 잘 설명
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// 프로토타입으로 교체할 객체
const parent = {
sayHello() {
console.log(`Hi! My name is ${this.name}`);
}
};
// ① me 객체의 프로토타입을 parent 객체로 교체한다.
Object.setPrototypeOf(me, parent);
// 위 코드는 아래의 코드와 동일하게 동작한다.
// me.__proto__ = parent;
me.sayHello(); // Hi! My name is Lee
19-44
- 생성자 함수에 의한 프로토타입 교체는 교체된 프로토타입을 생성자 함수가 계속 가리킴, 그러나 인스턴스의 경우 교체된 프로토타입을 가리키지 않음
// 프로토타입을 교체하면 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴된다.
console.log(me.constructor === Person); // false
// 프로토타입 체인을 따라 Object.prototype의 constructor 프로퍼티가 검색된다.
console.log(me.constructor === Object); // true
19-45
- 되살리기(꽤 번거롭다). 즉, 프로토타입은 직접 교체하지 않는걸 추천
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// 프로토타입으로 교체할 객체
const parent = {
// constructor 프로퍼티와 생성자 함수 간의 연결을 설정
constructor: Person,
sayHello() {
console.log(`Hi! My name is ${this.name}`);
}
};
// 생성자 함수의 prototype 프로퍼티와 프로토타입 간의 연결을 설정
Person.prototype = parent;
// me 객체의 프로토타입을 parent 객체로 교체한다.
Object.setPrototypeOf(me, parent);
// 위 코드는 아래의 코드와 동일하게 동작한다.
// me.__proto__ = parent;
me.sayHello(); // Hi! My name is Lee
// constructor 프로퍼티가 생성자 함수를 가리킨다.
console.log(me.constructor === Person); // true
console.log(me.constructor === Object); // false
// 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입을 가리킨다.
console.log(Person.prototype === Object.getPrototypeOf(me)); // true
19.10 instanceof 연산자(19-46)
- 객체 instanceof 생성자 함수 우변의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하는지 검사
// 생성자 함수
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Person); // true
// Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Object); // true
19-47
- 좀 더 자세히 이해해보자
// 생성자 함수
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// 프로토타입으로 교체할 객체
const parent = {};
// 프로토타입의 교체
Object.setPrototypeOf(me, parent);
// Person 생성자 함수와 parent 객체는 연결되어 있지 않다.
console.log(Person.prototype === parent); // false
console.log(parent.constructor === Person); // false
// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하지 않기 때문에 false로 평가된다.
console.log(me instanceof Person); // false
// Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Object); // true
19-48
- 되살리기
// 생성자 함수
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// 프로토타입으로 교체할 객체
const parent = {};
// 프로토타입의 교체
Object.setPrototypeOf(me, parent);
// Person 생성자 함수와 parent 객체는 연결되어 있지 않다.
console.log(Person.prototype === parent); // false
console.log(parent.constructor === Person); // false
// parent 객체를 Person 생성자 함수의 prototype 프로퍼티에 바인딩한다.
Person.prototype = parent;
// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Person); // true
// Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Object); // true
19-49
- instanceof 연산자를 함수로 표현한 것
function isInstanceof(instance, constructor) {
// 프로토타입 취득
const prototype = Object.getPrototypeOf(instance);
// 재귀 탈출 조건
// prototype이 null이면 프로토타입 체인의 종점에 다다른 것이다.
if (prototype === null) return false;
// 프로토타입이 생성자 함수의 prototype 프로퍼티에 바인딩된 객체라면 true를 반환한다.
// 그렇지 않다면 재귀 호출로 프로토타입 체인 상의 상위 프로토타입으로 이동하여 확인한다.
return prototype === constructor.prototype || isInstanceof(prototype, constructor);
}
console.log(isInstanceof(me, Person)); // true
console.log(isInstanceof(me, Object)); // true
console.log(isInstanceof(me, Array)); // false
19-50
const Person = (function () {
function Person(name) {
this.name = name;
}
// 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
Person.prototype = {
sayHello() {
console.log(`Hi! My name is ${this.name}`);
}
};
return Person;
}());
const me = new Person('Lee');
// constructor 프로퍼티와 생성자 함수 간의 연결은 파괴되어도 instanceof는 아무런 영향을 받지 않는다.
console.log(me.constructor === Person); // false
// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Person); // true
// Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Object); // true
19.11 직접 상속
19.11.1 Object.create에 의한 직접 상속(19-51)
- Object.create의 사용방식
// 프로토타입이 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 }
});
// 위 코드는 다음과 동일하다.
// obj = Object.create(Object.prototype);
// obj.x = 1;
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); // Lee
console.log(Object.getPrototypeOf(obj) === Person.prototype); // true
19-52
const obj = { a: 1 };
obj.hasOwnProperty('a'); // -> true
obj.propertyIsEnumerable('a'); // -> true
19-53
- Object.prototype의 빌트인 메서드를 사용할 수 없다(종점에 null 때문)
// 프로토타입이 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
19-54
- 따라서 간접적으로 호출하는 것이 좋다
// 프로토타입이 null인 객체를 생성한다.
const obj = Object.create(null);
obj.a = 1;
// console.log(obj.hasOwnProperty('a')); // TypeError: obj.hasOwnProperty is not a function
// Object.prototype의 빌트인 메서드는 객체로 직접 호출하지 않는다.
console.log(Object.prototype.hasOwnProperty.call(obj, 'a')); // true
19.11.2 객체 리터럴 내부에서 __proto__ 에 의한 직접 상속(19-55)
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
19.12 정적 프로퍼티/메서드(19-56)
-
정적 프로퍼티/메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드를 말함
-
책의 그림에서 보면 Person 생성자 함수안에 정적 프로퍼티, 메서드가 생성됨
// 생성자 함수
function Person(name) {
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};
// 정적 프로퍼티
Person.staticProp = 'static prop';
// 정적 메서드
Person.staticMethod = function () {
console.log('staticMethod');
};
const me = new Person('Lee');
// 생성자 함수에 추가한 정적 프로퍼티/메서드는 생성자 함수로 참조/호출한다.
Person.staticMethod(); // staticMethod
// 정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조/호출할 수 없다.
// 인스턴스로 참조/호출할 수 있는 프로퍼티/메서드는 프로토타입 체인 상에 존재해야 한다.
me.staticMethod(); // TypeError: me.staticMethod is not a function
19-57
-
Object 생성자 함수내에 존재하는 Object.create 정적 메서드이다
-
상속 구조로 생각하면 Object가 젤 위니까 Object 생성자 함수로 생성한 인스턴스는 아래단계인 Object.prototype에 있는 hawOwnProperty는 못 호출한다
-
하지만 Object.prototype은 모든 객체의 프로토타입 체인의 종점이므로 모든 객체가 호출할 수 있다
// Object.create는 정적 메서드다.
const obj = Object.create({ name: 'Lee' });
// Object.prototype.hasOwnProperty는 프로토타입 메서드다.
obj.hasOwnProperty('name'); // -> false
19-58
- 프로토타입 메서드를 호출하려면 인스턴스를 생성해야 하지만 정적 메서드는 인스턴스 생성 없이 호출할 수 있다
function Foo() {}
// 프로토타입 메서드
// this를 참조하지 않는 프로토타입 메소드는 정적 메서드로 변경해도 동일한 효과를 얻을 수 있다.
Foo.prototype.x = function () {
console.log('x');
};
const foo = new Foo();
// 프로토타입 메서드를 호출하려면 인스턴스를 생성해야 한다.
foo.x(); // x
// 정적 메서드
Foo.x = function () {
console.log('x');
};
// 정적 메서드는 인스턴스를 생성하지 않아도 호출할 수 있다.
Foo.x(); // x
19.13 프로퍼티 존재 확인
19.13.1 in 연산자(19-59)
- key in object
const person = {
name: 'Lee',
address: 'Seoul'
};
// person 객체에 name 프로퍼티가 존재한다.
console.log('name' in person); // true
// person 객체에 address 프로퍼티가 존재한다.
console.log('address' in person); // true
// person 객체에 age 프로퍼티가 존재하지 않는다.
console.log('age' in person); // false
19-60
- 상속받은 것까지 확인
console.log('toString' in person); // true
19-61
- in 연산자와 동일한 Reflect.has 메서드
const person = { name: 'Lee' };
console.log(Reflect.has(person, 'name')); // true
console.log(Reflect.has(person, 'toString')); // true
19.13.2 Object.prototype.hasOwnProperty 메서드(19-62)
- Object.prototype.hasOwnProperty 메서드 이용
console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('age')); // false
19-63
- 상속받은건 false!!(기존과의 차이점)
console.log(person.hasOwnProperty('toString')); // false
19.14 프로퍼티 열거
19.14.1 for… in 문(19-64)
const person = {
name: 'Lee',
address: 'Seoul'
};
// for...in 문의 변수 prop에 person 객체의 프로퍼티 키가 할당된다.
for (const key in person) {
console.log(key + ': ' + person[key]);
}
// name: Lee
// address: Seoul
19-65
- 상속받은 것까지 열거하는데, toString이 열거되지 않은 이유? [[Enumerable]]이 false이기 때문!!
const person = {
name: 'Lee',
address: 'Seoul'
};
// in 연산자는 객체가 상속받은 모든 프로토타입의 프로퍼티를 확인한다.
console.log('toString' in person); // true
// for...in 문도 객체가 상속받은 모든 프로토타입의 프로퍼티를 열거한다.
// 하지만 toString과 같은 Object.prototype의 프로퍼티가 열거되지 않는다.
for (const key in person) {
console.log(key + ': ' + person[key]);
}
// name: Lee
// address: Seoul
19-66
- enumerable: false 라는게 확인
// Object.getOwnPropertyDescriptor 메서드는 프로퍼티 디스크립터 객체를 반환한다.
// 프로퍼티 디스크립터 객체는 프로퍼티 어트리뷰트 정보를 담고 있는 객체다.
console.log(Object.getOwnPropertyDescriptor(Object.prototype, 'toString'));
// {value: ƒ, writable: true, enumerable: false, configurable: true}
19-67
- enumerable: true 인 프로퍼티를 순회하며 열거
const person = {
name: 'Lee',
address: 'Seoul',
__proto__: { age: 20 }
};
for (const key in person) {
console.log(key + ': ' + person[key]);
}
// name: Lee
// address: Seoul
// age: 20
19-68
- 심벌인 프로퍼티는 열거하지 않는다
const sym = Symbol();
const obj = {
a: 1,
[sym]: 10
};
for (const key in obj) {
console.log(key + ': ' + obj[key]);
}
// a: 1
19-69
- 상속받은 프로퍼티 제외하고 열거하는 방법
const person = {
name: 'Lee',
address: 'Seoul',
__proto__: { age: 20 }
};
for (const key in person) {
// 객체 자신의 프로퍼티인지 확인한다.
if (!person.hasOwnProperty(key)) continue;
console.log(key + ': ' + person[key]);
}
// name: Lee
// address: Seoul
19-70
- for…in문은 순서를 보장하지 않지만 대부분 모던 브라우저는 순서 보장하고 숫자(프로퍼티 키가)는 정렬
const obj = {
2: 2,
3: 3,
1: 1,
b: 'b',
a: 'a'
};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
console.log(key + ': ' + obj[key]);
}
/*
1: 1
2: 2
3: 3
b: b
a: a
*/
19-71
- 배열에는 for…in문 비추천
const arr = [1, 2, 3];
arr.x = 10; // 배열도 객체이므로 프로퍼티를 가질 수 있다.
for (const i in arr) {
// 프로퍼티 x도 출력된다.
console.log(arr[i]); // 1 2 3 10
};
// arr.length는 3이다.
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]); // 1 2 3
}
// forEach 메서드는 요소가 아닌 프로퍼티는 제외한다.
arr.forEach(v => console.log(v)); // 1 2 3
// for...of는 변수 선언문에서 선언한 변수에 키가 아닌 값을 할당한다.
for (const value of arr) {
console.log(value); // 1 2 3
};
19.14.2 Object.keys/values/entries 메서드(19-72)
const person = {
name: 'Lee',
address: 'Seoul',
__proto__: { age: 20 }
};
console.log(Object.keys(person)); // ["name", "address"]
19-73
console.log(Object.values(person)); // ["Lee", "Seoul"]
19-74
console.log(Object.entries(person)); // [["name", "Lee"], ["address", "Seoul"]]
Object.entries(person).forEach(([key, value]) => console.log(key, value));
/*
name Lee
address Seoul
*/
댓글남기기