모던 JS 튜토리얼 Part 1. 4강 객체 기본

ᴄsᴇ ᴘᴇʙʙʟᴇ·2023년 11월 22일
0
post-thumbnail

모던 JS 튜토리얼 Part 1. 코어 자바스크립트 문서를 읽고 내용을 정리합니다.


🌝 객체

객체란?

앞서 배운 원시형(primitive type) 자료형은 오직 하나의 데이터(문자열, 숫자 등)만 담을 수 있다.

그러나 객체형은 원시형과 달리 다양한 데이터를 담을 수 있다. 키로 구분된 데이터 집합이나 복잡한 개체(entity)를 저장할 수 있다.

객체는 중괄호 {}를 이용해 만들 수 있고, 중괄호 안에는 ‘키(key): 값(value)’ 쌍으로 구성된 프로퍼티(property)를 여러 개 넣을 수 있다.

키(key)엔 문자형, 값(value)엔 모든 자료형이 허용된다. 프로퍼티 키(key)는 ‘프로퍼티 이름’, '식별자'라고도 부른다.

  • 함수도 객체이다.

예시로 객체의 개념 이해하기

서랍장 예시로 객체를 이해해보자.

서랍장에 파일이 꽂혀있을 때, 파일에 붙어있는 이름표가 객체의 키(key), 파일의 내용이 값(value)이라고 생각하면 된다. 서랍장에서 파일 이름을 보고 파일을 쉽게 찾을 수 있듯이, 객체에서는 키(key)를 이용하여 프로퍼티를 쉽게 찾는다.

빈 객체(빈 서랍장)를 만드는 2가지 방법

//1. '객체 생성자' 문법
let user = new Object();

//2. '객체 리터럴' 문법
let user = {};

2번째 방법으로 객체를 선언하는 것을 객체 리터럴(object literal)이라고 부른다. 객체를 선언할 때는 주로 이 방법을 사용한다.

객체 리터럴과 프로퍼티

중괄호 {} 안에는 "키:값" 쌍으로 구성된 프로퍼티가 들어간다.

let user = {
  name: "John",   //프로퍼티: name과 John, key: name, value: John
  age: 30 //프로퍼티: age와 30, key: age, value: 30
}

프로퍼티 키(key)는 문자형만 올 수 있다. 단, 띄어쓰기가 있는 경우에는 아래와 같이 따옴표로 묶어줘야 한다. 프로퍼티 값(value)에는 모든 자료형이 올 수 있다. 아래와 같이 불린형도 가능하다.

let user = {
  "likes birds": true
}

프로퍼티 값을 읽고, 프로퍼티를 추가하고, 삭제하기

프로퍼티 값을 읽는 방법 2가지

  1. 점 표기법(dot notation)
alert(user.name); // John
alert(user.age); // 30
  1. 대괄호 표기법

띄어쓰기가 있는 프로퍼티 키(key)의 경우 1번 표기법을 사용할 수 없다.

user.likes birds = true // 에러 발생

이럴 때 대괄호 표기법을 사용할 수 있다. 단, 대괄호 표기법 안에서 문자열을 사용할 땐 문자열을 따옴표로 묶어줘야 한다.

user["likes birds"] = true

아래와 같은 방법도 가능하다. 1번 표기법은 불가능하다.

let key = "likes birds";

user[key] = true;

프로퍼티 값 추가하기

단순히 아래와 같이 작성해주면 된다.

//점 표기법
user.isAdmin = true;

//대괄호 표기법
user["isAdmin"] = true;

프로퍼티 삭제하기

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

//점 표기법
delete user.age;

//대괄호 표기법
delete user["age"];

cf) 상수 객체는 수정될 수 있다!
const로 선언된 객체는 수정될 수 없을 것 같지만, 수정될 수 있다.

const user = {
  name: "John"
};

user.name = "Pete";

laert(user.name); // Pete

const는 user의 값을 고정하지만, 그 내용은 고정하지 않는다. 즉 user=...를 전체적으로 설정하려고 할 때만 오류가 발생한다.

계산된 프로퍼티

객체 리터럴 안의 프로퍼티 키(key)가 대괄호로 둘러싸여 있는 경우, 이를 계산된 프로퍼티(computed property)라고 부른다.

대괄호로 둘러싸인 프로퍼티 키(key)를, 대괄호 안에 있는 변수에서 가져오겠다는 것을 의미한다.

예시를 보자.

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

let bag = {
  [fruit]: 5 // 변수 fruit에서 프로퍼티 키(key)를 동적으로 받아온다.
};

alert(bag.apple);

사용자가 prompt 창에 apple이라고 입력하면, 결과는 5가 출력된다. 위 코드는 아래와 같은 코드이다.

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

bag[fruit] = 5;

한편, 대괄호 안에는 복잡한 표현식이 올 수도 있다.

let fruit = "apple";
let bag = {
  [fruit + "Computers"]: 5 // bag.appleComputers = 5
}

대괄호 표기법은 프로퍼티 이름과 값의 제약을 없애주기 때문에 점 표기법보다 훨씬 강력하다. 그런데 작성하기 번거롭다는 단점이 있다.

프로퍼티 이름이 확정된 상황이고, 단순한 이름이라면 처음엔 점 표기법을 사용하다가 뭔가 복잡한 상황이 발생했을 때 대괄호 표기법으로 바꾸는 경우가 많다.

단축 프로퍼티

아래의 예시를 보자.

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

이렇게 프로퍼티 키(key)와 값(value)의 이름이 동일할 경우, 코드를 아래와 같이 짧게 줄일 수 있다.

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

프로퍼티 이름의 제약사항

프로퍼티 키(key)는 어떤 문자형이나 될 수 있다. (사실 심볼형도 되는데 이는 뒤에서 다룰 예정이다.)

