11. 원시값과 객체의 비교




원시타입 vs 객체타입?

  • 원시값은 immutable 객체는 mutable
  • 원시값을 변수에 할당하면 변수(확보된 메모리 공간)에는 실제 값이 저장
  • 객체를 변수에 할당하면 변수(확보된 메모리 공간)에는 참조 값이 저장
  • 원시값을 갖는 변수를 다른 변수에 할당하면 원본의 원시 값이 복사되어 전달 (값에 의한 전달)
  • 객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조 값이 복사되어 전달 (참조에 의한 전달)

원시값

변경 불가능한 값

  • 한번 생성된 원시값은 읽기 전용 값으로서 변경할 수 없다. > 신뢰성 보장
  • 변수 : 하나의 값을 저장하기 위해 확보한 메모리 공간 자체, 그 메모리 공간을 식별하기 위해 붙인 이름
  • 값 : 변수에 저장된 데이터로서 표현식이 평가되어 생성된 결과
  • immutable : 변수가 아니라 값
  • 원시 값은 변경이 불가능하다 === 원시 값 자체는 변경 불가능, 변수값은 변경 가능(by 재할당)(엄밀히 교체)
  • 상수 : 한번만 할당이 허용되어 변수 값을 교체할 수 없다. (재할당 금지된 변수)
  • 상수와 immutable 값을 동일시 여기면 안된다.
// const 키워드를 사용해 선언한 변수는 재할당이 금지된다. 상수는 재할당이 금지된 변수일 뿐이다.
const o = {}

// const 키워드를 사용해 선언한 변수에 할당한 원시 값(상수)은 변경할 수 없다.
// 하지만 const 키워드를 사용해 선언한 변수에 할당한 객체는 변경할 수 있다.
o.a = 1
console.log(o) //{ a: 1 }
  • 값의 재할당시, 새로운 메모리 공간을 확보하고, 재할당한 원시 값을 저장한 후, 변수가 참조하던 메모리 공간의 주소가 변경된다. (why? 변수에 할당된 원시 값이 immutable한 값이기 때문에)
  • 불변성을 갖는 원시값을 할당한 변수는 재할당 이외에 변수 값을 변경할 수 있는 방법이 없다.
  • 불변성을 없앤다면, 예기치 않게 변수 값이 변경될 수 있게 되고 상태 변경을 추적하기 어려워진다.

문자열과 불변성

  • 원시값을 저장하려면 확보해야하는 메모리 공간의 크기를 결정해야 한다.
  • ECMAScript 에서는 문자열 2바이트, 숫자 타입 8바이트만 규정하고 있다.
  • 문자열은 0개 이상의 문자로 이루어져있으면 1개의 문자는 2바이트의 메모리 공간에 저장된다.
  • 따라서 문자열은 몇 개의 문자로 이뤄졌느냐에따라 필요한 메모리 공간의 크기가 결정된다.
  • 이와 같은 이유로 c에서는 문자열을 문자의 배열로, java는 String 객체로 처리
  • but js에서는 원시타입으로 문자열을 제공하며 immutable하다.
var str = "hello";
// hello 가 저장된 메모리 공간의 첫번째 메모리 주소셀을 가리킨다.
str = "world";
// 새로운 메모리 공간에 world를 생성하고 이 공간의 첫번째 메로리 주소를 가리킨다.
// 이때 메모리에 hello, world 둘다 존재한다.
// 식별자 str이 hello를 가리키다가 world를 가리키도록 변경되었을 뿐이다.
  • 문자열은 유사 배열 객체이면서, 이터러블이므로 배열과 유사하게 각 문자에 접근할 수 있다.

유사 배열 객체 마치 배열처럼 인덱스로 프로퍼티 값에 접근할 수 있고, length 프로퍼티를 갖는 객체 문자열은 배열처럼 인덱스를 통해 각 문자에 접근 가능 lenght 프로퍼티를 갖기 때문에 유사 객체, for문으로 순회 가능 (원시값을 객체처럼 사용하면 원시값을 감싸는 래퍼 객체로 자동 변환된다)

var str = "hello";

// 문자열은 유사배열이므로 인덱스로 접근할 수 있다.
// 하지만 문자열은 원시값으로 imuutable하기 때문에 값을 변경할 수 없다.
// but 에러가 발생하지 않는다.
str[0] = "H";

console.log(str); //hello
  • 이미 생성된 문자열의 일부 문자를 변경해도 반영 x
  • 한번 생성된 문자열은 읽기 전용 값으로서 변경 x
  • 이는 예기치 못한 변경으로부터 자유롭기 때문에 신뢰성 보장
  • but, 변수에 새로운 문자열을 재할당하는 것은 가능.

값에 의한 전달

var score = 80;
var copy = score; // score는 80으로 평가됨

console.log(score, copy); // 80 80
console.log(score === copy); //true

score = 100;

