First written: 23-01-17
Uploaded: 23-02-28
구멍난sparse 배열은 유의할 점이 있다.
var a = [];
a[0] = 1;
a[2] = [3];
a[1]; // undefined
a.length; // 3
또한 array는 object의 하위 타입이라 key - property 문자열을 추가할 수 있다.
var a = [];
a[0] = 1;
a["example"] = "hello";
a.length; // 1
a["example"]; // "hello"
a.example; // "hello"
length가 증가하지 않음에 유의하자. 다만 key로 들어간 문자열이 10진수로 변환이 되는 값이면 문자열은 숫자처럼 인식이 된다.
var a = [];
a["26"] = "hi";
a.length; // 27
Array-like object(유사 배열)의 값을 진짜 배열로 바꾸고 싶을 수 있다.
function changeToArray() {
var arr = Array.prototype.slice.call(arguments);
return arr;
}
changeToArray("a", "b", "c"); // ["a", "b", "c"]
Array.prototype으로부터 slice 메서드를 가져온다. call 함수는 다른 객체의 메서드를 parameter로 전달된 object가 본인의 메서드처럼 사용할 수 있도록 도와준다. 사용 방법은 fn.call(thisArg[, arg1[, arg2[, ...]]])
처럼 사용하면 되고, 이렇게 되면 thisArg에 전달된 메서드 fn이 바로 호출된다. arg1, arg2 등이 주어지면 thisArg.fn(arg1, arg2)
와 같이 동작한다. 따라서 위의 코드는 마치 아래와 같이 작동하는 것이다.
var arr = arguments.slice();
ES6부터는 아래와 같이 쓸 수도 있다.
function changeToArray() {
var arr = Array.from(arguments);
return arr;
}
changeToArray("a", "b", "c"); // ["a", "b", "c"]
string과 array를 생각해보면 둘은 닮은 점이 많다. length를 가져올 수 있고, index를 통해 접근할 수도 있다. concat 메서드도 사용 가능하다. 그래서 String도 Array-like object다.
물론 차이가 있다. String은 immutable(불변 값)이지만 Array는 mutable(가변 값)이다.
var a = "happy";
var b = ["h", "a", "p", "p", "y"];
a[4] = "Y";
b[4] = "Y";
a; // "happy"
b; // ["h", "a", "p", "p", "Y"]
따라서 String의 메서드들은 항상 새로운 값을 return하지만, 대부분의 Array 메서드는 그 즉시 기존의 array를 수정한다. 그래서 string의 유용한 메서드들을 array가 가져다 쓰는 것은 가능한 경우가 대부분이지만, string은 array의 메서드를 사실상 쓰기 어렵다.
var a = "foo";
a.join; // undefined
a.map; // undefined
var b = Array.prototype.join.call(a, "^");
var c = Array.prototype.map.call(a, function(v) {
return v.toUpperCase() + ".";
}).join("");
b; // "f^o^o"
c; // "F.O.O"
string이 array의 메서드를 쓰고 싶은 경우 split()
메서드를 사용해 아래와 같이 기교를 부린다.
var a = "string";
var b = a.split("").reverse().join("");
b; // gnirts;
Javascript의 숫자 type은 Number뿐이다.
정수를 아래와 같이 이상하게 표현하는 것도 가능하다.
var a = 6.;
a; // 6
소수점 이하 값이 0이기에 가능한 표현이다.
큰 값은 Exponent form(지수형)으로 표현할 수 있다.
var a = 1E10;
typeof a; // number
a; // 10000000000
a.toExponential(); // "1e+10"
숫자 value는 Number object Wrapper로 Boxing할 수 있다. 따라서 Number.prototype 메서드들에 접근할 수 있다. 하지만 주의할 점이 있다.
77.toFixed(3); // SyntaxError
(77).toFixed(3); // "77.000"
77..toFixed(3); // "77.000"
77 .toFixed(3); // "77.000"
0.77.toFixed(3); // "0.770"
이것은 77.
이 우선적으로 정수로 parsing되었기 때문이다.
다른 진법으로 숫자를 나타내는 것은 아래와 같다. 대표적인 binary(2진수), octal(8진수), hexadecimal(16진수)만 살펴보자.
var a = 0b11;
var b = 0o11;
var c = 0x11;
a; // 3
b; // 9
c; // 17
0.1 + 0.2 === 0.3; // false
IEEE 754 표준을 따르는 모든 언어에 상존하는 문제다. 이진 부동 소수점으로 표현된 0.1, 0.2, 0.3 모두 원래 숫자와 정확하게 일치하지 않는다. 이를 테면 0.3은 0.30000000000000004에 '가깝다'.
그렇다면 두 수의 비교는 어떻게 하면 좋을까?
function twoNumsCloseEnoughToEqual(num1, num2) {
return Math.abs(num1 - num2) < Number.EPSILON;
}
var a = 0.1 + 0.2;
var b = 0.3;
twoNumsCloseEnoughToEqual(a, b); // true
미세한 반올림 오차를 tolerance(허용 공차)로 처리하는 코드다. 이런 미세한 오차는 machine epsilon이라고 불리는데, Javascript의 machine epsilon은 2의 -52승이다. ES6부터 Number.EPSILON
형태로 접근이 가능하다.
표현된 값과 실제의 값이 일치한다고 확신할 수 있는 정수의 범위를 안전한safe 정수 범위라고 한다. Number.MAX_SAFE_INTEGER
에서 Number.MIN_SAFE_INTEGER
까지의 범위를 가지며 절대값은 9천조가 넘는다.
이와 같이 큰 숫자가 사용되는 경우는 주로 DB에서 64비트 id값을 설정하거나 할 때인데, 이런 한계 때문에 id값은 String으로 저장되어야 한다.
그리고 사실 비트 연산과 같이 숫자에만 가능한 연산이 있어 실제로 안전한 정수의 범위는 더 줄어들게 된다. 해당 값은 Math.pow(-2, 31)
에서 Math.pow(2, 31) -1
까지이다. 이 값들의 절대값은 대략 21억 전후의 숫자다.
sloppy mode에서는 undefined라는 식별자에 값을 할당하는 것이 가능하다. 물론 strict mode에서는 TypeError가 발생한다.
function fn1() {
undefined = 1;
}
fn1();
function fn2() {
"use strict";
undefined = 1; // TypeError
}
fn2();
그러나 두 mode에서 undefined라는 지역 변수는 만들 수 있다.
var undefined = 1;
또한 void 연산자를 가지고 undefined를 return할 수 있다. 드물게 어떤 표현식의 return 값이 없음을 명시하기 위해 사용한다.
function doSomething() {
if (!App.ready) {
return void setTimeout(doSomething, 1000);
}
var result;
/* ... */
return result;
}
if (doSomething()) {
/* ... */
}
setTimeout 함수는 타이머 취소 시 사용되는 고유 식별자를 return하기 때문에 이를 void로 undefined로 바꾸어주어 false positive(긍정 오류)가 없도록 구성했다. 물론 일반적으로는 아래와 같이 쓰는 것을 더 선호한다.
if (!App.ready) {
setTimeout(doSomething, 1000);
return;
}
var a = 1 / "string";
var b = "string2";
a == NaN; // false
a === NaN; // false
isNaN(a); // true
isNaN(b); // true
Number.isNaN(a); // true
Number.isNaN(b); // false
NaN은 그 어떤 NaN과도 같지 않다.
어쨌든 NaN인지 아닌지를 판단하기 위해 isNaN이 등장한다. 내장 전역 메서드인 isNaN()
은 일반 string에 대해서도 true를 return하기 때문에 예상과 다른 결과가 나오는 경우가 있지만, 이를 보완하기 위해 ES6부터는 Number.isNaN()
을 사용한다.
특이한 동등비교를 할 때 머리아프지 않도록 ES6는 Object.is()
메서드도 제공한다.
var a = 1 / "string";
var b = "string2";
Object.is(a, NaN); // true
Object.is(b, NaN); // false
Javascript에는 0으로 나누는 연산이 정의가 잘 되어 있다.
var a = 1/0;
var b = -1/0;
a; // Infinity
b; // -Infinity
또한 IEEE 754 부동 소수점을 사용하다 보니(유한 숫자 표현식Finite Numeric Representations), 아주 큰 수의 연산의 결과는 Infinity나 -Infinity가 될 수 있다. IEEE 754의 명세에 따르면 연산 결과가 너무 큰 경우, 가장 가까운 수로 반올림을 해서 그걸 토대로 결과를 정한다.
var maxNum = Number.MAX_VALUE;
maxNum + Math.pow(2, 969); // 1.7976931348623157e+308
maxNum + Math.pow(2, 970); // Infinity
그 결과 첫 번째 연산은 숫자로 표현되었으나 두 번째 연산은 Infinity가 결과값이 되었다.
0과 -0이 존재한다. 값의 음/양으로 어떤 변수값이 어디서부터 출발해 0으로 도달했는지를 나타내야 하는 애플리케이션(비디오 재생 방향 등)이 존재할 수 있기 때문에 그렇다.
var a = 0/-5; // -0
var b = 0/5; // 0
명세에 의하면 Number -0을 String으로 바꾸면 항상 "0"이다. 반대로 String "-0"을 Number로 바꾸면 항상 -0이다.
var a = 0/-5;
a.toString(); // "0"
a + ""; // "0"
String(a); // "0"
JSON.stringify(a); // "0"
+"-0"; // -0
Number("-0"); // -0
JSON.parse("-0"); // -0
0과 -0도 같다고 인식한다.
0 === -0; // true
function isNegZero(num) {
num = Number(num);
return (num === 0) && (1/num === -Infinity);
}
물론 NaN
에서와 마찬가지로 ES6부터 Object.is()
를 사용할 수 있다.
var a = 0/-5;
Object.is(a, 0); // false
Object.is(a, -0); // true
Javascript에서는 copy-by-value와 copy-by-reference만 존재한다. Pointer는 없다. 그리고 Primitives들은 전부 copy-by-value 방식을 따르고, object만이(하지만 function과 array 등이 같이 있는) copy-by-reference 방식을 따른다.
copy-by-value와 copy-by-reference는 무엇일까? copy-by-value는 어떤 값을 복사해올 때 memory에 담긴 value를 복사해온다는 뜻이다.
var a = 10;
var b = a;
b = 20;
a; // 10
b; // 20
할당이 이루어지는 시점에서 memory에 저장된 a
의 value를 가져와서 b
에 할당했기 때문에 b
를 건드려도 a
가 변화하지 않는다.
copy-by-reference는 어떨까?
var a = [1, 2, 3];
var b = a;
b.push(4);
a; // [1, 2, 3, 4]
b; // [1, 2, 3, 4]
할당이 이루어지는 시점에서 참조한 a
, 그러나 여기서 참조한 것은 value가 아닌 주소(reference)에 존재하는 value였기 때문에 b
에 변화가 생기면 a
에도 변화가 생기게 된다. 이것을 copy-by-reference라고 한다.
둘 다 value를 가져온 거 아닌가요? 무슨 차이죠? copy-by-value는 a
의 주소에 가서 value만 복사해온 뒤 b
에 할당했다. 그 뒤로 b
에 대한 작업을 할 때, 다시는 a
를 찾아뵙지 않는다. 반면 copy-by-reference는 a
의 "주소에 있는" value를 복사해 b
에 할당한 것이므로, b
에 할당된 해당 value가 존재하는 이상 변경 작업이 이루어지면 a
의 주소에 가서 거기에 존재하는 value에 대해 변경 작업을 실행하는 것이다.
그러나 주소 자체를 복사해오는 pointer와는 다르다.
var a = [1, 2, 3];
var b = a;
b = [4, 5, 6];
a; // [1, 2, 3]
b; // [4, 5, 6]
만일 b
가 pointer였다면, a
의 값도 바뀌었겠지만 copy-by-reference 방식을 따르므로 a
의 값이 변하지 않았다는 것을 확인할 수 있다. b
가 더 이상 a
주소에 있는 값이 아닌 다른 값이 할당되자, copy-by-reference는 a
에 대한 참조를 그만둔다. Pointer였다면 b
는 a
의 주소를 계속해서 참조했을 것이므로 a
의 값도 같이 변했을 것이다.
object에 copy-by-value 방식을 적용하고자 한다면 shallow copy(얕은 복사)를 실행해야 한다. 이럴 때는 parameter 없이 실행되었을 때 shallow copy를 실행하는 slice()
메서드를 사용하면 좋다.
b = a.slice();
Primitives에 copy-by-reference를 적용하고 싶다면, Primitives를 object(혹은 array 등)로 감싸야 한다.
function changeValue(wrapper) {
wrapper.value = 100;
}
var obj = {
value: 1
}
changeValue(obj);
obj.value; // 100
참고 서적 :
카일 심슨, 2017, 한빛미디어, 『YOU DON'T KNOW JS: 타입과 문법, 스코프와 클로저』