본문 바로가기
web/javascript

[javascript]모던 자바스크립트 - 객체(가비지 컬렉션, this, new, 생성자, 심볼, 형 변환)

by fien 2021. 1. 27.

 

객체: 기본

 

ko.javascript.info

위 사이트를 정리한 내용입니다. 

 

목차 입니다

 

객체

  1. 객체
    • 프로퍼티 단축 property value shorthand
    • 프로퍼티 존재 여부 확인 : in 연산자
    • for ... in 반복문
  2. 객체의 복사
    • 참조에 의한 비교
    • 객체 복사와 병합 : Object.assign
    • 중첩 객체 복사
  3. 가비지 컬렉션
    • 가비지 컬렉션 기준
    • mark-and-sweep 알고리즘
  4. 메서드와 this
    • 메서드 생성 & this
    • this의 결정
    • 화살표 함수의 this
  5. new 연산자와 생성자 함수
    • 생성자 함수
    • 생성자 내 메서드
  6. 옵셔널 체이닝
    • ?.
  7. 심볼형 (ES6)
    • 심볼
    • 심볼의 등장배경
    • 전역 심볼
  8. 객체의 형 변환
    • ToPrimitive : 형 변환 hint
    • Symbol.toPrimitive
    • toString과 valueOf

 

숫자나 문자열처럼 더 이상 쪼갤 수 없이 하나의 값만 담을 수 있는 데이터를 primitive type이라 합니다. 객체는 이러한 원시 타입의 데이터를 key-value 쌍으로 구성하여 복합 데이터를 구성할 수 있습니다.

1. 객체

객체는 중괄호{ } 를 이용해 만들 수 있고 중괄호 내에 key-value 쌍으로 구성된 프로퍼티(property)를 여러 개 넣을 수 있습니다.

빈 객체를 만드는 두 가지 방법을 확인해봅시다.

// 1. 객체 생성자 방식
const user = new Object();

// 2. 객체 리터럴 방식 : {...} 중괄호를 이용해 선언하는 것
const user = {};
const fruit = {
    name: 'apple', //프로퍼티 나열
    price: 1000
};

객체의 프로퍼티를 추가하거나 삭제 할 수 있습니다.

fruit.season = 'fall';
delete fruit.price;

객체를 const로 선언해도 내부 프로퍼티는 수정 가능하다. 대신, 객체 자체를 재 할당 할 수 없다.

const fruit = {};
const fruit = {name: 'apple'}; // Error: Identifier 'fruit' has already been declared

프로퍼티는 아래의 두 가지 방식으로 접근 할 수 있습니다. 일반적으로는 점표기법을 사용하고 실행중에 동적으로 키를 정할 때는 대괄호 표기법을 사용합니다.

// 점 표기법
console.log(fruit.name);
// 대괄호 표기법
console.log(fruit['name']);

프로퍼티 단축 property value shorthand

변수를 이용하여 객체를 선언할 때 키와 변수 명이 동일하다면 단축 구문을 사용 할 수 있다.

const name = 'apple'
const season = 'fall'
const fruit = {name, season};

프로퍼티 존재 여부 확인 in 연산자

자바스크립트에서 존재하지 않는 프로퍼티에 접근하면 에러가 발생하는 대신 undefined가 반환된다. 이와 비슷한 용도로 in 연산자를 사용할 수 있다.

const fruit = {
    name: 'apple',
    season: 'fall'
};

console.log(season in fruit);
console.log(price in fruit);

for .. in 반복문

for .. in 반복문을 사용해 객체의 모든 키를 순회하면서 작업을 수행 할 수 있습니다.

for(key in fruit){
    console.log(`${key} : ${fruit[key]}`);
}

2. 객체의 복사

객체와 원시타입primitive type와 다르게 참조에 의해(by reference) 저장하고 복사합니다.

반면, 원시타입은 값을 그대로 복사해서 사용합니다.

// primitive type
let msg1 = 'hello';
let msg2 = msg1;
msg1 = 'bye';

