Object

raccoonback·2020년 6월 19일
1

javascript

목록 보기
5/11
post-thumbnail

이 글은 You Don't Know JS 서적을 참고하여 작성하였습니다.

자바스크립트 함수는 호출 가능한 특성이 있는 일급 객체이다.

객체 선언

객체는 선언적(리터럴) 방식과 생성자 형식으로 생성할 수 있다.

// 선언적(리터럴)
const obj = {}

// 생성자
const obj = new Object();

객체 내부는 프로퍼티로 이루어져 있는데, key-value 형태로 key를 통해서 프로퍼티 값이 있는 곳을 참조할 수 있다.

const obj = {
	name: '철수'
}

obj['name'];
obj.name;

여기서 주의해야 할 점은 객체(배열) 프로퍼티명은 항상 문자열이라는 것이다. 만약 문자열 이외의 숫자를 쓰더라도 우선 문자열로 변환된다. 따라서, 객체또는 배열의 프로퍼티명을 숫자와 혼용해서 사용하는 것은 권장하지 않는다.

const obj = {};
obj[true] = 'foo';
obj[1] = 'bar';

console.log(obj['true']); // foo
console.log(obj['1']); // bar

그럼 프로퍼티명을 런타임에 동적으로 설정할 수 있는 방법은 없을까?

계산된 프로퍼티명

ES6 부터는 계산된 프로퍼티명라는 기능이 추가되어, 객체 리터럴 선언의 프로퍼티명을 [표현식] 으로 구성해 활용할 수 있다.

const prefix = Math.floor(Math.random() * 100) % 2 === 1 ? 'a' : 'b';
const obj = {
		// 계산된 프로퍼티명
    [prefix + '_cal'] : '철수'
};

console.log('prefix', prefix);
console.log(obj['a_cal']);
console.log(obj['b_cal']);

객체 복사

객체 복사는 얕은 복사, 깊은 복사 두 가지 방법이 존재한다.

얕은 복사는 새로운 객체를 생성하고 기존 객체의 프로퍼티의 참조만 복사하는 방법이다. 프로퍼티의 경우에는 새로 생성하지 않고 동일한 레퍼런스를 참조하도록 한다.

깊은 복사는 새로운 객체 생성과 동시에 기존 객체의 프로퍼티까지 새로운 객체로 생성해 복사하는 방법이다. 만약 객체간에 상호 참조하고 있는 경우에는 깊은 복사가 환형 참조를 형태가 되어 무한 복사를 하게 되는 문제가 있다.

const a = {
    name: 'a'
};

const b = {
    name: 'b'
};

// 환영 참조
a.b = b;
b.a = a;

// 얕은 복사
function shallowCopy(obj) {
    const result = {};
    for (let [key, value] of Object.entries(obj)) {
        result[key] = value;
    }

    return result;
}

// 깊은 복사
function deepCopy(obj) {
    const result = {};
    for (let [key, value] of Object.entries(obj)) {
        if (typeof value === 'object') {
            result[key] = deepCopy(value);
        } else {
            result[key] = value;
        }
    }

    return result;
}

console.log(shallowCopy(a));
console.log(shallowCopy(b));
// { name: 'a', b: { name: 'b', a: { name: 'a', b: [Circular] } } }
// { name: 'b', a: { name: 'a', b: { name: 'b', a: [Circular] } } }

console.log(deepCopy(a));
console.log(deepCopy(b));
//function deepCopy(obj) {
//                 ^
//RangeError: Maximum call stack size exceeded

깊은 복사를 직접 구현할 수도 있지만, JSON 객체를 이용해서 JSON 문자열로 만들고, 이를 다시 객체로 역직렬화하는 방법도 하나의 대안이 될 수 있다.

JSON.parse(JSON.stringify(obj), [출력할 프로퍼티 목록]);
const a = {
    name: 'a'
};

const b = {
    name: 'b'
};

a.b = b;
b.a = a;

function shallowCopy(obj) {
    const result = {};
    for (let [key, value] of Object.entries(obj)) {
        result[key] = value;
    }

    return result;
}

