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) => { ... })
'Books > 모던 JS Deep Dive ✔️' 카테고리의 다른 글
[모던 JS Deep Dive] 21장 - 빌트인 객체 (0) | 2021.12.30 |
---|---|
[모던 JS Deep Dive] 20장 - strict mode (0) | 2021.12.30 |
[모던 JS Deep Dive] 18장 - 함수와 일급 객체 (0) | 2021.12.22 |
[모던 JS Deep Dive] 17장 - 생성자 함수에 의한 객체 생성 (0) | 2021.12.21 |
[모던 JS Deep Dive] 16장 - 프로퍼티 어트리뷰트 (0) | 2021.12.20 |
댓글