본문 바로가기

Programming/JavaScript

[JavaScript] 원시값과 참조값 (Primitive & Reference)

 

자바스크립트의 데이터 타입은 그 작동 원리에 따라 두 종류로 나눌 수 있다.

 

- 원시값 : string, number, boolean, null, undefined

- 참조값 : object, symboll

 

Primitive(원시값)과 Reference(참조값)의 차이점

모든 변수는 선언과 할당의 과정을 거친다.

 

선언: 메모리의 빈 공간에 식별자를 저장한다.

 

* 선언된 순간 이 공간은 비어있다. 그러나 이 시점에서 변수를 호출하면 undefined가 출력되는데, 그 이유는 자바스크립트 엔진이 값이 없는 변수를 출력하려고 할 때 빈 공간 대신 undefined를 보여주기 때문이다.

 

할당: 변수의 데이터 영역에 주소를 연결한다.

 

원시값과 참조값의 차이는 주소의 차이에서 발생하는데,

 

간단히 말해서 원시값은 변수의 데이터 영역에 값이 저장된 주소를 연결하고

 

참조값내부 프로퍼티들을 위한 변수 영역을 확보하고 그 변수 영역의 주소를 연결한다.

 

즉 참조값에서 실제 데이터에 접근하려면 두 번의 참조가 필요한 것이다.

 

1. 원시값

Primitive : 원시의, 초기의, 근본의

 

일반적으로 원시값의 값을 조작할 때 '값으로 접근'한다고 표현한다.

 

하지만 엄밀히 말하면 원시값은 원본값에 접근하는 것이 아니라 원본 값이 담긴 주소에 접근하는 것이다.

 

 *원시값은 불변성(immutability)을 띄고 있다.

 

 (→ 여기서 변경 불가능하다고 진술하는 값이란 변수가 아니라 '값'을 뜻한다는 것에 주목해야 한다.)

 

그렇기 때문에 변수의 값이 바뀔 때 해당 원시값이 변경되는 것이 아니라 새로운 메모리 공간을 확보하여 새로운 값을 담은 주소를 재할당하여 변수의 값이 변경되는 것이다.

즉, 변수는 값이 담긴 주소값을 저장하고 있고 그 주소값이 바뀌면서 변수 안에 담긴 값이 변경된다.

 

따라서 '값으로 접근한다'라고 인식하고 데이터를 조작해도 큰 문제는 없을 것이다.

 

 

원시값은 데이터의 타입에 따라 데이터의 크기가 정해져있는 반면 참조값의 데이터의 크기는 가변적이다.

 

그러나 문자열은 다른 원시값과 다르게 독특한 특성이 있다.

 

< 문자열의 특성 >

 

다른 데이터타입은 값의 크기와 상관없이 동일한 메모리 공간이 필요하다.

 

예를 들어 숫자형 데이터의 경우 1도, 100000도 동일한 8바이트가 필요하다.

 

그러나 문자열의 경우 그 값의 길이에 따라 필요한 메모리 공간이 다르다.

 

그러나 *유사배열이기 때문에 마치 배열처럼 인덱스값으로 문자를 불러올 수 있고 .length 등의 메서드도 사용할 수 있다.

 

* 유사배열이란 쉽게 말해 배열은 아니지만 배열처럼 사용 가능한 요소를 말한다.
 배열인지 아닌지 판단하는 방법은 Array.isArray(요소) , array instanceof 요소 명령으로 판별할 수 있다.

 

var str = "abcde";

console.log(str[0]); // "a"

str[0] = "s";

console.log(str); // "abcde"

 

- 문자열은 인덱스를 사용해 값에 접근할 수 있다.

 

 - 그러나 변경 불가능한 원시값이기 때문에 이미 생성된 문자열의 일부를 변경해도 반영되지 않는다.

 

2. 참조값

참조값은 메모리 절약을 위해 객체의 값의 주소가 아닌 참조의 주소로 객체를 조작한다.

 

즉, 객체 자체가 아니라 객체의 참조를 조작하는 것이다.

 

따라서 객체를 가리키는 값은 '참조로 접근한다'고 표현한다.

 

참조값은 언제든 프로퍼티와 메서드를 추가하거나 삭제할 수 있다. 

 

* 원시값에는 실제로는 프로퍼티가 없지만, 추가해도 에러가 생기진 않는다.
(문자열의 경우는 위에서 설명한 바와 같이 유사배열이기 때문에 프로퍼티를 사용할 수 있다!)

 

< 얕은 복사와 깊은 복사 >

 

모든 원시값의 복사에는 깊은 복사가 일어난다.

 

깊은 복사란 말 그대로 깊은 곳에 위치한, 근본적인 실제 값이 복사되는 것을 말한다. 

 

깊은 복사를 통한 복제본의 값을 바꾸면 값 자체가 바뀐다.

var a = "abc";
var b = a; // a값을 복사해 b변수에 저장

b = 2; // b값 변경

console.log(a); // "abc"
console.log(b); // 2

 

참조값의 복사에서는 얕은 복사가 일어난다.

 

얕은 복사가 일어나게 되면 그 값이 담고 있는 객체가 복사되는 것이 아니라 그 객체를 참조하는 참조값이 복사된다.

 

따라서 복사된 객체를 변경하면 원본 객체도 함께 변경된다. 

var foo = {
  name : "foo",
}

