19장. 프로토타입

Happhee·2022년 1월 27일
0

JS : Depp Dive

목록 보기
16/35
post-thumbnail

자바스크립트를 이루고 있는 거의 모든것이 객체이다


1. 객체 지향 프로그래밍

객체
속성을 통해 여러가지의 값을 하나의 단위로 구성한 복합적인 자료구조를 말한다

  • 속성
    특징이나 성질을 나타내는 것

  • 추상화
    여러가지의 속성들 중에서 필요한 속성만을 빼오는 것

따라서
객체 지향 프로그래밍은 상태를 나타내는 데이터와 그 데이터를 조작할 수 있는 동작을 하나로 논리적인 단위로 묶은 복합적인 자료구조라고 한다


2. 상속과 프로토타입

상속이란
객체 지향 프로그래밍에서 가장 핵심되는 개념이며, 어떤 객체의 프로퍼티나 메서드를 다른객체가 상속받아 그대로 사용할 수 있음을 말한다

자바스크립트는 프로토타입을 기반으로 하여 상속을 구현해 중복을 제거한다

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

다음과 같이 Circle의 프로토타입에 getArea를 지정하여 한번만 메서드를 생성하여 효율을 높일 수 있다


3. 프로토타입 객체

모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며 , 이는 __proto__접근자로 내부 슬롯에 간접접근이 가능하다
즉, 모든 객체는 하나의 프로토타입을 갖고, 그 프로토타입은 생성자함수와 연결되어 있다

__ proto__ 접근자 프로퍼티

  • 접근자 프로퍼티이다
const obj = {};
const parent = { x: 1 };

// getter 함수인 get __proto__가 호출되어 obj 객체의 프로토타입을 취득
obj.__proto__;
// setter함수인 set __proto__가 호출되어 obj 객체의 프로토타입을 교체
obj.__proto__ = parent;

console.log(obj.x); // 1
  • 상속을 통해 사용된다
const person = { name: 'Lee' };

// person 객체는 __proto__ 프로퍼티를 소유하지 않는다.
console.log(person.hasOwnProperty('__proto__')); // false

// __proto__ 프로퍼티는 모든 객체의 프로토타입 객체인 Object.prototype의 접근자 프로퍼티다.
console.log(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__'));
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}

// 모든 객체는 Object.prototype의 접근자 프로퍼티 __proto__를 상속받아 사용할 수 있다.
console.log({}.__proto__ === Object.prototype); // true
  • 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지한다
const parent = {};
const child = {};

// child의 프로토타입을 parent로 설정
child.__proto__ = parent;
// parent의 프로토타입을 child로 설정
parent.__proto__ = child; // TypeError: Cyclic __proto__ value
  • 코드내에서 직접 사용하는 것은 권장하지 않는다

prototype 프로퍼티

함수 객체만이 소유할 수 있는 프로퍼티이며 이는 생성자 함수가 생성할 인스턴스의 프로토타입을 말한다

// 함수 객체는 prototype 프로퍼티를 소유한다.
(function () {}).hasOwnProperty('prototype'); // -> true
// 일반 객체는 prototype 프로퍼티를 소유하지 않는다.
({}).hasOwnProperty('prototype'); // -> false

단, 화살표 함수처럼 비생성자 함수로 사용되는 함수는 prototype 프로퍼티를 소유하지 않음에 주의하자

// 화살표 함수는 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

정리를 하면, 다음과 같다

최종적으로
모든 객체가 가지고 있는Object.prototype으로부터 상속받은 __proto__접근자 프로퍼티와
함수 객체만이 가지고 있는 prototype프로퍼티는
결국 동일한 프로토타입을 가리키고 있다

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

const me = new Person('Lee');

// 결국 Person.prototype과 me.__proto__는 결국 동일한 프로토타입을 가리킨다.
console.log(Person.prototype === me.__proto__);  // true

contructor프로퍼티는 prototype프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다


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

프로토타입과 생성자 함수는 단독으로 존재할 수 없고, 언제나 으로 존재한다

// obj 객체는 Object 생성자 함수로 생성한 객체가 아니라 객체 리터럴로 생성했다.
const obj = {};

// 하지만 obj 객체의 생성자 함수는 Object 생성자 함수다.
console.log(obj.constructor === Object); // true

// 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 {}
new Foo(); // Foo {}

// 3. 인수가 전달된 경우에는 인수를 객체로 변환한다.
// Number 객체 생성
obj = new Object(123);
console.log(obj); // Number {123}

// String  객체 생성
obj = new Object('123');
console.log(obj); // String {"123"}

5. 프로토타입의 생성 시점

프로토타입은 생성자 함수가 생성되는 시점에 같이 생성된다
즉, 생성자 함수로 호출할 수 있는 함수인contructor함수 정의평가되어 함수 객체를 생성하는 시점에 같이 되는 것을 말한다

// 함수 정의(constructor)가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다.
console.log(Person.prototype); // {constructor: ƒ}

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

// 화살표 함수는 non-constructor다.
const Person = name => {
  this.name = name;
};

