프로토타입
JS는 명령형 / 함수형 / 프로토타입 기반 / 객체 지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어다. 간혹 객체지향 언어가 아니라는 오해를 받지만 JS는 클래스 기반 객체지향 프로그램 언어보다 더 강력한 프로토타입 기반의 객체지향 프로그래밍 언어이며 원시 타입의 값을 제외한 모든 값들(함수, 배열, 정규 표현식 등)은 객체다
객체지향 프로그래밍(OOP) = 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임
const circle = {
radius: 5, // 반지름
// 원의 지름: 2r
getDiameter() {
return 2 * this.radius;
},
// 원의 둘레: 2πr
getPerimeter() {
return 2 * Math.PI * this.radius;
},
// 원의 넓이: πrr
getArea() {
return Math.PI * this.radius ** 2;
}
};
console.log(circle);
// {radius: 5, getDiameter: ƒ, getPerimeter: ƒ, getArea: ƒ}
console.log(circle.getDiameter()); // 10
console.log(circle.getPerimeter()); // 31.41592653589793
console.log(circle.getArea()); // 78.53981633974483
현실의 사물이나 개념의 상태(프로퍼티)와 동작(메서드)을 추상화해 객체로 표현할 수 있다
// 생성자 함수
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
프로토타입을 기반으로 상속을 구현해 불필요한 중복을 제거할 수 있다
// obj 객체는 Object 생성자 함수로 생성한 객체가 아니라 객체 리터럴로 생성했다.
const obj = {};
// 하지만 obj 객체의 생성자 함수는 Object 생성자 함수다.
console.log(obj.constructor === Object); // true
생성자 함수가 아닌 리터럴로 객체를 생성해도 constructor로 생성자 함수를 가진다
* Object 생성자 함수로 생성한 객체와 세부 내용은 다름 (new.target, 프로퍼티 처리 등)
// 생성자 함수
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// 결국 Person.prototype과 me.__proto__는 결국 동일한 프로토타입을 가리킨다.
console.log(Person.prototype === me.__proto__); // true
생성자 함수는 생성되는 시점에 prototype 프로퍼티를 추가로 가지는데 이는 모든 객체가 가지는 __proto__ 접근자 프로퍼티와 결국 동일하지만 사용하는 주체가 다르다
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
프로토타입 체인에 의해 상위 프로토타입의 메서드를 상속받아 사용할 수 있다
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
프로토타입 체인에 같은 이름의 메서드가 있다면 오버라이딩(overriding)되어 더 이상 타고 올라가지 않는데, 이를 프로퍼티 섀도잉이라 한다
// 생성자 함수
function Person(name) {
this.name = name;
}
const me = new Person('Lee');
// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Person); // true
// Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Object); // true
instanceof 연산자로 인스턴스가 해당 객체의 프로토타입 체인 상에 존재하는지 확인할 수 있다
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
Object.create 메서드로 명시적으로 프로토타입을 지정해 객체를 생성할 수 있다
* new 연산자 없이 프로토타입을 지정하면서 객체를 생성 + 리터럴 객체도 상속 가능한 장점이 있지만 체인의 종점에 위치하는 객체를 생성할 수도 있어 ESLint에서는 권장되지 않음
// 생성자 함수
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
생성자 함수 객체가 소유한 프로퍼티/메서드를 정적 프로퍼티/메서드라고 하며, 이는 생성한 인스턴스로 참조/호출할 수 없다
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 (프로토타입 체인을 기준으로 찾는다)
console.log(Reflect.has(person, 'name')); // true
console.log(Reflect.has(person, 'toString')); // true
in 연산자 / Reflect.has 메서드는 객체의 프로토타입 체인에 특정 프로퍼티가 존재하는지 여부를 확인한다
console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('age')); // false
console.log(person.hasOwnProperty('toString')); // false
Object.prototype.hasOwnProperty 메서드는 프로토타입 체인 상이 아닌 해당 객체에 특정 프로퍼티 존재 여부를 확인한다
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
for…in 문은 객체의 프로토타입 체인 상의 프로퍼티 중에서 [[Enumerable]] 값이 true인 프로퍼티를 순회하며 열거한다
* 일부 브라우저는 열거할 때 순서 보장 X
const person = {
name: 'Lee',
address: 'Seoul',
__proto__: { age: 20 }
};
console.log(Object.keys(person)); // ["name", "address"]
console.log(Object.values(person)); // ["Lee", "Seoul"]
console.log(Object.entries(person)); // [["name", "Lee"], ["address", "Seoul"]]
Object.entries(person).forEach(([key, value]) => console.log(key, value));
/*
name Lee
address Seoul
배열로 다룰 수 있는 Object.keys/values/entries 메서드를 사용하도록 하자
본 내용은 위키북스의 '모던 자바스크립트 Deep Dive'를 바탕으로 작성되었습니다.
'Languages > JavaScript' 카테고리의 다른 글
[HUFS/GnuVil] #12 클로저 (0) | 2022.10.18 |
---|---|
[HUFS/GnuVil] #11 strict mode, 빌트인 객체, this (0) | 2022.10.13 |
[HUFS/GnuVil] #9 함수와 일급 객체 (0) | 2022.10.13 |
[HUFS/GnuVil] #8 프로퍼티 어트리뷰트, 생성자 함수에 의한 객체 생성 (0) | 2022.10.02 |
[HUFS/GnuVil] #7 전역변수의 문제점, 블록 레벨 스코프 (0) | 2022.10.02 |