시작하기에 앞서, 1,2,3 단원은 잘못된 마음 먹음으로 날려버렸다.
동적 타입의 특성을 가지는 JavaScript는 변수의 형태를 코드에서 지정하지 않고 코드가 실행될 때 변수의 형태도 해석된다.
이러한 변수의 형태를 해석하는 것에 있어 강제적으로 형변환이 일어나, 방어적으로 코딩을 하지 않으면 많은 문제가 생길 수 있는데 이 과정에 대해 살펴보도록 한다.
숫자형 데이터의 -, *, %, /와 같은 연산의 피연산자로 문자열을 전달하면 빌트인 함수인 Number
와 유사하게 변환한다. 이는 꽤나 단순하고 숫자형 문자는 그 값대로 변환된다. 다만, 문자열에 숫자형 문자이외의 문자가 있는 경우 NaN
을 반환한다.
3 * "3" // 9
3 * Number("3") // 9
Number("1.") // 1
Number("0012") // 12
Number("1.34") // 1.34
Number("1,") // NaN
Number("1+1") // NaN
Number("1a") // NaN
Numver("one") // one
+ 연산자의 경우, 다른 수학적 연산자와는 다르게 동작하는 데, 아래의 두 기능을 수행한다.
문자열 데이터가 + 연산의 연산자인 경우 문자열을 숫자형으로 변환하는 것이 아니라, 숫자형을 문자열로 변환한다.
1+"2" // "12"
"1"+2 // "12"
1+"2js" // "12js"
1+2+"1" // 31
(1+"2")+1 // 121
대부분의 JavaScript 객체는 [object Object]로 변한된다.
name + {} // "name[object Object]"
다만 모든 JavaScript 객체는 어쨋든 객체를 문자열로 변환해주는 toString
메소드를 상속받는다.
const foo = {}
foo.toString() // "[object Object]"
const baz = {
toString:
() => "run toString"
}
baz.toString() + ! // "run toString!"
배열형에 상속된 toString
은 약간 다르게 동작한다. 인자가 없는 join
과 비슷하게 동작한다.
[1,2,3].toString() // "1,2,3"
[1,2,3].join() // "1,2,3"
[].toString() // ""
[].join() // ""
"me" + [1,2,3] // "me1,2,3"
4 + [1,2,3] // 41,2,3
4 * [1,2,3] // NaN
그래서 문자열 형태의 반환값이 기대되는 실행에 배열을 전달하면, 두 번째 피연산자로 toString
의 반환 값을 연결한다. 만약 숫자형이 기대되는 실행에 전달하면 반환 값을 숫자형으로 변환하려고 시도한다.
So when you pass an array where it expects a string, Javascript concatenates the return value of the toString method with the second operand. If it expects a number, it attempts to convert the return value to a number
4 * [] // 4
4 / [2] // 2
4 * Number([]) // 0
4 * Number([].toSring()) // 0
4 * Number("") // 0
4 * 0 // 0
객체가 숫자형이나 문자열로 예상되는 곳에 넘겨질 때 사용하기 위해 valueOf
를 정의할 수 있다.
const foo = {
valueOf: () => 3
}
3 + foo // 6
3 * foo // 9
객체에 toString
과 valueOf
메소드가 함께 정의되어 있는 경우, JavaScript는 valueOf
메소드를 대신 사용한다.
const bar = {
toString:() => 2,
valueOf:() => 5
}
"sa" + bar // "sa5"
3 * bar // 15
2 + bar // 7
valueOf
메소드는 객체를 숫자형 값으로 표현하는 것을 지원한다.
const two = new Number(2)
two.valueOf() // 2
모든 JavaScript 값은 true나 false로 강제 변환(coerced) 될 수 있다. 아래의 몇 안되는 값들은 falsy 값을 반환한다.
나머지 모든 값은 true이다.
if (-1) // truthy
if ("0") // truthy
if ({}) // truthy
위의 사실들로 충분하지만, 참인지 거짓인지 확실하기 위해선 명확하게 하는것이 좋다. 예를 들어, 아래의 코드 A 대신 코드 B를 사용하는 것이 낫다.
// Code A
const counter = 2;
if (counter)
// Code B-1
if (counter === 2)
// Code B-2
if (typeof counter === "number")
예를 들어, 함수를 숫자형으로만 동작하게 정의했다면, 이러한 검사를 통해 의도지 않은 동작을 방지할 수 있다.
// Uhm.....
const add = (number) => {
if(!number)
new Error("Only accepts arguments of type: number")
// code
}
add(0) // Error!
// Better Good!!
const add = (number) => {
if (typeof number !== "number")
new Error("Only accepts arguments of type: number")
//number
}
add(0) // no Error
NaN은 자신과 같지 않은 특별한 값이다.
NaN === NaN // false
const notANumber = 3 * "a" // NaN
notANumber == notANumber // false
notANumber === notANumber // false
NaN
은 JavaScript 내에서 유일하게 그 자신과 같지 않은 값이다. 그래서 검사하기 위해선 자기 자신과 비교해야 한다.
if (notANumber !== notANumber) // true
ECMAScript 6에서는 NaN을 확인하기 위해 Number.isNaN
이 제공된다.
Number.isNaN(NaN) // true
Number.isNaN("Number") // false
Global NaN
의 경우 확인하기 전해 형을 강제로 변환하는 것에 주의해야 한다.
isNaN("number") // true
isNaN("1") // false
Global NaN
은 피하는 것이 낫다. 이와 같이 동작하기 위해선 아래와 같이 작성하면 된다.
const coerceThenCheckNaN = (val) => {
const coercedVal = Number(val)
return coercedVal !== coercedVal ? true : false
}
coerceThenCheckNaN("1a") // true
coerceThenCheckNaN("1") // false
coerceThenCheckNaN("as") // true
coerceThenCheckNaN(NaN) // true
coerceThenCheckNaN(10) // false