객체
객체형은 원시형과 달리 다양한 데이터를 담을 수 있다.
키로 구분된 데이터 집합이나 복잡한 개체를 저장할 수 있다.
객체는 중괄호 {...}를 이용해 만들 수 있다.
중괄호 안에는 '키(key): 값(value)' 쌍으로 구성된 프로퍼티를 여러개 넣을 수 있다.
키엔 문자형, 값엔 모든 자료형이 허용된다.
// 객체를 생성하는 두가지 문법
let user = new Object(); // '객체 생성자' 문법
let user = {}; // '객체 리터럴' 문법
리터럴과 프로퍼티
let user = { // 객체
name: "John", // 키: "name", 값: "John"
age: 30 // 키: "age", 값: 30
"likes birds": true // 복수의 단어는 따옴표로 묶어야 한다.
};
user["likes birds"] = false; // 복수의 단어는 대괄호로 접근 가능
// 프로퍼티 값 얻기
alert(user.name); // John
alert(user.age); // 30
alert(user["likes birds"]; // false
// 프로퍼티 삭제
delete user.age;
delete user["likes birds"];
대괄호 표기법을 사용하면 변수를 키로 사용한 것과 같이 문자열뿐만 아니라 모든 표현식의 평가 결과를 프로퍼티 키로
사용할 수 있다.
let key = "likes birds";
// user["likes birds"] = true; 와 같다.
user[key] = true;
변수 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]: 5, // 변수 fruit에서 프로퍼티 이름을 동적으로 받아 온다.
};
alert(bag.apple); // fruit에 "apple"이 할당되었다면, 5가 출력된다.
[fruit]는 프로퍼티 이름을 변수 fruit에서 가져오겠다는 것을 의미한다.
사용자가 프롬프트 대화상자에 apple을 입력했다면 bag에는 {apple: 5}가 할당되었을 것이다.
// 위의 예시와 같다
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,
// ...
};
}
프로퍼티 이름의 제약사항
- 변수 이름엔 예약어 사용이 불가하지만, 객체는 이런 제약이 없다.
- 키는 문자열로 자동 형변환이 된다.
'in' 연산자로 프로퍼티 존재 여부 확인하기
존재하지 않는 프로퍼티에 접근하려 하면 에러가 발생하지 않고 undefined를 반환한다.
let user = { name: "John", age: 30 };
alert("age" in user); // user.age가 존재하므로 true가 출력된다.
alert("blabla" in user); // user.blabla는 존재하지 않으므로 false가 출력된다.
'for...in' 반복문
for...in 반복문을 사용하면 객체의 모든 키를 순회할 수 있다.
for...in은 앞서 학습했던 for(;;) 반복문과는 완전히 다르다.
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
}
객체 정렬 방식
정수 프로퍼티는 자동으로 정렬되고, 그 외의 프로퍼티는 객체에 추가한 순서대로 정렬된다.
let codes = {
"49": "독일"
"41": "스위스",
"44": "영국",
// ..,
"1": "미국"
};
for(let code in codes) {
alert(code); // 1, 41, 44, 49
}
// 위 예시에서 순서대로 출력하고 싶으면 +를 붙여준 트릭을 이용한다
let codes = {
"+49": "독일"
"+41": "스위스",
"+44": "영국",
// ..,
"+1": "미국"
};
for(let code in codes) {
alert(+code); // 49, 41, 44, 1
}
참조에 의한 객체 복사
원시값은 '값 그대로' 저장,할당되고 복사되는 반면에,
객체는 '참조에 의해' 저장되고 복사된다.
let user = {
name: "John"
};
객체는 메모리 내 어딘가에 저장되고, 변수 user엔 객체를 '참조'할 수 있는 값이 저장된다.
따라서 객체가 할당된 변수를 복사할 땐 객체의 참조 값이 복사되고 객체는 복사되지 않는다.
참조에 의한 비교
객체 비교 시 동등 연산자 ==와 일치 연산자 ===는 동일하게 동작한다.
비교 시 피연산자인 두 객체가 동일한 객체인 경우에 참을 반환한다.
let a = {};
let b = a; // 참조에 의한 복사
alert(a == b); // true, 두 변수는 같은 객체를 참조
alert(a === b); // true
/* ------------------------------------------------- */
let a = {};
let b = {}; // 독립된 두 객체
alert(a == b); // false
객체 복사, 병합과 Object.assign
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이 있다.
let user = {name: "John"};
let permission1 = {canView: true};
let permission2 = {canEdit: true};
// permission1과 permission2의 프로퍼티를 user로 복사한다.
Object.assign(user, permission1, permission2);
// now user = {name: "John", canView: true, canEdit: true}
let user = {
name: "John",
age: 30
};
let clone = Object.assign({}, user);
// user에 있는 모든 프로퍼티가 빈 배열에 복사되고 변수에 할당된다.
가비지 컬렉션
가비지 컬렉션 기준
자바스크립트는 도달 가능성이라는 개념을 사용해 메모리 관리를 수행한다.
'도달 가능한' 값은 쉽게 말해 어떻게든 접근하거나 사용할 수 있는 값을 의미한다.
도달 가능한 값은 메모리에서 삭제되지 않는다.
- 아래의 값들은 태생부터 도달 가능한 값이기 때문에 명백한 이유없이 삭제되지는 않는다.
- 현재 함수의 지역 변수와 매개변수
- 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
- 전역 변수
- 기타 등등
* 위의 값들은 루트라고 부른다
- 루트가 참조하는 값이나 체이닝으로 루트에서 참조할 수 있는 값은 도달 가능한 값이 된다.
내부 알고리즘
'mark-and-sweep'이라 불리는 가비지 컬렉션 기본 알고리즘이다.
'가비지 컬렉션'은 대개 다음 단계를 거쳐 수행된다.
- 가비지 컬렉터는 루트 정보를 수집하고 이를 'mark' 한다.
- 루트가 참조하고 있는 모든 객체를 방문하고 이것들을 'mark'한다.
- mark 된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark한다.
한번 방문한 객체는 전부 mark 하기 때문에 같은 객체를 다시 방문하는 일은 없다. - 루트에서 도달 가능한 모든 객체를 방문할 때까지 위 과정을 반복한다.
- mark 되지 않은 모든 객체를 메모리에서 삭제한다.
최적화 기법
- generation collection(세대별 수집) - 객체를 '새로운 객체'와 '오래된 객체'로 나눈다. 객체 상당수는 생성 이후
제 역할을 빠르게 수행해 금방 쓸모가 없어지는데, 이런 객체를 '새로운 객체'로 구분한다.
일정 시간 이상 동안 살아남은 객체는 '오래된 객체'로 분류하고, 가비지 컬렉터가 덜 감시한다. - incremental collection(점진적 수집) - 방문해야 할 객체가 많다면 모든 객체를 한 번에 방문하고 mark 하는데
상당한 시간이 소요된다. 그래서 가비지 컬렉션을 여러 부분으로 분리한 다음, 각 부분을 별도로 수행한다. - idle-time collection(유휴 시간 수집) - CPU가 유휴 시간일때만 가비지 컬렉션을 실행한다.
메서드와 this
메서드 만들기
let user = {
name: "John",
age: 30
};
user.sayHi = function() {
alert("안녕하세요!");
}
/* ------------------------- */
// 미리 함수 정의 가능
// 함수 선언
function sayHi() {
alert("안녕하세요!");
};
// 선언된 함수를 메서드로 등록
user.sayHi = sayHi;
user.sayHi(); // 안녕하세요!
메서드 단축 구문
// 아래 두 객체는 동일하게 동작한다.
user = {
sayHi: function() {
alert("Hello");
}
};
// 단축 구문
user = {
sayHi() {
alert("Hello");
}
};
메서드와 this
메서드 내부에서 this 키워드를 사용하면 객체에 접근할 수 있다.
let user = {
name: "John",
age: 30,
sayHi() {
// 'this'는 '현재 객체'를 나타낸다.
alert(this.name);
}
};
user.sayHi(); // John
만약 this 대신에 user를 사용할 시, user를 복사해 다른 변수에 할당(admin = user)하고, user는 전혀 다른 값으로
덮어썼다고 가정하면 sayHi()는 원치 않는 값을 참조할 수 있다.
자유로운 this
자바스크립트의 this는 다른 프로그래밍 언어의 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가 없는 화살표 함수
화살표 함수는 일반 함수와는 달리 '고유한' this를 가지지 않는다.
화살표 함수에서 this를 참조하면, 화살표 함수가 아닌 '평범한' 외부 함수에서 this 값을 가져온다.
아래 예시에서 함수 arrow()의 this는 외부 함수 user.sayHi()의 this가 된다.
let user = {
firstName: "민준"
sayHi() {
let arrow = () => alert(this.firstName);
arrow();
}
};
user.sayHi(); // 민준
별개의 this가 만들어지는 것은 원하지 않고, 외부 컨텍스트에 있는 this를 이요하고 싶은 경우 화살표 함수가 유용하다.
new 연산자와 생성자 함수
생성자 함수
- 함수 이름의 첫 글자는 대문자로 시작한다.
- 반드시 'new' 연산자를 붙여 실행한다.
function User(name) {
this.name = name;
this.isAdmin = false;
}
let user = new User("민준");
alert(user.name); // 민준
alert(user.isAdmin); // false
new User(...)를 써서 함수를 실행하면 아래와 같은 알고리즘이 동작한다
- 빈 객체를 만들어 this에 할당한다.
- 함수 본문을 실행한다. this에 새로운 프로퍼티를 추가해 this를 수정한다.
- this를 반환한다.
function User(name) {
// this = {}; (빈 객체가 암시적으로 만들어짐)
// 새로운 프로퍼티를 this에 추가함
this.name = name;
this.isAdmin = false;
// return this; (this가 암시적으로 반환됨)
}
생성자 내 메서드
function User(name) {
this.name = name;
this.sayHi = function() {
alert("제 이름은" + this.name + "입니다.");
};
}
let minjun = new User("김민준");
minjun.sayHi(); // 제 이름은 김민준입니다.
/*
minjun = {
name: "김민준",
sayHi: function() { ... }
}
*/
옵셔널 체이닝 '?.'
옵셔널 체이닝이 필요한 이유
let user = {}; // 주소 정보가 없는 사용자
alert(user.address.street); // TypeError: Cannot read property 'street' of undefined
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
요약
- obj?.prop - obj가 존재하면 obj.prop를 반환하고, 그렇지 않으면 undefined를 반환함
- obj?.[prop] - obj가 존재하면 obj[prop]를 반환하고, 그렇지 않으면 undefined를 반환함
- obj?.method() - obj가 존재하면 obj.method()를 호출하고, 그렇지 않으면 undefined를 반환함
?. 왼쪽 평가 대상이 null이나 undefined인지 확인하고 null이나 undefined가 아니라면 평가를 계속 진행한다.
?. 은 ?. 왼쪽 평가대상이 없어도 괜찮은 경우에만 선택적으로 사용해야 한다.
심볼형
심볼
'심볼(symbol)'은 유일한 식별자를 만들고 싶을 때 사용한다.
Symbol()을 사용하면 심볼값을 만들 수 있다.
심볼은 유일성이 보장되는 자료형이기 때문에, 설명이 동일한 심볼을 여러 개 만들어도 각 심볼값은 다르다.
설명이 같은 심볼 두 개를 만들고 동일 연산자(==)로 비교 시 false가 반환된다.
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
'숨김' 프로퍼티
심볼을 이용하면 '숨김' 프로퍼티를 만들 수 있다.
숨김 프로퍼티는 외부 코드에서 접근이 불가능하고 값도 덮어쓸 수 없는 프로퍼티이다.
서드파티 코드에서 가지고 온 user라는 객체가 여러 개 있고, user를 이용해 어떤 작업을 해야 하는 상황이라고
가정해보자. user에 심볼을 이용해 식별자를 붙여보자
let user = { // 서드파티 코드에서 가져온 객체
name: "John"
};
let id = Symbol("id");
user[id] = 1;
alert(user[id]); // 심볼을 키로 사용해 데이터에 접근
문자열 "id"를 키로 사용해도 되는데 Symbol("id")를 사용한 이용한 이유가 무엇일까?
user는 서드파티 코드에서 가지고 온 객체이므로 함부로 새로운 프로퍼티를 추가할 수 없다.
그런데 심볼은 서드파티 코드에서 접근할 수 없기 때문에, 심볼을 사용하면 서드파티 코드가 모르게 user에 식별자를
부여할 수 있다.
symbols in a literal
객체 리터럴 {...} 을 사용해 만든 경우, 대괄호를 사용해 심볼형 키를 만들어야 한다.
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // "id": 123은 안됨
};
심볼은 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)에서도 키가 심볼인 프로퍼티는 배제된다.
하지만 Object.assign은 키가 심볼인 프로퍼티를 배제하지 않고 객체 내 모든 프로퍼티를 복사한다.
let id = Symbol("id");
let user = {
[id]: 123
};
let clone = Object.assign({}, user);
alert(clone[id]); // 123
전역 심볼
전역 심볼 레지스트리 안에 심볼을 만들고 해당 심볼에 접근하면, 이름이 같은 경우 항상 동일한 심볼을 반환해준다.
레지스트리 안에 있는 심볼을 읽거나, 새로운 심볼을 생성하려면 Symbol.for(key)를 사용하면 된다.
이 메서드를 호출하면 이름이 key인 심볼을 반환한다. 조건에 맞는 심볼이 레지스트리 안에 없으면 새로운 심볼
Symbol(key)을 만들고 레지스트리 안에 저장한다.
// 전역 레지스트리에서 심볼을 읽는다.
let id = Symbol.for("id"); // 심볼이 존재하지 않으면 새로운 심볼을 만든다.
// 동일한 이름을 이용해 심볼을 다시 읽는다.
let idAgain = Symbol.for("id");
// 두 심볼은 같다.
alert(id === idAgain); // true
Symbol.keyFor
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
객체를 원시형으로 변환하기
- 객체는 논리 평가 시 true를 반환한다.
객체는 숫자형이나 문자형으로만 형변환이 일어난다. - 숫자형으로의 형 변환은 객체끼리 빼는 연산을 할 때나 수학 관련 함수를 적용할 때 일어난다.
객체 Date 끼리 차감하면 두 날짜의 시간 차이가 반환된다. - 문자형으로의 형 변환은 대개 alert(obj)와 같이 객체를 출력하려고 할 때 일어난다.
객체 - 원시형으로의 형 변환은 hint를 기준으로 세 종류로 구분할 수 있다.
- "string" (alert와 같이 문자열을 필요로 하는 연산)
- "number" (수학 연산)
- "default" (드물게 발생함)
객체 - 원시형 변환엔 다음 알고리즘이 적용된다.
- 객체에 obj[Symbol.toPrimitive](hint) 메서드가 있는지 찾고, 있다면 호출한다.
- 1에 해당하지 않고 hint가 "string"이라면,
- obj.toString()이나 obj.valueOf()를 호출한다.
- 1과 2에 해당하지 않고, hint가 "number"나 "default"라면
- obj.valueOf()나 obj.toString()을 호출한다.
> 실무에선 obj.toString()만 사용해도 충분한 경우가 많다.
'Web > Vanilla JS' 카테고리의 다른 글
Modern Javascript / 코드 품질 - Chrome으로 디버깅 (0) | 2021.07.07 |
---|---|
Modern Javascript / 자료구조와 자료형 (0) | 2021.07.01 |
Modern Javascript / 자바스크립트 기본 (0) | 2021.06.28 |
Vanilla JS / Number() 와 parseInt() (0) | 2020.04.04 |