자바스크립트의 내부 동작은 어떻게 이루어질까? ECMAScript에서 자바스크립트 엔진 내부 동작의 구현을 설명하기 위해 정의한 [[...]]
이중 대괄호로 감싼 이름들이 있는데, 이들을 내부 슬롯과 내부 메서드라고 한다. 내부 슬롯(Internal slot)은 상태(값)을 의미하고, 내부 메서드(Internal method)는 말그대로 메서드이기 때문에 동작을 의미한다.
위의 표는 ECMAScript 2015 스펙에서 내부 슬롯의 일부와 내부 메서드의 일부를 가져왔다.
왼쪽이 내부 슬롯이고, 오른쪽이 내부 메서드다. 표를 보면 알 수 있듯이 내부 슬롯은 Type에 해당하는 값을 가지고 있는 것을 볼 수 있고, 내부 메서드는 Signature에 해당하는 어떤 동작을 가지고 있는 것을 볼 수 있다.
내부 슬롯과 내부 메서드는 자바스크립트 엔진 내부의 로직이므로, 원칙적으로는 내부 슬롯과 내부 메서드에 직접적으로 접근할 수 있는 방법을 제공하지 않는다. 다만, 일부 내부 슬롯과 내부 메서드에 한해서 간접적으로 접근할 수 있는 방법을 제공한다.
예를 들어, 모든 객체는 [[Prototype]]
이라는 내부 슬롯을 가지고 있다. 자바스크립트 엔진 내부에 구현된 로직이므로 직접 접근할 수 없지만, [[Prototype]]
내부 슬롯은 __proto__
를 통해 간접적으로 접근할 수 있다.
const myObject = {}
myObject.[[Prototype]] // Uncaught Syntax Error!
myObject.__proto__ // Object.prototype
프로퍼티 어트리뷰트는 ECMAScript에서 프로퍼티의 상태를 정의하고 설명하기 위해 사용된다. 즉, 프로퍼티 어트리뷰트는 자바스크립트 엔진이 관리하는 내부 상태 값인 내부 슬롯과 내부 메서드로 이루어져 있다.
자바스크립트 엔진은 프로퍼티를 정의할 때 어트리뷰트를 명시하지 않으면,내부 슬롯과 내부 메서드를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.
프로퍼티 어트리뷰트는 내부 슬롯(메서드)이기 때문에 직접 접근할 수 없지만, Object.getOwnPropertyDescriptor
메서드를 사용하여 간접적으로 접근할 수 있고, 프로퍼티 디스크립터 객체를 반환하여 정의된 어트리뷰트를 확인할 수 있다.
const person = {
name: 'Jay'
};
// person 객체안에 name 프로퍼티의 어트리뷰트를 확인할 수 있는 프로퍼티 디스크립터 객체 출력
console.log(Object.getOwnPropertyDescriptor(person, "name"));
/*
프로퍼티 디스크립터 객체
{value: 'Jay', writable: true, enumerable: true, configurable: true}
configurable: true
enumerable: true
value: "Jay"
writable: true
*/
객체 리터럴로
person
객체 안에name
이라는 데이터 프로퍼티를 만들 때, 프로퍼티 어트리뷰트에 해당하는[[Configurable]]
,[[Enumerable]]
,[[Writable]]
을 명시하지 않았는데 왜 기본값인false
가 아닌true
로 출력될까?
이 부분은 프로퍼티 정의 부분에서 다시 설명하겠다.
프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 구분할 수 있다.
데이터 프로퍼티인지, 접근자 프로퍼티인지에 따라 연관된 내부 슬롯(메서드) 즉, 프로퍼티 어트리뷰트가 달라진다.
위의 표는 ECMAScript에 정의된 데이터 프로퍼티와 접근자 프로퍼티의 어트리뷰트에 대한 설명이다. 데이터 프로퍼티부터 하나씩 알아가보자.
데이터 프로퍼티는 키key
와 값value
으로 구성된 일반적인 프로퍼티다.
데이터 프로퍼티는 다음과 같은 어트리뷰트를 가지고 있다.
프로퍼티 어트리뷰트 | 타입 | 설명 |
---|---|---|
[[Value]] | Any | 프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값. 프로퍼티 키를 통해 값을 변경하면, [[Value]]에 값을 재할당. 프로퍼티가 존재하지 않으면 자동으로 생성 하고, 생성된 프로퍼티의 [[Value]]에 저장. |
[[Writable]] | Boolean | 프로퍼티 값의 변경 가능 여부를 표현. [[Writable]] 값이 false인 경우, [[Value]]값을 변경할 수 없는 read-only 프로퍼티가 됨. |
[[Enumerable]] | Boolean | 프로퍼티 열거 가능 여부를 표현. [[Enumerable]]값이 false인 경우, 해당 프로퍼티의 값은 열거할 수 없다. (for ..in 사용 불가능) |
[[Configurable]] | Boolean | 프로퍼티의 재정의 가능 여부를 표현. 객체 밀봉 참조 [[Configurable]]값이 false인 경우, 해당 프로퍼티 값 추가, 삭제가 금지된다. 다만, 존재하는 프로퍼티의 값을 수정하는 것은 가능하다.([[Writable]]이 true일 경우) |
프로퍼티 어트리뷰트는 자바스크립트 엔진이 프로퍼티를 생성할 때 기본값으로 자동 정의한다.
const person = {
name: 'Jay' // 데이터 프로퍼티
};
console.log(Object.getOwnPropertyDescriptor(person, "name"));
/*
프로퍼티 디스크립터 객체
{value: 'Jay', writable: true, enumerable: true, configurable: true}
configurable: true
enumerable: true
value: "Jay"
writable: true
*/
위의 코드를 실행하여 반환된 디스크립터 객체를 보면, value
키의 값이 Jay
인 것을 확인할 수 있는데, 이 값은 프로퍼티 어트리뷰트 [[Value]]
의 값이다. [[Writable]]
,[[Enumerable]]
, [[Configurable]]
값도 true
로 자동 정의되어 있는 것을 볼 수 있다. 위에서 말했듯이, 기본값이 false
인데 true
로 설정된 이유는 프로퍼티 정의 부분에서 설명하겠다.
접근자 프로퍼티는 데이터 프로퍼티와 다르게 자체적으로 값을 가지고 있지는 않다. 다만, 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티다.
접근자 프로퍼티는 다음과 같은 어트리뷰트를 가지고 있다.
프로퍼티 어트리뷰트 | 타입 | 설명 |
---|---|---|
[[Get]] | Function Object | Undefined | 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수. 프로퍼티 키로 접근하면, [[Get]] 접근자 함수가 호출되어 프로퍼티의 값을 반환. |
[[Set]] | Function Object | Undefined | 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수. 프로퍼티 키로 접근하면, [[Set]] 접근자 함수가 호출되어 프로퍼티의 값을 저장. |
[[Enumerable]] | Boolean | 프로퍼티 열거 가능 여부를 표현. [[Enumerable]]값이 false인 경우, 해당 프로퍼티의 값은 열거할 수 없다. (for ..in 사용 불가능) |
[[Configurable]] | Boolean | 프로퍼티의 재정의 가능 여부를 표현. 객체 밀봉 참조 [[Configurable]]값이 false인 경우, 해당 프로퍼티 값 추가, 삭제가 금지된다. 다만, 존재하는 프로퍼티의 값을 수정하는 것은 가능하다. ([[Writable]]이 true일 경우) |
접근자 함수는 getter
/ setter
함수라고도 부른다. 접근자 프로퍼티는 getter
와 setter
함수 중 하나만 정의할 수도 있고, 둘 다 정의할 수도 있다.
const person = {
firstName: 'Jay', // firstName 데이터 프로퍼티
lastName: 'Kim', // lastName 데이터 프로퍼티
get fullName() { //getter 함수
return `${this.firstName} ${this.lastName}`;
}
}
// fullName 접근자 프로퍼티의 디스크립터 객체 출력
console.log(Object.getOwnPropertyDescriptor(person, 'fullName'));
/*
프로퍼티 디스크립터 객체
{set: undefined, enumerable: true, configurable: true, get: ƒ}
configurable: true
enumerable: true
get: ƒ fullName()
set: undefined
*/
위의 코드를 보면, fullName
이라는 getter
함수를 정의해서 접근자 프로퍼티가 만들어진 것을 볼 수있다. 하지만 fullName
접근자 프로퍼티에 setter
함수를 정의하지 않았기 때문에, 기본값인 undefined
로 정의 있는 것을 볼 수 있다. setter
함수까지 정의한다면,
const person = {
firstName: 'Jay', // firstName 데이터 프로퍼티
lastName: 'Kim', // lastName 데이터 프로퍼티
get fullName() { //getter 함수 ---------------------- fullName
return `${this.firstName} ${this.lastName}`;// | 접근자
}, // | 프로퍼티
set fullName(name) { // setter 함수 -----------------
[this.firstName, this.lastName] = name.split(' ');
}
}
// fullName 접근자 프로퍼티의 디스크립터 객체 출력
console.log(Object.getOwnPropertyDescriptor(person, 'fullName'));
/*
프로퍼티 디스크립터 객체
{enumerable: true, configurable: true, get: ƒ, set: ƒ}
configurable: true
enumerable: true
get: ƒ fullName()
set: ƒ fullName(name)
*/
fullName
접근자 프로퍼티에 getter
함수와 setter
함수가 정의되어 있는 것을 볼 수 있다.
객체에 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나, 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의 할 수 있다.
Object.defineProperty
메서드로 프로퍼티 정의를 할 수 있는데, 아래의 예시로 확인해보자.
const person = {};
Object.defineProperty(person, "firstName", {
value: "Jay",
writable: true,
enumerable: true,
configurable: true,
});
console.log(Object.getOwnPropertyDescriptor(person, "firstName"))
/*
{value: 'Jay', writable: true, enumerable: true, configurable: true}
configurable: true
enumerable: true
value: "Jay"
writable: true
*/
person
이라는 객체에 Object.defineProperty
메서드를 활용해서 firstName
이라는 데이터 프로퍼티를 정의했다. 이처럼 프로퍼티 어트리뷰트를 명시적으로 정의할 수도 있지만 프로퍼티 어트리뷰트를 생략할 수도 있다.
프로퍼티 어트리뷰트를 생략하고 정의하게 되면 아래와 같은 기본값이 적용된다.
const person = {};
Object.defineProperty(person, "firstName", {
value: "Jay",
});
console.log(Object.getOwnPropertyDescriptor(person, "firstName"))
/*
{value: 'Jay', writable: false, enumerable: false, configurable: false}
configurable: false
enumerable: false
value: "Jay"
writable: false
*/
위의 코드를 보면, 정의하지 않은 어트리뷰트들은 false
로 정의되어 있는 것을 확인할 수 있다.
이제, 위에서 질문한 내용을 다시 한 번 살펴보자.
객체 리터럴로
person
객체 안에name
이라는 데이터 프로퍼티를 만들 때, 프로퍼티 어트리뷰트에 해당하는[[Configurable]]
,[[Enumerable]]
,[[Writable]]
을 명시하지 않았는데 왜 기본값인false
가 아닌true
로 출력될까?
그 이유는 자바스크립트 엔진이 데이터 프로퍼티를 만들 때 어떻게 동작하는지 살펴보면 알 수 있다.
ECMAScript 2015에 정의된 내용을 살펴보면, 데이터 프로퍼티가 만들어질 때, 자바스크립트 엔진 내부적으로 CreateDataProperty
추상 연산을 하게 된다. 프로퍼티를 정의하기 전에 [[Writable]]
[[Enumerable]]
[[Configurable]]
값을 true
로 설정하기 때문에, 데이터 프로퍼티가 생성될 때, 자동으로 true
값으로 만들어지게 된다.
객체는 원시값과 다르게 객체 인스턴스가 존재하는 힙 메모리 주소가 변수에 할당된다. 따라서 해당 객체에 직접 접근하여 프로퍼티를 추가/수정/삭제 할 수 있고, Object.defineProperty
를 사용해 프로퍼티 어트리뷰트를 재정의 할 수도 있다.
따라서 자바스크립트는 객체의 변경을 방지하는 다양한 메서드를 제공하는데, 각 메서드별로 변경을 금지하는 정도의 차이가 있다.
const person = {
name: 'Jay'
}
Object.preventExtensions(person)
console.log(Object.isExtensible(person)); // false
person.age = 1000 // 무시
console.log(person) // name: 'Jay'
Object.defineProperty(person, 'age', { value: 1000}) // 무시
// TypeError: Cannot define property age, object is not extensible
Object.preventExtensions
메서드는 객체의 확장을 금지한다. 그 말은, 프로퍼티를 추가할 수 없다는 말과 같다. 위에 코드에서 처럼 프로퍼티를 동적으로 추가하는 방법은 무시 되고, Object.defineProperty
메서드 는 에러가 발생하는 것을 볼 수 있다.
const person = {
name: 'Jay'
}
Object.seal(person);
console.log(Object.isSealed(person)); // true
// 밀봉(seal)된 객체는 [[Configurable]]값이 false다.
console.log(Object.getOwnPropertyDescriptors(person));
/*
name:
value: "Jay"
writable: true
enumerable: true
configurable: false
*/
person.age = 1000; // 무시
delete person.name; // 무시
console.log(person); // {name: 'Jay'}
person.name = 'Kim'; // 프로퍼티 값 수정 가능
console.log(person); // {name: 'Kim'}
// 프로퍼티 어트리뷰트 재정의는 금지된다.
Object.defineProperty(person, 'name', {configurable: true});
// Uncaught TypeError: Cannot redefine property: name
Object.seal
메서드를 사용하면 객체를 밀봉한다. 객체를 밀봉하게 되면 프로퍼티 값을 수정할 수는 있지만, 프로퍼티 추가/삭제할 수 없고, 프로퍼티 어트리뷰트를 재정의할 수도 없다.
const person = {
name: 'Jay',
};
Object.freeze(person); // 객체 동결
console.log(Object.isFrozen(person)); // true
person.age = 1000; // 무시
delete person.name; // 무시
person.name = "Kim"; // 무시
console.log(person); // name: 'Jay'
Object.defineProperty(person, 'age', { value: 1000 });
// TypeError: Cannot define property age, object is not extensible
Object.freeze
메서드는 객체를 동결시킨다. 객체를 동결시키게 되면, 프로퍼티의 추가/수정/삭제와 프로퍼티 어트리뷰트 재정의까지 금지시킨다. 결국 동결된 객체는 읽기만 가능하다.