예를 들어 아래처럼 객체 프로퍼티 키(key) 이름을 예약어로 설정할 수 있다!

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

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

만약 문자형이 아닌 다른 자료형으로 설정할 경우 문자열로 자동 형 변환된다.

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

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

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

2가지 방법이 있다.

  1. 객체의 undefined 반환 특징 이용하기
    객체는 존재하지 않는 프로퍼티에 접근하려고 해도 에러 발생 없이 undefined 값을 반환한다. 이러한 특징을 이용하면 프로퍼티 존재 여부를 확인할 수 있다.
let user = {};

alert( user.noSuchProperty === undefined ); // true. true는 '프로퍼티가 존재하지 않음'을 의미합니다.
  1. 연산자 in 사용하기

문법은 아래와 같다.

"key" in 객체

예시를 보자.

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

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

아래와 같이 변수로 접근해도 된다.

let user = { age: 30 };

let key = "age";
alert( key in user ); // true, 변수 key에 저장된 값("age")을 사용해 프로퍼티 존재 여부를 확인합니다.

프로퍼티 값(value) 자체에 undefined가 할당된다면 1번이 원하는대로 동작하지 않을 수 있기 때문에, 2번이 존재한다. (자세한 것은 문서 참고)

객체의 모든 키를 순회하는 'for...in' 반복문

for...in 반복문을 사용하면 객체의 모든 키(key)를 순회할 수 있다. for 반복문과는 완전히 다르다!

문법은 아래와 같다.

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

객체의 모든 프로퍼티 키와 값을 출력하는 예제를 보자.

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

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

여기서 key는 단순한 변수명이다. 다른 변수명으로 바꿔도 된다.

객체 정렬 방식

객체와 객체 프로퍼티를 다루다 보면 "프로퍼티엔 순서가 있을까?"라는 의문이 생기기 마련이다. 특히 반복문은 프로퍼티를 추가한 순서대로 실행될지, 그리고 이 순서는 항상 동일할지 궁금해진다.

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

  • 정수 프로퍼티 : 정수로 변환하거나 변환된 정수를 다시 문자열로 바꿔도 변형이 없는 문자열을 의미함

키(key)가 정수인 경우는 오름차순으로 자동 정렬된다.

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

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

키(key)가 정수가 아닌 경우는 작성된 그대로 나열된다.

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

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

🌝 참조에 의한 객체 복사

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

  • 객체 : 참조에 의해(by reference) 저장되고 복사된다.
  • 원시 타입 : 값 그대로 저장, 할당되고 복사된다.

원시타입 예제를 보자.

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

이렇게 하면 message, phrase 변수에 각각 문자열 Hello!가 저장된다.

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

let user = {
  name: "John"
}

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

객체 복사하기

위의 내용에 따라, 객체가 할당된 변수를 복사할 때, 객체의 참조 값이 복사되는 것이지 객체 자체가 복사되는 것이 아니다.

let user = { name: "John" };

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

그림을 보면 이해가 쉽다.

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

아래의 예시도 보자.

let user = { name: 'John' };

let admin = user;

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

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

객체를 서랍장에 비유하면 변수는 서랍장을 열 수 있는 열쇠라고 할 수 있다. 서랍장은 하나, 서랍장을 열 수 있는 열쇠는 두 개인데, 그중 admin 열쇠를 사용해 서랍장을 열어 정돈한 후, user 열쇠로 서랍장을 열면 내용이 정돈되어 있는 것을 확인할 수 있다.

  • 참조에 의한 비교에 대한 내용을 알고 싶다면 문서 참고

객체 복사, 병합과 Object.assign

앞에서 한 것과 달리 객체를 복제하고 싶다면 어떻게 해야 할까? 즉, 기존에 있던 객체와 똑같으면서 독립적인 객체를 만들고 싶다면 말이다.

방법은 있는데 자바스크립트는 객체 복제 내장 메서드를 지원하지 않기 때문에 조금 어렵다. 사실 객체를 복제해야 할 일은 거의 없다. 참조에 의한 복사로 해결 가능한 일이 대다수이다.

정말 복제가 필요한 상황이라면 2가지 방법을 사용하면 된다.

  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.assign 사용하기

문법을 보자.

Object.assign(dest, [src1, src2, src3...])
  • 첫 번째 인수 dest는 목표로 하는 객체이다.
  • 이어지는 인수 src1, ..., srcN는 dest에 복사하고자 하는 객체이다. ...은 필요에 따라 얼마든지 많은 객체를 인수로 사용할 수 있다는 것을 나타낸다.
  • 객체 src1, ..., srcN의 프로퍼티를 dest에 복사한다. dest를 제외한 인수(객체)의 프로퍼티 전부가 첫 번째 인수(객체)로 복사된다.
  • 마지막으로 dest를 반환한다.

assign 메서드를 사용해 여러 객체를 하나로 병합하는 예시를 살펴보자.

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 }

목표 객체(user)에 동일한 이름을 가진 프로퍼티가 있는 경우엔 기존 값이 덮어씌워 진다.

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);

예시를 실행하면 user에 있는 모든 프로퍼티가 빈 배열에 복사되고 변수에 할당된다.

중첩 객체 복사

아래의 경우에는 어떻게 해야할까?

