13 분 소요

개체

개체는 속성의 집합이며 {} 내부에 속성을 정의합니다. .을 사용하거나 속성명()으로 속성값에 접근할 수 있습니다.

1
2
3
4
5
6
const empty = {}; // 빈 개체
const user = {
    name: 'Lee' // name 속성이 있는 개체
};
console.log('개체 속성 접근, . 사용', user.name === 'Lee');
console.log('개체 속성 접근, 속성명 사용', user['name'] === 'Lee');

자바스크립트의 모든 것들은 개체이며, 심지어 함수도 개체입니다.

  1. 함수를 속성으로 사용할 수도 있으며, 이러한 함수를 특별히 메서드라고 합니다.
  2. this를 사용하여 개체 자신을 나타낼 수 있습니다.
  3. 개체alert()으로 출력하면 object Object만 표시됩니다.

    image

    하지만, console.log()로 출력하면 트리 형태로 표시되는데요, 이를 확장하면 개체의 속성명과 속성값을 확인할 수 있습니다.

    image

  4. new Object()개체를 생성한뒤 뒤늦게 속성들을 설정하는 방식도 있으나, 코딩 계약에 좋지 않아 잘 사용하지 않습니다.
  5. 개체 내부에서만 사용하는 속성은 관례적으로 밑줄(_)을 접두어로 사용합니다. 설계자가 캡슐화를 위해 조치한 것이니(언어적 차원에서 막을 수 있다면 더 좋았겠지만, 방도가 마땅치 않아서 한 조치이니), 밑줄(_) 접두어가 있다면 외부에서는 사용하지 마세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 리터럴 방식 개체 생성
const user1 = {
    name: 'Lee',
    number: '123-4567',
    getName: function() { // #1. 함수를 사용할 수 있으며 메서드라고 합니다.
        return this.name; // #2. this는 자기 자신을 나타냅니다.
    }
};
alert(user1); // #3. object Object
console.log(user1); // #3. Object. 트리를 확장하여 내부 속성값을 확인할 수 있습니다.{name: 'Lee', number: '123-4567', getName: f}

console.log('개체 메서드 호출 user1.getName()', user1.getName()); // Lee

// #4. new Object 방식 
const user2 = new Object();
user2.name = 'Lee';
user2.getName = function() {
    return this.name;
}; 
console.log('개체 메서드 호출 user2.getName()', user2.getName()); // Lee

// #5. 내부에서만 사용하는 속성은 관습적으로 _을 붙임
const user3 = {
    _name1: 'Bruce', // 외부에서 사용하지 마세요.
    _name2: 'Lee', // 외부에서 사용하지 마세요.
    getName: function() {
        return `${this._name1} ${this._name2}`
    }
} 
console.log('개체 메서드 호출 user3.getName()', user3.getName()); // Bruce Lee

개체 속성 접근

속성명은 변수명에서 사용할 수 없는 숫자나 -, 공백 문자 조차도 사용할 수 있습니다.

심지어, for, let과 같은 예약어를 사용할 수 있으며, __proto__만 사용할 수 없습니다.

각 속성은 보통 마침표로 접근할 수 있는데요, 숫자나 -, 공백 문자를 사용한 경우에는 []에 속성명()을 이용해야 접근할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
const user = {
    name: 'Lee',
    'addr': 'Seoul', // 속성명을 문자열로 선언할 수 있습니다.
    1 : '1', // 속성명을 숫자로 선언할 수 있습니다.
    2 : '2',
    'my number': '123-4567' // 속성명에 공백 문자등 일반적으로 사용할 수 없는 문자가 있으면 문자열로만 선언할 수 있습니다.
};
console.log("개체 속성 접근 user.name === 'Lee'", user.name === 'Lee'); // 마침표로 속성값에 접근합니다.
console.log("개체 속성 접근 user.addr === 'Seoul'", user.addr === 'Seoul'); 
console.log("개체 속성 접근 user[1] === '1'", user[1] === '1'); // 속성명이 숫자이면 배열처럼 접근할 수 있습니다.
console.log("개체 속성 접근 user[2] === '2'", user[2] === '2'); 
console.log("개체 속성 접근 user['my number'] === '123-4567'", user['my number'] === '123-4567'); // 일반적으로 사용할 수 없는 속성명이면, [] 로 접근할 수 있습니다. 

