본문 바로가기

2022-2 웹개발 스터디

[모던 JS] CH19 프로토타입

CH19. 프로토타입

19.1 객체지향 프로그래밍

- 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료구조를 객체라고 함

- 객체지향 프로그래밍은 객체의 상태를 나타내는 데이터와

  상태 데이터를 조작할 수 있는 동작을 하나의 논리적인 단위로 묶어 생각함

- 객체의 상태 데이터를 프로퍼티, 동작을 메서드라고 함

19.2 상속과 프로토타입

- 자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거함

- 중복을 제거하는 방법? 기존의 코드를 적극적으로 재사용하는 것 -> 상속

// 생성자 함수
function Circle(radius) P
    this.radius = radius;
}

Circle.prototype.getArea = function() {
    return Math.PI * this.radius ** 2;
};

const circle1 = new Circle(1);
const circle2 = new Circle(2);

console.log(circle1.getArea === circle2.getArea);

console.log(circle1.getArea());
console.log(circle2.getArea());

19.3 프로토타입 객체

- 객체 간 상속을 구현하기 위해 사용하는 프로토타입 객체

- 프로토타입을 상속받은 하위 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용 가능

- 모든 객체는 [[Prototype]]이라는 내부 슬롯을 가짐 

- [[Prototype]]에 저장되는 프로토타입은 객체 생성 방식에 의해 결정됨

- [[Prototype]] 내부 슬롯에는 접근할 수 없지만, __proto__ 접근자 프로퍼티를 통해 간접적 접근 가능

19.3.1 __proto__ 접근자 프로퍼티

1. __proto__는 접근자 프로퍼티

2. __proto__ 접근자 프로퍼티는 상속을 통해 사용됨

3. __proto__ 접근자 프로퍼티를 통해 프로토타입에 전근하는 이유?

: 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위함

4. __proto__ 접근자 프로퍼티를 코드 내에서 직접 사용하는 것은 권장하지 않음

: 모든 객체가 __proto__ 접근자 프로퍼티를 사용할 수 있는 것은 아니기 때문

19.3.2 함수 객체의 prototype 프로퍼티

- 함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킴

- Object.prototype으로부터 상속받은 __proto__ 접근자 프로퍼티와

  함수 객체만이 가지고 있는 prototype 프로퍼티는 결국 동일한 프로토 타입을 가리킴

19.3.3 프로토타입의 constructor 프로퍼티와 생성자 함수

- 모든 프로토타입이 가진 constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킴

- 이 연결은 생성자 함수가 생성될 때 이뤄짐

// 생성자 함수
function Person(name) {
    this.name = name;
}

const me = new Person('Lee');

console.log(me.constructor === Person);

19.4 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토 타입

- constructor 프로퍼티가 가리키는 생성자 함수는 인스턴스를 생성한 생성자 함수

const obj = new Object();
console.log(obj.constructor === Object);

const add = new Function('a', 'b', 'return a + b');
console.log(add.constructor === Function);

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

const me = new Person('Lee');
console.log(me.constructor === Person);

 

- 명시적으로 new 연산자와 함께 생성자 함수를 호출하여 인스턴스를 생성하지 않는 객체 생성 방식도 있음

const obj = {};

const add = function (a, b) {return a + b; };

const arr = {1, 2, 3};

const regxp = /is/ig;

 

- 객체 리터럴이 평가될 때는 추상 연산 OrdinaryObjectCreate를 호출하여 빈 객체를 생성하고 프로퍼티를 추가하도록 정의되어 있음

- 프로토 타입과 생성자 함수는 단독으로 존재할 수 없고 항상 쌍으로 존재함

- 리터럴 표기법에 의해 생성된 객체는 생성자 함수에 의해 생성된 객체가 아님

19.5 프로토타입의 생성 시점

19.5.1 사용자 정의 생성자 함수와 프로토타입 생성 시점

- 생성자 함수로서 호출할 수 있는 함수는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성됨

