자바스크립트 딥다이브 - 원시값과 객체의 비교

ChoiYongHyeun·2023년 12월 8일
0

원시값과 객체의 비교

자바스크립트는 원시 타입과 객체 타입의 데이터 타입이 존재한다고 했다.

그럼 이 둘을 왜 구분할까 ?

뭐가 다를까 ?

  1. 원시타입의 값은 변경 불가능한 값이다. 이에 비해 객체 타입의 값은 변경 가능한 값이다.
  2. 원시 값을 변수에 할당하면 변수에는 실제 값이 저장된다. 이에 비해 객체를 변수에 할당하면 변수에는 참조 값이 저장된다.
  3. 원시 값을 갖는 변수에 할당하면 원본의 원시 값이 복사되어 전달된다. 이를 값에 의한 전달이라 하며, 객체는 변수를 다른 변수에 할당하면 원본의 참조 값이 복사되어 전달된다. 이를 참조에 의한 전달이라 한다.

네 ?

원시값

변경 불가능 한 값

원시 타입의 값은 변경 불가능한 값이다. 다시 말해 한 번 생성된 원시 값은 읽기 전용 값으로 변경 불가능하다.

원시 타입에 값을 지정하면 어떻게 되는지를 생각해보자

특정 메모리에 값을 넣고, 해당 값을 가진 메모리의 주소를 변수에 할당한다.

즉 변수는 해당 값의 주소를 식별하기 위한 이름이고

변수 값은 해당 변수의 주소를 평가하여 생성된 값을 의미한다. (해당 주소에 존재하는 값)

변경 불가능 한것은 변수가 아니라 값에 대한 진술이다.

즉 변수값은 변경 할 수 있으나, 메모리에 할당 되어 있는 값은 변경이 불가능 하다.

변수의 상대 개념인 상수는 재할당이 금지된 변수를 말한다.

상수도 값을 저장하기 위한 메모리 공간이 필요하기에 변수라고 할 수 있으나 변수는 언제든지 값을 교체 할 수 있으나 상수는 단 한 번만 할당이 허용되므로 변수 값을 변경 할 수 없다.

// 변수
var variable = 10;
console.log(variable); // 10
variable = 20;
console.log(variable); // 20

// 상수
const constant = 10;
console.log(constant); // 10
constant = 20; // TypeError: Assignment to constant variable. 
console.log(constant);

상수는 변수이지만 재할당이 불가능한 변수이다.

원시 값은 변경 불가능한 읽기 전용의 값이다. 이러한 특성을 불변성 (immutablity) 라고 한다.

변수에 새로운 원시 값을 넣는 것은 원시 값을 변경하는게 아니라 새로운 메모리 주소에 새로운 원시 값을 넣고 해당 메모리의 주소를 변수에 할당하는 것임을 잊지 말자

왜 원시값을 변경하지 못하게 할까 ?

원시 값인 변수 값을 변경 할 수 있다면 예기치 않게 변수 값이 변경 될 수 있으며 상태 변경을 추적하기 어렵게 한다.

문자열과 불변성

원시 값을 저장하려면 원시 값을 할당 할 메모리 공간의 크기를 결정해야 한다.

원시 타입 별로 메모리 공간의 크기가 정해진다고 하였다.

단 문자열 타입 (2바이트) , 숫자열 타입 (8바이트) 이외의 원시 타입은 크기를 명확히 규정하고 있지 않고 있다.

원시 값인 문자열은 다른 원시값과 비교 할 때 독특한 특징이 있다.

문자열은 0 개 이상의 문자로 이뤄진 집합이며 1개의 문자는 2바이트의 메모리 공간에 저장된다.

따라서 문자열은 몇 개의 문자로 이뤄져있냐에 따라 필요한 메모리 공간의 크기가 결정된다.

숫자값은 1이나 10000000 도 동일한 8바이트가 필요하지만

'1' 은 2바이트, 10000000 은 20바이트가 필요하다.

따라서 다른 언어인 C 에선 문자를 위한 데이터 타입 char 만 존재 하며 문자열을 배열로 처리한다.

