개발/Javascript

[You don't know JS] Part1 - 5장. 문법

lanace 2020. 9. 6. 06:23

Ch5. 문법

자바스크립트에는 혼란과 오해를 살만한 많은 트릭과 묘수로 가득차있다.

어떤것들이 있는지 살펴보고 올바르게 코드를 짤 수 있도록 하자.

5.1 문(Statement) 과 표현식(expression)


Statement와 expression이 비슷해 보인다면 주의하자. 이 둘은 완전 다르다.

1 --- var a = 3 * 6;
2 --- var b = a;
3 --- b;

위의 코드에서 expression은 다음과 같다.

  • 3 * 6
  • b = a
  • b

마찬가지로 statement 는 다음과 같다.

  • var a = 3 * 6; ⇒ 선언문
  • var b = a; ⇒ 할당문
  • b; ⇒ 표현식 문

그래서 문장과 표현식의 정확한 정의란 무엇인가?

a && b 에서 b는 문장인가 표현식인가?

foo(1, 2); 는 문장인가 표현식인가?

5.1.1 문의 완료 값

모든 문은 완료 값을 가진다.

브라우저 콘솔창에 statement 를 치면 어떤 값이 찍히는데, 이게 완료 값이다.

var a = b;

위의 코드에서 완료값은 var문의 완료값이 undefined이기 때문에 undefined가 출력된다.

정확하게는 변수 정의 알고리즘은 실제로 할당받은 값을 반환하지만 이를 변수 선언문 알고리즘이 undefined를 반환하여 위와같은 현상이 발생하는 것이다.

  • 완료값을 얻어내는 방법

5.1.2 표현식의 부수 효과

대부분의 표현식에는 부수 효과가 없다.

부수 효과
ㅇㅇㅇㅇㅇㅇㅇㅇ

var a = 2;
var b = a + 3;

위의 코드는 부수효과가 없다.

함수의 부수 효과

function foo() {
    a = a + 1;
}

var a = 1;
foo();

위와 같은 코드에선 foo() 를 호출 했을때 a가 변경되는 부수효과가 발생하게 된다.

++ 와 같은 연산자의 부수 효과

delete 연산자의 부수 효과

할당 연산자 (=) 의 부수 효과

이러한 효과를 이용하여 연쇄 할당문을 사용할 수 있다.

var a, b, c;

a = b = c = 42;

c = 42에서 결과값은 42가 되고, b = 42가 된다.

이렇게 연쇄적으로 할당이 가능하다.

var a = b = 42;

잘못된 예시

이 코드는 b를 선언하지 않았기 때문에 에러가 난다.

할당 연산자의 부수효과 예시2

function vowels(str) {
    var matches;

    if (str) {
        matches = str.match(/[aeiou]/g);

        if (matches) {
            return matches;
        }
    }
}

vowels("Hello World");
function vowels(str) {
    var matches;

    if (str && (matches = str.match(/[aeiou]/g))) {
        return matches;
    }
}

vowels("Hello World");

위와 같이 코드를 줄일 수 있다.

두 조건이 서로 상관관계가 있어서 두번째 축약 코드를 더 선호하는 사람도 있다.

개인 취향이다. 존중. 필요.

5.1.3 콘텍스트 규칙

콘텍스트 규칙이란, 같은 코드지만 서로 다르게 인식하고 동작하는것을 의미한다.

자바스크립트에는 이런 것들이 꽤 있는데 자세히 알아보자.

중괄호 - 객체 리터럴과 레이블

var a = {
    foo: bar()
}

var a = {};
a.foo = bar();
{
    foo: bar()
}

위 두개의 코드의 차이점은 var a = 의 유무이다.

중괄호 ({, })는 각각 객체 리터럴과 레이블로 해석된다.

후자의 중괄호는 레이블 기능때문에 정상적인 구문으로 해석되는데, 기존 중괄호가 if 문이나 while 문에 있는 중괄호와 동일하게 인식된다.

레이블 구문에 대해서 자세히 알아보면 다음과 같다.

레이블은 제한적인 goto 구문을 사용할 수 있도록 해준다.

continue나 break 문 다음에 해당 레이블 명을 넣어주면 해당 레이블 위치로 점프할 수 있다.

레이블 구문은 사용 빈도가 극도로 낮고 개발자에게 혼란을 야기하므로 피하는걸 권장한다.
하지만 사용했을때 큰 이득을 가져다 준다면 꼭 주석과 문서화를 하여 혼란을 피하자.

한가지 더 주의해야 할 항목이 있는데, 바로 JSON이다.

{
    "a": 42
}

위의 코드는 JSON 형태지만 javascript에서 이를 JSON으로 해석하지 않고 레이블로 인식한다.

하지만 레이블은 문자열로 생성이 불가능하므로 에러가 발생하게 될것이다.

이것을 JSON-P 기법을 사용하여 호출할 수 있는데, 바로 객체 리터럴로 인식 시키는 것이다.

foo({"a": 42});

위와 같이 표현된다면 JSON은 객체 리터럴로 인식되어 foo에 정상적으로 호출될 것이다.

JSON-P 에 대해서 좀더 자세히 알아보자

var key = 'abc';

var json = {
    [key]: 3
}

json;

블록

[] + {};  // "[object Object]"
{} + []; // 0

객체 분해

else if 와 선택적 블록

if (a) {

} else if (b) {

} else if (c) {

} else {

}
if (a) {

} else {
    if (b) {

    } else {
        if (c) {

        } else {

        }
    }
}

5.2 연산자 우선순위


연산자 우선순위란 표현식에 연산자가 여러개 있는 경우 어떤 규칙에 의해 처리되는지를 정의한것이다.

예시 1 - 대입 연산자와 , 연산자

var a = 42, b;
b = (a++, b);

a;  // 43
b;  // 42
var a = 42, b;
b = a++, b;

a;  // 43
b;  // 43

, 연산자가 = 연산자보다 우선순위가 낮기 때문.

즉, b = a++ 가 먼저 계산된 뒤 ,b 가 계산되기 때문이다.

예시 2 - 대입 연산자와 선택 연산자

if (str && (match = str.match(/[aeiou]/g))) {
    // do something...
}
if (str && match = str.match(/[aeiou]/g)) {
    // do something...
}

아래는 에러가 날것이다.

이유는 && 연산자가 = 연산자보다 우선순위가 높기때문에 str && match 가 먼저 연산되고,

결과값에 str.match(/[aeiou]/g) 를 대입하므로 에러가 날것이기 때문이다.

예시 3 - 삼항 연산자와 선택연산자 (?, && , ||)

var a = 28;
var b = 'foo';
var c = false;

var d = a && b || c ? c || b ? a : c && b : a;

d;  // 28

개 쓰래기같은 코드다... 이렇게는 하지 말자 진짜로...

이제 위 결과가 어떻게 나왔는지 하나씩 살펴보자.

5.2.1 단락 평가

&& 연산자가 || 연산자보다 우선순위가 높다.

선택 연산자 (&&와 ||) 는 앞에 결과를 먼저 확인하고, 뒤를 확인할 필요가 없다면 뒤의 단락은 실행조차 하지 않는다.

사실 이걸 단락 평가로 그냥 넘기기엔 아쉬운 주제이다.
왜냐하면 이 특성을 살려서 매우 유용하게 활용이 가능하기 떄문이다.

이후에 실행할 일이 없다면 뒤를 보지않는다는건 성능적인 측면에서도 매우 이득이다.
다른 언어에서도 이러한 부분을 사용하고 있다.

5.2.2 끈끈한 우정

선택연산자 (&&와 ||)는 삼항연산자 (?:) 보다 우선순위가 높다.

5.2.3 결합성

일반적으로 연산자가 왼쪽부터 그룹핑이 일어나는지, 오른쪽부터 그룹핑이 일어나는지에 따라서 좌측 결합성우측 결합성으로 나뉜다.

var result = a && b && c

&& 연산자는 좌측 결합성을 갖으므로 (a && b) && c 와 동일하다.

var result = a ? b : c ? d : e;

// (a ? b : c) ? d : e;
// a ? b : (c ? d : e);

삼항 연산자는 우측 결합성을 갖는다.

따라서 a ? b : (c ? d : e) 이다.

그냥 삼항연산자가 두개 쓰는걸 피하는게 좋을것 같다.

var a, b, c;

a = b = c = 28;

대입연산자 = 도 우측 결합성을 갖으므로 a = (b = (c = 28)) 로 해석된다.

5.2.4 분명히 하자

모든 우선순위와 결합성을 다 이해하고 코드를 짜야할까?

or 괄호를 사용해 그룹핑을 해주는게 좋을까?

⇒ 매우 주관적인 문제

⇒ 논란의 연속

결론적으로 중용하자.

적절히 우선순위와 결합성을 사용하고 깔끔한 코드를 유지하자

5.3 세미콜론 자동 삽입


ASI (Automatic Semicolon Insertion)은 javascript에 세미콜론이 누락된 경우 엔진이 자동으로 넣어주는것을 말한다.

단, 개행에만 적용되며, 한줄에 있는 코드 사이에 세미콜론이 추가되는 경우는 없다

var a = 10;

do {
    // do something
} while (a)

a;

do-while 문에 끝에는 세미콜론을 넣어주는게 국룰이다.

하지만 이 사실을 아는 개발자가 얼마나 될까... 이럴때 ASI가 자동으로 넣어주는것이다.

주로 break, countinue, return, yield 에서 활약한다.

5.3.1 에러 정정

ASI에 의존해야 하는가?

찬성파


ASI가 보다 간결한 코드를 도와주는 도구이기 때문에 잘 이용해야 한다.

반대파


ASI를 어떻게 믿냐! 코드 달라지는거 책임질꺼?

의미가 달라질 수 있고, 실수할 가능성이 크다.

논란은 꽤 크니 적당히 의견정도로 듣자

ASI를 보편적인, 유효 개행 문자 규칙쯤으로 여기고 코드를 작성하면 곤경에 처하게 될것이다.
ASI가 유효 개행 문자를 넣어주는 것처럼 착각하지 마라

  • Brendan Eich (javascript 창시자)
var foo(a) {
    return
    a * 2
    ;
}

foo(2);  // 'undefined'

5.4 에러


런타임 에러도 있지만 실행 전에 컴파일 타임에 발생하는 에러도 존재.

조기 에러 (컴파일 타임 에러, early error)

  • 구문 에러

      var a = ,;  // error
  • 정규 표현식 에러

      var a = /+foo/;  // error
  • strict mode에서 함수 인자 중복

      function bar(a, b, a) {
        "use strict";
          // do something...
      }  // error
    
      var a = {
          b: 20,
        b: 'b'
      }  // error

5.4.1 너무 이른 변수 사용

ES6에서 임시 데드존 (TDZ) 이라는 개념이 생겼다.

TDZ는 아직 초기화 되지 않은 변수를 참조할 수 없는 코드영역이다.

{
    a = 2; // ReferenceError
    let a;
}

a = 2 를 실행할때 a 가 아직 TDZ 영역에 있으므로 에러가 발생한다.

{
    typeof a; // "undefined"
    typeof b; // ReferenceError

  let b;
}

typeof는 선언되지 않은 변수 참조시에 에러가 나지 않는데,

TDZ 참조할땐 에러가 발생한다.

5.5 함수 인자


var b = 3;

function foo(a = 4, var b = a + b + 4) {

}

이부분에서 b가 왜 아직 TDZ에 있는지 모르겠음.
게다가 a는 왜 TDZ를 지난 이후라 문제가 없는지도 모르겠음...

함수의 디폴트 값은 좌측 → 우측으로 let으로 선언된거랑 동일해서 TDZ에 간다는데... 뭘까

function foo(a = 42, b = a + 1) {
    console.log(a, b);
}

foo();  // 42, 43
foo(undefined);  // 42, 43
foo(5);  // 5, 6
foo(void 0, 7 );  // 42, 7
foo(null);  // null, 1 => null + 1 = 1 이기 때문...

argument 문제

디폴트값을 사용하게 되면 각 인자에는 값이 잘 들어가있지만 argument에는 값이 들어가지 않는다.

none strict mode에선 해당 인자가 값이 바뀌면 argument도 바뀌지만

strict mode에선 인자가 바뀌어도 연결되지 않는다.

argument는 ES6에서 비권장한다.
하지만 "인자와 이 인자에 해당하는 arguments를 동시에 참조하지 마라"는 규칙을 지키면 안전하다.

글세... 그냥 쓰지 말라면 쓰지 않는게 좋지 않을까...

5.6 try... finally


finally를 무슨짓을 해도 항상 실행되는 콜백함수 같다고 봐야된다.

function foo() {
    try {
        return 0;
    } finally {
        console.log('finally');
    }
}

foo();  
// "finally" 
// 0

try 에서 반환되거나 throw 되더라도 finally는 실행된다.

심지어 try 안에 continue나 break가 들어있어도 실행된다.

단, yield는 동작하지 않는다... 실제로 끝난게 아니기때문에...!

만약 finally에 return 값이 있다면 이전에 return된 값은 무시된다.

5.7 switch


switch의 비교 알고리즘은 === 이다.

중간에 default가 있을 순 있지만 위에 조건이 아니면 다 들어가므로 주의하자. ⇒ 걍 쓰지말자...

var a = 10;

switch (a) {
    case 1: 
        console.log(1);
    case 2:
        console.log(2);
    default:
        console.log('default');
    case 3: 
        console.log(3);
        break;
}

// 'default'
// 3

5.8 정리하기


위에 잘 읽자