내용이 상당히 많고 복잡해서 공부하는데 3일이나 넘게 걸렸다.
자바스크립트는 명령형 , 함수형, 프로토타입 기반, 객체지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍
이다.
개체지향 프로그래밍은 프로그램을 명령어 또는 함수의 목록으로 보는 명령형 프로그래밍
의 절차지향형 관점을 벗어나 여러 개의 독립적인 단위인 객체
의 집합으로 프로그램을 표현하려고 하는 프로그래밍 패러다임을 의미한다.
실세계의 사물이나 개념들은 특징이나 성질을 나타내는 속성 (attribute)
을 가지고 있다.
사람은 여러가지 특징과 성질을 가지고 있지만, 그 중 필요한 속성들만 가지고 객체를 생성해보자
let person = {
name: 'tom',
age: 16,
};
이처럼 다양한 속성 중 프로그램에 필요한 속성만 간추려 표현하는 것을 추상화
라고 한다.
추상화
추상화는 객체지향 프로그래밍에서 중요한 개념 중 하나다.
중요한 특징만 포착
현실 세계의 개체나 시스템에서 핵심적인 특징만 포착하고 나머지는 무시함으로서 복잡성을 간소화한다.세부 사항 숨김
불필요한 세부사항을 숨기고 필요한 부분만 노출하여 사용자가 해당 개념을 더 쉽게 이해할 수 있게 돕는다.
- 모델링
추상화는 모델을 만들어내는 과정이며, 모델은 실제 세계의 시스템을 단순화하고 표현한 것이다.
이처럼 속성을 통해 여러개의 값을 하나의 단위로 구성한 복합적인 자료구조를객체
라고 한다.
이 때 객체에는 상태를 나타내는 데이터와, 데이터를 조작할 수 있는 동작을 하나의 논리저인 단위로 묶어 사용한다.
상태를 나타내는 데이터를 property
, 동작을 method
라 부른다.
정리
객체지향 프로그래밍은 독립적인 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임이다.
객체는 객체의 상태를 나타내는 프로퍼티와 메소드로 이뤄진 복합적인 자료구조이다.
상속은 객체지향 프로그래밍의 핵심 개념이다.
어떤 객체의 프로퍼티 또는 메소드를 다른 객체가 상속받아 사용할 수 있는 것을 말한다.
function person(name) {
this.name = name;
}
를 통해 person
이라는 생성자 함수를 정의하였다.
이렇게 되면 person
이란 생성자 함수는 프로퍼티로 name
을 갖고 , 생성자 함수를 호출 할 때 받은 인자를 name
프로퍼티의 프로퍼티 값으로 설정하여 객체를 반환한다.
이것이 우리가 여태까지 배운 생성자 함수로 인스턴스를 생성하는 방법이였다.
한 발자국만 더 나아가 보자
생성자 함수는 생성될 때 사용자가 설정하지 않아도 prototype
이란 객체를 생성해둔다.
일단은 아 생성자 함수를 생성하면 내부 프로퍼티로 prototytpe
이란 객체를 같이 생성해두는구나 ? 만 알고 있어보자
function person(name) {
this.name = name;
}
let tom = new person('tom');
let jerry = new person('jerry');
이렇게 person
생성자 함수로 tom , jerry
라는 인스턴스를 만들어주었다.
이 때 모든 person
으로 생성된 인스턴스들에게 프로퍼티를 추가하거나 , 메소드를 추가하고 싶다면 어떻게 할까 ?
모두에게 자기 소개를 하는 메소드를 추가 해보자
function person(name) {
this.name = name;
this.introduce = function () {
console.log(`hi i am ${this.name}`);
};
}
let tom = new person('tom');
let jerry = new person('jerry');
tom.introduce(); // hi i am tom
이렇게 생성자 함수에 메소드를 추가해주면 인스턴스들도 메소드를 사용 할 수 있을 것이다.
하지만 이는 메모리 관리 관점에서 매우 비효율적이다.
introduce
메소드는 어떤 매개변수도 받지 않는 항상 같은 메소드인데 인스턴스가 생성 될 때 마다
메모리에 할당 되기 때문이다.
만약 인스턴스를 1억개를 만든다면 1억개의 똑같은 introduce
메소드가 메모리에 할당된다.
이를 상속
이란 개념을 통해서 해결 할 수 있다.
생성자 함수
도 함수이며, 결국 함수 또한 객체
이다.
위에서는 여태 배운 내용들을 가지고 설명하려고 하니 반복적으로 생성자 함수
라고 말을 했지만
생성자 함수
를 상위 객체, 인스턴스
를 하위 객체로 바라 볼 수 있다.
이 때 하위 객체들이 상위 객체의 어떤 속성이나 기능을 공유 할 때, 하위 객체들은 상위 객체로부터 상속
받는다고 한다.
function person(name) {
this.name = name;
}
person.prototype.introduce = function () {
console.log(`hi i am ${this.name}`);
};
let tom = new person('tom');
let jerry = new person('jerry');
tom.introduce(); // hi i am tom
person
상위 객체에서 인스턴스
들이 공유 할 프로퍼티나 메소드
를 담은 객체에 동적으로 introduce
를 할당해주었다.
이후 인스턴스
들은 상위 객체의 prototype
객체를 상속 받아 prototype
내에 존재하는 프로퍼티나 메소드
를 사용 할 수 있다.
이렇게 하게되면 인스턴스
들은 공유되는 메소드를 독립적인 메모리에 할당 할 필요 없이 상위 객체
가 지정한 prototytpe
객체가 저장된 메모리를 참조하면 되기 때문에 메모리를 불필요하게 사용 할 필요가 없다.
또한 상위 객체들도 인스턴스에게 상속 하고 싶은 프로퍼티나 메소드가 있으면 모든 인스턴스들에 대해서 설정해줄 필요 없이 prototype
객체에서 추가하거나 삭제, 수정 해주면 된다.
상속과 프로토타입 정리
생성자 함수인
상위 객체
는 선언 될 때prototype
이란 프로퍼티를 가지고 있으며prototype
프로퍼티는인스턴스
들에게 공유할프로퍼티 , 메소드
를 담고 있는 객체이다.
인스턴스
들은상위 객체
의prototype
객체를 상속 받아상위 객체
에서 지정한 프로퍼티나 메소드를 사용 할 수 있다.
위에서 프로토타입을 이용한 상속에 대해 알아보았다.
상속을 통해 인스턴스는 상위 객체에서 공유하는 프로토타입
을 사용 할 수 있다.
좀 더 예시를 통해 알아보자
function Veichle() {}
Veichle.prototype.drive = function () {
console.log('앞으로가유');
};
function car() {
this.seat = 4;
}
car.prototype = new Veichle();
car.prototype.wheel = 4;
function autobycycle() {
this.seat = 2;
}
autobycycle.prototype = new Veichle();
autobycycle.prototype.wheel = 2;
let bmw = new car();
let kawasaki = new autobycycle();
위 코드에서 가장 상위 객체은 Vehicle
이 존재하고
그 밑에 car , autobycycle
은 Vehicle
을 상속 받은 인스턴스이자 생성자 함수 (객체)이다.
생성자 함수가 다른 생성자 함수를 상속받기 위해
prototype
객체를 다른 생성자 함수 객체로 설정해주었다.
그 밑에 bmw , kawasaki
는 car , autobycycle
로 생성된 인스턴스이다.
해당 모습을 도식화 하면 다음과 같다.
다음처럼 상속된 객체들간의 관계를 프로토타입 체인
이라고 한다.
프로토타입 체인 정의
객체간 상속을 구현하는 메커니즘
프로토타입 체인을 통해 상속 받은 프로퍼티들을 사용하는 예시를 살펴보자
console.log(bmw.wheel); // 4 (autobycycle의 프로퍼티)
bmw.drive(); // 앞으로 가유 (vehicle의 프로퍼티)
console.log(kawasaki.wheel); // 2 (car의 프로퍼티)
kawasaki.drive(); // 앞으로 가유 (vehicle 의 프로퍼티)
bmw , kawasaki
객체 자체에는 wheel
이란 프로퍼티도 , drive()
라는 메소드도 존재하지 않는다.
하지만 프로토타입 체인
을 통해 상위 객체들의 프로퍼티들을 사용 할 수 있다.
위 도식화에서 살펴 볼 수 있듯이 프로토타입 체인
은 단방향으로 이뤄져 있으며
프로퍼티를 호출하는 인스턴스를 기준으로 상위 객체를 향해 올라가며 프로퍼티나 메소드를 탐색한다.
그럼 이번에는 새로운 인스턴스를 생성해보자
let porsche = new car();
porsche.drive = function () {
console.log('겁나게 빨리가유');
};
porsche
라는 인스턴스를 생성하고 porsche
에 프로퍼티를 drive
로 새로운 것을 할당해주었다.
그리고 prosche
의 drive
를 실행하면 ?
porsche.drive() // 겁나게 빨리가유
porsche
내부에 존재하는 프로퍼티인 drive
가 먼저 실행된다.
이처럼 프로토타입 체인은 프로퍼티나 메소드를 호출한 위치로부터
자신이 소유한 프로퍼티를 먼저 탐색 => 존재하지 않으면 상위 객체가 소유한 프로퍼티 탐색 => 상위 객체의 프로토타입 => ... 최상위 계층이 소유한 프로퍼티 탐색 => 최상의 계층의 프로토타입 탐색
한다.
프로토타입 체인 정리
프로토타입 체인이란 객체들의 상속 관계를 나타내는 메커니즘으로
단방향 연결리스트
로 구성되어 있다.
인스턴스의 프로퍼티나 메소드를 호출하면 인스턴스 내부를 살펴보고 최상위 객체까지 호출한 프로퍼티나 메소드를 찾을 때 까지 올라가며 참조한다.
위에서 모든 생성자 함수(상위 객체가 될 수 있는)는 선언과 함께 prototype
프로퍼티가 내부적으로 생성된다고 하였다.
그럼 prototype
객체에는 무엇이 들어있을까 ?
function Veichle() {}
내부에 존재하는 prototype
객체 또한 Veichle
로 인해 생성된 하나의 객체이다.
prototype
객체또한 Veichle
로 인해 생성된 인스턴스로 볼 수 있으며
인스턴스 내부 프로퍼티에는 자신을 생성한 생성자를 가리키는 Constructor
프로퍼티가 존재한다.
우리는 프로토타입 체인은 상속 받은 prototype
들을 타고 올라가 상위 객체를 향해 간다고하였다.
위 도식화에서 bmw
는 car.prototype
객체를 상속 받았다고 하였다.
function Veichle() {}
Veichle.prototype.drive = function () {
console.log('앞으로가유');
};
function car() {
this.seat = 4;
}
car.prototype = new Veichle();
car.prototype.wheel = 4;
function autobycycle() {
this.seat = 2;
}
autobycycle.prototype = new Veichle();
autobycycle.prototype.wheel = 2;
let bmw = new car();
let kawasaki = new autobycycle();
bmw
[[Prototype]]
은 자신이 상속 받은prototype
객체를 의미한다.
bmw
인스턴스는 car.prototype
을 상속받았으며 car
은 Veichle.prototype
을 상속 받는다.
각 prototype
은 자신을 생성한 생성자 함수를 가리키는 프로퍼티인 constructor
를 가지고 있기 때문에 prototype
에 존재하는 constructor
프로퍼티를 이용하며 상위 객체로 올라가며 탐색하는 것이 가능하다.
프로토타입 객체 정리
프로토타입 객체
는 상위 객체가 될 수 있는생성자 함수
가 선언됨과 동시에 생성되는 객체이다>
프로토타입 객체
는 생성과 동시에 자신을 생성한 생성자 함수를 가리키는constructor
프로퍼티를 가지고 있다.
프로토타입 객체
는 인스턴스에게 상속되는 객체인데, 상속된프로토타입 객체
의constructor
프로퍼티를 통해 프로토타입 체인을 따라 상위 객체로 접근하는 것이 가능하다.
__proto__
모든 객체는 __proto__
를 통해 자신에게 상속된 프로토타입 , 즉 [[Prototype]]
내부 슬롯에 접근 할 수 있다.
bmw.__proto__
bmw
는 car
로 인해 생성된 인스턴스이지만 상속 받은 prototype
은 car
의 prototype
인 Veichle
이다.
그로 인해 Veichle
을 가리키고 있는 모습을 볼 수 있다.
__proto__
는 [[Prototype]]
내부 슬롯에 접근하는 메소드라고 하였는데, 이처럼 접근하는 메소드를 접근자 프로퍼티
라고 한다고 하였다.
접근자 프로퍼티
는 호출하는 getter
함수와 설정하는 setter
함수로 이뤄져있다고 하였다.
let obj = {}
let parent = {x : 1}
console.log(obj.__proto__);
obj.__proto__ = parent
console.log(obj.__proto__);
이처럼 __proto__
를 통해 obj
가 상속 받은 prototype
에 접근 할 수도 있고
__proto__ = 상속할 객체
를 통해서 parent
객체를 프로토타입으로 상속 받는 것도 가능하다.
의문이 들 수 있다.
엥obj
는 어떤 프로토타입도 상속 안받고 객체 리터럴로 생성했는데 왜prototype
이 존재하지 ?
이것은 빌트인 생성자 함수인Object
로 생성되었기 때문이며,Object
로 생성되어Object
의 프로토타입을 상속 받았기 때문에 상속받은 프로토타입이 존재한다.
__proto__
접근자 프로퍼티는 상속을 통해 사용된다.위에서는 생성자 함수를 선언하면 prototype
객체가 자동으로 생성된다고 하였다.
그럼 객체를 생성하면 사용 가능한 __proto__
도 객체가 생성되면 객체의 프로퍼티로 설정된 내부 메소드
일까 ?
반은 맞고 반은 틀리다.
객체를 생성하면 __proto__
를 사용 가능한 것은 맞다.
하지만 이것은 객체가 생성되면 모든 객체는 Object()
생성자 함수를 이용해 생성 된 것이기 때문에
Object
의 프로토타입을 자동으로 상속 받고, Object.prototype
내부에 존재하는 메소드인 __proto__
를 사용 할 수 있는 것이다.
let obj = { name: 'lee' };
console.log(obj.hasOwnProperty('__proto__')); // false
console.log(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__'));
/*
{
get: [Function: get __proto__],
set: [Function: set __proto__],
enumerable: false,
configurable: true
}
*/
hasOwnProperty(프로퍼티명)
는 해당 객체의 프로퍼티가 상속받은 것이 아닌 해당 객체 자신만의 프로퍼티임을 묻는 것인데 이에선 false
가 나오고
Object.prototype
객체가 가지고 있는 __proto__
란 프로퍼티에 대한 설명을 물어보면 get , set
등 프로퍼티의 어트리뷰트들이 나온다.
이를 통해 __proto__
프로퍼티는 Object.prototype
을 상속받아 사용 가능한 프로퍼티라는 것을 알 수 있다.
__proto__
를 사용해 프로토타입에 접근하는 이유[[Prototype]]
에 접근하기 위해 접근자 프로퍼티를 사용하는 이유는
상호 참조
를 방지하기 위함이다.
let child = {};
let parent = {};
child.__proto__ = parent;
parent.__proto__ = child; // TypeError: Cyclic __proto__ value
프로토타입 체인은 단방향 연결리스트 형태여야 하는데 상호 참조하게 되면 무한 로프에 빠질 수 있기 때문에
상호 참조를 방지하기 위해 접근자 프로포티를 사용한다.
__proto__
사용하지 마세유아니 실컷
__proto__
설명해놓고 사용하지 말라고 하면
모든 객체가 Object
의 프로토타입을 상속받는 것이 아니다.
직접 상속을 통해 Object.prototype
을 상속받지 않는 객체를 생성 할 수도 있다.
let obj = Object.create(null); // 직접 상속을 통해 obj 생성
// obj 는 Object 의 프로토타입을 상속받지 않아 obj는 프로토타입 체인의 ㅈ오점이다.
console.log(obj.__proto__); // undefined
console.log(Object.getPrototypeOf(obj)); // null
그렇기 때문에 어떤 객체가 상속받은 프로토타입을 확인할 때에는 Object.getPrototypeOf
를 사용하는 것이 바람직하다.
프로로타입을 설정 할 때는 Object.setPrototypeOf
를 사용하면 된다.
.prototype
vs __proto__
두개 모두 상속 시키거나, 상속 받을 프로로타입 객체를 가리킨다는 공통점이 있지만
다른 부분들이 존재한다.
속성 | .prototype | __proto__ |
---|---|---|
사용 목적 | 생성자 함수가 객체 인스턴스에게 상속할 프로퍼티와 메소드를 정의 | 객체 인스턴스가 자신의 프로토타입 객체를 가리키는 속성 |
존재하는 위치 | 생성자 함수 내에서 사용됨 | 객체 인스턴스에서 사용됨 |
종속성 | 생성자 함수에만 해당 | 모든 객체 인스턴스가 공통적으로 사용 |
예시 |
function Example() {} Example.prototype.method = function() { // ... }; |
const obj = new Example(); console.log(obj.__proto__); |
이전 챕터에서 객체 리터럴로 생성한 객체도 Object
생성자 함수로 생성한 객체로 취급한다고 했었다.
이는 자바스크립트의 추상 연산 (OrdinaryObjectCreate)
를 호출하여 Object.prototype
을 갖게 하기 때문이다.
let obj1 = new Object({ a: 1 });
let obj2 = { a: 1 };
console.log(obj1.constructor); // [Function: Object]
console.log(obj2.constructor); // [Function: Object]
추상 연산
추상 연산은 언어 사양에서 사용되는 일련의 동작을 나타내는 명세서이다. 이러한 추상 연산은 언어의 여러 부분에서 사용되는 동작이나 변환을 표현하며, 구현자들이 언어를 일관되게 구현하도록 도와준다.
추상 연산 | 정의 |
---|---|
ToPrimitive (input [, preferredType]) | 객체를 해당하는 원시 값으로 변환 |
GetValue (V) | 주어진 값을 평가하고 반환 |
ToBoolean (argument) | 주어진 값을 불리언 값으로 변환 |
ToString (argument) | 주어진 값을 문자열로 변환 |
ToNumber (argument) | 주어진 값을 숫자로 변환 |
IsCallable (argument) | 주어진 값이 호출 가능한지 여부 확인 |
IsArray (argument) | 주어진 값이 배열인지 여부 확인 |
IsObject (argument) | 주어진 값이 객체인지 여부 확인 |
IsUndefined (argument) | 주어진 값이 `undefined`인지 여부 확인 |
IsNaN (argument) | 주어진 값이 NaN (숫자가 아님)인지 여부 확인 |
let obj1 = new Object(123);
console.log(obj1); // [Number: 123]
let obj2 = new Object('123');
console.log(obj2); // [String: 123]
추상 연산의 예시이다.
객체 리터럴
로 생성된 객체는 추상 연산을 통해 안의 값을 추상 연산하여 객체 형태로 반환한다.
생성 과정에서 미묘한 차이는 있지만 동일한 특성과 프로토타입을 갖기 때문에 같게 생각해도 크게 무리는 없다.
이전 도식화에서 Vehicle
객체로부터 프로토타입 체인이 쭉쭉 내려가는 모습을 보였다.
이 때 사용된 Vehicle
은 프로토타입 체인의 종점처럼 표현했지만 사실 Vehicle
도 객체이기에
다른 생성자 함수로 생성된 객체이다.
그것은 바로 ~~~
전역 객체는 코드가 실행되기 이전 단계에서 자바스크립트 엔진에 의해 생성되는 특수한 객체를 말한다.
전역 환경이란 브라우저 환경에선 window
, Nodejs 환경에선 global
을 의미한다.
전역 객체는 표준 빌트인 객체 Object . String , Number , Function , Array ...
등과 Var
키워드로 선언한 전역 변수 및 전역 함수를 프로퍼티로 갖는다.
Math , Reflect , JSON
을 제외하고 표준 빌트인 객체는 모두 생성자 함수이다.
자세한 내용은 다음에 ..
하지만 모든 객체 프로토타입 종점에는 전역 객체가 존재한다는 것을 명심해두자
console.log(global.Object === Object); // true
객체는 프로퍼티와 메소드로 이뤄진 복잡한 자료구조이다.
이 때 객체의 프로퍼티 및 메소드는 인스턴스 내부에 존재 할 수도 있고 프로토타입이 소유한 프로퍼티 일 수도 있다.
이 때 인스턴스가 소유한 프로퍼티 및 메소드를 인스턴스 프로퍼티
, 프로토타입이 소유한 프로퍼티 및 메소드를 프로토타입 프로퍼티
라고 한다.
function Dog(name) {
this.name = name;
this.introduce = function () {
console.log(`hi i am ${this.name}`);
};
}
Dog.prototype.bark = function () {
console.log('왕왕');
};
let bbomi = new Dog('bbomi');
다음 같은 코드가 있을 때 뽀미는 Dog
의 인스턴스 프로퍼티와 프로토타입 프로퍼티를 상속 받은 인스턴스이다.
이 때 뽀미는 매우 예의가 바른 강아지라서 자신을 소개 할 때 격식을 차린다고 해보자
bbomi.introduce = function () {
console.log(`hello gentle man and ladies , i am ${this.name}`);
};
bbomi.introduce(); // hello gentle man and ladies , i am bbomi
상속 받은 introduce
프로퍼티가 존재하였으나 뽀미의 인스턴스의 프로퍼티로 동일한 프로퍼티를 수정하였다.
이 때 상속 시킨 부모 객체의 introduce
프로퍼티는 변하지 않고, 뽀미 인스턴스의 프로퍼티만 수정된다.
이처럼 상속 받은 프로퍼티 (메소드 포함) 을 수정하는 행위를 오버라이딩
이라고 한다.
뽀미가 introduce
프로퍼티를 수정하였기 때문에 Dog
의 introduce
는 뽀미에게는 사용되지 않는다.
프로토타입 체인에 의해 뽀미가
introduce
라는 프로퍼티를 가지고 있기 때문에 부모 객체까지 탐색하지 않고 뽀미의 프로퍼티가 호출되었다.
이처럼 상속 시킨 프로퍼티가 가려지는 행위를 프로퍼티 쉐도잉
이라고 한다.
동일한 매개변수를 가지고 프로퍼티를 덮는 행위를 오버 라이딩이라고 하고
기존 프로퍼티와 다른 매개변수를 받아 덮는 행위는 오버로딩
이라고 한다.
bbomi.introduce = function (friendName) {
console.log(`hello Mr /Ms ${friendName} , i am ${this.name}`);
};
bbomi.introduce('yongdol'); // hello Mr /Ms yongdol , i am bbomi
상위 객체와 다르게 이번엔 friendName
이란 매개 변수를 받아 프로퍼티를 덮었다.
이는 위에서 말한 오버로딩
에 해당한다.
뽀미 예시를 가지고 계속 진행해보자
function Dog(name) {
this.name = name;
this.introduce = function () {
console.log(`hi i am ${this.name}`);
};
}
Dog.prototype.bark = function () {
console.log('왕왕');
};
let bbomi = new Dog('bbomi');
bbomi.introduce = function (friendName) {
console.log(`hello Mr /Ms ${friendName} , i am ${this.name}`);
};
bbomi.bark = function () {
console.log('왈왈왈크르르르르르릉');
};
이번에는 인스턴스 프로퍼티와 프로토타입 프로퍼티 모두 오버라이딩을 했다.
모두 삭제해보자
프로퍼티를 삭제하는 삭제선언문은 delete
라고 하였다.
delete bbomi.bark; // 프로토타입 프로퍼티 삭제
delete bbomi.introduce; // 인스턴스 프로퍼티 삭제
console.log(bbomi.bark()); // 왕왕
console.log(bbomi.introduce()); // TypeError: badduk.introduce is not a function
오버라이딩 한 프로토타입 프로퍼티를 삭제하면 프로토타입 체인을 따라 상위 객체의 프로퍼티인 bark
가 실행된다.
하지만 오버라이딩한 인스턴스 프로퍼티를 삭제하면 상위 객체의 프로토타입을 참조하고, 상위 객체의 프로토타입안에는 introduce
가 존재하지 않아 참조하지 못한다.
프로토타입 프로퍼티를 완전히 제거하기 위해서는 상위 객체의 프로토타입에 접근하여 프로퍼티를 완전히 제거하거나
delete Dog.prototype.introduce
인스턴스의 프로퍼티를 undefined
로 변경하는 방법이 있다.
bbomi.bark = undefined
프로토타입은 객체임으로 다른 객체로 변경하는 것이 가능하다.
이러한 특징을 활용하여 객체 간의 상속 관계를 동적으로 변경 할 수 있다.
왜
Dog
이란 생성자 함수를 굳이 즉시 실행 함수로 모듈화 하여 사용했는지 모르겠지만 서적에서 이렇게 했으니 따라가도록 하겠다.
다음처럼 생성자 함수인 Dog
의 프로토타입을 {bark : ...}
로 감싸진 객체로 변경했다.
그렇게 생성된 bbomi
인스턴스를 살펴보면 상속받은 프로토타입이 Dog
가 아닌 Object
임을 알 수 있다.
하지만
Dog
생성자 함수로 인해 생성되었기 때문에Dog
로 생성된 객체임은 Dog {name : bbomi} 을 통해 알 수 있다.
이는 생성자 함수인 Dog
의 프로토타입이 {bark : ..}
로 변경되면서 constructor
가 Dog
를 가리키고 있는 것이 아닌
{bark : ... }
자체 객체가 상속받은 프로토타입이 constructor : Object
를 가리키고 있기 때문이다.
{bark : ... }
또한 빌트인 생성자 함수인Obejct
로 생성된 객체이기 때문에 상속 받은 프로토타입이 존재한다.
이처럼 상속 관계를 유지하기 위해서는 상속 받는 prototype
객체에 상속 관계를 유지할 constructor
프로퍼티가 필요하다.
const myPet = (function () {
function Dog(name) {
this.name = name;
}
Dog.prototype = {
constructor: Dog,
bark: function () {
console.log('왕왕!');
},
};
return Dog;
})();
let bbomi = new myPet('bbomi');
console.log(bbomi); // Dog { name: 'badduk' }
console.log(bbomi.constructor); // [Function: Dog]
이처럼 상속 줄 prototype
객체를 변경하여 상속 관계를 제거하거나 변경하는 것이 가능하다.
물론 상속 받은 입장에서
__proto__
를 이용해 상위 받은prototype
을 변경 하는 방법이나
Object.setPropertyOf('객체명' , '프로토타입 객체')
를 통해 수정하는 것도 가능하다.
하지만 프로토타입 교체를 통해 객체 간의 상속 관계를 동적으로 변경하는 것은 꽤나 번거롭다.
따라서 프로토타입은 직접 교체하지 않는 편이 좋고 , 상속 관계를 인위적으로 설정 하려면 곧 살펴볼 직접 상속
이 더 편리하고 안전하다.
또는 ES6
에서 도입된 class
문법을 이용하는 편이 좋다.
instanceof
연산자instanceof
는 이항 연산자로 좌항에는 객체를 가리키는 식별자 , 우변에는 생성자 함수를 가리키는 식별자를 피연산자로 받는다 .
우변의 피연산자가 함수가 아닌 경우 TypeError
가 발생한다.
function Person(name) {
this.name = name;
}
let tom = new Person('tom');
console.log(tom instanceof Person); // true
console.log(tom instanceof Object); // true
console.log(tom instanceof String); // false
console.log(Person instanceof Object); // true
프로토타입 체인을 따라 상위 계층의 객체까지 모두 포함한다.
생성자 함수도 객체이기 때문에 instanceof
를 통해 확인 할 수 있다.
instanceof
는 생성자 함수를 가리키는 constructor
를 따라 확인하는걸까 ? 아니면 prototype
으로 확인을 하는걸까 ?
function Person(name) {
this.name = name;
}
let tom = new Person('tom');
const parent = { x: 1 };
console.log('tom 의 prototype 을 parent 로 설정 후');
Object.setPrototypeOf(tom, parent);
console.log(`tom.__proto__ : ${tom.__proto__}`);
console.log(`tom instanceof Person : ${tom instanceof Person}`);
console.log(`tom instanceof Object : ${tom instanceof Object}`);
Person.prototype = parent;
console.log('----------------------------------------');
console.log('Person 의 prototype = parent 설정 후');
console.log(`tom instanceof Person : ${tom instanceof Person}`);
다음처럼 설정했을 때 어떤 결과가 나올 것 같은지 생각해보자
tom 의 prototype 을 parent 로 설정 후
tom
은 Person
으로 생성된 인스턴스이지만 상속 받은 prototype
은 parent
객체이다.
tom
이 상속받은 prototype
은 Person
의 prototype
이 아니며
parent
객체는 Object
의 프로토타입을 상속 받았기 때문에
tom 의 prototype 을 parent 로 설정 후
tom.__proto__ : [object Object]
tom instanceof Person : false
tom instanceof Object : true
결과는 다음과 같다.
Person 의 prototype = parent 설정 후
Person
의 prototype
이 parent
객체로 설정 된 경우
tom
이 상속 받은 prototype
은 parent
객체 , Person
의 prototype
은 parent
객체이므로
Person 의 prototype = parent 설정 후
tom instanceof Person : true
결과는 다음과 같다.
결국 instanceOf
는 프로토타입 체인을 따라 해당 인스턴스가 상속 받은 프로토타입이 상위 객체의 프로로타입인지를 확인하는 것이다.
재귀 함수로 구현해보자
function IsInstanceOf(instance, constructor) {
let prototype = Object.getPrototypeOf(instance);
if (prototype === null) return false; //프로토타입이 존재하지 않으면 최상위 계층이기 때문에 false 반환
return (
prototype === constructor.prototype || IsInstanceOf(prototype, constructor)
);
}
재귀함수는 항상 볼 때마다 헷갈린다.
매개변수로는 인스턴스와 생성자 함수를 받는다. (instance , constructor
)
재귀함수의 중단 조건은 prototype === null
일 경우이구나
그 말은 최상위 객체까지 탐색하였을 때의 프로토타입은 null
이기 때문에 그럴 경우 false
를 반환하란 거구나
그 다음 반환문을 보면 단축 연산
을 이용하는 것을 볼 수 있다.
||
는 or
을 이용하는 연산으로 만약 prototype === constructor.prototype
이면 true
를 반환하고 멈춘다.
만약 좌항이 false
일 경우 우항에 존재하는 재귀 함수가 발동하여 prototype
을 instance
로 받아 확인한다.
console.log(IsInstanceOf(tom, Person)); // true
console.log(IsInstanceOf(tom, Object)); // true
결국 어떤 인스턴스
의 프로토타입 체인에 어떤 생성자 함수가 존재하는지를 확인 하는 것은 해당 생성자 함수의 프로토타입과 인스턴스
가 상속받은 프로토타입
들이 동일한지를 확인 하는 것이다.
이번엔 constructor
를 제거한 프로토타입을 상속받은 인스턴스
를 생성하여 확인해보자
const MakePerson = (function () {
function Person(name) {
this.name = name;
}
Person.prototype = {
x: 1,
};
return Person;
})();
const tom = new MakePerson('tom');
console.log(tom.constructor); // Object
console.log(tom.constructor === MakePerson); // false
console.log(tom instanceof MakePerson); // true
tom.constructor === Object
인 이유
tom
의prototype
은{x:1}
이라는 객체이다.tom
의constructor
를 부르면tom
의 인스턴스 프로퍼티를 확인한다.- 없기 때문에
tom
이 상속 받은 프로토타입을 확인한다.{x:1}
객체 내 인스턴스 프로퍼티를 확인한다.- 없기 때문에
{x:1}
이 상속받은 프로토타입을 확인한다.{constructor : Object , ... }
내에constructor
가 존재하기 때문에Object
를 반환
결국 프로토타입 체인을 통해 존재하는 프로퍼티 ({x:1}
이Object
에게서 상속받은 프로토타입을 참조)를 반환한 것이다.
instanceof
연산자 정리어떤 인스턴스가 어떤 생성자 함수의 프로토타입을 상속받았는지를 확인하는
instanceof
연산자는
우항이 소유하고 있는 프로토타입이 좌항이 상속 받은 프로트타입과 동일한지 좌항의 프로토타입 체인을 따라 재귀적으로 확인한다.
우리는 현재 생성자 함수를 이용해 객체를 생성하면, 생성자 함수에 존재하는 prototytpe
객체들 또한 상속되며 , 이런 상속 관계 (프로토타입 체인
) 을 통해서 상위 객체의 프로퍼티나 메소드에 접근 할 수 있다고 하였다.
이번에는 생성자 함수를 이용하지 않고 프로퍼티를 직접 객체 형태로 만들어 상속 시키는 직접 상속
에 대해 알아보자
let proto = {
introduce() {
console.log(`hello~ i am ${this.name}`);
},
}; // 상속 시킬 프로토타입 객체 생성
let tom = Object.create(proto, {
name: {
value: 'tom',
writable: true,
enumerable: true,
configurable: true,
},
});
console.log(tom);
tom.introduce(); // hello ~ i am tom
Object.create(상속 시킬 프토토타입 객체 , 프로퍼티 디스크립터 형태의 객체)
를 통해 객체에게 프로토타입을 직접 상속 시킬 수 있다.
직접 상속을 하면 편리한 점은 다음과 같다.
new
연산자 없이도 객체를 생성 할 수 있다.Object.create()
를 사용하면 생성자 함수 사용 없이도 객체 리터럴을 이용해 프로토타입 체인에 속하는 객체를 생성 할 수 있다.
이 때 Object.create()
에 사용된 프로토타입 객체의 프로토타입 체인 또한 같이 연결된다.
프로토타입 체인을 동적으로 조절할 수 있기 때문에
빌트인 생성자 함수인 Object
의 프로토타입을 상속받지 않는 객체를 생성하는 것도 가능하다.
let tom = Object.create(null, {
name: {
value: 'tom',
writable: true,
enumerable: true,
configurable: true,
},
});
let jerry = { name: 1 };
function logPrototypeChain(instance) {
while (instance) {
console.log(instance);
instance = Object.getPrototypeOf(instance);
}
}
logPrototypeChain(tom);
logPrototypeChain(jerry);
그럼으로 객체는 프로토타입 체인에서 Object
의 프로토타입을 상속 받지 않았을 수도 있기 때문에
최상위 빌트인 객체의 프로토타입을 사용 할 때 바로 불러내어 사용 하는 것은 추천되지 않는다.
/*
jerry : 객체 리터럴로 생성하여 Object 의 프로토타입을 상속받음
tom : Object.create(null) 로 생성하여 어떤 프로토타입도 상속받지 않음
*/
console.log(jerry.hasOwnProperty('name')); // true
console.log(tom.hasOwnProperty('name')); // TypeError: tom.hasOwnProperty is not a function
따라서
console.log(Object.hasOwnProperty.call(jerry, 'name')); // name
console.log(Object.hasOwnProperty.call(tom, 'name')); // name
다음처럼 call
을 이용해 최상위 빌트인 객체의 프로토타입을 호출하는 것을 권장한다고 한다.
__proto__
를 이용한 직접 상속let proto = { age: 16 };
let obj = {
name: 'lee',
__proto__: proto,
};
console.log(Object.getPrototypeOf(obj)); // { age: 16 }
객체 리터럴로 객체를 생성 할 때 __proto__
를 직접 프로퍼티로 불러 상속 시키는 방법도 있으나
웬만하면 __proto__
를 이용해 프로토타입에 접근하지 말라고 했기 때문에 추천하는 방식은 아니라고 한다.
// 생성자 함수 생성 및 인스턴스 프로퍼티 설정
function Person(name) {
this.name = name;
this.age = 16;
this.introduce = function () {
console.log(`hi i am ${this.name}`);
};
}
// 프로토타입 메소드 추가
Person.prototype.sing = function sing() {
console.log('lalala~~');
};
let tom = new Person('tom');
tom.introduce(); // hi i am tom
tom.sing(); // lalala~~
생성자 함수를 이용한 상속에서
생성자 함수를 선언하고 생성자 함수를 통해 생성한 인스턴스는 생성자 함수의 인스턴스 프로퍼티
와 프로토타입 프로퍼티
를 상속받는다고 하였다.
하지만 생성자 함수 자체에서 본인의 프로퍼티들을 가지고 동작만 하고 싶고 인스턴스에겐 상속시키지 않는 프로퍼티들도 분명 존재한다.
이를 정적 메소드 or 정적 프로퍼티라고 한다.
// 생성자 함수 생성 및 인스턴스 프로퍼티 설정
function Person(name) {
this.name = name;
this.age = 16;
this.introduce = function () {
console.log(`hi i am ${this.name}`);
};
}
// 프로토타입 메소드 추가
Person.prototype.sing = function sing() {
console.log('lalala~~');
};
// 정적 메소드 추가
Person.twerk = function twerk() {
console.log('twerk twerk');
};
let tom = new Person('tom');
tom.introduce(); // hi i am tom
tom.sing(); // lalala~~
Person.twerk(); // twerk twerk
tom.twerk(); // TypeError: tom.twerk is not a function
정적 메소드 , 정적 프로퍼티는 생성자 함수 선언 이후 동적으로 추가된 프로퍼티나 메소드 를 의미한다.
이는 생성자 함수에서만 호출이 가능하며 인스턴스에서는 호출이 불가능하다.
let tom = { name: 'tom' };
let jerry = { name: 'jerry' };
console.log(Object.getOwnPropertyDescriptors(jerry));
/*
{
name: {
value: 'jerry',
writable: true,
enumerable: true,
configurable: true
}
}
(/
console.log(tom.getOwnPropertyDescriptors(jerry)); // TypeError: tom.getOwnPropertyDescriptors is not a function
마치 tom
은 Object
과 프로토타입 체인연결되어 있으나 Object.getOwnPropertyDescriptors
는 사용 불가능 하고 Object
생성자 함수로만 사용 할 수 있듯이 말이다.
in
연산자in
연산자는 이항 연산자로 좌항의 프로퍼티 키가 우항의 객체에 존재하는지를 묻는다.
let tom = {
name: 'tom',
age: 16,
};
console.log('name' in tom); // true
console.log('age' in tom); // true
console.log('toString' in tom); // true (상속받은 프로퍼티들 또한 묻는다)
console.log('address' in tom); // false
in
연산자는 상속받은 프로퍼티들에 대한 내용들 조차도 평가한다는 것을 기억하자.
Object.prototype.hasOwnProperty
연산자Object.prototype.hasOwnProperty
는 Object
의 prototype
객체에 존재하는 프로퍼티로
인스턴스가 상속받지 않고 생성한 프로퍼티들이 존재하는지를 묻는다.
let tom = {
name: 'tom',
age: 16,
};
console.log(tom.hasOwnProperty('name')); // true
console.log(Object.prototype.hasOwnProperty.call(tom, 'name')); // true
다음처럼 Object.prototype
을 상속 받았음을 가정하고 불러도 되고
아니면 Object
객체와 call
을 이용한 간접 호출을 이용하여 부를 수도 있다.
for in
문for
문은 반복문이라고 하였다.
let tom = {
name: 'tom',
age: 16,
};
for (let key in tom) {
console.log(key);
} // name age
그런데 in
문은 상속받은 프로퍼티 키들도 불러온다고 하지 않았나 ?
맞아용
그런데 상속받은 프로퍼티 키들이 나타나지 않는 이유는 상속받은 프로퍼티 키들이 호출은 가능해도 나열하는 것이 불가능하기 때문이다.
즉 프로퍼티 어트리뷰트 중 enumerable
이 false
로 되어있기 때문이다.
즉 for in
문은 프로퍼티 어트리뷰트중 enumerable : true
인 프로퍼티들에 대해서 모두 나열한다.
그럼 만약 상속된 프로퍼티들 중 enumerable : true
인 프로퍼티들도 있을 텐데 상속 안받고 본인만의 프로퍼티를 나열하고 싶다면 ?
function person(name) {
this.name = name;
}
person.prototype.introduce = function introduce() {
console.log(`hi i am ${this.name}`);
};
let tom = new person('tom');
for (let key in tom) {
console.log(key); // name introduce
}
상속받은 프로퍼티인 introduce
까지 같이 로그 된다.
for (let key in tom) {
if (tom.hasOwnProperty(key)) { // 본인만의 프로퍼티인지 확인 후 로그
console.log(key); // name
}
}
본인만의 프로퍼티인지 확인하는 조건문을 넣어주면 된다.
Object. keys/values/entries
function person(name) {
this.name = name;
this.address = 'korea';
}
person.prototype.introduce = function introduce() {
console.log(`hi i am ${this.name}`);
};
let tom = new person('tom');
console.log(Object.keys(tom)); // [ 'name', 'address' ]
console.log(Object.values(tom)); // [ 'tom', 'korea' ]
console.log(Object.entries(tom)); // [ [ 'name', 'tom' ], [ 'address', 'korea' ] ]
Object
의 정적 메소드를 이용해 객체의 프로퍼티, 프로퍼티 값 , 프로퍼티 키와 값 쌍을 배열 형태로 반환 할 수 있다.
이 때 설명한 key , values , entries
는 상속받지 않고 본인만의 고유한 프로퍼티들만을 반환하기에 불필요한 조건문을 넣을 필요가 없다.