프로토타입
생성자함수
생성자 함수는 다른 언어에서 클래스와 같은 역할을 합니다. ES6에서 다른 언어와 같은 class 용법이 등장했지만, 이전까지는 자바스크립트에서 클래스를 구현하기 위해서 함수를 사용했습니다. 따라서 함수는 일반 함수로 호출 되었을 때와 생성자 함수(클래스)로 호출 되었을 때 다른 내부 메서드가 호출이 됩니다. 일반 함수로 호출되었을 경우에는 [[Call]]이 호출되고, new 연산자와 함께 생성자 함수로 호출이 되면 내부 메서드 [[Construct]]가 호출됩니다.
- 모든 함수는 호출 가능해야 하기에 [[Call]]을 포함합니다. 하지만 모든 함수 객체가 [[Construct]]인 것은 아닙니다.
- constructor : 함수 선언문, 함수 표현식, 클래스
- non-constructor : 메서드(ES6 정의), 화살표 함수
이때 함수가 생성자 함수로 호출 되는지 혹은 일반 함수로 호출되는지 결정하는 것은 함수의 형태가 아닌, 호출했을 시 new 연산자의 여부입니다. 함수의 프로퍼티 중에 prototype 프로퍼티는 생성자 함수로 호출할 수 있는 함수 객체, 즉 constructor만이 소유하는 프로퍼티입니다. 일반 객체와 생성자 함수로 호출할 수 없는 non-constructor에는 prototype 프로퍼티가 없습니다.
객체지향
자바스크립트는 명령형, 함수형, 프로토타입 기반 객체지향 프로그래밍 언어를 지원하는 멀티 패러다임 프로그래밍 언어입니다. 또한 자바스크립트에서 원시 값을 제외한 모든 값들은 객체입니다. 즉 자바스크립트를 이루고 있는 거의 모든 것이 객체입니다.
- 객체: 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료구조, 따라서 객체는 상태 데이터와 동작(상태 데이터를 조작)을 하나의 논리적인 단위로 묶은 복합적인 자료구조이다. 이때 상태 데이터를 프로퍼티(property), 동작을 메서드(method)라 합니다.
- 즉 객체지향 프로그래밍은 독립적인 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임이다.
상속
- 자바스크립트는 프로토타입을 기반으로 상속을 구현합니다.
- 내용이 동일한 메서드는 상속을 통해 공유하여 사용합니다.
// 생성자 함수
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


자바스크립트는 프로토타입을 기반으로 상속을 구현합니다.
프로토타입
모든 객체는 [[Prototype]]이라는 내부 슬롯을 가집니다. 상속되는 속성과 메소드들은 각 객체가 아니라 객체 생성자의 prototype
이라는 속성에 정의되어 있습니다.
즉 하나의 객체에 포함된 메서드 또한 객체입니다. 따라서 모든 메서드 내부에는 [[Prototype]]이라는 내부 슬롯을 가집니다.
const obj = {
name : 'son',
getName(){
console.log(`${this.name}`)
}
}
obj.getName()
console.dir(obj)

getName의 [[Prototype]]에 함수 프로토타입이 있다는 것을 확인할 수 있으며, 함수 [[Prototype]]의 부모 프로토타입은 Object임을 확인 할 수 있습니다.
모든 객체는 __proto__
접근자 프로퍼피틀 통해 자신의 프로토타입,
즉 [[Prototype]] 내부 슬롯에 Object.prototype
에 간접적으로 접근할 수 있습니다.
Object.is()
,Object.keys()
등 prototype
에 정의되지 않은 멤버들은 상속되지 않습니다. Object()
생성자에서만 사용할 수 있는 멤버들입니다.
이러한 프로토타입은 체인으로 연결됩니다. 결국 마지막 프로토타입은 항상 Object
를 참조합니다.
프로토타입 체인은 단방향 링크드 리스트로 구현되어야 합니다(상호 참조 불가!).
즉, 프로퍼티 검색 방향이 한쪽 방향으로만 흘러가야 합니다.
따라서 아무런 체크 없이 무조건적으로 프로토타입을 교체할 수 없도록 __proto__
접근자 프로퍼티를 통해 프로토타입에 접근하고 교체하도록 구현되어 있습니다.
하지만 __proto__
는 ECMA SCRIPT에서 권장하지 않습니다.
왜냐하면 각 브라우저에서 임으로 만든 방식이기 때문입니다.
따라서 프로토타입을 참조하고 싶다면, Object.getPrototypeOf()
, 프로토타입을 교체하고 싶다면 Object.setPrototypeOf()
메서드 사용을 권장합니다.
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
모든 객체가 가지고 있는(엄밀히 말하면 Object.prototype으로부터 상속받은) __proto__
접근자 프로퍼티와 함수 객체만이 가지고 있는 prototype 프로퍼티는 결국 동일한 프로토타입을 가리킵니다.
함수 객체 만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킵니다. 즉 클래스가 생성할 객체의 프로토타입을 가리킵니다. 즉 각 프로토타입 내부 슬롯의 construstor 프로퍼티는 언제나 자기 자신을 가리킵니다.
function hello() {
return 'hello';
}
console.dir(hello);

