Front-End/JavaScript

[You Don't Know JS] #04 자바스크립트의 값(value) vs 레퍼런스(reference)

koh1018 2022. 4. 6. 14:28
반응형

 

 

/* 이 글은 카일 심슨의 You Don't Know JS를 읽으며 배운 것들을 개인적으로 정리하고 기록하기 위한 글입니다. */

 

 

 

들어가기 전에..

  • 포인터(Pointer) : Null 허용, 참조 대상에 대해 주소값을 할당
  • 레퍼런스(Reference) : Null 허용 X, 참조 대상을 그대로 할당

즉, 포인터와 레퍼런스는 어떠한 대상을 가리킨다는 점에서는 같지만 포인터는 주소값, 레퍼런스는 값 그 자체를 할당한다는 점에서 다르다. (레퍼런스는 마치 일반변수처럼 접근이 가능하다.)

 

자바스크립트에서의 레퍼런스는 다른 언어의 레퍼런스/포인터와는 전혀 다른 개념으로, 또 다른 변수/레퍼런스가 아닌 오직 자신의 값만을 가리킨다.

 

 

 


 

 

다른 언어에서 값은 사용하는 구문에 따라 값 복사 또는 레퍼런스 복사의 형태로 할당 또는 전달된다.

하지만 자바스크립트에서는 값 또는 레퍼런스의 할당 또는 전달을 제어하는 구문 암시(Syntactic Hint)가 전혀 없고 '값의 타입' 만으로 값 복사 또는 레퍼런스 복사 중 한 쪽이 결정된다.

 

이처럼 자바스크립트는 포인터라는 개념 자체가 없고 참조하는 방법도 조금 다르다.

자바스크립트에서는 어떤 변수가 다른 변수를 참조할 수 없다.

 

아래 예시 코드를 보겠다.

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]

보면 number 타입의 경우 값이 복사가 됐지만 배열(객체의 하위타입)의 경우 레퍼런스가 복사됐다.

즉, 값의 타입에 의해 값 복사 또는 레퍼런스 복사가 결정됨을 알 수 있다.

b를 바꿈으로써 a까지 동시에 값을 변경할 방법은 없다.

하지만 c와 d는 공유 값 [1,2,3]에 대한 개별 레퍼런스이다. (c와 d가 [1,2,3]을 소유하는 것이 아니라 동등하게 참조만 하는 상태)

 

 

값 복사 방식 레퍼런스 복사 방식
null
undefined
string
number
boolean
symbol (ES6에 추가)
객체
함수

단순 값, 다시 말해 스칼라 원시 값(Scalar Primitives)은 언제나 값 복사 방식으로 할당/전달된다.

객체나 함수 등 함성 값(Compound Values)은 할당/전달 시 반드시 레퍼런스 사본을 생성한다.

 

 

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;  // [1,2,3,4]

a를 인자로 넘기면 a는 배열이므로 a의 레퍼런스 사본이 x에 할당된다.

따라서 x와 a는 동일한 [1,2,3] 값을 가리키는 별도의 레퍼런스이고 함수 내부에서 값을 변경한다고 해서 a가 참조하고 있는 값에는 아무런 영향이 없다.

 

만약 함수 안에서도 변경하고 싶다면 다음과 같이 할 수 있다.

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;  // [4,5,6,7]

위 코드는 새 배열을 생성하는 코드가 아니라 이미 두 변수가 공유한 배열을 변경하는 코드이므로 a의 값이 변했다.

 

 

 


 

 

그렇다면 합성 값(Compound Values)을 값 복사하고 스칼라 원시 값(Scalar Primitives)을 레퍼런스 복사하려면 어떻게 해야할까?

 

- 합성 값(Compound Values)을 값 복사하는 경우

foo( a.slice() );

위와 같이 값의 사본을 만들어 전달한 레퍼런스가 원본을 가리키지 않게 하면 된다.

인자 없이 slice()를 호출하면 얕은 복사(Shallow Copy)에 의한 새로운 배열의 사본을 만든다.

이렇게 하면 foo()는 a의 내용을 건드릴 수 없다.

 

- 스칼라 원시 값(Scalar Primitives)을 레퍼런스 복사하는 경우

function foo(wrapper) {
  wrapper.a = 42;
}

var obj = {
  a: 2
};

foo(obj);
obj.a;  // 42

스칼라 원시 값인 a를 레퍼런스 복사하기 위해선 원시 값을 다른 합성 값(객체, 배열 등)으로 감싸야한다.

obj는 합성 값이므로 foo() 함수의 wrapper에 레퍼런스 사본이 전달되고 래퍼 인자의 값을 바꾼다.

 

같은 원리로 2와 같은 스칼라 원시 값을 레퍼런스 형태로 넘기려면 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

위 코드에 따르면 b는 3이 아니라 2가 나온다.

이는 내부의 스칼라 원시 값이 불변이기 때문이다. (문자열, 불리언도 마찬가지)

스칼라 원시 값 2를 가진 Number 객체가 있다면, 이와 동일한 객체가 다른 원시 값을 가지도록 변경할 수 없다.

 

표현식 x + 1에서 x가 사용될 때 내부에 간직된 스칼라 원시 값 2는 Number 객체에서 자동 언박싱(추출)되고 결과값 x는 공유된 레퍼런스에서 Number 객체로 교묘하게 뒤바뀐 뒤 스칼라 원시 값 3을 갖게 된다.

따라서 바깥의 b는 원시 값 2를 씌운, 변경되지 않은 불변의 원본 Number 객체를 참조한다.

 

반응형