프로퍼티 어트리뷰트
내부 슬롯과 내부 메서드란 JS 엔진의 구현 알고리즘을 설명하기 위해 사용되는 의사(pseudo) 프로퍼티와 의사(pseudo) 메서드이다. ES 사양에서 이중 대괄호([[…]])로 감싼 이름들이 그것이다
const o = {};
// 내부 슬롯은 자바스크립트 엔진의 내부 로직이므로 직접 접근할 수 없다.
o.[[Prototype]] // -> Uncaught SyntaxError: Unexpected token '['
// 단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단을 제공하기는 한다.
o.__proto__ // -> Object.prototype
모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이는 __proto__를 통해 간접적으로 접근할 수 있지만 권장되지 않는다
const person = {
name: 'Lee'
};
// 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 반환한다.
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// {value: "Lee", writable: true, enumerable: true, configurable: true}
대신 위와 같이 프로퍼티 어트리뷰트 정보(프로퍼티의 상태)를 제공하는 프로퍼티 디스크립터 객체를 볼 수 있다
console.log(Object.getOwnPropertyDescriptors(person));
/*
{
name: {value: "Lee", writable: true, enumerable: true, configurable: true},
age: {value: 20, writable: true, enumerable: true, configurable: true}
}
*/
객체의 모든 프로퍼티 디스크립터 객체를 한 번에 볼 수도 있다
내부의 프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 분리할 수 있는데, 지금까지 살펴본 프로퍼티들은 모두 데이터 프로퍼티로 프로퍼티가 생성될 때 자동 정의된다
접근자(accessor) 프로퍼티는 자체적으로 값을 가지지 않고 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티다
const person = {
// 데이터 프로퍼티
firstName: 'Ungmo',
lastName: 'Lee',
// fullName은 접근자 함수로 구성된 접근자 프로퍼티다.
// getter 함수
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
// setter 함수
set fullName(name) {
// 배열 디스트럭처링 할당: "31.1 배열 디스트럭처링 할당" 참고
[this.firstName, this.lastName] = name.split(' ');
}
};
// 데이터 프로퍼티를 통한 프로퍼티 값의 참조.
console.log(person.firstName + ' ' + person.lastName); // Ungmo Lee
// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
// 접근자 프로퍼티 fullName에 값을 저장하면 setter 함수가 호출된다.
person.fullName = 'Heegun Lee';
console.log(person); // {firstName: "Heegun", lastName: "Lee"}
// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
// 접근자 프로퍼티 fullName에 접근하면 getter 함수가 호출된다.
console.log(person.fullName); // Heegun Lee
// firstName은 데이터 프로퍼티다.
// 데이터 프로퍼티는 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]] 프로퍼티 어트리뷰트를 갖는다.
let descriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(descriptor);
// {value: "Heegun", writable: true, enumerable: true, configurable: true}
// fullName은 접근자 프로퍼티다.
// 접근자 프로퍼티는 [[Get]], [[Set]], [[Enumerable]], [[Configurable]] 프로퍼티 어트리뷰트를 갖는다.
descriptor = Object.getOwnPropertyDescriptor(person, 'fullName');
console.log(descriptor);
// {get: ƒ, set: ƒ, enumerable: true, configurable: true}
접근자 함수는 위와 같이 활용되며 getter/setter 함수라고도 부른다
// 일반 객체의 __proto__는 접근자 프로퍼티다.
Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}
// 함수 객체의 prototype은 데이터 프로퍼티다.
Object.getOwnPropertyDescriptor(function() {}, 'prototype');
// {value: {...}, writable: true, enumerable: false, configurable: false}
데이터 프로퍼티와 접근자 프로퍼티는 위와 같이 구별할 수 있다
const person = {};
Object.defineProperties(person, {
// 데이터 프로퍼티 정의
firstName: {
value: 'Ungmo',
writable: true,
enumerable: true,
configurable: true
},
lastName: {
value: 'Lee',
writable: true,
enumerable: true,
configurable: true
},
// 접근자 프로퍼티 정의
fullName: {
// getter 함수
get() {
return `${this.firstName} ${this.lastName}`;
},
// setter 함수
set(name) {
[this.firstName, this.lastName] = name.split(' ');
},
enumerable: true,
configurable: true
}
});
person.fullName = 'Heegun Lee';
console.log(person); // {firstName: "Heegun", lastName: "Lee"}
Object.defindeProperties()로 데이터/접근자 프로퍼티를 직접 정의할 수 있다
객체의 프로퍼티는 재할당 없이 직접 변경할 수 있지만, 필요에 따라 객체의 변경을 방지할 수 있으며, 메서드에 따라 금지하는 강도가 다르다
const person = {
name: 'Lee',
address: { city: 'Seoul' }
};
// 얕은 객체 동결
Object.freeze(person);
// 직속 프로퍼티만 동결한다.
console.log(Object.isFrozen(person)); // true
// 중첩 객체까지 동결하지 못한다.
console.log(Object.isFrozen(person.address)); // false
person.address.city = 'Busan';
console.log(person); // {name: "Lee", address: {city: "Busan"}}
보통 Object.freeze()로 객체를 동결시키는데, 이는 중첩 객체까지 동결시키지는 못한다
* 전부 동결시키려면 모든 프로퍼티에 대해 재귀적으로 동결 메서드를 호출해야 함
생성자 함수에 의한 객체 생성
// 빈 객체의 생성
const person = new Object();
// 프로퍼티 추가
person.name = 'Lee';
person.sayHello = function () {
console.log('Hi! My name is ' + this.name);
};
console.log(person); // {name: "Lee", sayHello: ƒ}
person.sayHello(); // Hi! My name is Lee
// String 생성자 함수에 의한 String 객체 생성
const strObj = new String('Lee');
console.log(typeof strObj); // object
console.log(strObj); // String {"Lee"}
// Number 생성자 함수에 의한 Number 객체 생성
const numObj = new Number(123);
console.log(typeof numObj); // object
console.log(numObj); // Number {123}
// Boolean 생성자 함수에 의한 Boolean 객체 생성
const boolObj= new Boolean(true);
console.log(typeof boolObj); // object
console.log(boolObj); // Boolean {true}
// Array 생성자 함수에 의한 Array 객체(배열) 생성
const arr = new Array(1, 2, 3);
console.log(typeof arr); // object
console.log(arr); // [1, 2, 3]
// RegExp 생성자 함수에 의한 RegExp 객체(정규 표현식) 생성
const regExp = new RegExp(/ab+c/i);
console.log(typeof regExp); // object
console.log(regExp); // /ab+c/i
// Date 생성자 함수에 의한 Date 객체 생성
const date = new Date();
console.log(typeof date); // object
console.log(date); // Mon May 04 2020 08:36:33 GMT+0900 (대한민국 표준시)
생성자 함수로 다양한 객체(인스턴스)를 생성하고 조작할 수 있다 (모두 객체 타입이다)
// 생성자 함수
function Circle(radius) {
// 생성자 함수 내부의 this는 생성자 함수가 생성할 인스턴스를 가리킨다.
this.radius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
}
// 인스턴스의 생성
const circle1 = new Circle(5); // 반지름이 5인 Circle 객체를 생성
const circle2 = new Circle(10); // 반지름이 10인 Circle 객체를 생성
console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20
마치 템플릿(클래스)처럼 객체(인스턴스)를 간편하게 생성할 수 있다
* 이 때, this는 생성될 인스턴스의 프로퍼티
함수는 객체이지만 일반 객체와 달리 호출할 수 있다. 따라서 함수는 함수 객체만을 위해 [[Call]], [[Construct]]와 같은 내부 메서드를 추가로 가진다
function foo() {}
// 일반적인 함수로서 호출: [[Call]]이 호출된다.
foo();
// 생성자 함수로서 호출: [[Construct]]가 호출된다.
new foo();
[[Call]]과 [[Construct]]는 호출 방식에 따라 구분된다
* new 연산자와 호출하는 함수는 non-constructor가 아닌 constructor 함수
new 연산자 없이 생성자 함수가 호출되는 것을 방지하기 위한 두 가지 방법이 있다
// 생성자 함수
function Circle(radius) {
// 이 함수가 new 연산자와 함께 호출되지 않았다면 new.target은 undefined다.
if (!new.target) {
// new 연산자와 함께 생성자 함수를 재귀 호출하여 생성된 인스턴스를 반환한다.
return new Circle(radius);
}
this.radius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
}
// new 연산자 없이 생성자 함수를 호출하여도 new.target을 통해 생성자 함수로서 호출된다.
const circle = Circle(5);
console.log(circle.getDiameter());
1. new.target을 활용한 패턴
// Scope-Safe Constructor Pattern
function Circle(radius) {
// 생성자 함수가 new 연산자와 함께 호출되면 함수의 선두에서 빈 객체를 생성하고
// this에 바인딩한다. 이때 this와 Circle은 프로토타입에 의해 연결된다.
// 이 함수가 new 연산자와 함께 호출되지 않았다면 이 시점의 this는 전역 객체 window를 가리킨다.
// 즉, this와 Circle은 프로토타입에 의해 연결되지 않는다.
if (!(this instanceof Circle)) {
// new 연산자와 함께 호출하여 생성된 인스턴스를 반환한다.
return new Circle(radius);
}
this.radius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
}
// new 연산자 없이 생성자 함수를 호출하여도 생성자 함수로서 호출된다.
const circle = Circle(5);
console.log(circle.getDiameter()); // 10
2. 스코프 세이프 생성자 패턴
const str = String(123);
console.log(str, typeof str); // 123 string
const num = Number('123');
console.log(num, typeof num); // 123 number
const bool = Boolean('true');
console.log(bool, typeof bool); // true boolean
String, Number, Boolean 함수는 new 연산자 없이 호출했을 때 객체가 아닌 값을 반환한다
* Object 생성자는 new 연산자 유무와 관계없이 동일하다
본 내용은 위키북스의 '모던 자바스크립트 Deep Dive'를 바탕으로 작성되었습니다.
'Languages > JavaScript' 카테고리의 다른 글
[HUFS/GnuVil] #10 프로토타입 (0) | 2022.10.13 |
---|---|
[HUFS/GnuVil] #9 함수와 일급 객체 (0) | 2022.10.13 |
[HUFS/GnuVil] #7 전역변수의 문제점, 블록 레벨 스코프 (0) | 2022.10.02 |
[HUFS/GnuVil] #6 실행 컨텍스트, 스코프 (0) | 2022.10.02 |
[HUFS/GnuVil] #5 함수 (0) | 2022.09.20 |