console.log(msg1); // bye
console.log(msg2); // hello

// object
const fruit1 = { name: 'apple'};
const fruit2 = fruit1;
fruit1.name = 'banana';

console.log(fruit1); // fruit1
console.log(fruit2); // fruit2

원시타입은 값을 그대로 복사해서 사용하기 때문에 서로 다른 데이터가 저장되지만 객체는 참조를 전달하기 때문에 동일한 데이터를 사용합니다.

참조에 의한 비교

객체 비교 시 ==과 ===은 동일하기 동작합니다.

객체의 참조 값을 비교하기 때문에 데이터가 똑같다고 해서 같은 객체는 아닙니다.

const a = {};
let b = a;

console.log(a == b); //true
console.log(a === b); //true

const c = {};
const d = {};

console.log(c == d); // false
console.log(c === d); //false

객체 복사와 병합 Object.assign

Obejct.assign은 열거한 객체들의 프로퍼티를 복사해서 새로운 객체를 반환하는 메서드입니다. 첫 번째 인수부터 복사를 하고 동일한 프로퍼티가 있으면 덮어쓰기를 합니다.

Object.assign(obj1, obj2, ..., objN);

const fruit1 = { name: 'apple', season: 'fall' };
const fruit2 = { price: 1000 };
const fruit3 = { name: 'grape'};

const fruit = Object.assign(fruit1, fruit2, fruit3);
console.log(fruit); //{name: "grape", season: "fall", price: 1000}

중첩 객체 복사

앞에서는 프로퍼티가 원시타입인 경우를 다뤘습니다. 객체의 프로퍼티가 객체인 경우는 어떻게 해야 할까요? 객체를 포함한 객체를 복사해보겠습니다.

const member1 = {
    name: 'Sora',
    address: {
        city: 'Seoul',
        gu: 'seocho-gu'
    }
}

const member2 = Object.assign({},member1);

console.log(member1.address === member2.address); // true

member1.address.gu = 'gangnam-gu';

console.log(member2.address); // {city: "Seoul", gu: "gangnam-gu"}

위의 경우 address 객체의 참조를 복사하여 member2의 adress에 할당합니다. 즉, member1과 member2가 동일한 address 객체를 사용하게 됩니다.

이러한 경우 각 키-값을 검사하면서 복사해줘야 하는데 이를 **깊은 복사(deep cloning)**이라고 합니다.

아래는 재귀 함수로 깊은 복사를 구현한 코드입니다.

function copyObj(obj){
    const result = {};

  for(let key in obj){
        if (typeof obj[key] === 'object'){
            result[key] = copyObj(obj[key]);
        } else { 
            result[key] = obj[key];
        }
    }
    return result;
}

const member3 = copyObj(member1);

console.log(member1.address === member3 .address); // false

3. 가비지 컬렉션 Garbage Collection

자바스크립트는 메모리를 자동으로 관리해준다. 가비지 컬렉터가 메모리 할당을 추적하고 할당된 메모리 블록이 더 이상 참조하지 않을 때 메모리를 해제합니다.

가비지 컬렉션 기준

자바스크립트는 도달 가능성 reachability 개념을 사용해 메모리 관리를 수행합니다. 도달 가능하다는 것은 어떻게든 접근하거나 사용할 수 있는 값을 뜻합니다.

전역변수, 현재 함수의 지역변수와 매개변수, 중첩 함수에서 사용하는 변수와 매개변수 등을 루트root 라고 부릅니다. 루트는 애초부터 도달 가능하기 때문에 명백한 이유 없이는 삭제되지 않습니다. 루트가 참조하는 값이나 체이닝을 통해 루트로부터 참조할 수 있는 값은 도달 가능한 값으로 삭제 대상이 아닙니다.

간단한 예를 들어보겠습니다.

const user = { name: 'jenny' };

위 코드에서는 전역변수 user{ name: 'jenny'} 객체를 참조합니다.

user = null;

