As i wish

[You don't know JS] - 객체 본문

JavaScript

[You don't know JS] - 객체

어면태 2019. 5. 13. 01:00

You don't know JS에 3 장 객체에 대하여 포스팅해보겠습니다.

 

앞서 this에 관해 포스팅했었는데 참고해주세요.

3.1 객체 정의

일반적으로 JS에서 객체는 선언적(리터럴) 형식과 생성자 형식, 두 가지로 정의할 수 있습니다..

 // 선언적(리터럴) 형식
 var myObj = {
     key: value
     // ...
 }

 // 생성자 형식
 var myObj = new Object();
 myObj.key = value;

두 형식 모두 생성되는 객체는 같습니다. 그러나 리터럴 형식은 한 번의 선언으로 다수의 키/값을 추가할 수 있고, 생성자는 한 번에 한 프로퍼티만 추가 할 수 없어요.

3.2 타입

자바스크립트는 대략 7가지 정도 타입이 있는데,

<단순 원시 타입>

null 

undefined

boolean

number

string

<복합 원시 타입>

object

symbol (ES6에서 추가)

<단순 원시 타입>은 객체가 아니고 <복합 원시 타입>이라는 독특한 객체 하위 타입이 있습니다. function은 객체의 하위 타입입니다.(정확히는 호출 가능한 객체라고 책에 서술되어 있네요)

3.2.1 내장 객체

내장 객체는

String

Number

Boolean

Object

Function

Array

Date

RegExp

Error

언뜻 보면 단순 원시 타입과 직접 연관되어 보이지만 실제 관계는 뜻밖에 복잡합니다.

내장 객체는 단지 자바스크립트의 내장 함수일 뿐 각각 생성자(new 연산자가 앞에 붙은 함수 호출)로 사용되어 주어진 하위 타입의 새 객체를 생성합니다.

var strPrimitive = 'Im string';
typeof strPrimitive // "string"
strPrimitive instanceof String // false

var strObject = new String('Im string');
typeof strObject // "object"
strObject instanceof String // true

'Im string'라는 원시 값은 객체가 아닌 원시 리터럴이며 불변 값입니다. 문자 개수를 세는 증 문자 별로 접근할 때엔 String 객체가 필요한데 다행히도 자바스크립트 엔진은 상황에 맞게 문자열 원시 값을 String객체로 자동 강제 변환하므로 명시적으로 객체를 생성할 일이 거의 없습니다. 따라서 되도록 생성자 형식은 지양하고 리터럴 형식을 사용하면 되겠습니다.

단, Date값은 리터럴 형식이 없어서 반드시 생성자 형식으로 생성해야 하죠.

Object, Arrays, Functions, RegExpsㄴ,ㄴ 형식과 무관하게 모두 객체입니다. 따라서 리터럴 형식을 조금 더 많이 사용합니다.

마지막으로 Error 객체는 예외가 던져지면 알아서 생성되니 명시적으로 생성할 일이 드뭅니다.

3.3 내용

var myObject = {
    a: 2
};

myObject.a; // 2
myObject['a']; // 2

myObject객체에서 a 위치의 값에 접근하려면 '.' 또는 []을 사용합니다. 일반적으로. a 구문을 프로퍼티 접근, ['a'] 구문을 키 접근이라고 하죠.

또한 프로퍼티 접근은 식별자 호환 프로퍼티명이 와야 하지만 키 접근은 UTF-8/유니코드 호환 문자열이라면 모두 접근할 수 있습니다. 키 접근에서는 문자열로 나타낼 수 있어서 문자열을 프로그램으로 조합하는 일도 가능합니다.

var myObject = {
    a: 2
};

var idx;

idx = 'a';

myObject[idx] // 2

마지막으로 객체 프로퍼티 명은 언제나 문자열입니다. 따라서 문자열 이외의 다른 원시 값을 쓰면 우선 문자열로 변환됩니다. 따라서 헷갈리지 않도록 주의해야 합니다.

var myObject = {};
myObject[true] = 'foo';
myObject[3] = 'bar';

myObject['true']; // 'foo'
myObject['3']; // 'bar'

 

3.3.1 계산된 프로퍼티명

myObject [ ] 같은 프로퍼티 접근 구문은 myObject [prefix + name] 형태의 계산식 값으로 키 이름을 나타낼 때 유용하지만 리터럴 구문으로 객체 선성 시엔 별로 소용이 없습니다.

var prefix = 'foo';
var myObejct = {
    [prefix + 'bar']: 'hello'
};

myObejct['foobar']; // 'hello'

 

3.3.2 프로퍼티 vs 메서드

여타 언어에서 객체에 부속된 함수를 주로 '메서도'라고 부르고 자바스크립트 함수 역시 객체의 부속물이라 생각하기에 '프로퍼티 접근'에 대비되는 용어로 '메서드 접근' 이란 말을 종종 사용합니다.

하지만 엄밀히 말해 함수는 결코 객체에 속하는 것이 아니며, 객체 레퍼런스로 접근한 함수를 그냥 메서드라 칭하는 건 확대 해석에 속하게 됩니다.

따라서 객체에 존재하는 프로퍼티에 접근할 때마다 반환 값 타입에 상관없이 항상 프로퍼티 접근을 하고 이런 식으로 함수를 가져왔다고 해서 저절로 함수가 메서드가 되는 건 아닙니다.

 function foo() {
     console.log('foo');
 }

 var someFoo = foo // 'foo' 에 대한 변수 레퍼런스

 var myObject = {
     someFoo: foo
 }

 foo; // function foo() {}
 someFoo // function foo() {}
 myObject.someFoo // function foo() {}

위처럼 함수 표현식을 객체 리터럴의 한 부분으로 선언해도 이 함수가 저절로 객체에 달라붙는 건 아니며 해당 함수 객체를 참조하는 레퍼런스가 하나 더 생기는 것뿐입니다.

 

3.3.3 배열

배열도 [ ]로 접근하는 형태이지만 이미 언급한 대로 값을 저장하는 방법과 장소가 더 체계적입니다.

var myArray = ['foo', 42, 'bar'];

myArray.length; // 3
myArray[0] // 'foo'
myArray[2] // 'bar'

 

인덱스는 양수 지만 배열 자체는 객체여서 배열에 프로퍼티를 추가하는 것도 가능합니다. 그러나 프로퍼티를 추가해도 배열 길이에는 변함이 없습니다.

var myArray = ['foo', 42, 'bar'];

myArray.baz = 'baz';
myArray.length; // 3
myArray.baz = 'baz'

위처럼 인덱스를 쓰지 않고 일반적인 키/값 객체로 배열을 사용할 수도 있지만 나름대로 정해진 용도에 맞게 최적화되어 작동하므로 그다지 좋은 방법은 아닙니다.

 

3.3.4 객체 복사

function anotherFunction () {}
var anotherObject = {
    c: true
};
var anotherArray = [];
var myObejct = {
    a: 2,
    b: anotherObject, // 사본이 아닌 레퍼런스
    c: anotherArray, // 역시 레퍼런스
    d: anotherFunction
};

anotherArray.push(anotherObject, myObejct);

myObject의 사본은 얕은 복사(Shallow Copy), 깊은 복사(Deep Copy) 중 선택해야 합니다. 얕은 복사 후 생성된 새 객체의 a 프로퍼티는 원래 값 2가 그대로 복사되지만 b, c, d 프로퍼티는 원 객체의 레퍼런스와 같은 대상을 가리키는 또 다른 레퍼런스입니다.

깊은 복사를 하면 myObjcet는 물론이고 anotherObject와 anotherArray까지 모조리 복사합니다. 그러나 여기서 문제는 anotherArray가 anotherObject와 myObject를 가리키는 레퍼런스를 갖고 있으므로 원래 레퍼런스가 보존되는 게 아니라 이들까지 함께 복사됩니다. 따라서 환형 참조 형태가 되어 무한 복사의 구렁텅이에 빠지고 말죠.

그럼 이 난관을 어떻게 극복해야 할까요? 책에서는 'JSON 안전한 객체'는 쉽게 복사할 수 있으므로 하나의 대안이 될 수 있다고 말하고 있어요.

var newObj = JSON.parse(JSON.stringify(someObj))

그러나 이것만으로도 충분하지 않을 경우가 있다고 합니다.

한편, 얕은 복사는 이해하기 쉽고 별다른 이슈가 없기에 ES6부터는 Object.assign() 메서드를 제공합니다.

var newObj = Object.assign({}, myObject);

newObj.a; // 2
newObj.b === anotherObject // true
newObj.c === anotherArray // true
newObj.d === anoterFunction // true

 

3.3.5 프로퍼티 서술자

var myObject = {
    a: 2
};

Object.getOwnPropertyDescriptor(myObject, 'a');
// {
// value: 2
// writable: true
// enumerable: true
// configuarable: true
// }

위와 같이 프로퍼티 서술자를 조회해 보면 value, writable, enumerable, configurable이라는 특징들이 보입니다.

defineProperty() 메서드를 이용해서 새로운 프로퍼티를 추가하거나 기존 프로퍼티의 특성을 원하는 대로 수정할 수 있습니다.. 하지만 프로퍼티 서술자의 특성을 직접 수정해야 하는 경우가 아니면 굳이 이런 식으로 프로퍼티를 추가할 일은 거의 없습니다.

var myObejct = {};

Object.defineProperty(myObejct, 'a', {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
});

myObejct.a // 2

 

쓰기 가능

var myObejct = {};

Object.defineProperty(myObejct, 'a', {
    value: 2,
    writable: false, // 쓰기 금지
    configurable: true,
    enumerable: true
});

myObejct.a = 3;
myObejct.a; // 2

위처럼 프로퍼티의 쓰기가 불가능 해 집니다.

설정 가능

var myObejct = {};

Object.defineProperty(myObejct, 'a', {
    value: 2,
    writable: true,
    configurable: false, // 설정 불가
    enumerable: true
});

myObejct.a = 3;
myObejct.a; // 3

Object.defineProperty(myObejct, 'a', {
    value: 4,
    writable: true,
    configurable: true,
    enumerable: true
}) // TypeError

위처럼 프로퍼티를 설정할 수가 없습니다.

열거 가능

for 루프를 통해서 객체 프로퍼티를 열거할 때에 enumerable이 false인 프로퍼티는 보이지가 않습니다.

var myObject = {}
Object.defineProperty(myObject, 'a', {value: 2, enumerable: true});
Object.defineProperty(myObject, 'b', {value: 3, enumerable: false});

for (var k in myObject) {
    console.log(k, myObject[k]);
}

// 'a' 2

myObject.a // 2
myObject.b // 3

 

3.3.6 불변성

불변성은 객체 자신과 직속 프로퍼티 특성만 불변으로 만들 뿐 다른 객체(배열, 객체, 함수 등)를 가리키는 레퍼런스가 있을 때 해당 객체의 내용까지 불변으로는 만들지 못합니다.

immutableObject.foo; // [1, 2, 3]
immutableObject.foo.push(4)
immutableObject.foo; // [1, 2, 3, 4]

immutableObject는 불변으로 생성되어 보호되는데 immutableObject.foo의 내용까지 보호하진 못합니다.

객체 상수

var myObject = {}
Object.defineProperty(myObject, 'FAVORITE_NUMBER', {
    value: 42,
    writable: false,
    configurable: false
})

writable, configurable를 둘 다 false로 하면 객체 프로퍼티를 다음과 같이 상수처럼 쓸 수 있습니다.

확장 금지

var myObject = {
    a: 2
};

Object.preventExtensions(myObject);

myObject.b = 3;
myObject.b // undefined

preventExtensions을 해주게 되면 b를 추가해도 추가가 되지 않습니다.

봉인

Object.seal()는 봉인된 객체를 생성합니다. 즉, 어떤 객체에 대해 Object.preventExtensions()를 실행하고 프로퍼티를 전부 configurable: false로 처리됩니다. 그러나 값은 얼마든지 수정이 가능합니다.

동결

Object.freeze()는 Object.seal() 하고 writable: false까지 처리해 버려서 수정 및 추가도 안됩니다.

 

3.3.7 [[Get]]

var myObject = {
    a: 2
};

myObject.a // 2

myObject.a 는 누가 봐도 프로퍼티 접근이지만 보이는 것처럼 a란 이름의 프로퍼티를 myObject에서 찾지 않습니다. 명세에 따르면 실제로 이 코드는 myObject에 대해 [[Get]]연산을 합니다. 만약 주저인 프로퍼티 값을 어떻게 해도 찾을 수 없으면 [[Get]] 연산은 undefined를 반환합니다.

 

3.3.8 [[Put]]

[[Put]] 을 실행하면 주어진 객체에 프로퍼티가 존재하는지 등 여러 가지 요소에 따라 이후 작동 방식이 달라집니다.

1. 프로퍼티가 접근 서술자인가? 맞으면 세터를 호출합니다.

2. 프로퍼티가 writable: false인 데이터 서술자인가? 맞으면 실패합니다.

3. 이외에는 프로퍼티에 해당 값을 세팅합니다.

 

3.3.9 게터와 세터

[[Put]] 과 [[Get]] 기본 연산은 이미 존재합니다. 그러나 ES5부터 게터/세터를 오버 라이딩할 수 있게 되었죠.

var myObject = {
    // 'a' 게터를 정의
    get a() {
        return 2;
    }
}

Object.defineProperty(myObject, 'b', {
    get: function() {
        return this.a * 2
    }
})

myObject.a // 2
myObject.b // 4
var myObject = {
    get a() {
        return this._a;
    }

    set a(var) {
        this._a = val * 2;
    }
}

myObject.a = 2;
myObject.a; // 4

 

3.3.10 존재 확인

var myObject = {
    a: undefined
}

myObject.a // undefined
myObject.b // undefined

('a' in myObject) // true
('b' in myObject) // false

myObject.hasOwnProperty('a') // true
myObject.hasOwnProperty('b') // false

이렇게 프로퍼티 값이 undefined로 적용되었을 때(a: undefined) 어떤 객체에 없는 프로퍼티 값을 불러도(myObject.b) undefined로 출력되어 나와 헷갈릴 때가 있습니다. 그때는 위와 같이 확인해 보면 프로퍼티가 객체에 있는지 없는지 여부를 존재 확인할 수 있습니다.

 

3.4 순회

for 루프는 열거 가능한 객체 프로퍼티를 차례로 순회합니다.

var myArray = [1, 2, 3];
for (var v of myArray) {
    console.log(v);
}
// 1
// 2
// 3

위와 같이 for.. of 루프는 순회할 원소의 순회자 객체가 있어야 합니다. 순회당 한번씩 이 순회자 객체의 next() 메서드를 호출하여 연속적으로 반환 값을 순회 합니다. 배열은 @@iterator가 내장된 덕분에 for .. of 루프를 사용할 수 있습니다.

var myArray = [1, 2, 3];
var it = myArray[Symbol.iterator]();

it.next(); {value: 1, done: false}
it.next(); {value: 2, done: false}
it.next(); {value: 3, done: false}
it.next(); {done: true}

이거는 사실 디자인 패턴 중 Iterator pattern에 속하는데 배열은 이 패턴을 내장(@@iterator) 하고 있다고 생각하면 쉬울 것 같습니다.

이터레이터 패턴

 

이터레이터 패턴 (iterator pattern)

이터레이터 패턴 (iterator pattern) 컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에 접근할 수 있는 방법을 제공한다. 컬렉션 객체 안에 들어있는 모든 항목에 접근하는 방식이..

jusungpark.tistory.com

 

정리

- 자바스크립트 캑체는 리터럴 형식, 생성자 형식으로 만들 수 있습니다.

- 객체는 6개의 원시 타입 중 하나이고 함수를 비롯한 하위 타입이 있습니다. (Number, Array...)

- 객체는 키/값을 모아놓고 프로퍼티를 통해 접근할 수 있습니다.(.(프로퍼티 접근), [] (키 접근))

- 프로퍼티는 프로퍼티 서술자를 통해 제어 가능한 writable, configurable 등 특정한 속성을 지닌다. 그리고 여러 가지 방법(seal(), freeze()) 를 이용하여 불변성을 적용할 수 있습니다.

- 프로퍼티는 게터/세터로 접근자 프로퍼티 형태를 취할 수도 있습니다.

- ES6부터는 for.. of 구문에서 한 번에 하나씩 다음 데이터 값으로 이동하는 next() 메서드를 가진 내장/커스텀 @@iterator객체를 통해 순회가 가능합니다.

 

이상 포스팅을 마치겠습니다.

Comments