자바스크립트의 객체

📚프런트엔드 레벨을 높이는 자바스크립트 퀴즈북📚 | Chapter02. 객체

객체(Object)

0개 이상의 속성으로 구성된 집합을 의미함.

자바스크립트는 객체 기반의 프로그래밍 언어이며, 원시 타입을 제외한 모든 것이 객체 타입에 해당함.

  • 원시 타입: 단 하나의 값만 나타내며, 변경 불가능한(immutable) 값

  • 객체 타입: 다양한 타입의 값(원시 값 또는 다른 객체)을 하나의 단위로 구성한 복합적인 자료구조이며, 변경 가능한(mutable) 값

원시타입과 메모리 저장방식을 비교했을 때, 원시값은 실제 값이 스택(Stack)에 저장되지만
객체는 실제 데이터가 힙(Heap)에 저장되고 스택에는 참조값(메모리 주소)만 저장되므로 객체를 직접 수정하더라도 힙의 데이터만 변경될 뿐, 스택 주소값은 변경되지 않기 때문

const a = { x: 1, mthd() {console.log(x)}}; 
const b = { x: 1, mthd() {console.log(x)}}; 

console.log(a === b);
  • 프로퍼티(Property)

    객체의 상태를 나타내는 값(data)을 의미하며, 키(key)와 값(value)로 구성됨.

  • 메서드(Method)

    프로퍼티를 참조하고 조작할 수 있는 동작(behavior)

    자바스크립트의 함수는 일급 객체임.
    따라서 프로퍼티의 값이 함수일 경우, 이를 일반 함수와 구별하기 위해 메서드라고 부름.

객체의 생성

자바스크립트에서 객체를 생성할 수 있는 방식은 객체 리터럴생성자 함수가 있음.

  • 객체 리터럴: 키와 값을 콤마(,)와 중괄호({})를 사용하여 객체를 생성하는 직관적 방식
  • 생성자 함수: new Object()를 호출하여 빈 객체를 생성하는 방식
// 1. 빈 객체 생성 (일반적)
const obj1 = {}; // 객체 리터럴
const obj2 = new Object(); // 생성자 함수

// 2. 인수가 null/undefined인 경우: 빈 객체 {} 반환
const obj3 = new Object(null); 
const obj4 = new Object(undefined); 

// 3. 인수가 원시 타입인 경우: 해당 타입을 래핑한 '객체' 반환
const obj5 = new Object(1);    // Number 객체
const obj6 = new Object("hi"); // String 객체

자바스크립트는 객체기반 언어이기 때문에 원시값조차 Object생성자를 사용하면 객체로 변환함.
하지만 이는 메모리 효율이나 직관성 면에서 좋지 않아, 실제로는 원시타입 그대로를 사용하는 것이 일반적임.

연관 배열(Associative Array)로서의 객체

자바스크립트 객체는 키(Key)와 값(Value)의 쌍으로 구성되어, 타 언어의 연관 배열 또는 해시 맵(Hash Map)처럼 동작함.

  • 동적 프로퍼티 접근

    대괄호 표기법(obj['key'])을 사용하면 프로퍼티 키를 동적으로 결정할 수 있음.

  • 유연성

    정해진 구조 없이 실행 중에 필요한 데이터를 키-값 형태로 마음껏 저장할 수 있음.

객체를 단순히 속성들의 묶음으로만 보지 말고, 키와 값으로 구성된 저장소의 형태로도 볼 수 있는데, 특히 대괄호 표기법을 사용하면 런타임에 키를 동적으로 만들 수 있어서 유연성 면에서의 장점이 있음.

const dictionary = {};
const userInput = "toString"; // "", "valueOf" 도 마찬가지

if (userInput in dictionary) {
	console.log("입력한 값이 존재합니다"); // 실행됨
} else {
	console.log("입력한 값이 존재하지 않습니다");
}

