JS 정리 #4

Hyun·2024년 4월 4일
0

20. 객체

8개의 자료형 중 7개는 오직 하나의 데이터 (문자열, 숫자 등만) 담을 수 있어서 원시형(primitive)이다.

객체형은 원시형과 달리 다양한 데이터를 담을 수 있다.

키로 구분된 데이터 집합이나 복잡한 개체(entity)를 저장.

  • 객체는 중괄호를 이용해 만들 수 있다.
  • 중괄호 안에는 '키(key)': '값(value)' 쌍으로 구성된 프로퍼티(property)를 여러 개 넣을 수 있음.
  • 키에는 문자형, 값에는 모든 자료형이 허용됨.
  • 프로퍼티 키 (프로퍼티 이름)

// 빈 객체를 만드는 방법
let user = new Object(); // '객체 생성자' 문법
let user = {};  // '객체 리터럴' 문법 : 중괄호를 이용해 객체 선언

20.1. 리터럴과 프로퍼티

  • 중괄호 안에는 '키: 값'으로 구성된 프로퍼티가 들어감.
  • '콜론(:)'을 기준으로 왼쪽엔 키가, 오른쪽엔 값이 위치.
  • 프로퍼티 키는 프로퍼티 '이름' 혹은 '식별자'라고 부름.
let user = {     // 객체
  name: "John",  // 키: "name",  값: "John"
  age: 30        // 키: "age", 값: 30
};

위 코드에서는 서랍장(객체 USER) 안에 파일 두 개 (프로퍼티 두 개)가 담겨져 있는데 각 파일에 "name", "age"라는 이름표가 붙어있다고 생각하면 된다.

서랍장에 파일을 추가하고 뺄 수 있듯이, 개발자는 프로퍼티를 추가, 삭제할 수 있다.

  1. 점 표기법(dot notation)을 이용하면 프로퍼티 값을 읽을 수 있다.
// 프로퍼티 값 얻기
alert( user.name ); // John
alert( user.age ); // 30

프로퍼티 값에는 모든 자료형이 올 수 있다.

user.isAdmin = true;

  1. delete 연산자를 사용하면 프로퍼티를 삭제할 수 있다.
delete user.age;

여러 단어를 조합해 프로퍼티 이름을 만들었을 때는 프로퍼티 이름을 따옴표로 묶어줘야 한다.

let user = {
  name: "John",
  age: 30,
  "likes birds": true  // 복수의 단어는 따옴표로 묶어야 합니다.
};

// 끝에 쉼표가 붙어있음
let user = {
  name: "John",
  age: 30,
}

마지막 프로퍼티 끝은 쉼표로 끝날 수 있다. 이런 쉼표를 ‘trailing(길게 늘어지는)’ 혹은 ‘hanging(매달리는)’ 쉼표라고 부른다.

이렇게 끝에 쉼표를 붙이면 모든 프로퍼티가 유사한 형태를 보이기 때문에 프로퍼티를 추가, 삭제, 이동하는 게 쉬워진다.

주의: 상수 객체는 수정될 수 있다.
const로 선언된 객체는 수정될 수 있다. const 는 user의 값을 고정하지만, 그 내용은 고정되지 않는다.

const user = {
  name: "John"
};

user.name = "Pete"; // (*)

alert(user.name); // Pete
// const는 user=...를 전체적으로 설정하려고 할 때만 오류가 발생한다.

20.2. 대괄호 표기법

여러 단어를 조합해 프로퍼티 키를 만든 경우에는, 점 표기법을 사용해 프로퍼티 값을 읽을 수 없다.

// 문법 에러가 발생합니다.
user.likes birds = true

점은 키가 '유효한 변수 식별자'인 경우에만 사용할 수 있다. 유효한 변수 식별자에는 공백이 없어야 하고, 숫자로 시작하지 않아야 하며, $와 _를 제외한 특수 문자가 없어야 한다.

키가 유효한 변수 식별자가 아닌 경우네는 점 표기법 대신 대괄호 표기법(squeare bracket notation)이라 불리는 방법을 사용할 수 있다.

