객체
객체는 0개 이상의 프로퍼티로 구성된 하나의 자료구조라고 할 수 있습니다. 자료구조라는 것은 데이터를 표현하는 하나의 방법이라고 생각하면 될 것 같네요. 프로퍼티는 무엇인지 차근차근 설명하겠습니다.
객체
자바스크립트는 객체를 기반으로 하는 프로그래밍 언어입니다. 자바스크립트의 거의 모든 것이 객체로 이루어져 있다고 할 수 있는데요. 객체는 무엇일까요?
객체는 0개 이상의 프로퍼티로 구성된 하나의 자료구조라고 할 수 있습니다. 자료구조라는 것은 데이터를 표현하는 하나의 방법이라고 생각하면 될 것 같네요. 프로퍼티는 무엇인지 차근차근 설명하겠습니다.
이전 타입을 설명할 때 원시 타입과 객체 타입을 언급했었는데, 원시 타입은 변경 불가능한 값이고, 객체 타입은 변경 가능한 값이라고 설명했습니다. 이에 대해서는 마지막에 설명할게요.
객체 생성
자바스크립트는 프로토타입 기반의 객체 지향 언어로 다른 클래스 기반 객체 지향 언어와 다르게 객체를 생성하는 방식을 여러 개 제공합니다.
- 객체 리터럴
- Object 생성자 함수
- 생성자 함수
- Object.create 메서드
- 클래스
보통 객체 리터럴을 사용하는 것이 가장 일반적이고 간단합니다. 클래스를 사용하는 경우도 있습니다만, 클래스는 기존 객체 생성 방식을 클래스로 보이게 만든 것으로 프로토타입을 없앤 것이 아닙니다. 이는 조금은 깊은 내용이므로 나중에 다른 포스트에서 설명하겠습니다.
객체 리터럴
그렇다면 객체 리터럴로 객체를 생성해 보겠습니다.
var x = {} // 빈 객체를 생성합니다.
리터럴이란 것은 사람이 이해할 수 있는 문자나 기호로 값을 표기하는 방법입니다. 즉, {}라는 기호가 객체를 리터럴로 표현한 것이죠. 여기서 헷갈리지 말아야 할 것은 객체는 코드블록을 의미하는 것이 아닙니다. 이전에 반복문이나 조건문에서 중괄호를 썼는데, 이때의 중괄호는 여러 코드를 묶는 의미로 코드블록이 되는 것입니다. 코드 블록은 값을 의미하는 것이 아니라는 차이가 있죠.
var person = {
name : 'kim'
}
객체는 0개 이상의 프로퍼티로 구성된다고 했습니다. 그렇다면 프로퍼티는 무엇일까요? 위의 코드에서 객체 안에 name과 kim이 :를 기준으로 쓰여있습니다. 이것이 프로퍼티입니다.
프로퍼티
객체의 프로퍼티는 키와 값으로 구성됩니다.
var person = {
name: 'kim',
age: 20,
'last-grade':'d',
}
프로퍼티의 형식은 키 : 값의 형태입니다. 여러 개를 나열할 때는 위 코드와 같이 ,를 이용하면 됩니다.
프로퍼티의 키는 문자열이나 심벌 값이 올 수 있습니다. 문자열의 경우 네이밍 규칙을 준수하는 것이 좋습니다. 위 코드에서 네이밍 규칙을 준수한 경우 ''로 키값을 감싸지 않지만, 준수하지 않으면 문자열임을 표현하기 위해서 ''로 감싸야 합니다. 그렇지 않으면 연산자가 있는 표현식으로 인식하기 때문이죠. 즉, 위 코드에서 last-grade를 last에서 grade를 -(뺄셈 연산)한다고 인식한다는 것입니다. 이런 일을 방지하려면 네이밍 규칙을 준수하는 것이 좋겠죠.
메서드
객체 내에 프로퍼티 값으로 함수를 사용하는 것이 메서드입니다. 일반 함수와 같이 호출을 할 수 있고, 호출 시 그 메서드가 실행되는 것도 함수와 같습니다.
var person = {
name : 'kim',
sayHello : function(){
console.log(`hello, i am ${this.name}`);
}
}
메서드에서 사용되는 this는 객체 자신을 가리킵니다. this에 대해서는 따로 포스트에 정리하도록 하겠습니다.
프로퍼티 접근
객체를 생성하고 프로퍼티가 무엇인지 메서드는 무엇인지 알게 되었습니다. 하지만 이것을 어떻게 사용하는 지는 알지 못하죠. 이제 프로퍼티와 메서드를 사용하는 법을 알려드리겠습니다. 이것을 접근이라고 합니다.
프로퍼티에 접근하는 방식은 두 가지가 있습니다.
- 마침표 접근 연산자 이용
- 대괄호 접근 연산자 이용
예를 보여드릴게요.
var person = {
name: 'kim',
age: 19,
}
person.name // 'kim'
person['age'] // 19
위와 같은 방식으로 프로퍼티의 값을 가져올 수 있습니다. 대괄호를 이용할 때는 따옴표로 감싸야 합니다. 그렇지 않으면 프로그램이 키가 아니라 변수로 인식하여 변수를 찾게 됩니다. 만약 프로퍼티가 네이밍 규칙을 따르지 않는다면 반드시 대괄호 표기법을 이용해야 합니다. 보통 마침표를 이용하기 때문에 네이밍 규칙을 준수하는 것이 좋습니다.
만약 객체에 존재하지 않는 프로퍼티에 접근하면 undefined가 반환됩니다. 에러가 발생하지 않는다는 것을 주의합시다.
메서드를 호출할 때는 마침표 표기법을 이용합니다.
person.sayHello();
프로퍼티 갱신
프로퍼티의 값을 갱신하는 방법은 이미 존재하는 프로퍼티에 다른 값을 할당하면 갱신됩니다.
var person = {
name: 'kim',
}
person.name = 'Lee'; // name의 값이 'Lee'로 바뀝니다.
console.log(person.name); // 'Lee'
프로퍼티 동적 생성
객체에 새로운 프로퍼티를 넣고 싶다면 이와 같은 방법을 사용합니다.
var person = {
name : 'kim'
}
person.age = 19; // age 프로퍼티를 생성합니다.
console.log(person.age); // 19
프로퍼티 삭제
delete키워드를 사용하면 프로퍼티를 삭제할 수 있습니다. 만약 없는 프로퍼티를 삭제한다면 에러가 발생하지 않고 삭제 명령이 무시됩니다.
var person = {
name = 'kim'
}
person.age = 19;
delete person.age; // age프로퍼티가 삭제됩니다.
delete person.address; // 없는 프로퍼티를 삭제하라고 했지만 에러가 발생하지 않고 아무 일도 일어나지 않습니다.
console.log(person); // { name : 'kim' }
ES6에서 추가된 기능
ES6부터 새로운 기능이 도입되어 사용되고 있습니다. 그 중 프로퍼티 축약과 메서드 축약에 대해 설명할게요.
// ES6이전
var x = 1, y = 2;
var foo = {
x : x,
y : y,
}
// ES6
var x = 1, y = 2;
var foo ={
x,
y
}
변수를 프로퍼티 값으로 사용하고, 프로퍼티의 키가 변수명과 동일할 경우 위와 같이 표기할 수 있습니다.
// ES6이전
var person = {
name : 'kim',
sayHello : function (){
console.log('hello');
}
}
// ES6
var person = {
name : 'kim',
sayHello(){
console.log('hello');
}
}
ES6의 메서드와 그 이전의 메서드는 약간의 차이가 있습니다. 이전에는 함수가 모두 일반함수였고, 객체의 메서드는 그 함수를 할당하는 방식이었지만, ES6부터는 constructor가 없어 생성자 함수로 역할을 하지 못하게 됩니다. 좀 더 메서드의 의미에 맞는다고 볼 수 있죠.
즉, 인스턴스를 생성할 수 없고, 그렇기 때문에 prototype프로퍼티가 존재하지 않습니다. 추가로 자신을 바인딩하는 객체를 가리키는 내부슬롯이 있어 super 키워드를 사용할 수 있습니다.
갑자기 이상한 말들이 나오는데 이에 대해서는 지금 몰라도 괜찮습니다. 그냥 넘어가도 상관없습니다.
원시값과 객체
계속해서 원시 타입은 변경이 불가능하고 객체 타입은 변경이 가능하다고 설명했는데, 이게 도대체 무슨 의미인지 아직 잘 모르겠죠? 먼저 변경이 가능한가 불가능한가를 결정짓는 것은 변수가 아닌 값에 대한 설명입니다. 값 자체가 바뀔 수 있냐 없냐는 것이죠.
원시 타입
변수 포스트에서 설명했듯이 자바스크립트에서 값을 변수에 할당할 때 먼저 메모리라는 건물에 특정 방에 값이 들어가고 그 방에 변수명으로 이름표를 붙인다고 했습니다. 여기서 새로운 값을 같은 변수에 할당한다고 해서 값이 바뀌는 것이 아니라 새로운 방에 새로운 값이 들어가고 이름표를 옮겨 붙이는 방식이라고 설명했죠. 아래의 사진을 참고하면 될 겉 같네요. 각 칸을 방이라고 생각하시면 될 겁니다.