var bar = foo; // bar변수에 foo값의 참조 주소 복사

bar.name = "eunbin"; // bar 객체의 프로퍼티 값 변경

console.log(foo.name); // "eunbin"
console.log(bar.name); // "eunbin"



 

 

"원본객체인 foo객체는 변경되면 안되는데...ㅜ"

 

그러나 복사된 객체의 프로퍼티를 변경해도 원본 객체의 프로퍼티는 변하지 않아야 하는 경우가 종종 발생한다.

 

이러한 문제를 해결하기 위해 참조값이 복사될 때 깊은 복사가 일어나도록, 즉, 참조값이 불변객체가 되도록 작업을 해주어야 한다.

 

<불변객체를 만드는 방법>

1. 프로퍼티 하나하나 복사하기! (얕은복사)

 

- 객체의 프로퍼티를 복사하여 새로운 객체를 반환하는 함수 생성

var copyObject = function (target) {
  var result = {};
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
}

var target = { a: 1, b: 2 }; // 복사할 객체

var copyTarget = copyObject(target); //copyTarget에 target객체의 프로퍼티 복사

console.log(copyTarget); // { a: 1, b: 2 }

copyTarget.a = "hello"; // 복사한 객체의 프로퍼티 변경

console.log(target); // { a: 1, b: 2 }
console.log(copyTarget); // { a: "hello", b: 2 }


 

또는 비슷한 기능을 하는 Object.assign()메소드를 사용할 수도 있다.

 

object.assign()메소드는 첫 번째 인자로 들어온 객체에 두 번째 인자로 들어온 객체의 프로퍼티를 차례대로 복사하는 메소드이다.

 

두 번째 이상의 인자에 여러 개의 출처 함수를 넣으면 해당 함수들을 병합하여 대상객체를 반환한다. 

 

(복사할 객체가 1개)

Object .assign(대상 객체, 복사할 객체1, 복사할 객체2,,,,)

(복사할 객체가 여러개)

Object .assign(대상 객체, 복사할 객체1, 복사할 객체2,,,,)

 

var target = { a: 1, b: 2 };

var copyTarget = Object.assign({}, target);

console.log(target); // { a: 1, b: 2 };

console.log(copyTarget); // { a: 1, b: 2 };

 

또는 spread operater(전개 연산자)을 이용할 수도 있다.

var arr = [1, 3, 5, 7, 9]

var copyArr = [...arr];

copyArr[1] = 10;

console.log(arr); // [1, 3, 5, 7, 9]
console.log(copyArr); // [1, 10, 5, 7, 9]

var obj = { a: 1, b: 2 }; 

var copyObj = {...obj};

copyObj.a = "hello"; 

console.log(obj); // { a: 1, b: 2 }
console.log(copyObj); // { a: "hello", b: 2 }


 

그러나 위 방법 모두 복사할 객체의 프로퍼티에 객체나 배열이 있으면 그 참조값을 복사한다.

 

따라서 객체 내에 프로퍼티로 원시값만을 취하는 객체를 복사하는 경우에만 유용하다. (중첩객체는 X)

 

2. JSON을 활용해 문자열로 전환했다가 다시 객체로 바꾸기! (깊은 복사)

 

JSON.stringify(오브젝트)

 자바스크립트 오브젝트를 스트링 포멧으로 변환하는 메소드


JSON.parse(문자열)

스트링 포멧을 자바스크립트 오브젝트로 변환하는 메소드

 

var target = { 
  name: "eunbin, 
  birthDay: {year: 1998, month: 2, day: 28},
  age: function () {
    console.log("24");
    },
};

var copyTarget = JSON.parse(JSON.stringify(target));


console.log(target); // { name: "eunbin", birthDay: {...}, age: f };

console.log(copyTarget); // { name: "eunbin", birthDay: {...}};

 

3. 재귀함수 사용하기!(깊은 복사)

 

재귀함수란 함수 내에서 자신을 다시 호출하여 작업을 수행하는 방식의 함수를 의미한다. 

 

1번 방법에서 객체인 프로퍼티의 경우 참조값이 복사되는 단점이 있었다.

 

재귀함수를 이용하면 프로퍼티의 데이터가 객체인 경우 해당 객체를 매개변수로 지정해 함수 자신을 다시 호출한다.

 

function copyObject(target) {
    var copyTarget = {};
    for(var i in target) {
        // 데이터타입이 오브젝트이면 같은 함수 다시 호출
        if(typeof(target[i])=="object" && target[i] != null) 
            copyTarget[i] = cloneObject(obj[i]);
        else
            copyTarget[i] = obj[i];
    }
    return copyTarget;
}

 

 

 

참고

 

velog.io/@ddalpange/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EA%B0%9D%EC%B2%B4-%EB%B3%B5%EC%82%AC%ED%95%98%EA%B8%B0

 

 

자바스크립트 객체 복사하기

자바스크립트에서 객체를 복사하는 방법을 설명하는 글. 시작하기 전에 A코드 B코드 A코드와 B코드 두가지의 코드가 있다. 두 코드 모두 b에 a를 대입하였다.라고 생각하는가? 혹은 두 코드 모두 b

velog.io

velog.io/@ddalpange/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EA%B0%9D%EC%B2%B4-%EB%B3%B5%EC%82%AC%ED%95%98%EA%B8%B0