user 에 다른 값을 대입하여 더 이상 { name: 'jenny'} 객체를 참조하지 않습니다. 이제 { name: 'jenny'} 객체는 도달할 수 없는 상태가 되었기 때문에 메모리에서 삭제합니다.

mark-and-sweep 알고리즘

mark-and-sweep은 가비지 컬렉션의 기본 알고리즘으로 아래의 단계를 거쳐 수행됩니다.

  1. 가비지 컬렉터는 루트 정보를 수집하고 이를 mark 합니다
  2. 루트가 참조하고 있는 모든 객체를 방문하고 각각을 mark 합니다
  3. mark된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark 합니다. 이때, mark한 객체는 다시 방문하지 않습니다.
  4. 도달 가능한 객체가 있는 한 위의 과정을 반복합니다.
  5. mark되지 않는 모든 객체를 메모리에서 삭제합니다.

자바스크립트 엔진은 위 알고리즘에 다양한 최적화 기법을 적용해 성능을 향상 시킵니다.

  • 세대별 수집 generational collection : 객체를 old와 new로 구분합니다. 객체의 상당수는 생성 이후에 역할을 빠르게 수행하여 금방 쓸모가 없어지는데 이러한 객체를 새로운 객체로 구분합니다. 가바지 컬렉터는 새로운 객체는 공격적으로 감시하고 일정 시간 동안 살아남은 객체는 오래된 객체로 분류하여 덜 감시합니다.
  • 점진적 수집 incremental collection : 방문해야 할 객체가 많으면 리소스나 시간이 많이 소모됩니다. 자바스크립트 엔진은 이런 현상을 개선하기 위해 가비지 컬렉션을 여러 부분으로 분리해 작업을 분산합니다.
  • 유휴 시간 수집 idle-time collection : 실행에 주는 영향을 최소화하기 위해 CPU가 유휴 상태일 떄만 가비지 컬렉션을 실행합니다.

이 외에도 다양한 알고리즘과 최적화 기법이 있습니다.


4. 메서드와 this

객체 내부에 객체와 관련된 함수를 작성할 수 있습니다. 이처럼 객체의 프로퍼티로 선언된 함수를 메서드라고 합니다.

메서드 생성 & this

메서드는 다른 프로퍼티처럼 선언할 때 등록하거나 추가, 변경이 가능합니다.

메서드 내에서 객체 내부의 프로퍼티에 접근하고자 할 때 this 키워드를 사용합니다.

const user = { name:'yuri'};
//함수 표현식으로 추가
user.sayHi = function(){ 
    console.log(`Hi, I'm ${this.name}`); 
};

user.sayHi();

const fruit = {
    name:'apple',
    sayHi: function() { console.log(`Hi, I'm ${this.name}`); },
    //단축 구문으로 작성가능
    sayHello(){ console.log(`Hello, I'm ${this.name}`); }
}
fruit.sayHi();

this의 결정

자바스크립트에서는 아래 코드를 작성해도 에러가 발생하지 않습니다.

function sayHi(){
    console.log(this.name);
}

this의 값은 런타임 중에 결정되는데 호출하는 객체에 따라 this의 참조 값이 달라집니다. 즉, 동일한 함수라도 호출하는 객체가 다르면 this의 참조 값도 다릅니다.

const user = {name:'user', sayHi:sayHi};
const admin = {name:'admin', sayHi};

user.sayHi(); // user
admin.sayHi(); // admin

this가 자유롭기 때문에 재 사용성이 증가하지만 실수로 이어질 수 있기 때문에 개발 시에 유의해야 합니다.

참고로 객체 없이 this를 호출하면 undefined을 반환합니다.

sayHi(); // undefined

화살표 함수의 this

화살표 함수는 일반 함수와 다르게 선언할 때 this 값이 정적으로 정해집니다. 화살표 함수의 this는 외부 함수의 this 값을 가집니다. 별개의 this 대신 외부 컨텍스트의 this를 사용하고 싶을 때는 화살표 함수가 유용합니다. 자신만의 this가 없음!

