[JS] 모던 자바스크립트 Deep Dive - 19장-1

Leona·2023년 12월 5일

프로토타입

  • 자바스크립트는 프로토타입 기반 객체지향 프로그래밍 언어
  • C++이나 자바같은 클래스, 상속, 캡슐화 등이 없어서 무슨 객체지향 언어냐 무시받곤 함

    Class
    ES6에서 도입되었지만 기존 프로토타입 기반 객체지향 모델을 폐지한 것은 아니다. 사실 클래스도 함수이고, 클래스와 생성자 함수 모두 프로토타입 기반 인스턴스를 생성한다. 이는 새로운 객체 생성 메커니즘으로 보는 것이 더 알맞다.

  • 자바스크립트를 이루고 있는 원시 타입 값을 제외한 모든 것이 객체다.

19.1 객체지향 프로그래밍

  • 객체: 속성으로 여러 개 값을 단위 하나로 구성한 복합적인 자료구조

  • 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임

  • 실세계의 실체(사물이나 개념)를 인식하는 철학적 사고를 프로그래밍에 접목하려는 시도

  • 실체를 구별할 수 있는 특징이나 성질을 나타내는 속성으로 실체를 인식하고 구별

    const person = {
     name: 'Leona',
     age: 30,
    }
    
    console.log(person); // {name: 'Leona', age: 30}
  • 레오나를 인식하고 구별할 수 있는 속성은 많으나 그 중에 이름과 나이만 추려 표현한 것을 추상화라고 한다.

  • 또한 객체는 상태(프로퍼티)와 동작(메서드)을 논리적인 단위 하나로 묶은 복합적인 자료구조이다.

    const circle = {
      // 반지름
      radius: 5,
      // 원의 지름
      getDiameter() {
        return 2 * this.radius;
      },
      // 원의 둘레, 너비 등...
      ...
    }
    
    // Quiz
    console.log(circle.radius);     	// ???
    console.log(circle.getDiameter()); // ???

19.2 상속과 프로토타입

  • 상속은 객체지향 프로그래밍의 핵심 개념. 어떤 객체의 프로퍼티나 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 의미

  • 자바스크립트는 프로토타입 기반으로 상속을 구현해서 불필요한 중복 제거 = 기존 코드 재사용

  • 코드를 재사용하면 개발 비용을 현저히 줄일 수 있다.

    // 생성자 함수
    function Circle(radius) {
     this.radius = radius;
     this.getArea = function () {
       return Math.PI * this.radius ** 2;
     }
    }
    
    // Circle 인스턴스
    const circle1 = new Circle(1);
    const circle2 = new Circle(2);
    
    // 모든 인스턴스가 중복으로 갖고 있는 메서드라 false
    console.log(circle1.getArea === circle2.getArea); // false
    
     // 각 Circle 인스턴스 값은 다를지라도 getArea 메서드는 동일한 내용이다.
    console.log(circle1.getArea());
    console.log(circle2.getArea());
  • 프로토타입 상속

    // 생성자 함수
    function Circle(radius) {
     this.radius = radius;
    }
    
    Circle.prototype.getArea = function () {
     return Math.PI * this.radius ** 2;
    }
    
    // 부모 객체의 프로토타입 Circle.prototype으로부터 getArea를 상속받았기 때문에 true
    console.log(circle1.getArea === circle2.getArea); // true
    
     // 각 Circle 인스턴스가 getArea 메서드를 공유한다.
    console.log(circle1.getArea());
    console.log(circle2.getArea());
  • Circle 생성자 함수가 생성한 모드 인스턴스는 부모 역할을 하는 자신의 프로토타입의 모든 프로퍼티와 메서드를 상속받는다.(Circle.prototype.getArea)

  • 모든 인스턴스가 공통적으로 사용할 프로퍼티나 메서드를 프로토타입에 미리 구현해 두면 인스턴스는 별도로 구현할 필요없이 상위 객체(프로토타입) 자산을 공유해서 사용할 수 있다.

19.3 프로토타입 객체

  • 모든 객체는 프로토타입을 가진다.
  • 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용
  • 프로토타입은 어떤 객체의 부모(상위) 객체 역할을 하는 객체다.
  • 다른 객체에 공유 프로퍼티, 메서드를 제공하며, 자식(하위) 객체는 이 프로퍼티와 메서드를 자유롭게 사용할 수 있다.
  • 모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조다.(null인 경우는 프로토타입이 없는 경우다.)
  • [[Prototype]] 은 객체 생성 방식에 따라 프로토타입이 결정되고 이 슬롯에 담긴다.
  • [[Prototype]] 은 직접 접근할 수 없고 _ _ proto _ _ 접근자 프로퍼티로 간접적으로 접근할 수 있다.
  • 프로토타입은 constructor 프로퍼티로 생성자 함수에 접근, 생성자 함수는 자신의 prototype 프로퍼티로 접근할 수 있다.