만약 원시 타입이 변경 가능하다면 첫 선언 시 변수명 num의 위치가 바뀌지 않고, undefined의 값이 할당 시에 80, 재할당 시에 100으로 바뀔 겁니다. 하지만, 변경이 불가능하기 때문에 위와 같이 바뀌죠. 그렇다면 이전에 있던 값들은 어떻게 될까요? 그것은 가비지 콜렉터와 관련된 이야기인데, 쉽게 말해서 자바스크립트 엔진의 가비지 콜렉터가 알아서 정리해 줍니다. 즉, 다시 사용할 수 있는 공간으로 만들어 준다는 것이죠. 하지만, 그 시점을 개발자는 정확하게 알 수 없습니다.
값에 의한 전달
var a = 10;
var b = a;
a = 20;
console.log(a , b);
위의 코드 출력 결과가 어떻게 될까요? a는 20이고 b는 10이라는 값을 출력하게 될 것입니다. 자세히 보면 2번째 줄에서 b에 a를 할당했는데, a가 바뀌어도 a의 원래 값인 10이 그대로 유지되는 것을 알 수 있죠. 이를 값에 의한 전달이라고 합니다.
사실 오해의 소지가 있는 표현인데요. 엄밀히 말하면 변수에는 값이 아니라 메모리 주소가 전달됩니다. 변수는 값을 기억하는 것이 아니라 메모리 주소를 기억하기 때문이죠.
값은 건물의 방에 있는 이용자인 것은 알겠는데, 메모리 주소는 뭐고, 변수가 왜 그것을 기억한다는 것인지 모르겠습니다. 무슨 의미일까요?
먼저 변수를 설명할 때 변수는 방에 이름표를 붙이는 거라고 했습니다. 즉, 변수는 이용자인 값에 이름을 붙이는 것이 아니라 몇 층에 있는 몇 번 방에 이름을 붙이는 것이죠. 그래서 변수를 가져다 쓴다는 것은 몇 층에 있는 몇 번 방이라는 주소를 얻는 것이고 그 주소에 들어가면 값이 있게 되는 거에요.
그렇다면 위의 예시에서 b가 받은 것은 a가 가리키는 주소인 것이고, a에 20을 할당하는 순간 a의 주소는 바뀌지만 b의 주소는 이전에 a가 가리키던 주소를 가지고 있으니 10이라는 값이 유지가 될 수 있다고 생각할 수 있습니다. 엄밀히 말해서 이는 파이썬이라는 언어가 작동되는 방식입니다. 자바스크립트는 변수의 메모리 관리에 대해 자세하게 설명되어 있지 않기 때문에 실제 동작과 일치하지 않을 수 있죠. 하지만 원시 타입은 바뀌지 않는 다는 것을 알고 있으면 됩니다.
문자열
문자열의 경우 배열과 비슷하게 접근이 가능합니다. 이를 유사 배열객체라고 합니다. 자세한 설명은 추후 배열 포스트에서 설명하겠지만, 배열의 메서드는 사용할 수 없으면서 배열처럼 접근이 가능한 것이라고 생각하면됩니다. 또한 문자열은 이터러블입니다. 이에 대해서 기회가 되면 설명드리겠습니다.
여기서 중요한 것은 유사배열객체라는 것이죠. 분명 문자열은 원시 타입이라고 했는데 객체라니요. 이상합니다. 사실 자바스크립트에서 원시 값을 객체처럼 사용한다면, (프로퍼티를 사용하거나, 메서드를 이용하는 것과 같은 일) 래퍼 객체라는 것으로 자동으로 변환됩니다. 하지만, 처리가 끝난 후 값은 다시 원시 값으로 돌아옵니다. 잠시 객체처럼 되었다가 돌아온다고 생각하면 돼요. 이에 대해서도 나중에 설명해보겠습니다.
var str = 'string';
console.log(str[0]); // 배열처럼 접근 가능
str[0] = 'S';
console.log(str); // 'string'
분명 배열처럼 접근이 가능하고, 위에서도 래퍼 객체인지 뭔지로 변환된다고 했는데 3번째 줄에서 첫 글자를 대문자로 바꾼 것이 적용되지 않았습니다. 바로 문자열은 원시 타입으로, 변경이 불가능하기 때문이죠.
위에서 헷갈리게 말을 써놨지만, 결국 문자열도 변경이 불가능한 원시 타입입니다.
객체 타입(참조 타입)
객체 타입 혹은 참조 타입이라고도 합니다. 그렇다면 왜 객체 타입은 변경이 가능하다고 할까요? 원시 타입의 경우 보통 타입별로 공간을 차지하는 크기가 정해져 있습니다. 단, 자바스크립트 사양에서는 문자열(2바이트), 숫자 타입(8바이트) 외의 다른 타입의 크기에 대한 명시가 없기 때문에 각 엔진마다 다를 수 있죠. 그래도 그 크기가 정해져 있다는 사실은 변하지 않습니다. 단, 객체 타입은 다르죠.
먼저 객체는 프로퍼티의 집합이라고 말했습니다. 하지만, 프로퍼티의 수가 몇 개가 될지 알 수 없고, 동적으로 프로퍼티를 추가할 수 있기 때문에 그 크기를 미리 정해놓기 어렵습니다. 그래서 원시타입과 다르게 동작하는 것이죠.
객체는 위의 사진과 같은 방식으로 저장되고 참조됩니다. 방을 이용하는 이용자로 객체가 들어가긴 합니다. 하지만, 방의 이름표는 다른 방에 붙어 있고 그 방의 값 역시 이상한 숫자로 되어 있죠.
이런 방식이 객체가 저장되는 방식입니다. 먼저 객체가 생성되어 방에 들어가면 그 방의 주소를 가져와 새로운 방에 넣고 그 주소가 담긴 방에 이름표를 붙이게 됩니다. 왜 이런 방식을 사용할까요?
원시타입과 달리 객체 타입은 복잡한 자료구조이면서 공간을 얼마나 차지할 지 예측할 수 없기 때문에 많은 객체를 만드는 것은 무리가 될 수 있습니다. 그렇기 때문에 같은 객체를 얻고 싶으면 위의 원시 타입을 복사하는 것처럼 동작하면 안되는 것이죠. 대신 하나의 방에 객체를 넣고, 그 객체를 할당한 변수에는 그 방의 주소를 주어 변수를 가져다 쓰게되면 방의 주소를 얻게 되고, 그 주소로 향해 객체에 접근하는 다소 복잡한 방식을 구현한 것이겠죠.
변경 가능하다.
지금까지 설명한 것과 비교하면, 변경이 가능하다는 것은 방에 있는 값이 변할 수 있다는 뜻일 겁니다. 맞긴 하지만, 헷갈릴 수 있는 부분이 있습니다. 객체가 저장되고 참조되는 방식을 알고 있어야 합니다.
var obj1 = {
a:'a'
}
var obj2 = obj1; // 같은 주소를 가지고 있고 그 주소를 따라가면 { a:'a' }라는 객체가 있다.
중요한 것은 객체는 여러 변수가 하나의 객체를 공유할 수 있다는 것입니다. 이는 복사의 개념과는 다릅니다. 복사는 새롭게 만들어 낸다고 생각해야 합니다. 위의 경우에 obj2는 새로운 객체를 생성하는 것이 아닙니다. 이미 있는 객체를 같이 참조한다는 것이죠.
변경 가능하다는 것에서 원시 타입과 차이를 확인할 수 있습니다.
var a = 10;
var b = a;
a = 20;
a === b; // false
var obj1 = { a:'a'};
var obj2 = obj1;
obj1.a = 'b';
obj1 === obj2; // true obj1 === { a:'b'}, obj2 === { a: 'b'}
위의 예시를 보면 원시 타입과 객체 타입이 다르게 동작한다는 것을 알 수 있습니다. 원시 타입의 경우 값을 변경하면 이전에 같은 변수로 할당했지만, 다르다고 평가합니다. 당연한거죠. 20과 10은 다르기 때문이에요. 하지만, 객체는 obj1에 할당한 객체에 변화를 주었는데 obj2의 값도 동일하게 변했습니다. 객체는 내용이 변해도 같은 방에 있다고 생각하면 됩니다. 같은 방에 가구들을 바꾼다고 생각하죠.
객체의 프로퍼티가 변경되었지만, 주소는 변경되지 않았습니다. 객체를 할당한 변수는 객체의 본래 주소를 값으로 가지고 있고, 변수로 접근하면 그 주소를 통해 객체에 접근하게 되는 방식이기 때문이죠.
그렇다면 아래의 코드는 어떤 결과가 나올까요?
var obj1 = { a : 'a' };
var obj2 = obj1;
obj1 = { a: 'a' };
obj1 === obj2 // ?
위 코드는 변경가능한 값으로 객체를 이용한 것이 아닙니다.
이전 코드와 다른 점은 obj1에 새로운 { a : ‘a’ }라는 객체를 할당했다는 것입니다. 결과는 false가 나오죠. 리터럴 방식으로 객체를 생성하고 그 객체를 할당하는 시점에서 새로운 객체가 생성이 되는 것입니다. 그렇다면 그 객체의 주소는 첫 줄의 객체의 주소와 다르게 되는 것이죠. 아무리 프로퍼티가 같다고 해도 다른 것입니다.
그렇기 때문에 obj2가 가리키는 주소는 첫 객체의 주소이고 obj1이 가리키는 주소는 이후 만든 객체의 주소이기 때문에 서로 다르다고 볼 수 있는 것이죠.
이처럼 객체를 비교할 때는 같은 주소에 있는 객체인지를 비교한다고 생각하면 좋겠네요.