모던 자바스크립트 Deep Dive : 19장 프로토타입

EdLee·2022년 11월 7일

javascript

목록 보기
9/37
post-thumbnail

19장 프로토타입

1. 객체지향 프로그래밍


1.1 절차지향 vs 객체지향

  • 절차지향 : 프로그램이란 명령어 또는 함수의 목록
  • 객체지향 : 프로그램이란 속성을 가진 객체의 집합
    ※ 추상화 : 프로그램에 필요한 속성만 간추려 객체로 만드는 것

1.1 객체의 구성

  • 프로퍼티(property) : 객체의 상태(state)를 나타내는 데이터
  • 메서드(method) : 객체의 상태 데이터를 조작할 수 있는 동작(behavior)

2. 상속과 프로토타입


2.1 상속

  • 객체지향 프로그래밍의 핵심 개념
  • 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것
  • JS는 프로토타입을 기반으로 상속을 구현해 불필요한 중복을 제거
function Circle(radius) {
  this.radius = radius;
}

// Circle의 프로토타입에 "getArea" 함수를 선언할 수 있다
Circle.prototype.getArea = function() {
  return Math.PI * this.radius ** 2;
};

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

// 서로 다른 객체라도, 프로토타입으로부터 getArea 메서드를 상속받아 공유하고 있다.
console.log(circle1.getArea == circle2.getArea); // true

console.log(circle1.getArea()); // 3.14..
console.log(circle2.getArea()); // 12.56..

3. 프로토타입 객체


  • 모든 객체는 하나의 프로토타입을 내부 슬롯으로 갖는다.
  • 모든 프로토타입은 생성자 함수와 연결되어 있다.
  • 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토 타입에 간접적으로 접근 가능

3.1 __proto__ 접근자 프로퍼티

__proto__는 접근자 프로퍼티다

const obj = {};
const parent = { x: 1 };

console.log(obj.x); // undefined
console.log(parent.x); // 1

// 이건 안되는데
obj = parent; // TypeError: Assignment to constant variable.

// 이건 된다
obj.__proto__ = parent; // __proto__를 호출하면, 해당 접근자 프로퍼티의 getter, stter가 호출된다
console.log(obj.x); // 1
console.log(parent.x); // 1

__proto__는 상속을 통해 사용된다

// __proto__는 객체가 가진 프로퍼티가 아니라, prototype의 프로퍼티이다.
const obj = {};
const parent = { x: 1 };

obj.__proto__ = parent;
console.log(obj.x); // 1
console.log(parent.x); // 1

parent.x = 2; // parent만 바꿔본다
console.log(obj.x); // 2
console.log(parent.x); // 2

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

  • __proto__ 접근자 프로퍼티는 호출됐을 때, 순환 참조인지 확인하여 무한 루프에 빠지지 않도록 구현되어있다.
const parent = {};
const child = {};

child.__proto__ = parent;
parent.__proto__ = child; // TypeError: Cyclic __proto__ value

__proto__ 접근자 프로퍼티는 코드 내에서 직접 사용하지 말 것

  • prototype을 상속받지 않는 객체도 존재하는데, 이 경우 __proto__를 사용할 수 없는 경우가 있다
  • getPrototypeOf(), setPrototypeOf() 사용할 것
const obj = Object.create(null);
const parent = { x: 1 };

console.log(obj.__proto__); // undefined, 아예 할당이 안되어 타입조차 없다
console.log(Object.getPrototypeOf(obj)); // null

obj.__proto__ = parent; // setter 호출도 안된다
console.log(obj.x); // undefined

Object.setPrototypeOf(obj, parent); // setPrototypeOf() 함수는 정상 동작한다
console.log(obj.x); // 1

3.2 함수 객체의 prototype 프로퍼티

  • prototype 프로퍼티 : 생성자 함수로 생성할 객체(인스턴스)의 프로토타입이므로, 함수 객체만이 소유 가능
console.log((function() { }).hasOwnProperty('prototype')); // true, 생성자를 가진 함수
console.log(({}).hasOwnProperty('prototype')); // false, 객체
console.log((() => { }).hasOwnProperty('prototype')); // false, 화살표 함수