const shallowCopyA = shallowCopy(a);
console.log(shallowCopyA);
console.log(shallowCopyA.b === b);

const deepCopyA = JSON.parse(JSON.stringify(a, ['name', 'b']));
console.log(deepCopyA);
console.log(deepCopyA.b === a.b);

// { name: 'a', b: { name: 'b', a: { name: 'a', b: [Circular] } } }
// true
// { name: 'a', b: { name: 'b' } }
// false

또한, ES6부터는 얕은 복사를 위한 Object.assign() 메서드를 제공한다.

Object.assign(target, ...sources)
const target = {a: 1, b: 2};
const source = {b: 3, c: 4};
const obj = Object.assign(target, source);

console.log(target);
console.log(source);
console.log(obj);
console.log(target === obj);

// { a: 1, b: 3, c: 4 }
// { b: 3, c: 4 }
// { a: 1, b: 3, c: 4 }
// true

프로퍼티 서술자

객체의 모든 프로퍼티프로퍼티 서술자로 구성된다.

프로퍼티 서술자value, writable, enumerable, configurable 네가지 특징을 가지고 있다.

Object.getOwnPropertyDescriptor() 메서드를 이용해서, 프로퍼티 서술자에 특성들의 값을 아래와 같이 확인할 수 있다.

const object = {
    property: '철수'
};

console.log(Object.getOwnPropertyDescriptor(object, 'property'));
//{ value: '철수',
//  writable: true,
//  enumerable: true,
//  configurable: true }

또한, 프로퍼티에 대한 프로퍼티 서술자의 특성값들을 변경하거나 새로운 프로퍼티를 생성하는 경우에는 Object.defineProperty() 함수를 이용할 수 있다.

const object = {
    name: '철수'
};

// 새로운 프로퍼티 추가
Object.defineProperty(object, 'age', {
    value: 29,
    writable: true,
    enumerable: true,
    configurable: true
});

// 기존 프로퍼티 수정
Object.defineProperty(object, 'name', {
    value: '미애',
    writable: true,
    enumerable: true,
    configurable: true
});

console.log(object);
// { name: '미애', age: 29 }

이제 프로퍼티 서술자 에서 value는 값을 나타내는 것을 알 수 있고, 나머지 특성들에 대해서 하나씩 살펴보자.

writable

writable 특성은 프로퍼티의 쓰기 가능 여부를 나타낸다.

쓰기가 금지된 프로퍼티(writable == false)를 변경하려는 경우, strict mode에서는 에러가 발생하고 non strict mode에서는 조용히 넘어간다.

'use strict';
const object = {};

Object.defineProperty(object, 'name', {
    value: '철수',
    writable: false,
    enumerable: true,
    configurable: true
});

object.name = '미애';
//object.name = '미애';
//            ^
//TypeError: Cannot assign to read only property 'name' of object '#<Object>'

configurable

configurable 특성은 프로퍼티 서술자 변경 여부를 의미한다. 즉, defineProperty() 함수로 프로퍼티 서술자를 변경할 수 있는지를 나타낸다.

const object = {};

Object.defineProperty(object, 'name', {
    value: '철수',
    writable: true,
    enumerable: true,
    configurable: false
});

object.name = '미애';
console.log(object); // { name: '미애' }

Object.defineProperty(object, 'name', {
    value: '철수',
    writable: false,
    enumerable: true,
    configurable: true // 서술자 변경하게 되면, TypeError 발생한다.
});
// Object.defineProperty(object, 'name', {
//       ^
// TypeError: Cannot redefine property: name

위 예시와 같이, 프로퍼티 서술자 특성 변경을 막은 경우(configurable == false)에는 writable, enumerable, configurable 특성을 변경할 수 없다. (단, writable 특성이 true인 경우에는 false로 변경 가능하다.)

따라서, configurable 특성이 false 가 되면 특성 변경의 제한이 있으므로 유의해야 한다.

enumerable

enumerable 특성은 열거 가능성을 의미하고 for...in문처럼 프로퍼티를 열거하는 구문에서 해당 프로퍼티를 표현할 것인 지를 나타낸다. 즉, enumerable 특성을 false로 하면 for...in문에서 처리되지 않는다.

