Expressions and Operators

Primary Expressions

최소 단위의 표현식을 primary expression이라 합니다. 아래와 같은 것들이 있어요:

// literal
console.log(1.23, 'hello', /pattern/);

// language keywords
console.log(true, false);

// variable reference
let val = 10;
console.log(val, undefined);

undefined는 전역 객체의 프로퍼티 값이라서 리터럴이나 키워드가 아닌 varaible reference입니다:

console.log('undefined' in window);
console.log(window.undefined === undefined);

Object and Array Initializers

Array initializer 내부의 표현식은 매번 평가됩니다:

let f = () => [Math.random()];
console.log(f());
console.log(f());

쉼표 사이를 비워 undefined를 배열 사이에 넣을 수 있어요.

// 실행 컴포넌트의 한계(...)로 지금은 undefined가 안뜹니다
// 브라우저 콘솔에 넣어보세요!
console.log([1, , , 4, 5]);

Function Definition Expressions

객체를 만드는 객체 리터럴처럼 function definition expression은 함수 리터럴로 볼 수 있습니다. ES6에서 추가된 화살표 함수와 함께 챕터 8에서 자세하게 살펴볼게요.

let square = function (x) {
  return x * x;
};

Property Access Expressions

null과 undefined는 프로퍼티를 가지지 않습니다.

let val = 2; // null이나 undefined로 수정해보세요
console.log(val.toString());

대괄호를 사용한 프로퍼티 참조에서는 대괄호 내부의 표현식이 문자열로 변환됩니다.

let name = {};
name.toString = () => 'str';
console.log(String(name));

let obj = { [name]: 'name' };
console.log(obj);

대괄호를 사용한 방법은 프로퍼티 이름에 공백이나 문장부호가 있을 때, 프로퍼티 이름이 숫자일 때(배열), 프로퍼티 이름이 동적으로 정해질 때 사용돼요.

ES2020에서는 optional chaining을 사용한 프로퍼티 접근이 추가되었습니다. ? 왼쪽 값이 null/undefined면 최종 평가 결과가 undefined가 됩니다.

let a = null;
let b = { val: 10 };
console.log(a?.val, b?.val);
console.log(a?.b.c.d.e.f.g);

let c = { d: {} };

try {
  c.d?.e.f;
} catch {
  console.log('c.d가 객체여서 short-circuit되지 않습니다');
}

let arr;
let index = 0;
arr?.[index++];
console.log(index); // 대괄호 내부 표현식이 평가되지 않았습니다.

Invocation Expressions

Conditional invocation의 예시입니다:

function square(x, log) {
  log?.(x); // 함수인지 아닌지는 체크하지 않습니다.
  return x * x;
}

square(1, 1);

Conditional invocation의 short circuit 관련 특징입니다:

let f = null,
  x = 0;

try {
  f(x++);
} catch {}
console.log(x); // 인자로 전달된 표현식이 평가되었습니다.

f?.(x++); // 평가되지 않습니다.
console.log(x);

Object Creation Expressions

(굳이 싶지만 ㅎ,,) 생성자에 전달되는 인자가 없으면 아래와 같은 문법도 가능합니다:

// prettier-ignore
console.log(new Date);

Operator Overview

모든 자바스크립트 값들은 truthy 혹은 falsy합니다. 따라서 피연산자로 불 값을 받는 연산자는 자동 형변환 덕분에 무슨 값에 대해서든 잘 동작합니다.

변수, 객체 프로퍼티, 배열의 요소는 모두 lvalue입니다.

Side effect가 있는 연산자들:

  • Assigment operator
  • increment, decrement operator
  • delete operator
  • some function/object creation

할당 연산자는 우선순위가 아주 낮아 거의 마지막에 적용됩니다. 프로퍼티 접근(a.b) 및 함수 실행 연산자(f())는 우선순위가 아주 높습니다.

최근에 추가된 몇몇 연산자들은 우선순위가 명확히 정의되어 있지 않고 애매한 경우 아래와 같이 에러가 발생합니다:

-2 ** 10

연산자의 결합 방향(associativity)의 예제 코드입니다:

let a = 2,
  b = 1,
  c = 4;

// left-to-right associativity
console.log(a - b - c);