const obj = {
  foo() { } // 메서드 축약 표현
};
console.log(obj.hasOwnProperty('prototype')); // false, obj는 객체
console.log(obj.foo.hasOwnProperty('prototype')); // false
구분소유사용 주체사용 목적
__proto__
접근자 프로퍼티
모든 객체프로토타입의 참조모든 객체객체가 자신의 프로토타입에 접근 또는 교체하기 위해 사용
prototype
프로퍼티
constructor프로토타입의 참조생성자 함수생성자 함수가 자신이 생성할 객체(인스턴스)의 프로토타입을 할당하기 위해 사용
  • 결국, 객체의 __proto__ 접근자 프로퍼티와 함수 객체의 prototype 프로퍼티는 동일한 프로토타입을 가리키고 있다
function Person(name) {
  this.name = name;
}
const me = new Person('Lee');

console.log(Person.prototype === me.__proto__); // true

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

  • 모든 프로토 타입은 constructor 프로퍼티를 갖는다
  • 생성자 함수는 prototype 프로퍼티로 이 constructor 프로퍼티를 참조한다
  • constructor 프로퍼티는 자신을 참조하는 생성자 함수를 가리킨다
  • 이 연결은 함수 객체가 생성될 때 이뤄진다
function Person(name) {
  this.name = name;
}
const me = new Person('Lee');
console.log(me.constructor === Person); // true

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


  • 앞서, 생성자 함수에 의해 생성된 인스턴스는 프로토타입의 constructor 프로퍼티를 통해, 자신을 생성한 생성자 함수와 연결되어 있다.
  • 그러나 리터럴 표기법에 의한 객체 생성 방식은 constructor 프로퍼티가 객체를 생성한 생성자 함수가 아닐 수도 있다.
// 여러 리터럴 표기법

// 객체 리터럴
const obj = {};

// 함수 리터럴
const add = function(a,b) {return a + b;};

// 배열 리터럴
const arr = [1,2,3];

// 정규 표현식 리터럴
const regexp = /is/ig;

console.log(obj.constructor === Object); // 객체 리터럴로 생성된 obj 객체의 생성자 함수는 Object 생성자 함수
// 그러면... obj는 Object 생성자 함수가 만들었을까? => NoNo
  • 그러면... obj는 Object 생성자 함수가 만들었을까? => NoNo🙃
  • 객체 리터럴은 추상 연산 함수를 호출해 빈 객체를 생성한 뒤 프로퍼티를 추가하는 방식
  • 하지만, 이렇게 만들어진 객체 리터럴도 생성자 함수는 필요하므로 Object 생성자 함수에 연결시키는 것
리터럴 표기법생성자 함수프로토타입
객체 리터럴ObjectObject.prototype
함수 리터럴FunctionFunction.prototype
배열 리터럴ArrayArray.prototype
정규표현식 리터럴RegExpRegExp.prototype

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


  • 프로토 타입은 생성자 함수가 생성되는 시점에 같이 생성된다

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

함수 정의(constructor)가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 생성된다

// Person {}
// constructor: ƒ Person(name)
//__proto__: Object
console.log(Person.prototype);

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

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

빌트인 생성자 함수 : Object, String, Number, Function, Array, RegExp, Date, Promise 등등

  • 빌트인 생성자 함수도 생성자 함수가 생성되는 시점에 프로토 타입이 생성된다.
  • 즉, 전역 객체(window)가 생성되는 시점에 생성된다.

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


객체의 생성 방법들

  • 객체 리터럴
  • Object 생성자 함수
  • 생성자 함수
  • Object.create 메서드
  • 클래스

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

  • 앞서 확인했듯이, Object.prototype을 갖는다
  • 따라서, Object.prototype의 프로퍼티와 메서드를 자유롭게 쓸 수 있다
const obj = { x: 1 };

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

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

  • 너무 당연하게도.. Object.prototype을 갖는다
const obj = new Object();
obj.x = 1;

console.log(obj.constructor === Object); // true, Object가 아닐 수가 있나?
console.log(obj.hasOwnProperty('x')); // true

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

  • 자신을 생성한 생성자 함수를 가리킨다
  • 생성된 객체는 프로토타입의 메서드를 자유롭게 쓸 수 있다.
