본문 바로가기
Books/모던 JS Deep Dive ✔️

[모던 JS Deep Dive] 19장 - 프로토타입

by Aaron-Kim 2021. 12. 30.

19.0 프로토타입

  - JS는 멀티 패러다임 프로그래밍 언어 (명령형, 함수형, 프로토타입 기반 객체 지향 프로그래밍 지원)

  - ES6 클래스 도입

    - 클래스도 함수임, 기존 프로토타입 기반 패턴의 syntactic sugar -> 새로운 객체 생성 메커니즘 시각

    - 클래스와 생성자 함수는 모두 프로토타입 기반의 인스턴스 생성하지만 정확하게 동일하게 동작 X

    - 클래스는 생성자 함수보다 엄격하고 생성자 함수에서 제공하지 않는 기능도 제공

  - JS는 객체 기반의 프로그래밍 언어, JS를 이루고 있는 거의 모든 것이 객체임 (함수, 배열, 정규 표현식, ...)

19.1 객체지향 프로그래밍

  - 명령형 프로그래밍의 절차지향적 관점 => 프로그램을 명령어 or 함수의 목록으로 바라봄

  - 객체 지향 프로그래밍: 여러 개의 독립적 단위, 독립적인 객체의 집합으로 프로그램 표현

    - 객체: 상태 데이터(프로퍼티), 동작(메서드)을 하나의 논리적 단위로 묶은 복합적인 자료구조

      - 속성 (attribute/property); 실체(사물, 개념)를 인식하거나 구별 가능, 객체의 상태를 나타내는 데이터

      - 메서드; 객체의 상태 데이터를 조작할 수 있는 동작

  - 추상화: 다양한 속성 중에서 프로그램에 필요한 속성만 간추려 내어 표현

19.2 상속과 프로토타입

  - 상속: 어떤 객체의 프로퍼티/메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것

  - JS는 프로토타입 기반으로 상속 구현하여 불필요햔 중복 제거 (코드 재사용)

  - 생성자 함수; new 연산자로 인스턴스 생성

    - 객체의 프로퍼티 값은 일반적으로 인스턴스마다 다름

    - 메서드는 모든 인스턴스가 동일한 내용의 메서드를 사용하므로 단 하나만 생성하여

      모든 인스턴스가 공유해서 사용하는 것이 바람직함 (내용이 동일한 메서드의 중복 생성 문제)

    - 상속을 통해 불필요한 중복 제거 필요 (JS는 프로토타입 기반의 상속)

    - 모든 인스턴스가 메서드 공유해서 사용할 수 있도록 프로토타입에 추가

      (프로토타입은 Circle 생성자 함수의 prototype 프로퍼티에 바인딩되어 있음)

      - ex. Circle.prototype.getArea = function() { ... };

  - 상속은 코드의 재사용 관점에서 매우 유용

