[JS Deep Dive] 19장. 프로토타입(2)

lyshine·2023년 5월 23일
0

JS Deep Dive 정리

목록 보기
14/18

프로토타입 체인

  • 생성자 함수에 의해 생성된 객체는 프로토타입 체인을 통해 Object.prototype 메서드인 hasOwnProperty를 호출할 수 있다.
    • 생성자 함수 Person에 의해 생성된 me객체가 Person.prototype뿐만 아니라 Object.prototype도 상속받았다는 것을 의미
  • 프로토타입의 프로토타입은 언제나 Object.prototype이다.
  • Object.getPrototypeOf(Person.prototype) === Object.prototype//true

    프로토타입 체인

    자바스크립트는 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. 이를 프로토타입 체인이라고 한다.

프로토타입 체인의 최상위에 위치하는 객체는 언제나 Object.prototype이다.(프로토타입 체인의 종점)

  • 따라서 모든 객체는 Object.prototype을 상속받는다.
  • Object.prototype의 프로토타입 즉, [[Prototype]] 내부슬롯의 값은 null이다.
  • 프로토타입 체인은 “상속과 프로퍼티 검색을 위한 메커니즘”
  • 프로퍼티가 아닌 식별자는 스코프 체인에서 검색한다.(스코프 체인은 식별자 검색을 위한 메커니즘)
  • 스코프 체인과 프로토타입 체인은 서로 연관없이 별도로 동작하는 것이 아니라 서로 협력하여 식별자와 프로퍼티를 검색하는 데 사용된다.

예시 > me.hasOwnProperty('name') : 먼저 스코프체인에서 me식별자를 검색한 다음, me 객체의 프로토타입 체인에서 hasOwnProperty메서드를 검색한다.


오버라이딩와 프로퍼티 섀도잉

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

delete me.sayHello();
// 인스턴스에는 sayHello가 없으므로 프로토타입 메서드 호출
me.sayHello(); //Hi!  My name is Lee

//하지만 하위객체를 통해 프로토타입의 프로퍼티를 변경/삭제하는 것은 불가능
delete me.sayHello();
// 프로토타입 메서드 그대로 호출
me.sayHello(); //Hi!  My name is Lee
  • 프로토타입이 소유한 프로퍼티를 프로토타입 프로퍼티, 인스턴스가 소유한 프로퍼티를 인스턴스 프로퍼티

  • 프로토타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스에 추가하면 프로토타입 체인을 따라 프로토타입 프로퍼티를 검색하여 프로토타입 프로퍼티를 덮어쓰는 것이 아니라 인스턴스 프로퍼티로 추가한다.

  • 이처럼 상속 관계에 의해 프로퍼티가 가려지는 현상을 ”프로퍼티 섀도잉”이라 한다.

  • 하위 객체를 통해 프로토타입에 get 액세스는 허용되나 set 액세스는 허용되지 않는다. ⇒ 프로토타입 프로퍼티를 변경/삭제하려면 하위 객체를 통해 프로토타입 체인으로 접근이 아닌 프로토타입에 직접 접근해야한다.


프로토타입의 교체

프로토타입은 임의의 다른 객체로 변경할 수 있다.

  • 부모 객체인 프로토타입을 동적으로 변경할 수 있다는 것을 의미 => 이러한 특징으로 객체 간의 상속 관계를 동적으로 변경할 수 있다.
  • 프로토타입은 생성자 함수 또는 인스턴스에 의해 교체할 수 있다.

1. 생성자 함수에 의한 프로토타입의 교체

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');

console.log(me.consturctor === Person); //false
console.log(me.constructor === Object); //true

①에서 Person.prototype에 객체 리터럴을 할당했는데, 이는 Person 생성자 함수가 생성할 객체의 프로토타입을 객체 리터럴로 교체한 것이다.

  • 프로토타입으로 교체한 객체 리터럴에는 constructor 프로퍼티가 없다.
  • 위와 같이 프로토타입을 교체하게 되면 me 의 생성자 함수는 Person이 아닌 Object로 바뀐다.
  • 이처럼 프로토타입을 교체하게 되면 constructor프로퍼티와 생성자 함수간의 연결이 파괴되는데,constructor 프로퍼티를 추가하여 프로토타입의 constructor 프로퍼티를 되살릴 수 있다.

2. 인스턴스에 의한 프로토타입의 교체

  • 생성자 함수의 prototype 프로퍼티뿐만 아니라 인스턴스의 proto 접근자 프로퍼티를 통해 접근할 수 있다.
  • 생성자 함수의 prototype 프로퍼티에 다른 임의의 객체를 바인딩 하는 것은 미래에 생성할 인스턴스의 프로토타입을 교체하는 것이다.
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

