[JavaScript] 3-5. JavaScript operator in depth

JavaScript operator in depth


표현식 (expression)

코드 중에 값으로 변환될 수 있는 부분을 표현식이라고 부릅니다.

표현식을 값으로 표현하기 위해 실제로 해당 표현식을 실행시키는 잘차를 평가라고 합니다.



Short-circuit Evaluation(단축 평가)

피연산자가 두 개인 연산 중에, 값을 결정하기 위해 양쪽 피연산자 모두가 필요하지는 않은 경우가 있습니다.
아래 식에서는 expression 부분의 평가식이 평가될 필요가 있습니다.
그리고 JS 에서는 실제로 expression 부분이 평가되지 않습니다.
이런 평가 방식을 short-circuit evaluation 이라고 합니다.

false && expression;
true || expression;

&& 연산자와 || 연산자의 실제 동작 방식은 다음과 같습니다.

위 성질들을 이용해 if 구문을 흉내낼수 있습니다.

// `func1`과 `func2`는 동일하게 동작합니다.
function func1(cond) {
  if (cond) {
    console.log("조건을 만족합니다.");
  }
}

function func2(cond) {
  cond && console.log("조건을 만족합니다.");
}
// `func1`과 `func2`는 동일하게 동작합니다.
function func1(arg) {
  if (!arg) {
    arg = "hello";
  }
  console.log(arg);
}

function func2(arg) {
  arg = arg || "hello";
  console.log(arg);
}

특히 || 연산자는 ‘기본 매개변수’ 문법이 생기기 전까지 매개변수의 기본값을 지정하는 용도로 많이 사용됐습니다.
위에서 볼 수 있듯이 short-circuit evaluation 을 사용하면 코드의 길이가 줄어드는 효과가 있습니다.
다만 코드의 의미가 불명확해질 수 있고 논리적으로 놓치는 부분이 생길 수 있으니 주의해서 사용하세요.



삼항 연산자 (ternary operator)

a ? b : c 와 같이 쓰이는 삼항 연산자(ternary operator)는 조건부 연산자(conditional operator)라고도 불립니다.
앞의 표현식에서, a 가 truthy 이면 b 가, falsy 이면 c 가 반환됩니다.

console.log(true ? 1 : 2); // 1
console.log(false ? 1 : 2); // 2

삼항 연산자 역시 평가할 필요가 없는 부분은 평가하지 않습니다.

true ? console.log("left") : console.log("right"); // left
false ? console.log("left") : console.log("right"); // right

삼항 연산자 역시 if…else 를 대신하는 용도로 자주 사용됩니다.

// `func1`과 `func2`는 동일하게 동작합니다.
function func1(cond) {
  if (cond) {
    return true;
  } else {
    return false;
  }
}

function func2(cond) {
  return cond ? true : false;
}



증가/감소 연산자 (increment/decrement operator)

JavaScript 에는 1 단위로 정수의 증가/감소 연산을 할 수 있는 ++, – 연산자가 있습니다.

let num = 10;

num++;
console.log(num); // 11

num--;
console.log(num);

그런데 여기서 num++ 역시 표현식입니다. 즉, 값으로 변환된다는 말입니다.

let num = 10;
console.log(num++); // 10
console.log(num); // 11

분명 num++ 표현식이 평가된 이후에 num 의 값이 증가하기는 했습니다.
그런데 num++ 표현식 자체는 증가시키기 전의 값을 반환합니다.

증가 연산자에는 이러한 성질이 있습니다.


이 동작 방식은 감소 연산자(–)에도 적용됩니다.

let i = 3;
while (i--) {
  // 평가를 하고 감소되는 형식이라 i가 1일떄 0으로 감소하긴 하지만 평가가 된 이후이기에 console.log(..)은 실행됩니다.
  console.log("감소 연산자를 뒤에 쓰면 어떻게 될까요?");
}

let j = 3;
while (--j) {
  console.log("감소 연산자를 앞에 쓰면 어떻게 될까요?");
}

증감 연산자의 성질을 활용하면, 코드를 조금 더 짧게 작성할 수 있습니다.

function Counter(initial = 0) {
  this._count = initial;
}

