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
*/

댓글남기기