개체 속성 추가/삭제

속성을 동적으로 추가/삭제 할 수도 있습니다.

1
2
3
4
5
6
const user = {};
user.name = 'Lee'; // name 속성이 없으면 추가합니다.
console.log("속성 추가 user.name === 'Lee'", user.name === 'Lee'); // name 속성이 추가되었습니다.

delete user.name;
console.log("속성 삭제 후 user.name === undefined", user.name === undefined); // name 속성이 삭제되었습니다.

속성 나열 : for-in

for-in으로 속성명을 나열할 수 있고, 이를 개체의 키로 사용하면 속성값을 확인할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
const user = {
    name: 'Lee',
    'addr': 'Seoul', 
    1 : '1',
    2 : '2',
    'my number': '123-4567' 
};

for (let prop in user) { // prop은 속성명, user[prop]은 속성값
    console.log('속성명 :' + prop , '속성값 :' + user[prop]);
}

배열에도 사용할 수 있지만, 배열 요소외에 다른 속성이 있다면 함께 나열되기 때문에, 배열에서는 잘 사용하지 않습니다.

1
2
3
4
5
const arr = ['one', 'two', 'three'];
arr.extraData = "추가 정보 입니다."; // 속성을 추가합니다.
for (let prop in arr) { // 배열 요소와 추가 속성이 나열됩니다.
    console.log('속성명 :' + prop , '속성값 :' + arr[prop]);
}

상기 코드를 실행하면, 다음처럼 배열 요소와 extraData가 나열되는 것을 확인할 수 있습니다.

image

defineProperty(), getOwnPropertyDescriptor()

개체의 속성은 세부적으로 다음과 같은 플래그가 있습니다.

항목 내용
writable true이면 읽기/쓰기가 가능하고, false이면 읽기만 가능합니다.
enumerable true이면 for-in으로 나열이 가능합니다.
configurable true 이면 속성 삭제나 플래그 수정이 가능하고, false이면 불가능 합니다. writable, enumerablefalse로 만들며 다시 수정할 수 없게 만듭니다.

다음은 username속성이 열거되지 않도록 defineProperty()를 이용해서 enumerable을 설정한 예입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const user = {
    name: 'Lee',
    addr: 'Seoul'
};

for (let prop in user) {
    console.log('속성명 :' + prop , '속성값 :' + user[prop]); // name과 addr이 열거됩니다.
}

Object.defineProperty(user, 'name', {
    enumerable: false // name은 열거되지 않게 합니다.
});
for (let prop in user) {
    console.log('name은 열거되지 않습니다. 속성명 :' + prop , '속성값 :' + user[prop]);
}

const descriptor = Object.getOwnPropertyDescriptor(user, 'name');
console.log('name의 enumerable은 false 입니다.', descriptor.enumerable === false);

개체 비교와 대입

개체배열===비교를 할때 동일한 개체인지 검사합니다. 값이 동일한 지 검사하는게 아니라 개체 자체가 동일한지를 검사합니다.

따라서, 값이 동일한지를 검사하려면, #1과 같이 하위 속성을 모두 뒤져서 기본 타입끼리 검사해야 합니다.

#2와 같이 =로 대입하면 동일 개체가 됩니다. 한 개체의 새로운 별칭은 만든 셈입니다.(이를 얕은 복사라 합니다.) 동일한 개체이므로 === 비교시 true이며, 한쪽의 속성을 바꾸더라도, 다른쪽에 변경사항이 반영됩니다.

1
2
3
4
5
6
7
8
9
10
const user1 = {name: 'Lee'};
const user2 = {name: 'Lee'};

console.log('값은 같지만 다른 개체입니다. user1 !== user2', user1 !== user2); 
console.log('개체의 하위 속성을 모두 뒤져서 기본 타입끼리  검사해야 합니다. user1.name === user2.name', user1.name === user2.name); // #1