let user = {
  name: "John",
  sizes: { //중첩 객체
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182

clone.sizes = user.sizes로 프로퍼티를 복사하는 것만으론 객체를 복제할 수 없다. user.sizes는 객체이기 때문에 참조 값이 복사되기 때문이다. clone.sizes = user.sizes로 프로퍼티를 복사하면 clone과 user는 같은 sizes를 공유하게 된다.

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

자바스크립트 라이브러리 lodash의 메서드인 _.cloneDeep(obj)을 사용하면 이 알고리즘을 직접 구현하지 않고도 깊은 복사를 처리할 수 있다.

🌝 가비지 컬렉션

가비지 컬렉션이란?

자바스크립트는 눈에 보이지 않는 곳에서 메모리 관리를 수행한다.

원시값, 객체, 함수 등 우리가 만드는 모든 것은 메모리를 차지하는데, 더는 쓸모 없어지게 된 것들은 찾아내 삭제해야 한다. 자바스크립트 엔진이 필요 없는 것을 찾아내 삭제하는 것이 가비지 컬렉션이다.

가비지 컬렉션 기준

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

가비지 컬렉터는 모든 객체를 모니터링하고, 도달할 수 없는 객체는 삭제한다. 도달 가능한 값은 메모리에서 삭제되지 않는다.

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

그림과 예시로 이해하기

let user = {
  name: "John"
};

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

user = null;

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

let admin = user;

이번에도 user의 값을 다른 값으로 덮어써 보자.

user = null;

1번 예시와 달리 전역 변수 admin을 통하면 여전히 객체 John에 접근할 수 있기 때문에 John은 메모리에서 삭제되지 않는다.

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

  return {
    father: man,
    mother: woman
  }
}

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

참조 2개를 지워보자.

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

외부로 나가는 참조는 도달 가능한 상태에 영향을 주지 않는다. 외부에서 들어오는 참조만이 도달 가능한 상태에 영향을 준다. 그 결과 아래와 같이 된다.

결론은, 객체들이 연결되어 섬 같은 구조를 만드는데, 이 섬에 도달할 방법이 없는 경우, 섬을 구성하는 객체 전부가 메모리에서 삭제된다.

내부 알고리즘

  • 가비지 컬렉션 기본 알고리즘 : mark-and-sweep
  1. 가비지 컬렉터는 루트(root) 정보를 수집하고 이를 ‘mark(기억)’ 한다.
  2. 루트가 참조하고 있는 모든 객체를 방문하고 이것들을 ‘mark’ 한다.
  3. mark 된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark 한다. 한번 방문한 객체는 전부 mark 하기 때문에 같은 객체를 다시 방문하는 일은 없다.
  4. 루트에서 도달 가능한 모든 객체를 방문할 때까지 위 과정을 반복한다.
  5. mark 되지 않은 모든 객체를 메모리에서 삭제한다.

최적화 기법

자바스크립트 엔진은 실행에 영향을 미치지 않으면서 가비지 컬렉션을 더 빠르게 하는 다양한 최적화 기법을 적용한다.

자세한 내용은 문서 참조!

🌝 메서드와 this

메서드 만들기

객체의 프로퍼티에 함수를 할당해 객체에게 행동할 수 있는 능력을 부여해줄 수 있다. 그렇게 객체 프로퍼티에 할당된 함수를 메서드(method)라고 한다.

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

user.sayHi = function() {
  alert("안녕하세요!");
};

user.sayHi(); // 안녕하세요!

이미 정의된 함수를 이용해서 메서드로 만들어 줄 수도 있다.

let user = {
  // ...
};

// 함수 선언
function sayHi() {
  alert("안녕하세요!");
};

// 선언된 함수를 메서드로 등록
user.sayHi = sayHi;

user.sayHi(); // 안녕하세요!

메서드 단축 구문

// 아래 두 객체는 동일하게 동작합니다.

user = {
  sayHi: function() {
    alert("Hello");
  }
};

// 단축 구문을 사용하니 더 깔끔해 보이네요.
user = {
  sayHi() { // "sayHi: function()"과 동일합니다.
    alert("Hello");
  }
};

사실 일반적인 방법과 단축 구문을 사용한 방법이 완전히 동일하진 않고 미묘한 차이가 있는데, 우선은 중요하지 않으므로 넘어가자.

메서드와 this

메서드 내부에서 this 키워드를 사용하면 현재 객체에 접근할 수 있다. this는 메서드를 호출할 때 사용된 객체를 나타낸다.

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

  sayHi() {
    // 'this'는 '현재 객체'를 나타냅니다.
    alert(this.name);
  }

};

user.sayHi(); // John

user.sayHi()가 실행되는 동안에 thisuser를 나타낸다.

자세한 내용은 문서를 참고하자.

JS의 자유로운 this

자바스크립트의 this는 다른 프로그래밍 언어의 this와 달리 모든 함수에 this를 사용할 수 있다.

아래와 같이 작성해도 에러가 발생하지 않는다.

function sayHi() {
  alert( this.name );
}

this 값은 런타임에 결정된다. 즉, 컨텍스트에 따라 달라진다.

동일한 함수라도 다른 객체에서 호출했다면 'this’가 참조하는 값이 달라진다.

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
  alert( this.name );
}

// 별개의 객체에서 동일한 함수를 사용함
user.f = sayHi;
admin.f = sayHi;

// 'this'는 '점(.) 앞의' 객체를 참조하기 때문에
// this 값이 달라짐
user.f(); // John  (this == user)
admin.f(); // Admin  (this == admin)

admin['f'](); // Admin (점과 대괄호는 동일하게 동작함)

객체 없이 호출하고 this 값 확인하기

function sayHi() {
  alert(this);
}

sayHi(); // undefined

엄격모드에서는 객체가 없으므로 thisundefined가 할당된다. (런타임에서 할당될 객체가 없기 때문) 그리고 this.name으로 name에 접근하려고 하면 에러가 발생한다.

헷갈리지 말자! 객체가 없을 경우에 해당되는 얘기다. 프로퍼티가 아니라 !!
예시를 보자.

// 클래스와 모듈을 사용하지 않아 엄격모드가 자동으로 적용되지 않기 때문에 직접 적용함
"use strict"; 