자바에서는 문자열을 String 객체로 처리한다.

하지만 자바스크립트는 개발자의 편의를 위해 원시 타입인 문자열을 제공한다.

또한 문자열은 유사 배열 객체이면서 이터러블이므로 배열과 유사하게 문자에 접근 할 수 있다.

유사 배열 객체

배열처럼 인덱스로 프로퍼티 값에 접근 할 수 있고 length 프로퍼티를 갖는 객체를 의미한다.

var string = 'String';
console.log(string[0]); // S
for (var i = 0; i < string.length; i++) {
  console.log(string[i]);  // S t r i n g
}

다만 주의 할 점이 있다. 문자열은

var string = 'String';
console.log(string); // String
string = 'Hello';
console.log(string); // Hello

다음처럼 변수 값을 변경하는 것은 가능하다.

이것은 변수에 새로운 원시값을 할당하는 것이니 가능하다.

var string = 'String';
console.log(string); // String
string[0] = 'V';
console.log(string); // String

다만 인덱스를 이용해 원시 값에 접근 한 후 원시 값을 변경하려하는 것은 불가능하다.

원시값은 불변하니까 ..

값에 의한 전달

var score = 80;
var copy = score;

console.log(score); // 80
console.log(copy); // 80
score = 90;
console.log(score); // 90
console.log(copy); // 80

해당 코드에서는 score 라는 변수애 80 이란 값을 할당했고 copy 변수에는 score 변수를 할당했다.

변수를 할당하면 변수가 할당 될까 ? 변수에 존재하는 원시 값이 할당될까 ?

변수에 존재하는 원시 값이 할당 된다. 이를 값에 의한 할당이라고 한다.

그렇기 때문에 score 의 메모리 주소가 A 일 때 A 에 80이 존재하는 경우

copy = score 를 하면 copy 는 본인의 메모리 주소 B 에 80 을 할당하게 된다.

그렇기 때문에 score 의 변수 값을 변경해도 copy 가 가리키고 있는 메모리 주소 B 엔 여전히 80 이 존재하기 대문에 변경되지 않는다.

엄밀히 말하면 변수에 변수를 할당할 때 원시 값을 할당 하는 것이 아닌

할당 된 변수가 가리키고 있는 메모리 주소를 전달 받고, 해당 주소의 원시 값을 복사해 새로운 메모리 주소에 해당 원시 값을 할당한다.

객체

객체는 가질 수 있는 프로퍼티의 데이터 타입의 제한이 없기 때문에

원시 타입에 비해 메모리 접근 방식이 다양하다.

변경 가능한 값

var person = {
  name: 'kim',
};

person.name = 'lee';
console.log(person); { name : lee }

변수를 할당 받은 객체는 (name 을 할당 받은 person) 어떻게 변수에 접근할까 ?

A 라는 메모리 주소에 person 객체가 존재한다.

A에 존재하는 person 객체는 {} 로 감싸진 자료구조가 존재하는 메모리 B 를 가리키고 있다.

B 에는 {} 로 감싸진 자료구조가 존재한다.

{name : 'kim'} 처럼 말이다.

하지만 객체는 변경이 가능하다고 했다.

위처럼 personnamelee 로 변경하면 B에 존재하는 name 변수가 lee 가 존재하는 새로운 메모리 C 를 추가하고 재할당할까 ?

노농

객체는 이전에 말했듯이 용량이 매우 클 수도 있고 복잡 할 수 있기 때문에 원시 값을 변경 가능하게 설계 되어 있다.

효율적인 메모리 관리를 위하여

name 의 값을 lee 로 변경하면 B 에 존재하는 {} 내부의 내용이 변경된다.

이를 통해 효율적인 메모리 관리가 가능하지만 그에 따른 부작용으로는 여러 개의 식별자가 하나의 객체를 공유 할 수 있다

var person1 = {
  name: 'kim',
};

var person2 = person1;

person2.name = 'lee';
console.log(person1); // lee
console.log(person2); // lee

person2name 을 바꿨음에도 person1name 도 바뀌었다

이는 person1 이 가지고 있는 {name : kim} 객체를 person2 도 공유하고 있기 때문이다.

