값 vs 레퍼런스
다른 언어에서 값은 사용하는 구문에 따라 값-복사 또는 레퍼런스-복사의 형태로 할당/전달 된다.
C++에서는 어떤 함수에 전달한 숫자 인자 값을 그 함수 내에서 수정하려면 int& myNum
형태로 함수 인자를 선언하고, 호출하는 쪽에서는 변수 x를 넘기면 myNum은 x를 참조한다. 레퍼런스는 포인터의 특수한 형태로 다른 변수의 포린터를 가진다. 레퍼런스 인자를 선언하지 않으면 전달한 값은 아무리 복잡한 객체일지라도 언제나 복사된다.
자바스크립트는 포인터라는 개념 자체가 없고 참조하는 방법도 조금 다르다. 우선 어떤 변수가 다른 변수를 참조할 수 없다. 그냥 안 된다.
자바스크립트에서 레퍼런스는 (공유된)값을 가리키므로 서로 다른 10개의 레퍼런스가 있다면 이들은 저마다 항상 공유된 단일값(서로에 대한 레퍼런스/포인터 따위는 없다.)을 개별적으로 참조한다.
더구나 자바스크립트에는 값 또는 레퍼런스의 할당 및 전달을 제어하는 구문 암시가 전혀 없다. 대신, 값 타입만으로 값-복사, 레퍼런스-복사 둘 중 한쪽이 결정된다. 예를들면
var a = 2;
var b = a; // b는 언제나 a에서 값을 복사한다.
b++;
a; // 2
b; // 3
var c = [1, 2, 3];
var d = c; // d는 공유된 '[1,2,3]'값의 레퍼런스이다.
d.push(4);
c; // [1,2,3,4]
d; // [1,2,3,4]
null
, undefined
, string
, number
, boolean
그리고 ES6의 symbol
같은 단순한 값(스칼라 원시값)은 언제나 값-복사 방식으로 할당/전달 된다.
객체(배열과 박싱된 객체 래퍼 전체)나 함수 등 합성 값은 할당/전달 시 반드시 레퍼런스 사본을 생성한다.
예제코드에서 2는 스카랄 원시 값이므로 a엔 이 값의 초기 사존이 들어가고, b에는 또 다른 사본이 자리를 잡느다. 따라서 b를 바꿈으로써 a까지 동시에 값을 변경할 방법은 없다.
하지만, c와 d는 모두 합성 값이자 동일한 공유 값 [1, 2, 3]
에 대한 개별 레퍼런스다. 여기서 기억해야 할 점은 c와 d가 [1,2,3]
을 '소유'하는 것이 아니라 단지 이 값을 동등하게 참조한 한다는 사실이다. 따라서 레퍼런스로 실제 공유한 배열 값이 변경되면, 이 공유 값 한군데에만 영향을 미치므로 두 레퍼런스는 갱신된 값을 동시에 바라보게 된다.
레퍼런스는 변수가 아닌 값 자체를 가리키므로 A레퍼런스로 B레퍼런스가 가리키는 대상을 변경할 수는 없다.
var a = [1, 2, 3];
var b = a;
a; // [1, 2, 3]
b; // [1, 2, 3]
// 그 후
b = [4, 5, 6];
a; // [1, 2, 3]
b; // [4 ,5, 6]
b = [4, 5, 6]
으로 할당해도 a가 참조하는 [1, 2, 3]
은 영향을 받지 않는다. 그렇게 되려면 b가 배열을 가리키는 레퍼런스가 아닌 포인터가 되어야 하는데, 다시 말하지만 자바스크립트에 포인터란 없다.
함수 인자 역시 가장 자주 헷갈리는 부분이다.
function foo(x) {
x.push(4);
x; // [1,2,3,4]
// 그후
x = [4, 5, 6];
x.push(7);
x; // [4, 5, 6, 7]
}
var a = [1, 2, 3];
foo(a);
a; // [4, 5, 6, 7]이 아닌 [1, 2, 3, 4]
a를 인자로 넘기면 a의 레퍼런스 사본이 x에 할당된다. x와 a는 모두 동일한 [1, 2, 3]
값을 가리키는 별도의 레퍼런스다. 이제 함수 내부에서 이 레퍼런스를 이용하여 값 자체를 변경한다. 하지만 그 후 x = [4, 5, 6]
으로 새 값을 할당해도 초기 레퍼런스 a가 참조하고 있던 값에는 아무런 영향이 없다. 즉, a레퍼런스는 여전히 [1, 2, 3, 4]
값을 바라보고 있다.
레퍼런스 x로 a가 가리키고 있는 값을 바꿀 도리는 없다. 다만 a와 x둘 다 가리키는 고융 값의 내용만 바꿀 수 있다.
배열을 새로 생성하여 할당하는 식으로는 a의 내용을 [4, 5, 6, 7]
로 바꿀 수 없다. 기존에 존재하는 배열 값만 변경해야 한다.
function foo(x) {
x.push(4);
x; // [1,2,3,4]
// 그후
x.length = 0; // 기존 배열을 즉시 비운다.
x.push(4, 5, 6, 7);
x; // [4, 5, 6, 7]
}
var a = [1, 2, 3];
foo(a);
a; // [1, 2, 3, 4]가 아닌 [4, 5, 6, 7]
짐작했겠지만 x.length = 0
, x.push(4, 5, 6, 7)
은 새 배열을 생성하는 코드가 아니라, 이미 두 변수가 공유한 배열을 변경하는 코드이므로 a는 새로운 값 [4, 5, 6, 7]
을 가리킨다.
값-복사냐 레퍼런스-복사냐를 우리 마음대로 결정할 수 없음을 기억하자. 전적으로 값의 타입을 보고 엔진의 재량으로 결정된다.
(배열같은) 합성 값을 값-복사에 의해 효과적으로 전달하려면 손수 값의 사본을 만들어 전달한 레퍼런스가 원본을 가리키지 않게 하면 된다. 예를들어
foo(a.slice());
인자없이 slice()
를 호출하면 전혀 새로운 배열의(얕은 복사에 의한) 사본을 만든다. 이렇게 복사한 사본만을 가리키는 레퍼런스를 전달하니 foo()
는 a의 내용을 건드릴 수 없다.
반대로 스칼라 원시 값을 레퍼런스 처럼 바뀐 값이 바로바로 반영되도록 넘기려면 원시 값을 다른 합성 값(객체, 배열 등)으로 감싸야 한다.
function foo(wrapper) {
wrapper.a = 42;
}
var obj = {
a: 2,
};
foo(obj);
obj.a; // 42
obj는 스칼라 원시 프로퍼티 a를 감싼 래퍼로 foo()
함수에 obj레퍼런스 사본이 전달되거 래퍼 인자의 값을 바꾼다. 이제 래퍼 레퍼런스로 공유된 객체에 접근하여 프로퍼티를 수정할 수 있다. 함수가 종료되면 obj.a
는 수정된 값 42가 된다.
같은 원리로 2와 같은 스칼라 원시 값을 레퍼런스 형태로 넘기려면 Number
객체 레퍼로 원시 값을 박싱하면 된다.
Number
객체의 레퍼런스 사본이 함수 에전달되는 것은 맞지만 아수비게도 우리의 예상대로 공유된 객체를 가리키는 레퍼런스가 있다고 자동으로 공유된 원시 값을 변경할 권한이 주어지는 것은 아니다.
function foo(x) {
x = x + 1;
x; // 3
}
var a = 2;
var b = new Number(a); // 'Object(a)'도 같은 표현이다.
foo(b);
console.log(b); // 3이 아닌 2
문제는 내부의 스칼라 원시 값이 불변이란 점이다.(문자열, 불리언도 마찬가지이다.) 스칼라 원시 값2를 가진 Number
객체가 있다면, 이와 동일한 객체가 다른 원시 값을 가지도록 변경할 수는 없다. 단지 다른 값을 넣은 완전히 별개의 Number
객체를 생성할 수는 있다.
표현식 x + 1
에서 x가 사용될 때 내부에 간직된 스칼라 원시 값 2는 Number
객체에서 가동 언박싱 되므로 x = x + 1
의 x는 공유된 레퍼런스에서 Number
객체로 아주 교모하게 뒤 바뀌고 2 + 1
덧셈 결과인 스칼라 원시 값 3을 갖게 된다. 따라서 바깥의 b는 원시 값 2를 씌운, 변경되지 않은/불변의 원본 Number
객체를 참조한다.
Number
객체에(내부 원시 값 변경이 아니라) 프로퍼티를 추가하고 간접적이나마추가된 프로퍼티를 통하여 정보를 교환할 수는 있다. 그러나 별로 일반적이디조 않을 뿐더러 많은 개발자가 좋은 습관이라고 생각하지 않는다.
이렇게 객체 래퍼 Number
를 사용하기보단 차라리 처음부터 손수 객체 레퍼(obj)를 쓰는 펴이 훨씬 낫다. 그렇다고 Number
처럼 박싱된 객체 래퍼를 적절하게 잘 쓰는 것이 애당초 가능하지 않다는 말은 아니지만, 대부분의 경우 스칼라 원시 값을 사용하는 것이 좋다.
레퍼런스는 꽤 강력하지만 이따끔 걸림돌이 되기도 하고 심지어 존재하지도 않은 레퍼런스를 찾아 정처 없이 헤매기도 한다. 값-복사냐 레퍼런스-복사냐를 결정하느 유일한 단서는 값의 타입뿐이므로 사용할 값 타입을 잘 정해서 간접적으로 할당/전달 로직에 반영해야 한다.
정리
자바스크립트 배열은 모든 타입의 값들을 숫자로 인덱싱한 집합이다. 문자열은 일종의 '유사배열'이지만, 나름 특성이 있기 때문에 배열로 다루고자 할때에는 조심하는 것이 좋다. 자바스크립트 숫자는 '정수'와 '부동 소수점 숫자' 모두 포함한다.
원시 타입에는 몇몇 특수 값이 있다.
null
타입은 null
이란 값 하나 뿐이고, 마찬가지로 undefined
타입도 값은 undefined
뿐이다. undefined
는 할당된 값이 없다면 모든 변수/프로퍼티의 디폴트 값이다. void
연산자는 어떤 값이라도 undefined
로 만들어 버린다.
숫자에는 NaN
(설명은 '숫자아님'이지만, 사실은 '유효하지 않은 숫자'라고 해야 더 정확하다.), +Infinity
, -Infinity
, -0
같은 특수 값들이 있다.
단순 스칼라 원시값(문자열, 숫자 등)은 값-복사에 의해, 합성 값(객체 등)은 레퍼런스-복사에 의해 값이 할당/전달 된다. 자바스크립트에서의 레퍼런스는 다른 언어의 레퍼런스/포인터와는 전혀 다른 개념으로, 또 다른 변수/레퍼런스가 아닌 오직 자신의 값만을 가리킨다.
참고
- 한빛 미디어 : You Don't Know JS : 타입과 문법, 스코프와 클로저
'JAVASCRIPT' 카테고리의 다른 글
[JS][네이티브] 네이티브 개요 (0) | 2022.01.27 |
---|---|
[JS] this 란? (0) | 2022.01.26 |
[JS][값] 특수 값 (0) | 2022.01.26 |
[JS][값] 숫자 (0) | 2022.01.25 |
[JS][값] 문자열 (0) | 2022.01.24 |
댓글