하지만 이런 방식들의 프로토타입 교체는 객체 간의 상속관계를 동적으로 변경하는 것은 꽤나 번거롭고 직접 교체하지 않는 것이 좋다.
→ 직접 상속이 더 편리하고 안전 or 클래스 사용


instanceof 연산자

  • instanceof 연산자는 이항 연산자로서 좌변에 객체를 가리키는 식별자, 우변에 생성자 함수를 가리키는 식별자를 피연산자로 받는다.
  • 만약, 우변의 피연산자가 함수가 아닌 경우 TypeError가 발생한다.
객체 instanceof 생성자 함수
  • 우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true로 평가됨
  • instanceof 연산자는 프로토타입의 constructor프로퍼티가 가리키는 생성자 함수를 찾는 것이 아니라 생성자 함수의 prototype에 바인딩된 객체가 프로토타입 체인 상에 존재하는지 확인한다.

직접 상속

1. Object.create

  • Object.create 메서드는 명시적으로 프로토타입을 지정하여 새로운 객체를 생성한다. 다른 객체와 마찬가지로 추상 연산 OrdinaryObjectCreate를 호출한다.
Object.create(생성할 객체의 프로토타입으로 지정할 객체, 
생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터 객체로 이뤄진 객체)
//두 번째 인수는 옵션이므로 생략 가능하다.

Object.create 메서드는 첫 번째 매개변수에 전달된 객체의 프로토타입 체인에 속하는 객체를 생성한다. 이 메서드의 장점은 다음과 같다.

  • new 연산자가 없이도 객체를 생성할 수 있다.
  • 프로토타입을 지정하면서 객체를 생성할 수 있다.
  • 객체 리터럴에 의해 생성된 객체도 상속받을 수 있다.

2. 객체 리터럴 내부에서 __proto__에 의한 직접 상속

  • ES6에서는 객체 리터럴 내부에서 proto 접근자 프로퍼티를 사용하여 직접 상속을 구현할 수 있다.
  • 더 깔끔한 방법
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

정적 프로퍼티/메서드..

  • 정적 프로퍼티/메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드를 말한다.
// 생성자 함수
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

프로퍼티 존재 확인

1. in 연산자

  • in연산자는 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인한다.
/**
* key: 프로퍼티 키를 나타내는 문자열
* object: 객체로 평가되는 표현식
*/
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

console.log('toString' in person); //true //Object.prototype의 메서드

in 연산자는 객체가 상속받은 모든 프로토타입의 프로퍼티를 확인한다.

2. Object.prototype.hasOwnProperty 메서드

  • 이를 이용해서도 객체에 특정 프로퍼티가 존재하는지 알 수 있다.
  • 프로토타입 체인 적용 x
console.log(person.hasOwnProperty('name'));//true
console.log(person.hasOwnProperty('age'));//false
console.log(person.hasOwnProperty('toString'));//false

프로퍼티 열거

1. for…in 문

  • 객체의 모든 프로퍼티를 순회하며 열거하려면 for .. in문을 사용한다
  • 상속받은 프로퍼티도 열거하지만 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 어트리뷰트 [[Enumerable]]의 값이 true인 프로퍼티를 순회하며 열거한다.(toString과 같은 Object.prototype의 프로퍼티가 열거되지 않는다.)
  • 열거시 순서를 보장하지 않지만 대부분의 모던 브라우저는 순서를 보장하고 숫자 프로퍼티 정렬을 실시한다.
  • 배열에는 for…in 문 보다 for..of / forEach를 사용한다.
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

2. Object.keys/values/entries 메서드

  • for...in문 객체는 상속받은 프로퍼티까지 열거하기 때문에 Object.prototype.hasOwnProperty 메서드를 사용하여 객체 자신의 프로퍼티인지 확인하는 추가 처리가 필요했다. → Object.keys/values/entries 메서드 사용하면 추가 처리 필요x
  • Object.keys와 Object.values 메서드는 객체 자신의 열거 가능한 프로퍼티 키와 값을 배열로 반환한다.
  • Object.entries 메서드는 객체 자신의 열거 가능한 키와 값의 쌍의 배열을 배열에 담아 반환한다.
const person = {
  name: 'Lee',
  address: 'Seoul',
  __proto__: { age: 20 }
};

console.log(Object.keys(person)); // ["name", "address"]
console.log(Object.values(person)); // ["Lee", "Seoul"]

console.log(Object.entries(person)); // [["name", "Lee"], ["address", "Seoul"]]

Object.entries(person).forEach(([key, value]) => console.log(key, value));
/*
name Lee
address Seoul
*/

0개의 댓글