위의 코드에서 “입력된 값이 존재합니다”가 출력되는 이유는 프로토타입 체인(Prototype Chain)때문임.

  • 기본 상속

    모든 객체는 기본적으로 최상위 객체인 Object.prototype을 상속 받는데, 해당 프로토 타입에는 toString, hasOwnProperty, valueOf와 같은 기본 메서드가 저장되어 있음.

  • in연산자

    in 연산자는 직접 소유한 프로퍼티 뿐만 아니라, 상속받은 프로토타입의 프로퍼티까지 모두 확인함.

객체의 프로퍼티(Property)

자바스크립트에서 객체의 속성에 접근하기 위해서는 점 표기법대괄호 표기법을 사용하는데,

점 표기법을 사용하는 경우 속성명이 일반적인 자바스크립트 식별자 형태이어야 함.

  • 객체 축약 문법: 스코프의 이미 존재하는 변수를 복사하여 사용하는 문법 규칙
  • 계산된 속성명 (Computed Property): 객체 리터럴 속성명에 임의의 표현식을 넣어 정의할 수 있는 기능
Q. 자바스크립트에서 계산된 속성명을 활용할 수 있는 방법

💡 1. 동적 폼(Form) 핸들링

  1. API 응답 데이터 가공

  2. Enum 또는 상수를 이용한 객체 구성

const name = "Jake";
const job = "developer";

// 객체 축약문법
const person1 = {
	name, job
}
console.log(person1); // {name: "Jake", job: "developer"}

// 구조분해 할당
const {n, j} = person
console.log(n) // "Jake"
console.log(j) // "developer"

// 계산된 속성명
const person2 = {
	[name]: name
}
console.log(person2) // {"Jake": "Jake"}

속성 서술자

  • 데이터 서술자: 일반적으로 값을 저장하는 서술자

    • value: 속성의 실제 데이터 값
    • writable: false일 경우 값을 변경할수 없는 “읽기 전용” 상태가 됨.
    • enumerable: false일 경우, for…in 이나 Object.keys등에서 탐색되지 않음.
    const dictionary = {};
    const userInput = "toString"; 
    
    if (userInput in dictionary) {
    	console.log("입력한 값이 존재합니다"); // 실행됨
    } else {
    	console.log("입력한 값이 존재하지 않습니다");
    }
    
    for (key in dictionary) {
        console.log(key) // 실행 안 됨
    }
    
    • configurable: false일 경우, 속성 삭제나 서술자 설정 변경이 금지됨.

    객체 리터럴이나 생성자 함수로 생성한 객체는 writable, enumerable, configurable 속성의 디폴트 값이 true이지만,
    defineProperty로 정의한 속성은 해당 속성들이 false로 생성됨.

    // 1. 객체 리터럴 방식
    const literalObj = { name: "Jake" };
    console.log(Object.getOwnPropertyDescriptor(literalObj, "name"));
    // 결과: { value: "Jake", writable: true, enumerable: true, configurable: true }
    
    // 2. 생성자 함수 방식 (사용자 정의)
    function Person(name) {
      this.name = name;
    }
    const constructorObj = new Person("Finn");
    console.log(Object.getOwnPropertyDescriptor(constructorObj, "name"));
    // 결과: { value: "Finn", writable: true, enumerable: true, configurable: true }
    
    // 3. Object.defineProperty 방식 (디폴트가 false가 되는 경우)
    const defineObj = {};
    Object.defineProperty(defineObj, "name", {
      value: "BMO"
      // writable, enumerable, configurable을 생략함
    });
    
    console.log("--- Object.defineProperty ---");
    console.log(Object.getOwnPropertyDescriptor(defineObj, "name"));
    // 결과: { value: "BMO", writable: false, enumerable: false, configurable: false }
    
  • 접근자 서술자: 데이터를 직접 저장하지 않고, 읽기(get)와 쓰기(set)시점에 특정 함수를 실행함.

    • get: 속성에 접근할 때 호출되어 값을 반환하는 함수
    • set: 속성에 값을 할당할 때 호출되어 특정 로직을 수행하는 함수

속성 탐색

