[JavaScript] 딥다이브 / 19장 프로토타입

ungnam·2023년 6월 22일
0
post-thumbnail

객체지향 프로그래밍

프로그램을 명령어 또는 함수의 목록으로 보는 전통적인 명령형 프로그래밍의 절차지향적 관점에서 벗어나 여러 개의 독립적 단위, 즉 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임을 말한다.

상태 데이터와 동작을 하나의 논리적인 단위로 묶은 복합적인 자료구조객체라고 하며, 이 때 객체의 상태 데이터를 프로퍼티, 동작을 메서드라 부른다.

각 객체는 자신의 고유한 기능을 수행하면서 다른 객체와 관계를 가지고, 메시지를 주고받거나 데이터를 처리한다. 또는 다른 객체의 상태 데이터나 동작을 상속받아 사용하기도 한다.

상속과 프로토타입

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

자바스크립트는 프로토타입을 기반으로 상속을 구현한다.

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

생성자 함수에 의해 만들어지는 모든 객체(인스턴스)는 radius 프로퍼티 및 getArea 메서드를 갖는데, radius 는 일반적으로 인스턴스마다 다른 값을 갖지만, getArea는 모든 인스턴스에 대해 동일하다.
모든 인스턴스가 getArea을 공통적으로 갖는 것은 불필요한 중복을 야기하기 때문에 getArea 메서드를 프로토타입 객체(=생성자 함수의 prototype 프로퍼티에 바인딩)에 생성하는 것이 바람직하다.

function Circle(radius) {
  this.radius = radius;
}
// 모든 인스턴스는 Circle.prototype으로부터 getArea 메서드를 상속받는다.
// 즉 하나의 getArea 메서드를 모든 인스턴스가 공유한다.
Circle.prototype.getArea = function () {
  return Math.PI * this.radius ** 2;
};

프로토타입 객체

어떤 객체의 상위(부모) 객체 역할을 하는 객체로서 하위(자식) 객체에게 공유 프로퍼티를 제공한다.

__proto__ 접근자 프로퍼티

모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입, 즉 [[Prototype]] 내부 슬롯에 간접적으로 접근할 수 있다.

  • __proto__는 접근자 프로퍼티이다.
    __proto__는 getter/setter 함수라고 부르는 접근자 함수를 통해 [[Prototype]] 내부 슬롯의 값, 즉 프로토타입을 취득하거나 할당한다.

  • __proto__ 접근자 프로퍼티는 상속을 통해 사용된다.
    __proto__는 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype의 프로퍼티이기 때문에 모든 객체는 상속을 통해 __proto__를 사용한다.

  • __proto__ 접근자 프로퍼티를 통해 프로토타입에 접근하는 이유?
    상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서다.

  • __proto__ 접근자 프로퍼티를 코드 내에서 직접 사용하는 것은 권장하지 않는다.
    객체가 프로토타입의 종점에 위치하는 경우 __proto__를 사용할 수 없게 된다. -> Object.getPrototypeOf/setPrototypeOf 메서드 사용을 권장

함수 객체의 prototype 프로퍼티

함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다. 화살표 함수나 ES6 메서드 축약 표현 같은 non-constructor의 경우 prototype 프로퍼티를 소유하지 않기 때문에 프로토타입도 만들어 질 수 없다.

__proto__prototype은 사용 주체 및 사용 목적만 다르고, 가리키는 값은 프로토타입의 참조로 서로 같다.

  • __proto__: 객체가 자신의 프로토타입에 접근 또는 교체하기 위해 사용
  • prototype: 생성자 함수가 자신이 생성할 인스턴스의 프로토타입을 할당하기 위해 사용
function Person(name) {
  this.name = name;
}
const me = new Person('Lee');
console.log(Person.prototype === me.__proto__); // true

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

모든 프로토타입이 갖는 constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다.
이 연결 관계는 생성자 함수가 생성될 때, 즉 함수 객체가 생성될 때 이뤄지며, 생성자 함수에 의해 생성된 인스턴스는 프로토타입의 constructor 프로퍼티에 의해 생성자 함수와 연결된다.

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

리터럴 표기법에 의해 생성된 객체도 가상의 생성자 함수를 가지며 더불어 생성된 프로토타입과 prototype, constructor 프로퍼티에 의해 상호 연결되어 있다. 따라서 생성자 함수로 생성한 객체와 본질적인 측면에서 큰 차이가 없다.

리터럴 표기법에 의해 생성된 객체는 생성자 함수에 의해 생성된 객체는 아니지만 상속을 위해 프로토타입이 필요하므로 가상의 생성자 함수(객체 리터럴의 경우 Object)를 가지게 된다.

프로토타입의 생성 시점

프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성되어 생성자 함수의 prototype 프로퍼티에 바인딩되며, 객체가 생성될 때 생성된 객체의 [[Prototype]] 내부 슬롯에 할당된다.

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

함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다. 이 때 생성된 프로토타입의 프로토타입은 Object.prototype이다.

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

빌트인 생성자 함수는 전역 객체(브라우저의 경우 window)가 생성되는 시점에 생성되는데 이 때 프로토타입도 더불어 생성된다.

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

세부적인 객체 생성 방식의 차이는 있으나 추상 연산
OrdinaryObjectCreate에 의해 생성된다는 공통점이 있다.

OrdinaryObjectCreate는 빈 객체를 생성한 후, 객체에 추가할 프로퍼티 목록이 인수로 전달된 경우 프로퍼티를 객체에 추가하고, 인수로 전달받은 프로토타입을 자신이 생성한 객체의 [[Prototype]] 내부 슬롯에 할당한 다음 생성한 객체를 반환한다.