어떤 문자열이 있던지 상관없이 동작한다.

let user = {};

// set
user["likes birds"] = true;

// get
alert(user["likes birds"]); // true

// delete
delete user["likes birds"];

이를 응용하면, 코드를 유연하게 작성할 수 있다.

let user = {
  name: "John",
  age: 30
};

let key = prompt("사용자의 어떤 정보를 얻고 싶으신가요?", "name");

// 변수로 접근
alert( user[key] ); // John (프롬프트 창에 "name"을 입력한 경우)

20.2.1. 계산된 프로퍼티(computed property)

계산된 프로퍼티: 객체를 만들 때 객체 리터럴 안의 프로퍼티 키가 대괄호로 둘러싸여 있는 경우.

let fruit = prompt("어떤 과일을 구매하시겠습니까?", "apple");

let bag = {
  [fruit]: 5, // 변수 fruit에서 프로퍼티 이름을 동적으로 받아 옵니다.
};

alert( bag.apple ); // fruit에 "apple"이 할당되었다면, 5가 출력됩니다.

[fruit]는 프로퍼티 이름을 변수 fruit에서 받아온다는 것을 의미한다.

let fruit = prompt("어떤 과일을 구매하시겠습니까?", "apple");
let bag = {};

// 변수 fruit을 사용해 프로퍼티 이름을 만들었습니다.
bag[fruit] = 5;

// 대괄호 안에는 복잡한 표현식이 올 수 있다.
let fruit = 'apple';
let bag = {
  [fruit + 'Computers']: 5 // bag.appleComputers = 5
};

위 예시로 동일하게 바꿔서 쓸 수도 있다. 계산된 프로퍼티를 사용한 예시가 더 깔끔해 보인다.

대괄호 표기법

  • 장점: 프로퍼티 이름과 값의 제약을 없애주기 때문에 점 표기법보다 훨씬 강력하다.
  • 단점: 작성하기 번거롭다.

프로퍼티 이름이 확정되거나 단순한 이름이라면 처음에는 점 표기법.
복잡한 상황이 발생했을 때 대괄호 표기법으로 바꾸는 경우가 많다.

20.2.2. 단축 프로퍼티

function makeUser(name, age) {
  return {
    name: name,
    age: age,
    // ...등등
  };
}

let user = makeUser("John", 30);
alert(user.name); // John

위 예시의 프로퍼티들은 이름과 값이 변수의 이름과 동일하다.
이렇게 변수를 사용해 프로퍼티를 만드는 경우는 흔하다.

프로퍼티 값 단축 구문(property value shorthand)을 사용하면 코드를 짧게 줄일 수 있다.

name:name 대신 name만 적어줘도 프로퍼티를 설정할 수 있다.

function makeUser(name, age) {
  return {
    name, // name: name 과 같음
    age,  // age: age 와 같음
    // ...
  };
}

한 객체에서 일반 프로퍼티와 단축 프로퍼티를 함께 사용하는 것도 가능하다.

let user = {
  name,  // name: name 과 같음
  age: 30
};

20.2.3. 프로퍼티 이름의 제약사항

변수 이름(키)에는 'for', 'let', 'return' 같은 예약어를 사용하면 안되지만,

객체 프로퍼티에는 이런 제약이 없다.

// 예약어를 키로 사용해도 괜찮습니다.
let obj = {
  for: 1,
  let: 2,
  return: 3
};

alert( obj.for + obj.let + obj.return );  // 6

이와 같이 프로퍼티 이름에는 특별한 제약이 없다.
어떤 문자형, 심볼형 값도 프로퍼티 키가 될 수 있다.

문자형이나 심볼형에 속하지 않은 값은 문자열로 자동 형 변환된다.

예를 들자면, 키에 숫자 0을 넣으면 문자열 "0"으로 자동변환된다.

let obj = {
  0: "test" // "0": "test"와 동일합니다.
};