const person = {
  name: '철수',
  age: 25,
  city: '서울'
};
  • Object.keys(obj): 객체 본인이 소유한 속성 중 Symbol을 제외한 모든 키를 배열로 반환

    Object.keys(person);
    // ['name', 'age', 'city']
    
  • Object.values(obj) : 객체 속성의 값들만 모아서 배열로 반환

    Object.values(person);
    // ['철수', 25, '서울']
    
  • Object.entries(obj): 객체의 [키, 값] 쌍을 담은 이차원 배열을 반환

    Object.entries(person);
    // [['name', '철수'], ['age', 25], ['city', '서울']]
    
  • for...in 루프: 객체의 속성을 순회할 때 사용하지만, 상속된 열거 가능한(enumerable) 속성까지 포함하여 순회한다는 점에 유의해야 합니다.

    function Animal(type) {
      this.type = type;
    }
    Animal.prototype.sound = '...'; // 상속된 속성
    
    const dog = new Animal('강아지');
    dog.name = '멍멍이';
    
    for (const key in dog) {
      console.log(key); // 'type', 'name', 'sound' ← 상속된 sound까지 출력됨!
    }
    
    // 본인 소유 속성만 걸러내려면 hasOwnProperty 사용
    for (const key in dog) {
      if (dog.hasOwnProperty(key)) {
        console.log(key); // 'type', 'name'만 출력
      }
    }
    
  • Object.getOwnPropertyNames(obj): enumerable이 false인 속성(열거 불가능한 속성)을 포함하여 객체 본인의 모든 속성 이름을 반환합니다.

    const obj = {};
    Object.defineProperty(obj, 'hidden', {
      value: 42,
      enumerable: false  // 열거 불가능하게 설정
    });
    obj.visible = 'hello';
    
    Object.keys(obj);                    // ['visible']       ← hidden 안 보임
    Object.getOwnPropertyNames(obj);     // ['visible', 'hidden'] ← hidden도 보임
    
  • Object.getOwnPropertySymbols(obj): 객체에 정의된 Symbol 타입의 속성들만 반환합니다.

    const id = Symbol('id');
    const user = {
      name: '영희',
      [id]: 1004  // Symbol 키
    };
    
    Object.keys(user);                      // ['name']  ← Symbol 안 보임
    Object.getOwnPropertySymbols(user);     // [Symbol(id)]  ← Symbol만 반환
    

객체의 참조와 무력성 관리

객체타입은 원시타입과 달리 메모리 공간의 주소를 저장하는 참조타입임. 따라서 함수에 객체를 인자로 전달하면 실제 값이 복사되는 것 이 아니라 참조값(주소)이 전달됨.

이는 의도하지 않은 속성 수정이나 삭제로 인한 버그 발생 가능성을 높이고, 코드의 안정성을 떨어뜨림.

이를 방지하기 위한 수단이 얕은 복사, 깊은 복사, freeze 같은 것들

얕은 복사(Shallow Copy)

스프레드 연산자() 혹은 Object.assign()을 사용하여 객체의 1단계 깊이까지만 새로운 객체로 복사함.

또한, 객체 내부에 또 다른 객체(중첩 객체)가 있을 경우, 내부 객체는 여전히 원본과 참조 주소를 공유함. 따라서 복사한 객체의 내부 속성을 변경하면, 원본 객체에도 영향을 미침.

const user = {
	name: "홍길동",
	address: {
		country: "조선",
		city: "장성군",
	}
};

const clone = {...user};
clone.name = "전우치"; // 1depth
clone.address.country = "고려";

console.log(user.name); // 변경 안 됨
console.log(user.address.country) // 변경됨

깊은 복사 (Deep Copy)

재귀적 참조 혹은 JSON 포맷으로의 직렬화, structuredClone()을 사용하여 깊은 복사가 가능함.

다만, 함수나 심볼 같이 JSON 포맷으로 직렬화되지 않는 속성(Function, Symbol, undefined)이 객체에 포함된 경우에는, 복사 과정에서 데이터가 누락되며 Date객체는 문자열로 변환됨. 또한 순환 참조가 있는 경우도 깊은 복사가 불가함.

