[모던 자바스크립트 Deep Dive] 19장. 프로토타입 - 2

2023. 3. 13. 15:26모던 자바스크립트 Deep Dive 정리

8. 오버라이딩과 프로퍼티 섀도잉

1) 오버라이딩 : 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식

2) 오버로딩 : 함수의 이름은 동일하지만 매겨변수의 타입 또는 개수가 다른 메서드를 구현하고 매겨변수에 의해 메서드를 구별하여 호출하는 방식. 자바스크립트는 오버로딩을 지원하지 않지만 arguments 객체를 사용하여 구현할 수는 있다.

3) 프로퍼티 섀도잉 : 상속 관계에 의해 프로퍼티가 가려지는 현상.

4) 인스턴스 메서드를 삭제하더라도 프로토타입 메서드는 삭제되지 않는다. 프로토타입 프로퍼티를 변경 또는 삭제하기 위해서는 프로토타입에 직접 접근하여 변경하여야 한다.

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

// 프로토타입에 직접 접근하여 메서드를 삭제하였다.
delete Person.prototype.sayHello;

me.sayHello(); // TypeError: me.sayHello is not a function

 

9. 프로토타입의 교체

- 프로토타입은 생성자 함수 또는 인스턴스에 의해 교체할 수 있다.

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

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

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

- 위 예제에서 Person.prototype에 객체 리터럴을 할당하여, Person 생성자 함수가 생성할 객체의 프로토타입을 객체 리터럴로 교체했다. 교체된 객체 리터럴에는 constructor 프로퍼티가 없다. constructor 프로퍼티는 자바스크립트 엔진이 프로토타입을 생성할 때 암묵적으로 추가한 프로퍼티다. 그래서 me 객체의 생성자 함수를 검색하면 Object가 나온다.

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.constructor === Person) // true
console.log(me.constructor === Object) // false

- 위와 같이 교체할 프로토타입에 constructor 프로퍼티를 추가하여 프로토타입의 constructor 프로퍼티를 되살릴 수 있다.

 

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

- 프로토타입은 생성자 함수의 prototype 프로퍼티 뿐만 아니라 인스턴스의 __proto__ 접근자 프로퍼티, Obect.getPrototypeOf 메서드를 통해 접근할 수 있고, __proto 접근자 프로퍼티 또는 Object.setPrototypeOf 메서드를 통해 프로토타입을 교체할 수 있다.

function Person(name){
    this.name = name;
}

Person.prototype.sayHello = function(){
    console.log(`Hi! My name is ${this.name}.`);
}

let me = new Person('Lee');

me.sayHello(); // Hi! My name is Lee.

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

const parent = {
    sayHello() {
        console.log(`Hi! My parent name is ${this.name}.`);
    }
}

// me 객체의 프로토타입을 parent 객체로 교체.
Object.setPrototypeOf(me, parent);

me.sayHello(); // Hi! My parent name is Lee.

// 프로토타입의 교체로 인해 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴.
console.log(me.constructor === Person); // false
console.log(me.constructor === Object); // true

- 위 예제에서 constructor 프로퍼티와 생성자 함수 간의 연결을 위해서는

...

const parent = {
    // constructor 프로퍼티와 생성자 함수 간의 연결
    constructor : Person,
    ...
}

// 생성자 함수의 prototype 프로퍼티와 프로토타입 간의 연결.
Person.prototype = parent;

Object.setPrototype(me, parent);
// me.__proto__ = parent; 와 동일

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

console.log(Person.prototype === Object.getPrototypeOf(me)); // true
/* 
 만약 Person.prototype === parent; 가 없을 경우 
 생성자 함수의 prototype 프로퍼티와 프로토타입 간의 연결 설정이 없기 때문에 
 console.log(Person.prototype === Object.getPrototypeOf(me)); // false
*/

 

10. instanceof 연산자

- 이항 연산자로서 좌변에 객체를 가리키는 식별자. 우변에 생성자 함수를 가리키는 식별자를 피연산자로 받는다.

객체 instanceof 생성자 함수 // 우변의 피연산자가 함수가 아닌 경우 TypeError

 

- 우변의 생성자 함수가 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true,

  그렇지 않다면 false.

function Person(name) {
    this.name = name;
}

const me = new Person('Lee');

// Person.prototype, Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true
console.log(me instanceof Person); // true
console.log(me instanceof Object); // true

// 프로토타입 교체하면
const parent = {};

Object.setPrototypeOf(me, parent);

// 프로토타입 교체로 Person 생성자 함수와 parent 객체의 연결이 파괴되었다.
console.log(Person.prototype === parent); // false
console.log(parent.constructor === Person); // false

// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하지 않게 되었다.
console.log(me instanceof Person); // false

// Objetc.prototype이 me 객체의 프로토타입 체인 상에 존재한다.
console.log(me instanceof Objetc); // true

// parent 객체를 Person 생성자 함수의 prototype 프로퍼티에 바인딩하면
Person.prototype = parent;

// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하게 되었다!
console.log(me instanceof Person); // true

console.log(me instanceof Object); // true

 

* constructor 프로퍼티와 생성자 함수 간의 연결이 파괴되어도 instanof는 아무런 영향을 받지 않는다.

 

