JavaScript - 프로퍼티와 어트리뷰트

Juhyeong Kim·2022년 1월 13일
0

JS

목록 보기
3/3

내부 슬롯과 내부 메서드

자바스크립트의 내부 동작은 어떻게 이루어질까? 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에 정의된 데이터 프로퍼티와 접근자 프로퍼티의 어트리뷰트에 대한 설명이다. 데이터 프로퍼티부터 하나씩 알아가보자.

1. 데이터 프로퍼티

데이터 프로퍼티는 키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로 설정된 이유는 프로퍼티 정의 부분에서 설명하겠다.

2. 접근자 프로퍼티

접근자 프로퍼티는 데이터 프로퍼티와 다르게 자체적으로 값을 가지고 있지는 않다. 다만, 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티다.

접근자 프로퍼티는 다음과 같은 어트리뷰트를 가지고 있다.

프로퍼티 어트리뷰트타입설명
[[Get]]Function Object |
Undefined
데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수.
프로퍼티 키로 접근하면, [[Get]] 접근자 함수가 호출되어 프로퍼티의 값을 반환.
[[Set]]Function Object |
Undefined
데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수.
프로퍼티 키로 접근하면, [[Set]] 접근자 함수가 호출되어 프로퍼티의 값을 저장.
[[Enumerable]]Boolean프로퍼티 열거 가능 여부를 표현.
[[Enumerable]]값이 false인 경우, 해당 프로퍼티의 값은 열거할 수 없다.
(for ..in 사용 불가능)
[[Configurable]]Boolean프로퍼티의 재정의 가능 여부를 표현. 객체 밀봉 참조
[[Configurable]]값이 false인 경우, 해당 프로퍼티 값 추가, 삭제가 금지된다.
다만, 존재하는 프로퍼티의 값을 수정하는 것은 가능하다.
([[Writable]]이 true일 경우)

접근자 함수는 getter / setter 함수라고도 부른다. 접근자 프로퍼티는 gettersetter 함수 중 하나만 정의할 수도 있고, 둘 다 정의할 수도 있다.

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를 사용해 프로퍼티 어트리뷰트를 재정의 할 수도 있다.

따라서 자바스크립트는 객체의 변경을 방지하는 다양한 메서드를 제공하는데, 각 메서드별로 변경을 금지하는 정도의 차이가 있다.

1. 객체 확장 금지

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 메서드 는 에러가 발생하는 것을 볼 수 있다.

2. 객체 밀봉

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메서드를 사용하면 객체를 밀봉한다. 객체를 밀봉하게 되면 프로퍼티 값을 수정할 수는 있지만, 프로퍼티 추가/삭제할 수 없고, 프로퍼티 어트리뷰트를 재정의할 수도 없다.

3. 객체 동결

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메서드는 객체를 동결시킨다. 객체를 동결시키게 되면, 프로퍼티의 추가/수정/삭제와 프로퍼티 어트리뷰트 재정의까지 금지시킨다. 결국 동결된 객체는 읽기만 가능하다.

0개의 댓글