요약
객체는 몇 가지 특수한 기능을 가진 연관 배열(associative array)입니다.객체는 프로퍼티(키-값 쌍)를 저장합니다.
- 프로퍼티 키는 문자열이나 심볼이어야 합니다. 보통은 문자열입니다.
- 값은 어떤 자료형도 가능합니다.
아래와 같은 방법을 사용하면 프로퍼티에 접근할 수 있습니다.
- 점 표기법:
obj.property
- 대괄호 표기법
obj["property"]
. 대괄호 표기법을 사용하면obj[varWithKey]
같이 변수에서 키를 가져올 수 있습니다.객체엔 다음과 같은 추가 연산자를 사용할 수 있습니다.
- 프로퍼티를 삭제하고 싶을 때:
delete obj.prop
- 해당 key를 가진 프로퍼티가 객체 내에 있는지 확인하고자 할 때:
"key" in obj
- 프로퍼티를 나열할 때:
for (let key in obj)
지금까진 '순수 객체(plain object)'라 불리는 일반객체
에 대해 학습했습니다.자바스크립트에는 일반 객체 이외에도 다양한 종류의 객체가 있습니다.
Array
– 정렬된 데이터 컬렉션을 저장할 때 쓰임Date
– 날짜와 시간 정보를 저장할 때 쓰임Error
– 에러 정보를 저장할 때 쓰임- 기타 등등
키(key): 값(value)
쌍으로 구성된 프로퍼티(property) 를 여러 개 넣을 수 있는데, 키엔 문자형, 값엔 모든 자료형이 허용됨let user = new Object(); // '객체 생성자' 문법
let user = {}; // '객체 리터럴' 문법
let user = { // 객체
name: "John", // 키: "name", 값: "John"
age: 30 // 키: "age", 값: 30
"likes birds": true // 복수의 단어는 따옴표로 묶음
};
// 프로퍼티 값 얻기 - 점 표기법(dot notation)
alert( user.name ); // John
alert( user.age ); // 30
// 프로퍼티 추가
user.isAdmin = true;
// 프로퍼티 삭제
delete user.age;
🚨
const
로 선언된 객체는 수정될 수 있음
🚨 여러 단어를 조합해 프로퍼티 키를 만든 경우엔, 점 표기법을 사용해 프로퍼티 값을 읽을 수 없음
대괄호 표기법으로 대체 가능
["something new"]
// 에러
user.likes birds = true
// set
user["likes birds"] = true;
// get
alert(user["likes birds"]); // true
// delete
delete user["likes birds"];
동적 key
를 할당 가능하게 됨let user = {
name: "John",
age: 30
};
let key = prompt("사용자의 어떤 정보를 얻고 싶으신가요?", "name");
// 변수로 접근
alert( user[key] ); // John (프롬프트 창에 "name"을 입력한 경우)
let user = {
name: "John",
age: 30
};
let key = "name";
alert( user.key ) // undefined
let fruit = prompt("어떤 과일을 구매하시겠습니까?", "apple");
let bag = {};
// 변수 fruit을 사용해 프로퍼티 이름을 만들었습니다.
bag[fruit] = 5;
이름이 확정된 상황이고, 단순한 이름이라면 처음엔 점 표기법을 사용하다가 뭔가 복잡한 상황이 발생했을 때 대괄호 표기법으로 바꾸는 경우가 많음
프로퍼티 이름과 값이 변수의 이름과 동일할 경우
수정 전
function makeUser(name, age) {
return {
name: name,
age: age,
};
}
let user = makeUser("John", 30);
alert(user.name); // John
function makeUser(name, age) {
return {
name,
age,
};
}
let user = makeUser("John", 30);
alert(user.name); // John
__proto__
제외)let obj = {
0: "test" // "0": "test"와 동일
};
// 숫자 0은 문자열 "0"으로 변환되기 때문에 두 얼럿 창은 같은 프로퍼티에 접근
alert( obj["0"] ); // test
alert( obj[0] ); // test (동일한 프로퍼티)
"key" in object
를 통해 프로퍼티 존재 여부 확인 가능let user = { name: "John", age: 30 };
alert( "age" in user ); // user.age가 존재하므로 true가 출력
alert( "blabla" in user ); // user.blabla는 존재하지 않기 때문에 false가 출력
for..in
반복문을 사용하면 객체의 모든 키를 순회for (key in object) {
// 각 프로퍼티 키(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
}
요약
객체는 참조에 의해 할당되고 복사됩니다. 변수엔 ‘객체’ 자체가 아닌 메모리상의 주소인 '참조’가 저장됩니다. 따라서 객체가 할당된 변수를 복사하거나 함수의 인자로 넘길 땐 객체가 아닌 객체의 참조가 복사됩니다.
그리고 복사된 참조를 이용한 모든 작업(프로퍼티 추가·삭제 등)은 동일한 객체를 대상으로 이뤄집니다.
객체의 '진짜 복사본’을 만들려면 '얕은 복사(shallow copy)'를 가능하게 해주는Object.assign
이나 '깊은 복사’를 가능하게 해주는_.cloneDeep(obj)
를 사용하면 됩니다. 이때 얕은 복사본은 중첩 객체를 처리하지 못한다는 점을 기억해 두시기 바랍니다.
let user = { name: "John" };
let admin = user; // 참조값을 복사함
// 변수는 두 개이지만 각 변수엔 동일 객체에 대한 참조 값이 저장
==
와 일치 연산자 ===
는 동일하게 동작함let a = {};
let b = a; // 참조에 의한 복사
alert( a == b ); // true, 두 변수는 같은 객체를 참조
alert( a === b ); // true
let a = {};
let b = {}; // 독립된 두 객체
alert( a == b ); // false
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이 있습니다.
Object.assign(dest, [src1, src2, src3...])
let user = {
name: "John",
age: 30
};
Object.assign(clone, 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
를 공유lodash
의 메서드_.cloneDeep(obj)
사용요약
- 가비지 컬렉션은 엔진이 자동으로 수행하므로 개발자는 이를 억지로 실행하거나 막을 수 없습니다.
- 객체는 도달 가능한 상태일 때 메모리에 남습니다.
- 참조된다고 해서 도달 가능한 것은 아닙니다. 서로 연결된 객체들도 도달 불가능할 수 있습니다.
도달 가능한 값
- 현재 함수의 지역 변수와 매개변수
- 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
- 전역 변수
- 루트가 참조하는 값
- 체이닝으로 루트에서 참조할 수 있는 값
- 기타 등등
mark-and-sweep
가비지 컬렉션 수행 단계
1. 가비지 컬렉터는 루트(root) 정보를 수집하고 이를 ‘mark(기억)’ 합니다.
2. 루트가 참조하고 있는 모든 객체를 방문하고 이것들을 ‘mark’ 합니다.
3. mark 된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark 합니다. 한번 방문한 객체는 전부 mark 하기 때문에 같은 객체를 다시 방문하는 일은 없습니다.
4. 루트에서 도달 가능한 모든 객체를 방문할 때까지 위 과정을 반복합니다.
5. mark 되지 않은 모든 객체를 메모리에서 삭제합니다.
가비지 컬렉션 최적화 기법
- generational collection(세대별 수집) – 객체를 '새로운 객체’와 '오래된 객체’로 나눕니다. 객체 상당수는 생성 이후 제 역할을 빠르게 수행해 금방 쓸모가 없어지는데, 이런 객체를 '새로운 객체’로 구분합니다. 가비지 컬렉터는 이런 객체를 공격적으로 메모리에서 제거합니다. 일정 시간 이상 동안 살아남은 객체는 '오래된 객체’로 분류하고, 가비지 컬렉터가 덜 감시합니다.
- incremental collection(점진적 수집) – 방문해야 할 객체가 많다면 모든 객체를 한 번에 방문하고 mark 하는데 상당한 시간이 소모됩니다. 가비지 컬렉션에 많은 리소스가 사용되어 실행 속도도 눈에 띄게 느려지겠죠. 자바스크립트 엔진은 이런 현상을 개선하기 위해 가비지 컬렉션을 여러 부분으로 분리한 다음, 각 부분을 별도로 수행합니다. 작업을 분리하고, 변경 사항을 추적하는 데 추가 작업이 필요하긴 하지만, 긴 지연을 짧은 지연 여러 개로 분산시킬 수 있다는 장점이 있습니다.
- idle-time collection(유휴 시간 수집) – 가비지 컬렉터는 실행에 주는 영향을 최소화하기 위해 CPU가 유휴 상태일 때에만 가비지 컬렉션을 실행합니다.
요약
객체 프로퍼티에 저장된 함수를 '메서드’라고 부릅니다.
object.doSomthing()은 객체를 '행동’할 수 있게 해줍니다.
메서드는 this로 객체를 참조합니다.
this 값은 런타임에 결정됩니다.함수를 선언할 때 this를 사용할 수 있습니다. 다만, 함수가 호출되기 전까지 this엔 값이 할당되지 않습니다.
함수를 복사해 객체 간 전달할 수 있습니다.
함수를 객체 프로퍼티에 저장해 object.method()같이 ‘메서드’ 형태로 호출하면 this는 object를 참조합니다.
화살표 함수는 자신만의 this를 가지지 않는다는 점에서 독특합니다. 화살표 함수 안에서 this를 사용하면, 외부에서 this 값을 가져옵니다.
- 자바스크립트에선 객체의 프로퍼티에 함수를 할당해 객체에게 행동할 수 있는 능력을 부여할 수 있음
- 객체 프로퍼티에 할당된 함수를 메서드(method) 라고 부름
user
에 할당된 sayHi
가 메서드let user = {
name: "John",
age: 30
};
user.sayHi = function() {
alert("안녕하세요!");
};
user.sayHi(); // 안녕하세요!
객체 지향 프로그래밍
객체를 사용하여 개체를 표현하는 방식을 객체 지향 프로그래밍(object-oriented programming, OOP) 이라 부름
user = {
sayHi: function() {
alert("Hello");
}
};
// 단축구문
user = {
sayHi() { // "sayHi: function()"과 동일
alert("Hello");
}
};
this
키워드를 통해 객체에 접근할 수 있음let user = {
name: "John",
age: 30,
sayHi() {
// 'this'는 '현재 객체'를 나타냄
alert(this.name);
}
};
user.sayHi(); // John
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 (점과 대괄호는 동일하게 동작함)
obj.f()
를 호출했다면 this
는 f
를 호출하는 동안의 obj
obj
가 user
나 admin
을 참조객체 없이 호출하기
- 엄격 모드가 아닐 때:
this == window
(전역 객체)- 엄격 모드일 때:
this == undefined
🚨 자유로운 this가 만드는 결과
다른 언어를 사용하다 자바스크립트로 넘어온 개발자는 this를 혼동하기 쉬움
this는 항상 메서드가 정의된 객체를 참조할 것이라고 착각함
이런 개념을 'bound this'라고 함자바스크립트에서 this는 런타임에 결정됨메서드가 어디서 정의되었는지에 상관없이 this는 ‘점 앞의’ 객체가 무엇인가에 따라 ‘자유롭게’ 결정됨
함수(메서드)를 하나만 만들어 여러 객체에서 재사용할 수 있다는 것은 장점이지만, 이런 유연함이 실수로 이어질 수 있다는 것은 단점
function makeUser() {
return {
name: "John",
ref: this
};
};
let user = makeUser();
alert( user.ref.name ); // 에러
에러가 발생하는 이유는 this 값을 설정할 땐 객체 정의가 사용되지 않기 때문입니다. this 값은 호출 시점에 결정됩니다.
위 코드에서 makeUser() 내 this는 undefined가 됩니다. 메서드로써 호출된 게 아니라 함수로써 호출되었기 때문입니다.
function makeUser() {
return {
name: "John",
ref() {
return this;
}
};
};
let user = makeUser();
alert( user.ref().name ); // John
이렇게 하면 user.ref()가 메서드가 되고 this는 . 앞의 객체가 되기 때문에 에러가 발생하지 않습니다.
let ladder = {
step: 0,
up() {
this.step++;
},
down() {
this.step--;
},
showStep: function() { // 사다리에서 몇 번째 단에 올라와 있는지 보여줌
alert( this.step );
}
};
ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1
up, down, showStep을 수정해 아래처럼 메서드 호출 체이닝이 가능하도록 해봅시다.
ladder.up().up().down().showStep(); // 1
let ladder = {
step: 0,
up() {
this.step++;
return this;
},
down() {
this.step--;
return this;
},
showStep() {
alert( this.step );
return this;
}
}
ladder.up().up().down().up().down().showStep(); // 1
요약
생성자 함수(짧게 줄여서 생성자)는 일반 함수입니다. 다만, 일반 함수와 구분하기 위해 함수 이름 첫 글자를 대문자로 씁니다.
생성자 함수는 반드시 new 연산자와 함께 호출해야 합니다. new와 함께 호출하면 내부에서 this가 암시적으로 만들어지고, 마지막엔 this가 반환됩니다.
유사한 객체를 여러 개 만들 때 생성자 함수가 유용합니다.자바스크립트는 언어 차원에서 다양한 생성자 함수를 제공합니다. 날짜를 나타내는 데 쓰이는 Date, 집합(set)을 나타내는 데 쓰이는 Set 등의 내장 객체는 이런 생성자 함수를 이용해 만들 수 있습니다.
new
연산자와 생성자 함수를 사용하면 유사한 객체 여러 개를 쉽게 만들 수 있음new
연산자를 붙여 실행합니다.function User(name) {
this.name = name;
this.isAdmin = false;
}
let user = new User("Jack");
alert(user.name); // Jack
alert(user.isAdmin); // false
new User(...)
를 써서 함수를 실행하면 아래와 같은 알고리즘이 동작
this
에 새로운 프로퍼티를 추가해 this
를 수정합니다.this
를 반환합니다.function User(name) {
// this = {}; (빈 객체가 암시적으로 만들어짐)
// 새로운 프로퍼티를 this에 추가함
this.name = name;
this.isAdmin = false;
// return this; (this가 암시적으로 반환됨)
}
new function() { … }
재사용할 필요가 없는 복잡한 객체를 만들어야 한다고 해봅시다. 많은 양의 코드가 필요할 겁니다. 이럴 땐 아래와 같이 코드를 익명 생성자 함수로 감싸주는 방식을 사용할 수 있습니다.
let user = new function() { this.name = "John"; this.isAdmin = false; // 사용자 객체를 만들기 위한 여러 코드. // 지역 변수, 복잡한 로직, 구문 등의 // 다양한 코드가 여기에 들어갑니다. };
위 생성자 함수는 익명 함수이기 때문에 어디에도 저장되지 않습니다. 처음 만들 때부터 단 한 번만 호출할 목적으로 만들었기 때문에 재사용이 불가능합니다. 이렇게 익명 생성자 함수를 이용하면 재사용은 막으면서 코드를 캡슐화 할 수 있습니다.
function User() {
alert(new.target);
}
// "new" 없이 호출함
User(); // undefined
//"new"를 붙여 호출함
new User(); // function User { ... }
return
문이 없음this
에 저장되고, this
는 자동으로 반환되기 때문에 반환문을 명시적으로 써 줄 필요가 없음return
문이 있을 경우 (지양)return
한다면, this
대신 객체가 반환됨return
한다면, return
문이 무시됨function User(name) {
this.name = name;
this.sayHi = function() {
alert( "My name is: " + this.name );
};
}
let john = new User("John");
john.sayHi(); // My name is: John
function Calculator() {
this.read = function() {
this.a = +prompt('a?', 0);
this.b = +prompt('b?', 0);
// +를 붙여 숫자형으로 변환
};
this.sum = function() {
return this.a + this.b;
};
this.mul = function() {
return this.a * this.b;
};
}
let calculator = new Calculator();
calculator.read();
alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );
let accumulator = new Accumulator(1); // 최초값: 1
accumulator.read(); // 사용자가 입력한 값을 더해줌
accumulator.read(); // 사용자가 입력한 값을 더해줌
alert(accumulator.value); // 최초값과 사용자가 입력한 모든 값을 더해 출력함
요약
옵셔널 체이닝 문법 ?.은 세 가지 형태로 사용할 수 있습니다.
obj?.prop
–obj
가 존재하면obj.prop
을 반환하고, 그렇지 않으면undefined
를 반환함obj?.[prop]
–obj
가 존재하면obj[prop]
을 반환하고, 그렇지 않으면undefined
를 반환함obj?.method()
–obj
가 존재하면obj.method()
를 호출하고, 그렇지 않으면undefined
를 반환함
여러 예시를 통해 살펴보았듯이 옵셔널 체이닝 문법은 꽤 직관적이고 사용하기도 쉽습니다.?.
왼쪽 평가 대상이null
이나undefined
인지 확인하고null
이나undefined
가 아니라면 평가를 계속 진행합니다.
?.
를 계속 연결해서 체인을 만들면 중첩 프로퍼티들에 안전하게 접근할 수 있습니다.
?.
은?.
왼쪽 평가대상이 없어도 괜찮은 경우에만 선택적으로 사용해야 합니다.
꼭 있어야 하는 값인데 없는 경우에?.
을 사용하면 프로그래밍 에러를 쉽게 찾을 수 없으므로 이런 상황을 만들지 말도록 합시다.
?.
을 사용하면 프로퍼티가 없는 중첩 객체를 에러 없이 안전하게 접근할 수 있음user.address.street
를 사용해 주소 정보에 접근하면 에러가 발생let user = {}; // 주소 정보가 없는 사용자
alert(user.address.street); // TypeError: Cannot read property 'street' of undefined
// querySelector(...) 호출 결과가 null인 경우 에러 발생
let html = document.querySelector('.my-element').innerHTML;
명세서에
?.
이 추가되기 전엔 이런 문제들을 해결하기 위해&&
연산자를 사용
AND를 연결해서 사용하면 코드가 아주 길어진다는 단점let user = {}; // 주소 정보가 없는 사용자 alert( user && user.address && user.address.street ); // undefined, 에러가 발생하지 않습니다.
?.
은 ?.
'앞’의 평가 대상이 undefined
나 null
이면 평가를 멈추고 undefined
를 반환let user = {}; // 주소 정보가 없는 사용자
alert( user?.address?.street ); // undefined, 에러가 발생하지 않습니다.
user?.address
로 주소를 읽으면 아래와 같이 user 객체가 존재하지 않더라도 에러가 발생하지 않음let user = null;
alert( user?.address ); // undefined
alert( user?.address.street ); // undefined
user
가 null
이나 undefined
가 아니고 실제 값이 존재하는 경우엔 반드시 user.address
프로퍼티는 있어야 함.user?.address.street
의 두 번째 점 연산자에서 에러가 발생🚨 옵셔널 체이닝을 남용하지 마세요.
?.
는 존재하지 않아도 괜찮은 대상에만 사용해야 합니다.사용자 주소를 다루는 위 예시에서 논리상 user는 반드시 있어야 하는데
address
는 필수값이 아닙니다. 그러니user.address?.street
를 사용하는 것이 바람직합니다.실수로 인해
user
에 값을 할당하지 않았다면 바로 알아낼 수 있도록 해야 합니다. 그렇지 않으면 에러를 조기에 발견하지 못하고 디버깅이 어려워집니다.
?.
앞의 변수는 꼭 선언되어 있어야 합니다.변수
user
가 선언되어있지 않으면user?.anything
평가시 에러가 발생합니다.// ReferenceError: user is not defined user?.address;
?.
는 왼쪽 평가대상에 값이 없으면 즉시 평가를 멈춤?.
오른쪽에 있는 부가 동작은 ?.
의 평가가 멈췄을 때 더는 일어나지 않음let user1 = {
admin() {
alert("관리자 계정입니다.");
}
}
let user2 = {};
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 user?.name; // user가 존재할 경우 name을 삭제
?.
은 읽기나 삭제하기에는 사용할 수 있지만 쓰기에는 사용할 수 없습니다.
- ?.은 할당 연산자 왼쪽에서 사용할 수 없음
// user가 존재할 경우 user.name에 값을 쓰려는 의도 user?.name = "Violet"; // SyntaxError: Invalid left-hand side in assignment // undefined = "Violet" => 에러
요약
Symbol은 원시형 데이터로, 유일무이한 식별자를 만드는 데 사용됩니다.
Symbol()을 호출하면 심볼을 만들 수 있습니다. 설명(이름)은 선택적으로 추가할 수 있습니다.
심볼은 이름이 같더라도 값이 항상 다릅니다. 이름이 같을 때 값도 같길 원한다면 전역 레지스트리를 사용해야 합니다. Symbol.for(key)는 key라는 이름을 가진 전역 심볼을 반환합니다. key라는 이름을 가진 전역 심볼이 없으면 새로운 전역 심볼을 만들어줍니다. key가 같다면 Symbol.for는 어디서 호출하든 상관없이 항상 같은 심볼을 반환해 줍니다.심볼의 주요 유스 케이스는 다음과 같습니다.
1. 객체의 ‘숨김’ 프로퍼티 – 외부 스크립트나 라이브러리에 ‘속한’ 객체에 새로운 프로퍼티를 추가해 주고 싶다면 심볼을 만들고, 이를 프로퍼티 키로 사용하면 됩니다. 키가 심볼인 경우엔 for..in의 대상이 되지 않아서 의도치 않게 프로퍼티가 수정되는 것을 예방할 수 있습니다. 외부 스크립트나 라이브러리는 심볼 정보를 갖고 있지 않아서 프로퍼티에 직접 접근하는 것도 불가능합니다. 심볼형 키를 사용하면 프로퍼티가 우연히라도 사용되거나 덮어씌워 지는 걸 예방할 수 있습니다.
이런 특징을 이용하면 원하는 것을 객체 안에 ‘은밀하게’ 숨길 수 있습니다. 외부 스크립트에선 우리가 숨긴 것을 절대 볼 수 없습니다.
2. 자바스크립트 내부에서 사용되는 시스템 심볼은 Symbol.*로 접근할 수 있습니다. 시스템 심볼을 이용하면 내장 메서드 등의 기본 동작을 입맛대로 변경할 수 있습니다. iterable 객체에선 Symbol.iterator를, 객체를 원시형으로 변환하기에선 Symbol.toPrimitive이 어떻게 사용되는지 알아보겠습니다.사실 심볼을 완전히 숨길 방법은 없습니다. 내장 메서드 Object.getOwnPropertySymbols(obj)를 사용하면 모든 심볼을 볼 수 있고, 메서드 Reflect.ownKeys(obj)는 심볼형 키를 포함한 객체의 모든 키를 반환해줍니다. 그런데 대부분의 라이브러리, 내장 함수 등은 이런 메서드를 사용하지 않습니다.
Symbol()
// id는 새로운 심볼이 됩니다.
let id = Symbol();
// 심볼 id에는 "id"라는 설명이 붙습니다.
let id = Symbol("id");
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
🚨 심볼은 문자형으로 자동 형 변환되지 않음
- 심볼형 값은 다른 자료형으로 암시적 형 변환(자동 형 변환)되지 않음
- 문자열과 심볼은 근본이 다르기 때문에 우연히라도 서로의 타입으로 변환돼선 안 됨
- 자바스크립트에선 '언어 차원의 보호장치(language guard)'를 마련해 심볼형이 다른 형으로 변환되지 않게 막아줌
let user = { // 서드파티 코드에서 가져온 객체
name: "John"
};
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // 심볼을 키로 사용해 데이터에 접근 가능
문자열 "id"를 키로 사용해도 되는데 Symbol("id")을 사용한 이유
- user는 서드파티 코드에서 가지고 온 객체이므로 함부로 새로운 프로퍼티를 추가할 수 없음
- 그런데 심볼은 서드파티 코드에서 접근할 수 없기 때문에, 심볼을 사용하면 서드파티 코드가 모르게 user에 식별자를 부여할 수 있음
{...}
을 사용해 객체를 만든 경우, 대괄호를 사용해 심볼형 키를 만들어야 함let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // "id": 123은 안됨
};
Object.assign
은 키가 심볼인 프로퍼티를 배제하지 않고 객체 내 모든 프로퍼티를 복사함Symbol.for(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
let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");
alert( Symbol.keyFor(globalSymbol) ); // name, 전역 심볼
alert( Symbol.keyFor(localSymbol) ); // undefined, 전역 심볼이 아님
alert( localSymbol.description ); // name
export const apiStatus = {
IDLE: Symbol('IDLE'),
PENDING: Symbol('PENDING'),
SUCCESS: Symbol('SUCCESS'),
ERROR: Symbol('ERROR')
}
export const IDLE = 'IDLE'
export const PENDING = 'PENDING'
export const SUCCESS = 'SUCCESS'
export const ERROR = 'ERROR'
요약
원시값을 기대하는 내장 함수나 연산자를 사용할 때 객체-원시형으로의 형 변환이 자동으로 일어납니다.객체-원시형으로의 형 변환은 hint를 기준으로 세 종류로 구분할 수 있습니다.
- "string" (alert 같이 문자열을 필요로 하는 연산)
- "number" (수학 연산)
- "default" (드물게 발생함)
연산자별로 어떤 hint가 적용되는지는 명세서에서 찾아볼 수 있습니다. 연산자가 기대하는 피연산자를 '확신할 수 없을 때’에는 hint가 "default"가 됩니다. 이런 경우는 아주 드물게 발생합니다. 내장 객체는 대개 hint가 "default"일 때와 "number"일 때를 동일하게 처리합니다. 따라서 실무에선 hint가 "default"인 경우와 "number"인 경우를 합쳐서 처리하는 경우가 많습니다.객체-원시형 변환엔 다음 알고리즘이 적용됩니다.
1. 객체에 objSymbol.toPrimitive메서드가 있는지 찾고, 있다면 호출합니다.
2. 1에 해당하지 않고 hint가 "string"이라면,
obj.toString()
이나obj.valueOf()
를 호출합니다.
3. 1과 2에 해당하지 않고, hint가 "number"나 "default"라면
obj.valueOf()
나obj.toString()
을 호출합니다.
obj.toString()
만 사용해도 '모든 변환’을 다 다룰 수 있기 때문에, 실무에선obj.toString()
만 구현해도 충분한 경우가 많습니다. 반환 값도 ‘사람이 읽고 이해할 수 있는’ 형식이기 때문에 실용성 측면에서 다른 메서드에 뒤처지지 않습니다.obj.toString()
은 로깅이나 디버깅 목적으로도 자주 사용됩니다.