console.log(Person.prototype);

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

 

- 생성자 함수로서 호출할 수 없는 non-constructor는 프로토타입이 생성되지 않음

const Person = name => {
    this.name = name;
};

console.log(Person.prototype);

19.5.2 빌트인 생성자 함수와 프로토타입 생성 시점

- 빌트인 생성자 함수도 일반 함수와 마찬가지로 빌트인 생성자 함수가 생성되는 시점에 프로토타입이 생성됨

- 모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성됨

- 생성된 프로토 타입은 빌트인 생성자 함수의 prototype 프로퍼티에 바인딩됨

- 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면 프로토타입은 생성된 객체의 [[Prototype]] 내부 슬롯에 할당됨

: 프로토 타입 상속

19.6 객체 생성 방식과 프로토 타입의 결정

19.6.1 객체 리터럴에 의해 생성된 객체의 프로토 타입

- 자바스크립트 엔진은 객체 리터럴을 평가하여 객체를 생성할 때 추상 연산을 호출함

- 객체 리터럴에 의해 생성되는 객체의 프로토타입은 Object.prototype

const obj = {x: 1};

console.log(obj.constructor === Object);
console.log(obj.hasOwnProperty('x'));

19.6.2 Object 생성자 함수에 의해 생성된 객체의 프로토 타입

- Object 생성자 함수를 인수 없이 호출하면 빈 객체가 생성됨

- 일단 빈 객체를 생성한 이루 프로퍼티를 추가해야 함

const obj = new Object();
obj.x = 1;

console.log(obj.constructor === Object);
console.log(obj.hasOwnProperty('x'));

19.6.3 생성자 함수에 의해 생성된 객체의 프로토 타입

- 다른 객체 생성 방식과 마찬가지로 추상 연산이 호출됨

- 생성자 함수에 의해 생성되는 객체의 프로토 타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체

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();
you.sayHello();

19.7 프로토타입 체인

프로토타입 체인?

: 자바스크립트는 객체의 프로퍼티에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면

  [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색함

- 프로토타입 체인은 자바스크립트가 객체지향 프로그래밍의 상속을 구현하는 메커니즘

- Object.prototype을 프로토타입 체인의 종점

 

- 프로토타입 체인의 종점에서도 프로퍼티를 검색할 수 없는 경우 undefined 반환

- 이때 에러가 발생하지 않음

console.log(me.foo); // undefined

 

- 프로토타입 체인은 상속과 프로퍼티 검색을 위한 메커니즘

- 스코프 체인은 식별자 검색을 위한 메커니즘

- 스코프 체인과 프로토타입 체인은 서로 연관없이 별도로 동작하는 것이 아니라 

  서로 협력하여 식별자와 프로퍼티를 검색하는 데 사용됨

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

? 오버라이딩

: 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식

 

? 오버로딩

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

 

? 프로퍼티 섀도잉

: 상속 관계에 의해 프로퍼티가 가려지는 현상

19.9 프로토타입의 교체

- 부모 객체인 프로토타입을 동적으로 변경 가능

- 객체 간의 상속 관계를 동적으로 변경 가능

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

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

- 프로토타입을 교체하면 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴됨

- 프로토타입으로 교체한 객체 리터럴에 constructor 프로퍼티를 추가하여 프로토타입의 constructor 프로퍼티를 되살림

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

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

	return Person;
} ());

const me = new Person('Lee');

console.log(me.constructor === Person);
console.log(me.constructor === Object);

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

- 프로토타입 교체를 통해 객체 간의 상속 관계를 동적으로 변경하는 것은 꽤나 번거로움

- 프로토 타입은 직접 교체하지 않는 것이 좋음

19.10 instanceof 연산자

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

- 만약 우변의 피연산자가 함수가 아닌 경우 TypeError 발생함

- 생성자 함수의 prototype에 바인딩된 객체가 프로토타입 체인 상에 존재하는지 확인함

 