19.3 프로토타입 객체

  - 프로토타입 객체; OOP의 근간을 이루는 객체 간 상속을 구현하기 위해 사용됨

    - 어떤 객체의 상위(부모) 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티/메서드 제공

    - 모든 객체는 [[Prototype]] 내부 슬롯 가짐, 내부 슬롯의 값은 프로토타입의 참조 (null 가능)

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

      - 객체 리터럴에 의해 생성된 객체의 프로토타입 => Object.prototype

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

    - 모든 객체는 하나의 프로토타입 가짐, 모든 프로토타입은 생성자 함수와 연결되어 있음
      (객체 - 프로토타입 - 생성자 함수)

    - 프로토타입은 자신의 constructor 프로퍼티를 통해 생성자 함수에 접근 가능,

      생성자 함수는 자신의 prototype 프로퍼티를 통해 프로토타입에 접근 가능

  - __proto__ 접근자 프로퍼티

    - 모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입, 즉 [[Prototype]] 내부 슬롯에 간접 접근 가능

      (Object.prototype)

    - __proto__는 접근자 프로퍼티다.

      - 내부 슬롯은 프로퍼티가 아님, JS에서 내부 슬롯/메서드 직접 접근, 호출 방법 제공 X

      - 일부 내부 슬롯과 메서드에 한해서 간접 접근 수단 제공

      - [[Prototype]] 내부 슬롯 직접 접근 불가, __proto__ 접근자 프로퍼티 통해 간접적으로 접근 가능 (프로토타입)

      - 접근자 프로퍼티는 [[Value]] 없고 [[Get]], [[Set]] 프로퍼티 어트리뷰트로 구성된 프로퍼티

    - __proto__ 접근자 프로퍼티는 상속을 통해 사용된다.

      - __proto__ 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype의 프로퍼티임

      - 모든 객체는 상속을 통해 Object.prototype.__proto__ 접근자 프로퍼티 사용 가능

      - Object.prototype (모든 객체의 프로토타입 객체)

        - 모든 객체는 프로토타입의 계층 구조인 프로토타입 체인에 묶여 있음

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

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

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

      - 프로토타입 체인은 단방향 링크드 리스트로 구현되어야 함

      - 순환 참조하는 프로토타입 체인이 만들어지면 프로토타입 체인 종점이 존재하지 않기 때문에

        프로토타입 체인에서 프로퍼티 검색 시 무한 루프에 빠짐

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

      - 브라우저 호환성 고려하여 ES6에서 __proto__ 표준이 됨

      - 모든 객체가 __proto__ 접근자 프로퍼티 사용할 수 있는 것은 아님

        - 직접 상속을 통해 Object.prototype 상속받지 않는 객체 생성 가능 => 접근자 프로퍼티 사용 불가

        - ex. Object.create(null) => 프로토타입 체인의 종점, 접근자 프로퍼티 상속 불가

        - __proto__ 보다 Object.getPrototypeOf() 메서드 사용 권장 (프로토타입 참조 취득) // ES5

        - 프로토타입 교체 시에는 Object.setPrototypeOf() // ES6

  - 함수 객체의 prototype 프로퍼티

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

      - 함수 객체는 prototype 프로퍼티 소유 O

      - 일반 객체는 prototype 프로퍼티 소유 X

      - non-constructor인 화살표 함수, ES6 메서드 축약 표현 => prototype 프로퍼티 소유 X, 프로토타입 생성 X

    - 모든 객체(인스턴스)가 가지고 있는 (Object.prototype으로부터 상속 받은) __proto__ 접근자 프로퍼티와

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

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

    - 모든 프로토타입은 constructor 프로퍼티를 가짐

    - constructor 프로퍼티는 자신을 참조하는 생성자 함수 가리킴

    - 인스턴스 객체는 프로토타입의 constructor 프로퍼티 상속받아 사용 가능

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

  - 리터럴 표기법에 의한 객체 생성 방식 (new 연산자 사용하여 생성자 함수 호출하면서 인스턴스 생성 X)

  - 리터럴 표기법에 의해 생성된 객체도 프로토타입 존재

    - 프로토타입의 constructor 프로퍼티가 가리키는 생성자 함수가 반드시 객체를 생성한 생성자 함수로 단정 불가

    - Object 생성자 함수에 인수 전달 X, undefiend or null 을 인수로 전달하면서 호출 시

      내부적으로 추상 연산 OrdinanryObjectCreate 호출하여 Object.prototype을 프로토타입으로 갖는 빈 객체 생성

    - 객체 리터럴 평가 시 추상 연산 OrdinaryObjectCreate 호출하여 빈 객체 생성하고 프로퍼티 추가하도록 정의됨

    - 객체 리터럴에 의해 생성된 객체는 Object 생성자 함수가 생성한 객체가 아님

    - 리터럴 표기법에 의해 생성된 객체도 가상적인 생성자 함수 가짐

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

19.5 프로토타입의 생성 시점

  - 결국 모든 객체는 생성자 함수와 연결되어 있음

  - 프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성됨

    - 프로토타입과 생서자 함수는 언제나 쌍으로 존재

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

    - 생성자 함수로서 호출 가능한 함수 (constructor)는 함수 정의가 평가되어 함수 객체 생성 시점에

      프로토타입도 더불어 생성됨

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

      - 생성된 프로토타입은 오직 constructor 프로퍼티만 갖는 객체임

      - 생성된 프로토타입의 프로토타입은 Object.prototype (모든 객체는 프로토타입 가짐)

    - non-constructor는 프로토타입이 생성되지 않음

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

    - 빌트인 생성자 함수가 생성되는 시점에 프로토타입이 생성됨

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

    - 빌트인 객체 Object는 전역 객체의 프로퍼티임

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

      (생성된 객체는 프로토타입 상속 받음)

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

  - 객체의 다양한 생성 방법

    - 객체 리터럴

    - Object 생성자 함수

    - 생성자 함수

    - Object.create 메서드

    - 클래스(ES6)

  - 프로토타입은 추상 연산 OrdinaryObjectCreate에 전달되는 인수에 의해 결정됨

    (인수는 객체가 생성되는 시점에 객체 생성 방식에 의해 결정됨)

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

    - JS 엔진이 객체 리터럴 평가하여 객체 생성 시 추상 연산 OrdinaryObjectCreate 호출

    - 객체 리터럴에 의해 생성된 객체의 프로토타입 => Object.prototype (상속 받음)

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

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

    - Object 생성자 함수 호출 시 추상 연산 OrdinaryObjectCreate가 호출됨

    - Object 생성자 함수에 의해 생성된 객체의 프로토타입 => Object.prototype

      - 다양한 빌트인 메서드(hasOwnProperty, propertyIsEnumerable 등) 가지고 있음

    - 객체 리터럴과의 차이는 프로퍼티 추가 방식의 차이

      - 객체 리터럴 방식; 객체 리터럴 내부에 프로퍼티 추가

      - Object 생성자 함수 방식; 일단 빈 객체 생성 후 프로퍼티 추가

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

    - new 연산자 사용하여 생성자 함수 호출하여 인스턴스 생성 시 추상 연산 OrdinaryObjectCreate 호출됨

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

      - 생성된 프로토타입의 프로퍼티는 constructor 뿐임

    - 표준 빌트인 생성자 함수에 의해 생성된 객체의 프로토타입에는 다양한 빌트인 메서드 존재