19.3.1 _ _ proto _ _ 접근자 프로퍼티

_ _ proto _ _는 접근자 프로퍼티다

  • 모든 객체는 _ _ proto _ _ 접근자 프로퍼티로 자신의 프로토타입([[Prototype]] 슬롯)에 간접적으로 접근할 수 있다.

  • 접근자 프로퍼티는 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수([[Get]], [[Set]] 프로퍼티 어트리뷰트로 구성된 함수)로 구성된 프로퍼티

    const obj = {};
    const parent = {x: 1};
    
    obj.__proto__; // get
    obj.__proto__ = parent; // set
    
    console.log(obj.x); // 1

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

  • _ _ proto _ _는 Object.prototype의 프로퍼티로, 상속을 통해 사용할 수 있다.

    Object.prototype
    모든 객체는 프로토타입 계층 구조인 프로토타입 체인에 묶여있다. 자바스크립트 엔진은 객체의 프로퍼티(+메서드)에 접근하려고 할 때 해당 객체에 프로퍼티가 없다면 _ _ proto _ _ 접근자 프로퍼티가 가리키는 참조를 따라 자신의 부모 프로토타입의 프로퍼티를 순차적으로 검색한다.
    프로토타입 최상위 객체는 Object.prototype이며, 이 프로퍼티와 메서드는 모든 객체에 상속된다.

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

  • 서로가 자신의 프로토타입이 되는 비정상적인 프로토타입 체인을 방지하기 위함이다.

    const parent = {};
    const child = {};
    
    child.__proto__ = parent;
    parent.__proto__ = child; // TypeError: Cyclic __proto_ value
  • 프로토타입 체인은 단방향 링크드 리스트로 구현되어야 한다.(프로퍼티 검색 방향이 한쪽 방향으로만 흘러야 한다)

  • 단방향 링드크 리스트: 각 노드가 데이터와 포인터를 가지고 한 줄로 연결되어 있는 방식으로 데이터를 저장하는 자료 구조

_ _ proto _ _접근자를 코드 내에서 직접 사용하는 것은 권장하지 않는다.

  • 모든 객체가 _ _ proto _ _ 접근자 프로퍼티를 사용할 수 있는 것은 아니다.

  • 직접 상속을 통해 Object.prototype을 상속받지 않는 객체를 생성할 수도 있기 때문에 _ _ proto _ _ 접근자 프로퍼티를 사용할 수 없는 경우도 있다.

    // 프로토타입 체인의 종점이기 때문에 Object.__proto__를 상속받을 수 없다.
    const obj = Object.create(null);
    const parent = {x: 1};
    
    console.log(obj.__proto__); // undefined
    
    // 프로토타입을 참조할 때
    console.log(Object.getPrototypeOf(obj)); // null
    // 프로토타입을 교체할 때
    Object.setPrototypOf(obj, parent); // obj.__proto__ = parent;

19.3.2 함수 객체의 prototype 프로퍼티

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