// 숫자 0은 문자열 "0"으로 변환되기 때문에 두 얼럿 창은 같은 프로퍼티에 접근합니다,
alert( obj["0"] ); // test
alert( obj[0] ); // test (동일한 프로퍼티)

이와같이 객체 프로퍼티 키에 쓸 수 있는 문자열에는 제약이 없지만,

역사적인 이유 때문에 __proto__ 는 특별 대우를 받습니다.

let obj = {};
obj.__proto__ = 5; // 숫자를 할당합니다.
alert(obj.__proto__); // [object Object] - 숫자를 할당했지만 값은 객체가 되었습니다. 의도한대로 동작하지 않네요.

원시값 5를 할당했지만 무시된 것을 확인할 수 있다.

20.2.4. 'in' 연산자로 프로퍼티 존재 여부 확인하기

자바스크립트 객체의 중요한 특징:

  • 다른 언어와는 달리, 존재하지 않는 프로퍼티에 접근하려 해도 에러가 발생하지 않고 undefined 를 반환한다.

이런 특징을 프로퍼티 존재 여부를 쉽게 확인할 수 있다.

let user = {};

alert( user.noSuchProperty === undefined ); // true는 '프로퍼티가 존재하지 않음'을 의미합니다.

이렇게 undefined 와 비교하는 것 이외에도 연산자 in을 사용하면 프로퍼티 존재 여부를 확인할 수 있다.

문법은 다음과 같다.

"key" in object

// in 왼쪽에는 반드시 프로퍼티 이름이 와야 한다. 프로퍼티 이름은 보통 따옴표로 감싼 문자열.
let user = { name: "John", age: 30 };

alert( "age" in user ); // user.age가 존재하므로 true가 출력됩니다.
alert( "blabla" in user ); // user.blabla는 존재하지 않기 때문에 false가 출력됩니다.

대부분의 경우, 일치 연산자를 사용해서 프로퍼티 존재 여부를 알아내는 방법("=== undefined")은 꽤 잘 동작한다.

그런데 가끔은 이 방법이 실패할 때 in을 사용하면 프로퍼티 존재 여부를 제대로 판별할 수 있다.

20.3. 'for... in' 반복문

객체의 모든 키를 순회할 수 있다.
앞서 학습했던 for(;;) 반복문과는 완전히 다르다.

for (key in object) {
  // 각 프로퍼티 키(key)를 이용하여 본문(body)을 실행합니다.
}

아래 예시를 실행하면 객체 user 의 모든 프로퍼티가 출력된다.

let user = {
  name: "John",
  age: 30,
  isAdmin: true
};

for (let key in user) {
  // 키
  alert( key );  // name, age, isAdmin
  // 키에 해당하는 값
  alert( user[key] ); // John, 30, true
}

for..in 반복문에서도 for(;;)문처럼 반복 변수(looping variable)를 선언(let key)했다.

반복 변수명은 자유롭게 정할 수 있다. 'for (let prop in obj)'같이 key 말고 다른 변수명을 사용해도 괜찮다.

20.4. 객체 정렬 방식

프로퍼티에는 순서가 있을까? (반복문은 프로퍼티를 추가한 순서대로 실행되는지, 이 순서는 항상 동일할지 궁금해진다.)

객체는 '특별한 방식으로 정렬' 된다.

정수 프로퍼티(integer property) 는 자동으로 정렬되고, 그 외의 프로퍼티는 객체에 추가한 순서 그대로 정렬된다.

let codes = {
  "49": "독일",
  "41": "스위스",
  "44": "영국",
  // ..,
  "1": "미국"
};

for (let code in codes) {
  alert(code); // 1, 41, 44, 49
}

이유는 나라 번호(키)가 정수라서 1, 41, 44, 49 순으로 프로퍼티가 자동 정렬된다.

이때, 정수 프로퍼티는 변형 없이 정수에서 왔다 갔다 할 수 있는 무자열을 의미한다. 위 예시에서 "49"는 정수로 변환하거나 변환한 정수를 다시 문자열로 바꿔도 변형이 없기 때문에 정수 프로퍼티이다.