19.7 프로토타입 체인

  - 모든 객체는 Object.prototype 상속 받음 (프로토타입의 프로토타입은 언제나 Object.prototype)

  - 프로토타입 체인: JS가 OOP의 상속을 구현하는 메커니즘 (상속과 프로퍼티 검색을 위한 메커니즘)

    - JS는 객체의 프로퍼티/메서드에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면

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

  - Object.prototype은 프로토타입 체인의 종점임 ([[prototype]] 내부 슬롯의 값은 null)

    - 만약 최상위 프로토타입에서 프로퍼티 검색 못하면 undefined 반환 (에러 발생 X)

  - 프로퍼티가 아닌 식별자는 스코프 체인에서 검색!

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

    - ex. const me = new Person('Lee');

          me.hasOwnProperty('name');

    - 스코프 체인에서 me 식별자 검색, me 식별자는 전역에서 선언되었으므로 전역 스코프에서 검색됨

    - me 식별자 검색한 다음 me 객체의 프로토타입 체인에서 hasOwnProperty 메서드 검색

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

  - 프로토타입이 소유한 프로퍼티/메서드 => 프로토타입 프로퍼티

    인스턴스가 소유한 프로퍼티 => 인스턴스 프로퍼티

  - 프로퍼티 섀도잉: 상속 관계에 의해 프로토타입의 프로퍼티 가려지는 현상 (인스턴스 메서드 오버라이딩에 의해)

  - 하위 객체를 통해 프로토타입 체인을 거쳐 프로토타입의 메서드가 삭제되지는 않음

    - 하위 객체를 통해 프로토타입에 get 액세스는 허용되지만 set 엑세스는 허용되지 않음

    - 프로토타입 프로퍼티에 직접 접근해야 프로토타입의 프로퍼티/메서드 삭제 또는 변경 가능

19.9 프로토타입의 교체

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

    - 생성자 함수의 prototype 프로퍼티를 통해 프로토타입 교체

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

      - 프로토타입의 constructor 프로퍼티로 인스턴스의 생성자 함수 검색 시 Object 나옴

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

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

    - 생성자 함수의 prototype 프로퍼티에 다른 임의의 객체 바인딩하는 것은

      미래에 생성할 인스턴스의 프로토타입 교체하는 것을 의미

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

    - 생성자 함수에 의한 프로토타입의 교체와의 차이

      - 생성자 함수에 의한 프로토타입 교체; 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입 가리킴

      - 인스턴스에 의한 프로토타입 교체; 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입 가리키지 X

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

      - 프로토타입 직접 교체 권장 X

      - 프로토타입 직접 상속이 더 편리하고 안전

      - ES6 클래스 사용해도 직관적으로 상속 구현 가능

19.10 instanceof 연산자

  - 객체 instanceof 생성자 함수

    - 이항 연산자, 좌변: 객체 가리키는 식별자 / 우변: 생성자 함수 가리키는 식별자

    - 우변의 피연산자가 함수가 아니면 TypeError 발생

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

       true로 평가되고, 그렇지 않으면 false로 평가됨

  - instanceof 연산자는 프로토타입의 constructor 프로퍼티가 가리키는 생서자 함수를 찾는 것이 아니라,

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

