개발의변화
모던자바스크립트 19장 프로토타입 본문
자바스크립트는 거의 "모든 것"이 객체
19.1 객체지향 프로그래밍
객체의 상태를 나타내는 데이터와 상태 데이터를 조작할 수 있는 동작을 하나의 논리적인 단위로 묶는ㄷㅏ.
객체의 상태 데이터를 프로퍼티,동작을 메서드
추상화: 사물이나 표상을 어떤 성질,공통성,본질에 착안하여 그것을 추출하여 파악하는 것
상속: 어떤 객체의 프로퍼티나 메ㅎ서드를 다른 객체가 상속받는 것
다형성: 어떤 객체의 속성이나 기능이 상황에 따라 여러가지 형태를 가질 수 있는 성질
캡슐화: 서로 연관있는 속성과 기능들을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것
19.2 상속과 프로토타입
// 생성자 함수
function Circle(radius) {
this.radius = radius;
this.getArea = function () {
// Math.PI는 원주율을 나타내는 상수다.
return Math.PI * this.radius ** 2;
};
}
// 반지름이 1인 인스턴스 생성
const circle1 = new Circle(1);
// 반지름이 2인 인스턴스 생성
const circle2 = new Circle(2);
// Circle 생성자 함수는 인스턴스를 생성할 때마다 동일한 동작을 하는
// getArea 메서드를 중복 생성하고 모든 인스턴스가 중복 소유한다.
// getArea 메서드는 하나만 생성하여 모든 인스턴스가 공유해서 사용하는 것이 바람직하다.
console.log(circle1.getArea === circle2.getArea); // false
console.log(circle1.getArea()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172
getArea 메서드는 모든 인스턴스가 동일한 내용의 메서드를 사용하므로 단 하나만 생성하여 모든 인스턴스가 공유해서 사용하는 것이 바람직하다.
// 생성자 함수
function Circle(radius) {
this.radius = radius;
}
// Circle 생성자 함수가 생성한 모든 인스턴스가 getArea 메서드를
// 공유해서 사용할 수 있도록 프로토타입에 추가한다.
// 프로토타입은 Circle 생성자 함수의 prototype 프로퍼티에 바인딩되어 있다.
Circle.prototype.getArea = function () {
return Math.PI * this.radius ** 2;
};
// 인스턴스 생성
const circle1 = new Circle(1);
const circle2 = new Circle(2);
// Circle 생성자 함수가 생성한 모든 인스턴스는 부모 객체의 역할을 하는
// 프로토타입 Circle.prototype으로부터 getArea 메서드를 상속받는다.
// 즉, Circle 생성자 함수가 생성하는 모든 인스턴스는 하나의 getArea 메서드를 공유한다.
console.log(circle1.getArea === circle2.getArea); // true
console.log(circle1.getArea()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172
상속은 코드의 재사용이란 관점에서 매우 유용
19.3 프로토타입 객체
객체 간 상속을 구현하기 위해 프로토타입을 구현
프로토타입은 어떤 객체의 상위 객체역할을 하는 객체로서 다른 객체에 공유 프로퍼티를 제공
프로토타입을 상속받은 하위 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용할 수 있다.
모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조
// obj는 프로토타입 체인의 종점이다. 따라서 Object.__proto__를 상속받을 수 없다.
const obj = Object.create(null);
// obj는 Object.__proto__를 상속받을 수 없다.
console.log(obj.__proto__); // undefined
// 따라서 __proto__보다 Object.getPrototypeOf 메서드를 사용하는 편이 좋다.
console.log(Object.getPrototypeOf(obj)); // null
함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다
// 함수 객체는 prototype 프로퍼티를 소유한다.
(function () {}).hasOwnProperty('prototype'); // -> true
// 일반 객체는 prototype 프로퍼티를 소유하지 않는다.
({}).hasOwnProperty('prototype'); // -> false
prototype 프로퍼티는 생성자 함수가 생성할 객체의 프로토타입을 가리킨다.
즉 생성자 함수로서 호출할 수 없는 함수 화살표함수는 prototype 프로퍼티를 소유하지 않으며 프로토타입도 생성하지 않는다.
// 화살표 함수는 non-constructor다.
const Person = name => {
this.name = name;
};
// non-constructor는 prototype 프로퍼티를 소유하지 않는다.
console.log(Person.hasOwnProperty('prototype')); // false
// non-constructor는 프로토타입을 생성하지 않는다.
console.log(Person.prototype); // undefined
// ES6의 메서드 축약 표현으로 정의한 메서드는 non-constructor다.
const obj = {
foo() {}
};
// non-constructor는 prototype 프로퍼티를 소유하지 않는다.
console.log(obj.foo.hasOwnProperty('prototype')); // false
// non-constructor는 프로토타입을 생성하지 않는다.
console.log(obj.foo.prototype); // undefined
모든 객체가 가지고 있는 (어밀히 말하면 Object.prototype으로부터 상속받은) proto 접근자 프로퍼티아 함수 객체 만이 가지고 있는 prototype 프로퍼티는 결국 동일한 프로토타입을 가리킨다.
// 생성자 함수
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// 결국 Person.prototype과 me.__proto__는 결국 동일한 프로토타입을 가리킨다.
console.log(Person.prototype === me.__proto__); // true
모든 프로토타입은 constructor 프로퍼티를 갖는다. 이 constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨.
19.4 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입
// 생성자 함수
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// 결국 Person.prototype과 me.__proto__는 결국 동일한 프로토타입을 가리킨다.
console.log(Person.prototype === me.__proto__); // true
하지만 리터럴 표기법에 의한 객체 생성 방식과 같이 명시적으로 new 연산자와 함께 생성자 함수를 호출하여 인스턴스를 생성하지 앟는ㄴ 방식도 있다.
// 2. Object 생성자 함수에 의한 객체 생성
// 인수가 전달되지 않았을 때 추상 연산 OrdinaryObjectCreate를 호출하여 빈 객체를 생성한다.
let obj = new Object();
console.log(obj); // {}
// 1. new.target이 undefined나 Object가 아닌 경우
// 인스턴스 -> Foo.prototype -> Object.prototype 순으로 프로토타입 체인이 생성된다.
class Foo extends Object {}
new Foo(); // Foo {}
// 3. 인수가 전달된 경우에는 인수를 객체로 변환한다.
// Number 객체 생성
obj = new Object(123);
console.log(obj); // Number {123}
// String 객체 생성
obj = new Object('123');
console.log(obj); // String {"123"}
객체 리터럴에 의해 생성된 객체는 Object 생성자 함수가 생성한 객체가 아니다.
// foo 함수는 Function 생성자 함수로 생성한 함수 객체가 아니라 함수 선언문으로 생성했다.
function foo() {}
// 하지만 constructor 프로퍼티를 통해 확인해보면 함수 foo의 생성자 함수는 Function 생성자 함수다.
console.log(foo.constructor === Function); // true
프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재
19.5 프로토타입 생성 시점
프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성된다.
// 함수 정의(constructor)가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다.
console.log(Person.prototype); // {constructor: ƒ}
// 생성자 함수
function Person(name) {
this.name = name;
}
// 화살표 함수는 non-constructor다.
const Person = name => {
this.name = name;
};
// non-constructor는 프로토타입이 생성되지 않는다.
console.log(Person.prototype); // undefined
생성된 프로토타입은 오직 constructor 프로퍼티만을 갖는 객체다.
19.6 객체 생성 방식과 프로토타입의 결정
객체 생성 방식 (객체 리터럴, Object 생성자 함수, 생성자 함수, Object.create 메서드, 클래스)
객체 리터럴에 의해 생성된 객체의 프로토타입
const obj = { x: 1 };
// 객체 리터럴에 의해 생성된 obj 객체는 Object.prototype을 상속받는다.
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty('x')); // true
추상 연산 OrdinaryObjectCreate에 의해 다음과 같이 Object 생성자 함수와 Object.prototype과 생성된 객체 사이에 연결이 만들어진다.
Object 생성자 함수에 의해 생성된 객체의 프로토타입
const obj = new Object();
obj.x = 1;
// Object 생성자 함수에 의해 생성된 obj 객체는 Object.prototype을 상속받는다.
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty('x')); // true
객체 리터럴과 Object 생성자 함수에 의해 객체 생성 방식의 차이는 프로퍼티를 추가하는 방식에 있다.
생성자 함수에 의해 생성된 객체의 프로토타입
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
추상연산 OrdiranryObjectCreate에 의해 다음과 같이 생성자 함수와 생성자 함수의 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
19.7 프로토타입 체인
function Person(name) {
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};
const me = new Person('Lee');
// hasOwnProperty는 Object.prototype의 메서드다.
console.log(me.hasOwnProperty('name')); // true
프로토타입의 프로토타입인 Object.prototype
자바스크립트는 객체의 프로퍼티에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색하는 것이 프로토타입 체인이다.
프로토타입 체인의 최상위는 언제난 Object.protoypedlek.
프로토타입 체인의 종점인 Object.prototype에도 프로퍼티를 검색할 수 없는 경우 undefined를 반환한다.
19.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
상속관계에 의해 프로퍼티가 가려지는 현상을 프로퍼티 섀도잉이라 한다.
오버라이딩: 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용
오버로딩: 함수의 이름은 동일하지만 매개변수의 타입 또는 개수가 다른 메서드를 구현하고 매개변수에 의해 메서드를 구별하여 호출하는 방식 ->javascript에서는 지원하지 않지만 arguments객체를 사용하여 구현할 수 있다.
하위 객체가 프로토타입의 프로퍼티를 변경 삭제하는 것은 불가능하다
직접 삭제하기 위해서는
// 프로토타입 메서드 변경
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
프로토타입의 교체
const Person = (function () {
function Person(name) {
this.name = name;
}
// ① 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
Person.prototype = {
sayHello() {
console.log(`Hi! My name is ${this.name}`);
}
};
return Person;
}());
const me = new Person('Lee');
프로토타입으로 교체한 객체 리터럴에는 constructor 프로퍼티가 없다
// 프로토타입을 교체하면 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴된다.
console.log(me.constructor === Person); // false
// 프로토타입 체인을 따라 Object.prototype의 constructor 프로퍼티가 검색된다.
console.log(me.constructor === Object); // true
되살리는 방법
const Person = (function () {
function Person(name) {
this.name = name;
}
// 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
Person.prototype = {
// constructor 프로퍼티와 생성자 함수 간의 연결을 설정
constructor: Person,
sayHello() {
console.log(`Hi! My name is ${this.name}`);
}
};
return Person;
}());
const me = new Person('Lee');
// constructor 프로퍼티가 생성자 함수를 가리킨다.
console.log(me.constructor === Person); // true
console.log(me.constructor === Object); // false
인스턴스에 의한 프로토타입 교체
인스턴스의 proto 접근자 프로퍼티를 통해 접근이 가능하다.
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// 프로토타입으로 교체할 객체
const parent = {
sayHello() {
console.log(`Hi! My name is ${this.name}`);
}
};
// ① me 객체의 프로토타입을 parent 객체로 교체한다.
Object.setPrototypeOf(me, parent);
// 위 코드는 아래의 코드와 동일하게 동작한다.
// me.__proto__ = parent;
me.sayHello(); // Hi! My name is Lee
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// 프로토타입으로 교체할 객체
const parent = {
// constructor 프로퍼티와 생성자 함수 간의 연결을 설정
constructor: Person,
sayHello() {
console.log(`Hi! My name is ${this.name}`);
}
};
// 생성자 함수의 prototype 프로퍼티와 프로토타입 간의 연결을 설정
Person.prototype = parent;
// me 객체의 프로토타입을 parent 객체로 교체한다.
Object.setPrototypeOf(me, parent);
// 위 코드는 아래의 코드와 동일하게 동작한다.
// me.__proto__ = parent;
me.sayHello(); // Hi! My name is Lee
// constructor 프로퍼티가 생성자 함수를 가리킨다.
console.log(me.constructor === Person); // true
console.log(me.constructor === Object); // false
// 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입을 가리킨다.
console.log(Person.prototype === Object.getPrototypeOf(me)); // true
instanceof 연산자
객체 instanceof 생성자 함수
우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true 아님 false
// 생성자 함수
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// 프로토타입으로 교체할 객체
const parent = {};
// 프로토타입의 교체
Object.setPrototypeOf(me, parent);
// Person 생성자 함수와 parent 객체는 연결되어 있지 않다.
console.log(Person.prototype === parent); // false
console.log(parent.constructor === Person); // false
// parent 객체를 Person 생성자 함수의 prototype 프로퍼티에 바인딩한다.
Person.prototype = parent;
// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Person); // true
// Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Object); // true
function isInstanceof(instance, constructor) {
// 프로토타입 취득
const prototype = Object.getPrototypeOf(instance);
// 재귀 탈출 조건
// prototype이 null이면 프로토타입 체인의 종점에 다다른 것이다.
if (prototype === null) return false;
// 프로토타입이 생성자 함수의 prototype 프로퍼티에 바인딩된 객체라면 true를 반환한다.
// 그렇지 않다면 재귀 호출로 프로토타입 체인 상의 상위 프로토타입으로 이동하여 확인한다.
return prototype === constructor.prototype || isInstanceof(prototype, constructor);
}
console.log(isInstanceof(me, Person)); // true
console.log(isInstanceof(me, Object)); // true
console.log(isInstanceof(me, Array)); // false
19.11 직접상속
Object.create는 명시적으로 프로토타입을 지정하여 새로운 객체를 생성
// 프로토타입이 null인 객체를 생성한다. 생성된 객체는 프로토타입 체인의 종점에 위치한다.
// obj → null
let obj = Object.create(null);
console.log(Object.getPrototypeOf(obj) === null); // true
// Object.prototype을 상속받지 못한다.
console.log(obj.toString()); // TypeError: obj.toString is not a function
// obj → Object.prototype → null
// obj = {};와 동일하다.
obj = Object.create(Object.prototype);
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
// obj → Object.prototype → null
// obj = { x: 1 };와 동일하다.
obj = Object.create(Object.prototype, {
x: { value: 1, writable: true, enumerable: true, configurable: true }
});
// 위 코드는 다음과 동일하다.
// obj = Object.create(Object.prototype);
// obj.x = 1;
console.log(obj.x); // 1
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
const myProto = { x: 10 };
// 임의의 객체를 직접 상속받는다.
// obj → myProto → Object.prototype → null
obj = Object.create(myProto);
console.log(obj.x); // 10
console.log(Object.getPrototypeOf(obj) === myProto); // true
// 생성자 함수
function Person(name) {
this.name = name;
}
// obj → Person.prototype → Object.prototype → null
// obj = new Person('Lee')와 동일하다.
obj = Object.create(Person.prototype);
obj.name = 'Lee';
console.log(obj.name); // Lee
console.log(Object.getPrototypeOf(obj) === Person.prototype); // true
객체 리터럴 내부에서 "proto"에 의한 직접 상속
const myProto = { x: 10 };
// 객체 리터럴에 의해 객체를 생성하면서 프로토타입을 지정하여 직접 상속받을 수 있다.
const obj = {
y: 20,
// 객체를 직접 상속받는다.
// obj → myProto → Object.prototype → null
__proto__: myProto
};
/* 위 코드는 아래와 동일하다.
const obj = Object.create(myProto, {
y: { value: 20, writable: true, enumerable: true, configurable: true }
});
*/
console.log(obj.x, obj.y); // 10 20
console.log(Object.getPrototypeOf(obj) === myProto); // true
19.12 정적 프로퍼티
// 생성자 함수
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는 정적 메서드다.
const obj = Object.create({ name: 'Lee' });
// Object.prototype.hasOwnProperty는 프로토타입 메서드다.
obj.hasOwnProperty('name'); // -> false
```js
function Foo() {}
// 프로토타입 메서드
// this를 참조하지 않는 프로토타입 메소드는 정적 메서드로 변경해도 동일한 효과를 얻을 수 있다.
Foo.prototype.x = function () {
console.log('x');
};
const foo = new Foo();
// 프로토타입 메서드를 호출하려면 인스턴스를 생성해야 한다.
foo.x(); // x
// 정적 메서드
Foo.x = function () {
console.log('x');
};
// 정적 메서드는 인스턴스를 생성하지 않아도 호출할 수