// 함수 Math.trunc는 소수점 아래를 버리고 숫자의 정수부만 반환합니다.
alert( String(Math.trunc(Number("49"))) ); // '49'가 출력됩니다. 기존에 입력한 값과 같으므로 정수 프로퍼티입니다.
alert( String(Math.trunc(Number("+49"))) ); // '49'가 출력됩니다. 기존에 입력한 값(+49)과 다르므로 정수 프로퍼티가 아닙니다.
alert( String(Math.trunc(Number("1.2"))) ); // '1'이 출력됩니다. 기존에 입력한 값(1.2)과 다르므로 정수 프로퍼티가 아닙니다.

키가 정수가 아니면 작성된 순서대로 프로퍼티가 나열된다.

let user = {
  name: "John",
  surname: "Smith"
};
user.age = 25; // 프로퍼티를 하나 추가합니다.

// 정수 프로퍼티가 아닌 프로퍼티는 추가된 순서대로 나열됩니다.
for (let prop in user) {
  alert( prop ); // name, surname, age
}

위 예시에서 49(독일 나라 번호)를 가장 위에 출력되도록 하면 정수로 취급되지 않게 각 나라 번호 앞에 "+"를 붙이면 된다.

let codes = {
  "+49": "독일",
  "+41": "스위스",
  "+44": "영국",
  // ..,
  "+1": "미국"
};

for (let code in codes) {
  alert( +code ); // 49, 41, 44, 1
}

21. 참조에 의한 객체 복사

객체와 원시 타입의 근본적인 차이는

객체는 '참조에 의해 (by reference)' 저장되고 복사된다.

반면에 원시값(문자열, 숫자, 불린 값)은 '값 그대로' 저장, 할당되고 복사된다.

let message = "Hello!";
let phrase = message;

두 개의 독립된 변수에 각각 문자열 "Hello!"가 저장된다.

객체의 동작 방식은 이와 다르다.

변수에 객체가 그대로 저장되는 것이 아니라,
객체가 저장되어 있는 '메모리 주소'인 객체에 대한 '참조 값'이 저장된다.

예를 들어 변수 user에 객체를 할당해보자.

let user = {
  name: "John"
};

객체는 메모리 내 어딘가에 저장된다.
변수 user에는 객체를 '참조' 할 수 있는 값이 저장된다.

따라서, 객체가 할당된 변수를 복사할 때에는 객체의 참조 값이 복사되고 객체는 복사되지 않는다.

let user = { name: "John" };

let admin = user; // 참조값을 복사함

변수는 두 개지만 각 변수에는 동일 객체에 대한 참조값이 저장된다.

따라서, 객체에 저급하거나 객체를 조작할 때는 여러 변수를 사용할 수 있다.

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // 'admin' 참조 값에 의해 변경됨

alert(user.name); // 'Pete'가 출력됨. 'user' 참조 값을 이용해 변경사항을 확인함

객체가 서랍장이라면, 변수는 서랍장을 열 수 있는 열쇠다.
서랍장은 하나, 열쇠는 두 개인데, 그 중 하나(admin)를 사용해 서랍장을 열어 정돈한 후, 또 다른 열쇠로 서랍장을 열면 정돈된 내용을 볼 수 있다.

21.1. 참조에 의한 비교

객체 비교 시 동등 연산자(==)와 일치 연산자(===)는 동일하게 동작한다.

비교 시 피연산자인 두 객체가 동일한 객체인 경우에 참을 반환.

두 변수가 같은 객체를 참조하는 예시를 살펴보자.

let a = {};
let b = a; // 참조에 의한 복사

alert( a == b ); // true, 두 변수는 같은 객체를 참조합니다.
alert( a === b ); // true

일치, 동등 비교에서 둘다 모두에서 참이 반환된다.

다른 예시를 살펴보자. 두 객체 모두 비어있다는 점에서 같아 보이지만, 독립된 객체이기 때문에 일치, 동등 비교를 했을 때 거짓이 반환된다.