// `this._count`를 1 증가시키면서도 증가시키기 전 값을 반환하는 코드를,
Counter.prototype.longInc = function() {
  const result = this._count;
  this._count += 1;
  return result;
};
// 아래와 같이 짧게 쓸 수 있습니다.
Counter.prototype.inc = function() {
  return this._count++;
};



할당 연산자 (assignment operator)

= 연산자를 비롯해, 연산 후 할당을 하는 +=, -= 등등의 연산자 역시 모두 피연산자와 함께 표현식을 이룹니다.

let x;
console.log((x = 5)); // 5

할당 연산자에 대한 표현식의 결과값은 왼쪽 피연산자에 실제로 할당된 값이 됩니다.

let x = 5;
console.log((x += 5)); // 10

let y = 6;
console.log((x -= 3)); // 3



연산자 우선 순위 (Operator Precedence)

연산자 여러 개가 연이어 사용된 표현식에서는, 연산자 우선 순위(operator precedence)에 따라 어떤 연산자를 먼저 계산할지가 결정됩니다.

// `+` 연산자가 왼쪽에 있지만, `*` 연산자의 우선 순위가 더 높으므로 먼저 계산됩니다.
2 + 3 * 4; // 14

JavaScript 에는 많은 연산자가 있고, 이 연산자들 간의 우선 순위도 복잡합니다.

// ???
typeof "helloworld" === "hello" + "world";

위와 같이, 서로 다른 연산자를 이어서 사용한 경우에는 코드의 의미를 명확히 파악하기 어렵게 됩니다.
같은 연산자를 연이어 사용한 경우가 아니라면,
코드 가독성을 위해 가장 높은 우선 순위를 갖는 연산자인 괄호로 둘러싸 주는 것이 좋습니다.

// 연산 순서가 명확해졌습니다.
typeof ("helloworld" === "hello" + "world"); // boolean



연산자 결합 순서 (operator associativity)

어떤 연산자는 같은 연산자를 연이어 쓴 경우에 왼쪽부터 결합되어 계산됩니다.

// 위아래 식은 완전히 같은 방식으로 동작합니다.
1 + 2 + 3 + 4 + 5
(((1 + 2) + 3) + 4) + 5;

// 왼쪽부터 결합되어, 처음으로 등장하는 falsy 값이 표현식의 결과값이 됩니다. 나머지는 평가되지 않습니다.
a && b && c && d;
((a && b) && c) && d;

// 왼쪽부터 결합되어, 처음으로 등장하는 truthy 값이 표현식의 결과값이 됩니다. 나머지는 평가되지 않습니다.
a || b || c || d;
((a || b) || c) || d;

연산자의 결합성 때문에, 수학에서 쓰이는 식을 JavaScript 에서는 그대로 쓸 수 없는 경우가 있습니다.

// 위아래 식은 완전히 같은 방식으로 동작합니다.
// 결과적으로 `true > 1`이 되어 결과값이 `false`가 됩니다.
3 > 2 > 1;
(3 > 2) > 1;
true > 1; // false

// 세 개의 수에 대한 비교를 하고 싶다면 아래와 같이 해야 합니다.
3 > 2 && 2 > 1; // true

그리고 어떤 연산자는 오른쪽부터 결합되어 계산됩니다.

// 위아래 식은 완전히 같은 방식으로 동작합니다.
2 ** 2 ** 3; // 256
2 ** (2 ** 3); // 256

// 위아래 식은 완전히 같은 방식으로 동작합니다.
let x, y, z;
z = y = x = 1
z = (y = (x = 1))

// 위아래 식은 완전히 같은 방식으로 동작합니다.
a ? b : c ? d : e ? f : g;
a ? b : (c ? d : (e ? f : g));
// a가 true이면 b, 아니면 c가 true일 경우 d, 아니면 e가 true일 경우 f, 아니면 g
// if...else 구문을 쓰면 되지 않은가 싶지만 if...else는 표현식이 아닙니다. 반면에 삼항 연산자는 표현식입니다.
// 삼항 연산자를 연속으로 이어서 쓸 수 있다는 것을 기억하도록 합니다.

