객체와 프리미티브의 근본적인 차이점 중 하나는 객체가 "참조에 의해" 저장되고 복사되는 반면, 프리미티브 값(문자열, 숫자, 부울 등)은 항상 "전체 값으로" 복사된다는 것입니다.
값을 복사할 때 무슨 일이 일어나는지 좀 더 자세히 살펴보면 이해하기 쉽습니다.
문자열과 같은 기본 요소부터 시작해 보겠습니다.
여기서는 message
사본을 phrase
에 넣습니다.
메시지 = "안녕하세요!"; 문구 = 메시지를 보자;
결과적으로 우리는 "Hello!"
라는 문자열을 각각 저장하는 두 개의 독립 변수를 갖게 됩니다. .
너무나 당연한 결과죠?
객체는 그렇지 않습니다.
객체에 할당된 변수는 객체 자체가 아니라 "메모리의 주소", 즉 객체에 대한 "참조"를 저장합니다.
이러한 변수의 예를 살펴보겠습니다.
사용자 = { 이름: "존" };
실제로 메모리에 저장되는 방법은 다음과 같습니다.
개체는 메모리(그림 오른쪽) 어딘가에 저장되는 반면 user
변수(왼쪽)에는 이에 대한 "참조"가 있습니다.
user
와 같은 객체 변수는 객체의 주소가 적힌 종이처럼 생각할 수 있습니다.
객체에 대해 작업을 수행할 때(예: user.name
속성 가져오기) JavaScript 엔진은 해당 주소에 있는 내용을 살펴보고 실제 객체에 대한 작업을 수행합니다.
이제 그것이 중요한 이유가 여기에 있습니다.
개체 변수를 복사하면 참조는 복사되지만 개체 자체는 복제되지 않습니다.
예를 들어:
사용자 = { 이름: "John" }; 관리자 = 사용자로 두십시오. // 참조 복사
이제 두 개의 변수가 있는데, 각각은 동일한 객체에 대한 참조를 저장합니다.
보시다시피, 여전히 하나의 개체가 있지만 이제는 이를 참조하는 두 개의 변수가 있습니다.
두 변수 중 하나를 사용하여 객체에 액세스하고 해당 내용을 수정할 수 있습니다.
사용자 = { 이름: 'John' }; 관리자 = 사용자로 두십시오. admin.name = '피트'; // "admin" 참조에 의해 변경됨 경고(사용자.이름); // 'Pete', "user" 참조에서 변경 사항이 표시됩니다.
마치 두 개의 키가 있는 캐비닛이 있고 그 중 하나( admin
)를 사용하여 캐비닛에 들어가서 변경하는 것과 같습니다. 그런 다음 나중에 다른 키( user
)를 사용하면 여전히 동일한 캐비닛을 열고 변경된 내용에 액세스할 수 있습니다.
두 객체는 동일한 객체인 경우에만 동일합니다.
예를 들어, 여기서 a
와 b
동일한 객체를 참조하므로 동일합니다.
a = {}; b = a라고 하자; // 참조 복사 경고( a == b ); // true, 두 변수 모두 동일한 객체를 참조합니다. 경고( a === b ); // 진실
그리고 여기서 두 개의 독립적인 개체는 비록 비슷해 보이지만 동일하지 않습니다(둘 다 비어 있음).
a = {}로 놔두세요; b = {}로 둡니다. // 두 개의 독립 객체 경고( a == b ); // 거짓
obj1 > obj2
와 같은 비교 또는 기본 obj == 5
에 대한 비교의 경우 객체는 기본 형식으로 변환됩니다. 객체 변환이 어떻게 작동하는지 곧 연구하겠지만, 사실 이러한 비교는 거의 필요하지 않습니다. 일반적으로 이러한 비교는 프로그래밍 실수의 결과로 나타납니다.
Const 개체를 수정할 수 있습니다.
객체를 참조로 저장할 때 발생하는 중요한 부작용은 const
로 선언된 객체를 수정할 수 있다는 것입니다.
예를 들어:
const 사용자 = { 이름: "존" }; user.name = "피트"; // (*) 경고(사용자.이름); // 피트
(*)
행이 오류를 일으키는 것처럼 보일 수도 있지만 그렇지 않습니다. user
값은 일정하며 항상 동일한 개체를 참조해야 하지만 해당 개체의 속성은 자유롭게 변경할 수 있습니다.
즉, const user
user=...
전체적으로 설정하려고 하는 경우에만 오류를 발생시킵니다.
즉, 상수 개체 속성을 만들어야 하는 경우에도 가능하지만 완전히 다른 방법을 사용합니다. 이에 대해서는 속성 플래그 및 설명자 장에서 언급하겠습니다.
따라서 개체 변수를 복사하면 동일한 개체에 대한 참조가 하나 더 생성됩니다.
하지만 객체를 복제해야 한다면 어떻게 될까요?
우리는 새 객체를 생성하고 해당 속성을 반복하고 기본 수준에서 복사하여 기존 객체의 구조를 복제할 수 있습니다.
이와 같이:
사용자 = { 이름: "존", 나이: 30 }; 복제 = {}; // 새로운 빈 객체 // 모든 사용자 속성을 여기에 복사해 보겠습니다. for (사용자에게 키 입력) { 클론[키] = 사용자[키]; } // 이제 복제본은 동일한 내용을 가진 완전히 독립된 개체입니다. clone.name = "피트"; // 그 안의 데이터를 변경했습니다. 경고(사용자.이름); // 여전히 원래 객체에 John이 있습니다.
Object.ass 메소드를 사용할 수도 있습니다.
구문은 다음과 같습니다.
객체.할당(dest, ...소스)
첫 번째 인수 dest
는 대상 객체입니다.
추가 인수는 소스 개체 목록입니다.
모든 소스 객체의 속성을 대상 dest
에 복사한 다음 이를 결과로 반환합니다.
예를 들어 user
개체가 있는데 여기에 몇 가지 권한을 추가해 보겠습니다.
사용자 = { 이름: "John" }; 허용 권한1 = { canView: true }; letPermissions2 = { canEdit: true }; //Permission1 및Permission2의 모든 속성을 사용자에게 복사합니다. 객체.할당(사용자, 권한1, 권한2); // 이제 user = { name: "John", canView: true, canEdit: true } 경고(사용자.이름); // 존 경고(user.canView); // 진실 경고(user.canEdit); // 진실
복사한 속성 이름이 이미 존재하는 경우 덮어쓰게 됩니다.
사용자 = { 이름: "John" }; Object.sign(사용자, { 이름: "피트" }); 경고(사용자.이름); // 이제 user = { 이름: "Pete" }
또한 Object.assign
사용하여 간단한 객체 복제를 수행할 수도 있습니다.
사용자 = { 이름: "존", 나이: 30 }; 복제 = Object.할당({}, 사용자); 경고(클론.이름); // 존 경고(clone.age); // 30
여기서는 user
의 모든 속성을 빈 개체에 복사하고 반환합니다.
객체를 복제하는 다른 방법도 있습니다. 예를 들어 스프레드 구문 clone = {...user}
사용하는 방법은 튜토리얼의 뒷부분에서 다룹니다.
지금까지 우리는 user
의 모든 속성이 원시적이라고 가정했습니다. 그러나 속성은 다른 개체에 대한 참조일 수 있습니다.
이와 같이:
사용자 = { 이름: "존", 크기: { 키: 182, 폭: 50 } }; 경고( user.sizes.height ); // 182
이제 clone.sizes = user.sizes
복사하는 것만으로는 충분하지 않습니다. user.sizes
는 객체이고 참조로 복사되므로 clone
과 user
동일한 크기를 공유하게 됩니다.
사용자 = { 이름: "존", 크기: { 키: 182, 폭: 50 } }; 복제 = Object.할당({}, 사용자); 경고( user.sizes === clone.sizes ); // true, 동일한 객체 // 사용자 및 클론 공유 크기 user.sizes.width = 60; // 한 곳에서 속성을 변경합니다. 경고(clone.sizes.width); // 60, 다른 것에서 결과를 얻습니다.
이 문제를 해결하고 user
와 clone
완전히 분리된 개체를 만들려면 user[key]
의 각 값을 검사하는 복제 루프를 사용해야 하며, 개체인 경우 해당 구조도 복제해야 합니다. 이를 "심층 복제" 또는 "구조적 복제"라고 합니다. 심층 복제를 구현하는 StructuredClone 메서드가 있습니다.
structuredClone(object)
호출은 모든 중첩 속성을 사용하여 object
복제합니다.
예제에서 이를 사용하는 방법은 다음과 같습니다.
사용자 = { 이름: "존", 크기: { 키: 182, 폭: 50 } }; 복제 = 구조화Clone(사용자); 경고( user.sizes === clone.sizes ); // 거짓, 다른 객체 // 사용자와 클론은 이제 전혀 관련이 없습니다. user.sizes.width = 60; // 한 곳에서 속성을 변경합니다. 경고(clone.sizes.width); // 50, 관련 없음
structuredClone
메소드는 객체, 배열, 기본 값과 같은 대부분의 데이터 유형을 복제할 수 있습니다.
또한 객체 속성이 객체 자체를 참조하는 경우(직접 또는 체인이나 참조를 통해) 순환 참조를 지원합니다.
예를 들어:
사용자 = {}로 두십시오; // 순환 참조를 생성해 보겠습니다. // user.me는 사용자 자신을 참조합니다. user.me = 사용자; 복제 = 구조화Clone(사용자); 경고(clone.me === 복제); // 진실
보시다시피 clone.me
user
아닌 clone
참조합니다! 따라서 순환 참조도 올바르게 복제되었습니다.
그러나 structuredClone
실패하는 경우가 있습니다.
예를 들어, 객체에 함수 속성이 있는 경우:
// 오류 구조화된클론({ f: 함수() {} });
함수 속성은 지원되지 않습니다.
이러한 복잡한 경우를 처리하려면 복제 방법의 조합을 사용하거나, 사용자 정의 코드를 작성하거나, 바퀴를 재발명하지 않으려면 JavaScript 라이브러리 lodash의 _.cloneDeep(obj)와 같은 기존 구현을 사용해야 할 수도 있습니다.
객체는 참조에 의해 할당되고 복사됩니다. 즉, 변수는 "객체 값"이 아니라 해당 값에 대한 "참조"(메모리 내 주소)를 저장합니다. 따라서 이러한 변수를 복사하거나 함수 인수로 전달하면 객체 자체가 아닌 해당 참조가 복사됩니다.
복사된 참조를 통한 모든 작업(예: 속성 추가/제거)은 동일한 단일 개체에서 수행됩니다.
"실제 복사본"(복제본)을 만들기 위해 소위 "얕은 복사본"(중첩된 객체는 참조에 의해 복사됨) 또는 "심층 복제" 함수인 structuredClone
Object.assign
사용하거나 다음과 같은 사용자 정의 복제 구현을 사용할 수 있습니다. _.cloneDeep(obj)으로.