let a = {};
let b = {}; // 독립된 두 객체

alert( a == b ); // false

obj1 > obj2 같은 대소 비교나 obj == 5 같은 원시값과의 비교에선 객체가 원시형으로 변환된다.

21.2. 객체 복사, 병합과 Object.assign

객체를 복제하고 싶다면? (기존에 있던 객체와 똑같으면서 독립적인 객체를 만들고 싶다면?)

자바스크립트는 객체 복제 내장 메서드를 지원하지 않기 때문에 어렵다.
자주 있는 일은 아니지만, 정말 필요하다면,

  1. 새로운 객체를 만든 다음 기존 객체의 프로퍼티들을 순회해 원시 수준까지 프로퍼티를 복사하면 된다.
let user = {
  name: "John",
  age: 30
};

let clone = {}; // 새로운 빈 객체

// 빈 객체에 user 프로퍼티 전부를 복사해 넣습니다.
for (let key in user) {
  clone[key] = user[key];
}

// 이제 clone은 완전히 독립적인 복제본이 되었습니다.
clone.name = "Pete"; // clone의 데이터를 변경합니다.

alert( user.name ); // 기존 객체에는 여전히 John이 있습니다.
  1. Object.assgin을 사용할 수도 있다.
Object.assign(dest, [src1, src2, src3...])
  • dest 는 목표로 하는 객체.
  • 이어지는 인수 [src1, src2, src3...]는 복사하고자 하는 객체. 얼마든지 많은 객체를 인수로 사용할 수 있다.
  • 객체 src1, ..., srcN의 프로퍼티를 dest 에 복사한다. dest를 제외한 인수의 프로퍼티 전부가 첫번째 인수로 복사된다.
  • 마지막으로 dest 가 반환.
let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// permissions1과 permissions2의 프로퍼티를 user로 복사합니다.
Object.assign(user, permissions1, permissions2);

// now user = { name: "John", canView: true, canEdit: true }
  • 목표 객체에 동일한 이름을 가진 프로퍼티가 있는 경우에는 기존 값이 덮어 씌워진다.
let user = { name: "John" };

Object.assign(user, { name: "Pete" });

alert(user.name); // user = { name: "Pete" }
  • object.assign을 사용하면 반복문 없이도 간단하게 객체를 복사할 수 있다.
let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

21.3. 중첩 객체 복사

user의 모든 프로퍼티가 원시값인 경우만 가정했음.

그러나, 프로퍼티가 다른 객체에 대한 참조 값일 때는?

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true, 같은 객체입니다.

// user와 clone는 sizes를 공유합니다.
user.sizes.width++;       // 한 객체에서 프로퍼티를 변경합니다.
alert(clone.sizes.width); // 51, 다른 객체에서 변경 사항을 확인할 수 있습니다.

clone.sizes = user.sizes로 프로퍼티를 복사하는 것만으론 객체를 복제할 수 없다. user.sizes는 객체이기 때문에 참조 값이 복사됨.

clone.sizes = user.sizes로 프로퍼티를 복사하면 clone과 user는 같은 sizes를 공유하게 된다.

문제를 해결하기 위해서는, 깊은 복사(deep cloning)을 해야 한다.
user[key]의 값을 검사하면서, 그 값이 객체인 경우 객체의 구조도 복사해주는 반복문을 사용해야 한다.

깊은 복사 시 사용되는 표준 알고리즘(structured cloning algorithm)을 사용하면 다양한 상황에서 객체를 복사할 수 있다.

22. 가비지 컬렉션

22.1. 가비지 컬렉션 기준

자바스크립트는 도달 가능성(reachability) 개념을 사용해 메모리 관리를 수행한다.

도달 가능한 값(reachable): 어떻게든 접근하거나 사용할 수 있는 값, 도달 가능한 값은 메모리에서 삭제 되지 않는다.

  1. 루트(root): 태생부터 도달 가능하기 때문에, 명백한 이유없이 삭제되지 않는 값들.
  • 현재 함수의 지역 변수와 매개변수
  • 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
  • 전역 변수
  • 기타 등등
  1. 루트가 참조하는 값 OR 체이닝으로 루트에서 참조할 수 있는 값