// right-to-left
// prettier-ignore
console.log(a ** b ** c);
console.log((a = b = c), a);

console.log(a ? b : c ? a : b);
// (a ? b : (c ? a : b))

표현식은 연산자의 우선순위나 결합 방향에 상관없이 왼쪽에서 오른쪽 방향으로 평가됩니다:

let helper = (n) => () => (console.log(n), n);

let f = helper(1);
let g = helper(2);
let h = helper(3);

console.log(f() + g() * h());
console.log(f() * g() + h());

Arithmetic Expressions

0/0은 NaN입니다.

Because that's how floating-point is defined (more generally than just Javascript)

stackoverflow.comIn JavaScript, why does zero divided by zero return NaN, but any other divided by zero return Infinity?It seems to me that the code console.log(1 / 0) should return NaN, but instead it returns Infinity. However this code: console.log(0 / 0) does return NaN. Can someone help me to

나머지(%) 연산자 결과의 부호는 첫번째 피연산자의 부호와 같습니다.

console.log(5 % 2, -5 % 2);

덧셈 연산자 알고리즘:

  1. 두 피연산자 중 하나라도 객체면 no-preference 알고리즘을 통해 원시값으로 변환합니다. 즉 Date 객체는 prefer-string, 나머지 객체는 prefer-number를 사용합니다.
  2. 피연산자의 변환 결과 중 하나라도 문자열이면 나머지도 문자열로 바꾸고 문자열 덧셈을 수행합니다.
  3. 아니면 숫자로 바꾸고 숫자 덧셈을 수행합니다.
let a = { valueOf: () => 1 };
let b = { valueOf: () => 2 };
let c = { valueOf: () => '3' };
let d = { toString: () => 1 };

console.log(a + b);
console.log(b + c);
console.log(d + 1);

참고할만한 링크에요:

Actually, no. Because in step 5 and 6, both operands are resolved into their primitives first, and this resolution is done without "hint". When there is no hint passed to ToPrimitive, Objects return their default values, which is the return of the valueOf() method.

stackoverflow.comHow can an empty string plus an object equal a numberv = { toString: function () { return 'foo' }, valueOf: function () { return 5 } } console.log('' + v); //5 console.log(v); // { [Number: 5] toString: [Function], valueOf: [Function] } Why does t...

갑자기 생각난 재미있는 상황:

let obj = { valueOf: () => obj };
console.log(Number(obj));

obj = { toString: () => obj };
console.log(String(obj));

시프트 연산자의 두 번째 피연산자는 0 이상 32 미만의 값만 가능하고 이를 위해 피연산자에서 하위 5개의 비트만 남깁니다:

console.log(32 >> (1024 + 1));
console.log(32 >> NaN);
console.log(32 >> Infinity);
console.log(32 >> -Infinity);

Unsigned right shift는 bigint를 쓸 수 없는 유일한 비트 연산자입니다.

This is because it fills the leftmost bits with zeroes, but conceptually, BigInts have an infinite number of leading sign bits, so there's no "leftmost bit" to fill with zeroes.

developer.mozilla.orgUnsigned right shift (>>>) - JavaScript | MDNThe unsigned right shift (>>>) operator returns a number whose binary representation is the first operand shifted by the specified number of bits to the right. Excess bits shifted off to the right are discarded, and zero bits are shifted in from the left. This operation is also called "zero-filling right shift", because the sign bit becomes 0, so the resulting number is always positive. Unsigned right shift does not accept BigInt values.

console.log(-1 >> 1);
console.log(-1 >>> 1);

console.log(-1n >> 1n);
console.log(-1n >>> 1n);

Relational Expressions

==는 레거시 취급합시다.

NaN은 그 어떤 값과도 같지 않습니다:

console.log(NaN === NaN, NaN == NaN);

let 난인가요 = (x) => x !== x;
console.log(난인가요(NaN));

두 문자열이 같아 보여도 다른 유니코드 시퀀스로 인코딩되어있으면 다릅니다.

== 연산자는 다음 규칙을 사용합니다:

// null과 undefiend는 같습니다
console.log(null == undefined);

// 숫자와 문자열 비교는 문자열을 숫자로 바꿉니다.
console.log(123 == ' 123');