이에 person2{} 를 변경하면 person1 도 같은 객체를 공유하고 있기 때문에 같이 변경된다.

var person1 = {
  name: 'kim',
};

var person2 = {
  name: 'kim',
};

person2.name = 'lee';
console.log(person1); // kim
console.log(person2); // lee

이건 괜찮다.생김새는 같지만 다른 객체로서 독립적인 메모리 공간에 존재한다.

var person1 = {
  name: 'kim',
};

var person2 = person1;

다음처럼 객체를 복사하는 행위를 얕은 복사 라고 한다.

얕은 복사 vs 깊은 복사

얕은 복사는 내부 객체들까지 새로운 참조를 만들지 않고 원래 객체의 참조를 공유한다.
위 예시처럼 persone2person1 이 가지고 있는 객체 {name : kim} 과 같은 주소를 참조하고 있다.

깊은 복사는 내부 객체들까지 새로운 참조를 만들어 새로운 객체를 생성한다.
이로써 깊은 복사된 객체는 복사한 원본과 독립적인 개체가 된다.

var person1 = {
  name: 'lee',
};
var person2 = Object.assign({}, person1);
person2.name = 'kim';
console.log(person1); // lee
console.log(person2); // kim

다양한 방법으로 깊은 복사를 시행 할 수 있는데 이 중 Object.assign 을 이용해서 깊은 복사를 시행해주었다.
깊은 복사는 원본 객체의 주소를 참조하는 것이 아닌 주소에 존재하는 값을 복사하여 새로운 객체를 생성한다.

참조에 의한 전달

person2 = person1

처럼 person1 이 가지고 있는 객체가 참조하고 있는 주소를 person1 에게 할당하는 것을 참조에 의한 전달이라고 한다.

예시를 통해 생각해보기

var person1 = {
  name: 'lee',
};

var person2 = {
  name: 'lee',
};

console.log(person1 === person2);
console.log(person1.name === person2.name);

에서 결과값이 어떻게 될까 ?

=== 는 변수에 저장되어 있는 값을 타입을 변환하지 않고 비교한다.

원시값을 할당한 변수는 원시값 자체를 가지고 있고, 변수를 할당한 객체는 변수의 참조값을 가지고 있다.

person1person2 는 동일한 형태의 {} 를 가지고 있으나 두 객체의 참조 값이 다르기 때문에
console.log(person1 === person2);false 가 나온다.

두 번째로 console.log(person1.name === person2.name) 이 부분에서

person1 , person2name 은 원시값인 lee 를 참조하고 있는 변수이다.

그러니 원시값을 할당하고 있는 변수의 비교에서는 두 변수의 원시값이 lee 로 같기 때문에 true 가 나온다.

변수의 원시값을 비교한것이지, 원시값이 존재하는 주소를 참조한 것이 아니다. person1.name 과 person2.name의 원시값은 같지만 각 원시값이 존재하는 메모리 주소가 다르다.

그럼 얕은 복사의 경우엔 어떻게 될까 ?

var person1 = {
  name: 'lee',
};
var person2 = person1;

console.log(person1 === person2); // true
console.log(person1.name === person2.name); // true

가 나온다.

그 이유는 person1 과 perrson2 는 같은 객체인 {} 의 주소를 참조하고 있기 때문이다.

깊은 복사 재귀적으로 구현하기

function deepCopy(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 배열일 경우 배열의 객체들에 대해서 복사

  if (Array.isArray(obj)) {
    const newArray = [];
    for (let i = 0; i < obj.length; i++) {
      newArray[i] = deepCopy(obj[i]);
    }
    return newArray;
  }

  // 객체인 경우
  const newObj = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = deepCopy(obj[key]);
    }
  }
  return newObj;
}

// 테스트
const originalObject = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    country: 'USA',
  },
  hobbies: ['reading', 'traveling'],
};

const copiedObject = deepCopy(originalObject);