따라서 이를 해결하기 위해서는 structuredClone()을 사용하는 것이 권장됨.

bookmark

// 값과 스스로를 순환 참조하는 객체 생성
const original = { name: "MDN" };
original.itself = original;

// 복제
const clone = structuredClone(original);

console.assert(clone !== original); // 동일하지 않은 객체 (같지 않은 동일성)
console.assert(clone.name === "MDN"); // 같은 값을 가집니다.
console.assert(clone.itself === clone); // 순환 참조가 보존됩니다.

무결성 관리

객체의 변경 가능성(Mutability)을 제어하여 데이터의 신뢰성을 높이는 방법으로,

속성 서술자(Property Descriptor) 설정을 일괄적으로 적용하는 것과 유사함.

const user = { name: "Jake" };

// 속성 서술자로 무결성 관리
Object.defineProperty(user, "name", {
    writable: false,
    configurable: false
});

// 정적 메소드로 무결성 관리
Object.freeze(user);
  • Object.preventExtensions

    객체에 속성이 추가되지 못하도록 막음. 이미 존재하는 속성은 수정, 삭제 가능함.

  • Object.seal

    = configurable: false

    객체에 속성이 추가되거나 삭제하지 못하도록 막음. 이미 존재하는 속성은 수정 가능함.

  • Object.freeze

    = configurable: false, writable: false

    객의 추가, 수정, 삭제를 모두 막음. 객체의 속성이 또 다른 객체를 참조하고 있는 경우, 해당 객체의 변경까지는 막지 못함. (Shallow Freeze)

Object.freeze 도 참조된 속성의 무결성은 보장하지 못하므로

완벽한 불변성을 원한다면 재귀적으로 모든 레벨의 객체를 동결하거나, 외부 라이브러리(Immutable.js, Immer.js 등)를 고려해야 함.

객체의 순회와 심화구조

이터러블 프로토콜 (Iterable Protocol)

객체가 for...of 루프, 스프레드 연산자, 구조 분해 할당 등과 같은 순회 기능을 사용할 수 있도록 규정된 규칙

  • 이터러블(Iterable)

    이터러블 프로토콜을 준수하는 객체, 반복 순회가 가능함.

    내부에 Symbol.iterator 를 키로 가진 메서드가 반드시 존재해야 함.

이터레이터 프로토콜 (Iterator Protocol)

객체가 반복 과정에서 다음 값을 반환하는 방식과 반복이 종료되었는지의 여부를 정의하는 규칙

  • 이터레이터 (Iterator)

    이터레이터 프로토콜을 준수하는 객체 = 포인터, 내부 상태를 공유하므로 반복 순회 불가

    next() 메서드를 가져야 하며, 호출 시 { value, done } 객체를 반환해야 함.

const myIterator = {
	count: 0,
	next(): {
		if (++this.count > 2) {
			return {done: true};
		} else {
			return {done: false, value: this.count}
		}
	}
}

console.log(myIterator.next()) // {done: false, value: 1}
console.log(myIterator.next()) // {done: false, value: 2}
console.log(myIterator.next()) // {done: true}

이터러블 이터레이터

이터레이터이면서 이터러블인 객체

순회 도중 멈춘 위치에서 다시 순환을 시작하거나, 이터레이터 자체를 for...of에 다시 넣기 위함임.

ex- 배열의 이터레이터, 제너레이터

next() 메서드를 가지면서, Symbol.iterator 메서드도 가져야 합니다. 이때 Symbol.iterator는 **자기 자신(this)**을 반환해야 함

const myIterator = {
	count: 0,
	next(): {
		if (++this.count > 2) {
			return {done: true};
		} else {
			return {done: false, value: this.count}
		}
	},
	[Symbol.iterator]() {
		return this;
	}
}