// 하나가 불 값이면 이를 숫자로 바꿉니다.
console.log('1' == true);
// -> '1' == 1
// -> 1 == 1
// -> true

// 하나가 객체고 나머지가 숫자/문자열이면 객체를 원시값으로 변환하고 비교합니다.
let a = { valueOf: () => 1 };
console.log(a == 1);

let b = { toString: () => '!@#' };
console.log(b == '!@#');

// 나머지 조합은 모두 false입니다.

비교 연산자에서는 다음 규칙을 사용합니다:

// 둘 다 문자열이면 문자열 비교를 합니다.

// 하나라도 객체면 숫자로 변환합니다(valueOf 실패시 toString)
let a = { valueOf: () => 1 };
console.log(a < 2);

// 하나라도 문자열이 아니면 숫자로 변환 후 비교합니다.
let b = { toString: () => '  5' };
console.log(4 < b, b < 6);

// 하나라도 NaN이면 false입니다.

문자열 비교는 결과적으로 16비트 정수 배열의 비교라서 결과값이 예상과 다를 수 있습니다:

console.log('Z' < 'a');
console.log('Z'.localeCompare('a'));

비교 연산자와 다르게 + 연산자는 둘 중 하나라도 문자열이면 나머지를 문자열로 바꿉니다:

console.log('1' + 2);
console.log('11' < 3);

// 그래서 이런 식으로 문자열 형변환을 하는 경우도 있어요.
console.log(123 + '');

<=>=는 같은지는 체크하지 않고 >와 < 의 부정으로 정의됩니다.

in 연산자는 첫번째 피연산자를 필요시 문자열로 변환합니다:

let a = { toString: () => '0' };
console.log(a in [123]);
console.log(a in []);

Logical Expressions

방금 살펴본 relational operator들은 &&, || 보다 우선순위가 높습니다.

Side effect가 있는 표현식을 &&나 || 오른쪽에 쓸 때는 실행이 될 수도 안될 수도 있으니 주의합시다.

Assigment Expressions

let a;
let b = 1;
console.log((a = b) === 1, a);

a op= ba = a op b가 다른 경우가 있습니다:

// a op= b
let data = [0, 1, 2];
let i = 0;

data[i++] *= 2;
console.log(data);

// a = a op b
data = [0, 1, 2];
i = 0;

data[i++] = data[i++] * 2;
console.log(data);

Evaluation Expressions

let a = 1;
console.log(eval('a'));

지역 변수 접근으로 인한 최적화의 어려움때문에 인터프리터는 eval을 호출한 함수를 덜 최적화합니다. 그렇다고 alias된 이름으로 eval을 호출하는 경우까지 컴파일러가 알 수는 없으므로 이렇게 실행된 경우 eval은 전역 스코프에서 동작합니다. 이를 eval의 indirect call이라 하며 코드를 독립된 top-level 스크립트로 실행시킬 때 유용하게 활용됩니다.

let a = 1;
console.log((0, eval)('a'));

strict mode에서는 eval이 reserved word가 되는 등 함수보다 키워드에 가까워집니다.

Miscellaneous Operators

First-defined (??) 연산자는 첫 피연산자가 null이나 undefined면 두번째 피연산자를, 아니면 첫번째 피연산자를 반환합니다. Truthy함이 기준인 ||와 다르게 정의만 되어있으면 됩니다:

console.log(0 || 1);
console.log(0 ?? 1);

이상하지만 typeof null === 'object'입니다:

stackoverflow.comWhy is typeof null "object"?I'm reading 'Professional Javascript for Web Developers' Chapter 4 and it tells me that the five types of primitives are: undefined, null, boolean, number and string. If null is a primitive, why d...

void 연산자는 피연산자를 평가하고 undefined를 반환합니다:

let a = 0;
let inc = () => void a++;
console.log(inc(), a);

Comma operator는 왼쪽 표현식들의 결과를 버리기 때문에 side effect가 있는 표현식들을 쓸 때만 의미있습니다. 써본 적 없는 것 같아도 for문에서 종종 사용돼요:

for (let i = 1, j = 4; j; i++, j--) {
  console.log(' '.repeat(i) + '#'.repeat(j));
}