19.11 직접 상속

  - Object.create에 의한 직접 상속

    - Object.create(): 명시적으로 프로토타입 지정하여 새로운 객체 생성

                           지정된 프로토타입 및 프로퍼티 갖는 새로운 객체 생성하여 반환

      - 추상 연산 OrdinaryObjectCreate 호출

      - 첫번째 매개변수: 생성할 객체의 프로토타입으로 지정할 객체

      - 두번쨰 매개변수: 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터 객체로 이뤄진 객체 [옵션]

      - 첫번째 매개변수에 전달한 객체의 프로토타입 체인에 속하는 객체 생성

        (객체 생성하면서 직접적으로 상속 구현)

    - 프로토타입이 null인 객체 => 프로토타입 체인상 종점에 위치 (Object.prototype 상속받지 못함)

    - Object.create() 메서드 장점

      - new 연산자 없이 객체 생성 가능

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

      - 객체 리터럴에 의해 생성된 객체도 상속받을 수 있음

    - ESLint 에서 Object.prototype의 빌트인 메서드를 객체가 직접 호출하는 것 권장 X

      (Object.create 메서드를 통해 프로토타입 체인 종점에 위치하는 객체를 생성할 수 있음

       => 프로토타입 체인 종점에 위치하는 객체는 Object.prototype 빌트인 메서드 사용 불가)

      - Function.prototype.call 메서드 이용하여 간접 호출하는 것이 좋음

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

    - ES6 객체 리터럴 내부에 __proto__ 접근자 프로퍼티 사용하여 직접 상속 구현 가능

19.12 정적 프로퍼티/메서드

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

    - 생성자 함수쪽에 생김

    - 생성자 함수를 이용하여 호출

    - 생성자 함수가 생성한 인스턴스로 참조/호출 불가

    - 인스턴스로 참조/호출할 수 있는 프로퍼티/메서드는 프로토타입 체인 상에 존재해야 함

  - ex. Object.create() 에서 create() 메서드는 Object의 정적 메서드,

         Object.prototype.hasOwnProperty() 에서 hasOwnProperty() 메서드는 프로토타입 메서드

  - 인스턴스/프로토타입 메서드 내에서 this 사용하지 않으면 정적 메서드로 변경 가능

  - MDN 문서; 정적 프로퍼티/메서드, 프로토타입 프로퍼티/메서드 구분

    - 프로토타입 프로퍼티/메서드 앞에 # 붙여서 표기하는 경우도 있음

19.13 프로퍼티 존재 확인

  - in 연산자

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

    - key in object

      - key: 프로퍼티 키를 나타내는 문자열

    - in 연산자는 확인 대상 객체의 프로퍼티뿐만 아니라

      확인 대상 객체가 상속받은 모든 프로토타입의 프로퍼티 확인함! (주의 필요)

    - ES6 Reflect.has(객체, 키) 메서드 사용 가능

  - Object.prototype.hasOwnProperty 메서드

    - 객체에 특정 프로퍼티 존재하는지 확인 가능

    - Object.prototype.hasOwnProperty(키)

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

        상속받은 프로토타입의 프로퍼티 키면 false 반환

19.14 프로퍼티 열거

  - for ... in 문

    - 객체의 모든 프로퍼티 순회하며 열거하기 위해 사용 (객체의 프로퍼티 개수만큼 순회)

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

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

      하지만 Object.prototype.toString() 와 같이 프로퍼티 어트리뷰트 [[Enumerable]]가 false인 것은 열거 안함

    - for ... in 문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서

      프로퍼티 어트리뷰트 [[Enumerable]]의 값이 true인 프로퍼티를 순회하며 열거함

    - 프로퍼티 키가 심벌인 프로퍼티는 열거 안함

    - 객체 자신의 프로퍼티만 연결하려면 Object.prototype.hasOwnProperty() 메서드 사용

    - for ... in 문은 프로퍼티 열거 시 순서 보장하지 않음!

      - 대부분의 모던 브라우저는 순서 보장함 (문자열인 숫자에 대해서 정렬 실시)

    - 배열에 for ... in 문 말고 일반적인 for 문이나 for ... of 문 또는 Array.prototype.forEach() 메서드 사용 권장!

      - 배열도 객체이므로 프로퍼티와 상속받은 프로퍼티가 포함될 수 있음

      - forEach() 메서드는 요소가 아닌 프로퍼티는 제외함

      - for ... of 문은 변수 선언문에서 선언한 변수에 키가 아닌 값을 할당함

  - Object.keys/values/entries 메서드

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

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

    - Object.values() 메서드; ES8, 객체 자신의 열거 가능한 프로퍼티 값을 배열로 반환

    - Object.entries() 메서드; ES8, 객체 자신의 열거 가능한 프로퍼티 키와 값의 쌍의 배열을 배열에 담아 반환

      - cf. Object.entries(person).forEach((key, value) => { ... })

반응형

댓글