const object = {name: '철수'};

Object.defineProperty(object, 'age', {
    value: 27,
    writable: true,
    enumerable: false, // 열거 가능성을 제거
    configurable: true
});

Object.defineProperty(object, 'height', {
    value: 166,
    writable: true,
    enumerable: true,
    configurable: true
});

for(let key in object) {
    console.log(key, object[key]);
}
// name 철수
// height 166

위 예시를 보면, age 프로퍼티가 for...in문에서 감춰진 것을 확인할 수 있다.

불변성

불변성은 얕은 불변성, 깊은 불변성 두 가지로 구분해 생각할 수 있다.

얕은 불변성은 객체 자신과 프로퍼티만 불변으로 만든다는 것을 의미한다. 즉, 프로퍼티가 가리키는 레퍼런스까지는 불변으로 만들지 못한다.

반면, 깊은 불변성은 자신뿐 만 아니라 프로퍼티가 가르키는 레퍼런스까지 불변으로 만드는 것을 의미한다.

자바스크립트는 얕은 불변성을 위한 기능만 지원하는데 하나씩 알아보자.

객체 상수

앞서 살펴본 프로퍼티 서술자를 이용하면 상수처럼 사용할 수 있다.

const object = {};

Object.defineProperty(object, 'name', {
    value: '철수',
    writable: false,    // 쓰기 제한
    configurable: false,// 설정 변경 제한
    enumerable: true
});

// 변경되지 않고 무시된다.
object.name = '미애';
console.log(object);
// { name: '철수' }

확장 금지

객체가 가진 현재 프로퍼티를 유지하고 더는 프로퍼티 추가할 필요가 없을 경우, Object.preventExtensions() 함수를 이용할 수 있다.

const object = {
    name: '철수',
    age: 23
};

// 확장 금지
Object.preventExtensions(object);

// 더는 object 객체에 프로퍼티가 추가되지 않고 무시된다.
object.weight = 89;

console.log(object);
// { name: '철수', age: 23 }

봉인

Object.seal() 함수를 이용해서 봉인된 객체를 생성한다.

봉인된 객체라 함은 프로퍼티 서술자 특성 변경할 수 없고 프로퍼티 확장이 불가한 객체를 의미한다.

Object.seal() 함수가 실행되면 Object.preventExtensions()와 모든 프로퍼티대한 프로퍼티 서술자configurable 설정이 false가 된다.

const object = {
    name: '철수',
    age: 23
};

// 봉인된 객체 생성
const sealedObject = Object.seal(object);

// 값 변경은 가능
sealedObject.name = '미애';

// 확장 불가되어 무시된다.
sealedObject.weight = 89;

console.log(sealedObject);
console.log(Object.getOwnPropertyDescriptors(sealedObject));
// { name: '미애', age: 23 }
// { name:
//    { value: '미애',
//      writable: true,
//      enumerable: true,
//      configurable: false },
//   age:
//    { value: 23,
//      writable: true,
//      enumerable: true,
//      configurable: false } }

동결

Object.freeze() 함수는 인자로 전달한 객체를 동결하여 반환한다.

동결한다는 것은 무엇일까?

Object.seal() 함수로 객체를 봉인하고, 모든 프로퍼티대한 writable 특성을 false로 변경해 객체 변화를 제한하는 것을 의미한다. 뿐만 아니라, 프로토타입 변경도 방지한다.

const object = {
    name: '철수',
    age: 23
};

// 봉인된 객체 생성
const freezeObject = Object.freeze(object);

// 확장 금지
freezeObject.height = 177;

console.log(freezeObject);
console.log(Object.getOwnPropertyDescriptors(freezeObject));
// { name: '철수', age: 23 }
// { name:
//    { value: '철수',
//      writable: false,
//      enumerable: true,
//      configurable: false },
//   age:
//    { value: 23,
//      writable: false,
//      enumerable: true,
//      configurable: false } }

[[Get]]

const obj = {
	name: '철수'
}

obj.name;

obj.name 코드는 obj 객체의 name 프러퍼티에 접근한 것으로 보이지만, 실제로는 obj 객체가 [[Get]] 연산을 한 것이다.

