지난 포스팅에서 자바스크립트가 객체 지향 언어이고, 객체가 행위와 속성을 갖는다는 것을 object가 property를 갖는 것으로 구현한다는 것을 알아보았다.
따라서 이번 장에서는 object와 property가 구체적으로 어떤 것인지 보다 자세하게 알아보고자 한다.
이번 장의 내용은 YOU DON'T KNOW JS(this와 객체 프로토타입, 비동기와 성능)의 '객체'파트를 중심으로 내용을 가감한 것이다.
- 용어 상의 혼란을 막기 위해 미리 언급해두자면,
- 객체와 object는 동일한 용어이다.
- '객체 지향' 개념을 강조해야 할 때는 '객체'라고 표기하고,
- 데이터 타입으로서의 개념을 강조해야 할 때는 object라고 표기하겠지만,
- 기본적으로는 같은 용어로 봐도 무방하다.
이번 장의 내용은 '코어 자바스크립트'의 1장(데이터 타입)의 내용을 다소 수정하여 정리한 것이다.
저번 포스팅에서 다루었던 렌더링 과정에서 built-in 데이터 타입이 초기화된다고 언급한 적이 있다.
이렇게 렌더링 과정에서 생성되는 built-in 데이터 타입의 종류는 다음과 같다.
참조형
기본형(혹은 원시형)
null
undefined
boolean
number
string
symbol(ES6스펙)
이렇듯 자바스크립트의 데이터 타입은 크게 두 종류로 나뉘는데 이는 '불변성'을 기준으로 한다.
기본형 데이터는 '변하지 않는 값'이고,
참조형 데이터는 '변할 수 있는 값'이다.
'변한다'는 것의 정확한 의미에 대해서는 메모리 영역을 살펴보며 알아보자.
사실 변수의 선언과 데이터의 할당은 '스코프'와도 떨어질 수 없는 개념인데, 한 포스팅에서 연계해서 다루기에는 내용이 너무 많아 따로 작성했다.
또한 ES3와 ES5에서 스코프의 개념이 조금 다르기 때문에 각각의 개념과 연계해서 이 부분을 볼 필요가 있다.
일단은 이 포스팅을 읽고 넘어간 뒤 스코프에 대한 포스팅들을 읽고 다시 돌아와 읽으면 이해가 더 잘 될 것이다.
변수는 '변할 수 있는 값'을 저장하는 메모리 공간이다.
즉, 변수에는 '참조형 데이터'만을 담을 수 있다.
결론부터 말하자면, '참조형 데이터'는 '가리키는 메모리 주소값'(일종의 포인터)이다.
var a;
위와 같이 변수를 선언하면 메모리 영역에서는 다음과 같은 작업이 진행된다.
메모리의 빈 공간(주소)을 찾는다.
해당 메모리 공간(주소)에 그 공간을 다른 공간과 식별하기 위한 이름을 붙인다.
해당 메모리 공간에 undefined라는 '기본형 데이터가 존재하는 메모리의 주소값'을 할당한다.
식별자 | a | |
---|---|---|
주소 | @100 | @101 |
데이터 | @101 | undefined |
@는 주소값을 구분하기 위해 임의로 붙인 문자이다.
작성의 편의를 위해 바로 인접한 주소에 표기했을 뿐, 실제로 메모리의 어느 공간에 데이터가 할당되는지는 운영체제가 배분하기 나름이다.
메모리의 빈 공간(@100)을 찾는다.
해당 메모리 공간을 다른 공간과 구분하기 위해 식별자 a를 붙인다.
해당 메모리 공간에 undefined라는 기본형 데이터가 존재하는 메모리의 주소값 @101을 할당한다.
렌더링 과정에서 undefined라는 데이터 타입이 @101이라는 주소에 이미 할당되었다고 가정한다
스코프 상으로는 글로벌 스코프에 undefined가 Global object의 property로서 존재하고 있다
여기서 '변수'는 바로 @100이라는 주소에 해당하는 메모리 공간이다.
(처음 공부하는 사람 입장에서는 식별자 a를 변수라고 생각하기 쉬운데, 엄밀히는 구분해야 하는 개념이다)
여기서 변수(@100)에 할당된 데이터를 보자
바로 @101이라는 메모리 주소값이다.
즉, @101이라는 주소를 '가리키는(참조하는)' 참조형 데이터이다.
이제 @101이라는 주소를 살펴보자.
이 메모리 공간에는 메모리 주소값, 즉 참조형 데이터가 담겨있지 않다.
undefined라는 기본형 데이터가 담겨 있다.
바꿔 말하면, @101이라는 메모리 공간은 '변수'가 아니다.
a = "woo";
위의 문장을 보고 프로그래밍에 익숙하지 않은 사람은 이렇게 표현할 것이다.
"변수 a에 문자열 woo를 할당한다"
그러나 이는 엄밀히는 틀린 표현이다.
보다 정확히 표현하자면 다음과 같이 바꿔야 한다.
"변수 a에 문자열 woo가 존재하는 메모리 주소를 할당한다"
혹은 "변수 a는 문자열 woo가 존재하는 메모리 주소를 가리킨다"
메모리 영역을 살펴보며 정확히 이해해보자.
식별자 | a | ||
---|---|---|---|
주소 | @100 | @101 | @102 |
데이터 | @102 | undefined | "woo" |
변수 a(@100)에 할당된 메모리 주소값 @101이 @102로 바뀐다.
앞서 말했듯이, 변수는 변할 수 있는 값을 저장하는 메모리 공간이다.
즉, 참조형 데이터를 담는 메모리 공간이 변수이다.
참조형 데이터는 말그대로 '무언가를 참조하는(가리키는)'데이터이다.
(즉, 메모리 주소값이다)
위의 예제에서 알 수 있듯이 참조형 데이터인 메모리 주소값은 변수가 무엇을 가리키는지에 따라 '변한다'.
주의해야 할 것이 있다.
'데이터'가 바뀌는 것이지 '주소'가 바뀌는 것이 아니다.
@100에 할당된 데이터가 @101에서 @102로 바뀐다는 의미에서 '변한다'는 것이지, 주소인 @101자체가 바뀐 것이 아니다.
a = "woobuntu";
식별자 | a | |||
---|---|---|---|---|
주소 | @100 | @101 | @102 | @103 |
데이터 | @103 | undefined | "woobuntu" |
메모리에 "woobuntu"라는 string 타입의 기본형 데이터가 없으니, 메모리의 빈 공간을 찾아 "woobuntu"를 할당한다.
변수 a(@100)에 할당된 메모리 주소값 @102가 @103으로 바뀐다.
이제 @102에 있는 "woo"를 가리키는(참조하는) 변수는 없다.
따라서 @102주소에 존재하는 "woo"라는 기본형 데이터는 garbage collector에 의해 수거된다.
(=@102주소의 메모리 공간이 비어 새로운 데이터를 할당할 수 있다)
굳이 @103에 새롭게 "woobuntu"를 만들지 말고, @102에 있는 "woo"를 "woobuntu"로 바꾸면 되지 않냐고 생각할 수도 있겠다.
하지만, "woo"는 string타입의 기본형 데이터이다.
앞서 말했듯이 기본형 데이터는 '변하지 않는 값'이다.
이번 예제에서처럼, 기본형 데이터는 자신을 참조하는 변수가 하나도 없을 때 '수거'될 수는 있다.
그러나 한 번 생성된 이후 garbage collector에 의해 수거되기 전까지는 '불변'한다.
- 변수 : 변할 수 있는 값(=참조형 데이터)을 담는 메모리 공간
- 참조형 데이터 : '가리키는(참조하는)' 메모리 주소값
- 기본형 데이터 : 생성된 이후 수거되기 이전까지 변하지 않는 '불변값'
자바스크립트가 데이터를 이렇게 처리하는 것은 나름대로 효율성을 고려한 것이다.
예를 들어 "woobuntu"라는 문자열을 다른 변수에서도 사용해야 할 때, 그때마다 새롭게 "woobuntu"라는 문자열을 만들 필요 없이 @103주소를 참조만 하면 된다.
자바스크립트가 왜 이런 데이터 처리 메커니즘을 택했는지는 '코어 자바스크립트' 1장에 보다 자세히 기술되어 있다
var a = "woo";
앞선 예제들에서는 선언과 할당을 분리했는데, 위와 같이 선언과 할당을 동시에 할 수도 있다.
이 경우 a가 undefined를 참조하는 과정 없이 바로 "woo"를 참조하게 된다.
앞서 참조형 데이터를 '가리키는 메모리 주소값'이라고 하였다.
즉, object라는 데이터 타입은 '가리키는 메모리 주소값'이라는 것이 된다.
하지만, 이것만으로는 설명이 조금 부족하다.
이전 포스팅에서 '객체 지향 개념'의 관점에서 object를 property들의 집합이라고 했다.
이 개념과 연결지어 다시 말하자면, object는 변수(property)들의 집합이라고 할 수 있다.
(모든 property가 변수인 것은 아닌데, 이는 이후 property descriptor부분에서 다시 다루겠다)
var a = {
woo: "우",
buntu: "분투",
};
식별자 | a | a.woo | a.buntu | |||
---|---|---|---|---|---|---|
주소 | @100 | @101 | @102 | @103 | @104 | @105 |
데이터 | @101 | {woo:@102, buntu:@103} | @104 | @105 | "우" | "분투" |
object a는 woo와 buntu라는 property의 집합이고, 이 property각각 또한 변수이다.
즉, woo와 buntu 역시 참조형 데이터(메모리 주소값)를 담는 메모리 공간이다.
따라서 a(@100)는 '집합으로서의 object'를 가리키는 @101주소값을 할당받고,
@101은 woo 변수와 buntu 변수를 가리키는 메모리 주소값들을 할당받는다.
woo와 buntu는 각각 가리키는 기본형 데이터의 주소값을 할당받는다
(만약, woo나 buntu가 가리키는 것이 또다른 object라면 다시 같은 참조 구조를 반복한다)
- 기본형 데이터가 메모리 참조 구조의 종착지라면, 참조형 데이터는 기본형 데이터까지 이어지는 경유지이다.
- 참조형 데이터가 경유지라는 점을 감안할 때,
- object는 그중에서도 '또 다른 경유지(@102,@103)를 가리키는 경유지들의 집합(@101)'으로, '참조형 데이터'의 일종의 하위 개념으로 볼 수 있겠다.
'불변성'과 관련하여 중요한 이슈 중 하나가 바로 '복사'이다.
앞서 살펴봤듯이 '=' 연산자를 이용한 할당은 전부 참조형 데이터(가리키는 메모리 주소값)를 변수에 할당하는 과정이다.
var a = "woobuntu";
var b = a;
위와 같은 문장을 실행했을 때, a와 b가 서로 독립적인 경우를 '복사'가 됐다고 표현한다.
즉, a가 b의 변화에 관계없이(역으로도) 자신의 값을 유지할 때 복사가 되었다고 표현하는데 종종 불변성의 의미를 이것으로 오해하기도 한다.
그러나 앞서 언급했듯이 '불변성'은 기본형 데이터가 생성된 이후 소멸되기 이전까지 값이 바뀌지 않는다는 것을 의미하는 용어이고,
복사는 두 변수의 값이 독립적임을 의미하는 것이다.
위 문장의 실행결과 메모리의 구조는 다음과 같다.
식별자 | a | b | |
---|---|---|---|
주소 | @100 | @101 | @102 |
데이터 | @102 | @102 | "woobuntu" |
a = 3;
식별자 | a | b | ||
---|---|---|---|---|
주소 | @100 | @101 | @102 | @103 |
데이터 | @103 | @102 | "woobuntu" | 3 |
메모리에 3이 할당된 공간이 없으니 새로 빈 공간을 찾아 3을 할당하고,
a에 할당되었던 @102를 @103으로 바꾸어준다.
a와 b가 가리키는 주소가 각각 @103과 @102로 다르니, a와 b는 서로 독립적이다.
a = ["우", "분투"];
식별자 | a | b | a[0] | a[1] | ||||
---|---|---|---|---|---|---|---|---|
주소 | @100 | @101 | @102 | @103 | @104 | @105 | @106 | @107 |
데이터 | @103 | @102 | "woobuntu" | {0:@104,1:@105} | @106 | @107 | "우" | "분투" |
a는 @103을, b는 @102를 가리키므로 서로 독립적이다.
복잡한 것은 object를 복사했을 때이다.
var a = {
woo: "우",
buntu: "분투",
};
var b = a;
식별자 | a | b | woo | buntu | |||
---|---|---|---|---|---|---|---|
주소 | @100 | @101 | @102 | @103 | @104 | @105 | @106 |
데이터 | @102 | @102 | {woo: @103, buntu:@104} | @105 | @106 | "우" | "분투" |
앞서 언급했듯이, object는 다른 경유지들을 가리키는 경유지들의 집합이다.
여기서 a나 b에 아예 새로운 object를 할당한다고 하면 새로운 메모리 주소(ex:@107)에 메모리 주소들의 집합값을 만들고, a나 b의 데이터를 대체할 것이기에 별 문제가 안 된다.
(= 앞의 기본형 데이터의 예제처럼 독립적이다)
문제는 object의 property값을 변경하려 할 때이다.
위의 예제에 다음 문장을 덧붙였다고 가정해보자.
a["woo"] = "리눅스";
식별자 | a | b | woo | buntu | ||||
---|---|---|---|---|---|---|---|---|
주소 | @100 | @101 | @102 | @103 | @104 | @105 | @106 | @107 |
데이터 | @102 | @102 | {woo: @103, buntu:@104} | @107 | @106 | "분투" | "리눅스" |
기존의 메모리 공간에 "리눅스"가 없으니, 새로운 메모리 공간(@107)에 "리눅스"를 할당한다.
a["woo"]로 property에 접근했다는 것은 다음의 절차를 의미한다.
a가 가리키는 메모리 주소인 @102로 가서,
그중에서도 식별자 woo와 연결된 메모리 주소인 @103으로 간다.
이 @103이라는 주소(변수 woo)가 "리눅스"가 존재하는 @107을 가리켜야 하기 때문에 데이터값을 @105에서 @107로 대체한다.
더 이상 "우"를 참조하고 있는 변수가 없으므로, @105메모리 주소는 garbage collector에 의해 비워진다.
여전히 a와 b는 동일한 주소인 @102를 가리키고 있다.
a의 property인 woo에 접근해서 값을 바꿨으니 b의 property인 woo의 값은 그대로일 것이라고 기대하지만,
a로 접근하든 b로 접근하든 똑같이 @102주소로 가서 woo의 주소값인 @103을 찾아갈 것이기 때문에 a와 b는 서로 독립적일 수 없다.
다시 말하자면, a와 b는 경유지가 동일하기 때문에 종착지를 바꾼다고 하더라도 결국 바뀐 종착지를 함께 바라볼 뿐이다.
그렇다면 해답은, a와 b가 서로 다른 경유지를 거치다가 마지막에는 동일한 종착지에 도달하면 된다.
(= a와 b가 서로 다른 '변수'를 property로 가지면서, property value가 같으면 된다)
식별자 | a | woo | buntu | |||
---|---|---|---|---|---|---|
주소 | @101 | @102 | @103 | @104 | @105 | @106 |
데이터 | @102 | {woo: @103, buntu:@104} | @105 | @106 | "우" | "분투" |
식별자 | a | a.woo | a.buntu | b | b.woo | b.buntu | ||||
---|---|---|---|---|---|---|---|---|---|---|
주소 | @101 | @102 | @103 | @104 | @105 | @106 | @107 | @108 | @109 | @110 |
데이터 | @102 | {woo: @103, buntu:@104} | @105 | @106 | "우" | "분투" | @108 | {woo:@109, buntu:@110} | @105 | @106 |
위와 같은 메모리 구조가 되면, a와 b는 서로 다른 주소를 가리키면서도 동일한 property value를 가진다
이 상태에서 a["woo"] = "리눅스"; 를 실행한다고 해보자.
식별자 | a | a.woo | a.buntu | b | b.woo | b.buntu | |||||
---|---|---|---|---|---|---|---|---|---|---|---|
주소 | @101 | @102 | @103 | @104 | @105 | @106 | @107 | @108 | @109 | @110 | @111 |
데이터 | @102 | {woo: @103, buntu:@104} | @111 | @106 | "우" | "분투" | @108 | {woo:@109, buntu:@110} | @105 | @106 | "리눅스" |
a["woo"]는 "리눅스"를, b["woo"]는 "우"를 가리키게 되었다.
a["woo"]는 @102 => @103 => @111(종착지)
b["woo"]는 @108 => @109 => @105(종착지)
경유지가 다르니 종착지의 변경이 서로 독립적으로 이루어진 것이다.
경유지와 종착지 비유가 와닿지 않는다면, 그냥 a와 b의 property들이 서로 다른 변수여야 한다고 이해해도 된다.
(같은 변수면 같은 곳을 가리킬 수 밖에 없으니 가리키는 곳을 바꿔도 여전히 같은 곳을 가리키니까)
앞선 예제와 같이 계층이 하나인 object는 얕은 복사만으로도 목표하는 바를 이룰 수 있다.
여기서는 '코어 자바스크립트'에서 제시한 코드를 소개한다.
var copyObject = function (target) {
var result = {};
// 새로운 객체를 생성함으로써, 일단 여기서 경유지가 달라진다.
for (var prop in target) {
result[prop] = target[prop];
// 위에서 새로운 object를 만들었기에, result[prop]과 target[prop]의 주소, 즉 2차 경유지의 주소도 당연히 다르다.
// 마지막으로 result[prop]이 '가리키는' 도착지 주소와 target[prop]이 '가리키는' 도착지 주소를 동일하게 설정함으로써 '복사'한다는 목적을 달성한다.
}
return result;
};
정재남 선생님의 말씀처럼 아쉬운 점은 있지만, object의 계층이 하나라는 전제하에 앞서 말한 경유지를 달리 설정하는 목적은 충분히 달성할 수 있다.
그리고 ES6부터는 '열거 가능한' object를 얕은 복사하는 정형화된 방법으로 Object.assign함수를 제공한다.
(열거 가능하다는 의미는 곧 설명한다)
Object.assign함수도 순회하면서 =할당으로만 복사한다고 YDKJS에 나와 있어서 위의 copyObject 함수와 정확히 어떻게 다른지는 잘 모르겠다.
추후 공부를 더 하고 난 뒤 따로 포스팅해야 할 것 같다
깊은 복사는 object의 계층이 2 이상일 때 필요한 방법이다.
object의 계층이 2 이상이라는 것은 거쳐야 할 경유지의 단계가 더 늘어난다는 것이기에 앞서의 얕은 복사로는 경유지를 전부 다르게 설정할 수 없다.
그렇다면 앞선 얕은 복사의 방법에서 object인 property를 만날 때마다 copyObject함수를 재귀적으로 실행해주면 된다.
이번에도 '코어 자바스크립트'의 코드를 소개하겠다.
var copyObjectDeep = function (target) {
var result = {};
if (typeof target === "object" && target !== null) {
// null의 typeof 연산 값은 object이다(x같은 자스)
// target의 타입이 object라면 아직 경유지라는 것이므로
// target의 property마다 다시 복사 함수 호출
for (var prop in target) {
result[prop] = copyObjectDeep(target[prop]);
}
} else {
// target의 타입이 object가 아니라면 종착지라는 의미이므로
// 종착지 주소 할당
result = target;
}
return result;
};
이 또한 완전한 방법은 아니며, 목적에 따라 함수를 수정해야 할 필요가 있다.
얕은 복사의 Object.assign처럼 깊은 복사에서도 간편법이 있는데 바로 JSON함수를 이용하는 것이다.
var copyObjectViaJSON = function (target) {
return JSON.parse(JSON.stringify(target));
};
이는 object를 JSON 형태의 문자열로 전환했다가 다시 자바스크립트 데이터 타입으로 바꾸는 방법이다.
(이 과정도 설명하자면 하세월)
이 또한 JSON과의 호환(변환 과정에서 손실 여부 등) 문제를 비롯하여 여러 고려할 점이 있기에 완전한 방법은 아니다.
데이터 타입과 관련하여 null의 typeof 연산값과 같은 경우를 제외하고도 사람 환장하게 만드는 예외는 무궁무진하기 때문에 데이터 타입에 관해서는 또 다른 포스팅에서 자세하게 다루어야 할 것 같다
- 이렇듯 자바스크립트는 모든 데이터를 참조 구조로 연결시키기 때문에 복사에 친화적이지 않다.
- '클래스 지향'개념이 '복사'를 통해 인스턴스를 생성하고, 상속을 구현한다는 점을 감안하면, 자바스크립트의 객체 지향은 '클래스 지향'과 분명히 구별되어야 하는 개념이다.
이제 object라는 데이터 타입이 기본형 데이터 타입과는 어떻게 다른지 개략적으로나마 이해했다.
이제는 object내에서의 구분은 어떻게 하는지 알아보자.
지난 포스팅에서 다루었던 built-in objects들은 object의 하위 데이터 타입들이다.
데이터 타입이 object인 데이터는 [[Class]]라는 내부 property가 존재하는데, 이 내부 property의 값이 바로 하위 데이터 타입을 의미한다.
객체 지향 개념의 클래스와는 아무 상관도 없다. 그저 분류의 의미일 뿐이다
[[Class]]내부 프로퍼티에 대해서는 추후 다른 포스팅에서 더 자세하게 기술할 예정이다
이 property는 내부 property이기에 직접 접근할 수는 없고 Object.prototype.toString()메소드를 통해서만 property의 값을 확인할 수 있다.
모든 built-in object는 toString()메소드를 가지고 있다.
(왜 그런지는 [[Prototype]]포스팅에서 다룰 것이다)
각각의 toString메소드마다 수행하는 기능이 다르기 때문에 구별해서 알아두어야 한다.
Object.prototype.toString은 object 타입을 문자열 표시 형태로 변환하여 반환한다.
1. (Object.prototype 위임객체).toString();
2. Object.prototype.toString.call(인자);
메소드를 호출하는 일반적인 방법은 위임객체.메소드()의 형태로 불러내는 것이다.
즉, 1번의 형태로 메소드를 호출하는 것이 일반적인 방법이다.
그런데, toString메소드의 목적이 object 타입을 반환하는 것임을 감안했을 때 1번과 같은 호출 형태는 의미가 없다.
왜냐하면 항상 똑같은 값인 "[object Object]"를 반환할 것이기 때문이다.
(Object.prototype.toString()은 인자를 전달해줄 수 없다)
'Object.prototype 위임객체' 자리에 다른 (함수.prototype)의 위임객체가 온다면, Object.prototype.toString()이 아니라 다른 (함수.prototype).toString()메소드가 호출되기 때문에 "object 타입을 확인한다"는 목적을 달성할 수 없다.
그래서 보통 2번의 형태로 사용하는데, 이렇게 하면 인자로 넘겨준 데이터의 데이터 타입을 "[object 하위 타입]"과 같은 형태로 확인할 수 있다.
이전 포스팅부터 사용하던 Object object라던가 String object라던가 하는 표현은 이것에 기인한 것이다.
이후로는 object와 하위 타입을 구분하기 위해 단순히 'Object'처럼 하위 타입만 적도록 하겠다.
메소드 호출과 관련해서는 this바인딩에 대한 이해가 필요한데, 상당히 양이 방대하므로 다른 포스팅에서 따로 다루도록 하겠다.
앞서 데이터 타입으로서의 object에 대해서 살펴보았다.
이제 object를 구성하는 property에 대해서 알아보자.
1. object.prop // Property Access
2. object["prop"] // Key Access
둘 다 같은 기능을 수행하지만, Key Access가 좀 더 활용도가 높다.
Property Access의 경우 .뒤에 '자바스크립트 식별자 조건'을 충족하는 property 이름만 올 수 있다.
(자바스크립트 식별자 조건은 다른 포스팅에서 다루자...)
반면, Key Access는 UTF-8/유니코드 호환 문자열이라면 모두 property 이름으로 사용할 수 있다.
(즉, 자바스크립트 식별자 조건의 제한을 받지 않는다)
또한, Key Access의 경우 문자열을 평가식으로 조합하는 것도 가능하다.
object property 이름은 언제나 문자열이다.
var arr = [];
object[true] => object["true"]
object[3] => object["3"]
object[arr] => object["[object Array]"]
ES6부터 추가된 기능으로, 문자열과 변수를 조합하여 object의 property name으로 사용할 수 있다.
다시 말하자면 object의 property에 접근할 때만 사용할 수 있던 Key Access를, 위임객체를 object 리터럴로 생성할 때도 사용할 수 있다는 것이다.
(위임객체의 생성은 밑에서 살펴볼 것이다)
var prefix = "woo";
var obj = {
[prefix + "buntu"]: "닉값하고 싶다",
};
obj["woobuntu"]; // 닉값하고 싶다
이런 것이 가능하다는 것이다.
Computed property name은 ES6의 Symbol과 가장 조합이 많이 된다고 한다.
(나중에 심볼까지 볼 여력이 되면, 그때 포스팅해서 링크 걸어둬야겠다)
ES5부터 property의 속성을 구체적으로 정의하거나 처리 기준을 정의하는 것이 가능해졌다.
이 파트에서 다루는 property descriptor와 Object의 함수들은 모든 object 타입에 통용되는 내용이다.
property descriptor는 두 종류로 분류할 수 있다.
data property descriptor
value
writable
enumerabale
configurable
access property descriptor
get
set
enumerable
configurable
보다시피 enumerable과 configurable은 공통 속성이지만, value와 writable 그리고 get과 set은 각각 다른 descriptor의 속성이기에 함께 작성하면 에러가 발생한다.
자바스크립트 엔진은 value 또는 writable이 작성되어 있으면 data property descriptor로 인식하고, 아니라면 access property descriptor로 인식한다.
참고로 property descriptor들의 디폴트 값은 전부 부정형이라고 생각하면 된다.
(false나 undefined)
var obj = {};
Object.defineProperty(obj, "woobuntu", {
value: "닉값하고 싶다",
});
수정 가능 여부를 표현한다.
writable이 false인 값의 수정
for-in 문으로 열거 가능한지 여부를 표현한다.
enumarable값이 false이더라도 for-in문에서 감춰질 뿐이지 property에 대한 접근은 여전히 가능하다.
Object.prototype.propertyIsEnumerable(property 이름)함수로 [[
Enumerable]]값을 확인할 수 있다.
property의 삭제 가능 여부를 표현한다.
또한, configurable값이 false일때 value외의 property descriptor의 속성을 변경할 수 없다.
configurable이 false인 값을 수정하려고 시도하면 strict모드와 상관없이 TypeError를 반환한다.
다만, writable속성을 true에서 false로 변경은 가능하다
(설계 누가 했냐)
YDKJS에 의하면 object의 property에 접근할 때 object의 [[Get]]에 정의된 함수가 호출되어 property를 반환한다고 한다.
이 [[Get]]은 해당 object에 해당 property가 존재하는지 찾아보고 존재하면 해당 프로퍼티를 반환한다고 한다.
own property에 해당 property가 없다면 [[Prototype]] chain상에서 검색한다.
(own property에 대한 개념은 아래에서 후술한다)
[[Prototype]] chain을 끝까지 타고 올라가도 해당 property를 찾지 못한 경우에 undefined를 반환한다
내부 프로퍼티라 코드가 공개되어 있지 않아 정확한 작동 방식은 모르겠다
이 [[Get]]의 동작을 property 단위에서 재정의할 수 있게끔 하는 것이 바로 property descriptor의 [[Get]]이다.
이 property 단위의 [[Get]]은 디폴트 값이 undefined이므로, property 단위에서 [[Get]]을 정의해주지 않으면 object의 [[Get]]이 호출된다.
// ES5
var obj = {};
Object.defineProperty(obj, "woobuntu", {
get: function () {
return "닉값하고 싶다";
},
});
var result = obj.woobuntu; // "닉값하고 싶다"
// ES6
let obj = {
linux: "닉값하고 싶다",
get woobuntu() {
return this.linux;
},
};
let result = obj.woobuntu; // "닉값하고 싶다"
defineProperty를 통해 woobuntu property에 get함수를 설정한 것이다.
이렇게 설정된 get함수는 일반적인 함수와는 달리 함수(); 의 형태로 호출할 수 없다.
obj.woobuntu와 같이 해당 property에 접근하는 것만으로 함수를 호출하기 때문이다.
// ES5
var obj = {};
Object.defineProperty(obj, "woobuntu", {
get: function () {
return this.linux;
},
set: function (param) {
this.linux = param;
},
});
obj.woobuntu = "닉값하고 싶다";
var result = obj.woobuntu; // "닉값하고 싶다."
// ES6
let obj = {
get woobuntu() {
return this.linux;
},
set woobuntu(param) {
this.linux = param;
},
};
obj.woobuntu = "닉값하고 싶다";
let result = obj.woobuntu; // "닉값하고 싶다."
set함수도 get함수와 마찬가지로 일반적인 함수 호출의 형태로 호출할 수 없다.
'obj.woobuntu = 데이터'와 같이 할당문의 형식으로 호출하면 set함수가 호출된다.
this에 대해서는 넘어가자(this바인딩 포스팅에서 다룰 예정이다)
그리고 위의 예제처럼 getter와 setter는 항상 같이 정의해두는 것이 좋다.
여기서 말하는 불변성은 객체에 대한 불변성으로, 앞서 언급한 기본형 데이터의 불변성과는 구분된다.
아래의 작업들로 발생하는 불변성은 모두 객체의 바로 아래 property에 대해서만 적용된다.(like 얕은 복사)
var obj = {};
Object.defineProperty(obj, "MY_NICK", {
value: "woobuntu",
writable: false,
// value인 woobuntu를 변경할 수 없다
configurable: false,
// 다시 writable속성을 true로 되돌릴 수 없고,
// MY_NICK property의 삭제도 불가능하다.
});
object에 더는 property를 추가할 수 없도록 할 때 Object.preventExtensions를 호출한다.
불가역적이라 한 번 설정하면 돌이킬 수 없다.
property가 아닌 object가 기준이다.
즉 object를 기준으로 property를 추가할 수 없는 것이지,
property 각각에는 개입한 것이 아니기 때문에 property를 삭제할 수도 있고, property value를 변경할 수도 있다.
(property의 descriptor(writable, configurable)에 관여하지 않는다)
object의 내부 property인 [[Extensible]]값을 false로 설정하는 작업이다.
([[Extensible]]의 디폴트 값은 true이다)
Object.isExtensible(object)로 해당 object의 [[Extensible]]값을 확인할 수 있다.
object에 '추가', '삭제' 금지를 설정한다.
앞서 Object.preventExtensions에서 '삭제 금지'가 더해진 것이다.
즉 object의 [[Extensible]]을 false로 설정하고,
모든 property의 [[Configurable]]을 false로 설정하는 것.
(configurable false이기 때문에 writable을 true에서 바꾸는 것은 가능함...)
(대체 왜...?)
Object.isSealed(object)로 해당 object에 Object.seal이 적용되었는지 여부를 확인할 수 있다.
Object.seal에 더해 모든 property의 [[writable]]을 false로 설정한다.
Object.isFrozen(object)로 해당 object에 Object.freeze가 적용되었는지 여부를 확인할 수 있다.
위 코드에서 확인할 수 있듯이, property가 있을 때 그 값이 undefined인 경우와 property자체가 없는 경우 모두 property 접근의 결과는 undfined이다.
그렇다면 특정 property가 존재하는지 아닌지의 여부는 어떻게 판단하면 좋을까?
이에 앞서 own property에 대해 알아보고 넘어가자
[[Prototype]]에 대한 내용은 다음 포스팅에서 다루니 지금 당장은 이해하지 못해도 괜찮다.
다만, 자신의 직속 property까지만 해당하기 때문에, bun과 tu property는 own property에 해당하지 않는다.
__proto__는 [[Prototype]]을 나타내는 것으로, 결국 own property는 이를 제외한 나머지 모든 직속 property이다.
이 own property의 개념에 의존해 다음과 같은 함수들이 작동하는 것이다.
Object.prototype.hasOwnProperty(property name)
Object.getOwnPropertyNames(object)
Object.getOwnPropertyDescriptor(object, property name)
"0"은 0번째 인덱스로 arr의 own property이다.
(앞서도 말했지만 object의 property 이름은 언제나 문자열이다)
따라서 hasOwnProperty메소드로도, in 연산자로도 true가 반환된다.
반면, concat 메소드는 [[Prototype]] chain상에 존재하는 property이기 때문에 hasOwnProperty는 false를, in 연산자는 true를 반환한다.
hasOwnProperty는 Object.prototype의 메소드이므로, Array.prototype에 존재하지 않는다.
그럼에도 Array.prototype 위임객체에서 호출할 수 있는 이유는
Array.prototype의 [[Prototype]]이 Object.prototype을 참조하고 있기 때문이다.
이렇듯 in 연산자는 [[Prototype]]에 대해서는 몇 단계든 타고 올라갈 수 있다.
결국 arr는 hasOwnProperty메소드를 [[Prototype]] chain을 타고 올라가서 호출한 것이다.
바꿔 말하면, in 연산자의 결과값이 true인 property는 해당 위임객체에서 접근할 수 있다는 것이다.
앞서 property descriptor에서 enumerable에 대한 것을 알아봤다.
당연하게도 property 순회는 이 enumerable속성이 true인 property에만 적용된다.
이렇게 개발자 도구에서 보면 인덱스 0은 색깔이 진하고, length와 __proto__는 색깔이 연한데 아마 enumerable값이 true이면 진한색이고 아니면 연한색인 듯하다.
for in 구문에서의 'in'은 방금 알아본 in 연산자이다.
따라서 for in 구문은 [[Prototype]] chain상에서 열거가능한 모든 property를 순회한다.
property라는 것은 모든 object 타입에 통용되는 개념임을 강조하기 위하여 Array.prototype 위임객체를 예시로 들었을 뿐이다.
위의 예처럼 Array.prototype 위임객체에 for in 구문을 사용할 경우 인덱스가 아니지만, 열거 가능한 property까지 같이 순회를 돌기 때문에 Array.prototype 위임객체에 for in 구문은 사용하지 말도록 하자.
또한, for in 구문은 순회되는 property의 순서가 일정하지 않다는 단점이 있다.
리터럴 형식
const obj = {
name: value,
// ...
};
생성자 형식
const obj = new Object();
// const obj = new Object(undefined);
// const obj = new Object(null);
// const obj = Object();
// const obj = Object(undefined);
// const obj = Object(null);
obj.name = value;
'클래스 지향'의 생성자를 흉내낸 형태로, new 함수()의 형태를 지닌다.
위와 같이 Object에 인자를 전달하지 않거나, undefined나 null을 인자로 전달하면 Object.prototype 위임객체가 생성된다.
new연산자를 생략할 수 있는데 이에 대해 YDKJS와 '몰입 자바스크립트'의 입장이 다르다.
몰입 자바스크립트 : 차이가 없다
YDKJS : 불필요한 object의 생성과 garbage collection을 줄일 수 있다.
Object에 인자를 전달하면 Object.prototype 위임객체가 생성되지 않고, 인자로 전달한 (object 하위 타입).prototype의 위임객체를 반환한다.
예를 들어 123이라는 number타입의 값을 인자로 전달하면 new Number(123)로 생성한 object를 반환하는 것이다.
const result = 'sports'.concat('soccer', 11);
// 'sportssoccer11'
object.함수(인자)
자바스크립트에서는 'object.함수(인자)'와 같은 형태로 함수를 호출한다.
이때 object의 하위 타입에 따라 호출되는 함수가 결정된다.
(Array와 String의 concat메소드는 서로 다른 함수다)
좀 더 구체적으로 설명해보자.
위의 그림에서 a에는 바로 문자열 리터럴을 할당했고, b에는 생성자 형식으로 생성한 String.prototype 위임객체를 할당했다.
a의 데이터 타입은 string이고 b의 데이터 타입은 object이다.
위임객체는 [[Prototype]]을 통해 수임객체의 property를 참조할 수 있다고 했는데, 그림에서 보이는 __proto__ property가 수임객체인 prototype을 참조한 것이다.
(엄밀히는 __proto__가 아니라 __proto__가 반환하는 값이 수임객체인 prototype을 참조하는 것인데, 링크를 달아두었으니 참고하자)
즉, b가 'String.prototype을 참조할 수 있는' 위임객체이므로 b만이 concat메소드를 사용할 수 있어야 한다.
그런데 앞선 예제에서는 String함수로 생성한 위임객체가 아닌 문자열 리터럴 "sports"에서 바로 concat메소드를 호출했고 오류없이 정상 작동했다.
이는 자바스크립트 엔진이 함수 앞에 위치하는 데이터의 타입을 감지하고 해당 데이터 타입의 오브젝트를 생성한 뒤, 해당 함수를 호출하기 때문이다.
자바스크립트 엔진이 concat메소드를 만난다.
concat메소드 앞에 위치한 object의 하위 타입을 파악한다
(여기서는 String)
Object(데이터) 호출로 새로운 위임객체를 생성한다
(데이터의 타입에 따라 object 하위 타입이 결정된다.
이 경우 String.prototype 위임객체가 생성된다)
해당 String.prototype 위임객체에서 concat메소드를 검색한다.
(String.prototype.concat)
(만약, 해당 object에 메소드가 없다면 에러를 반환한다)
해당 메소드에 인자("soccer")를 전달하여 호출한다.
여러 자바스크립트 커뮤니티에서도 되도록 생성자 형식은 지양하고 리터럴 형식을 사용하라고 적극 권장한다. - YOU DON'T KNOW JS(this와 객체 프로토타입, 비동기와 성능), 68page
여기서는 메소드 호출에 대해서만 다루었지만, 사실 기본형 데이터에 대한 모든 property 접근은 이러한 '박싱' 과정을 거친다.
이렇게 '박싱'되지 않는 값은 null과 undefined뿐이다.
역으로 Date object의 경우 리터럴 형식이 없어서 반드시 생성자 형식으로 생성해야 한다.
메모리 공간에는 2종류의 값이 담길 수 있다.
하나는 다른 메모리 공간의 주소값이고, 다른 하나는 자바스크립트에서 말하는 기본형 데이터이다.
변수는 메모리 공간의 주소값을 담는 공간이다.
기본형 데이터가 할당된 공간은 더 이상 다른 곳을 가리키지 않는 반면, 변수는 항상 다른 공간을 가리킨다.
(즉 기본형 데이터는 종착지이고, 참조형 데이터(메모리 주소값)는 경유지이다)
변수가 종착지가 아닌 경유지를 가리킬 때, 이 변수에 담긴 데이터 타입을 특히 'object'라고 부른다.
(메모리 주소값이지만 일정 조건이 붙는다는 점에서 '참조형 데이터'의 하위 개념이라고 할 수 있다)
기본형 데이터는 생성된 이후 수거되기 전까지 '불변'한다.
property에는 property descriptor라는 속성들이 존재한다.
이를 통해 object의 불변성을 조절할 수도 있고,
순회를 돌 property들을 제한할 수도 있다.
위임객체는 리터럴 형식과 생성자 형식으로 생성할 수 있다.
(여기서는 Object 위임객체에 대해서만 예시를 들었지만 대부분의 object가 그러하다)
대부분의 경우 리터럴 형식으로 위임객체를 생성하는 것이 좋다
기본형 데이터([[PrimitiveValue]]값)의 property에 접근할 때 '박싱'과정을 통해 object(위임객체)로 자동 변환된다.
([[PrimitiveValue]]와 '강제변환'에 대해서는 다른 포스팅에서 더 자세하게 다루도록 하겠다)
이번 포스팅에서는 object와 property의 전반적인 특징에 대해 살펴보았다.
다음 포스팅에서는 두 객체가 [[Prototype]]을 통해 어떻게 연결되는지 살펴본다.