11. 원시값과 객체의 비교
in 모던자바스크립트
원시타입 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 엔진을 구현하는 제조사에 따라 실제 내부 동작 방식은 다를 수 있다.
- 그 이유로 코어 자바스크립트에서 설명하는 방식과 이 개념이 다르다.
- 변수에 원시 값을 갖는 변수를 할당하는 시점에는 두 변수가 같은 원시 값을 참조하다가 어느 한쪽의 변수에 재할당이 이뤄졌을 때 비로소 새로운 메모리 공간에 재할당된 값을 저장하는 경우
- 파이썬이 코어 자바스크립트 내용처럼 동작한다.
- 값에 의한 전달 이라는 용어도 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"}));