let imObject = {
  callThisObejct() {
    alert(this);
  },
  generateProperty() {
    this.a = "나는 this와 함께 생성된 a야.";
    alert(this.a);
  },
};

//1
imObject.callThisObejct(); // 결과 : [object Object]
//2
imObject.generateProperty(); // 결과 : 나는 this와 함께 생성된 a야.

여기서 imObject라는 객체는 존재한다. 따라서 1번 결과에서 확인할 수 있듯 thisundefined가 할당되지 않는다.
2번의 경우 a라는 프로퍼티는 존재하지 않는다. 하지만 이것은 위에서 말한 내용과 전혀 관계가 없다. thisa라는 프로퍼티를 자동으로 생성하고, 정상적으로 값도 출력한다.

다시 한번 헷갈리지 말자. 객체가 존재하지 않을 경우thisundefined가 할당되는 것이다.

function func() {
  alert(this);
}

func(); // 결과 : undefined

음? 근데 함수도 객체아닌가 이게 뭔소리지........???????엥??? 함수가 존재한다는 건 객체가 존재한단 뜻인데....... 그 객체가 그 객체가 아닌가? 어쩌라는거임?
👉 아무래도 프로퍼티가 존재하는.. 고러한 전형적인(?) 객체를 의미하는듯.

정리해보자면,

  • 함수를 선언할 때 this를 사용할 수 있다. 다만 함수가 호출되기 전까지 this엔 값이 할당되지 않는다. 즉, 함수가 호출되는 순간에 this에 값이 할당된다.
  • 함수를 복사해 객체 간 전달할 수 있다.
  • 함수를 객체 프로퍼티에 저장해 객체명.method()와 같이 메서드 형태로 호출하면 this는 그때 해당 객체명에 해당되는 객체를 참조한다.

🌝 new 연산자와 생성자 함수

new 연산자와 생성자 함수를 사용하면 유사한 객체 여러 개를 쉽게 만들 수 있다.

생성자 함수는 처음 나오는 개념이다.. 함수 파트 글 찾아보지마...

생성자 함수

생성자 함수와 일반 함수에 기술적인 차이는 없지만, 생성자 함수는 2가지의 컨벤션이 있다.

  1. 함수 이름의 첫 글자는 대문자로!
  2. 반드시 new 연산자를 붙여 실행!

예시를 보자.

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("보라");

alert(user.name); // 보라
alert(user.isAdmin); // false

new User(...)를 써서 함수를 실행할 때 어떤 알고리즘이 동작하는지 알아보자.

  1. User() 함수 안에 빈 객체를 만들어 this에 할당한다.
  2. User() 함수 본문을 실행한다. this에 새롭게 할당된 프로퍼티를 추가하여 this를 수정한다.
  3. this를 반환한다.

User() 함수 안에서 일어나는 일을 아래의 예시를 통해 보자.

function User(name) {
  // this = {};  1. 빈 객체가 암시적으로 만들어짐

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

  // return this;  3. this가 암시적으로 반환됨
}

왜 생성자 함수를 사용하면 객체를 쉽게 만들 수 있다는 것일까?
아래의 예시를 통해 알아보자.

  1. 생성자 함수로 객체 생성
let user = new User("보라");
  1. 객체 리터럴 문법으로 객체 생성
let user = {
  name: "보라",
  isAdmin: false
}

new User("보라")이외에도 new User("호진"), new User("지민") 등을 이용하면 손쉽게 사용자 객체를 만들 수 있다. 객체 리터럴 문법으로 일일이 객체를 만드는 방법보다 훨씬 간단하고 읽기 쉽게 객체를 만들 수 있다.

생성자의 의의는 바로 여기에 있다. 재사용할 수 있는 객체 생성 코드를 구현하는 것이다.

익명 생성자 함수

재사용할 필요가 없는데 복잡한 객체를 만들어야 할 때, 코드를 익명 생성자 함수로 감싸주는 방식을 사용할 수 있다.

let user = new function() {
  this.name = "John";
  this.isAdmin = false;

  // 사용자 객체를 만들기 위한 여러 코드.
  // 지역 변수, 복잡한 로직, 구문 등의
  // 다양한 코드가 여기에 들어갑니다.
};

위 생성자 함수는 익명 함수이기 때문에 어디에도 저장되지 않는다. 처음 만들 때부터 단 한 번만 호출할 목적으로 만들었기 때문에 재사용이 불가능하다.

이렇게 익명 생성자 함수를 이용하면 재사용은 막으면서 코드를 캡슐화 할 수 있다.

new.target과 생성자 함수

new.target 프로퍼티를 사용하면 함수가 new와 함께 호출되었는지 아닌지를 알 수 있다.

new와 함께 호출하지 않고 일반적인 방법으로 함수를 호출하면 new.targetundefined를 반환하고, new과 함께 호출한 경우에는 함수 자체를 반환해준다.

function User() {
  alert(new.target);
}

// 'new' 없이 호출함
User(); // undefined

// 'new'를 붙여 호출함
new User(); // function User { ... }

new.target 문법은 자주 쓰이지 않는다. 자세한 내용은 문서를 참고하자.

생성자와 return문

생성자 함수에는 보통 return문이 없다. 반환해야 할 것들은 모두 this에 저장되고, this는 자동으로 반환도기 때문에 반환문을 명시적으로 써 줄 필요가 없다.

그러나 만약 return문이 있다면, 아래의 규칙이 적용된다.

  1. 객체를 return한다면 this가 반환되는 대신 해당 객체가 반환된다.
  2. 원시형을 return하거나 return 예약어만 쓰고 아무것도 반환하지 않는다면 return문이 무시된다.

1번 규칙이 적용되는 예시

function BigUser() {

  this.name = "원숭이";

  return { name: "고릴라" };  // <-- this가 아닌 새로운 객체를 반환함
}

