19.0
- JS는 명령형, 함수형, 프로토타입 기반 객체지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어
- JS는 클래스 기반 객체지향 프로그래밍 언어보다 효율적이고 더 강력한 프로토타입 기반의 객체지향 프로그래밍 언어
- 객체 기반의 프로그래밍 언어로, 원시 타입의 값을 제외한 나머지 ‘모든 것’이 객체임
19.1 객체지향 프로그래밍
여러 개의 독립적 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임
- 실세계의 실체를 인식하는 철학적 사고 (속성을 통해 실체를 인식하거나 구별하는 것) 를 프로그래밍에 접목하려는 시도에서 시작됨
- 추상화 : 다양한 속성 중에서 프로그램에 필요한 속성만 간추려 내어 표현하는 것
- 객체지향 프로그래밍은 객체의 상태를 나타내는 데이터와 상태 데이터를 조작할 수 있는 동작을 하나의 논리적인 단위로 묶어서 구별함
- 객체는 고유의 기능을 갖는 독립적인 부품이지만, 기능을 수행하면서 다른 객체와 관계성을 가질 수 있음 (상속, 메세지 주고받는 등)
- 객체는 (상태 데이터 + 동작) 하나의 논리적인 단위로 묶은 복합적인 자료구조
19.2 상속과 프로토타입
상속은 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것
- 동일한 생성자 함수에 의해 생성된 모든 인스턴스가 동일한 메서드를 중복 소유하면 메모리 낭비
- 상속을 기반으로 상속을 구현하여 기존의 코드를 재사용하여 불필요한 중복 제거
- 생성자가 생성한 모든 인스턴스가 동일한 메서드를 공유해서 사용할 수 있도록 프로토타입을 생성자 함수 프로토타입 프로퍼티에 바인딩시킴
function Circle(radius) {
this.radius = radius;
}
Cricle.prototype.getArea = function() {
return Math.PI * this.radius ** 2;
};
- 이때 Circle 생성자 함수가 생성하는 모든 인스턴스는 하나의 getArea 메서드를 공유함
- 자신의 상태를 나타내는 radius 프로퍼티만 개별적으로 소유하고 메서드는 상속을 통해 공유하여 사용
💡 모든 인스턴스가 공통적으로 사용할 프로퍼티나 메서드를 프로토타입에 미리 구현해 두면 생성자 함수가 생성할 모든 인스턴스는 별도의 구현없이 부모 객체인 프로토타입의 자산을 공유하여 사용할 수 있다.
19.3 프로토타입 객체
프로토타입 객체는 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용된다.
- 프로토타입 : 어떤 객체의 부모 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티(메서드 포함)을 제공함
- 모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가지고 값은 프로토타입의 참조로 이루어짐
- [[Prototype]]에 저장되는 값은 객체가 생성될 때 객체 생성 방식에 따라 결정되고 저장됨
__proto__ 접근자 프로퍼티를 통해 [[Prototype]] 내부 슬롯을 간접적으로 접근 가능
- 프로토타입은
constructor 프로퍼티를 통해 생성자 함수에 접근 가능
- 생성자 함수는
property 프로퍼티를 통해 프로토타입에 접근 가능
생성자 함수 (prototype) → ←생성자 함수.prototype ← 객체
__proto__ 접근자 프로퍼티
모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입인 내부 슬롯에 간접적으로 접근할 수 있다.
- 내부 슬롯은 프로퍼티가 아니기 때문에 간접적인 접근만 가능
__proto__ 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아닌 Object.prototype의 프로퍼티이므로 상속을 통해 사용 가능
- 접근자 프로퍼티를 사용해 프로토타입에 접근하는 이유는 상호 참조에 의한 순환 참조하는 프로토타입 체인 생성을 방지하기 위해서
- 프로토타입 체인은 무조건 단방향 링크드 리스트로 구현되야 함!!
- 순환 참조하는 프로토타입 체인은 프로퍼티 검색시 체인 종점이 없어 무한 루프에 빠지게됨
__proto__ 를 코드 내에서 직접 사용하는 것은 비추
- 모든 객체가 접근자 프로퍼티를 사용할 수 있는 것이 아님
- 접근자 프로퍼티 대신 프로토타입의 참조 할 경우
Object.getPrototypeOf() 메서드 사용
- 프로토타입 교체시
Object.setPrototypeOf() 메서드 사용
함수 객체의 prototype 프로퍼티
함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.
- non-constructor 함수인 화살표 함수와 메서드 축약 표현으로 정의한 메서드는 prototype 프로퍼티를 소유하지 않고, 생성하지도 않음
- 함수 객체는 prototype 프로퍼티 소유함
- 일반 객체는 prototype 프로퍼티 소유하지않음
- 모든 객체가 가지고 있는
__proto__ 접근자 프로퍼티와 함수 객체만이 가지고 있는 prototype 프로퍼티는 동일한 프로토타입 가리킴
프로토타입의 constructor 프로퍼티와 생성자 함수
- 모든 프로토타입은 constructor 프로퍼티를 가짐
- constructor 프로퍼티는 자신을 참조하고 있는 생성자 함수를 가리킴
- 인스턴스 객체는 프로토타입의 constructor 프로퍼티 상속받아 사용 가능
19.4 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입
- 명시적으로 new 연산자와 함께 생성자 함수를 호출하여 인스턴스를 생성하지 않는 객체 생성 방식
- 리터럴 표기법에 의해 생성된 객체도 프로토타입이 존재하지만 constructor 프로퍼티가 가리키는 생성자 함수가 객체를 생성한 생성자 함수가 아닐 수도 있음
- Object 생성자 함수에 인수를 전달하지 않거나 undefiend, null이 인수일 경우 내부적으로 추상 연산(OrdinanryObjectCreate)을 호출하여 Object.prototype을 프로토타입으로 갖는 빈 객체 생성
- Object 생성자 함수 호출과 객체 리터럴의 평가는 추상 연산을 호출하여 빈 객체를 생성하는 것은 동일하지만 세부 내용은 다름
- Object 생성자 함수가 생성한 객체 ≠ 객체 리터럴에 의해 생성된 객체
- 하지만 리터럴 표기법에 의해 생성된 객체도 상속을 위한 프로토타입이 필요하므로 가상의 생성자 함수를 가짐
💡 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재함
19.5 프로토타입의 생성 시점
- 모든 객체는 생성자 함수와 연결되어 있음
- 프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성됨 (프로토타입-생성자함수 한 쌍)
- 사용자 정의 생성자 함수와 빌트인 생성자 함수 구분으로 프로토타입 생성 시점을 확인할 수 있음
사용자 정의 생성자 함수와 프로토타입 생성 시점
생성자 함수로서 호출할 수 있는 함수인 constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성됨
- non-constructor 함수는 프로토타입이 생성되지 않음
- 사용자 정의 생성자 함수는 자신이 평가되어 함수 객체로 생성되는 시점에 프로토타입도 동시에 생성됨
- 프로토타입은 Object.prototype
빌트인 생성자 함수와 프로토타입 생성 시점
- 빌트인 생성자 함수가 생성되는 전역 객체가 생성되는 시점에 프로토타입이 생성됨
- 프로토타입은 빌트인 생성자 함수의 prototype 프로퍼티에 바인딩 됨
- 객체가 생성되기 전에 생성자 함수와 프로토타입은 이미 객체화되어 존재
- 이후 함수 또는 리터럴 표기법으로 객체 생성시 생성된 객체의 프로퍼티 내부 슬롯에 할당되며 프로토타입을 상속받음
19.6 객체 생성 방식과 프로토타입의 결정
- 다양한 방식으로 생성된 객체는 모두 추상 연산에 의해 생성됨
- 추상 연산은 필수적으로 자신이 생성할 객체의 프로토타입을 인수로 받음
- 프로토타입은 추상 연산에 전달되는 인수에 의해 결정됨
객체 리터럴에 의해 생성된 객체의 프로토타입
- 추상 연산에 전달되는 프로토타입은 Object.prototype
- 객체 리터럴이 평가되면 추상 연산에 의해 Object 생성자 함수와 Object.prototype과 생성된 객체 사이에 연결이 만들어짐
- 프로토타입을 상속받으면 상속받은 프로토타입의 프로퍼티와 메서드를 자유롭게 사용 가능
Object 생성자 함수에 의해 생성된 객체의 프로토타입
- 추상 연산에 전달되는 프로토타입은 Object.prototype
- 객체 리터럴에 의해 생성된 객체와 동일한 구조를 가짐
- 객체 리터럴과 생성자 함수에 의한 객체 생성 방식의 차이는 프로퍼티를 추가하는 방식에 있음
- Object 생성자 함수는 빈 객체 생성후 프로퍼티 추가
- 객체 리터럴은 객체 리터럴 내부에 프로퍼티를 추가
생성자 함수에 의해 생성된 객체의 프로토타입
- 추상 연산에 전달되는 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체
- 생성자 함수와 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체와 생성된 객체 사이에 연결이 생성됨
- 프로토타입의 프로퍼티는 constructor 뿐임
- 프로토타입도 객체이므로 프로퍼티를 추가/삭제할 수 있으며 프로토타입 체인에 즉시 반영됨
프로토타입 체인
객체의 프로퍼티에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색하는 것
- 프로토타입 체인은 자바스크립트가 객체지향 프로그래밍의 상속을 구현하는 매커니즘
- call 메서드 : this로 사용할 객체를 전달하면서 함수를 호출하는 것
- 프로토타입 체인의 최상위의 객체는 항상 Object.prototype이므로 모든 객체는 이를 상속받음
- Object.prototype은 프로토타입 체인의 종점으로, 종점에서도 프로퍼티를 검색할 수 없는 경우 undefined 반환
- Object.prototype의 프로토타입인 [[Prototype]] 내부 슬롯은 null
- 프로퍼티가 아닌 식별자는 스코프 체인에서 검색
💡 스코프 체인과 프로토타입 체인은 서로 협력하여 식별자와 프로퍼티를 검색하는데 사용됨
19.8 오버라이딩과 프로퍼티 섀도잉
- 프로토타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스에 추가하면 프로토타입 체인을 따라 프로토타입 프로퍼티를 검색하여 덮어쓰는 것이 아닌 인스턴스 프로퍼티로 추가됨 (오버라이딩)
- 상속 관계에 의해 프로퍼티가 가려지는 현상을 프로퍼티 섀도잉이라고 함
- 오버라이딩 : 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식
- 오버로딩 : 함수 이름은 동일하지만 매개변수의 타입 또는 개수가 다른 메서드를 구현하고 매개변수에 의해 메서드를 구별하여 호출하는 방식 (JS에선 지원하지 않음)
- 하위 객체를 통해 프로토타입의 프로퍼티를 변경/삭제는 불가능 직접 변경/삭제만 가능
19.9 프로토타입의 교체
- 부모 객체인 프로토타입을 동적으로 변경할 수 있음
- 객체 간의 상속 관계를 동적으로 변경할 수 있음
- 프로토타입은 생성자 함수 또는 인스턴스에 의해 교체할 수 있음
생성자 함수에 의한 프로토타입의 교체
- 생성자 함수의 prototype 프로퍼티를 통해 프로토 타입을 교체할 수 있음
- 프로토타입 교체시 constructor 프로퍼티와 생성자 함수 간의 연결이 끊어지기 때문에 프로토타입의 체인에 따라 Object.prototype constructor가 검색됨
- 연결을 되살리려면, 프로토타입으로 교체한 객체 리터럴에 constructor 프로퍼티를 추가하면 됨
Person.prototype = {
constructor: Person,
sayHi() {
console.log('Hi');
}
}
인스턴스에 의한 프로토타입의 교체
- 프로토타입은 인스턴스의
__proto__ 접근자 프로퍼티를 통해 접근 가능하기 때문에 교체도 가능
- 생성자 함수에 의한 교체는 미래에 생성할 인스턴스의 프로토타입을 교체하는 것
__proto__ 접근자 프로퍼티에 의한 교체는 이미 생성된 객체의 프로토타입을 교체하는 것
Object.setPrototypeOf()로 객체의 프로토타입을 교체
- 프로토타입은 직접 교체하지 않는 것이 좋음
19.10 instanceOf 연산자
- 우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true로 평가
💡 instanceOf 연산자는 생성자 함수의 prototype에 바인딩된 객체가 프로토타입 체인 상에 존재하는지 확인
19.11 직접 상속
Object.create에 의한 직접 상속
Object.create()는 명시적으로 프로토타입을 지정하여 새로운 객체를 생성
- 이 또한 추상 연산을 호출함
Object.create() 의 첫번째 매개변수는 생성할 객체의 프로토타입으로 지정할 객체를 전달
- 객체를 생성하면서 직접적으로 상속을 구현함
- new 연산자 없이도 객체 생성 가능
- 프로토타입을 지정하면서 객체 생성 가능
- 객체 리터럴에 의해 생성된 객체도 상속받을 수 있음
- 두번째 매개변수는 옵션으로, 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터 객체로 이루어진 객체를 전달함
- Object.prototype의 빌트인 메서드를 직접 호출하는 것은 체인의 종점에 객체를 생성할 수 있으므로 권장 X
- Function.prototype.call() 사용해서 직접적으로 호출하는걸 추천
객체 리터럴 내부에서 __proto__ 에 의한 직접 상속
- 객체 리터럴 내부에서
__proto__ 접근자 프로퍼티를 사용하여 직접 상속 구현 가능
const obj = {
y: 20,
__proto__: myProto
};
19.12 정적 프로퍼티/메소드
정적 프로퍼티/메소드는 생성자 함수로 인스턴스를 생성하지 않아도 프로토타입 메서드를 참조/호출할 수 있는 프로퍼티/메서드를 의미함
- 정적 프로퍼티/메소드는 생성자 함수가 생성한 인스턴스로 참조/호출할 수 없음
- 생성자 함수가 생성한 인스턴스는 자신의 프로토타입 체인에 속한 객체의 프로퍼티/메서드에 접근 가능
- 정적 프로퍼티/메소드는 인스턴스의 프로토타입 체인에 속한 객체의 프로퍼티/메소드가 아니므로 인스턴스로 접근 불가
- 인스턴스/프로토타입 메서드에서 this 생략시 → 정적 프로퍼티/메소드로 변경
- 프로토타입 프로퍼티/메서드 표기시 prototype을 #으로 표기
19.13 프로퍼티 존재 확인
in 연산자
in 연산자는 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인함
key in object
- in 연산자는 확인 대상 객체의 프로퍼티뿐만 아니라 확인 대상 객체가 상속받은 모든 프로토타입의 프로퍼티를 확인하므로 주의 !!
Object.prototype.hasOwnProperty 메서드
- 객체에 특정 프로퍼티가 존재하는지 확인하는 메서드
- 인수로 전달받은 프로퍼티 키가 객체 고유의 프로퍼티 키인 경우에만 true 반환
19.14 프로퍼티 열거
for … in 문
for (변수선언문 in 객체) {....}
- 객체의 프로퍼티 개수만큼 순회하며 변수선언문에서 선언한 변수에 프로퍼티 키를 할당함
- in 연산자처럼 순회 대상 객체의 프로퍼티뿐만 아니라 상속받은 프로토타입의 프로퍼티까지 열거함
- toString 메서드는 열거할 수 없도록 정의되어 있는 프로퍼티임
- 프로퍼티 키가 Symbol인 프로퍼티도 열거에서 제외됨
- 프로퍼티 열거시 순서가 보장되지 않음
- 배열에는 일반적인 for문이나 for .. of 문 또는 forEach 문 사용 추천
💡 for .. in 문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 어트리뷰트 [Enumerable]의 값이 true인 프로퍼티를 순회하며 열거함
Object.keys/values/entries 메서드
- for … in 문은 상속받은 프로퍼티까지 열거 → hasOwnProperty()로 고유 프로퍼티인지 확인 필요
Object.keys/values/entries 메서드는 고유 프로퍼티만 열거해줌
Object.keys/values()는 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환
Object.entries()는 객체 자신의 열거 가능한 프로퍼티 키와 값의 쌍의 배열을 배열에 담아 반환