- 생성자 함수에 의해 프로토타입이 교체되어 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴되어도 생성자 함수의 prototype 프로퍼티와 프로토타입 간의 연결은 파괴되지 않으므로 instanceof는 아무런 영향을 받지 않음

const Person = (function() {
    function Person(name) {
        this.name = name;
    }
    
    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 instanceof Person); // true
console.log(me instanceof Object); // true

19.11 직접 상속

19.11.1 Object.create에 의한 직접 상속

- 명시적으로 프로토타입을 지정하여 새로운 객체를 생성함

- 첫 번째 매개변수에는 생성할 객체의 프로토타입으로 지정할 객체를 전달함

- 두 번째 매개변수에는 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터 객체로 이뤄진 객체를 전달함

- 두 번째 인수는 옵션이므로 생략 가능

 

장점?

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

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

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

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

- Object.create 메서드에 의한 직접 상속은 여러 장점이 있지만, 두 번째 인자로 프로퍼티를 정의하는 것은 번거로움

- ES6에서는 객체 리터럴 내부에서 __proto__ 접근자 프로퍼티를 사용하여 직접 상속을 구현할 수 있음

19.12 정적 프로퍼티/메서드

정적 프로퍼티/메서드?

: 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드

 

- Person생성자 함수는 객체이므로 자신의 프로퍼티/메서드를 소유할 수 있음

- 이렇게 소유한 프로퍼티/메서드를 정적 프로퍼티/메서드라고 함

- 이는 생성자 함수가 생성한 인스턴스로 참조/호출할 수 없음

 

- 생성자 함수가 생성한 인스턴스는 자신의 프로토타입 체인에 속한 객체의 프로퍼티/메서드에 접근할 수 있음

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

const obj  = Object.create({name: 'Lee'});

obj.hasOwnProperty('name'); // false

 

- 프로토타입 프로퍼티/메서드를 표기할 때 prototype을 #으로 표기하는 경우도 있음

ex) Object.prototype.isPrototypeOf 를 Object#isPrototypeOf

19.13 프로퍼티 존재 확인

19.13.1 in연산자

- 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인

-  확인 대상 객체의 프로퍼티뿐만 아니라 확인 대상 객체가 상속받은 모든 프로토타입의 프로퍼티를 확인하므로 주의

 

- in 연산자 대신 ES6에 도입된 Reflect.has 메서드를 사용할 수도 있음 (동일하게 작동)

const person = {name: 'Lee'};

console.log(Reflect.has(person, 'name'));
console.log(Reflect.has(person, 'toString'));

19.13.2 Object.prototype.hasOwnProperty 메서드

- 객체에 특정 프로퍼티가 존재하는지 확인할 수 있는 또다른 메서드

- 인수로 전달받은 프로퍼티 키가 객체 고유의 프로퍼티 키인 경우에만 true 반환

- 상속받은 프로토타입의 프로퍼티 키인 경우 false를 반환

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

19.14 프로퍼티 열거

- 객체의 모든 프로퍼티를 순회하며 열거하기 위함

19.14.1 for ... in 문

- 객체의 프로퍼티 개수만큼 순회하며 프로퍼티 키를 할당함

- 순회 대상 객체의 프로퍼티뿐만 아니라 상속받은 프로토타입의 프로퍼티까지 열거함

 

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

- 프로퍼티 키가 심벌인 프로퍼티는 열거하지 않음

const sym = Symbol();
const obj = {
    a : 1,
    [sym] : 10
};

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

19.14.2 Object.keys/values/entries 메서드

- for ... in 문은 객체 자신의 고유 프로퍼티뿐 아니라 상속받은 프로퍼티도 열거함

- 객체 자신의 고유 프로퍼티만 열거하기 위해서는 Object.keys/values/entries 메서드를 사용하는 것을 권장함

- Object.keys 메서드는 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환함

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

console.log(Object.keys(person));