alert( new BigUser().name );  // 고릴라

2번 규칙이 적용되는 예시

function SmallUser() {

  this.name = "원숭이";

  return; // <-- this를 반환함
}

alert( new SmallUser().name );  // 원숭이

사실 return문이 있는 생성자 함수는 거의 없다. 특이 케이스이기 때문에 넘어가도 좋다.

생성자 내 메서드

생성자 함수를 사용하면 매개변수를 이용해 객체 내부를 자유롭게 구성할 수 있다. this에 프로퍼티를 더해주는 것 말고도, 메서드를 더해주는 것도 가능하다.

예시를 보자.

function User(name) {
  this.name = name;

  this.sayHi = function() {
    alert( "제 이름은 " + this.name + "입니다." );
  };
}

let bora = new User("이보라");
bora.sayHi(); // 제 이름은 이보라입니다. 

맨 아래 두 줄의 코드는 아래의 코드와 동일하게 동작한다.

bora = {
   name: "이보라",
   sayHi: function() { ... }
}

🌝 옵셔널 체이닝 '?.'

옵셔널 체이닝(optional chaining) ?.을 사용하면 프로퍼티가 없는 중첩 객체를 에러 없이 안전하게 접근할 수 있다.

옵셔널 체이닝이 필요한 이유

  1. 사용자가 여러 명 있는데 그중 몇 명은 주소 정보를 가지고 있지 않다고 가정해보자. 이럴 때 user.address.street를 사용해 주소 정보에 접근하면 에러가 발생할 수 있다. 이렇게 에러가 발생하지 않아도 되는 상황에서 발생하는 에러를 막기 위해 옵셔널 체이닝이 필요하다.
let user = {}; // 주소 정보가 없는 사용자

alert(user.address.street); // TypeError: Cannot read property 'street' of undefined
  1. 또 다른 사례론 브라우저에서 동작하는 코드를 개발할 때 발생할 수 있는 문제가 있다. 자바스크립트를 사용해 페이지에 존재하지 않는 요소에 접근해 요소의 정보를 가져오려 하면 문제가 발생하는데, 이런 에러를 막기 위해 옵셔널 체이닝이 필요하다.
// querySelector(...) 호출 결과가 null인 경우 에러 발생
let html = document.querySelector('.my-element').innerHTML;

옵셔널 체이닝의 반환값

?.?.의 평가 대상이 undefinednull이면 평가를 멈추고 undefined를 반환한다.

예시를 보자.

let user = {}; // 주소 정보가 없는 사용자

alert( user?.address?.street ); // undefined, 에러가 발생하지 않습니다.

user?.address로 주소를 읽으면 아래와 같이 user 객체가 존재하지 않더라도 에러가 발생하지 않는다.

let user = null;

alert( user?.address ); // undefined
alert( user?.address.street ); // undefined

위 예시를 통해 우리는 ?.은 ?. ‘앞’ 평가 대상에만 동작되고, 확장은 되지 않는다는 사실을 알 수 있다.

참고로 위 예시에서 사용된 user?.usernull이나 undefined인 경우만 처리할 수 있다. usernull이나 undefined가 아니고 실제 값이 존재하는 경우엔 반드시 user.address 프로퍼티는 있어야 합니다. 그렇지 않으면 user?.address.street의 두 번째 점 연산자에서 에러가 발생한다.

옵셔널 체이닝을 사용할 때 주의점

  1. 옵셔널 체이닝을 남용하지 말 것!

?.존재하지 않아도 괜찮은 대상에만 사용해야 한다.

사용자 주소를 다루는 위 예시에서 논리상 user는 반드시 있어야 하는데 address필수값이 아니다. 그러니 옵셔널 체이닝 user.address?.street를 사용하는 것이 바람직합니다.

실수로 인해 user에 값을 할당하지 않았다면 바로 알아낼 수 있도록 해야 한다(즉 옵셔널 체이닝을 쓰면 안된다). 그렇지 않으면 에러를 조기에 발견하지 못하고 디버깅이 어려워진다.

  1. ?.앞의 변수는 꼭 선언되어 있어야 한다.

변수 user가 선언되어있지 않으면 user?.anything 평가시 에러가 발생한다.

// ReferenceError: user is not defined
user?.address;

이렇게 옵셔널 체이닝은 선언이 완료된 변수를 대상으로만 동작한다.

단락 평가

?.는 왼쪽 평가대상에 값이 없으면(null이나 undefined이면) 즉시 평가를 멈춘다. 참고로 이런 평가 방법을 단락 평가(short-circuit)라고 부른다.

let user = null;
let x = 0;

user?.sayHi(x++); // 아무 일도 일어나지 않습니다.

alert(x); // 0, x는 증가하지 않습니다.

?.()와 ?.[]

?.은 연산자가 아니다. ?.은 함수나 대괄호와 함께 동작하는 특별한 문법 구조체(syntax construct)이다.

함수 관련 예시와 함께 존재 여부가 확실치 않은 함수를 호출할 때 ?.()를 어떻게 쓸 수 있는지 알아보자.

let user1 = {
  admin() {
    alert("관리자 계정입니다.");
  }
}

let user2 = {};

user1.admin?.(); // 관리자 계정입니다.
user2.admin?.();

두 상황 모두에서 user 객체는 존재하기 때문에 admin 프로퍼티는 .만 사용해 접근했다.

그리고 난 후 ?.()를 사용해 admin의 존재 여부를 확인했다. user1엔 admin이 정의되어 있기 때문에 메서드가 제대로 호출되는 반면, user2엔 admin이 정의되어 있지 않았음에도 불구하고 메서드를 호출하면 에러 없이 그냥 평가가 멈추는 것을 확인할 수 있다.