// 1. 전개 연산자로 이터러블 이터레이터 호출
console.log([...myIterator]) // [1, 2]
console.log([...myIterator]) // [], 내부 상태를 공유하므로, 한 번 순회하면 더 이상 value를 뱉어내지 않음
console.log(myIterator.next()) // {done: true}

// 2. for...of로 이터러블 이터레이터 호출
for (const v of myIterator) {
  console.log(v);
  
  if (v === 1) break;
}

전개 연산자()와 for…ofdone을 반복 제어용으로만 사용하고 done === false 일 때의 value만 순회 결과로 취급함.

제너레이터 (Generator)

함수의 실행을 중간에 멈췄다가 필요한 시점에 다시 재개할 수 있는 함수

function* 키워드를 사용하며 이터러블이자 이터레이터인 제너레이터 객체를 반환함.

함수가 종료되면 반복도 종료되며

function* generator() {
	for (let i = 1; i <= 2; i++) {
		yield i;
	}
}

const myIterator = generator();

// 1. 전개 연산자로 제너레이터 호출
console.log([...myIterator]); // [1, 2]
console.log([...myIterator]); // []

// 2. next()로 제너레이터 호출
console.log(myIterator.next().value) // 1
console.log(myIterator.next()) // {done: false, value: 2}
console.log(myIterator.next()) // {done: true, value: undefined}

Map

키와 값의 쌍을 저장하는 자료구조로, 일반 객체보다 더 넓은 범위의 키를 사용할 수 있음.

  • 문자열과 심볼뿐만 아니라 숫자, 객체, 함수 등 모든 타입을 키로 사용 가능함.
  • 데이터의 빈번한 추가 및 제거에 최적화된 성능을 제공함.
  • size 프로퍼티를 통해 요소의 개수를 쉽게 파악할 수 있으며, 이터러블 프로토콜을 준수하여 순회가 편리함.
const myMap = new Map();
myMap.set('color', 'red');

// ❌ 주의: 일반 객체처럼 접근하면 undefined (Map의 기능을 쓰지 못함)
console.log(myMap.color); // undefined
console.log(myMap['color']) // undefined

// ✅ 권장: get 메서드 사용
console.log(myMap.get('color')); // "red"

// ⚠️ 참조 값 주의: 모양이 같다고 같은 키가 아님
myMap.set({}, 'empty object');
console.log(myMap.get({})); // undefined (새로운 객체{}는 주소값이 다르기 때문)

Set

중복되지 않는 유일한 값들의 집합을 다루는 자료구조

  • 수학의 집합과 유사하며, 중복된 값을 자동으로 제거하므로 유일한 값만 포함해야 하는 데이터 관리에 적합gka
  • add, has, delete 등의 메서드를 통해 값을 관리하며, 교집합(intersection), 합집합(union) 등 다양한 집합 연산을 지원함.
const mySet = new Set([1, 2, 3]);

// ❌ 주의: 배열이 아니므로 인덱스 접근 불가
console.log(mySet[0]); // undefined

// ✅ 권장: has로 확인하거나 배열로 변환 후 접근
console.log(mySet.has(1)); // true
const arr = [...mySet]; // 배열로 변환
console.log(arr[0]); // 1

// ⚠️ 중복 주의: 객체는 주소가 다르면 중복으로 보지 않음
mySet.add({a: 1});
mySet.add({a: 1}); // 에러 없이 추가됨 (두 객체의 참조가 다르기 때문)
console.log(mySet.size); // 5 (1, 2, 3, 객체1, 객체2)

WeakMap & WeakSet

객체에 대한 약한 참조를 유지하는 특수한 자료구조

  • 메모리 누수 방지: 키로 사용된 객체가 외부에서 더 이상 참조되지 않으면, 가비지 컬렉션(GC)에 의해 메모리에서 자동으로 제거됨.
*** **가비지 컬렉션 (Garbage Collection)**

자바스크립트 엔진이 _더 이상 사용되지 않는 객체를 식별하여 메모리 공간을 자동으로 확보하는 과정_으로 개발자가 명시적으로 메모리를 해제할 필요 없이 자바스크립트 엔진이 백그라운드에서 주기적으로 수행함.

