
시작하기 앞서 프로퍼티와 어트리뷰트에 대해 알고가자.
프로퍼티 : 속성이라는 뜻으로 JS에서는 객체 내부의 속성을 의미한다.
객체는 프로퍼티로 구성이된다. key(키) : value(값) 의 형식으로 객체 안의 쉼표로 구분을 한다.
객체에 함수를 정의할 경우, 속성값이 아니라 메소드라고 한다.
프로퍼티 어트리뷰를 이해하기 위해 내부슬롯과 내부 메서드 개념을 알아야 한다.
내부 슬롯과 내부 메서드는 자바스크립트 엔진의 구현 알고리즘을
설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티와 의사 메서드다.
내부 슬롯과 내부 메서드는 ECMAScript 사양에 정의된 대로 구현되어
자바스크립트 엔진에서 실제로 동작하지만 개발자가 직접 접근할 수 있도록 외부로 공개된 객체의 프로퍼티는 아님. 일부는 간접적으로 접근 가능하긴하다.
예를 들어 모든 객체는 [[Prototype]]이라는 내부 슬롯을 가짐.
내부 슬롯은 자바스크립트 엔진의 내부로직이므로 원칙적으로 직접 접근
불가능하지만 [[Prototype]] 내부 슬롯의 경우, "__proto__"를 통해 간접적 접근으로 접근할 수 있다.
const o = {
name:'seung'
};
console.log(o.__proto__);
result:
[Object: null prototype] {}
자바스크립트 엔진은 프로퍼티를 생성할 때
프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동정의한다.
프로퍼티 어트리뷰트 : 프로퍼터의 상태
( 프로퍼티 값, 갱신 가능여부, 열거 가능 여부, 재정의 가능 여부)
프로퍼티 어트리뷰트는 자바스크립트 엔진이 관리하는 내부 상태 값인
내부 슬롯
1. [[Value]]
2. [[Writalbe]]
3. [[Enumerable]]
4. [[Configurable]]
프로퍼티 어트리뷰트에 직접 접근할 수 없지만,
Object.getOwPropertyDesciptor 메서드를 사용하여 간접적 확인 가능하다.
const person = {
name : 'Seung',
addr : "수원시 장안구"
};
console.log(Object.getOwnPropertyDescriptors(person));
result:
{
name: {
value: 'Seung',
writable: true,
enumerable: true,
configurable: true
},
addr: {
value: '수원시 장안구',
writable: true,
enumerable: true,
configurable: true
}
}
Object.getOwPropertyDesciptor 메서드는 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 반환한다.
프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 구분할 수 있다.
데이터 프로퍼티 : 키와 값으로 구성된 일반적인 프로퍼티.
지금까지 본 모든 프로퍼티는 데이터 프로퍼티.
EX) Value, Writable, Enumerable, Configurable ...
접근자 프로퍼티 : 자체적으로 값을 가지지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티.
getter/setter 함수라고도 불린다.
( 왜사용하냐고 물어본다면 구글링 해본 결과 캡슐화가 제일 큰 효과인듯하다. )
EX) get, set
[[Value]] : 프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값.
[[Writalbe]] :프로퍼티 값의 변경 가능 여부를 나타내며 불리언 값을 가진다.
[[Enumerable]] : 프로퍼티 열거 기능 여부를 나타내며 불리언 값을 가진다.
[[Configurable]] : 프로퍼티의 재정의 가능 여부를 나타내며 불리언 값을 가진다.
해당 [[Configurable]] 의 값이 false이면 프로퍼티 어트리뷰트 값의 변경이 금지된다. 단 [[Writable]]이 true인 경우 [[Value]]의 변경과 [[Writable]]을 false로 변경하는 것은 허용된다.
const person = {
name : 'seung'
};
// 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 흭득
console.log(Object.getOwnPropertyDescriptor(person,'name'));
result :
{
value: 'seung',
writable: true,
enumerable: true,
configurable: true
}
Object.getOwnPropertyDescriptor 메서드가 반환한 프로퍼티 해석
디스크립터 객체를 살펴보면 value 프로퍼티의 값은 'seung‘ 인데
이것은 프로퍼티 어트리뷰트 [[Value]]의 값이 'seung'인 것을 의미한다.
그리고 writable, Enumerable, Configurable의 값이 모두 true 인데
이것은 프로퍼티 어트리뷰트 [[Writable]], [[Enumerable]],[Configurable]] 의 값이 모두 true인 것을 의미한다.
이처럼 프로퍼티가 생성될 때
[[Value]]의 값은 프로퍼티 값으로 초기화되며
[[Writable]], [[Enumerable]]. [Configurable]] 은 모두 true로 초기화된다
person.age = 20;처럼 동적으로 추가해도 마찬가지다.
프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동정의한다.
프로퍼티 어트리뷰트 : 프로퍼터의 상태
프로퍼티 디스크립터 : 프로퍼티 어트리뷰트 정보를 제공하는 메소드
위에서 보았듯이 프로퍼티를 생성할 때는 어트리뷰트는 자바스크립트 엔진이 기본값으로 자동 정의한다
접근자 프로퍼티 : 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의
값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티.
[[Get]] : 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수다. 즉, 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 프로퍼티 어트리뷰트 [[Get]]의 값, 즉 getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환된다.
[[Set]] : 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수다. 즉 접근자 프로퍼티 키로 프로퍼티 값을 저장하면 프로퍼티 어트리뷰트 [[Set]]의 값, 즉 setter 함수가 호출되고 그 결과가 프로퍼티 값으로 저장된다.
접근자 프로퍼티는 getter와 setter 함수를 모두 정의할 수도 있고 하나만 정의할 수 도 있다.
const person = {
name : "빅뱅",
get getName() {
return `${this.name}`;
},
// set함수 사용하기 이 함수는 파라미터를 반드시 사용해야한다.
set setName(enteredName) {
this.name = enteredName;
}
};
// get함수 사용하기
console.log(person.getName); // 빅뱅
// set함수 사용하기
person.setName = "지드래곤"
// 문법오류발생한다. 이렇게 사용하지 않도록 하자.
person.setname("지드래곤") // TypeError
// set함수를 사용하여 수정한뒤 name값
console.log(person.name); // 지드래곤
프로퍼티 정의 : 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나, 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의하는 것을 말함.
ex ))
프로퍼티 값을 갱신 가능하도록 할 거야?
프로퍼티를 열거 가능하도록 할 거야?
프로퍼티를 재정의 가능하도록 할 거야?
이렇게 객체의 프로퍼티가 어떻게 동작해야 하는지를 명확히 정의 가능하다.
Object.definedProperty 메서드를 사용하여 프로퍼티 어트리뷰트를 정의.
const person = {} ;
// 데이터 프로퍼티 정의
// 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의한다.
// 인수로 객체의 참조, 키인 문자열, 프로퍼티 디스크립터 객체
// Object.defineProperty(객체의참조, '(키) 문자열' , 프로퍼티 디스크립터 객체);
Object.defineProperty(person, 'firstName', {
value : 'Kim',
writable : true,
enumerable : true,
configurable : true
});
Object.defineProperty(person, 'lastName', {
value : 'Park'
});
console.log(Object.getOwnPropertyDescriptor(person, 'firstName'));
result : { value: 'Kim', writable: true, enumerable: true, configurable: true }
// 디스크립터 객체의 프로퍼티를 기술할때 누락시킨다면 undefined, false가 기본값이다.
console.log(Object.getOwnPropertyDescriptor(person, 'lastName'));
result : {value: 'Park', writable: false, enumerable: false,, configurable: false }
enumberable이 false인 프로퍼티는 열거가 되지 않는다.
// person객체의 키들을 출력시도
console.log(Object.keys(person));
// lastName 프로퍼티는 false이므로 열거되지 않음.
[ 'firstName' ]
writable이 false인 프로퍼티는 값 변경이 불가능하다.
let descriptor = Object.getOwnPropertyDescriptor(person, 'lastName');
person.lastName = 'Change';
console.log(descriptor);
result :
{
value: 'Park',
writable: false,
enumerable: false,
configurable: false
}
configurable이 false인 프로퍼티는 해당 프로퍼티를 삭제 할 수 없다.
let descriptor = Object.getOwnPropertyDescriptor(person, 'lastName');
delete person.lastName;
console.log(descriptor);
result :
{
value: 'Park',
writable: false,
enumerable: false,
configurable: false
}
Configurable의 값이 false인 경우 해당 프로퍼티를 재정의할 수 없다.
Object.defineProperty(person, 'lastName', { enumerable : true });
result : TypeError: Cannot redefine property: lastName
접근자 프로퍼티 정의하기
const person = {} ;
Object.defineProperty(person, '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);
result : { fullName: [Getter/Setter], firstName: 'Heegun', lastName: 'Lee' }
object.defineProperties 메서드를 사용하여 여러 개의 프로퍼티를 한번에 정의.
const person2 = {};
Object.defineProperties(person2, {
// 데이터 프로퍼티 정의
firstName : {
value : 'Minho',
writable : true,
enumerable : true,
configurable : true
},
lastName : {
value : 'Kim',
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
}
});
person2.fullName = 'MinJi Park';
console.log(person2);
result : { firstName: 'MinJi', lastName: 'Park', fullName: [Getter/Setter] }
객체는 변경 가능한 값이므로 재할당 없이 직접 변경 가능하다.
즉 프로퍼티를 추가하거나 삭제할 수 있고,
프로퍼티 값을 갱신할 수 있으며
Object.definedProperty 또는 Object.definedProperties 메서드를 사용하여
프로퍼티 어트리뷰트를 재정의 할 수도 있다.
그렇다면 이러한 변경들을 사전에 막는 작업도 필요하지 않을까?
자바스크립트 객체의 변경을 방지하는 다양한 메서드를 제공한다.
객체 변경 방지 메서드들은 객체의 변경을 금지하는 강도가 다르다.
Objet.preventExtensions 메서드는 객체의 확장을 금지한다.
객체 확장 금지란 프로퍼티 추가 하는것을 못한다는 의미이다.
원래는 프로퍼티가 동적추가와 Object.defineProperty 메서드로
추가할 수 있다. 이 두가지 방법 모두 금지가 된다.
객체 확장이 가능한지 물어보는 메서드는 isExtensible을 사용한다.
// person 객체 생성
const person = { name : 'Seung'};
// 현재 person 객체는 확장이 금지된 객체가 아닌 상태다.
console.log(Object.isExtensible(person)); // true
// person 객체의 확장을 금지하여 프로퍼티 추가를 금지한다.
Object.preventExtensions(person);
// person 객체는 확장이 금지된 객체다.
console.log(Object.isExtensible(person)); // false
// 프로퍼티 추가가 금지된다.
// 현재는 에러가 나지않고 무시만되고 실행되지만
// strict mode에서는 에러가 발생한다.
person.age = 20;
// 추가만 불가능할뿐이고 삭제는 가능하다.
delete person.name;
console.log(person); // {}
Object.seal 메서드는 객체를 밀봉한다.
객체 밀봉 : 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지를 의미
밀봉된 객체인지 여부는 Object.isSealed 메서드로 확인 가능
const person = { name : 'Seung'};
// person 객체는 현재 밀봉된 객체의 상태가 아니다.
console.log(Object.isSealed(person)); // false
// person 객체를 밀봉(seal)하여 프로퍼티 추가, 삭제, 재정의를 금지한다.
Object.seal(person);
// person 객체는 현재 밀봉된 객체의 상태다.
console.log(Object.isSealed(person)); // true
// 밀봉된 객체는 configurable이 false다.
console.log(Object.getOwnPropertyDescriptor(person));
result : {
name: {
value: 'Seung',
writable: true,
enumerable: true,
configurable: false
}
}
// 프로퍼티 추가가 금지된다.
// 현재는 에러가 나지않고 무시만되고 실행되지만
// strict mode에서는 에러가 발생한다.
person.age = 20;
console.log(person); // {name : 'Seung'}
// 프로퍼티 값 갱신은 가능하다.
person.name = 'Kim';
console.log(person); // {name : "Kim"}
// 프로퍼티 어트리뷰트 재정의가 금지된다.
Object.defineProperty(person, 'name', { configurable : true });
result : TypeError: Cannot redefine property: name
object.freeze 메서드는 객체를 동결한다.
객체 동결 : 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지,
프로퍼티 값 갱신 금지를 의미한다.
즉 동결 객체는 읽기만 가능하다.
동결된 객체인지 여부는 Object.isFrozen 메서드로 확인할 수 있다.
const person = { name : 'lee'};
// person 객체는 현재 동결된 객체가 아니다
console.log(Object.isFrozen(person)); // fasle
// person 객체를 동결하여 프로퍼티 추가, 삭제, 재정의, 쓰기를 금지한다.
Object.freeze(person);
// person 객체는 현재 동결된 객체이다.
console.log(Object.isFrozen(person)); // true
// 동결된 객체는 writable과 configurable이 false다.
console.log(Object.getOwnPropertyDescriptors(person));
result : {
name: {
value: 'lee',
writable: false,
enumerable: true,
configurable: false
}
}
// 프로퍼티 추가가 금지된다.
// 현재는 에러가 나지않고 무시만되고 실행되지만
// strict mode에서는 에러가 발생한다.
person.age = 20;
console.log(person);
result : {name : 'Seung'}
// 프로퍼티 값 갱신이 금지된다.
// 현재는 에러가 나지않고 무시만되고 실행되지만
// strict mode에서는 에러가 발생한다.
person.name = 'Kim';
console.log(person);
// 프로퍼티 삭제가 금지된다.
// 현재는 에러가 나지않고 무시만되고 실행되지만
// strict mode에서는 에러가 발생한다.
delete person.name; //
console.log(person);
// 프로퍼티 어트리뷰트 재정의가 금지된다.
Object.defineProperty(person, 'name', { configurable : true });
result : TypeError: Cannot redefine property: name
지금까지 살펴본 변경 방지 메서드들은 얕은 변경 방지로
직속 프로퍼티만 변경이 방지되고 중첩 객체까지는 영향을 주지 못함.
따라서 Object.freeze 메서드로 객체를 동결하여도 중첩 객체까지 동결할 수 없다.
const person = {
name : 'Lee',
addr : { city : 'suwon'}
};
// 얕은 객체 동결
Object.freeze(person);
// 직속 프로퍼티만 동결한다
console.log(Object.isFrozen(person));
result : true
// 중첩 객체까지 동결하지 못한다.
console.log(Object.isFrozen(person.addr));
result : false
// 중첩 객체의 값 변경하기
person.addr.city = 'Busan';
console.log(person);
result : { name: 'Lee', addr: { city: 'Busan' } }
객체의 중첩 객체까지 동결하여 변경이 불가능한 읽기 전용의 불변 객체를 구현하려면 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로
Object.freeze 메서드를 호출해야한다.
function deepFreeze(target) {
// 객체가 아니거나 동결된 객체는 무시하고 객체이고 동결되지 않은 객체만 동결한다.
if(target && typeof target === 'object' && !Object.isFrozen(target)) {
Object.freeze(target);
Object.keys(target).forEach(key => deepFreeze(target[key]));
}
}
const person = {
name : 'Lee',
addr : { city : 'suwon'}
};
// 깊은 객체 동결
deepFreeze(person);
// person 객체는 동결되어있는 상태가 되었다.
console.log(Object.isFrozen(person)); // true
// 중첩 객체까지 동결되어있는 상태가 되었다.
console.log(Object.isFrozen(person.address)); // true
// 중첩 객체의 값을 변경하려고해도 동결되어있는 상태라 변경되지 않는다.
person.address.city = 'Busan'; // TypeError: Cannot set properties
console.log(person); // { name: 'Lee', addr: { city: 'suwon' } }