한편, ?.[]를 사용할 수도 있다. 예시를 보자.

let user1 = {
  firstName: "Violet"
};

let user2 = null; // user2는 권한이 없는 사용자라고 가정해봅시다.

let key = "firstName";

alert( user1?.[key] ); // Violet
alert( user2?.[key] ); // undefined

alert( user1?.[key]?.something?.not?.existing); // undefined

?.과 delete

?.delete와 조합해 사용할 수도 있다.

delete user?.name; // user가 존재하면 user.name을 삭제합니다.

?.과 할당 연산자

?.은 할당 연산자 왼쪽에서 사용할 수 없다.

// user가 존재할 경우 user.name에 값을 쓰려는 의도로 아래와 같이 코드를 작성해 보았습니다.

user?.name = "Violet"; // SyntaxError: Invalid left-hand side in assignment
// 에러가 발생하는 이유는 undefined = "Violet"이 되기 때문입니다.

요약

옵셔널 체이닝 문법 ?.은 세 가지 형태로 사용할 수 있다.

  1. obj?.prop : obj가 존재하면 obj.prop을 반환하고, 그렇지 않으면 undefined를 반환함
  2. obj?.[prop] : obj가 존재하면 obj[prop]을 반환하고, 그렇지 않으면 undefined를 반환함
  3. obj?.method() : obj가 존재하면 obj.method()를 호출하고, 그렇지 않으면 undefined를 반환함

?.?.의 왼쪽 평가대상이 없어도 괜찮은 경우에만 선택적으로 사용해야 한다. 꼭 있어야 하는 값인데 없는 경우에 ?.을 사용하면 프로그래밍 에러를 쉽게 찾을 수 없으므로 이런 상황을 만들지 말도록 하자.

🌝 심볼형

자바스크립트는 객체 프로퍼티 키로 오직 문자형과 심볼형만을 허용한다.

지금까지는 프로퍼티 키가 문자형인 경우만 살펴봤다. 이번에는 프로퍼티 키로 심볼값을 사용해 보면서, 심볼형 키를 사용할 때의 이점에 대해 살펴보자.

심볼

심볼(symbol)은 원시형 데이터로, 유일한 식별자(unique identifier)를 만들고 싶을 때 사용한다. Symbol()을 사용하면 심볼값을 만들 수 있다.

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

심볼을 만들 때 심볼 이름이라 불리는 '설명'을 선택적으로 추가할 수 있다. 심볼 이름은 디버깅 시 아주 유용하다.

설명이 같아도 심볼은 유일한 식별자이기 때문에, 동일 연산자(==)로 비교 시 false가 반환된다. 즉 이름이 같더라도 값이 항상 다른 것이다.

let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

symbol.description 프로퍼티를 이용하면 설명을 보여줄 수 있다.

let id = Symbol("id");
alert(id.description); // id

심볼은 문자형으로 자동 형 변환되지 않는다

심볼형 값은 다른 자료형으로 암시적 형 변환(자동 형 변환)되지 않는다. 문자열과 심볼은 근본이 다르기 때문에 우연히라도 서로의 타입으로 변환돼선 안된다.

let id = Symbol("id");
alert(id); // TypeError: Cannot convert a Symbol value to a string

심볼을 반드시 출력해줘야 하는 상황이라면 아래와 같이 .toString() 메서드를 명시적으로 호출해주면 된다.

let id = Symbol("id");
alert(id.toString()); // Symbol(id)가 얼럿 창에 출력됨

'숨김(hidden)' 프로퍼티

심볼을 이용하면 ‘숨김(hidden)’ 프로퍼티를 만들 수 있다. 숨김 프로퍼티는 외부 코드에서 접근이 불가능하고 값도 덮어쓸 수 없는 프로퍼티이다.

  1. third party 코드에서 가지고 온 user라는 객체를 를 이용해 어떤 작업을 해야 하는 상황이라고 가정해보자.
let user = { // 서드파티 코드에서 가져온 객체
  name: "John"
};

let id = Symbol("id");

user[id] = 1;

alert( user[id] ); // 심볼을 키로 사용해 데이터에 접근할 수 있습니다.

왜 문자형 프로퍼티가 아니라 심볼형 프로퍼티를 사용했을까?

user는 third party 코드에서 가지고 온 객체이므로 함부로 새로운 프로퍼티를 추가할 수 없다. 그런데 심볼은 third party 코드에서 접근할 수 없기 때문에, 심볼을 사용하면 third party 코드가 모르게 user에 프로퍼티를 추가할 수 있다(식별자 부여).

  1. 이 외에도 각각 user를 다르게 식별해야 하는 상황에도 심볼을 이용할 수 있다. user의 원천인 third party 코드, 현재 작성 중인 스크립트, 제3의 스크립트(자바스크립트 라이브러리 등)가 각자 서로의 코드도 모른 채 user를 식별해야 하는 상황이 발생했다고 해보자.
    제3의 스크립트에선 아래와 같이 Symbol("id")을 이용해 전용 식별자를 만들어 사용할 수 있다.
// ...
let id = Symbol("id");

user[id] = "제3 스크립트 id 값";

심볼은 유일성이 보장되므로 우리가 만든 식별자와 제3의 스크립트에서 만든 식별자가 충돌하지 않는다. 이름이 같더라도 충돌하지 않는다.

그럼 심볼 대신 문자형을 사용했다면 어떻게 될까? 충돌이 발생할 수 있다.

let user = { name: "John" };

// 문자열 "id"를 사용해 식별자를 만들었습니다.
user.id = "스크립트 id 값";

// 만약 제3의 스크립트가 우리 스크립트와 동일하게 문자열 "id"를 이용해 식별자를 만들었다면...