const user3 = user1; // #2. 대입하면 동일 개체입니다.
console.log('개체를 대입하면 동일 개체입니다. user1 === user3', user1 === user3);
user1.name = 'Kim';
console.log('user1을 수정하면, 동일 개체인 user3도 반영됩니다.', user3.name === 'Kim');

개체 복제/동결

개체 비교와 대입에서 언급한 것처럼 =로 대입하면 개체는 얕은 복사를 하며, 동일 개체를 참조합니다.

다음 코드에서, user2 = user1은 동일 개체를 참조하며, user2를 수정하면, user1도 수정됩니다.

1
2
3
4
const user1 = {name: 'Lee'};
const user2 = user1;
user2.name = 'Kim';
console.log("동일 개체를 참조합니다 user1.name === 'Kim'", user1.name === 'Kim'); // true. user2를 수정했지만, user1도 수정되었습니다.

동일 개체가 아니라 값이 동일한 복제본을 만드려면, 내부 속성들을 일일이 복제해야 하며, Object.assign()(ECMAScript6)을 이용할 수 있습니다.(또한 Spread를 이용하여 개체 속성을 복제할 수도 있습니다.) 하지만 하위 개체는 여전히 얕은 복사를 하니 주의해야 합니다. 다음 예에서 name은 복제되었지만, addr은 하위 개체이기 때문에 복제되지 않고 여전히 같은 개체를 참조합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const user1 = {
    name: 'Lee',
    detail: {
        addr: 'Seoul'
    }
};
const user2 = Object.assign({}, user1); // 복제합니다.
user2.name = 'Kim';
user2.detail.addr = 'Busan';

console.log("개체 복제 user1.name === 'Lee'", user1.name === 'Lee'); // true. user2를 수정했지만, user1은 수정되지 않습니다.
console.log("개체 복제 user2.name === 'Kim'", user2.name === 'Kim');

console.log("하위 개체는 여전히 참조 user1.detail.addr === 'Busan'", user1.detail.addr === 'Busan'); // true. 하위 개체는 얕은 복사됩니다.
console.log("하위 개체는 여전히 참조 user2.detail.addr === 'Busan'", user2.detail.addr === 'Busan'); 

const user3 = {...user1}; // spread를 이용하여 복제합니다.
user3.name = 'Park';
user3.detail.addr = 'Incheon';

console.log("개체 복제 user1.name === 'Lee'", user1.name === 'Lee'); // true. user3를 수정했지만, user1은 수정되지 않습니다.
console.log("개체 복제 user3.name === 'Park'", user2.name === 'Park');

console.log("하위 개체는 여전히 참조 user1.detail.addr === 'Incheon'", user1.detail.addr === 'Incheon'); // true. 하위 개체는 얕은 복사됩니다.
console.log("하위 개체는 여전히 참조 user2.detail.addr === 'Incheon'", user2.detail.addr === 'Incheon'); 

하위 개체까지 복제하고 싶으면, 하위 개체도 일일이 Object.assign()을 이용하거나, Spread를 이용하여 일일이 복제해야 합니다. 좀 번거롭죠. 그래서 대안으로 속도 성능은 떨어지지만, JSON을 이용하면 하위 개체도 복제할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const user1 = {
    name: 'Lee',
    detail: {
        addr: 'Seoul'
    }
};
const user2 = JSON.parse(JSON.stringify(user1)); // 하위 개체의 속성까지 복제합니다.
user2.name = 'Kim';
user2.detail.addr = 'Busan';

console.log("개체 복제 user1.name === 'Lee'", user1.name === 'Lee'); 
console.log("개체 복제 user2.name === 'Kim'", user2.name === 'Kim');

console.log("JSON으로 하위 개체도 복제 user1.detail.addr === 'Seoul'", user1.detail.addr === 'Seoul'); // 하위 개체도 복제됩니다.
console.log("JSON으로 하위 개체도 복제 user2.detail.addr === 'Busan'", user2.detail.addr === 'Busan');    

하위 속성까지 꼼꼼하게 복제하면 개체의 진정한 복제본이 될까요? 아쉽지만 아닙니다. __proto__가 남아 있습니다.

생성자 함수를 이용하여 개체를 생성하면, 내부적으로 __proto__가 생성되는데요, assign()이나 Spread로 복제하면 #1처럼 __proto__Object로 초기화 되어 버립니다. __proto__도 동일하게 하려면, #2처럼 Object.create()를 함께 이용해야 합니다.(prototype 참고)

1
2
3
4
5
6
7
8
9
10
function User(name) { // 생성자 함수.
    this.name = name;
}
const user1 = new User('Lee'); 
const user2 = {...user1}; 
const user3 = Object.assign(Object.create(User.prototype), user1); // #2. 동일한 prototype을 사용할 수 있도록 Object.create()를 이용합니다.

console.log('생성자 함수 User로 생성했습니다.', user1 instanceof User);
console.log('spread로 개체 속성을 복제한 개체입니다.', user2 instanceof Object); // #1. Object로 초기화 되어 있습니다.
console.log('create()와 assign()으로 복제했습니다.', user3 instanceof User); // #2. Object.create()를 이용하면 user개체로 복제됩니다.

Object.freeze()개체를 수정할 수 없게끔 동결시킬 수 있습니다. 하지만, 하위 개체에는 적용되지 않습니다.

이외에도 개체 수정과 관련하여 다음과 같은 메서드 들이 있습니다.

항목 내용
Object.preventExtensions() 개체에 새로운 속성 추가를 막습니다.
Object.seal() 새로운 속성 추가나 기존 속성 삭제를 막습니다.
Object.freeze() 새로운 속성 추가나 기존 속성 삭제, 속성값 수정을 막습니다.
Object.isExtensible() preventExtensions()인지 확인합니다.
Object.isSealed() seal()인지 확인합니다.
Object.isFrozen() freeze()인지 확인합니다.

getter, setter(ECMAScript5)

gettersetter를 이용하면 속성에 값을 저장하거나 불러올때 메서드 형태로 사용할 수 있고, 속성에 대한 접근 통제나 유효성 검사를 좀 더 캡슐화할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
    const user = {
    _name: '', // 실제 name 값을 저장합니다.
    get name() {
        return this._name;
    }, 
    set name(name) {
        // 값을 설정하기 전에 유효성 검사를 할 수 있고, 수정할 수도 있습니다.
        this._name = 'name is ' + name;
    }
};

user.name = 'Lee';
console.log('setter로 이름을 수정했습니다.', user.name === 'name is Lee');

JSON

JSON은 “자바스크립트 객체 문법으로 구조화된 데이터를 표현”하기 위한 표준 포맷입니다.

JSON을 이용하면, stringify()함수로 개체를 문자열로 만들고, parse()함수로 문자열을 개체로 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
    x: 10,
    y: 20,
    value: '문자열',
    datas: [1, 2, 3]
};

const str = JSON.stringify(obj);
console.log('JSON으로 만든 문자열', str === '{"x":10,"y":20,"value":"문자열","datas":[1,2,3]}');  

const result = JSON.parse(str);
console.log('JSON 문자열로부터 개체 생성', result.x === 10 && result.y === 20 && result.value === '문자열' && result.datas[0] === 1 && result.datas[1] === 2 && result.datas[2] === 3);

개체에 toJSON() 함수가 구현되어 있다면, stringify() 호출시 개체에 구현된 toJSON()을 호출하므로, 개체마다 커스터마이징 할 수 있습니다.

1
2
3
4
5
6
7
const obj = {
    name: 'Lee',
    toJSON: function() {
        return `name is ${this.name}`;
    }
};
console.log('toJSON을 이용합니다.', JSON.stringify(obj) === '"name is Lee"');

개체의 생성자 함수

동일한 구조의 개체를 여러개 생성하고자 할때 매번 리터럴 방식으로 생성하면, 속성값 선언이나 메서드 선언 코드가 중복 될 수 있습니다.

1
2
3
4
5
6
7
8
9
10
const user1 = {
    name: 'Kim',
    number: '123-4567',
    getName: function() {return this.name;} // 메서드 선언 코드가 중복됩니다.
};
const user2 = {
    name: 'Lee',
    number: '111-2222',
    getName: function() {return this.name;} // 메서드 선언 코드가 중복됩니다.
};

이러한 경우에는 생성자 함수를 통해 개체를 생성하면 메서드 선언 코드 중복을 어느정도 해결할 수 있습니다.

  1. 생성자 함수는 일반 함수와 구분하기 위해 관습적으로 Pascal 표기법을 사용합니다.
  2. this는 생성해서 리턴되는 개체를 지칭합니다.
  3. new생성자 함수를 호출합니다.
  4. 암시적으로 this를 생성하여 리턴하는 함수라고 이해하셔도 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function User(name, number) { // #1. 일반 함수와 구분하기 위해 Pascal 표기법을 사용합니다.
    this.name = name; // #2. this는 생성될 개체입니다.
    this.number = number;
    this.getName = function() {
        return this.name;
    };
    // #4. 암시적으로 this 개체를 리턴합니다.
}

const user1 = new User('Kim', '123-4567'); // #3. new로 함수를 호출합니다.
const user2 = new User('Lee','111-2222');

console.log('user1.getName()', user1.getName()); // Kim
console.log('user2.getName()', user2.getName()); // Lee

생성자 함수로 메서드 선언 코드 중복을 어느정도 해결할 수 있습니다만, 메서드 자체가 메모리에 중복 생성되는 문제가 있습니다.

image

name이나 number같은 데이터를 저장한 속성은 개체마다 따로 존재하는게 당연해 보이는데, getName()과 같은 메서드까지 따로 존재하는 것은 좀 낭비죠.

이렇게 쓸데없는 메모리 낭비를 줄이려면, 메서드를 프로토타입 개체에 정의하는 방법이 있습니다. 자세한 방법은 즉시 실행 함수를 이용한 개체 선언을 참고 하세요.

image

또한, 생성자 함수를 호출할때 new를 사용하지 않는다면, 일반 함수처럼 호출되니 주의해야 합니다.

만약 new를 생략한다면,

  1. 함수내에서 직접적인 return이 없으므로 userundefined입니다.
  2. this는 전역 개체이므로 전역 개체name속성을 추가합니다.
1
2
3
4
5
6
7
function User (name) { 
    this.name = name; 
}
const user = User('Kim'); // new를 생략했습니다.

console.log('리턴값이 없으므로 user는 undefined 입니다', user === undefined); // #1
console.log('this는 전역 개체이므로 전역 개체에 name을 저장합니다.', name === 'Kim'); // #2

상기처럼 의도치 않게 동작하니 생성자 함수호출시 new를 빼먹지 않도록 주의해야 합니다.

기본 타입생성자 함수로 생성할 수 있습니다.

하지만 개체이다 보니 기본 타입과 직접 비교 할 수 없어 형변환 하여야 하며, 값이 같더라도 === 비교시 다른 값으로 평가됩니다.(개체 비교 참고)

1
2
3
4
5
6
7
8
9
10
const num1 = new Number(1);
const num2 = new Number(1);

console.log('Number()로 기본타입 개체를 생성합니다. 기본 타입과 검사하려먼 형변환 해야 합니다. 개체이므로 실제값은 동일하지만 === 이진 않습니다.', num1 !== 1 && Number(num1) === 1, num1 !== num2);

const str1 = new String('Kim');
const str2 = new String('Kim');  

console.log('String()으로 기본타입 개체를 생성합니다. 기본타입과 검사하려면 형변환 해야 합니다. 개체이므로 실제값은 동일하지만 === 이진 않습니다.', str1 !== 'Kim', String(str2) === 'Kim', str1 !== str2);

속성 축약 표현(ECMAScript6)

개체의 속성명과 변수가 이름이 같은 경우 축약하여 표현할 수 있습니다.

