해당 포스트는 자바스크립트 완벽 가이드라는 교재로 스터디를 하면서 4장을 정리한 포스트입니다.
주관적으로 해석한 내용이 들어가 있어서 잘못된 내용이 포함될 수 있습니다.
또한 교재의 모든 내용을 정리하지 않고 주관적인 판단에 의해 필요한 내용만 작성했습니다.
📌 용어 정리
1. 표현식
어떤 값으로 평가되는 구절입니다.
리터럴, 변수 등이 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let digit = 0;
const arr = [1, 2, 3];
const person = { name: "john" };
const func = v => v;
// 아래부터는 모두 표현식입니다. 즉, 값으로 평가됩니다.
1;
"1";
true;
1 + 1;
digit;
arr[2];
person.name;
func(1);
📌 연산자 개요
교재에 나오는 모든 연산자에 대해 작성하지 않고 익숙하지 않은 연산자,
side effect
가 있는 연산자, 주관적인 기준으로 사용법을 알 필요가 있다고 판단한 연산자에 대해서만 살펴보겠습니다.
1. 전위/후위 증가/감소 연산자 ( Side Effect 발생 )
++
, --
로 피연산자의 앞쪽에 사용하면 전위, 뒤쪽에 사용하면 후위 연산자로 사용됩니다.
전위 연산자로 사용되는 경우 값으로 평가되기 전 연산을 하고 후위 연산자로 사용되는 경우 값으로 평가되고 나서 연산합니다.
간단한 내용이라 예시는 생략하겠습니다.
2. delete ( Side Effect 발생 )
객체의 프로퍼티를 제거하는 연산자입니다.
delete
의 특이한 점은 제거하는 즉시 할당 해제를 시키지 않는다는 점입니다.
제가 처음 생각했을 때는 제거하고 가비지 컬렉터에 의해서 바로 할당 해제를 시킬 거로 생각했습니다
하지만 바로는 아니고 이후에 참조가 가능한지 판단 후 제거한다고 합니다. ( MDN - delete 연산자 )
그래서 제 생각에는 만약 프로퍼티를 제거하고 참조하는 곳이 없다면 null
로 초기화하고 delete
를 하는게 좋을 것 같습니다.
null
로 바꾸면 참조 대상이 없으니 바로 가비지 컬렉터의 대상이 되고, delete
하면 프로퍼티로도 제거되기 때문에 깔끔한 것 같습니다.
물론 코드가 한줄 더 생긴다는 문제 때문에 판단이 된다면 상황에 맞게 사용하면 좋을 것 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const person = {
name: "john",
};
person.name = null;
delete person.name;
// 바로 할당 해제되면 안되는 예시
const person = {
vision: {
left: 1.2,
right: 0.8,
},
}
// destructuring
const { vision } = person;
delete person.vision;
console.log(person.vision); // undefined
console.log(vision); // { left: 1.2, right: 0.8 }
3. void
다른 연산자는 존재는 알았지만, 사용법을 모르거나 사용하지 않거나였는데 이 연산자는 존재 자체를 처음 봤습니다.
TypeScript
에서 함수의 반환 값이 없음을 의미하는 데 사용하는 것으로만 봐서 TypeScript
전용인 줄 알았습니다.
이 연산자는 특이하게 모든 값을 undefined
로 바꿔버립니다.
실질적인 사용 예시는 교재에서도 없었고 제가 생각했을 때도 딱히 없는 것 같습니다.
1
2
3
4
console.log(void []); // undefined
console.log(void {}); // undefined
console.log(void 1); // undefined
console.log(void true); // undefined
4. 옵셔널 체이닝 연산자
프로퍼티로 가질 수 없는 값에 .
를 사용하면 TypeError
가 발생합니다.
특정 변수가 값이 존재하는지 확인하는 구문 ? 존재한다면 프로퍼티에 접근 : 그게 아니라면 null
과 같은 형식으로 코드를 구성하면 매우 귀찮아집니다.
변수가 확실하게 값을 가지고 있다고 판단할 수 없는 경우 사용하면 좋습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let person;
// console.log(person.name); // TypeError
console.log(person?.name); // undefined
let func;
// func(); // TypeError
func?.(); // undefined
// 단, 위에서는 "null", "undefined"인지만 체크 ( 즉, 함수인지 체크하는 것은 아님 )
func = "func";
// func?.(); // TypeError
typeof func === "function" && func();
let arr;
// arr[0]; // TypeError
arr?.[0]; // undefined
5. null 병합 연산자
피연산자가 null
or undefined
인지 판단해주는 연산자입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
console.log(null ?? "a"); // "a"
console.log(undefined ?? "a"); // "a"
// 위쪽의 두 코드만 보면 굳이 "??"를 써야할 필요가 없다고 생각할 수 있습니다.
// 아래의 두 코드로도 같은 결과를 낼 수 있기 때문입니다.
console.log(null || "a"); // "a"
console.log(undefined || "a"); // "a"
// 하지만 가끔 유용하게 사용되는 경우가 falsy인 값을 true로 평가하고 싶은 경우 입니다.
// 만약 digit가 숫자인 경우 그대로 두고 그게 아니라면 100을 넣는 코드를 구현한다고 가정합니다.
// 실질적인 예시는 아니지만 떠오르는 예시가 없어 해당 상황으로 설명하겠습니다.
let digit;
// 아래의 경우 0이라는 숫자를 그대로 사용하고 싶지만 0은 falsy인 값이기 때문에 false로 판단돼서 숫자임에도 불구하고 100으로 교체됩니다.
digit = 0;
digit = digit || 100;
console.log(digit); // 100
// 하지만 ??를 사용하는 경우 null과 undefined만 체크하기 때문에 숫자 0을 그대로 유지할 수 있습니다.
digit = 0;
digit = digit ?? 100;
console.log(digit); // 0
6. in 연산자와 배열
in
연산자는 객체에 해당 프로퍼티를 갖는지 판단하는데 사용하는 연산자입니다.
이 연산자를 배열에 사용하면 특이한 결과가 나타납니다.
아래의 특이한 결과를 이해하기 위해서는 JavaScript
의 배열에 대해 조금 자세히 알아야 할 필요가 있습니다.
JavaScript
의 배열은 일단 객체이고, 배열의 동작을 흉내 낸 희소 배열입니다.
즉, 연속적인 크기로 메모리에 적재되지 않고 객체처럼 프로퍼티를 이용해서 접근해야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const fruits = ["apple", "banana", "melon"];
console.log("0" in fruits); // true
console.log("1" in fruits); // true
console.log("2" in fruits); // true
console.log("3" in fruits); // false
// 위 결과를 토대로 배열이 아래처럼 생겼다고 예측할 수 있습니다. ( 확실하지 않음 )
const fruits = {
"0": "apple",
"1": "banana",
"2": "melon",
"length": 3, // 프로퍼티의 개수를 감시해서 그에 맞게 증감 ( 감시하는 방법은 몰라서 직접적으로 작성 )
// ... 어떤 방식으로 반복되는지 선언 ( iterator protocol )
*[Symbol.iterator]() {
for (const key in this) {
yield this[key];
}
},
};
// 반복에서 제외
Object.defineProperty(fruits, "length", { enumerable: false });
for (let i = 0; i < fruits.length; i++) {
console.log(fruits[i]); // "apple", "banana", "melon"
}
console.log("========== 구분 ==========");
for (const iterator of fruits) {
console.log(iterator); // "apple", "banana", "melon"
}
console.log("========== 구분 ==========");
console.log(...fruits); // "apple" "banana" "melon"
📌 단축 평가 ( short circuit )
단축 평가란 연산을 끝까지 하지 않고 일부만의 연산으로 결과를 내는 것을 의미합니다.
예시가 이해하기 쉽기 때문에 바로 예시를 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 논리 OR 연산자 ( "||" ) ( 맨 처음 true인 놈을 반환 )
// OR은 하나만 true여도 평가를 끝내도 되기 때문에 true가 나온 시점 이후의 평가를 하지 않습니다.
console.log(1 || 2 || 3); // 1
// 만약 이후의 평가에 Side Effect가 발생한다면 문제(?)가 생길 수 있습니다.
let digit = 0;
// 아래의 예시는 1에서 평가가 끝나기 때문에 "++digit"구문까지 가지 않습니다.
console.log(1 || 2 || ++digit); // 1
// 따라서 digit가 증가되지 않습니다.
console.log(digit); // 0
// 논리 AND 연산자 ( "&&" ) ( 맨 처음 false인 놈을 반환, 없다면 제일 마지막놈 반환 )
console.log(1 && 2 && 3); // 3
console.log(0 && 1 && 2); // 0
// 이 예시도 위와 같은 문제(?)가 발생합니다.
let digit = 0;
console.log(0 || 1 || ++digit); // 1
console.log(digit); // 0
// 옵셔널 체이닝 연산자 ( "?." )
// "fix()"가 순수함수가 아니라고 가정했을 때 중간에 null or undefined가 존재하면 "fix()"에 의한 Side Effect가 발생하지 않습니다.
person?.vision?.right?.fix();
// null 병합 연산자 ( "??" )
let digit = 0;
// 아래에서 "++digit" 실행
0 ?? ++digit;
// 아래에서 "++digit" 실행하지 않음
null ?? ++digit;
위의 예시들은 조금 인위적으로 만든 예시입니다.
결론은 단축 평가가 될 수 있는 가능성이 있는 연산자를 사용하는 경우 Side Effect
를 발생하는 코드를 사용하지 않는 것이 좋다는 것입니다.
📌 연산자의 자동 형 변환
1
2
3
4
5
6
7
8
9
10
11
// "+"는 문자열 연결 연산자가 우선순위가 높기 때문에 문자열로 형변환해서 연산됩니다.
// 나머지는 피연산자들이 숫지인 경우밖에 없기 때문에 숫자로 형변환 됩니다.
console.log(2 + "2"); // "22"
console.log(2 - "2"); // 0
console.log(2 * "2"); // 4
console.log(2 / "2"); // 1
// 몰랐던 부분
console.log(1 + {}); // "1[Object Object]"
console.log(1 + null); // 1 + 0 -> 0
console.log(1 + {}); // 1 + NaN -> NaN
📌 연산자를 이용한 간단한 형 변환
제가 자주 사용하는 간단한 형태의 형변환 방법들입니다.
Number()
, parseInt()
, String()
, Boolean()
등을 사용해도 되지만 가장 간결하게 변환할 수 있는 것 같아서 소개해봤습니다.
1
2
3
4
5
6
7
8
9
// 숫자로 변환
console.log(+"1.123"); // 1.123
// 문자로 변환
console.log(123 + ""); // "123"
// 불리언 변환
console.log(!!1); // true
console.log(!!0); // false
단, 위처럼 사용하기 이전에 각 변환법의 차이점은 어느 정도 알고 이후에 사용하는 게 좋다고 생각합니다. ( 예시 2, 예시 3 )
저도 완벽하게 알지는 못하지만, 어느 정도 차이는 있으니 필요한 상황에 검색 or 테스트해서 원하는 기능을 골라서 사용할 수 있는 정도로 이해하고 있습니다.
1
2
3
4
5
6
7
8
9
// 예시 2
console.log(parseInt("1.123")); // 1
console.log(Number("1.123")); // 1.123
console.log(+"1.123"); // 1.123
// 예시 2
console.log(parseInt("1.123문자추가")); // 1
console.log(Number("1.123문자추가")); // NaN
console.log(+"1.123문자추가"); // NaN
📌 프로퍼티 접근 표현식
프로퍼티 접근 표현식은 두 가지 접근 방법이 있습니다.
.
로 접근[]
로 접근
일반적으로 프로퍼티의 이름을 알고 있는 경우 .
를 사용합니다.
하지만 프로퍼티 명에 변수에 넣을 수 없는 문자(아래 예시로는 -
)가 들어가거나, Symbol
인 경우, 변수를 값으로 평가한 값을 넣는 경우 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const uniqueValue = Symbol("unique");
const uniqueValueCopy = Symbol("unique");
const n = "name";
const obj = {
"api-key": "keyboardcat",
name: "john",
[uniqueValue]: "Symbol의 uniqueValue로만 접근이 가능",
};
// console.log(obj[name]); // Error: name is not defined
console.log(obj[n]); // "john"
console.log(obj["name"]); // "john"
console.log(obj.uniqueValue); // undefined -> "uniqueValue"라는 프로퍼티를 찾는 문법
console.log(obj[uniqueValue]); // "Symbol의 uniqueValue로만 접근이 가능"
console.log(obj[uniqueValueCopy]); // undefined
// console.log(obj.api-key); // 불가능한 문법
console.log(obj["api-key"]); // "keyboardcat"