대부분의 경우 결합 순서가 명확히 눈에 보이므로 결합 순서에 대한 걱정을 크게 할 필요는 없습니다.
다만, 거듭제곱 연산자, 할당 연산자, 삼항 연산자가 우결합성을 가진다는 사실은 기억해 둘 필요가 있습니다.



값을 비교하는 여러 가지 방법

JavaScript에서는 두 값이 같은지를 비교하기 위해 아래 세 가지 방법을 사용할 수 있습니다.

여기서 a != b는 !(a == b)와 항상 같습니다. a !== b 또한 !(a === b)와 항상 같습니다.



추상적 동일성 (Abstract Equality)

== 연산자는 두 피연산자의 타입이 다를 때는 타입을 변환한 후 비교합니다.
두 피연산자의 타입이 같다면 === 연산자와 같은 방식으로 동작합니다.

'1' == 1; // true
true == 1; // true
false == 0; // true
'' == false; // true

언뜻 보기에는 편해보이지만, 타입을 변환하는 과정에서 의도치 않은 방식으로 동작할 수 있기 때문에, 주의해서 사용해야 합니다.

'  \n\t  ' == 0; // true

다만, null check를 할 때 만큼은 == 연산자가 유용하게 사용됩니다. == 연산자는 아래과 같은 성질을 갖고 있습니다.

null == undefined; // true

null == 0; // false
null == ''; // false
undefined == false; // false
undefined == NaN; // false



엄격한 동일성 (Strict Equality)

===, !== 연산자는 두 피연산자의 타입이 다른 경우 무조건 false를 반환합니다.
따라서 ==, != 연산자와는 달리, 서로 다른 타입의 피연산자에 대해서도 안심하고 사용할 수 있습니다.

'1' === 1; // false
true === 1; // false
false === 0; // false

다만, number 타입에 대한 비교를 할 때에는 다음과 같이 특이한 동작을 합니다.

// `===` 연산에서, `NaN`은 number 타입의 **모든** 값과 다릅니다. 이는 자기 자신에 대해서도 마찬가지입니다.
NaN === NaN; // false

// `0`과 `-0`은 서로 다른 값이지만, `===` 연산은 이 둘을 같은 것으로 취급합니다.
0 === -0; // true



Object.is

Object.is 정적 메소드는 두 인수가 정말로 같은 값인지를 검사합니다.
아래의 두 예외를 제외하고는 === 연산자와 같은 방식으로 동작합니다.

Object.is(NaN, NaN); // true
Object.is(0, -0); // false

무엇을 써야 하나요?

특별한 경우를 제외하고는 === 혹은 !== 연산자를 사용해서 비교를 하면 됩니다.
다만 null check를 할 때 만큼은 == 혹은 != 연산자를 사용하는 것이 편합니다.



Spread Syntax

Spread 문법을 사용하면 배열(혹은 객체)을 다른 배열(혹은 객체)에 쉽게 삽입할 수 있습니다.
나머지 매개변수(rest parameters) 문법과 같은 기호인 …가 사용되지만, 그 의미는 다릅니다.

배열

Spread 문법을 통해 배열 리터럴의 중간에 다른 배열을 이어붙일 수 있습니다.
이 때, arr1 안에 있는 요소들이 arr2 안으로 복사됩니다.

const arr1 = [3, 4];
const arr2 = [1, 2, ...arr1, 5]; // [1, 2, 3, 4, 5]

// 이전에는 같은 작업을 하기 위해 `Array.prototype.concat` 메소드를 사용했습니다.
[1, 2].concat(arr1).concat([5]) // [1, 2, 3, 4, 5]

또한 배열 리터럴 안에 다른 요소를 써주지 않음으로써, 배열 전체를 쉽게 복사할 수 있습니다.

const arr1 = [1, 2, 3];
const arr2 = [...arr1];

// 이전에는 같은 작업을 하기 위해 `Array.prototype.slice` 메소드를 사용했습니다.
arr1.slice();
const arr1 = [1,2,3,[4,5]]; 
const arr2 = [...arr1];

arr2[0] = 30;
console.log(arr1[0]); //  1

arr2[3][0] = 40;
console.log(arr1[3][0]);  
// 40 - 얕은 복사가 되기 때문에 원시타입의 값은 변하지 않았지만 배열은 객체(참조)이기에 값이 변합니다.
// 값은 안변하게 하고 싶다면 깊은 복사를 하면 됩니다.