이 때 객체 리터럴 및 Object 생성자 함수에 의해 생성된 객체의 경우, OrdinaryObjectCreate에 전달되는 프로토타입은 Object.prototype이며 생성자 함수에 의해 생성된 객체의 경우 OrdinaryObjectCreate에 전달되는 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체이다.

프로토타입 체인

객체의 프로퍼티(메서드 포함)에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 프로토타입의 프로퍼티를 순차적으로 검색하는 것을 의미한다.
프로토타입 체인은 JS가 객체지향 프로그래밍의 상속을 구현하는 메커니즘이다.

function Person(name) {
  this.name = name;
}
const me = new Person('Lee');

me.hasOwnProperty('name');
  1. 스코프 체인에서 me 식별자 검색 -> 전역 스코프에서 검색됨
  2. me 객체의 프로토타입 체인에서 hasOwnProperty 메서드 검색
    (me --> Person.prototype --> Object.prototype)
  3. Object.prototypehasOwmProperty 메서드가 존재
    ->Object.prototype.hasOwnProperty 메서드의 thisme 객체가 바인딩되어 호출됨

이처럼 상속과 프로퍼티 검색을 위한 프로토타입 체인식별자 검색을 위한 스코프 체인은 서로 협력하여 식별자와 프로퍼티를 검색하는 데 사용된다.

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

프로토타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스에 추가하면 덮어씌우는 것이 아니라 인스턴스 프로퍼티로 새롭게 추가됨, 이렇게 상속받은 부모의 프로퍼티를 재정의하는 것을 오버라이딩이라 하며, 상속 관계에 의해 프로퍼티가 가려지는 현상을 프로퍼티 섀도잉이라 한다.

오버로딩
함수의 이름은 동일하지만 매개변수의 타입 또는 개수가 다른 메서드를 구현하여 매개변수에 따라 메서드를 구별하여 호출하는 방식으로, JS에는 지원하지 않으나 arguments 객체를 사용하여 구현할 수는 있다.

프로토타입의 교체

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

const Person = (function () {
  function Person(name) {
    this.name = name;
  }
  // 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
  Person.prototype = {
    // constructor: Person,
    sayHello() {
      console.log(`Hi! My name is ${this.name}`);
    }
  }
  
  return Person;
}());

const me = new Person("Lee");

이 때 교체한 객체 리터럴에는 constructor 프로퍼티가 없기 때문에 생성자 함수와 프로토타입의 연결이 파괴된다. constructor 프로퍼티는 생성자 함수가 평가되어 프로토타입이 생성되는 시점에 암묵적으로 추가된 프로퍼티이기 때문이다.
따라서, constructor: Person을 객체 리터럴에 추가하여 프로토타입의 constructor 프로퍼티를 되살려야 한다.

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

function Person(name) {
    this.name = name;
  }
const me = new Person("Lee");

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

// Person.prototype = parent;
Object.setPrototypeOf(me, parent);

__proto__ 접근자 프로퍼티나 Object.setPrototypeOf 메서드를 통해 이미 생성된 객체의 프로토타입을 교체
역시 constructor 프로퍼티가 없기 때문에 교체할 객체 리터럴에 constructor 프로퍼티를 추가하고 생성자 함수의 prototype 프로퍼티를 재설정까지 마쳐야 연결이 되살아난다.

instanceof 연산자

객체 instanceof 생성자 함수
-> 생성자 함수의 prototype에 바인딩된 객체가 프로토타입 체인 상에 존재하면 true

직접 상속

Object.create에 의한 직접 상속

Object.create(prototype[, propertiesObject])

지정된 프로토타입 및 프로퍼티를 갖는 새로운 객체를 생성하여 반환한다. 프로토타입을 지정하지 않을 시 생성된 객체는 프로토타입 객체의 종점에 위치한다.

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

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

const myProto = { x: 10 };

const obj = {
  y: 20,
  __proto__: myProto
};

/*
const obj = Object.create(myProto, {
  y: { value: 20, writable: true, enumerable: true, configurable: true }
});
*/

정적 프로퍼티/메서드

인스턴스를 생성하지 않아도 참조 및 호출할 수 있는 프로퍼티/메서드
-> 인스턴스의 프로토타입 체인 상에 존재하지 않기 때문에 인스턴스로 접근할 수 없다.
-> 만약 인스턴스/프로토타입 메서드 내에서 this를 사용하지 않는다면 그 메서드를 정적 메서드로 변경할 수 있다.

프로퍼티 존재 확인

in 연산자

key in object
해당 객체(정확히 말하면, 객체 내가 아닌 객체가 속한 프로토타입 체인) 내에서 특정 프로퍼티가 존재하는지 확인 ( = Reflect.has(object, key) )

Object.prototype.hasOwnProperty 메서드

in 연산자와 달리 프로퍼티 키가 객체 고유의 프로퍼티 키인 경우 true룰 반환

프로퍼티 열거

for ... in

순회 대상 객체의 프로퍼티뿐만 아니라 상속받은 프로토타입의 프로퍼티까지 열거한다. 단, [[Enumberable]] 값이 false이거나 키가 심벌인 프로퍼티는 열거하지 않는다.

Object.keys/values/entries 메서드

객체 고유 프로퍼티만 열거하고 싶으면 Object.keys/values/entries 메서드를 사용한다.

profile
꾸준함을 잃지 말자.

0개의 댓글