function Person(name) {
this.name = name;
}
const me = new Person('Lee');
console.dir(me);

생성자 함수로 정의하지 않은 일반 함수도 prtotype 프로퍼티를 가집니다. 왜냐하면 자바스크립트는 유연한 언어이기 때문에, 함수의 용도가 명확히 정해져 있지 않습니다. 따라서 일반 함수로 쓰이기 위해 형성된 함수여도 내부 구조로는 언제든 생성자함수로 사용 가능한 구조를 가집니다. 이를 보완하기 위해 등장한 것이 ES6의 화살표 함수와 메서드 용법입니다.

// 화살표 함수의 내부구조
const hi = () => 'hi';
console.dir(hi);

리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입
- 리터럴 표기법으로 생성한 객체도 생성자 함수로 생성한 객체와 본질적인 면에서 큰 차이는 없습니다.
- 결과적으로 모든 프로토타입의 종점에는 Object.prototype이 있습니다.

프로토타입 체인
자바스크립트는 객체의 프로퍼티(메서드)에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로터티를 순차적으로 검색합니다. 이를 프로토타입 체인이라 한다. 프로토타입 체인은 자바스크립트가 객체지향 프로그래밍 상속을 구현하는 메커니즘이다.
프로토타입 체인의 최상위에 위치하는 객체는 언제나 Object.prototype입니다. 따라서 모든 객체는 Object.prototype
을 상속받습니다. 따라서 Object.prototype을 프로타입 체인의 종점 (end of prototype chain)이라 합니다. Object.prototype [[Prototype]] 내부 슬롯의 값은 null입니다.
이러한 프로토타입 체인은 상속과 프로퍼티 검색을 위함 메커니즘입니다.
반면에 스코프 체인은 식별자 검색을 위한 메커니즘입니다. 이둘을 혼동해서는 안됩니다.
오버라이딩과 프로퍼티 섀도잉
- 오버라이딩 : 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식
- 프로퍼티 섀도잉 : 상속 관계에 의해 프로퍼티가 가려지는 현상
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


instanceof
연산자
-
생성자 함수의 prototype에 바인딩 된 객체가 프로토타입 체인 상에 존재하는지 확인합니다.
// 생성자 함수 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 // Person.prototype이 me 객체의 프로토타입 체인 상에 존재하지 않기 때문에 false로 평가된다. console.log(me instanceof Person); // false // Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다. console.log(me instanceof Object); // true
정적 프로퍼티/메서드
- 생성자 함수로 인스턴스를 호출하지 않아도 참고/호출할 수 있는 프로퍼티/메서드를 의미합니다.
// 생성자 함수
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

프로퍼티 존재 확인
- in 연산자 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인합니다.
const person = {
name: 'Lee',
address: 'Seoul',
};
// person 객체에 name 프로퍼티가 존재한다.
console.log('name' in person); // true
// person 객체에 address 프로퍼티가 존재한다.
console.log('address' in person); // true
// person 객체에 age 프로퍼티가 존재하지 않는다.
console.log('age' in person); // false
console.log('toString' in person); // true
이외에도 Reflect.has
, Object.prototype.hasOwnProperty
메서드를 사용해도 객체에 특정 프로퍼티가 존재하는지 확인할 수 있습니다.
이때 Object.prototype.hasOwnProperty
메서드는 상속받은 프로퍼티 키인 경우 false를 반환한다.
const person = { name: 'Lee' };
console.log(Reflect.has(person, 'name')); // true
console.log(Reflect.has(person, 'toString')); // true
console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('age')); // false
console.log(person.hasOwnProperty('toString')); // false
프로퍼티 열거
- for … in 문
객체의 모든 프로퍼티를 순회하여 열거 하려면 for … in문을 사용합니다.
for (변수선언문 in 객체) { … }
for … in 문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 어트리뷰트 [[Enumerable]]의 값이 true인 프로퍼티를 순회하며 열거합니다.
const person = {
name: 'Lee',
address: 'Seoul',
__proto__: { age: 20 },
};
for (const key in person) {
console.log(key + ': ' + person[key]);
}
// name: Lee
// address: Seoul
// age: 20
-
Object.keys/values/entries 메서드
-
Object.keys
는 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환합니다.const person = { name: 'Lee', address: 'Seoul', __proto__: { age: 20 }, }; console.log(Object.keys(person)); // ["name", "address"]
-
Object.values
는 객체 자신의 열거 가능한 프로퍼티 값을 배열로 반환합니다.console.log(Object.values(person)); // ["Lee", "Seoul"]
-
Object.entries
는 객체 자신의 열거 가능한 프로퍼티 키와 값의 쌍의 배열을 배열에 담아 반환합니다.console.log(Object.entries(person)); // [["name", "Lee"], ["address", "Seoul"]]
-
참조
- 생성자함수
- 객체지향
- 상속
- 프로토타입
- - 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입
- - 프로토타입 체인
- - 오버라이딩과 프로퍼티 섀도잉
- - `instanceof` 연산자
- - 정적 프로퍼티/메서드
- - 프로퍼티 존재 확인
- - 프로퍼티 열거
- 참조