// non-constructor는 프로토타입이 생성되지 않는다.
console.log(Person.prototype); // undefined

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

객체 리터럴 Object 생성자 함수 생성자함수 Object.create 클래스의 방법으로 객체가 생성된다

  • 객체 리터럴에 의해 생성된 객체의 프로토타입
const obj = { x: 1 };

// 객체 리터럴에 의해 생성된 obj 객체는 Object.prototype을 상속받는다.
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty('x'));    // true
  • Object 생성자 함수
const obj = new Object();
obj.x = 1;

// Object 생성자 함수에 의해 생성된 obj 객체는 Object.prototype을 상속받는다.
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty('x'));    // true
  • 생성자 함수
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

7. 프로토타입 체인

프로토타입 체인이란?
자바스크립트는 객체의 프로퍼티에 접근하려고 할 때, 만약 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]]의 내부 슬롯 참조를 따라서 자신의 부모역할을 하는 프로토타입의 프로퍼티를 순차적으로 순회하여 검색하는 것을 말한다
이는 자바스크립트가 객체지향프로그래밍상속을 구현하는 매커니즘으로 볼 수 있다

위의 코드에 대한 프로토타입 체인을 살펴보자

Object.getPrototypeOf(me) === Person.prototype; // -> true
Object.getPrototypeOf(Person.prototype) === Object.prototype; // -> true

// hasOwnProperty는 Object.prototype의 메서드다.
// me 객체는 프로토타입 체인을 따라 hasOwnProperty 메서드를 검색하여 사용한다.
me.hasOwnProperty('name'); // -> true

Object.prototype을 프로토타입 체인의 종점이라고 한다

다시 정리하면,
프로토타입 체인은 상속과 프로퍼티 검색을 위한 매커니즘이며 스코프체인은 식별자 검색을 위한 매커니즘이다
즉, 프로토타입체인과 스코프체인이 서로 협력하여 식별자와 프로퍼티를 검색하는데 사용된다


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

  • 오버라이딩
    상위클래스가 가지고 있는 메서드를 하위클래스가 재정의하여 사용하는 방식
  • 프로퍼티 섀도잉
    상속관계에 의해 프로퍼티가 가려지는 현상

다음 예제를 보자

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

인스턴스 메서드 sayHello가 오버라이딩 되었고, 프로토타입 메서드인 sayHello는 상속에 의해 가려지게 된다

이때, 프로토타입 프로퍼티를 변경하거나 삭제하려면, 프로토타입 체인을 통해 접근하는 것이 아니라 해당 프로토타입에 직접접근해야 한다

// 프로토타입 체인을 통해 프로토타입 메서드가 삭제되지 않는다.
delete me.sayHello;
// 프로토타입 메서드가 호출된다.
me.sayHello(); // Hi! My name is Lee

// 프로토타입 메서드 변경
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

9. 프로토타입의 교체

프로토타입은 임의의 다른 객체로 변경이 가능하다
즉, 부모 객체인 프로토타입을 동적으로 변경할 수 있음을 말한다

  • 생성자 함수로의 프로토타입 교체
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

이지만

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


// 프로토타입을 교체하면 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴된다.
console.log(me.constructor === Person); // false
// 프로토타입 체인을 따라 Object.prototype의 constructor 프로퍼티가 검색된다.
console.log(me.constructor === Object); // true

Object.setPrototypeOf로 인해 프로토타입이 교체 되었다

  • 인스턴스에 의한 프로토타입의 교체
// 생성자 함수
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 생성자 함수와 parent 객체는 연결되어 있지 않다.
console.log(Person.prototype === parent); // false
console.log(parent.constructor === Person); // false

10. instanceof 연산자

객체 instanceof 생성자 함수
우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true로 , 그렇지 않으면 false로 평가된다

// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하지 않기 때문에 false로 평가된다.
console.log(me instanceof Person); // false

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

생성자 함수의 prototype에 바인딩된 객체가 프로토타입 체인 상에 존재하는지를 확인한다
예제를 보자

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

11.직접 상속

  • Object.create()에 의한 직접 상속은
    new연산자 없이, 프로토타입의 지정, 객체리터럴에 의해 생성된 객체까지도 상속이 가능하다
const obj = { a: 1 };

obj.hasOwnProperty('a');       // -> true
obj.propertyIsEnumerable('a'); // -> true

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. 정적 프로퍼티 / 메서드

생성자 함수로 인스턴스를 생성하지 않아도 참조/호출이 가능한 프로퍼티 /메서드를 말한다
staticMethod, staticProp

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

13. 프로퍼티 존재 확인

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

Object.prototype.hasOwnProperty메서드

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

14. 프로퍼티 열거

for (변수 선언문 in 객체 ) { ... }
객체의 모든 프로퍼티를 순회한다

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

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

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

단, 순서는 보장되지 않으므로 배열은 for of for문 Array.prototype.forEach를 사용하도록 하자

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
};

Object. keys/values/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
*/
profile
즐기면서 정확하게 나아가는 웹프론트엔드 개발자 https://happhee-dev.tistory.com/ 로 이전하였습니다

0개의 댓글