모던 자바스크립트 Deep Dive 정리
1. 객체지향 프로그래밍
2. 상속과 프로토타입
3. 프로토타입 객체
▶ 프로토타입을 상속받은 하위 객체 -> 상위 객체의 프로퍼티 사용 가능
▶ 생성자 함수에 의해 생성된 객체의 프로토타입 -> 생성자함수.prototype 형태로 바인딩된 객체
▶ 모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가짐
▷ __proto__ 접근자 프로퍼티를 통해 프로토타입에 접근 가능
3-1. __proto__ 접근자 프로퍼티
▶ 모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입([[Prototype]]) 에 접근 가능
▶ 접근자 프로퍼티 : getter, setter 함수로 구성
const obj = {};
const parent = { x: 1 };
// 1. __proto__ 접근자 프로퍼티를 통해 프로토타입에 접근 ->
// getter 함수인 get __proto__가 호출되어 obj 객체의 프로토타입을 취득
obj.__proto__;
// 2. 새로운 프로토타입을 할당 ->
// setter함수인 set __proto__가 호출되어 obj 객체의 프로토타입을 교체
obj.__proto__ = parent;
console.log(obj.x); // 1
__proto__ 접근자 프로퍼티는 상속을 통해 사용된다
▶ __proto__ 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니고
▷ Object.prototype의 프로퍼티임
▷ 즉, 모든 객체는 상속을 통해 Object.prototype.__proto__ 를 사용할 수 있음
Object.prototype
▶ 모든 객체는 프로토타입의 계층 구조인 프로토타입 체인에 묶여 있다 (스코프 체인 처럼)
▶ JS 엔진은 객체의 프로퍼티(메서드 포함)에 접근하려고 할때,
▷ 해당 객체에 접근하려는 프로퍼티가 없다면 __proto__ 가 가리키는 참조를 따라 거슬러 올라감
▷ 이때 프로토타입 체인의 최상위 객체는 Object.prototype 이며,
▷ 이 객체의 프로퍼티와 메서드는 모든 객체에 상속됨
__proto__ 접근자 프로퍼티로만 프로토타입에 접근하는 이유
▶ 프로토타입 체인은 일방통행의 구조를 가져야 함
▷ 순환참조하여 프로토타입 체인에서 프로퍼티 참조할 때 무한 루프가 만들어질 수 있음
__proto__ 접근자를 코드 내에서 직접 사용하는 것은 권장되지 않음
▶ 버전 이슈 또는 직접 상속을 통해 Object.prototype을 상속받지 않는 객체가 발생할 수 있음
__proto__ 대신
▶ 프로토타입의 참조를 취득할 때 : Object.getPrototypeof()
▶ 프로토타입을 할당(교체)할 때 : Object.setPrototypeof()
// 3-1. 과 같은 역할의 코드
const obj = {};
const parent = { x: 1 };
// obj 객체의 프로토타입을 취득
Object.getPrototypeOf(obj); // obj.__proto__;
// obj 객체의 프로토타입을 교체
Object.setPrototypeOf(obj, parent); // obj.__proto__ = parent;
console.log(obj.x); // 1
3-2. 함수 객체의 prototype 프로퍼티
▶ prototype 프로퍼티 : 함수 객체만이 소유, 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킴
▷ 화살표 함수와 ES6 메서드 축약 표현으로 정의한 메서드는 prototype 프로퍼티 생성 X (프로토타입도 없음)
▶ 모든 객체가 가진 (Object.prototype 에서 상속받은) __proto__ 접근자 프로퍼티와 동일한 프로토타입을 가리킴
3-3. 프로토타입의 constructor 프로퍼티와 생성자 함수
▶ 모든 프로토타입은 constructor 프로퍼티(생성자 함수 가리킴)를 갖는다
▷ 예시
// 생성자 함수
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// me 객체의 생성자 함수는 Person이다.
console.log(me.constructor === Person); // true
// me 객체의 프로토타입 체인 상위에 있는 Object.prototype에 constructor 프로퍼티가 있음
4. 리터럴 표기법으로 만든 객체의 생성자 함수와 프로토타입
▶ 객체 리터럴 방식으로 만든 객체도 생성자 함수는 Object 생성자 함수이다
▷ 예시
// obj 객체는 Object 생성자 함수로 생성한 객체가 아니라 객체 리터럴로 생성했다.
const obj = {};
// 하지만 obj 객체의 생성자 함수는 Object 생성자 함수다.
console.log(obj.constructor === Object); // true
▶ 하지만 new.target 등 세부내용은 엄연히 다르다
▷ 함수 리터럴 방식과 Function 생성자 함수 방식 함수가 생성과정, 스코프, 클로저 등 차이가 있는 것과 동일
★ 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 항상 맞물려 존재한다
▶ 리터럴 표기법
리터럴 표기법 | 생성자 함수 | 프로토타입 |
객체 리터럴 | Object | Object.prototype |
함수 리터럴 | Function | Function.prototype |
배열 리터럴 | Array | Array.prototype |
정규표현식 리터럴 | RegExp | RegExp.prototype |
5. 프로토타입의 생성 시점
▶ 프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성된다
▶ 생성자 함수는 1) 사용자 정의 생성자 함수, 2) 빌트인 생성자 함수 2가지 종류가 있음
5-1. 사용자 정의 생성자 함수와 프로토타입 생성 시점
▶ 생성자 함수로 호출할 수 있는 함수(constructor)는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입 생성
▷ 함수 선언문은 런타임 이전에 먼저 먼저 실행되기 때문에 (함수 호이스팅) 프로토타입도 런타임 이전에 생김
5-2. 빌트인 생성자 함수와 프로토타입 생성 시점
▶ Object, String, Number, Function, Array 등
▷ 마찬가지로 빌트인 생성자 함수가 생성되는 시점에 프로토타입 생성
★ 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면, 프로토타입은 생성된 객체의 [[prototype]] 내부 슬롯에 할당되고, 생성된 객체는 프로토타입을 상속받음
6. 객체 생성 방식과 프로토타입의 결정
6-1. 객체 리터럴 방식에 의해 생성된 객체의 프로토타입
6-2. Object 생성자 함수에 의해 생성된 객체의 프로토타입
▶ 객체 리터럴, Object 생성자 함수 방식 객체의 프로토타입은 다양한 메서드를 보유
▷ constructor, hasOwnProperty 등등
6-3. 생성자 함수에 의해 생성된 객체의 프로토타입
▶ 생성자 함수에 의해 생성된 객체(인스턴스)의 프로토타입은 constructor 메서드만 보유
▷ 하지만 프로토타입 체인을 거슬러 올라가 Object.prototype을 상속받아 사용할 수 있다
▷ 생성자 함수로 만들어지는 객체는 인스턴스이므로, Object.prototype에 프로퍼티를 추가하여 인스턴스들이 상속받도록 할 수 있다
▷ 상속 예시
function Person(name) {
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};
const me = new Person('Lee');
const you = new Person('Kim');
me.sayHello(); // Hi! My name is Lee
you.sayHello(); // Hi! My name is Kim
// Person 생성자 함수를 통해 생성된 모든 객체는 프로토타입에 추가된 sayHello 메서드를 상속받아
// 자신의 메서드처럼 사용할 수 있다
7. 프로토타입 체인
▶ 생성자 함수로 생성한 객체(인스턴스)의 프로토타입 : 인스턴스.prototype
▷ 하지만 Object.prototype 상속 가능
▶ 프로토타입 체인
▷ 상위 객체의 프로토타입의 프로퍼티를 순차적으로 검색
▷ 자바스크립트에서 상속을 구현하는 메커니즘
▷ 최상위에는 Object.prototype 존재
▶ call 메서드
▷ this로 사용할 객체를 전달하면서 함수 호출
▷ call 메서드 예시
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
Object.prototype.hasOwnProperty.call(me, 'name');
// this로 사용할 me 객체를 전달하면서 상위 객체인 Object.prototype의 hasOwnProperty 메서드 호출
▶ 프로퍼티가 아닌 식별자는 스코프 체인을 따름
8. 오버라이딩과 프로퍼티 새도잉
▶ 오버라이딩
▷ 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의(덮어쓰기)하여 사용
▶ 오버로딩
▷ 함수 이름은 동일하지만, 매개변수의 타입 또는 갯수가 다른 메서드를 구현하고, 매개변수에 의해 메서드를 구별하여 호출하는 방식
▶ 프로퍼티 새도잉
▷ 오버라이딩이 발생하여 프로토타입 프로퍼티가 가려지는 현상
▷ 예시
const Person = (function () {
// 생성자 함수
function Person(name) {
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};
// 생성자 함수를 반환
return Person;
}());
const me = new Person('Lee');
// 인스턴스 메서드
me.sayHello = function () {
console.log(`Hey! My name is ${this.name}`);
};
// 인스턴스 메서드가 호출된다. 프로토타입 메서드는 인스턴스 메서드에 의해 가려진다.
me.sayHello(); // Hey! My name is Lee
▶ 하위 객체를 통해 상위 객체의 프로퍼티를 변경 또는 삭제하는 것은 불가능
▷ 직접 prototype에 접근하면 변경이나 삭제 가능
▷ 예시
// 프로토타입 메서드 변경 (인스턴스 메서드 없는 상황)
Person.prototype.sayHello = function () {
console.log(`Hey! My name is ${this.name}`);
};
me.sayHello(); // Hey! My name is Lee
// 프로토타입 메서드 삭제
delete Person.prototype.sayHello;
me.sayHello(); // TypeError: me.sayHello is not a function
9. 프로토타입의 교체
9-1. 생성자 함수에 의한 프로토타입의 교체
▶ 생성자 함수의 프로토타입을 객체 리터럴로 교체할 수 있다
▷ 하지만, 객체 리터럴은 생성자 함수가 없기 때문에(constructor 프로퍼티 부재) 인스턴스에서 생성자 함수의 프로퍼티를 참조할 수 없어진다 (건너뛰어 Object.prototype 으로 올라감)
▷ 이 때, 객체 리터럴에 constructor 프로퍼티를 추가하고 생성자 함수를 value로 넣어주면, 연결이 끊어지지 않는다
9-2. 인스턴스에 의한 프로토타입의 교체
▶ 생성자 함수 이외의 객체에서는 (인스턴스) __proto__ 를 통해 프로토타입에 접근한다
▷ __proto__ 를 대신 조작하지 않고, Object.setPrototypeOf(프로토타입을 변경할 객체, 할당할 객체(리터럴)) 메서드 를 통해 교체할 수 있다
★ 프로토타입을 직접 교체하지 않는 것이 권장된다 (대신 클래스 또는 직접 상속을 이용)
10. instanceof 연산자
▶ 객체 instanceof 생성자 함수
▷ 우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체 프로토타입 체인 상에 존재하면 true
▷ 우변의 피연산자가 함수가 아닌 경우 TypeError 리턴
11. 직접 상속
11-1. Object.create에 의한 직접 상속
▶ Object.create(param1, param2)
▷ param1 : 생성할 객체의 프로토타입으로 지정할 객체
▷ param2 : 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터 객체로 이뤄진 객체
▷ return : 생성된 객체
▷ 프로토타입 체인의 종점에 위치하는 객체를 인위적으로 만들 수 있음
★ 이러한 이유로 Object.hasOwnProperty 같은 Object.prototype의 빌트인 메서드를 객체에 직접 사용하는 것은 권장되지 않음
11-2. 객체 리터럴 내부에서 __proto__에 의한 직접 상속
▶ ES6에서는 객체 내부에서 __proto__ 접근자 프로퍼티를 사용하여 직접 상속을 구현할 수 있음
▷ __proto__ 프로퍼티를 추가하고, 상위 객체(현재 객체가 상속받을)를 밸류값으로 넣어준다
12. 정적(static) 프로퍼티/메서드
▶ 인스턴스를 생성하지 않고도 참조/호출할 수 있는 프로퍼티/메서드
▷ 생성자 함수로 참조/호출
▷ 예시
// 생성자 함수
function Person(name) {
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};
// 정적 프로퍼티
Person.staticProp = 'static prop';
// 정적 메서드
Person.staticMethod = function () {
console.log('staticMethod');
};
const me = new Person('Lee');
// 생성자 함수에 추가한 정적 프로퍼티/메서드는 생성자 함수로 참조/호출한다.
Person.staticMethod(); // staticMethod
// 정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조/호출할 수 없다.
// 인스턴스로 참조/호출할 수 있는 프로퍼티/메서드는 프로토타입 체인 상에 존재해야 한다.
me.staticMethod(); // TypeError: me.staticMethod is not a function
▷ Object.create()가 정적 메서드에 해당 -> Object.create() 처럼 사용
▷ 반면 Object.hasOwnProperty() 는 프로퍼티 메서드 -> Object.prototype.hasOwnProperty() 처럼 사용
▷ MDN에서 이처럼 정적 메서드와 프로퍼티 메서드를 구분 (prototype 대신 #을 써서 간소화하기도 함)
★ 메서드 내에서 this를 사용하지 않으면 (인스턴스를 참조하지 않으면) 정적 메서드로 변환 가능
13. 프로퍼티 존재 확인
13-1. in 연산자
▶ 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인
▷ 객체의 프로토타입 체인까지 거슬러 올라가 프로퍼티를 검색
▷ ES6의 Reflect.has(객체, '프로퍼티') 가 동일하게 동작
13-2. Object.prototype.hasOwnProperty 메서드
▶ in 연산자와 동일하게 동작하지만
▷ 객체 고유의 프로퍼티인 경우에만 true (상속받은 프로퍼티이면 false) 반환
14. 프로퍼티 열거
14-1. for ... in 문
▶ 객체의 모든 프로퍼티(모든 프로토타입 체인 중 enumerable property만)를 순회하며 열거
▷ 데이터 프로퍼티 중 enumerable이 true인 것만 열거함
▷ 따라서 toString 같은 Object.prototype 프로퍼티는 열거되지 않음
▷ 정확한 순서가 보장되지않음
14-2. Object.keys/values/entries 메서드
▶ 객체 자신의 고유하고 열거 가능한 프로퍼티를 열거할 때 사용
▷ keys : 프로퍼티 키를 배열로 반환
▷ values : 프로퍼티 값을 배열로 반환
▷ entries : 프로퍼티 키와 값의 쌍의 배열을 배열에 담아(2차원 배열) 반환