// 깊은 복사
const arr1 = [1, 2, 3, [4, 5]];
const arr2 = [
  ...arr1.slice(0, 3),  //  원시타입의 값 복사
  [...arr1[3]]  //  참조가 아닌 배열안에 배열을 직접 복사 = 깊은 복사
];

arr2[3][0] = 40;
console.log(arr1[3][0]);

다만, 이 때 역시 깊은 복사가 아니라 얕은 복사를 합니다.

Spread 문법은 함수 호출 시에도 사용할 수 있습니다.
이 때 배열의 모든 요소를 함수의 인수로 넘깁니다.

const arr = [1, 2, 3, 4, 5];

// 아래 코드는 `Math.max(1, 2, 3, 4, 5)`와 동일합니다.
Math.max(...arr); // 5

// 이전에는 같은 작업을 하기 위해 `Function.prototype.apply` 메소드를 사용했습니다.
Math.max.apply(null, arr); // 5

[1,2,3,[4,5,[6,7,8,]]]처럼 중첩되어 있는 배열을
[1,2,3,4,5,6,7,8]처럼 바꿔주는 것을 flatten이라고 하는데,
아직 JS에서는 추가되지 않았습니다.
그래서 위처럼 중첩된 배열을 바꾸고 싶다면 lodash(https://lodash.com/)에서
flatten deep을 이용하도록 합니다.

객체

객체에 대해서도 spread 문법을 사용할 수 있습니다.
이 때 자기 자신의(own) 열거 가능한(enumerable) 속성만을 복사합니다.

const obj1 = {prop: 1};
const obj2 = {...obj1};

obj2.prop = 2;
console.log(obj2.prop); // 2
console.log(obj1.prop); // 1 - 원시타입의 값이라 원래의 객체값에 영향을 안 미치지만, 객체안의 객체 혹은 배열이라면 영향을 미치게 됩니다.

// 이전에는 같은 작업을 하기 위해 `Object.assign` 정적 메소드를 사용했습니다.
Object.assign({}, obj1);

아직 몇몇 브라우저에 이 문법이 구현되어 있지 않기 때문에,
이 문법을 사용하려면 Babel 플러그인 혹은 TypeScript 등의 트랜스파일러를 사용해야 합니다.



분해대입 (destructuring assignment)

ES2015에서 배열과 객체 안에 들어있는 값을 쉽게 추출해낼 수 있는 문법이 추가되었습니다.

배열의 분해대입

다음과 같이, 변수의 선언과 동시에 배열의 요소를 해당 변수에 대입할 수 있습니다.

const [a, b, c] = [1, 2, 3];

console.log(a, b, c); // 1 2 3

만약 요소의 순서와 일치하는 변수가 좌측 목록에 들어있지 않으면, 해당 요소는 무시됩니다.

// 여기서 `2`, `4`는 무시됩니다.
const [a, , c] = [1, 2, 3, 4];

console.log(a, c); // 1 3

이미 선언된 변수에 대해서도 분해대입을 할 수 있습니다.

let a, b;
[a, b] = [1, 2];

console.log(a, b); // 1 2

배열이 중첩되어 있으면, 해당 배열에 대해서도 분해대입을 할 수 있습니다.
이 때에는 등호의 좌측에서도 배열이 중첩된 것처럼 써주면 됩니다.

const [a, b, [c, d]] = [1, 2, [3, 4]];

console.log(a, b, c, d); // 1 2 3 4

만약 분해대입 시 배열의 뒷부분을 새로운 배열로 만들고 싶다면, 해당 위치의 변수 앞에 …을 붙여주면 됩니다.
나머지 매개변수(rest parameter)에서와 같이, …은 맨 마지막 요소에만 붙을 수 있습니다.

const [a, b, ...c] = [1, 2, 3, 4, 5];

console.log(c); // [3, 4, 5]

객체의 분해대입

다음과 같이, 변수의 선언과 동시에 객체의 속성을 해당 변수에 대입할 수 있습니다.

const {a: prop1, b: prop2} = {a: 1, b: 2};

console.log(prop1, prop2); // 1 2

좌측 객체 표기에서 속성값 부분을 생략하면, 속성 이름 부분이 곧 새 변수의 이름이 됩니다.

const {a, b} = {a: 1, b: 2};

console.log(a, b); // 1 2

만약 어떤 속성의 이름과 같은 이름을 갖는 변수가 좌측에 들어있지 않으면, 해당 속성은 무시됩니다.

// 여기서 `b`는 무시됩니다.
const {a} = {a: 1, b: 2};

console.log(a); // 1

이미 선언된 변수에 대해서도 분해대입을 할 수 있습니다.

let a, b;
// 문장이 여는 중괄호(`{`)로 시작되면 이는 '블록'으로 간주되므로,
// 아래와 같이 분해대입을 할 때는 식 전체를 괄호로 둘러싸주어야 합니다.
({a, b} = {a: 1, b: 2});

console.log(a, b); // 1 2


// 위의 식과 아래의 식과 같습니다.
const obj = {a:1, b:2};
const {a, b} = obj;

const a = obj.a;
const b = obj.b;

객체가 중첩되어 있으면, 해당 객체에 대해서도 분해대입을 할 수 있습니다.
이 때에는 등호의 좌측에서도 객체가 중첩된 것처럼 써주면 됩니다.

const {a, b: {c}} = {a: 1, b: {c: 2}};

console.log(a, c); // 1 2

배열과 객체가 함께 중첩되어 있는 경우에서도 분해대입이 가능합니다.

const {
  arr: [
    a, b, {
      c
    }
  ]
} = {
  arr: [
    1, 2, {
      c : 3
    }
  ]
};

console.log(a, b, c); // 1 2 3

객체의 나머지 속성 (object rest properties)

만약 분해대입 시 무시된 속성들을 가지고 새로운 객체를 만들고 싶다면, …을 붙여주면 됩니다.
나머지 매개변수(rest parameter)에서와 같이, …은 맨 마지막에만 붙을 수 있습니다.

const {a, b, ...rest} = {a: 1, b: 2, c: 3, d: 4};

console.log(rest); // { c: 3, d: 4 }

아직 몇몇 브라우저에 이 문법이 구현되어 있지 않기 때문에,
이 문법을 사용하려면 Babel 플러그인 혹은 TypeScript 등의 트랜스파일러를 사용해야 합니다.

분해대입의 기본값

분해대입 시, 만약 좌측 변수의 위치에 해당하는 값이
우측의 배열 혹은 객체에 존재하지 않으면 거기에는 대입이 일어나지 않습니다.

let a, b, c;

[a, b, c] = [1, 2];

console.log(c); // undefined

이 때에 좌측 변수에 기본으로 대입될 값을 미리 지정해둘 수 있습니다.

// `c` 위치에는 대입될 값이 없으므로, 기본값인 `3`이 대신 사용됩니다.
let [a, b, c = 3] = [1, 2];

console.log(c); // 3

이 동작은 객체에 대한 분해대입에서도 적용됩니다.

let {a, b, c = 3} = {a: 1, b: 2};

console.log(c); // 3

매개변수에서의 분해대입

함수의 매개변수에서도 분해대입을 할 수 있습니다.

function func({prop, array: [item1, item2, item3 = 4]}) {
  console.log(prop);
  console.log(item1);
  console.log(item2);
  console.log(item3);
}

// 1, 2, 3, 4가 차례대로 출력됩니다.
func({prop: 1, array: [2, 3]});



2. Today I Found Out

연산자에 대해서 심화적으로 공부를 하면서
좀 더 연산자의 기능과 특징을 자세히 알 수 있었습니다.
특히 분해대입은 많이 인상깊었는데 
변수의 선언과 동시에 배열 또는 객체, 함수에서 대입이 가능하다는게 신기했고,
예제를 보고 이해를 하면서 흥미를 느낄수 있었습니다.
그 전에는 소스코드를 보면 이해가 아주 일부분만 되었는데
계속 공부를 할수록 더 많이 읽혀지는 것 같습니다.
앞으로도 더 공부를 열심히 해야겠습니다.



3. refer

https://helloworldjavascript.net/pages/245-operator-in-depth.html

http://beomy.tistory.com/18

https://devdocs.io/javascript/global_objects/object/is