산술 Arithmetic Operators, 논리 Logical, 할당 Assignment, 비교 Comparison 연산자 등이 존재한다.
연산자의 종류와 특징을 살펴보자.
1 + 1; // 2
1 - 1; // 0
1 * 2; // 2
2 / 1; // 2
3 % 2; // 1
Binary Arithmetic Operators 이항 산술 연산자는 2개의 Operands 피연산자를 가진다.
산술 연산이므로 두 피연산자는 숫자로 이루어진 number
타입이어야 하며, 다른 타입인 경우 암묵적으로 number
타입으로 변환한다.
그러나 변환 후에도 연산이 불가능한 경우(1 * "abc"
등) NaN
를 반환한다.
1++; // 2
++1; // 2
1--; // 0
--1; // 0
+1; // 1
-1 // -1
++
, --
는 피연산자의 값을 변경하는 Side Effect가 발생하기 때문에 주의해야한다.
let a = 1;
let b = 1;
a++; // a == 2
b+1; // b == 1
변수 a
에 재할당이 일어나지 않았음에도 불구하고 값이 증가되었다.
++
, --
연산자는 암묵적 할당이 일어나기 때문이다.
++
, --
가 앞에 붙으면 Prefix Operator 전위 연산자, 뒤에 붙으면 Postfix Operator 후위 연산자라고 부르며, 연산 결과는 같지만 암묵적 할당의 시점이 달라진다.
아래의 예시를 보면 이해가 될 것이다.
let a = 1;
console.log(a++); // 1 원래의 값 반환 후, 1을 증가시킨다
console.log(a); // 2
let b = 1;
console.log(++b); // 2 1을 증가시킨 후, 그 값을 반환한다
console.log(b); // 2
전위 연산자는 연산 -> 할당 -> 반환
후위 연산자는 반환 -> 연산 -> 할당
위 순서로 처리된다.
a
라는 원본 변수는 변경하지 않으면서 피연산자로 사용하고 싶다면 반드시 Binary Operator를 사용해야 한다.
+1 // 1
-1 // -1
-
연산자는 부호를 반전시키지만, +
연산자는 숫자 앞에 붙여도 아무런 영향이 없다.
그러나 만약 문자열 앞에 +
or -
를 붙이면 암묵적으로 number
로 타입이 변환된다.
그래서 문자열 -> 숫자로 변환을 할 때 사용된다.
단, +"a"
등 숫자 이외의 문자가 포함된 문자열의 경우 NaN
을 반환한다.
+"1" // 1
typeof "1" // string
typeof +"1" // number
typeof -"1" // number
+"a" // NaN
typeof
는 변수의 데이터 타입을 문자열로 반환하는 연산자이며 "1"
앞에 +
or -
를 붙였을 때 string
-> number
로 타입이 변환되는 것을 볼 수 있다.(Type Coercion)
+
연산자는 피연산자가 2개일 때 덧셈 연산, 1개일 때 부호 반전 연산을 수행하였다.
그러나 피연산자가 2개면서 그 중 하나라도 string
이 포함되면 문자열을 이어붙이는 문자열 연결 연산자 String Concatenation Operator로 작동한다.
1 + "2" // "12"
1 + (+"2") // 3
그래서 덧셈 연산을 하고 싶은 경우에는 +
로 단항 연산을 통해 미리 string
-> number
로 타입 변환을 해서, 모든 피연산자를 number
로 되게끔 만든 후 연산을 수행해야 한다.
그렇게 하지 않으면 의도한대로 작동하지 않아서 예기치 못한 오류가 발생할 수 있을 것이다.
let a;
a = 1;
a += 10; // a = a + 10;
a -= 1; // a = a - 10;
a *= 2; // a = a * 2;
a /= 2; // a = a / 2;
a %= 3; // a = a % 3;
a **= 2; // a = a ** 2;
let s = "abc";
s += "def"; // s = s + "def";
=
는 메모리 공간에 값을 할당하는 연산자다.
나머지는 이 할당 연산자에 추가적인 연산을 곁들인 것인데,
만약 a += 1;
이라는 연산을 수행한다면 이전에 a
가 갖고 있던 값에 1을 더하여 재할당한다는 의미다.
JS는 변수를 생성할 때, 운영체제로부터 다른 변수에 의해 사용중이지 않은 메모리 공간을 할당 받고, 그 안에 값을 저장한다고 하였다.
만약 어떤 변수가 임의의 메모리 공간을 할당 받아 그 안에 값을 가지고 있는 상태에서 재할당이 일어난다면 어떻게 될까.
같은 공간에 다른 값을 덮어 쓸 것이라는 생각이 들 수도 있겠지만, JS에서는 재할당이 일어날 때마다 새로운 메모리 공간을 다시 할당받아서 사용하게 된다.
예를 들어, 변수 a
가 0xFFFFFFFF
라는 주소에 대한 별칭이고, 1
이라는 값을 담고 있다면, 재할당이 일어났을 때 0xEEEEEEEE
등 임의의 주소를 다시 받아서 사용하는, 메모리 공간 자체가 바뀌어버리는 방식인 것이다.
이전에 사용하던 0xFFFFFFFF
는 더이상 사용이 되고 있지 않으므로 다른 변수에게 할당될 수 있도록 사용 가능한 메모리 공간으로 지정되어야 할 것이다.
이 과정을 Free, 메모리 해제라고 부르며 Garbage Collector, 가비지 콜렉터에 의해 수행된다.
만약 메모리가 해제되지 않고 계속 사용중인 공간으로 표시된다면, 해당 공간은 영원히 다른 변수에게 할당되지 못하고 낭비될 것이다. 이를 Memory Leak, 메모리 누수라고 부른다.
메모리를 할당, 해제를 개발자가 직접 해줘야하는 C, C++ 프로그래밍 언어를 Unmanaged Language라고 부르는데, 사람이 처리하다보니 숙련된 개발자가 아니라면 실수로 인해 메모리 누수가 매우 발생할 가능성이 높다.
JS처럼 메모리 할당, 해제를 자동으로 해주는 프로그래밍 언어를 Managed Language라고 부르며, Garbage Collector가 그 역할을 담당한다.
1 == 1; // true
1 === 1; // true
1 != 1; // false
1 !== 1; // false
두 피연산자가 일치하는지 아닌지 비교하는 연산자로, 일치 여부에 따라 true
or false
를 값을 반환한다.
주의할 점은 !=
과 ==
의 작동 방식이 상당히 난해하기 때문에 사용이 권장되지 않는다는 것이다.
1 == "1"; // true
1 === "1"; // false
number
타입인 1
과 string
타입인 "1"
은 내부적으로 비트 패턴도 다르고, 애초에 다른 타입이기 때문에 비교가 가능하다는 것이 논리적으로도 말이 되지 않는다.
그래서 다른 언어에서는 이렇게 다른 타입 간에 비교 연산을 수행하려고 하면 오류를 발생시킨다.
그러나 JS의 비교 연산자들은 피연산자의 타입이 달라도 그대로 작동시키며, 특히 ==
, !=
연산자들은 타입이 다른 경우, 멋대로 타입을 변환시켜서 비교를 수행한다.
적어도 ===
, !==
연산자는 타입이 다르면 연산을 수행할지언정 false
를 뱉어서 논리적으로 올바른 결과를 도출하여 결과를 예측하기 쉽기 때문에 권장되는 편이다.
NaN === NaN; // false
단, 숫자가 아님을 나타내는 변수 NaN
을 비교하는 경우 같은 값을 비교했음에도 불구하고 false
를 반환하기 때문에 이 경우 isNaN()
을 사용해야 한다.
Number.isNaN(NaN); // true
1 > 0; // true
1 < 0; // false
1 <= 1; // true
1 >= 0; // true
"1" > "0"; // true
1 > "0"; // true
1 > "x"; // false
수의 대소를 비교하는 대소 비교 연산자다.
string
을 사용하는 경우 자동으로 number
로 변환한 뒤 실행된다.
x
등 대소 비교가 불가능한 string
을 넣는 경우 false
를 반환할 것이다.
<conditional expr> ? <expr1> : <expr2>;
의 형태로 표현되는 연산자로, 3개의 피연산자가 사용된다.
true ? 1 : 0; // 1
false ? 1 : 0; // 0
?
앞에 들어가는 조건식, <conditional expr>
이 반환값을 결정하고 만약 boolean
타입이라면 true
인 경우, 2번째 값인 1
을 반환하고 false
라면 3번째 값인 0
을 반환한다.
"some" ? 1 : 0; // 1
"" ? 1 : 0; // 0
" " ? 1 : 0; // 1
조건식에 string
타입이 들어갈 때는 ""
빈 문자열인 경우 false
취급되어 2번째 값을, 나머지 경우에는 1번째 값을 반환한다.
참고로 ""
가 빈 문자열이며 " "
은 공백을 하나 포함하고 있기 때문에 길이가 1인 문자열로 취급된다.(빈 문자열이 아니다)
undefined ? 1 : 0; // 0
null ? 1 : 0; // 0
빈 공간임을 나타내는 타입인 undefined
와 null
는 false
로 취급되어 마지막 값을 반환한다.
[] ? 1 : 0; // 1
{} ? 1 : 0; // Uncaught SyntaxError: Unexpected token '?'
let obj = {};
obj ? 1 : 0; // 1
아직 언급하지는 않았지만 []
는 여러 개의 데이터를 담을 수 있는 배열인데, object
타입 중 하나이다.
여러 개의 number
를 하나의 배열 안에 넣고 싶다면 let arr = [1, 2, 3, 4, 5, 6];
와 같이 사용하면 된다.
문자열과 달리 배열은 비어있더라도 true
처럼 취급되어 1번째 값을 반환한다.
object
도 마찬가지다.
단, object
는 객체 리터럴 형태로 넣었을 때 오류가 발생하는 것을 볼 수 있다.
빈 object
를 담고 있는 변수를 조건문으로 사용했을 경우는 배열과 마찬가지로 비어있더라도 true
처럼 취급된다.
3가지 논리 연산자가 있다.
NOT: !
AND: &&
OR: ||
!true; // false
true && true; // true
true && false; // false
true || false; // true
false || false; // false
!
은 NOT 부정의 의미로, true
를 false
로, false
를 true
로 반전시킨다.
그리고 단일 피연산자를 갖는다.
주의할 점은 true
, false
만 존재하는 boolean
뿐만 아니라, 숫자, 문자열 등의 다른 데이터 타입에도 사용할 수 있다는 것이다.(데이터 타입이 가진 값에 따라 자동으로 boolean
으로 변환)
다른 데이터 타입들을 !
로 연산했을 때 어떤 결과가 나오는지 살펴보자.
// number
// 0은 false, 나머지는 true 취급하여 반전
!1; // false
!0; // true
!-1; // false
// string
// 빈 문자열은 false, 나머지는 true 취급하여 반전
!""; // true
!" "; // false
!"a"; // false
// object
// 빈 배열, 객체 모두 true 취급하여 반전
![]; // false
![1]; // false
!{}; // false
&&
와 ||
은 두개의 피연산자를 갖는다.
&&
은 두 피연산자 모두 true
인 경우에만 true
를 반환하며 하나라도 false
라면 false
를 반환한다.
||
은 하나라도 true
라면 true
를 반환한다.
true && true; // true
false && true; // false
true || false; // true
false || false; // false
boolean
이외에 다른 데이터 타입을 사용하면 어떤 값이 반환되는지 아래 코드를 살펴보자.
"a" || false; // "a"
"a" || true; // "a"
[] || false; // []
{} || false; // Uncaught SyntaxError: Unexpected token '||'
true && "a"; // "a"
false && "a"; // false
처음 보면 조금 혼란스러울 수도 있는데, 우선 Short Circuit, 단축 평가라는 것에 대해 알아보자.
true || false; // true
OR 연산은 2개의 피연산자 중 하나라도 true
면 true
를 반환한다고 했다.
고로 첫번째 피연산자가 true
인데 두번째 피연산자를 검사하는 것은 자원 낭비기 때문에 두번째 피연산자는 검사하지 않은채 종료된다.
AND 연산은 반대로 첫번째 피연산자가 false
라면 두번째 피연산자와 관계 없이 무조건 false
가 반환되기 때문에 검사를 마친다.
이러한 개념을 단축 평가라고 부르며, 이를 boolean
이 아닌 다른 타입에 적용하면 검사가 멈추는 위치에 있는 값을 반환한다.
"abc" || false; // "abc"
string
타입의 값은 빈 문자열은 false
, 그 외는 true
취급을 하였다.
여기서도 "abc"
는 true
취급이 되기 때문에 피연산자가 하나라도 true
면 검사를 멈추는 ||
연산자는 검사가 멈춘 위치에 있는 값을 반환한다.
boolean
으로 변환은 일어나지 않기 때문에 그대로 abc
가 반환된다.
"abc" && "abc"; // "abc
&&
의 경우 반대로 앞의 값이 true
or 그에 상응하는 값인 경우, 뒤의 값을 변환 없이 그대로 반환한다.
익숙치 않다면 직접 실험해보는 것을 추천한다.
let a, b, c;
a = 1, b = 2, c = 3; // 3
좌 -> 우 순서대로 피연산자를 평가하고 마지막 피연산자의 평가 결과를 반환하는 연산자다.
한 번에 여러 변수에 값을 대입할 수 있다.
let a = 1, b = 2, c = 3;
선언 + 초기화를 동시에 할 수도 있지만 이렇게 되면 expression
이 아닌 statement
가 되기 때문에 반환값은 없다.
let a = b = c = 1;
위와 같이 여러 개의 변수를 같은 값으로 한 번에 초기화 할 수도 있다.
마찬가지로 statement
이므로 반환값은 없다.
let a = 1, b = 2;
a, b = b, a;
a // 1
b // 2
몇몇 언어에서는 위와 같은 방식으로 두 변수의 값을 교환할 수도 있지만, JS에서는 값이 그대로 나오는 것을 확인할 수 있었다.
조금 더 써줄게 많아지긴 하지만 아래와 같은 방법으로 교환이 가능하긴 하다는 것을 알 수 있었다.
let a = 1, b = 2;
[a, b] = [b, a];
a // 2
b // 1
let a = 1 + 2 * 3; // 7
let b = (1 + 2) * 3; // 9
괄호 안의 연산에 우선 순위를 둔다.
수학에서 괄호와 똑같은 개념이라고 보면 될 것 같다.
typeof 'abc' // "string"
typeof 1 // "number"
typeof NaN // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof null // "object"
typeof [] // "object"
typeof {} // "object"
typeof new Date() // "object"
typeof /test/gi // "object"
typeof function() {} // "function"
typeof
연산자는 각 변수의 데이터 타입의 이름을 string
으로 반환한다.
null
이 object
가 반환되는 것이 인상적이며, 아직 보지 않은 function
도 있다.
null
은 typeof
로 검사해서는 확인할 수가 없으니 반드시 ===
연산자를 사용하자.
typeof x; // "undefined"
한가지 더 주의할 점은 선언하지 않은 변수명을 typedef
의 피연산자로 줬을 때 오류가 발생하는 것이 아닌, "undefined"
가 된다는 것이다.
?.
??
delete
new
instanceof
in
나머지 연산자들을 모았다.
자세한 내용은 이후에 알아보도록 하자.
연산자도 우선순위와 진행 방향이 존재하는데, 아래 링크를 참고하고 전부 외울 수는 없으니 애매한 경우는 ()
를 사용하거나(모든 연산자 중에서 최우선순위를 갖는다) 문서를 다시 찾아보도록 하자.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence