해당 글의 내용은 poiemaweb 을 읽고 요약하여 재정리한 내용입니다.
이번에는 JavaScript lesson 10, 11에 해당되는 객체, 객체와 변경불가성에 대해 알아보겠습니다.
10. 객체(Object)
객체란?
JavaScript는 객체 기반의 스크립트 언어이며, 거의 모든 것이 객체로 이루어져 있습니다.
JavaScript의 객체는 키(key)와 값(value)으로 구성된 프로퍼티(property)들의 집합입니다.
프로퍼티 값으로 함수를 사용할 수도 있으며 프로퍼티 값이 함수일 경우, 일반 함수와 구분하기 위해 메소드라 부릅니다.
객체는 데이터를 의미하는 프로퍼티와 데이터를 참조하고 조작할 수 있는 동작을 의미하는 메소드로 구성된 집합 입니다.
프로퍼티
프로퍼티 키와 프로퍼티 값으로 구성되며, 프로퍼티 키는 프로퍼티를 식별하기 위한 식별자 입니다.
프로퍼티 키와 값은 다음과 같이 이루어져 있습니다.
프로퍼티 키
: 빈 문자열을 포함하는 모든 문자열 또는 symbol 값
프로퍼티 값
: 모든 값
프로퍼티 키에 이외의 값을 지정하면 암묵적으로 타입이 변환되어 문자열이 됩니다.
중복으로 선언 시 나중에 선언한 프로퍼티가 덮어쓰게 되며, 배열과는 달리 객체는 프로퍼티를 열거할 때 순서를 보장하지 않습니다.
메소드
프로퍼티 값이 함수일 경우, 일반 함수와 구분하기 위해 메소드라 부릅니다.
즉, 메소드는 객체에 제한되어 있는 함수를 의미합니다.
- 메소드 vs 함수 차이 - 호출의 차이
var obj = {
hello1: function() {
console.log('hello1() 호출');
}
};
function hello2() {
console.log('hello2() 호출');
};
hi.hello1(); // 메소드
hello2(); // 함수
객체 생성 방법
JavaScript는 프로토타입 기반 객체 지향 언어로 클래스라는 개념이 없고 별도의 객체 생성 방법이 존재합니다.
객체 리터럴
가장 일반적인 방식이며, 중괄호 {}
를 사용합니다.
{}
내에 1개 이상의 프로퍼티를 작성하면 해당 프로퍼티가 추가된 객체를 생성할 수 있으며, 아무것도 기술하지 않으면 빈 객체가 생성 됩니다.
형태는 다음과 같습니다.
var person = {
name: 'Kim',
gender: 'male',
sayHello: function () {
console.log('Hi! My name is ' + this.name);
}
};
Object 생성자 함수
new
연산자와 Object 생성자 함수를 호출하여 빈 객체를 생성할 수 있으며, 빈 객체 생성 이후에 프로퍼티 또는 메소드를 추가하여 객체를 완성합니다.
생성자(constructor) 함수란 new
키워드와 함께 객체를 생성하고 초기화하는 함수이며, 이를 통해 생성된 객체를 인스턴스(instance)라 한다.
일반 함수와 생성자 함수를 구분하기 위해 생성자 함수의 이름은 파스칼 케이스(PascalCase)를 사용하는 것이 일반적 입니다.
형태는 다음과 같습니다.
// 빈 객체의 생성
var person = new Object();
// 프로퍼티 추가
person.name = 'Lee';
person.gender = 'Female';
person.sayHello = function () {
console.log('Hi! My name is ' + this.name);
};
하지만 개발자가 일부러 Object 생성자 함수를 사용해 객체를 생성해야 할 일은 거의 없습니다.
객체 리터럴 방식으로 생성된 객체는 빌트인(Built-in) 함수인 Object 생성자 함수로 객체를 생성하는 것을 단순화시킨 축약 표현이기 때문 입니다.
생성자 함수
생성자 함수를 사용하면 객체를 생성하기 위한 템플릿(클래스)처럼 사용하여 프로퍼티가 동일한 객체 여러 개를 간편하게 생성할 수 있습니다.
형태는 다음과 같습니다.
// 생성자 함수
function Person(name, gender) {
var married = true; // private
this.name = name; // public
this.gender = gender; // public
this.sayHello = function(){ // public
console.log('Hi! My name is ' + this.name);
};
}
// 인스턴스의 생성
var person1 = new Person('Lee', 'Female');
var person2 = new Person('Kim', 'male');
생성자 함수 이름은 일반적으로 대문자로 시작하며, 프로퍼티 또는 메소드명 앞에 기술한 this
는 생성자 함수가 생성할 인스턴스(instance)를 가리킵니다.
this
에 연결되어 있는 프로퍼티와 메소드는 public
(외부에서 참조 가능)합니다.
생성자 함수 내에서 선언된 일반 변수는 private
(외부에서 참조 불가능)합니다.
또, 생성자 함수를 선언하고 new
연산자를 붙여서 호출하면 해당 함수는 생성자 함수로 동작 합니다.
객체 프로퍼티 접근
프로퍼티 키
프로퍼티 키는 문자열이므로 따옴표‘’
또는 “”
를 사용하지만, JavaScript에서 사용 가능한 유효한 이름인 경우, 따옴표를 생략할 수 있습니다.
var person = {
'first-name': 'Ung-mo',
'last-name': 'Lee',
gender: 'male',
1: 10,
function: 1 // OK. 하지만 예약어는 사용하지 말아야 한다.
};
예약어는 프로퍼티 키로 사용할 경우 예상치 못한 에러가 생길 수 있기에 사용하지 않는 것이 좋습니다.
프로퍼티 값 읽기
값에 접근하는 방법은 마침표(.) 표기법
과 대괄호([]) 표기법
이 있습니다.
var person = {
'first-name': 'Ung-mo',
'last-name': 'Lee',
gender: 'male',
1: 10
};
console.log(person);
console.log(person.first-name); // NaN: undefined-undefined
console.log(person[first-name]); // ReferenceError: first is not defined
console.log(person['first-name']); // 'Ung-mo'
console.log(person.gender); // 'male'
console.log(person[gender]); // ReferenceError: gender is not defined
console.log(person['gender']); // 'male'
console.log(person['1']); // 10
console.log(person[1]); // 10 : person[1] -> person['1']
console.log(person.1); // SyntaxError
대괄호([]) 표기법을 사용하는 경우, 대괄호 내에 들어가는 프로퍼티 이름은 반드시 문자열이어야 합니다.
객체에 존재하지 않는 프로퍼티를 참조하면 undefined
를 반환합니다.
프로퍼티 값 갱신 & 동적 생성 & 삭제
var person = {
'first-name': 'Ung-mo',
'last-name': 'Lee',
gender: 'male',
};
person['first-name'] = 'Kim';
console.log(person['first-name'] ); // 'Kim'
person.age = 20;
console.log(person.age); // 20
delete person.gender;
console.log(person.gender); // undefined
객체가 소유하고 있는 프로퍼티에 새로운 값을 할당하면 프로퍼티 값은 갱신됩니다.
객체가 소유하고 있지 않은 프로퍼티 키에 값을 할당하면 하면 주어진 키와 값으로 프로퍼티를 생성하여 객체에 추가합니다.
delete
연산자를 사용하면 객체의 프로퍼티를 삭제할 수 있으며, 이 때 피연산자는 프로퍼티 키여야 합니다.
for-in 문
객체의 문자열 키(key)를 순회하기 위한 문법이며, 객체에 포함된 모든 프로퍼티에 대해 루프를 수행할 수 있습니다.
var person = {
'first-name': 'Ung-mo',
'last-name': 'Lee',
gender: 'male'
};
// prop에 객체의 프로퍼티 이름이 반환된다. 단, 순서는 보장되지 않는다.
for (var prop in person) {
console.log(prop + ': ' + person[prop]);
}
/*
first-name: Ung-mo
last-name: Lee
gender: male
*/
var array = ['one', 'two'];
// index에 배열의 경우 인덱스가 반환된다
for (var index in array) {
console.log(index + ': ' + array[index]);
}
/*
0: one
1: two
*/
하지만 객체의 경우 프로퍼티의 순서가 보장되지 않고, 배열 요소들만 순회하지 않기 때문에 배열에는 사용하지 않는 것이 좋습니다.
이를 극복하기 위해 ES6에서 for-of
문이 추가 되었습니다.
for–in
문은 객체의 프로퍼티를 순회하기 위해 사용하고, for–of
문은 배열의 요소를 순회하기 위해 사용합니다.
Pass-by-reference
object type을 객체 타입 또는 참조 타입이라 합니다.
참조 타입이란 객체의 모든 연산이 실제 값이 아닌 참조값으로 처리됨을 의미합니다.
따라서 객체 타입은 변경이 가능한 값으로 동적으로 변화할 수 있습니다.
어느 정도의 메모리 공간을 확보해야 하는지 예측할 수 없기 때문에 런타임에 메모리 공간을 확보하고 메모리의 힙 영역(Heap Segment)에 저장됩니다.
var a = {}, b = {}, c = {}; // a, b, c는 각각 다른 빈 객체를 참조
console.log(a === b, a === c, b === c); // false false false
a = b = c = {}; // a, b, c는 모두 같은 빈 객체를 참조
console.log(a === b, a === c, b === c); // true true true
Pass-by-value
원시 타입은 값(value)으로 전달되며, 값이 복사되어 전달됩니다.
원시 타입은 값이 한 번 정해지면 변경할 수 없습니다.
이들 값은 런타임(변수 할당 시점)에 메모리의 스택 영역(Stack Segment)에 고정된 메모리 영역을 점유하고 저장 됩니다.
var a = 1;
var b = a;
console.log(a, b); // 1 1
console.log(a === b); // true
a = 10;
console.log(a, b); // 10 1
console.log(a === b); // false
객체의 분류
Built-in Object(내장 객체)
는 웹페이지 등을 표현하기 위한 공통의 기능을 제공하며, 로드되자 마자 바로 사용이 가능합니다.
Standard Built-in Objects(표준 빌트인 객체)
를 제외한 BOM
과 DOM
을 Native Object
라고 분류하기도 합니다.
또한 사용자가 생성한 객체를 Host Object(사용자 정의 객체)
라 합니다.
11. 객체와 변경불가성(Immutability)
Immutability(변경불가성)는 객체가 생성된 이후 그 상태를 변경할 수 없는 디자인 패턴을 의미합니다.
객체는 참조 형태로 전달하고 전달받기에 언제든지 변경 가능하고 이로 인해 문제가 될 가능성도 커지게 됩니다.
따라서 불변객체로 만들어 프로퍼티의 변경을 방지하며, 객체의 변경이 필요한 경우에는 참조가 아닌 객체의 방어적 복사(defensive copy)를 통해 새로운 객체를 생성한 후 변경해야 합니다.
또는 Observer 패턴으로 객체의 변경에 대처할 수도 있습니다.
불변 객체를 사용하면 복제나 비교를 위한 조작을 단순화 할 수 있고, 성능 개선에도 도움이 됩니다.
하지만 객체가 변경 가능한 데이터를 많이 가지고 있는 경우, 오히려 부적절 할 수 있습니다.
ES6에서는 불변 데이터 패턴(immutable data pattern)을 쉽게 구현할 수 있는 새로운 기능이 추가 되었습니다.
immutable value vs. mutable value
원시 타입인 Boolean
, null
, undefined
, Number
, String
, Symbol
은 변경 불가능한 값이며 이외의 모든 값은 객체 타입으로 변경 가능한 값 입니다.
var user = {
name: 'Lee',
address: {
city: 'Seoul'
}
};
var myName = user.name; // 변수 myName은 string 타입
user.name = 'Kim';
console.log(myName); // Lee
myName = user.name; // 재할당
console.log(myName); // Kim
var user2 = user; // 변수 user2는 객체 타입
user2.name = 'Park';
console.log(user.name); // Park
console.log(user2.name); // Park
변수 myName
에 user.name
을 할당 했을 때 user.name
의 참조를 할당하는 것이 아니라 immutable한 값 ‘Lee’가 메모리에 새로 생성되고 myName
은 이것을 참조합니다.
따라서 user.name
의 값이 변경되어도 변수 myName
이 참조하고 있는 ‘Lee’는 변함이 없습니다.
객체 user2
의 name
프로퍼티에 새로운 값을 할당하면 객체는 변경 불가능한 값이 아니므로 객체 user2
는 변경됩니다.
user
과 user2
가 같은 주소를 참조하고 있기 때문에 변경하지 않은 객체 user
도 동시에 변경됩니다.
불변 데이터 패턴(immutable data pattern)
의도하지 않은 객체의 변경을 막기 위한 방법에는 2가지가 있습니다.
객체의 방어적 복사(defensive copy)
Object.assign
불변객체화를 통한 객체 변경 방지
Object.freeze
Object.assign
Object.assign({}, 복사대상객체)
타겟 객체로 소스 객체의 프로퍼티를 복사합니다.
기존 객체를 변경하지 않고 객체를 복사하여 사용할 수 있습니다.
const user1 = {
name: 'Lee',
address: {
city: 'Seoul'
}
};
// 새로운 빈 객체에 user1을 copy함
const user2 = Object.assign({}, user1);
// user1과 user2는 참조값이 다름
console.log(user1 === user2); // false
user2.name = 'Kim';
console.log(user1.name); // Lee
console.log(user2.name); // Kim
// 객체 내부의 객체(Nested Object)는 Shallow copy 됨
console.log(user1.address === user2.address); // true
user1.address.city = 'Busan';
console.log(user1.address.city); // Busan
console.log(user2.address.city); // Busan
완전한 deep copy
를 지원하지 않으며, 객체 내부의 객체(Nested Object)는 shallow copy
됩니다.
- deep copy란? 변수의 내용 복사, 가리키는 객체 복사
- → 깊은 복사를 통해 복사한 변수의 값을 변경해도 원본 변수에 영향 X
- shallow copy란? 변수의 내용은 복사, 가리키는 객체 공유
- → 얕은 복사를 통해 복사한 변수의 값을 변경할 시 원본 변수에 영향을 미침
Object.freeze
Object.freeze(복사대상객체)
의 형태로 사용합니다.
사용 방법은 다음과 같습니다.
const user = {
name: 'Lee',
address: {
city: 'Seoul'
}
};
// Object.assign은 완전한 deep copy를 지원 X
const user2 = Object.assign({}, user, {name: 'Kim'});
console.log(user.name); // Lee
console.log(user2.name); // Kim
Object.freeze(user);
user.name = 'Kim'; // 무시됨
console.log(user); // { name: 'Lee', address: { city: 'Seoul' } }
console.log(Object.isFrozen(user)); // true
// 객체 내부의 객체는 변경 가능
user.address.city = 'Busan'; // 변경됨
console.log(user); // { name: 'Lee', address: { city: 'Busan' } }
Immutable.js
위 두 방법은 성능상 이슈가 있어서 큰 객체에는 사용하지 않는 것이 좋습니다.
Immutable.js
는 List
, Stack
, Map
, OrderedMap
, Set
, OrderedSet
, Record
와 같은 영구 불변 (Permit Immutable) 데이터 구조를 제공합니다.
npm을 활용하여 설치한 후 import 하여 사용합니다.
여기까지 JavaScript lesson 10, 11에 해당되는 객체, 객체와 변경불가성에 대해 알아보았습니다.