user.id = "제3 스크립트 id 값"
// 의도치 않게 값이 덮어 쓰여서 우리가 만든 식별자는 무의미해집니다.

객체 리터럴 안의 심볼

객체 리터럴 {...}을 사용해 객체를 만들고 심볼형 프로퍼티를 넣어줄 경우, 대괄호를 사용해 심볼형 키를 만들어야 한다.

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // "id": 123은 안됨
};

for...in에서 배제되는 심볼형

키가 심볼인 프로퍼티는 for..in 반복문에서 배제된다.

let id = Symbol("id");
let user = {
  name: "John",
  age: 30,
  [id]: 123
};

for (let key in user) alert(key); // name과 age만 출력되고, 심볼은 출력되지 않습니다.

// 심볼로 직접 접근하면 잘 작동합니다.
alert( "직접 접근한 값: " + user[id] );

Object.keys(user)에서도 키가 심볼인 프로퍼티는 배제됩니다. 심볼형 프로퍼티 숨기기(hiding symbolic property)라 불리는 이런 원칙 덕분에 외부 스크립트나 라이브러리는 심볼형 키를 가진 프로퍼티에 접근하지 못한다.

그런데 Object.assign은 키가 심볼인 프로퍼티를 배제하지 않고 객체 내 모든 프로퍼티를 복사한다.

let id = Symbol("id");
let user = {
  [id]: 123
};

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

alert( clone[id] ); // 123

객체를 복사하거나 병합할 때, 대개 id 같은 심볼을 포함한 프로퍼티 전부를 사용하고 싶어 할 것이라는 생각에서 설계되었다.

전역 심볼

심볼은 이름이 같더라도 값이 항상 다르다(모두 별개로 취급된다). 그런데 이름이 같을 때 값도 같길 원한다면, 즉 이름이 같은 심볼이 같은 개체를 가리키길 원한다면, 전역 심볼 레지스트리(global symbol registry)를 사용해야 한다.

전역 심볼 레지스트리 안에 심볼을 만들고 해당 심볼에 접근하면, 이름이 같은 경우 항상 동일한 심볼을 반환해준다.

레지스트리 안에 있는 심볼을 읽거나, 새로운 심볼을 생성하려면 Symbol.for(key)를 사용하면 된다. 이 메서드를 호출하면 이름이 key인 심볼을 반환한다. 조건에 맞는 심볼이 레지스트리 안에 없으면 새로운 심볼 Symbol(key)을 만들고 레지스트리 안에 저장한다.

// 전역 레지스트리에서 심볼을 읽습니다.
let id = Symbol.for("id"); // 심볼이 존재하지 않으면 새로운 심볼을 만듭니다.

// 동일한 이름을 이용해 심볼을 다시 읽습니다(좀 더 멀리 떨어진 코드에서도 가능합니다).
let idAgain = Symbol.for("id");

// 두 심볼은 같습니다.
alert( id === idAgain ); // true

전역 심볼 레지스트리 안에 있는 심볼은 전역 심볼이라고 불린다. 애플리케이션에서 광범위하게 사용해야 하는 심볼이라면 전역 심볼을 사용하자!

전역 심볼을 찾을 때 사용되는 Symbol.for(key)에 반대되는 메서드도 있다. Symbol.keyFor(sym)를 사용하면 이름을 얻을 수 있다.

// 이름을 이용해 심볼을 찾음
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// 심볼을 이용해 이름을 얻음
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id

Symbol.keyFor는 전역 심볼 레지스트리를 뒤져서 해당 심볼의 이름을 얻어낸다. 검색 범위가 전역 심볼 레지스트리이기 때문에 전역 심볼이 아닌 심볼에는 사용할 수 없다. 전역 심볼이 아닌 인자가 넘어오면 Symbol.keyForundefined를 반환한다.

전역 심볼이 아닌 모든 심볼은 description 프로퍼티가 있다. 일반 심볼에서 이름을 얻고 싶으면 description 프로퍼티를 사용하면 된다.

let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");

alert( Symbol.keyFor(globalSymbol) ); // name, 전역 심볼
alert( Symbol.keyFor(localSymbol) ); // undefined, 전역 심볼이 아님

alert( localSymbol.description ); // name

🌝 객체를 원시형으로 변환하기

형 변환 챕터에선 객체의 형 변환은 다루지 않았습니다. 메서드와 심볼에 대한 지식을 갖추었으니 본격적으로 객체의 형 변환에 대해 알아보자.

  1. 객체에 대한 논리평가
    객체는 논리 평가 시 예외없이 항상 true를 반환합니다. 따라서 객체는 숫자형이나 문자형으로만 형 변환이 일어난다고 생각하면 된다.

  2. 숫자형으로의 형 변환
    객체끼리 빼는 연산을 할 때나 수학 관련 함수를 적용할 때 일어난다.

  3. 문자형으로의 형 변환
    문자형으로의 형 변환은 대개 alert(obj)같이 객체를 출력하려고 할 때 일어난다.

ToPrimitive

객체 형 변환은 세 종류로 구분되는데, hint라 불리는 값이 구분 기준이된다. hint목표로 하는 자료형 정도로 이해하면 된다.

  1. string
    alert 함수같이 문자열을 기대하0는 연산을 수행할 때는(객체-문자형 변환), hintstring이 된다.
// 객체를 출력하려고 함
alert(obj);

// 객체를 프로퍼티 키로 사용하고 있음
anotherObj[obj] = 123;
  1. number
    수학 연산을 적용하려 할 때(객체-숫자형 변환), hintnumber가 된다.
// 명시적 형 변환
let num = Number(obj);

// (이항 덧셈 연산을 제외한) 수학 연산
let n = +obj; // 단항 덧셈 연산
let delta = date1 - date2;

