출처 모던 자바스크립트 Deep Dive을 보고 정리한 내용입니다.
자바스크립트는 크게 원시 타입과 객체 타입으로 구분할 수 있다. 둘의 차이점을 알아보자
원시 타입은 변경이 불가능한 값이다. 변수에 값을 할당하면 메모리에 공간이 만들어지고 그 공간에는 원시값이 저장된다. 식별자를 통해 값을 재할당하여도 저장된 원시값은 변경되지 않는다. 새로운 공간이 만들어지고 그곳에 재할당한 원시값이 저장될 뿐이다. 식별자는 가리키고 있는 주소만 달라진다.
var age; //원시값: undefined, 주소: 0x000000F2
age = 3; //원시값: 3, 주소: 0x000000FF
원시값이 변경이 불가능하다는 것은 변수 age의 선언과 동시에 할당한 undefined라는 원시값이 같은 메모리에서 3으로 변경되지 않음을 말한다. 3이라는 원시값을 갖는 새로운 메모리 공간이 만들어져 age는 새로 만들어진 메모리 주소를 가리키게 된다.
var age = 3; //원시값: 3, 주소: 0x000000FF
var age2 = age; //원시값: 3, 주소: 0x00000103 or 0x000000FF
console.log(age === age2) //true
위 코드에서 age에 원시값 3을 할당한다. age는 원시값 3을 저장하고 있는 메모리 주소 0x000000FF를 갖는다. age2는 age의 원시값인 3을 복사한다. 그러나 이때 age2가 age의 메모리 주소를 갖는지, 새로운 메모리 공간의 주소를 갖는지 알 수 없다. 왜냐하면 ECMAScript 사양에는 변수를 통해 메모리를 어떻게 관리해야하는지 명확히 정의하지 않고 있기 때문이다.
var age = 3; //원시값: 3, 주소: 0x000000FF
var age2 = age; //원시값: 3, 주소: 0x00000103 or 0x000000FF
console.log(age, age2) //3, 3
console.log(age === age2) //true
age2 = 5; //원시값: 5, 주소: 0x00000FFF
console.log(age, age2) //3, 5
console.log(age === age2) //false
그러나 중요한 것은 age2가 age를 복사한 시점에 갖은 주소를 갖는지, 다른 메모리를 할당하든 상관없이, age2에 새로운 원시값이 재할당되면 age에 어떠한 영향을 끼치지 않는다. age2는 재할당된 시점에 원시값 5를 갖는 공간을 할당하여 그 메모리 공간을 가리키게 된다.
여기서 주목해야할 것은 문자열이다. 문자열은 ECMAScript 사양에 따라 1개의 문자당 2바이트로 저장되며 인덱스로 프로퍼티 값에 접근할 수 있고, length 프로퍼티를 갖는다. 그러나 문자열은 변경이 불가능한 값이다.
var str = "string";
str[0] = 'S';
console.log(str); //string
문자열은 유사 배열 객체로 인덱스를 통해 프로퍼티에 접근이 가능하지만 값을 변경할 수 없다. 원시 타입의 변수가 갖는 특징처럼 새로운 할당을 통해서만 값의 변경이 가능하다.
객체 타입은 변경이 가능한 값이다. 자바스크립트의 객체는 C++, Java와 다르게 객체가 할당된 이후에도 동적으로 프로퍼티를 추가하거나 삭제할 수 있다는 차이점이 있다. 이러한 방식때문에 객체는 선언되면 원시값이 아닌 객체가 저장되어있는 메모리 주소를 갖는다. 이러한 특징때문에 객체 타입의 '변수는 값을 갖는다', '변수의 값은 ~다'가 아닌 '변수는 객체를 참조(reference)한다.' 또는 '변수는 객체를 가리키고(point) 있다'라고 한다.
Q: 왜 자바스크립트의 객체 타입 변수는 참조할까?
A: 앞서 서술하였듯 자바스크립트의 객체는 다른 언어와 달리 객체가 할당된 이후에 동적으로 프로퍼티를 추가하거나 삭제할 수 있다. 원시 타입의 변수는 메모리 소비가 적지만 객체는 상대적으로 메모리 소비가 크고, 비용이 많이 든다. 따라서 메모리를 효율적으로 사용하고, 객체를 복사하여 생성하는 비용을 절약하기 위해서 객체는 변경이 가능한 값으로 설계되었다.
자바스크립트의 객체 타입이 객체 값이 저장된 메모리 주소를 참조하고 있기 때문에 발생하는 문제가 있다. 여러 개의 식별자가 하나의 객체를 공유할 수 있다는 것이다.
var obj = {};
obj2 = obj;
console.log(obj); //
obj2.name = 'JK Cho';
console.log(obj); //{name: JK Cho}
console.log(obj === obj2) //true
위 코드의 결과처럼 원시 타입의 변수와는 다르게 obj2의 변경이 obj에게 영향을 주고 있음을 확인할 수 있다. 이를 '얕은 복사'라고 말한다.
const lodash = require('lodash');
var obj = {};
obj2 = lodash.cloneDeep(obj);
console.log(obj); //
obj2.name = 'JK Cho';
console.log(obj); //
console.log(obj2) //{name: JK Cho}
console.log(obj === obj2) //false
위 코드는 lodash모듈의 cloneDeep함수를 사용하여 깊은 복사를 사용한 결과다. 얕은 복사와는 다르게 obj2의 변경이 obj에게 영향을 주고 있지 않음을 알 수 있다. 즉 깊은 복사통해 값을 할당하면 원시 타입의 변수가 동작하는 방식과 같게 다른 메모리 주소를 가리킨다.
ECMAScript는 자바스크립트가 변수를 저장함에 있어 사용되는 동작 방식을 정의하지 않고 있다. 공유에 의한 전달(Call by sharing)이라고 표현하는 경우도 있지만 이는 자바스크립트의 동작방식을 정확히 설명하지는 못한다. 그러나 윈시 타입 변수의 저장방식과 객체 타입 변수의 저장방식 모두 식별자가 기억하는 메모리 공간에 저장된 값을 복사한다는 점은 동일하다. 그렇기 때문에 자바스크립트에서는 값에 의한 전달만이 존재한다고 말할 수 있다.