1
2
3
4
5
6
const x = 10;
const y = 20;
const obj = {
    x: x, 
    y: y
};

을 다음과 같이 축약하여 변수만 나열하면 됩니다.

1
2
3
4
5
6
7
const x = 10;
const y = 20;
const obj = {
    x, // x: x 와 동일합니다.
    y // y: y 와 동일합니다.
};
console.log('속성 축약 표현', obj.x === 10 && obj.y === 20);

속성명 동적 생성(ECMAScript6)

리터럴 방식으로 개체 생성시 속성명()을 동적으로 생성할 수 있습니다. 이때 속성명 표현식은 []로 묶습니다.

1
2
3
4
5
const index = 10;
const obj = {
    [`myData-${index + 1}`]: 1 // 속성명을 myData-11로 만듭니다.
};
console.log('속성명 동적 생성', obj['myData-11'] === 1);

메서드 축약 표현(ECMAScript6)

개체의 메서드 선언시 function을 생략할 수 있습니다.

1
2
3
4
5
6
7
8
const obj1 = {
    myMethod: function() {
    },
};
const obj2 = {
    myMethod() { // 축약해서 표현합니다.
    },
};

Date

Date 개체는 날짜를 처리합니다.

내부적으로는 UTC 1970년 1월 1일 0시 0분 0초 이후의 밀리초로 데이터를 관리하는데요, 이때 month는 0 base 여서 0이면 1월이고, 1이면 2월이고 그렇습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const now = new Date(); // 현재. 혹은 Date.now();
const milli = new Date(2 * 60 * 60 * 1000); // UTC 1970년 1월 1일 0시 0분 0초 후 2시간 뒤
const ymd = new Date('2024-05-06');
const date = new Date(2023, 4, 6, 10, 20, 30, 456); // monthIndex 0부터 시작함. 즉 4는 5월. 2023-05-06 10시20분30초 456밀리초
const parse = new Date('2024-05-06T10:20:30.456'); // T 시간 구분 기호

console.log('현재 시간', now);
console.log(`UTC 시분초 밀리초 ${now.getUTCHours()}:${now.getUTCMinutes()}:${now.getUTCSeconds()}.${now.getUTCMilliseconds()}`);
console.log('기준시에서 2시간뒤', milli);
console.log('지정 년월일', ymd);
console.log('지정 년월일시분초밀리초', date);
console.log('파싱한 시간', parse);

console.log(`로컬 년월일 ${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`);
console.log(`로컬 시분초 밀리초 ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}.${date.getMilliseconds()}`);
console.log(`로컬 요일 ${date.getDay()}`); // 0이 일요일

두 날짜의 차이는 Number로 변환한 뒤 계산할 수 있습니다. 계산 결과는 밀리초입니다.

1
2
3
4
5
6
7
8
const day = new Date('2024-05-06');
day.setDate(day.getDate() + 2)
console.log('2일 뒤', day.getDate() === 8);

const day1 = new Date('2024-05-06');
const day2 = new Date('2024-05-08');

console.log('날짜 차이', (Number(day2) - Number(day1)) === 2 * 24 * 60 * 60 * 1000); // 2일에 해당 하는 밀리초

서식화된 출력을 위해선 약간의 트릭이 필요합니다. 예를 들어 5월은 1자리수인데, 05와 같이 0을 붙여 표시하는게 좋죠. 이런 경우 앞에 0을 붙여서 문자열로 만든뒤, 뒤의 2자리만 취하여 원하는 결과를 얻을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
const date = new Date('2024-05-06');

const toYYYYMMDD = (date) => {
    const yyyy = date.getFullYear();
    const mm = ('0' + (date.getMonth() + 1)).slice(-2); // 앞에 0을 붙여서 문자열로 만들고, 뒤의 2자리만 취함
    const dd = ('0' + date.getDate()).slice(-2);
    return `${yyyy}-${mm}-${dd}`;
};

console.log('기본 형태', date); 
console.log('yyyy-mm-dd', toYYYYMMDD(date) === '2024-05-06'); 

댓글남기기