구체적으로, [[Get]] 연산 과정은 객체에서 동일한 이름의 프로퍼티를 찾아 값을 반환하고 발견하지 못하면 Prototype Chainning을 통해 상위 프로토타입에서 프로퍼티를 탐색한다.

[[Put]]

[[Put]] 연산은 객체에 프로퍼티를 생성/수정하는 경우 발생하는데, 다음과 같은 과정을 거친다.

  1. 프로퍼티가 접근 서술자라면 Setter를 호출한다.
  2. 프로퍼티가 쓰기 불가(writable: false)라면 무시하거나 TypeError를 던진다.
  3. 그 외에는 프로퍼티에 주어진 값을 할당한다.

Getter & Setter

프로퍼티는 프로퍼티 서술자 또는 접근 서술자 중 하나의 서술자를 갖는다.

프로퍼티 서술자는 value 특성으로 값을 가지고, 접근 서술자는 value 없이 Getter/Setter 함수를 정의한다.

정리해보면, 프로퍼티 경우가 프로퍼티 서술자라면 value에 접근하고,접근 서술자라면 Getter/Setter에 접근한다.

접근 서술자인 경우에는 value, writable 특성없이, get, set, enumerable, configuration 특성으로 구성된다.

MDN 문서에 따라 Getter, Setter 함수의 정의를 살펴보자.

  • Setter : 프로퍼티에 값을 설정하려고 할 때, 호출되는 함수로 바인딩(인자는 무조건 하나)
  • Getter : 프로퍼티를 가져올 때, 호출하는 함수로 바인딩(인자는 없어야됨)

이제 실제 코드에서 어떻게 쓰이는지 살펴보자.

const object = {
    // name getter 정의
		// name 프로퍼티를 가져올 때 호출하는 함수로 바인딩
    get name() {
        console.log('Getter Call', this._name_);
        return 'Hello! ' + this._name_;
    }

};

Object.defineProperty(object, 'name', {
    // name setter 정의
		// name 프로퍼티에 값을 설정하려고 할 때 호출되는 함수로 바인딩
    set: function (name) {
        console.log('Setter Call', name);
        this._name_ = name;
    },
    enumerable: true
});

object.name = '철수'; // [[Put]] 연산에서 Setter 함수 호출
// Setter Call 철수

console.log(object.name); // [[Get]] 연산에서 Getter 호출
// Getter Call 철수
// Hello! 철수

object.name = '미애'; // [[Put]] 연산에서 Setter 함수 호출
// Setter Call 미애

console.log(object.name); // [[Get]] 연산에서 Getter 호출
// Getter Call 미애
// Hello! 미애

위 예시를 보면, 리터럴 객체의 name 프로퍼티가 프로퍼티 서술자가 아닌 접근 서술자로 정의된 것을 확인할 수 있다.

따라서 object.name 코드의 [[Get]] 연산 과정에서 Getter 함수가 호출된 것을 알수있다.

이와 유사하게 Setter 함수가 정의되어 있다면, 프로퍼티 값 변경시 [[Put]] 연산에서 Setter 함수가 호출된다.

또한, 프로퍼티 서술자 와 유사하게 접근 서술자에서도 아래와 같이 계산된 프로퍼티명을 사용할 수 있다.

const computedProperty = Math.round(Math.random() * 100) % 2 === 1 ? 'a' : 'b';
const object = {
    get [computedProperty]() {
        return 'This is ' + this.target;
    },
    set [computedProperty](name) {
        this.target = 'JS_' + name;
    }
};

object[computedProperty] = '철수';    // [[Put]] 연산에서 Setter 함수 호출

console.log(object[computedProperty]);  // [[Get]] 연산에서 Getter 호출
// This is JS_철수

참고자료

존재 확인

객체 내부에 프로퍼티가 존재하는 지를 파악할 수 있는 두 가지 방법이 있다.

첫 번째로 in 연산자는 해당 객체와 [[Prototype]] 체이닝을 통한 상위 프로토타입에 찾고자 하는 프로퍼티가 존재하는지 탐색한다.