(ex) 전역변수에 객체가 저장되어 있음.
이 객체의 프로퍼티가 또 다른 객체를 참조하고 있다면, 프로퍼티가 참조하는 객체는 도달 가능한 값이 된다.
이 객체가 참조하는 다른 모든 것들도 도달 가능하다고 여겨짐.

자바스크립트 엔진 내에서는 가비지 컬렉터(garbage collector)가 끊임없이 동작한다.
가비지 컬렉터는 모든 객체를 모니터링하고, 도달할 수 없는 객체는 삭제.

간단한 예시

// user엔 객체 참조 값이 저장됩니다.
let user = {
  name: "John"
};

화살표는 객체 참조를 나타냄.

전역 변수 "user" 는 {name: "John"} (줄여서 John)이라는 객체를 참조한다.

user의 값을 다른 값으로 덮어쓰면 참조(화살표)가 사라진다.

user = null;

이제 존은 도달할 수 없는 상태가 되었다.
존에 접근할 방법도, 참조하는 것도 모두 사라졌기 때문.

이렇게 가비지 컬렉터는 존에 저장된 데이터를 삭제하고, 존을 메모리에서 삭제한다.

참조 두 개

참조를 user에서 admin으로 복사했다고 가정.

// user엔 객체 참조 값이 저장됩니다.
let user = {
  name: "John"
};

let admin = user;

위에서 한 것처럼 동일하게 user의 값을 다른 값으로 덮어써보자.

user = null;

전역 변수 admin을 통하면 여전히 객체 John에 접근할 수 있기 때문에 John은 메모리에서 삭제되지 않는다. 이 상태에서 admin을 다른 값(null 등)으로 덮어쓰면 John은 메모리에서 삭제될 수 있다.

연결된 객체

가족관계를 나타내는 복잡한 예시를 살펴보자.

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});

함수 marry(결혼하다)는 매개변수로 받은 두 객체를 서로 참조하게 하면서 '결혼' 시키고, 두 객체를 포함하는 새로운 객체를 반환.

지금은 모든 객체가 도달 가능한 상태.
여기서 참조 두 개를 지워보자.

delete family.father;
delete family.mother.husband;

삭제한 두 개의 참조 중 하나만 지웠다면 모든 객체가 여전히 도달 가능한 상태였을 것이다.

하지만 참조 두 개를 지우면 John으로 들어오는 참조(화살표)는 모두 사라져 John은 도달 가능한 상태에서 벗어난다.

외부로 나가는 참조는 도달 가능한 상태에 영향을 주지 않는다.
외부에서 들어오는 참조만이 도달 가능한 상태에 영향을 준다.

가비지 컬렉션 후 메모리 구조는 다음과 같다.

도달할 수 없는 섬

객체들이 연결되어 섬 같은 구조를 만든다.
그러나 이 섬에 도달할 방법이 없을 때, 섬을 구성하는 객체 전부가 메모리에서 삭제된다.

만약에, 근원 객체 family가 아무것도 참조하지 않도록 해보자.

family = null;

메모리의 내부 상태는 다음과 같다.

Jon과 Ann은 여전히 서로를 참조하고 있고, 두 객체 모두 외부에서 들어오는 참조를 가지고 있지만 이것으로 충분하지 않다.

"family" 객체와 루트의 연결이 사라지면 루트 객체를 참조하는 것이 아무것도 없게 된다.

이렇게 되면 섬 전체가 도달할 수 없는 상태가 되고, 섬을 구성하는 객체 전부가 메모리에서 제거된다.

내부 알고리즘

가비지 컬렉션 기본 알고리즘에 대한 내용 (자세하게 다루지는 않음.)

확인하고 싶을 때 공식 문서 - 가비지 컬렉션 참고하자.

0개의 댓글

관련 채용 정보