const user = {
    name: 'wendy',
    sayHi: () => {
        console.log(`Hi everyone, I'm ${this.name}`);
        console.log(`${this}`); // 외부 블록에 this => window 객체를 가져옴
    },
    sayHello: function() {
        (()=>{
            console.log(`Hi everyone, I'm ${this.name}`); 
            console.log(`${this}`); // sayHello 함수의 this를 가져옴 -> 함수를 호출 하는 객체
        })();
    }
};

user.sayHi();  // Hi everyone, I'm
user.sayHello(); // Hi everyone, I'm wendy

const user2= { name:'joy', sayHello:user.sayHello};
user2.sayHello();

5. new 연산자와 생성자 함수

개발을 하다 보면 유사한 객체를 여러 개 만들어야 하는 경우가 자주 발생합니다. new 연산자와 생성자 함수를 사용하면 템플릿 처럼 복수의 객체를 쉽게 생성 할 수 있습니다.

생성자 함수 Constructor Function

일반적인 함수와 차이는 없지만 아래의 두 가지 관례를 따릅니다.

  1. 함수 이름의 첫 글자는 대문자로 시작한다.
  2. 반드시 new 연산자를 붙여 실행한다.
function User(name) {
    this.name = name;
    this.isAdmin = false;
}

const user1 = new User('jenny');
const user2 = new User('rose');

console.log(user1.name); // jenny
console.log(user2.name); // rose

new User(...) 를 실행하면 다음과 같이 동작합니다.

  1. 빈 객체를 만들어 this에 할당한다.
  2. 함수를 실행한다. this에 프로퍼티를 추가한다.
  3. this를 반환한다.
function User(name) {
    // this = {}; 암묵적으로 빈 객체 생성

    // 새로운 프로퍼티를 this에 추가
    this.name = name;
    this.isAdmin = false;

    // return this; 암묵적으로 this를 반환
}

함수 리터럴로 작성하면 아래와 같습니다.

let user1 = {
    name: 'jenny',
    isAdmin: false;
}

이렇게 new 연산자와 생성자 함수를 재 사용하여 손쉽게 객체를 만들 수 있습니다.

생성자 내 메서드

생성자 함수의 매개변수를 이용해 객체를 자유롭게 구성할 수 있습니다. 프로퍼티로 메서드를 추가 할 수 있습니다.

function User(name) {

    this.name = name;
    this.sayHi = function() {
        console.log(`I'm ${this.name}`);
    };

}

const jenny = new User('jenny');
jenny.sayHi(); // I'm jenny

const rose = new User('rose');

console.log(rose.sayHi === jenny.sayHi); //false - 메서드도 각각 생성

new 연산자를 이용해 객체를 생성하면 메서드도 각각 생성됩니다. 이런 경우 메서드를 생성자의 프로토타입에 선언하여 메모리 낭비를 막을 수 있습니다. 해당 부분은 프로토타입 부분에서 설명하겠습니다.


6. 옵셔널 체이닝 ?.

** 최신 문법으로 구식 브라우저는 폴리필이 필요합니다.

과거 자바스크립트에서 존재하지 않는 프로퍼티의 내부에 접근하면 에러가 발생 합니다.

const user = {};

console.log(user.address); // undefined
console.log(user.address.city); // TypeError: Cannot read property 'city' of undefined

또 다른 예로는 페이지에 존재하지 않는 요소에 접근해 정보를 가져오는 경우 입니다.

//querySelector(...) 호출 결과가 null이면 에러 발생
let html = document.querySelector('.my-element').innerHTML;

과거에는 이러한 문제를 && 연산자로 해결했습니다.

// user.address 까지만 실행하고 undefined를 반환
console.log(user && user.address && user.address.city);  

?.

?. 은 앞의 평가 대상이 undefined나 null 이면 평가를 멈추고 undefined를 반환합니다.

이처럼 도중에 평가를 멈추는 방법을 단락 평가short-circuit라 합니다.

