JavaScript/모던 자바스크립트 딥다이브
[JS] 프로토타입
김춘삼씨의 고양이
2024. 4. 21. 19:17
📌 객체지향 프로그래밍
- 자바스크립트는 프로토타입 기반의 객체지향 프로그래밍 언어이며 자바스크립트를 이르고 있는 거의 모든 것이 객체임 (함수, 배열, 정규 표현식 등)
- 객체지향 프로그래밍: 여러 개의 독립적 단위(객체의 집합)로 프로그램을 표현하려는 프로그래밍 패러다임
- 추상화: 다양한 속성 중에서 프로그램에 필요한 속성만 간추려 내어 표현하는 것
- 객체:
- 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료구조
- 상태 데이터(프로퍼티)와 동작(메서드)을 하나의 논리적인 단위로 묶은 복합적인 자료구조
📌 상속과 프로토타입
- 상속(Inheritance): 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것
- 자바스크립트는 프로토타입을 기압으로 상속을 구현함
- 생성자 함수가 생성할 모든 인스턴스가 공통적으로 사용할 프로퍼티나 메서드를 프로토타입에 미리 구현해 두면 생성자 함수가 생성할 모든 인스턴스는 별도의 구현 없이 상위(부모) 객체인 프로토타입의 자산을 공유하여 사용할 수 있음 -> 코드 재사용에 유용함
📌 프로토타입 객체
- 프로토타입은 어떤 객체의 상위(부모) 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티(메서드 포함)를 제공함
- 프로토타입을 상속받은 하위(자식) 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용할 수 있음
__proto__ 접근자 프로퍼티
- 모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입, 즉 [[Prototype]] 내부 슬롯에 간접적으로 접근할 수 있음
- __proto__는 접근자 함수를 통해 프로토타입을 취득(get)하거나 할당(set)함
- 모든 객체는 상속을 통해 Object.prototype.__proto__ 접근자 프로퍼티를 사용할 수 있음
- 프로토타입에 접근하기 위해 접근자 프로퍼티를 사용하는 이유는 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위함임 (무조건적으로 프로토타입을 교체할 수 없도록 __proto__ 접근자 프로퍼티를 통해 프로토타입에 접근하고 교체하도록 구현됨)
- 모든 객체가 __proto__ 접근자 프로퍼티를 사용할 수 있는 것은 아니기 때문에(직접 상속으로 Object.prototype을 상속받지 않는 객체를 생성할 수 있음) 프로토타입의 참조를 취득하고 싶은 경우에는 Object.getPrototypeOf 메서드, 프로토타입을 교체하고 싶은 경우에는 Object.setPrototypeOf 메서드를 사용하는 편이 좋음
함수 객체의 prototype 프로퍼티
- 함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킴
- 모든 객체가 가지고 있는(Object.prototype으로부터 상속받은) __proto__ 접근자 프로퍼티와 함수 객체만이 가지고 있는 prototype 프로퍼티는 동일한 프로토타입을 가리키지만 프로퍼티를 사용하는 주체가 다름 (__proto__ 접근자 프로퍼티: 모든 객체, prototype 프로퍼티: 생성자 함수)
- 생성자 함수에 의해 생성된 인스턴스는 프로토타입의 contructor 프로퍼티에 의해 생성자 함수와 연결됨
📌 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입
- 리터럴 표기법에 의한 객체 생성 방식과 같이 명시적으로 new 연산자와 함께 생성자 함수를 호출하여 인스턴스를 생성하지 않는 객체를 만들 수 있음
- 리터럴 표기법에 의해 생성된 객체의 경우 프로토타입의 contructor 프로퍼티가 가리키는 생성자 함수가 반드시 객체를 생성한 생성자 함수라고 단정할 수는 없음
- 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재하기 때문에 리터럴 표기법에 의해 생성된 객체도 상속을 위해 프로토타입이 필요함 -> 가상적인 생성자 함수를 가짐
📌 프로토타입의 생성 시점
- 프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성됨
- 사용자 정의 생성자 함수
- contructor(생성자 함수로서 호출할 수 있는 함수)는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성됨
- non-constructor(생성자 함수로서 호출할 수 없는 함수)는 프로토타입이 생성되지 않음
- 생성된 프로토타입의 프로토타입은 언제나 Object.prototype임
- 빌트인 생성자 함수
- 모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성됨
- 객체가 생성되기 이전에 생성자 함수와 프로토타입은 이미 객체화되어 존재함
- 이후 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면 프로토타입은 생성된 객체의 [[Prototype]] 내부 슬롯에 할당됨
📌 객체 생성 방식과 프로토타입의 결정
추상 연산 OrdinaryObjectCreate
- 모든 객체는 추상 연산 OrdinaryObjectCreate에 의해 생성됨
- 필수적으로 자신이 생성할 객체의 프로토타입을 인수로 전달받음 -> 프로토타입은 추상 연산 OrdinaryObjectCreate에 전달되는 인수에 의해 결정됨
객체 생성 방식
- 객체 리터럴
- 객체 리터럴에 의해 생성되는 객체의 프로토타입은 Object.prototype임
- 객체 리터럴 내부에 프로퍼티를 추가함
- Object 생성자 함수
- Object 생성자 함수에 의해 생성되는 객체의 프로토타입은 Object.prototype임
- 먼저 빈 객체를 생성한 이후 프로퍼티를 추가해야 함
- 생성자 함수
- 생성자 함수에 의해 생성되는 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체임
- 프로토타입에도 프로퍼티를 추가/삭제할 수 있으며 이렇게 추가/삭제된 프로퍼티는 프로토타입 체인에 즉각 반영됨
- Object.create 메서드
- 클래스(ES6)
📌 프로토타입 체인
- 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없으면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로토타입을 순차적으로 검색하는 것
- 프로토타입 체인: 객체지향 프로그래밍의 상속을 구현과 프로터피 검색을 위한 메커니즘
- 스코프 체인: 식별자 검색을 위한 메커니즘
📌 오버라이딩과 프로퍼티 섀도잉
- 오버라이딩: 상위클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식
- 오버로딩: 함수의 이름은 동일하지만 매개변수의 타입 또는 개수가 다른 메서드를 구현하고 매개변수에 의해 메서드를 구별하여 호출하는 방식 (자바스크립트는 오버로딩 지원X)
- 프로퍼티 섀도잉: 상속관계에 의해 프로퍼티가 가려지는 현상
- 프로토타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스에 추가하면 프로퍼티를 인스턴스 프로퍼티로 추가하며 기존 메서드를 오버라이딩함
- 프로토타입 프로퍼티를 변경 또는 삭제하려면 하위 객체를 통해 프로토타입 체인으로 접근하는 것이 아니라 프로토타입에 직접 접근해야 함
📌 프로토타입의 교체
- 프로토타입은 생성자 함수 또는 인그턴스에 의해 교체할 수 있음
- 프로토타입 교체를 통해 객체 간의 상속관계를 동적으로 변경하는 것은 꽤 번거롭기 때문에 프로토타입을 직접 교체하지 않는 것이 좋음
생성자 함수에 의한 프로토타입의 교체
- 프로토타입을 교체하면 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴됨
- 프로토타입으로 교체한 객체 리터럴에 constructor 프로퍼티를 추가하여 프로토타입의 constructor 프로퍼티를 되살릴 수 있음
인스턴스에 의한 프로토타입의 교체
- 인스턴스의 __proto__접근자 프로퍼티를 통해 프로토타입을 교체할 수 있음
- 프로토타입으로 교체한 객체에는 constructor 프로퍼티가 없으므로 contructor 프로퍼티와 생성자 함수 간의 연결이 파괴됨
📌 instanceof 연산자
- 우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true, 아니면 fasle로 평가함
- instanceof 연산자는 프로토타입의 constructor 프로퍼티가 가리키눈 생성자 함수를 찾는 것이 아니라 생성자 함수의 prototype에 바인딩된 객체가 프로토타입 체인 상에 존재하는지 확인함
📌 직접 상속
Object.create에 의한 직접 상속
- Object.create 메서드는 명시적으로 프로토타입울 지정하여 새로운 객체를 생성함
- Object.create 메서드의 첫번째 매개변수에는 생성할 객체의 프로토타입으로 지정할 객체를, 두번째 매개변수에는 생성할 객체의 프로퍼티 키와 프로퍼티 디스크럽터 객체로 이루어진 객체를 전달함
- Object.create 메서드의 장점:
- new 연산자가 없어도 객체를 생성할 수 있음
- 프로토타입을 지정하면서 객체를 생성할 수 있음
- 객체 리터럴에 의해 생성된 객체도 상속받을 수 있음
- 프로토타입이 null인 객체가 있을 수 있기 때문에 Object.prototype의 빌트인 메서드는 간접적으로 호출하는 것이 좋음
객체 리터럴 내부에서 __proto__에 의한 직접 상속
- ES6에서는 객체 리터럴 내부에서 __proto__ 접근자 프로퍼티를 사용해 직접 상속을 구현할 수 있음
📌 정적 프로퍼티/메서드
- 정적 프로퍼티/메서드: 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드
- 정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조/호출할 수 없음
- Object.prototype.hasOwnProperty 메서드는 모든 객체의 프로토타입 체인의 종점인 Object.prototype의 메서드이므로 모든 객체가 호출할 수 있음
- 인스턴스/프로토타입 메서드 내에서 this를 사용하지 않는다면 그 메서드는 정적 메서드로 변경할 수 있음
- 정적 메서드는 인스턴스를 생성하지 않아도 호출할 수 있음
📌 프로퍼티 존재 확인
in 연산자
- 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인함
- 확인 대상 객체의 프로퍼티 뿐 아니라 확인 대상 객체가 상속받은 모든 프로퍼티를 확인함
- Reflect.has 메서드를 사용해도 동일하게 동작함
Object.prototype.hasOwnProperty
- 객체에 특정 프로퍼티가 존재하는지 확인할 수 있음
- 인수로 전달받은 프로퍼티 키가 객체 고유의 프로퍼티 키인 경우에만 true를 반환하고 상속받은 프로토타입의 프로퍼티 키인 경우 false를 반환함
📌 프로퍼티 열거
for ... in 문
- 객체의 모든 프로퍼티를 순회하며 열거함
- 객체의 프로퍼티 개수만큼 순회하며 for ... in 문의 변수 선언문에서 선언한 변수에 프로퍼티 키를 할당함
- 순회 대상 객체의 프로퍼티뿐만 아니라 상속받은 프로토타입의 프로퍼티까지 열거함
- for ... in 문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 어트리뷰트 [[Enumable]]의 값이 true인 프로퍼티를 순회하며 열거함
- 프로퍼티 키가 심벌인 프로퍼티는 열거하지 않음
- 객체 자신의 프로퍼티만 열거하려면 Object.prototype.haseOwnProperty 메서드를 사용해 객체 자신의 프로퍼티인지 확인함
- 배열에는 일반적인 for문이나 for ... of 문 또는 Array.prototype.foreach 메서드를 사용하기를 권장함
Object.keys / values / entries 메서드
- Object.keys: 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환하는 메서드
- Object.values: 객체 자신의 열거 가능한 프로퍼티 값을 배열로 반환하는 메서드
- Object.entries: 객체 자신의 열거 가능한 프로퍼티 키와 값의 쌍의 배열을 배열에 담아 반환하는 메서드
참고문헌 및 출처 : 모던 자바스크립트 Deep Dive (이웅모)