console.log(originalObject);
console.log(copiedObject);
console.log('복사본의 값을 변경한 후');
copiedObject['name'] = 'Tom';
console.log(originalObject);
console.log(copiedObject);
{
  name: 'John',
  age: 30,
  address: { city: 'New York', country: 'USA' },
  hobbies: [ 'reading', 'traveling' ]
}
{
  name: 'John',
  age: 30,
  address: { city: 'New York', country: 'USA' },
  hobbies: [ 'reading', 'traveling' ]
}
복사본의 값을 변경한 후
{
  name: 'John',
  age: 30,
  address: { city: 'New York', country: 'USA' },
  hobbies: [ 'reading', 'traveling' ]
}
{
  name: 'Tom',
  age: 30,
  address: { city: 'New York', country: 'USA' },
  hobbies: [ 'reading', 'traveling' ]
}

모듈을 이용해서 깊은 복사를 구현 할 수 있으나 아직 모듈 사용법에 대해서 공부하지 않았기 때문에 재귀적으로 구현하는 코드를 구현해봤다.

(또 어떤 모듈들은 객체 안에 객체가 들어있거나 하는 경우엔 모두 복사하지는 못한다고 하더라)

function deepCopy(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
	...
  }

재귀의 포인트는 객체를 계속하여 받으면서 객체가 아닌 원시값을 참조하고 있는 변수일 경우엔 해당 변수의 원시값을 return 하게 한다. (변수의 원시값을 복사하면, 원시값이 존재하는 주소를 참조하는게 아니라 원시값을 복사하여 새로운 주소에 같은 원시값을 할당한다.)

  // 배열일 경우 배열의 객체들에 대해서 복사

  if (Array.isArray(obj)) {
    const newArray = [];
    for (let i = 0; i < obj.length; i++) {
      newArray[i] = deepCopy(obj[i]);
    }
    return newArray;
  }

  // 객체인 경우
  const newObj = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = deepCopy(obj[key]);
    }
  }
  return newObj;
}

// 테스트
const originalObject = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    country: 'USA',
  },
  hobbies: ['reading', 'traveling'],
};

그리고 두 단계를 거친다. 복사하고자 하는 객체가 배열일 경우에는 newArray 를 만들어 준 후 복사하고자 하는 배열에 들어있는 변수들을 복사하여 newArray 에 담아준다.
(jsappend 기능이 없나 ? 바로 인덱스로 접근이 가능하다.)

jsarray 에 값을 추가할 때에는 바로 인덱스로 접근이 가능하다.

const arr = [];
arr[10] = 50;
console.log(arr); // [ <10 empty items>, 50 ] 

신기하구만 , 배열을 만들 때 미리 공간들은 만들어두는건가 ? 나중에 배우겠지

그리고 만약 복사하고자 하는 객체가 객체인 경우에는 객체에 존재하는 프로퍼티 키값을 가지고 newObj 에 추가해준다.

  const newObj = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = deepCopy(obj[key]);
    }
  }
  return newObj;

이 단계에서 for (let key in obj) 단계에서 이미 객체에 존재하는 프로퍼티 키값을 key 에 설정해뒀는데 그 후 조건문으로 obj.hasOwnProperty(key) 라는 조건문은 왜 실행시키는지 물어봤다.

hasOwnProperty를 사용하는 이유는 객체가 직접 소유한 속성만을 고려하기 위해서입니다. 예를 들어

var obj = {
  name: 'lee',
};
Object.prototype.additionalProperty = 'additional';
for (var key in obj) {
  console.log(key); // 'name', 'additionalProperty'
}

위의 코드에서 Object.prototype에 속한 additionalProperty도 반복문에서 나타납니다. 이는 객체가 상속받은 속성까지 반복되기 때문입니다.

hasOwnProperty를 사용하면 직접 소유한 속성만을 고려할 수 있습니다:

직접 소유한 속성 : 객체에 직접적으로 추가하거나 정의한 속성을 의미한다.

어찌됐든 깊은복사 는 객체에 존재하는 모든 객체들에 대해서 원시값을 참조하고 있는 변수까지 재귀적으로 들어가 원시값을 참조하고 있는 변수를 새로운 객체에 담아줌으로서 독립적앤 객체를 생성하는거구나 !

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글