자바스크립트는 **도달가능성(Reachability)**라는 개념을 통해 가비지 컬렉션을 수행하는데, 도달가능한 값이란 _어떻게든 접근하거나 사용할 수 있는 값_을 의미함.

  • 도달가능성(Reachability)

    • 전역 변수: window(브라우저) 또는 global(Node.js) 객체에 선언된 변수
    • 현재 함수의 지역 변수와 매개변수: 현재 실행 중인 함수 내의 데이터
    • 체인 상의 변수들: 현재 실행 중인 함수를 호출한 상위 함수들의 변수

    가 도달가능성의 기준이 되며 이들은 **루트(Root)**라고 불림

  • 도달가능성의 판단 과정

    1. Mark(마킹): 가비지 컬렉터가 루트(Root) 정보들을 수집하고 이들을 '마크'함.
    2. 탐색: 루트가 참조하고 있는 객체로 이동하여 그 객체들을 마크합니다. 마크된 객체가 또 다른 객체를 참조하고 있다면 그 객체도 따라가서 마크함.
    3. Sweep (삭제): 루트로부터 시작해서 방문할 수 있는 모든 객체를 마크한 후, 마크되지 않은 모든 객체를 메모리에서 삭제함.
*** **약한 참조 (Weak Reference)** - **WeakMap/WeakSet 사용**: `let ws = new WeakSet(); ws.add(obj);` - **특징**: `WeakMap`이나 `WeakSet`은 도달 가능성을 판단하는 **참조 체인에 포함되지 않음.** - **결과**: 외부의 강한 참조(`obj = null;`)가 끊어지면, `WeakSet`에 데이터가 남아있더라도 가비지 컬렉터는 "루트에서 이 객체로 갈 수 있는 길이 없다"고 판단하여 해당 객체를 메모리에서 지움.
*** **강한 참조 (Strong Reference)** - **일반적인 객체 할당**: `let obj = { a: 1 };` - **특징**: 객체를 변수에 할당하거나 일반적인 `Map`, `Set`에 넣으면 강한 참조가 생성됨. - **결과**: 외부에서 `obj = null;`을 해도 `Map`이 그 객체를 들고 있다면, **루트 → Map → 객체**로 이어지는 경로가 살아있어 도달 가능하므로 삭제되지 않음.
  • 보안 및 효율성: 객체와 연관된 부가 정보를 저장하고 싶지만, 해당 객체가 사라질 때 부가 정보도 함께 정리되길 원하는 경우(예: DOM 요소 관련 데이터 저장)에 필수적임
  • 열거(Enumeration)가 불가능하여 보안이 필요한 비공개 데이터를 저장하는 데 유리함.
const ws = new WeakSet();
const wm = new WeakMap();

// ❌ 에러: 원시 타입(숫자, 문자열 등)은 키가 될 수 없음
ws.add(1); // TypeError: Invalid value used in weak set
wm.set("key", "value"); // TypeError: Invalid value used as weak map key

// ✅ 올바른 사용: 반드시 객체만 사용
let obj = { name: "Jake" };
ws.add(obj); 

// ❌ 주의: 순회가 불가능하므로 아래 코드들은 작동하지 않음
console.log(ws.size); // undefined
for (let i of ws) { ... } // TypeError: ws is not iterable
MapSetWeakMapWeakSet
저장 형태[Key, Value] 쌍값(Value)의 집합[Key, Value] 쌍값(Value)의 집합
키의 타입모든 타입 가능 (객체, 함수 등)-객체(Object)
중복 허용키 중복 불가값 중복 불가키 중복 불가값 중복 불가
순서 보장OOXX
참조 특성강한 참조강한 참조약한 참조약한 참조
가비지 컬렉션대상이 되지 않음대상이 되지 않음참조가 끊기면 자동으로 수거됨참조가 끊기면 자동으로 수거됨