// 크고 작음 비교하기
let greater = user1 > user2;
  1. default
    아주 드물게 발생하지만 연산자가 기대하는 자료형이 ‘확실치 않을 때’ hintdefault가 된다.

이항 덧셈 연산자 +는 피연산자의 자료형에 따라 문자열을 합치는 연산을 할 수도 있고 숫자를 더해주는 연산을 할 수도 있다. 따라서 +의 인수가 객체일 때는 hintdefault가 된다.

동등 연산자 ==를 사용해 객체-문자형, 객체-숫자형, 객체-심볼형끼리 비교할 때도, 객체를 어떤 자료형으로 바꿔야 할지 확신이 안 서므로 hintdefault가 된다.

// 이항 덧셈 연산은 hint로 `default`를 사용합니다.
let total = obj1 + obj2;

// obj == number 연산은 hint로 `default`를 사용합니다.
if (user == 1) { ... };

크고 작음을 비교할 때 쓰이는 연산자 <, > 역시 피연산자에 문자형과 숫자형 둘 다를 허용하는데, 이 연산자들은 hintnumber로 고정하여 hintdefault가 되는 일이 없다. 이는 하위 호환성 때문에 정해진 규칙이다.

하지만 실제 일을 할 때는 이런 사항을 모두 외울 필요는 없고, Date 객체를 제외한 모든 내장 객체는 hint가 "default인 경우와 number인 경우를 동일하게 처리한다. 우리도 커스텀 객체를 만들 땐 이런 규칙을 따르면 된다.

"boolean" hint?

‘boolean’ hint는 존재하지 않는다. 모든 객체는 그냥 true로 평가된다.

자바스크립트의 형 변환 알고리즘에 따른 세가지 메서드

  1. 객체에 obj[Symbol.toPrimitive](hint)메서드가 있는지 찾고, 있다면 메서드를 호출한다. Symbol.toPrimitive는 시스템 심볼로, 심볼형 키로 사용된다.

  2. 1에 해당하지 않고 hint가 string이라면, obj.toString()이나 obj.valueOf()를 호출합니다(존재하는 메서드만 실행됨).

  3. 1과 2에 해당하지 않고, hint가 numberdefault라면
    obj.valueOf()obj.toString()을 호출한다(존재하는 메서드만 실행됨).

Symbol.toPrimitive

첫 번째 메서드부터 살펴보자. 자바스크립트엔 Symbol.toPrimitive라는 내장 심볼이 존재하는데, 이 심볼은 아래와 같이 목표로 하는 자료형(hint)을 명명하는 데 사용된다.

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

실제 예시를 살펴보자.

let user = {
  name: "John",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    alert(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};

// 데모:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500

이렇게 메서드를 구현해 놓으면 user는 hint에 따라 문자열로 변환되기도 하고 숫자로 변환되기도 한다. user[Symbol.toPrimitive]를 사용하면 메서드 하나로 모든 종류의 형 변환을 다룰 수 있다.

toString과 ValueOf

toStringValueOF 메서드를 이용하면 '구식’이긴 하지만 형 변환을 직접 구현할 수 있다.

객체에 Symbol.toPrimitive가 없으면 자바스크립트는 아래 규칙에 따라 toString이나 valueOf를 호출한다.

  • hint가 'string’인 경우: toString -> valueOf 순. toString이 있다면 toString을 호출, toString이 없다면 valueOf를 호출한다.
  • 그 외: valueOf -> toString 순

이 메서드들은 반드시 원시값을 반환해야한다. toString이나 valueOf가 객체를 반환하면 그 결과는 무시된다. toString이나 valueOf가 객체를 반환해도 에러가 발생하지는 않는다. 반면에 Symbol.toPrimitive는 무조건 원시자료를 반환해야 한다. 그렇지 않으면 에러가 발생한다.

일반 객체는 기본적으로 toString과 valueOf에 적용되는 다음 규칙을 따른다.

  • toString은 문자열 "[object Object]"을 반환한다.
  • valueOf는 객체 자신을 반환한다.
let user = {name: "John"};

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

Symbol.toPrimitive를 사용한 위쪽 예시와 동일하게 동작하는 예시를 보자.

let user = {
  name: "John",
  money: 1000,

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

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

};

alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500

객체에 Symbol.toPrimitivevalueOf가 없으면, toString이 모든 형 변환을 처리한다.

let user = {
  name: "John",

  toString() {
    return this.name;
  }
};

alert(user); // toString -> John
alert(user + 500); // toString -> John500

3개 메서드의 반환 타입에 대해 자세한 내용은 공식 문서를 참고하자.

추가 형 변환

객체가 피연산자일 때는 다음과 같은 단계를 거쳐 형 변환이 일어난다.

  1. 객체는 원시형으로 변환된다.
  2. 변환 후 원시값이 원하는 형이 아닌 경우엔 또다시 형 변환이 일어난다.
let obj = {
  // 다른 메서드가 없으면 toString에서 모든 형 변환을 처리합니다.
  toString() {
    return "2";
  }
};

alert(obj * 2); // 4, 객체가 문자열 "2"로 바뀌고, 곱셈 연산 과정에서 문자열 "2"는 숫자 2로 변경됩니다.
  1. obj * 2에선 객체가 원시형으로 변화되므로 toString에의해 obj는 문자열 "2"가 됩니다.
  2. 곱셈 연산은 문자열은 숫자형으로 변환시키므로 "2" 2는 2 2가 됩니다.

그런데 이항 덧셈 연산은 위와 같은 상황에서 문자열을 연결한다.

let obj = {
  toString() {
    return "2";
  }
};

alert(obj + 2); // 22("2" + 2), 문자열이 반환되기 때문에 문자열끼리의 병합이 일어났습니다.
profile
ꜱɪɴᴄᴇ 2021.09.01

0개의 댓글