본 내용은 카일 심슨의 YOU DON'T KNOW JS 타입과 문법, 스코프와 클로저(2017)를 읽고 서술적인 내용을 Q&A 형식으로 정리한 글입니다.
1. 추상 연산
1.1 자신의 프로토타입에 toString()이 있는 경우(1)와, 일반 객체처럼 Object.prototype.toString() 메소드를 호출하는 경우(2)의 차이점은?
(1) 내장 원시 값은 정해져 있는 방법대로 문자열화되어 반환한다.
(2) 내부 [[Class]]를 반환한다. 정확히는 ToPrimitive 과정을 거친 결과값이 반환되어 "[object Object]"가 반환된다.
1.2 JSON.stringify 함수의 각 인자에 대한 설명과 문자열화하는 규칙에 대해 설명하시오.
JSON.stringify(value, function(k, v){ } | [], " ")
- value: 문자열화할 값
- function(){ } | []: 대체자, 객체를 재귀적으로 직렬화하여 필터링
- 배열인 경우, 내부 원소는 문자열이며, 각 원소는 문자열화할 객체(value)의 프로퍼티명
- 함수인 경우, 처음은 자기자신의 객체에 대해 실행(key는 undefined)하고 그 이후 각 프로퍼티마다 한 번씩 실행되며 key와 value를 인자로 전달
- undefined를 반환하면 직렬화에서 제외
- " ": 들여쓰기 문자 정의(기본: 공백Space)
JSON.stringify(["a", "b", "c"], function(k, v){
if (!k) return v;
if (k >= 1) return v;
})
/* "[null, "b", "c"]" */
JSON 안전값에 속하지 않는 타입인 경우 (undefined, 함수, Symbol)
배열에 포함되어 있으면 null로 대체하고, 객체에 포함되어 있다면 자동으로 누락함
순환 참조인 경우
에러 발생
1.3 ToNumber(숫자 아닌 값 -> 수식 연산이 가능한 숫자)의 변환 로직에 대해 설명하시오.
만약 객체가 valueOf 혹은 toString을 커스텀 구현할 시 커스텀된 valueOf(toString)이 타입 변환 시 적용된다.
const a = {
name: "newjeong",
age: "27",
valueOf: function () {
return this.age;
}
}
Number(a); // 27
1.4 ToPrimivite란?
해당 객체가 valueOf()를 구현했는지 확인한 후 그렇지 않을 경우에는 toSTring()을 이용하여 강제변환한다. 어떻게 해도 원시 값으로 바꿀 수 없는 경우, TypeError를 throw한다.
즉, valueOf와 toString이 없는 객체를 생성하면 강제변환이 불가능한 객체를 생성할 수 있다.
const obj = Object.create(null)
obj.a = 1;
String(obj) // TypeError: Cannot convert object to primitive value
`${obj}` // TypeError: Cannot convert object to primitive value
1.5 ToBoolean 변환 규칙에서 falsy한 값은 무엇인가?
undefined, null, false, +-0, NaN, ""만 ToBoolean 변환 시 false가 되고 이외의 값은 truthy한 값이다.
(주의) "0", "false", "''" 처럼 falsy한 값들의 문자열은 truthy한 값이다.
2. 명시적 강제변환
2.1 문자열과 숫자간 명시적으로 강제변환하는 법에 대해 모두 설명하시오.
2.1.1 숫자 -> 문자열
String()
2.1.2 문자열 -> 숫자
- Number() : Toumber 추상 연산 로직을 따름
- 문자열 앞에 +, - 단항 연산자 (일반 연산자와 함께 쓰일 시 혼동하기 쉬우므로 다른 연산자와의 사용은 지양)
- 날짜: Date.now()(ES5~), new Date().getTime(), +new Date()
- ~(틸드) : ToInt32 숫자로 강제변환 후 NOT 연산 (비트 뒤집기),즉 2의 보수를 구할 수 있음
- ~x === -(x+1)
- parseInt() : 파싱 도중 숫자 변환 불가능한 문자를 만나면 변환 멈춤
Number("12a") // NaN
parseInt("12a") // 12
2.2 ~(틸드)의 용도는?
- 용도 1. -1 경계값에 틸드 적용 시 0(falsy), 나머지 값들은 1(truthy)인 점을 활용한 indexOf() 의 '구멍난 추상화' 숨기기
- 용도 2. ~~(더블 틸트)로 숫자의 소수점을 제거(=== 양수에 한하여 Math.floor(), 음수의 경우 Mah.ceil())
- 상위 비트를 잘라내는 ~~보다 x | 0로 하위 비트를 잘라내는게 미미하게 더 빠를 수 있지만 연산자 우선순위에 따라 ~~를 사용해야 하는 경우가 존재
const str = "newjeong is hungry. she wants to eat dessert such as cookie..."
if (~str.indexOf("hungry")) { // Exist
return 1
}
2.3 parseInt()의 동작 방식에 대해 설명하시오.
첫번째 인자가 비문자열인 경우, 문자열로 강제변환한 후 숫자 파싱을 진행
두번째 인자는 기수를 설정하여 진법 종류를 설정할 수 있음
두번째 인자를 주지 않을 경우,
- ES5 이전: 첫번째 인자의 문자열이 x로 시작하면 16진수, 0이면 8진수로 임의 해석
- ES5~ : "0x"는 16진수로, 나머지는 10진수로 해석
ES5 이전에서는 다음의 오류를 주의한다.
const hour = parseInt("07");
const minute = parseInt("05");
console.log("%d:%d", hour, minute); // "0:0"
비문자열 파싱
parseInt()는 내부에 비문자열이 인자로 들어올 경우, 문자열로 변환 후 진행
parseInt(1/0, 19) // "Infinity" -> 19진수 -> 18
parseInt(new String("1")) // "1"로 언박싱 -> 1
parseInt(false, 16) // "false" -> 16진수 -> 250
parseInt(0.00008) // "0.00008" -> 0
parseInt(0.00000008) // "8e-8" -> 8
parseInt(parseInt, 16) // "function ..." -> 16진수 -> 15
2.4 불리언 강제 변환
Boolean(x), !!x
3. 암시적 강제변환
3.1 문자열과 숫자 간의 암시적 강제변환에 대해 설명하시오.
사칙연산자로 암시적 강제변환이 가능하다.
+연산자는 인자 중 하나라도 문자열이 있거나 문자열로 변환 가능한 경우, 문자열로 강제변환 후 + 알고리즘을 수행한다.
const a = 1
const b = a + "" // "1"
const arr1 = [1,2,"new"]
const arr2 = ["jeong",3,4]
const arr3 = arr1 + arr2 // "1,2,newjeong,3,4"
[참고] 문자열로 변환 가능한 경우란, toPrimitive 로직을 거쳐 valueOf()과 toString()으로 문자열 변환이 가능한 경우를 말한다.
따라서 객체에 valueOf 혹은 toString을 커스텀 구현하면 예상치 못한 결과를 얻을 수 있으니 주의한다.
const a {
name: "newjeong",
age: 27
}
const b = a + "" // "[object Object]"
a.valueOf = function() {
return this.age
}
const d = a + "" // "27"
- , *, /연산자는 반드시 숫자 연산만 가능하므로 문자열을 숫자로 강제변환하고 싶을 때 활용한다.
const a = "27" - 0 // 27
const arr1 = [100]
const arr2 = [1]
const num = arr1 - arr2 // 99
3.2 인자로 받은 값들 중 하나라도 true/truthy라면 true를 반환하는 함수를 작성하시오.
function hasTruthy(...args) {
for (const v of args) {
if (!!v) {
return !!v
}
}
return false
}
const a=true,b=false,c=0,d=""
hasTruthy(a,b,c,d) // true
hasTruthy(b,c,d) // false
3.3 암시적으로 Boolean으로 강제변환되는 표현식을 열거하시오.
- if () 조건문
- for( ; ; )문의 두번째 조건 표현식
- while(), do {...} while() 문의 조건 표현식
- 삼항 연산자의 조건 표현식
- ||, && 논리 연산자의 좌측 피연산자
3.4 &&, || 논리 연산자의 동작 방식에 대해 설명하시오.
좌측 피연산자를 Boolean으로 암시적 강제변환했을 때 결과값이 true/false인지에 따라 첫번째 혹은 두번째 피연산자의 평가값을 선택한다.
const a = "newjeong"
const b = "juj"
const c = null
/**
* 첫번째 피연산자가 true라면 두번째 피연산자의 결과값을 선택한다.
* 반면, 첫번째 피연산자가 false라면 첫번째 피연산자의 결과값을 선택한다.
*/
a && b // "juj"
c && b // null
/**
* 첫번째 피연산자가 true라면 첫번째 피연산자의 결과값을 선택한다.
* 반면, 첫번째 피연산자가 false라면 두번째 피연산자의 결과값을 선택한다.
*/
a || b // "newjeong"
c || b // "juj"
[참고] 어떻게 보면 a && b는 a? b : a로, a || b는 a? a : b로 해석할 수 있다.
다만 삼항 연산자는 a가 두 번 평가되는 반면, a && b에서 a는 단 한번만 평가된 후 ToBoolean 추상 연산에 따라 Boolean으로 강제변환된다.
&& 연산자의 활용
&& 연산자는 '가드 연산자'로, 어떤 값이 truthy(true)라면 해당 값을 인자로 하는 함수를 호출하는 등 추가 로직을 수행할 수 있다. 이를 '단락 평가'라고 부른다.
const sayHello = (name) => {
console.log(`Hello, ${name}!`)
}
const a = "newjeong"
a && sayHello(a)
|| 연산자의 활용
첫번째 피연산자가 falsy일 때 무조건 두번째 피연산자를 평가하는 것이 보장된 로직에서 활용한다.
const getName = (firstName, lastName) => {
const fName = firstName || "Jane"
const lName = lastName || "Doe"
return `${fName} ${lName}`
}
getName("Newjeong", "Jeon") // "Newjeong Jeon"
getName() // "Jane Doe"
getName("Zoey", "") // "Zoey Doe"
4. 느슨한(==) / 엄격한(===) 동등 비교
질문으로 넘어가기 전에 ...
ECMA Script 2020 표준 명세에서 말하는 Abstract Equality Comparison (==) 의 동작방식에 대해 기술해보았다.
7.2.15 Abstract Equality Comparison
The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:
1. If Type(x) is the same as Type(y), then
a. Return the result of performing Strict Equality Comparison x === y.
7.2.16의 Strict Equality Comparison(아래 '더보기')을 수행
2. If x is null and y is undefined, return true.
3. If x is undefined and y is null, return true.
4. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
5. If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.
6. If Type(x) is BigInt and Type(y) is String, then
a. Let n be ! StringToBigInt(y).
b. If n is NaN, return false.
NaN은 자기자신과도 동등하지 않다.
c. Return the result of the comparison x == n.
7. If Type(x) is String and Type(y) is BigInt, return the result of the comparison y == x.
8. If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.
9. If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).
10. If Type(x) is either String, Number, BigInt, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
11. If Type(x) is Object and Type(y) is either String, Number, BigInt, or Symbol, return the result of the comparison ToPrimitive(x) == y.
12. If Type(x) is BigInt and Type(y) is Number, or if Type(x) is Number and Type(y) is BigInt, then
a. If x or y are any of NaN, +∞, or -∞, return false.
b. If the mathematical value of x is equal to the mathematical value of y, return true; otherwise return
false.
13. Return false.
다음은 엄격한 동등비교(===)의 매커니즘이다.
7.2.16 Strict Equality Comparison
The comparison x === y, where x and y are values, produces true or false. Such a comparison is performed as follows:
1. If Type(x) is different from Type(y), return false.
2. If Type(x) is Number or BigInt, then
a. Return ! Type(x)::equal(x, y).
+0과 -0은 다른 값으로 인식되어 false를 반환
3. Return ! SameValueNonNumeric(x, y).
※ SameValueNonNumeric
The internal comparison abstract operation SameValueNonNumeric(x, y), where neither x nor y are numeric type values, produces true or false. Such a comparison is performed as follows:
1. Assert: Type(x) is not Number or BigInt.
2. Assert: Type(x) is the same as Type(y).
3. If Type(x) is Undefined, return true.
4. If Type(x) is Null, return true.
5. If Type(x) is String, then
a. If x and y are exactly the same sequence of code units (same length and same code units at
corresponding indices), return true; otherwise, return false.
6. If Type(x) is Boolean, then
a. If x and y are both true or both false, return true; otherwise, return false.
7. If Type(x) is Symbol, then
a. If x and y are both the same Symbol value, return true; otherwise, return false.
8. If x and y are the same Object value, return true. Otherwise, return false.
두 객체가 같은 객체 레퍼런스일 때만 true를 반환한다.
4.1 숫자와 문자열의 느슨한 동등 비교(==)는?
4. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
5. If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.
문자열이 숫자로 변환됨
4.2 Boolean과의 비교
8. If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.
9. If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).
Boolean이 숫자로 변환됨
따라서 "42"라는 문자열은 truthy한 값이지만 Boolean이 숫자로 변환되어 0 또는 1로 변환되기 때문에 ==와 전혀 관련이 없다.
이는 오해의 소지가 다분하여 ==의 사용을 지양하고 강제변환이 일어나지 않는 === 사용(혹은 !!을 통한 명시적 변환)을 권장한다.
"42" == true // false
"42" == false // false
4.3 null == undefined의 결과는?
true
2. If x is null and y is undefined, return true.
3. If x is undefined and y is null, return true.
4.4 객체와 비객체의 느슨한 동등비교(==)는?
10. If Type(x) is either String, Number, BigInt, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
11. If Type(x) is Object and Type(y) is either String, Number, BigInt, or Symbol, return the result of the comparison ToPrimitive(x) == y.
const a = 42
const b = [42]
a == b // true. 42 === ToPrimitive([42]) -> "42" -> 42
이는 마치 객체 Wrapper의 언박싱을 통해 스칼라 값으로 강제변환하는 것과 같은 이치이다.
const a = "newjeong"
const b = Object(a) // new String(a)
a == b // true
단, null, undefined의 객체 Wrapper는 Object()가 되어 null == {}의 결과인 false가 반환된다.
NaN의 경우도 마찬가지로 NaN == NaN은 false임을 유의한다.
4.5 다음 느슨한 동등비교(==)의 결과는?
// (1)
"0" == false
// (2)
false == 0
// (3)
false == ""
// (4)
false == []
// (5)
"" == 0
// (6)
"" == []
// (7)
0 == []
// (8)
[] == ![]
(1) true ("0" ⇢ 0, false ⇢ 0)
(2) true (false ⇢ 0)
(3) true (false ⇢ 0, "" ⇢ 0)
(4) true (false ⇢ 0, [] ⇢ ToPrimitive([]) ⇢ "" ⇢ 0)
(5) true ("" ⇢ 0)
(6) true ("" ⇢ 0, [] ⇢ ToPrimitive([]) ⇢ "" ⇢ 0)
(7) true ([] ⇢ ToPrimitive([]) ⇢ "" ⇢ 0)
(8) true ([] ⇢ ToPrimitive([]) ⇢ "" ⇢ 0, ![] ⇢ false ⇢ 0)
위의 긍정오류처럼 false인 거 같지만 true인 비교를 주의한다.
예를 들어 func(a, b) { return a == b }에서 func(0, []) 을 넘기는 것처럼...
책에서는 다음 경우에 느슨한 동등비교의 사용을 주의하라고 한다.
- 피연산자중 하나가 true/false일 가능성이 있다면 절대로 ==을 사용하지 않는다.
- 피연산자중 하나가 [], "", 0이 될 가능성이 있다면 가급적 ==을 사용하지 않는다.
4.6 추상 관계 비교(<, >)는 어떻게 동작하는가?
피연산자가 모두 문자열인 경우, 각 문자 순서대로 어휘 비교를 수행한다.
"newjeong" > "jeon" // true
피연산자 중 하나라도 문자열이 아닌 경우, ToPrimitive() 연산을 하여 강제변환한다.
[42] < ["43"] // true, 42 < 43
[42] < ["aaa"] // true, "42" < "aaa"
<=, >=는 사실 <, >의 NOT 연산이다.
const val1 = {b: 10}
const val2 = {b: 10}
val1 < val2 // false
val1 <= val2 // true
추상 관계 비교는 강제 변환을 금할 수 없기 때문에 가급적 명시적 강제변환을 통해 비교한다.
'Books > YOU DON'T KNOW JS' 카테고리의 다른 글
스코프와 클로저 (0) | 2020.10.05 |
---|---|
문법 (0) | 2020.09.08 |
타입, 값 그리고 네이티브 (0) | 2020.08.04 |