function Person(name) {
  this.name = name;
}
const me = new Person('Ed');
const you = new Person('Lee');

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

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

7. 프로토타입 체인


  • 객체의 프로퍼티에 접근하려고 할 때, 찾는 프로퍼티가 없다면, 해당 객체의 [[Prototype]]을 뒤져서 부모 객체의 프로퍼티를 검색한다.
function Person(name) {
  this.name = name;
}
const me = new Person('Ed');

console.log(me.hasOwnProperty('name')); // true
  • me의 프로토타입 : Person
  • Person의 프로토타입 : Object
  • 따라서 me는 Object의 함수인 hasOwnProperty()를 사용할 수 있다

프로토타입의 최상위 객체는 언제나 Object.prototype이다

  • 그래서 Object.prototype을 end of prototype chain이라고 부르며, Object의 [[Prototype]] 값은 null이다.

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


  • 오버라이딩 : 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식
  • 오버로딩 : 동일한 함수이름을 쓰지만, 인자로 구별되도록 사용하는 방식. js는 지원하지는 않지만, arguments 객체를 사용해 구현할 수는 있다
  • 섀도잉 : 상속관계에 의해 프로퍼티가 가려지는 현상
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('Ed');

me.sayHello = function() {
  console.log(`하이! 마이 네임 이즈 ${this.name}`);
}

me.sayHello(); // 하이! 마이 네임 이즈 Ed
  • 검색 순서가 자식 객체 -> 부모 객체순 이므로, me 인스턴스의 함수가 동작한다

9. 프로토타입의 교체


  • 부모 객체를 동적으로 교체할 수 있다...?😨
  • 이번 챕터는 이게 메인인 듯...했으나 딱히 그것도 아니었다..🤬

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

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

  Person.prototype = {
    // constructor: Person, // 이게 없으면, 함수 연결이 파괴되어 Object와 연결된다
    sayHello() {
      console.log(`Hi! My name is ${this.name}`);
    }
  };
  return Person;
}());

const me = new Person('Ed');

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

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

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

const me = new Person('Lee');

// 축약 표현이라 constructor가 없다
const parent = {
  sayHello() {
    console.log(`Hi! My name is ${this.name}`);
  }
}

Object.setPrototypeOf(me, parent);

me.sayHello();

// 그런데 parent는 생성자 함수가 없으므로.. Person과의 연결이 파괴된다
console.log(me.constructor === Person); // false
console.log(me.constructor === Object); // true

그런데 이처럼 프로토타입 교체를 동적으로 직접 교체하지는 않는다. 보통은 직접 상속이나 클래스를 사용한다

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


객체 instanceof 생성자 함수

  • 우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true
function Person(name) {
  this.name = name;
}

const me = new Person('Lee');

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

11. 직접 상속


11.1 Object.create에 의한 직접 상속

// 프로토타입이 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
  • 위 방법의 장점?
  • new 연산자 없이도 객체를 생성할 수 있다.
  • 프로토타입을 지정하면서 객체를 생성할 수 있다.
  • 객체 리터럴에 의해 생성된 객체도 상속받을 수 있다.

11.2 객체 리터럴 내부에서 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. 정적 프로퍼티/메서드


  • 정적 프로퍼티/메서드 : 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드를 의미
// 생성자 함수
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
  • Person 생성자 함수 객체가 소유한 프로퍼티/메서드는 인스턴스가 없음에도 바로 호출할 수 있다
  • 반면, me에서는 호출할 수 없는데, 프로토 타입 체인 상에 없기 때문.

13 프로퍼티 존재 확인


13.1 in 연산자

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

14 프로퍼티 열거


14.1 for...in문

for ( 변수 선언문 in 객체 ) { ... }

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

// for...in 문의 변수 prop에 person 객체의 프로퍼티 키가 할당된다.
for (const key in person) {
  console.log(key + ': ' + person[key]);
}
// name: Lee
// address: Seoul

14.2 Object.keys/values/entries 메서드

Object.keys

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

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"]]

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

0개의 댓글