두 번째로 hasOwnProperty() 함수는 오직 해당 객체에 프로퍼티가 존재하는 지만 탐색한다.

const foo = {
    name: '철수'
};

const bar = Object.create(foo);
bar.age = 25;

console.log(bar.hasOwnProperty('name'));    // true
console.log(bar.hasOwnProperty('age'));     // false

console.log('name' in bar);    // true
console.log('age' in bar);     // true

순회

For in

for...in 반복문은 자신과 Prototype 체이닝으로 접근 가능한 객체에 차례대로 접근하여 열거 가능한 프로퍼티를 순회한다.

const foo = {
    name: '철수'
};

const bar = Object.create(foo);
bar.age = 25;

for(let key in bar) {
    console.log(key, bar[key]);
}
// age 25
// name 철수

for...in 문의 불편한 점은 객체 프로퍼티의 이름을 기준으로 순회한다.

그럼 객체 프로퍼티의 값으로 순회할 수 있는 방법이 있을까?

ES6부터는 배열을 위한 for...of 반복문을 지원한다.

For of

for...of 문은 객체 프로퍼티의 값을 대상으로 순회를 수행한다.

좀 더 자세히 살펴보면, for...of 문은 순회할 원소의 Iterator 객체가 필요한데, 한 사이클당 한 번씩 Iterator 객체의 next() 함수를 호출해 연속적으로 순회한다.

  • ES6부터는 Symbol.iterator 심볼을 이용해서 객체 내부 프로퍼티인 @@iterator(Iterator 객체를 반환하는 함수)에 접근할 수 있다.
const array = ['철수', '미애', '짱구']; // 배열에는 @@iterator가 내장되어 있다.
let iterator = array[Symbol.iterator]();

let entry;
do {
    entry = iterator.next();
    console.log(entry);

} while (!entry.done);

// { value: '철수', done: false }
// { value: '미애', done: false }
// { value: '짱구', done: false }
// { value: undefined, done: true }

이제 매 순회마다 호출하는 iterator 객체의 next(){ value, done } 형식의 객체를 반환한다.

value는 현재 사이클에서의 프로퍼티 값을 의미하고, done은 다음에 순회할 프로퍼티 값이 있는지 여부를 의미한다.

결과적으로, for...of 문에서 매번 iterator 객체의 next() 함수를 호출하면, 내부 포인터는 하나씩 증가하고 반환값의 done을 이용해 다음 사이클 실행 여부를 결정한다.

좀 더 부연 설명하기 위해, 일반적인 for 문에서 iterator 객체를 사용해보면 아래와 같다.

const array = ['철수', '미애', '짱구'];
let iterator = array[Symbol.iterator]();

for(let entry = iterator.next(); entry.done !== true; entry = iterator.next()) {
    console.log(entry);
}
// { value: '철수', done: false }
// { value: '미애', done: false }
// { value: '짱구', done: false }

위와 같이 for..of문은 배열에서 유용하게 사용되지만, 일반 객체에는 @@iterator가 없기 때문에 사용할 수가 없다. 일반 객체에도 필요한 경우에 따라 아래와 같이 @@iterator를 구현할 수도 있다.

const object = {
    foo: '철수',
    bar: '미애',
    baz: '짱구'
};

Object.defineProperty(object, Symbol.iterator, {
    enumerable: false,
    writable: false,
    configurable: true,
    value: function () {
        const values = Object.values(this);
        const {length} = values;
        let index = 0;
        return {
            next: function() {
                return {
                    value: values[index++],
                    done: index > length
                }
            }
        }
    }
});

const it = object[Symbol.iterator]();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
// { value: '철수', done: false }
// { value: '미애', done: false }
// { value: '짱구', done: false }
// { value: undefined, done: true }

for(let value of object) {
    console.log(value);
}
// 철수
// 미애
// 짱구

위 예제와 같이, 개발 상황에 따라 요구사항에 맞는 커스텀 @@iterator를 구현할 수 있다.

profile
한번도 실수하지 않은 사람은, 한번도 새로운 것을 시도하지 않은 사람이다.

0개의 댓글