console.log(score, copy); // copy는 ?? 100 80
console.log(score === copy; //false
  • 변수에 변수를 할당했을 때 무엇이 어떻게 전달되는가?
  • 변수에 원시값을 갖는 변수를 할당하면, 할당받는 변수(copy)에는 할당되는 변수(score)의 원시값이 복사되어 전달된다. (=== 값에 의한 전달)
  • score 변수와 copy 변수의 값 80은 각각 다른 메모리 공간에 저장된 별개의 값이다.
  • score값이 변하더라도 copy에는 어떠한 영향도 주지 않는다.
  • but ECMAScript 사양에는 변수를 통해 메모리를 어떻게 관리해야하는지 명확하게 정의되어 있지 않다.
  • 따라서 실제 js 엔진을 구현하는 제조사에 따라 실제 내부 동작 방식은 다를 수 있다.

모던js

  • 그 이유로 코어 자바스크립트에서 설명하는 방식과 이 개념이 다르다.
  • 변수에 원시 값을 갖는 변수를 할당하는 시점에는 두 변수가 같은 원시 값을 참조하다가 어느 한쪽의 변수에 재할당이 이뤄졌을 때 비로소 새로운 메모리 공간에 재할당된 값을 저장하는 경우
  • 파이썬이 코어 자바스크립트 내용처럼 동작한다.
  • 값에 의한 전달 이라는 용어도 ECMAScript 사양에는 등장하지 않는다.
  • 값에 의한 전달도 오해가 있다. 값이 전달되는 것이 아니라 메모리 주소가 전달되기 때문이다.
  • 식별자 : 어떤 값을 구별해서 식별해 낼 수 있는 고유한 이름
  • 식별자는 메모리 공간에 저장되어 있는 어떤 값을 구별해서 식별해낼 수 있어야 하므로, 식별자는 값이 아닌 메모리 주소를 기억하고 있다.
  • 식별자로 값을 구별해서 식별한다는 것 : 식별자가 기억하고 있는 메모리 주소를 통해 메모리 공간에 저장된 값에 접근할 수 있다는 의미. 식별자는 메모리 주소에 붙인 이름
var score = 80
var copy = 80
// 1. 새로운 80을 생성해서 메모리 주소를 전달한다. 할당 시점에 두 변수가 기억하는 메모리 주소가 다르다.
// 2. socre의 변수값 80의 메모리 주소를 그대로 전달. 할당 시점에는 두 변수가 기억하는 메모리 주소가 같다.
  • 값에 의한 전달도 사실은 메모리 주소를 전달한다.
  • 단, 전달된 메모리 주소를 통해 메모리 공간에 접근하면 값을 참조할 수 있다.
  • 중요한건, 1방식이든 2방식이든, 어느 하나의 변수가 재할당하는 시점에는 결국 두 변수의 원시 값은 서로 다른 메모리 공간에 저장된 별개의 값이 되어 어느 한쪽에서 재할당을 통해 값을 변경하더라도 서로 간섭할 수 없다는 것.

객체

  • 프로퍼티 개수가 정해져 있지 않고, 동적으로 추가되고 삭제할 수 있다.
  • 프로퍼티의 값에 제약 x
  • 따라서, 원시값과 같이 확보해야할 메모리 공간의 크기를 사전에 정해둘 수 없다.
  • 객체의 구현방식도 브라우저 제조사마다 다를 수 있다.
  • 원시값에 비해 복잡하고 구현 방식도 다르며, 비용이 많이 든다.
  • 따라서 객체는 원시 값과는 다른 방식으로 동작하도록 설계되어있다.

js 객체의 관리 방식

  • js 객체는 프로퍼티 키를 인덱스로 사용하는 해시 테이블이다.
  • js 엔진에서 해시테이블과 유사하지만 높은 성능을 위해 일반적인 해시 테이블보다는 나은 방법으로 객체를 구현
  • 클래스 기반 oop는 사전에 정의된 클래스를 기반으로 객체 생성.
  • 객체를 생성하기 이전에 이미 프로퍼티와 메서드가 정해져 있고, 그대로 객체 생성
  • 객체가 생성된 이후에는 프로퍼티 추가, 삭제 x
  • but js는 클래스 없이 객체를 생성할 수 있으며, 객체가 생성된 이후에도 동적으로 프로퍼티와 메서드를 추가할 수 있다.
  • 사용면에서는 편리하지만, 생성과 프로퍼티 접근에 비용이 더 많이 드는 비효율적인 방식이다.
  • V8에서는 프로퍼티에 접근하기 위해 동적 탐색 대신 히든 클래스를 사용한다. (자바 클래스와 유사)

변경 가능한 값

  • 객체는 mutable value이다.
  • 원시값을 할당한 변수가 기억하는 메모리 주소를 통해 메모리 공간에 가면 원시값을 접근할 수 있다.
  • 원시값을 할당한 변수는 원시값 자체를 값으로 갖는다.
  • but 객체를 할당한 변수가 기억하는 메모리 주소를 통해 메모리 공간에 접근하면 참조값에 접근할 수 있다.
  • 참조값은 생성된 객체가 저장된 메모리 공간의 주소, 그 자체다.
  • 객체를 할당한 변수에는, 생성된 객체가 실제로 저장된 메모리 공간의 주소가 저장되어 있다. (참조값)
  • 객체는 할당한 변수를 참조하면 메모리에 저장되어 있는 참조값을 통해 실제 객체에 접근한다.
  • 변수는 객체를 참조하고 있다. 변수는 객체를 가리키고 있다.
  • 변수는 mutable한 값이다.
  • 객체를 할당한 변수는 재할당 없이 객체를 직접 변경 할 수 있다.
  • 즉, 재할당 없이 프로퍼티를 동적으로 추가할 수도 있고, 값을 갱신할 수도있으며, 프로퍼티 자체를 삭제할 수 도 있다.
  • 객체는 mutable한 값이므로 메모리에 저장된 객체를 직접 수정할 수 있다.
  • 이때 객체에 할당한 변수에 재할당을 하지 않았으므로 객체를 할당한 변수의 참조값은 변경되지 않는다.

  • 객체를 생성하고 관리하는 방식은 복잡하고 비용 많이 든다.
  • 객체를 변경할때마다 원시값처럼 immutable하게 하면 신뢰성은 확보되지만 생성 비용이 크다. (메모리의 효율적 소비가 어렵고 성능이 나빠진다)
  • 따라서, 메모리를 효율적으로 사용하기위해, 객체를 복사해 생성하는 비용을 절약하여 성능을 향상시키기 위해, 객체는 mutable한 값으로 설계.
  • 부작용 : 여러 개의 식별자가 하나의 객체를 공유할 수 있다.

얕은 복사 객체를 프로퍼티 값으로 갖는 객체의 경우, 얕은 복사는 한 단계까지만 복사한다 깊은 복사 객체에 중첩되어 있는 객체까지 모두 복사

const o = { x: { y: 1 } };

// 얕은 복사
const c1 = { ...o };
console.log(c1 === o); // false
console.log(c1.x === o.x); //true

const lodash = require("lodash");
// 깊은 복사
const c2 = lodash.cloneDeep(o);
console.log(c2 === o); //false
console.log(c2.x === o.x); //false
  • 얕은 복사, 깊은 복사로 생성된 객체는 원본과 다르다.
  • 원본과 복사본은 참조 값이 다른 별개의 객체
  • 얕은 복사는 객체에 중첩되어 있는 객체의 경우, 참조 값을 복사
  • 깊은 복사는 중첩되어 있는 객체까지 모두 복사하여 원시값처럼 완전한 복사본
const v = 1;
// 원시값을 할당한 변수를 다른 변수에 할당하는 것 : 깊은 복사
const c1 = v;
console.log(c1 === v); // true

const o = { x: 1 };
// 객체를 할당한 변수를 다른 변수에 할당하는 것 : 얕은 복사
const c2 = o;
console.log(c2 === 0);

참조에 의한 전달

  • 객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조 값이 복사되어 전달된다. (참조에 의한 전달)
var person = {
  name: "jo",
};

var copy = person;
  • 이때 person과 copy에 저장된 메모리 주소는 다르지만 동일한 참조값을 갖는다.
  • 모두 동일한 객체를 가리킨다. === 두 개의 식별자가 하나의 객체를 공유
  • 따라서, 원본 또는 사본중 어느 한쪽에서 객체를 변경(재할당x)하면 서로 영향을 주고받는다
var person = {
  name: "jo",
};

var copy = person;
copy.age = 25;

console.log(person.age); //25
  • 결국 값에 의한 전달, 참조에 의한 전달은 식별자가 기억하는 메모리 공간에 저장되어 있는 값을 복사해서 전달한다는 것은 동일.
  • 식별자가 기억하는 메모리 공간이 원시값이냐, 참조값이냐의 차이
  • 따라서 js는 참조에 의한 전달은 존재하지 않고 값에 의한 전달만 존재한다고 말할 수 있다.
  • 이러한 동작 방식을 설명하는 정확한 용어는 ECMAScript에 없다.
var person = {
  name: "jo",
};

var person2 = {
  name: "jo",
};

console.log(person === person2); //false
console.log(person.name === person2.name); //true

// === 일치 비교 연산자 : 변수에 저장되어 있는 값을 타입 변환하지 않고 비교
// 객체에 할당한 변수는 참조값을 가진다. (참조값 비교)
// 객체 리터럴은 평가될 때마다 객체를 생성한다.
// 따라서 person과 person2가 가리키는 객체는 내용은 같지만 다른 메모리에 저장된 별개의 객체다.

// 프로퍼티 값을 참조하는 person1.name은 값으로 평가 될 수 있는 표현식
// 원시값 "Jo"로 평가된다🤫🤫🤫🤫🤫
console.log([{"name":"jorang"}].includes({"name":"jorang"}));