let user = {};
console.log(user?.address?.city); // undefined

user = null;
console.log(user?.address); // undefined
console.log(user?.address.city); //undefined

점 표기법 뿐 아니라 대괄호 표기법으로 프로퍼티에 접근 할 때도 사용할 수 있습니다. 또한, delete와 사용 할 수 도 있습니다.

console.log(user?.['address']);

delete user?.name; // user가 존재하면 name 프로퍼티를 제거

7. 심볼형 ES6

심볼

심볼은 객체의 프로퍼티로 사용할 수 있는 primitive data type으로 유일한 식별자unique identifier를 만들 때 사용합니다.

Symbol()를 사용해 심볼을 생성할 수 있습니다. 이때 new 연산자는 사용하지 않습니다.

// id는 새로운 심볼이 됩니다.
const id = Symbol();

// 이름부여
const id1 = symbol('id');
const id2 = Symbol('id');

console.log(id1 == id2); // false

심볼을 생성할 때 문자열을 부여할 수 있는데 같은 문자열로 심볼을 여러 개 만들어도 각 심볼은 별개의 값입니다.

객체 리터럴 { ... } 로 객체를 만드는 경우에는 대괄호를 사용합니다.

const user = {
    name: 'jenny',
    [id]: 123
}

심볼의 등장배경

문자열로 프로퍼티를 추가하면 이름이 충돌 할 수 있습니다.

예를 들어 개인이 Array.prototype에 커스텀 프로퍼티 toUpperCase를 추가해 사용하고 있다고 가정해봅시다. 그런데 새로 나온 자바스크립트 버전에 Array.prototype.toUpperCase가 추가 된다면 충돌이 발생하기 때문에 코드 수정이 불가피 합니다. 이런 경우 문자열 대신 Symbol을 키로 사용한다면 충돌이 발생하지 않습니다.

이전 버전과의 호환성을 유지한 채 새로운 기능을 추가해야 하는 경우가 있습니다.

Object.keys 메서드나 for...in 루프를 깨지 않고 객체에 프로퍼티를 추가해야 할 필요가 있습니다. Symbol은 for ... in 루프나 Object.key 메서드에서 배제되어 해당 속성은 반환되지 않습니다.

for (let key in user) console.log(key); // name만 출력되고 심볼은 출력 안됨

대신, Object.getOwnPropertySymbol()를 사용하면 심볼을 조회할 수 있고 Obejct.assign은 키가 심볼인 프로퍼티를 배제하지 않고 복사합니다.

이렇게 심볼을 이용하면 객체의 hidden property를 만들 수 있습니다. 외부 스크립트나 라이브러리에 속한 객체에 새로운 프로퍼티를 추가할 수 있고 이를 외부 스크립트로부터 숨길 수 있습니다.

전역 심볼

앞서 살펴보았듯이 심볼은 이름이 같더라도 모두 별개로 취급합니다. 그런데 이름이 같은 심볼이 같은 개체를 가리키길 원하는 경우가 있습니다. 이런 경우 전역 심볼 레지스트리global symbol registry를 이용하면 됩니다. 전역 심볼 레지스트리 안에 심볼을 만들고 이름으로 심볼에 접근해 사용할 수 있습니다. Symbol.for(key)를 사용하면 전역 심볼 레지스트리를 사용해 심볼을 읽거나 생성 할 수 있습니다.

const id = Symbol.for('id'); // 전역 레지스트리에 심볼이 존재하지 않으면 심볼을 생성

const id2 = Symbol.for('id'); // 전역 레지스트리에서 'id'에 해당하는 심볼을 읽는다.

console.log(id === id2); // true - 두 심볼은 동일하다

8. 객체를 primitive type으로 변환

자바스크립트의 primitive type은 필요에 따라 자동으로 형변환이 이뤄집니다.

ToPrimitive 형변환 hint

