- 자바스크립트는 명령형 imperative, 함수형 functional, 프로토타입 기반 prototype-based, 객체지향 프로그래밍 OOP; Object Oriented Programming을 지원하는 멀티 패러다임 프로그래밍 언어
- ES6에서 클래스 도입. 클래스는 생성자 함수보다 엄격하며 생성자 함수에서는 제공하지 않는 기능도 제공
객체지향 프로그래밍
- 객체지향 프로그래밍은 프로그램을 명령어 또는 함수의 목록으로 보느 전통적인 명령형 프로그래밍 imperative programming의 절차지향적 관점에서 벗어나 여러 개의 독립적인 단위, 즉 객체 object의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임
- 실체는 특징이나 성질을 나타내는 속성 attribute/property를 가지고 있고, 다양한 속성 중에서 프로그램에 필요한 속성만 간추려 내어 표현하는 것을 추상화 abstraction라고 함
- 객체: 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료구조
- 객체 object
- 프로퍼티 property: 객체의 상태 state를 나타내는 데이터
- 메서드 method: 객체의 상태 데이터를 조작할 수 있는 동작 behavior
const circle = {
radius: 5,
getDiameter() {
return 2 * this.radius;
},
};
console.log(circle); // { radius: 5, getDiameter: [Function: getDiameter] }
console.log(circle.radius); // 5
console.log(circle.getDiameter()); // 10
상속과 프로토타입
- 상속 inheritance: 어떤 객체의 프로퍼티 또는 메소드를 다른 객체가 상속 받아 그대로 사용할 수 있는 것
- 자바스크립트는 프로토타입 prototype 기반으로 상속을 구현
- 상속 받을 경우 생성자 함수가 생성한 모든 인스턴스는 자신의 프로토타입, 즉 상위(부모) 객체 역할을 하는 생성자.prototype의 모든 프로퍼티와 메서드를 상속 받음
- 상속은 코드의 재사용 관점에서 매우 유용
// 생성자 함수
function Circle(radius) {
this.radius = radius;
this.getArea = function () {
return Math.PI * this.radius ** 2;
};
}
// 반지름이 1인 인스턴스 생성
const circle1 = new Circle(1);
// 반지름이 2인 인스턴스 생성
const circle2 = new Circle(2);
/*
Circle 생성자 함수는 인스턴스를 생성할 때마다 동일한 동작을 하는 getArea 메서드를 중복 생성, 모든 인스턴스가 중복 소유
각 Circle의 특이한 속성인 radius가 아닌, 공통으로 갖는 getArea 메서드는 공유해서 사용하는 것이 바람직
*/
console.log(circle1.getArea === circle2.getArea); // false
// 생성자 함수
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 메서드 상속
즉, Cirlce 생성자 함수가 생성한 모든 인스턴스는 하나의 getArea 메서드를 공유
*/
console.log(circle1.getArea === circle2.getArea); // true
프로토타입 객체
- 프로토타입은 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용
- 모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토 타입의 참조
- 객체가 생성될 때 객체 생성 방식에 따라 프로토타입이 결정되고 [[Prototype]]에 저장
- 객체 리터럴에 의해 생성된 객체의 프로토타입: Object.prototype
- 생성자 함수에 의해 생성된 객체의 프로토타입: 생성자 함수의 prototype 프로퍼티에 바인딩 되어 있는 객체
- 모든 객체는 하나의 프로토타입을 갖음
- 모든 프로토타입은 생성자 함수와 연결되어 있음
- [[Prototype]] 내부 슬롯에는 직접 접근할 수 없지만 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입, 즉 자신의 [[Prototype]] 내부 슬롯이 가리키는 프로토타입에 간접적으로 접근 가능
- 프로토타입은 자신의 contructor 프로퍼티를 통해 생성자 함수에 접근 가능
- 생성자 함수는 자신의 prototype 프로퍼티를 통해 프로토타입에 접근 가능
__proto__ 접근자 프로퍼티
- 모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토 타입, 즉 [[Prototype]] 내부 슬롯에 간접적으로 접근 가능
__proto__는 접근자 프로퍼티다
- 내부 슬롯은 프로퍼티가 아니며 원칙적으로 직접적으로 접근할 수 없음
- 접근자 프로퍼티는 자체적으로는 값을 갖지 않고 다른 데이터의 값을 읽거나 저장할 때 사용하는 접근자 함수, 즉 [[Get]], [[Set]] 프로퍼티 어트리뷰트로 구성된 프로퍼티
const obj = {};
const parent = { x: 1 };
// getter 함수인 get __proto__가 호출되어 obj 객체의 프로토타입을 취득
console.log(obj.__proto__); // [Object: null prototype] {}
// setter 함수인 set __proto__가 호출되어 obj 객체의 프로토타입을 교체
obj.__proto__ = parent;
console.log(obj.x); // 1
__proto__ 접근자 프로퍼티는 상속을 통해 사용
- __proto__ 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype의 프로퍼티
- 모든 객체는 상속을 통해 Object.prototype.__proto__ 접근자 프로퍼티 사용 가능
const person = { name: "Jin" };
// person 객체는 __proto__ 프로퍼티를 소유하지 않음
console.log(person.hasOwnProperty("__proto__")); // false
// __proto__ 프로퍼티는 모든 객체의 ㅡㅍ로토타입 객체인 Object.prototype의 접근자 프로퍼티
console.log(Object.getOwnPropertyDescriptor(Object.prototype, "__proto__"));
// {enumerable: false, configurable: true, get: ƒ, set: ƒ}
// 모든 객체는 Object.prototype의 접근자 프로퍼티 __proto__를 상속받아 사용 가능
console.log({}.__proto__ === Object.prototype); // true
__proto__ 접근자 프로퍼티를 통해 프로토타입에 접근하는 이유
- 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위함
- 프로토타입은 단방향 링크드 리스트로 구현되어야 함
const parent = {};
const child = {};
// child의 프로토타입을 parent로 지정
child.__proto__ = parent;
// parent의 프로토타입을 child로 지정 (상호 참조에 의한 프로토타입 체인 생성)
parent.__proto__ = child; // TypeError: Cyclic __proto__ value
__proto__ 접근자 프로퍼티를 코드 내에서 직접 사용하는 것은 권장하지 않는다
- 모든 객체가 __proto__ 접근자 프로퍼티를 사용할 수 있는 것은 아님
- 직접 상속을 통해 Object.prototype을 상속 받지 않는 객체를 생성할 수도 있기 때문에 __proto__ 접근자 프로퍼티를 사용할 수 없는 경우가 있음
- __proto__ 접근자 프로퍼티 대신
- Object.getPrototypeOf 메서드: 프로토타입의 참조를 취득하고 싶은 경우 (ES5)
- Object.setPrototypteOf 메서드: 프로토 타입을 교체하고 싶은 경우 (ES6)
// obj는 프로토타입 체인의 종점
const obj = Object.create(null);
// obj는 Object.__proto__를 상속받을 수 없음
console.log(obj.__proto__); // undefined
// 따라서 __proto__보다 Object.getPrototypeOf 메서드를 사용 권장
console.log(Object.getPrototypeOf(obj)); // null
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
console.log(obj.__proto__ === parent); // true
함수 객체의 prototype 프로퍼티
- 함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로타입을 가리킴
- prototype 프로퍼티는 생성자 함수가 생성할 객체(인스턴스)의 프로타입을 가리키므로 non-constructor인 화살표 함수와 ES6 메서드 축약 표현을 정의한 메서드는 prototype 프로퍼티를 소유하지 않으며 프로토타입도 생성하지 않음
- 모든 객체가 가지고 있는 (엄밀히 말하면 Object.prototype으로부터 상속받은) __proto__ 접근 프로퍼티와 함수 객체만이 가지고 있는 prototype 프로퍼티는 결국 동일한 프로토타입을 가리킴. 하지만 이들 프로퍼티를 사용하는 주체가 다름
// 함수 객체는 prototype 프로퍼티를 소유
console.log(function () {}.hasOwnProperty("prototype")); // true
// 일반 객체는 prototype 프로퍼티를 소유 x
console.log({}.hasOwnProperty("prototype")); // false
// 화살표 함수는 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-contructor는 프로토타입을 생성하지 않음
console.log(obj.foo.prototype); // undefined
구분 | 소유 | 값 | 사용 주체 | 사용 목적 |
__proto__ 접근자 프로퍼티 |
모든 객체 | 프로토타입의 참조 | 모든 객체 | 객체가 자신의 프로토타입에 접근 또는 교체하기 위해 사용 |
prototype 프로퍼티 |
constructor | 프로토타입의 참조 | 생성자 함수 | 생성자 함수가 자신이 객체(인스턴스)의 프로토타입을 할당하기 위해 사용 |
// 생성자 함수
function Person(name) {
this.this = name;
}
const me = new Person("Jin");
// 결국 Person.prototype과 __proto__는 결국 동일한 가리킴
console.log(Person.prototype == me.__proto__); // true
프로토타입의 constructor 프로퍼티와 생성자 함수
- 모든 프로토타입은 constructor 프로퍼티를 갖음
- contructor 프로퍼티는 prototype 프로토타입 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킴
- 이 연결은 생성자 함수가 생성될 때, 즉 함수 객체가 생성될 때 이뤄짐
- me 객체에는 constructor가 없지만 프로토타입인 Person.prototype의 constructor 프로퍼티를 상속 받아 사용 가능
리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입
- 리터럴 표기법에 의해 생성된 객체는 생성자 함수가 생성한 객체는 아님
- 하지만 객체 리터럴 contructor 프로퍼티로 확인한 생성자 함수는 Object 생성자 함수
- 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재
- 리터럴 표기법으로 생성한 객체도 생성자 함수로 생성한 객체와 본질적인 면에서 큰 차이는 없음
// obj1 객체를 생성한 생성자 함수는 Object
const obj1 = new Object();
console.log(obj1.constructor === Object); // true
// obj2 객체는 객체 리터럴로 생성했으나 생성자 함수는 Object
const obj2 = {};
console.log(obj2.constructor === Object); // true
리터럴 표기법 | 생성자 함수 | 프로토타입 |
객체 리터럴 | Object | Object.prototype |
함수 리터럴 | Function | Function.prototype |
배열 리터럴 | Array | Array.prototype |
정규 표현식 리터럴 | RegExp | RegExp.prototype |
프로토타입의 생성 시점
- 프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성 (프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재)
사용자 정의 생성자 함수와 프로토타입 생성 시점
- 생성자 함수로서 호출할 수 있는 함수, 즉 constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성
- 생성자 함수로서 호출할 수 없는 함수, 즉 non-constructor는 프로토타입이 생성되지 않음
// 함수 정의(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
빌트인 생성자 함수와 프로토타입 생성 시점
- Object, String, Number, Function, Array, RegExp, Date, Promise 같은 빌트인 생성자 함수도 일반 함수와 마찬가지로 빌트인 생성자 함수가 실행되는 시점에 프로토타입이 생김
- 모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성
- 생성된 프로토타입은 빌트인 생성자 함수의 prototype 프로퍼티에 바인딩
- 객체가 생성되기 이전에 생성자 함수와 프로토타입은 이미 객체화되어 존재
- 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면 프로토 타입은 생성된 객체의 [[Prototype]] 내부 슬롯에 할당
// 객체가 생성되기 이전에 생성자 함수와 프로토타입은 이미 객체화되어 존재
// 브라우저 환경에서
console.log(window.Object === Object) // true
객체 생성 방식과 프로토타입의 결정
- 객체의 생성 방식
- 객체 리터럴
- Object 생성자 함수
- 생성자 함수
- Object.create 메서드
- 클래스(ES6)
- 세부적인 객체 생성 방식의 차이는 있으나 추상 연산 OrdinaryObjectCreate에 생성된다는 공통점이 있음
- OrdinaryObjectCreate
- 필수적으로 자신이 생성할 객체의 프로토타입을 인수로 전달 받고, 추가할 프로퍼티 목록을 옵션으로 전달
- 빈 객체를 생성한 후, 객체에 추가할 프로퍼티 목록이 인수로 전달될 경우 프로퍼티를 객체에 추가
- 인수로 전달 받은 프로토타입을 자신이 생성한 객체의 [[Prototype]] 내부 슬롯에 할당
- 객체 반환
객체 리터럴에 의해 생성된 객체의 프로토타입
const obj = { x: 1 };
// 객체 리터럴에 의해 생성된 obj 객체는 Object.prototype을 상속 받음
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty("x")); // true
Object 생성자 함수에 의해 생성된 객체의 프로토타입
- 객체 리터럴 방식은 객체 리터럴 내부에 프로퍼티를 추가하지만, Object 생성자 함수 방식은 일단 빈 객체를 생성한 후 프로퍼티 추가
const obj = new Object();
obj.x = 1;
// Object 생성자 함수에 의해 생성된 obj 객체는 Object.prototype을 상속 받음
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty("x")); // true
생성자 함수에 의해 생성된 객체의 프로토타입
- 생성자 함수에 의해 생성되는 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체
- 표준 빌트인 객체인 Object 생성자 함수와 더불어 생성된 프로토타입 Object.prototype은 다양한 빌트인 메서드 (hasOwnProperty, etc) 등 을 가짐 / 사용자 정의 생성자 함수 Person과 더불어 생성된 Person.prototype의 프로퍼티는 constructor 뿐임
function Person(name) {
this.name = name;
}
const me = new Person("Jin");
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hi my name is ${this.name}`);
};
const me = new Person("Jin");
const you = new Person("Park");
me.sayHello(); // Hi my name is Jin
you.sayHello(); // Hi my name is Park
프로토타입 체인
- 프로토타입 체인: 자바스크립트는 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때, 해당 객체에 접근하려는 프로퍼티가 없으면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색
- 프로토타입 체인은 객체 지향 프로그래밍의 상속과 프로퍼티 검색을 위한 메카니즘
- Object.prototype은 프로토타입 체인의 종점 end of prototype chain
- Object.prototype의 [[Prototype]] 내부 슬롯 값은 null
- 프로토타입 체인의 종점에서도 프로퍼티 값을 검색할 수 없는 경우 undefined 반환 (에러는 발생 x)
- 참고: 스코프 체인은 식별자 검색을 위한 메카니즘
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hi my name is ${this.name}`);
};
const me = new Person("Jin");
// hasOwnProperty는 Object.prototype의 메서드인데 동작
console.log(me.hasOwnProperty("name")); // true
// 이것은 me 객체가 Person.prototype뿐만 아니라 Object.prototype도 상속 받았다는 것을 나타냄
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true
오버라이딩과 프로퍼티 섀도잉
- 프로퍼티 섀도잉: property shadowing: 상속 관계에 의해 프로퍼티가 가려지는 현상
- 오버라이딩 overriding: 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식
- 오버로딩 overloading: 함수의 이름은 동일하지만 매개변수의 타입 또는 개수가 다른 메서드를 구현하고 매개변수에 의해 메서드를 구별하여 호출하는 방식. 자바스크립트는 오버로딩을 지원하지는 않지만 arguments 객체를 통하여 구현 가능
- 하위 객체를 통해 프로토타입의 프로퍼티를 변경 또는 삭제하는 것은 불가능, (get 엑세스는 허용되나 set 엑세스는 허용되지 않음)
- 프로토타입 프로퍼티를 변경하려면 하위 객체를 통해 프로토타입 체인으로 접근하는 것이 아니라 프로토타입에 직접 접근해야 함
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("Jin");
// 인스턴스 메서드
me.sayHello = function () {
console.log(`안녕! 내 이름은 ${this.name}`);
};
// 인스턴스 메서드가 호출됨. 프로토타입 메서드는 인스턴스 메서드에 의해 가려짐
me.sayHello(); // 안녕! 내 이름은 Jin
// 인스턴스 메서드 삭제
delete me.sayHello;
me.sayHello(); // Hi! my name is Jin
// 하위 객체를 통하여 프로토타입 프로퍼티를 변경 또는 삭제는 불가능
delete me.sayHello;
me.sayHello(); // Hi! my name is Jin 여전히 프로토타입 메서드 호출
// 프로토타입 프로퍼티를 변경하려면 프로토타입에 직접 접근
delete Person.prototype.sayHello;
me.sayHello(); // TypeError: me.sayHello is not a function
프로토타입의 교체
- 프로토타입은 임의의 다른 객체로 변경 가능
- 객체의 간의 상속 관계를 동적으로 변경 가능
const Person = (function () {
function Person(name) {
this.name = name;
}
// 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
// 자바스크립트가 암묵적으로 생성한 prototype을 객체 리터럴로 교체
Person.prototype = {
sayHello() {
console.log(`Hey, my name is ${this.name}`);
},
};
return Person;
})();
const me = new Person("Jin");
// constructor 와 생성자 함수간의 연결을 살려보자
const Person = (function () {
function Person(name) {
this.name = name;
}
// 생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체
// 자바스크립트가 암묵적으로 생성한 prototype을 객체 리터럴로 교체
Person.prototype = {
// constructor 프로퍼티와 생성자 함수간의 연결 설정
constructor: Person,
sayHello() {
console.log(`Hey, my name is ${this.name}`);
},
};
return Person;
})();
const me = new Person("Jin");
// constructor 프로퍼티가 생성자 함수를 가리킴
console.log(me.constructor === Person); // true
console.log(me.constructor === Object); // false
인스턴스에 의한 프로토타입의 교체
- 생성자 함수의 prototype 프로퍼티에 다른 임의의 객체를 바인딩 하는 것은 미래에 생성할 인스턴스의 프로토타입을 교체하는 것
- 인스턴스의 __proto__ 접근자 프로퍼티를 통해 프로토타입을 교체하는 것은 이미 생성된 객체의 프로토타입을 교체하는 것
- Object .setPrototypeOf 메서드 또는 인스턴스의 __proto__ 접근자 프로퍼티를 통해 인스턴스의 프로토타입 교체 가능
- 프로토타입은 직접 변경하지 않는 것을 권장 (객체 관의 상속 관계를 동적으로 관리하는 것은 번거로움)
- 상속 관계를 인위적으로 설정하려면 직접 상속 권장
- ES6에 도입된 클래스를 사용하면 간편하고 직관적으로 상속 관계 구현 가능
function Person(name) {
this.name = name;
}
const me = new Person("Jin");
// 프로토 타입으로 교체할 객체
const parent = {
sayHello() {
console.log(`Hi! my name is ${this.name}`);
},
};
// me 객체의 프로토타입을 parent로 교체
// me.__proto__ = parent; 와 동일하게 동작
Object.setPrototypeOf(me, parent);
me.sayHello(); // Hi! my name is Jin
// 당연히 생성자 함수 Person의 prototype은 parent가 아닌 Person.prototype임
console.log(Person.prototype);
/*
{constructor: ƒ}
constructor: ƒ Person(name)
[[Prototype]]: Object
*/
// 인스턴스 prototype이 교체되어서 생성자함수와 prototype간의 연결이 끊어짐
// 인스턴스의 constructor는 프로토타입 체인을 따라 Object 됨
console.log(me.constructor === Object); // true
instance of 연산자
- 객체 instance of 생성자 함수
- 우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true, 그렇지 않은 경우 false 반환
- 생성자 함수를 찾아주는 것이 아니라 생성자 함수의 prototype이 프로토타입 체인 상에 존재하는지 확인
const Person = (function () {
function Person(name) {
this.name = name;
}
// 프로토타입 교체
Person.prototype = {
sayHello() {
console.log(`Hi! my name is ${this.name}`);
},
};
return Person;
})();
const me = new Person("Jin");
// constructor 프로퍼티와 생성자 함수 간의 연결이 파괴되어도 intanceof 에는 영향 x
console.log(me.constructor === Person); // false
// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true
console.log(me instanceof Person); // true
// Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true
console.log(me instanceof Object); // true
직접 상속
Object.create에 의한 직접 상속
- Object.create 메서드는 명시적으로 프로토타입을 지정하여 새로운 객체 생성
- 첫 번째 매개변수: 생성할 객체의 프로토타입으로 지정할 객체 (null로 할 경우 프로토타입 종점에 위치)
- 두 번재 매개변수: 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터객체로 이뤄진 객체 (옵션; 생략가능)
- 장점
- new 연산자가 없어도 객체 생성 가능
- 프로토타입을 지정하면서 객체 생성 가능
- 객체 리터럴에 의해 생성된 객체도 상속 가능
- 참고: ESLint에서는 Object.prototype 빌트인 메서드를 직접 호출하는 것을 권장하지 않음. Object.create(null)처럼 프로토타입 종점에 위치하는 객체를 생성 가능하기 때문. 간접적으로 호출 권장
// 프로토타입이 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 = {}; 와 동일
obj = Object.create(Object.prototype);
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
// obj = {x: 1} 와 동일
obj = Object.create(
Object.create(Object.prototype, {
x: { value: 1, writable: true, enumerable: true, configurable: true },
})
);
// 임의의 객체 직접 상송
const parent = { x: 10 };
obj = Object.create(parent);
console.log(obj.x); // 10
console.log(Object.getPrototypeOf(obj) === parent); // true
// 생성자 함수
function Person(name) {
this.name = name;
}
obj = Object.create(Person.prototype);
obj.name = "Jin";
console.log(obj.name); // Jin
console.log(obj.__proto__ === Person.prototype); // true
// 프로토타입이 null인 객체 생성
const obj = Object.create(null);
obj.a = 1;
// console.log(obj.hasOwnProperty("a"));
// TypeError: obj.hasOwnProperty is not a function
// 간접 호출
console.log(Object.prototype.hasOwnProperty.call(obj, "a")); // true
객체 리터럴 내부에서 __proto__에 의한 직접 상속
- ES6에서는 __proto__ 접근자 프로퍼티를 사용하여 직접 상속 구현
const myProto = { x: 10 };
// 객체 리터럴에 의해 객체를 생성하면서 프로토타입을 지정하여 직접 상속 받을 수 있음
const obj = {
y: 20,
// 객체 직접 상속
__proto__: myProto,
};
/* 아래 코드와 동일하게 동작
const obj = Object.create(myProto, {
y: { value: 20, writable: true, enumerable: true, configurable: true },
});
*/
정적 프로퍼티 / 메서드
- 정적 static 프로퍼티/메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드를 뜻함
- 정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조/호출 당연히 불가능
- 사실상 일반 함수의 프로퍼티랑 메서드
- 프로토타입 프로퍼티/메서드를 표기할 때 prototype을 #으로 표기하는 경우도 있음
- 예시: Object.prototype.isPrototypeOf를 Object#isPrototypeof 로 표기
// 생성자 함수
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("static method");
};
const me = new Person("Jin");
// 생성자 함수에 추가한 정적 프로퍼티/메서드는 생성자 함수로 참조/호출
Person.staticMethod(); // static method
// 정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조/호출 불가
// 인스턴스로 참조/호출할 수 있는 프로퍼티/메서드는 프로토타입 체인 상에 존재해야 함
me.staticMethod(); // TypeError: me.staticMethod is not a function
프로퍼티 존재 확인
in 연산자
- in 연산자는 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인
- in 연산자는 확인 대상 객체의 프로퍼티 뿐만 아니라 확인 대상 객체가 상속 받은 모든 포로토타입의 프로퍼티를 확인
- ES6에서 도입된 Reflect.has 메서드는 in 연산자와 동일하게 동작
const person = {
name: "Jin",
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
// person에는 Object.prototype에서 상속받은 toString이라는 프로퍼티 존재
console.log("toString" in person); // true
// ES6에 도입된 Relect.has 메서드는 in과 동일하게 동작
console.log(Reflect.has(person, "name")); // true
console.log(Reflect.has(person, "toString")); // true
Object.prototype.hasOwnProperty 메서드
- 인수로 전달 받은 프로퍼티가 객체 고유의 프로퍼티 키인 경우에만 true 반환 (상속 받은 프로퍼티는 false)
const person = {
name: "Jin",
address: "Seoul",
};
// Object.prototype.hasOwnProperty는 객체 고유의 프로퍼티인 경우에만 true 반환
console.log(person.hasOwnProperty("name")); // true
console.log(person.hasOwnProperty("age")); // false
console.log(person.hasOwnProperty("toString")); // false
프로퍼티 열거
for ... in 문
- for (변수선언문 in 객체) {...}
- 객체의 모든 프로퍼티를 순회하며 열거 enumeration하려면 for ... in 문을 사용
- for ... in 문은 객체의 상속 받은 프로퍼티까지 열거
- [[Enumerable]] 값이 false인 값은 열거하지 않음
- for ... in 문은 프로퍼티 키가 심벌인 프로퍼티는 열거하지 않음
- for ... in 문은 프로퍼티를 열거할 때 순서를 보장하진 않음 (대부분의 모던 브라우저는 프로퍼티 키에 대해 정렬을 실시)
- 배열에는 for 문, for ... of, Array.prototype.forEach 메서드 사용 권장
const Person = (function () {
const Person = function (name) {
this.name = name;
};
Person.prototype.sayHello = function () {
console.log(`Hello! my name is ${this.name}`);
};
return Person;
})();
const me = new Person("Jin");
me.sayBye = function () {
console.log("Bye Bye");
};
// for ... in 문의 변수 person 객체의 프로퍼티 키
for (const key in me) {
console.log(key);
}
/*
name
sayBye
sayHello
true
*/
// me은 프로토타입의 프로퍼티로 toString을 갖고 있음
// toString은 [[Enumberable]] 값이 false라서 열거되지 않음
console.log("toString" in me); // true
console.log(Object.getOwnPropertyDescriptor(Object.prototype, "toString"));
// {writable: true, enumerable: false, configurable: true, value: ƒ}
// 배열
const arr = [1, 2, 3];
arr.x = 10; // 배열도 객체이므로 프로퍼티를 가질 수 있음
for (const i in arr) {
// 프로퍼티 x도 출력
console.log(arr[i]); // 1 2 3 10
}
// arr.length는 3
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]); // 1 2 3
}
// forEach 메서드는 요소가 아닌 프로퍼티는 제외
arr.forEach((v) => console.log(v)); // 1 2 3
// for ... of는 변수 선언문에서 선언한 변수에 키가 아닌 값을 할당
for (const value of arr) {
console.log(value); // 1 2 3
}
Object.keys/values/entries 메서드
- Object.keys 메서드는 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환
- Object.values 메서드는 객체 자신의 열거 가능한 프로퍼티 값을 배열로 반환 (ES8)
- Object.entries 메서드는 객체 자신의 열거 가능한 프로퍼티 키와 값의 쌍의 배열 을 배열에 담아 반환 (ES8)
const me = {
name: "Jin",
address: "Seoul",
__proto__: { age: 29 },
};
console.log(Object.keys(me)); // [ 'name', 'address' ]
console.log(Object.values(me)); // [ 'Jin', 'Seoul' ]
console.log(Object.entries(me)); // [ [ 'name', 'Jin' ], [ 'address', 'Seoul' ] ]
Object.entries(me).forEach(([key, value]) => console.log(key, value));
/*
name Jin
address Seoul
*/
'Javascript > 모던 자바스크립트 DeepDive' 카테고리의 다른 글
[모던 자바스크립트 Deep Dive] 21 빌트인 객체 (1) | 2023.12.16 |
---|---|
[모던 자바스크립트 Deep Dive] 20 strict mode (0) | 2023.12.14 |
[모던 자바스크립트 Deep Dive] 18 함수와 일급 객체 (0) | 2023.12.13 |
[모던 자바스크립트 Deep Dive] 17 생성자 함수에 의한 객체 생성 (0) | 2023.12.12 |
[모던 자바스크립트 Deep Dive] 16 프로퍼티 어트리뷰트 (0) | 2023.12.12 |