11. 직접 상속

1) Object.create에 의한 직접 상속

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

// 프로토타입이 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 = {}; 와 동일.
obj = Object.create(Object.prototype);
console.log(Object.getPrototypeOf(obj) === Object.prototype) // true

// 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(Object.getPrototypeOf(obj) === Object.prototype) // true

const myProto = { x : 10 };
//임의의 객체를 직접 상속받는다.
obj = Object.create(myProto);
console.log(obj.x) // 10
console.log(Object.getPrototypeOf(obj) === myProto) // true

- Object.create 메서드의 장점

   → new 연산자 없이도 객체 생성 가능.

   → 프로토타입을 지정하면서 객체 생성 가능.

   → 객체 리터럴에 의해 생성된 객체도 상속받기 가능.

- Object.prototype의 빌트인 메서드는 간접적으로 호출하는 것이 좋다.

const obj = Object.create(null);
obj.a = 1;

//직접 호출
console.log(obj.hasOwnProperty('a')); // TypeError : obj.hasOwnProperty is not a function.

//간접 호출
console.log(Object.prototype.hasOwnProperty.call(obj, 'a')); //true

 

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

- Object.create 메서드는 두 번째 인자로 프로퍼티를 정의하는 것이 번거롭다. 객체 리터럴 내부에서 __proto__ 접근자

프로퍼티를 사용하여 직접 상속을 구현할 수 있다.

const myProto = { x : 10 };

const obj = {
    y : 20,
    __proto__ : myProto
};

console.log(obj.x, obj.y) // 10 20
console.log(Object.getPrototypeOf(obj) === myProto); // true

 

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

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

// 생성자 함수
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('static method');
};

const me = new Person('Lee');

Person.staticMethod(); // static method

// 정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조/호출할 수 없다.
// 인스턴스로 참조/호출할 수 있는 프로퍼티/메서드는 프로토타입 체인 상에 존재해야 한다.
me.staticMethod(); // TypeError : me.staticMethod is not a function.

- 정적 프로퍼티/메서드는 인스턴스의 프로토타입 체인에 속한 객체의 프로퍼티/메서드가 아니므로 인스턴스로 접근할 수 없다.

- 만약 인스턴스/프로토타입 메서드 내에서 this를 사용하지 않는다면 그 메서드는 정적 메서드로 변경할 수있다.

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

 

13. 프로퍼티 존재 확인

1)  in 연산자

// in 연산자 사용법
/*
 key : 프로퍼티 키를 나타내는 문자열
 object : 객체로 평가되는 표현식
*/

key in object

// 예제
const person = {
    name : 'Lee',
    address : 'Seoul'
};

console.log('name' in person); // true
console.log('address' in person); // true
console.log('age' in person); // false

/* 
  in 연산자는 확인 대상 객체의 프로퍼티 뿐만 아니라 확인 대상 객체가 상속받는 모든 프로토타입의
  프로퍼틸를 확인하므로 주의! person 객체에는 toString 이라는 프로퍼티가 없지만 다음 코드의 실행
  결과는 true 다.
  toString은 Object.prototype의 메서드다.
*/
console.log('toString' in person); // true

// Reflect.has 메서드로 변경하여 사용할 수도 있다.
console.log(Reflect.has(person, 'name')); // true

 

2)  Object.prototype.hasOwnProperty 메서드

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

- 인수로 전달받은 프로퍼티 키가 객체 고유의 프로퍼티 키인 경우에만 true를 반환하고 상속받은 프로토타입의 프로퍼티 키인 경우 false를 반환.

 

14. 프로퍼티 열거

1) for... in 문

const person = {
    name : 'Lee',
    address : 'Seoul'
};

for (const key in person) {
    console.log(key + ': ' + person[key]);
}

// name: Lee
// address: Seoul

- for... in 문을 통하여 객체의 프로퍼티를 열거할 수 있다. toString과 같은 Object.prototype의 프로퍼티는 열거가 되지 않는데, 이는 toString 메서드가 열거할 수 없도록 정의되어 있는 프로퍼티이기 때문이다. Object.prototype.toString 프로퍼티의 프로퍼티 어트리뷰트 [[Enumerable]] 의 값이 false 이기 때문이다.

- for... in 문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중, 프로퍼티 어트리뷰트 [[Enumerable]]의 값이 true인 프로퍼티를 순회하며 열거한다.

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

const person = {
    name : 'Lee',
    address : 'Seoul',
    __proto__ : { age : 20 }
};

// Object.keys 메서드는 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환.
console.log(Object.keys(person)); // ['name','address']

// Object.values 메서드는 객체 자신의 열거 가능한 프로퍼티 값을 배열로 반환.
console.log(Object.values(person)); // ['Lee','Seoul']

// Object.entries 메서드는 객체 자신의 열거 가능한 프로퍼티 키와 값의 쌍의 배열을 배열에 담아 반환.
console.log(Object.entries(person)); // [['name','Lee'], ['address','seoul']]

// for...in문 대신 for 문, for...of 문, Array.prototype.forEach 메서드를 사용할 수 있다.
Object.entries(person).forEach(([key, value]) => console.log(key, value));
/*
 name Lee
 address Seoul
*/