객체의 형 변환은 세 종류로 구분되는데 hint라 불리는 값이 기준이 됩니다. '처리하려는 자료형' 정도로 이해하면 될 것 같습니다.

  • string

    alert 함수 같이 문자열을 기대하는 연산을 수행할 때는 hint가 string 이 됩니다.

// 객체를 출력
console.log(`${obj}`;
// 객체를 프로퍼티 키로 사용
anotherObj[obj] = 123;
  • number

    수학 연산을 적용할 때 hint는 number 입니다.

// 명시적 형변환
const num = Number(obj);

// 수학 연산
const a = +obj;

// 대소 비교
const greater = obj1 > obj2;
  • default

    연산자가 기대하는 자료형이 확실하지 않을 때 hint는 default 입니다. Date 객체를 제외한 모든 내장 객체는 hint가 default인 경우 number인 경우와 동일하게 처리한다.

// 이항 덧셈 연산은 문자열과 숫자 둘 다 처리 가능 -> hint가 default
const total = obj1 + obj2;

if (obj1 == 1) {...};

자바스크립트에서 형 변환 시 아래와 같은 순서를 따라 진행합니다.

  1. 객체에 obj\[Symbol.toPrimitive\](hint)메서드가 있는지 찾고, 있다면 메서드를 호출합니다. Symbol.toPrimitive는 시스템 심볼로, 심볼형 키로 사용됩니다.
  2. 1에 해당하지 않고 hint가 "string"이라면, obj.toString()이나 obj.valueOf()를 호출합니다.
  3. 1과 2에 해당하지 않고, hint가 "number"나 "default"라면 obj.valueOf()obj.toString()을 호출합니다.

Symbol.toPrimitive

자바스크립트에는 Symbol.toPrimitive라는 내장 심볼이 존재하는데 hint를 명명하는데 사용합니다.

obj[Symbol.toPrimitive] = function(){
    //반드시 프리미티브 값을 반환해야 합니다.
    //hint는 string, number, default 중 하나가 될 수 있습니다.
};

아래의 예를 살펴 봅시다. user 객체 안에 객체-원시형 변환 메서드인 [Symbol.toPrimitive](hint) 가 존재합니다.

const user = { 
    name: 'jenny',
    money: 2000,
    [Symbol.toPrimitive](hint) {
        console.log(`hint : ${hint}`);
        return hint == 'string' ? `{name : ${this.name}}` : this.money;
    }
};

console.log(user); // hint: string -> {name : 'jenny'}
console.log(+user); // hintL number -> 2000
console.log(user + 500); // hint: default -> 2000

이렇게 형 변환 메서드를 구현하면 hint에 따라 값을 반환 합니다.

toString과 valueOf

객체에 Symbol.toPrimitive가 없으면 자바스크립트는 다음의 규칙을 따릅니다.

  • hint가 "string"이라면, obj.toString()이나 obj.valueOf()를 호출합니다. (obj.toString()가 우선)
  • hint가 "number""default"라면 obj.valueOf()obj.toString()을 호출합니다(obj.valueOf() 가 우선).

toString과 valueOf 메서드는 원시 값을 반환해야 합니다. 객체를 반환하면 결과를 무시합니다.

객체는 기본적으로 포함하는 toString, valueOf 메서드는 아래 코드처럼 동작합니다.

const user = {name: "rose"};

console.log(user); // [object Object]
console.log(user.valueOf() === user); // true

toString과 valueOf 메서드를 재정의 하여 원하는 값을 반환하도록 만들어 줍니다.

const user = {
  name: "rose",
  money: 1000,

  // hint가 "string"인 경우
  toString() {
    return `{name: "${this.name}"}`;
  },

  // hint가 "number"나 "default"인 경우
  valueOf() {
    return this.money;
  }

};

console.log(`${user}`); // toString -> {name: "rose"}
console.log(+user); // valueOf -> 1000
console.log(user + 500); // valueOf -> 1500

모든 형 변환을 한 곳에서 처리할 때는 toString 메서드만 구현해주면 됩니다.

댓글