11 직접 상속

11.1 Object.create에 의한 직접 상속

Object.create 메서드는 명시적으로 프로토타입을 지정하여 새로운 객체를 생성한다. 다른 객체 생성 방식과 마찬가지로 추상 연산 OrdinaryObjectCreate를 호출한다.

Object.create 메서드로 객체를 생성할 때 장점은 다음과 같다.

  • new 연산자 없이도 객체 생성 가능
  • 프로토타입을 지정하면서 객체 생성 가능
  • 객체 리터럴에 의해 생성된 객체도 상속 가능
// 프로토타입이 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

Object.prototype의 빌트인 메서드를 객체가 직접 호출하는 것을 권장하지 않는다.

그 이유는 Object.create 메서드를 통해 프로토타입 체인의 종점에 위치하는 객체를 생성할 수 있기 때문이다. (Object.craete(null)로 생성하는 경우)

따라서 Object.prototype.hasOwnProperty.call() 메서드를 활용해 간접적으로 호출할 수 있다.

// 프로토타입이 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

11.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

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

정적(static) 프로퍼티/메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드를 말한다.

// 생성자 함수
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

alt text

이 그림을 통해서 위에서 말한 Object.create 메서드는 Object 생성자 함수의 정적 메서드고, Object.prototype.hasOwnProperty 메서드는 Object.prototype의 프로토타입 메서드다.

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

프로토타입 메서드 내에서 this를 사용하지 않는다면 정적 메서드로 변경할 수 있다.

그 이유는 프로토타입 메서드를 호출하려면 인스턴스를 생성해야 하지만, 정적 메서드는 인스턴스를 생성하지 않아도 호출이 가능하기 때문이다.

13. 프로퍼티 존재 확인

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
 
// in대신 ES6에서 도입된 Reflect.has메서드도 사용할 수 있다. in과 동일하게 동작한다.
const person = { name: 'Lee' };
 
console.log(Reflect.has(person, 'name'));     // true
console.log(Reflect.has(person, 'toString')); // true

in 연산자는 확인 대상 객체의 프로퍼티뿐만 아니라 객체가 상속받은 모든 프로토타입의 프로퍼티를 확인하므로 주의해야한다.

person 객체에는 toString 이라는 프로퍼티가 없지만 console.log('toString' in person); 코드 실행하면 true가 나온다.

그 이유는 person 객체는 Object를 상속 받기 때문에 Object.prototype.toString을 확인했기 때문이다.

Object.prototype.hasOwnProperty 메서드

Object.prototype.hasOwnProperty 메서드를 사용해도 객체에 특정 프로퍼티가 존재하는지 확인할 수 있다.

console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('age'));  // false
console.log(person.hasOwnProperty('toString')); // false

객체의 고유 프로퍼티인 경우에만 true를 반환하고 상속 받은 경우 false를 반환한다.

14. 프로퍼티 열거

14.1 for ... in 문

객체의 모든 프로퍼티를 순회하며 열거하려면 for ... in 문을 사용해야 한다.

for (변수 선언문 in 객체) {...}와 같이 사용하고

특징은 다음과 같다.

  • 프로퍼티 키를 순회한다.
  • 프로토타입 체인을 모두 순회한다.
  • [[Enumerable]] 값을 참조한다.
  • 심벌을 열거하지 않는다.
  • 순서를 보장하지 않는다. (하지만 모던 브라우저는 순서를 보장한다)
const age = Symbol();
 
const person = {
  name: 'Lee',
  address: 'Seoul',
  [age]: 10
};
 
// for...in 문의 변수 prop에 person 객체의 프로퍼티 키가 할당된다.
for (const key in person) {
  console.log(key + ': ' + person[key]);
}
// name: Lee
// address: Seoul

해당 객체의 프로토타입 체인 상에 존재하는 모든 프로퍼티 중에서 [[Enumerable]] 값이 true인 프로퍼티만 열거한다.

Object.getOwnPropertyDescriptor(Object.prototype, "toString")
// {writable: true, enumerable: false, configurable: true, value: ƒ}

toString과 같은 경우 [[Enumerable]] 값이 false이기 때문에 for ... in 문에서 열거되지 않는다.

또한 프로퍼티가 심벌인 경우에도 열거하지 않는다.

배열에서는 for ... in 문보다는 일반적인 for문이나 for ... of 문 또는 forEach 메서드를 사용하는 것을 권장한다.

사실 배열도 객체이기 때문에 for ... in 문에서 프로퍼티 값을 출력할 수 있다.

14.2 Object.keys / values / entries 메서드

for ... in 문은 자신의 고유 프로퍼티 뿐만 아니라 상속받은 프로퍼티도 열거한다.

만약 고유 프로퍼티만 열거하기 위해서는 Object.keys, Object.values, Object.entries 메서드 사용하는 것을 권장한다.

Object.keys 메서드는 자신의 열거가능한(Enumerable) 프로퍼티 키를 배열로 반환한다.

const person = {
  name: 'Lee',
  address: 'Seoul',
  __proto__: { age: 20 }
};
 
console.log(Object.keys(person)); // ["name", "address"]

Object.values 메서드는 ES8에서 도입되었고, 객체 자신의 열거 가능한 프로퍼티 값을 배열로 반환한다.

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

Object.entries 메서드도 ES8에서 도입되었고, 객체 자신의 열거 가능한 프로퍼티 키와 값의 쌍의 배열을 배열에 담아 반환한다.

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

환경과 상황에 맞게 각 메서드들을 사용하면 좋을 것 같다.

Q1 다음 코드의 결과는?

function Study(name) {
  this.name = name;
}
Study.prototype.getStudyName = function () {
  console.log(`Study is ${this.name}`);
}
 
Study.title = 'javascript';
Study.getTitle = function () {
  console.log(this.title)
}
const jsStudy = new Study('react');
Study.getTitle(); // -> ?
jsStudy.getTitle();  // -> ?

Q2 Object.prototype의 빌트인 메서드를 객체가 직접 호출하는 것을 권장하지 않는 이유?