// prototype 프로퍼티를 소유하는 함수 객체
(function () {}).hasOwnProperty('prototype'); // true
// 일반 객체는 소유하지 않는다.
({}).hasOwnProperty('prototype'); // false
  • 생성자 함수로 호출할 수 없는 함수, 즉 화살표 함수와 ES6 메서드 축약 표현(https://velog.io/@dev-redo/Javascript-ES6-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%B6%95%EC%95%BD-%ED%91%9C%ED%98%84)으로 정의한 메서드는 prototype 프로퍼티를 소유하지 않고 프로토타입도 생성하지 않음
    구분 소유 사용 주체 사용 목적
    __proto__
    접근자 프로퍼티
    모든 객체 프로토타입 참조 모든 객체 객체가 자신의 프로토타입에 접근 또는 교체하기 위함
    prototype
    프로퍼티
    생성자 프로토타입 참조 생성자 함수 생성자 함수가 생성할 객체(인스턴스)의 프로토타입을 할당하기 위함

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

  • 모든 프로토타입은 constructor 프로퍼티를 갖는데, 이 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다.

  • 생성자 함수가 생성될 때(함수 객체가 생성될 때) 연결된다.

    // 생성자 함수
    function Person(name) {
     this.name = name;
    }
    
    const me = new Person('Leona');
    console.log(me.constructor === Person); // true
  • 'me' 객체는 constructor 프로퍼티가 없어도 생성자인 Person이 constructor 프로퍼티를 가지고 있기 때문에 이를 상속받아 constructor 프로퍼티를 사용할 수 있는 것이다.

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

  • 생성자 함수로 생성된 인스턴스는 프로토타입의 constructor 프로퍼티로 생성자 함수와 연결된다.
  • constructor 프로퍼티가 가리키는 생성자 함수 = 인스턴스를 생성한 생성자 함수
  • 리터럴 표기법으로 생성한 객체도 프로토타입이 존재하나 반드시 그 constructor 프로퍼티가 가리키는 생성자 함수가 해당 객체의 생성자 함수라고 단정할 수 없다.
    // 생성자 함수로 생성한 객체가 아닌 객체 리터럴
    const obj = {};
    // 그러나 obj의 생성자 함수는 Object 생성자 함수다.
    console.log(obj.constructor === Object); // true

    결론
    1.프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재한다.
    2.리터럴 표기법으로 생성한 객체도 가상적인 생성자 함수를 갖는다.
    3.생성자 함수로 생성한 객체나 리터럴 표기법으로 생성한 객체나 본질적인 차이는 없다.

19.5 프로토타입의 생성 시점

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

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

  • 함수 선언문은 런타임 이전에 자바스크립트 엔진이 먼저 실행한다.

  • 함수 선언문으로 정의한 생성자 함수는 어떤 코드보다 먼저 평가되어 함수 객체가 된다. 이때 프로토타입도 함께 생성된다.

  • 이렇게 생성된 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩된다.

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

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

  • Object, String, Number, Function, Array, RegExp, Date, Promise 등 빌트인 생성자 함수도 생성되는 시점에 프로토타입이 함께 생성된다.

객체가 생성되기 전 이미 생성자 함수와 프로토타입이 객체화되어 존재하기 때문에 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면 프로토타입은 생성된 객체의 [[Prototype]] 내부 슬롯에 할당된다. 이렇게 생성된 객체는 프로토타입을 상속받는다.

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

  • 다양한 방식으로 생성한 모든 객체는 추상연산에 의해 생성된다는 공통점이 있다.
  • 추상연산: 대충 내부 동작 구현 알고리즘
  • 추상연산이 생성할 객체의 프로토타입과 객체 프로퍼티 목록(옵셔널)을 인수로 전달 -> 빈 객체 생성 -> 객체 프로퍼티 목록이 있는 경우 객체 프로퍼티에 추가 -> 인수로 전달받은 프로토타입을 [[Prototype]] 내부 슬롯에 할당 -> 객체 반환

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

  • 객체 리터럴을 생성할 때 추상연산이 인수로 전달하는 프로토타입은 Object.prototype이다.
  • Object.prototype을 상속받아 자신의 프로토타입으로 사용

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

  • 객체 리터럴과 마찬가지로 Object.prototype을 상속받아 자신의 프로토타입으로 사용한다.

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

  • 생성자 함수의 prototype 프로퍼티가 바인딩된다.

19.7 프로토타입 체인

  • 자바스크립트가 객체지향 프로그래밍의 상속을 구현하는 것과 프로퍼티 검색을 위한 메커니즘
  • 자바스크립트는 객체의 프로퍼티와 메서드에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부 슬롯의 참조를 따라 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다.
    // Object.prototype의 메서드로, 프로토타입 체인을 따라 hasOwnProperty 메서드를 검색해서 사용한다.
    me.hasOwnProperty('name') // true
  • 자바스크립트 엔진이 메서드를 검색하는 과정
    1. me 객체에서 hasOwnProperty를 검색한다. me 객체에는 hasOwnProperty 메서드가 없으므로 프로토 타입 체인을 따라 [[Prototype]] 내부 슬롯에 바인딩되어 있는 프로토타입(Person.prototype)으로 이동해서 메서드를 검색한다.
    2. Person.prototype에도 hasOwnProperty가 없으면 다시 프로토타입 체인을 따라 [[Prototype]] 내부 슬롯에 바인딩 되어있는 프로토타입(Object.prototype)으로 이동해서 메서드를 검색한다.
    3. Object.prototype에 있는 hasOwnProperty를 호출한다. 이때 해당 메서드의 this에는 me 객체가 바인딩된다.
      Object.prototype.hasOwnProperty.call(me, 'name');
  • 프로토타입 체인의 최상위에는 Object.prototype이 있기 때문에 모든 객체는 이 prototype을 상속받는다.
  • Object.prototype의 프로토타입, [[Prototype]] 내부 슬롯은 null이다. 만약 여기서도 프로퍼티를 검색하지 못하는 경우 undefined를 반환한다.(에러x)
  • 스코프체인: 함수 중첩 관계로 이루어진 스코프의 계층적 구조에서 식별자를 검색하는 메커니즘
    // me 식별자는 전역에 선언되었으므로 전역 스코프에서 검색된다.
    // me 식별자 검색 -> me 객체 프로토타입 체인에서 hasOwnProperty 메서드 검색
    me.hasOwnProperty('name');

    스코프 체인과 프로토타입 체인은 서로 협력하며 식별자와 프로퍼티를 검색하는 데 사용된다.

profile
레오나의 기묘한 개발 일지

0개의 댓글