by Douglas Crockford
시작하기 전에
- 자바스크립트는 이상한 corner case와 edge case로 가득 차 있다.
corner case
: 에상하지 못한 환경이나 입력 데이터 등으로 인해 프로그램에 문제가 발생하는 경우, 문제가 발생하는 환경을 동일하게 재현하기 어려워 디버그하기 힘듬edge case
: 데이터가 기대하는 범위의 최소 또는 최댓값일 때 프로그램에 문제가 발생하는 경우, 오버플로 or 언더플로
- 절대 코너 케이스와 엣지 케이스를 가까이하지 말라.
- 언어의 간결하고 분명한 부분만으로도 좋은 프로그램을 만들 수 있다.
- 가끔 언어의 모호한 면을 파고들곤 하는데, 이걸 굳이 프로그램에서 사용할 필요는 없을듯.
숫자
- js는 int형을 사용하지 않으므로 오버플로 문제가 발생하지 않는다.
- 오버플로가 발생하지 않기 때문에 자바스크립트의 정수는 훨씬 안정적이다.
- js의 number는 자바의 double과 밀접하게 64비트 2진 부동소수점 타입이다.
- js에는 18437736874454810627개 의 불변 숫자 객체가 내장되어 있는데, 각각은 고유한 숫자를 나타낸다.
와우! - 숫자 리터럴은 각 리터럴의 값과 가장 잘 맞는 숫자 객체에 대한 참조를 생성한다.
NaN
는 문자열을 숫자로 변환하려고 했으나 실패했을 때 결과값으로 반환될 수 있다.- typeof 연산자는 NaN 를 number형이라고 표시해서 아주 헷갈린다.
Number.prototype
은 모든 수가 상속하는 객체이다.더하기 부호(+)
는 만약 한 피연산자가 문자열이면, 이 연산자는 나머지 피연산자를 문자열로 바꾸고 연결한다.- js를 비롯한 많은 언어가 2진 부동소수점을 사용하기 때문에 0.1 + 0.2를 정확히 계산하지 못한다.
- 10진 소수점을 처리하는 단일 숫자 시스템이 나오기 전까지는 안전한 정수 범위에서 작업하자.
- js가 차용한 IEEE754 규격은 부동소수점 수의 밑수로 2를 사용한다.
- 1950년대 하드웨어는 밑수가 2인 부동소수점 시스템과 잘 맞았으나, 무어의 법칙 덕분에 이제는 상관없다.
- js 자체의 능력을 활용하면 되므로, 새로운 숫자형을 추가할 필요는 없다.
Boolean
- 불이 제대로 쓰이는 위치는 if조건문, !피연산자 등이다.
- 잘 설계된 언어라면 해당 위치들에 오직 불 값만 사용하도록 하지만, js는 여기에 아무 값이나 사용해도 된다.
- 불인 척하는 자료형에 속한 값은 truthy이거나 falsy 둘 중 하나이다.
falsy
: false, null, undefined, 빈 문자열(""), 0, NaNtruthy
: “false"나 “0"같은 문자열도 포함된다.
배열
- 배열은 객체 리터럴이 아닌 배열 리터럴을 사용해 만들어진다.
- typeof 연산자는 배열에 대해 object 를 반환하므로
Array.isArray
를 사용해 체크하자. - 최근 배열 요소를 한번에 하나씩 처리하기보다, 배열을 좀 더 함수처럼 처리해야 한다는 생각이 더 지배적이다.
- 변수(aka. i)를 두어 반복문을 통한 방식 -> map()과 같은 메서드를 통해 통째로 새로운 배열을 반환
- shift와 unshift 메서드는 pop과 push에 비해 느린데, 배열이 크면 특히 심해진다.
reduce
는 배열을 하나의 값으로 축약하며, 초기값을 전달하지 않으면 0번째 배열 요소가 초기 축약값이다.- 축약이라고 생각하니 reduce 명칭이 더 와닿는듯.
forEach
는 배열의 모든 요소에 대해 전달된 함수를 호출한다.- forEach나 find는 배열의 끝에 도달하기 전에 종료할 수 있지만, map, reduce, filter는 아니다.
sort
는 추가 메모리 공간을 사용하지 않고 배열 자체를 수정하므로 공유 중인 배열을 정렬하면 위험하다.
객체
- 객체에 찾을 수 없는 속성 값을 요청하면, 객체는 undefined라는 빈 값을 반환한다.
- 존재하지 않거나 빼먹은 속성 값을 요청하는 것은 에러가 아니므로, 이는 정상적인 동작이다.
- js 객체에서 오로지 null과 undefined만이 빈 값(bottom value)이다.
Object.create(prototype)
은 이미 있는 객체를 받아 이를 상속받는 새로운 객체를 반환한다. 즉, 기존의 객체가 새로운 객체의 ptorotype이 된다.- Object.create(null)을 쓰면 객체가 아무것도 상속받지 못하게 만들 수 있다.
- 객체의 속성에 값을 대입하면 가장 새로운 객체, 즉 프로토타입 체인의 가장 위쪽에 있는 객체만 변경된다.
WeakMap
은 문자열을 제외한 다른 객체를 키로 쓰고, 존재하는 키의 사본이 없다면 해당 키의 속성은 자동으로 삭제되기 때문에 js의 가비지 컬렉터와 잘 맞는다.
문자열
- 문자열은 0에서 65535 사이의 크기를 가지는 부호가 없는 16비트 정수로 이루어진 불변 배열이다.
- 문자열은 일반 배열과 다르게 항상 동결된 상태이고 === 연산자에 의해 동등하게 검사할 수 있다.
- 템플릿 문자열 리터럴은 여러 줄에 걸쳐 쓸 수 있는 문자열 리터럴이며, 백틱(`) 을 사용한다.
- 템플릿 문자열 리터럴은
interpoloation(${})
을 제공하는데 이는 XSS같은 보안에 취약하다. - 템플릿 문자열 리터럴을 함수로 전달해서 인코딩 후 조립해서 반환하면 어느정도 위험을 완화할 수는 있다.
Statement
- function문은 세미콜론으로 끝나지 않지만, let문과 선언문은 세미콜론으로 끝난다.
- function 선언문은 호이스팅되므로 함수 본문이나 모듈 안에 두는 것은 괜찮지만, if나 switch 등에 두는 것은 좋지 않다.
const
는 상수의 줄임말이지만, 함수의 실행이 끝나면 사라질 수도 있는 임시적인 거고 프로그램이 실행될 때나 함수가 호출될 때마다 다른 값을 가질 수도 있다.- js는 조건문이 ‘불인 척하는 값’이길 기대하지만, 개발할 때는 정확한 불 값으로 처리하자.
- 반복문을 만드는 가장 좋은 방법은 꼬리재귀(tail recursion)을 사용하는 것이다.
- js에는 break, continue, throw, return 4가지 중단문이 있는데 이들은 모두 goto가 아니다. (그러니 안심하자!)
함수
- 최초의 프로그램은 루틴이라고 불렸고, 같은 루틴을 서브루틴으로 만들었다.
- 유용한 여러 서브루틴을 묶어서 라이브러리라고 부르기 시작했다.
- 확산 연산자(…)를 인자 목록에서 쓰면 전개(spread)문라고 부르고, 매개변수 목록에서 쓰면 나머지(rest)문이 된다.
// rest function func(...args) { return args; } // spread func(...a);
- 함수가 호출되면 활성 객체(activation object)가 만들어진다.
- js의 활성 객체는 다른 객체와 마찬가지로 힙에 저장되고 가비지 컬렉터에 의해 처리된다.
- 함수 실행 코드뿐 아니라 함수 객체가 생성되는 시점에 활성화된 활성 객체에 대한 참조가 있기 때문에 비로소 클로저를 사용할 수 있는 것이다.
- 함수 객체가 외부 함수에 대한 활성 객체의 참조를 가지는 방식을
클로저
라고 한다.
예외
- js는 throw에 어떤 값이든 허용하므로 아무거나 던져도 된다.
- 함수의 모든 문장은 제각각
try ~ catch
를 가질 수 있다. - 하나의 함수에서 try를 두번 이상 쓰지 않도록 주의해야 한다.
- throw문이 실행되면 현재 함수에 대한 캐치맵을 참조하며, 지정된 catch절이 있으면 제어권을 얻어 실행된다. catch절이 없으면 함수를 호출한 곳을 찾아 여기가 새로운 위치가 된다.
- 이 과정을 반복해서 catch절을 찾을 때까지 가상의 호출 스택을 타고 내려간다.
- 호출 스택이 더 이상 없으면
처리할 수 없는 예외(uncaught exception)
가 된다. - catch절이 복잡해지면 오히려 원인을 추론하기 어려워진다.
- 이에 따라 finally절을 통해 본인이 저지른 잘못을 수습하려는 코드가 나오게 된다!
- 예외 처리는 스택을 거꾸로 되감으면서 진행된다.
- 예외로 발생된 값은 스택의 더 아래쪽에 있는 함수 호출로 전달된다.
- 스택은 아래에서 위로 쌓이므로 결국 가장 처음 함수를 호출한 곳까지 전달될 수 있다.
- 비동기에서는 스택이 매 회 비워지기 때문에 지역적인 예외만 처리할 수 있다.
프로그램
- HTML이 텍스트 형태였기 때문에, js 역시 웹페이지에 텍스트 형태로 삽입했다.
- js 엔진은 소스코드를 컴파일해서 기계어나 중간 언어, 혹은 둘 다로 변환할 수 있어 이식성이 뛰어나다.
- 최근에는 HTML 페이지에 js 코드를 포함하지 않는 것이 좋다는 생각이 지배적이다.
- 성능상 페이지에 있는 코드를 압축하거나 캐싱할 수 없다.
- 보안상 XSS 공격을 받을 수도 있다.
- 따라서 모듈화된 여러 파일들을 번들링을 통해 js 자체를 분리하는 것이다.
- 함수 바깥에 변수를 선언하는 더 좋은 방법은
모듈(module)
스코프에 선언하는 것이다.- import/export를 통해 명시적으로 모듈화할 수 있다.
- 모듈은 일반적으로 하나의 함수, 또는 함수로 가득찬 한 개의 객체를 export해야 한다.
- export문은 가장 아래에 한 번만 쓰는 것이 좋다.
this 그리고 클래스 없는 자바스크립트
- 프로토타입에 있는 함수가 어떤 객체에서 동작하고 있는지 알아내가 위해 this가 등장한다!
- 메서드가 발견되면 인자 목록을 주어 메서드를 호출하고, 자신을 동작시킨 객체에 대한 정보를 this라는 묵시적인 매개변수로 전달받는다. 따라서 오직 메서드 호출만 this 바인딩을 제공받게 된다.
- this는 동적으로 바인딩되므로 함수를 만든 쪽이 아닌 호출하는 쪽이 this를 결정한다.
- js의 아주 좋은 점 중 하나가 바로 객체 리터럴이다.
- 메서드만 갖고 있는 hard object: 클로저 내부에 있는 데이터의 완전함을 보장
- 데이터만 갖고 있는 soft data object: 함수에 의해 처리될 수 있는 편리한 데이터 집합
- js는 클래스가 없이도 클래스 형태의 프로그램을 만들 수 있다.
- js의 더 좋은 구조는
함수 컴포지션(functional composition)
이다.function test_constructor(spec) { let {memeber} = sepc; const reuse = other_constructor(sepc); const method = function () { // 이 스코프에서 spec, member, reuse, method를 사용가능 }; return Object.freeze({ method, goodness: reuse.goodness }); }
~
꼬리 호출
은 함수가 마지막으로 하는 일이 어떤 함수를 호출해 그 결과를 반환할 때 일어난다.- 활성 객체가 충분히 크다면, 새로운 활성 객체를 할당할 필요가 없어 재사용할 수 있다.
- 메모리 할당과 가비지 컬렉션에 드는 시간을 줄일 수 있다.
- 꼬리 호출 최적화를 통해 재귀 함수 호출을 반복문만큼 빠르게 할 수 있다.
- 단, try 블록 내의 꼬리 호출은 최적화되지 않는다.
- try는 catch로의 실행 흐름이 있기 때문에 활성 객체가 최적화되면 안된다.
- 이 세상은 비동기적이고, 완전히 분산되어 있으므로 프로그램을 최대한 순수하게 만들어야 한다.
- 순차적 언어는 입출력을 블록(block) 방식으로 처리한다.
- js는 탄생 목적 자체가 사용자와의 상호 작용이었기 때문에 순차적 모델에 영향을 덜 받는다.
- 비동기 프로그래밍은 콜백 함수와 프로세싱 루프 두 가지 아이디어에 근간한다.
- 이벤트 루프(프로세싱 루프)는 큐(queue)에서 가장 높은 우선순위인 이벤트 혹은 메시지를 가져와서 해당 이벤트나 메시지를 처리하도록 등록된 콜백 함수를 호출한다.
- 다른 서버나 시스템과 통신하는 상황에서는 작업의 단위별로 함수를 구분해야 한다.
리퀘스터(requester)
: 콜백 함수를 받고, 호출 즉시 반환되지만 바로 끝나지 않고 언젠가 끝나는 함수들- 작업의 단위(서버에 요청을 보내는 등)를 구분해 리퀘스터 함수들을 작성해 조합하면 된다!
- 그러나 이 작업을 위해 콜백지옥, 프로미스, async/await을 남발하고 있다.
- js가 사용하는 epoch는 유닉스로 1970-01-01이다.
- js에서 객체는 형식이 없는 이름:값 쌍의 집합이며, JSON은 이를 사용했을 뿐이다.
- 단위 테스트는 고수준으로 올라갈수록 효과가 작은데, 의존성이 증가하면서 테스트 의미가 없어지기 때문이다.
- 의미 없는 데이터로 결정하기 보다는, 더 읽기 쉽고 유지보수 하기 쉬운 기능을 골라야 한다.
- 빠르지만 이상하게 동작하는 것은 아무런 도움이 되지 못한다.
트랜스파일링(transpiling)
은 하나의 프로그래밍 언어를 다른 언어로 컴파일하는 특별한 형태이다.
다음 세대 언어는
- 프로세싱 루프, 메시지 디스패치, 순차적 전송을 포함해서 비동기 프로그래밍을 잘 지원해야 한다.
- 유니코드를 잘 지원해야 한다. 예를 들면 split 함수가 문자를 어떻게 조합하는지 알 수 있어야 한다.
- 내부 문자 표현 방식으로 UTF-32를 사용해야 한다.
blob(Binary Large OBject)
을 직접 지원해야 한다.- 데이터들이 크고 지저분하지만 멋지게 포장되길 원할 것이기 때문이다.
- 순수 함수의 병렬 처리를 지원해야 한다.
- CPU 성능이 점점 수가 많아지고 있어서 결국 병렬성을 통해 성능을 향상시킬 수 있을 것이다.
- 시작, 통신, 파괴와 같은 프로세스에 대한 관리를 지원해야 한다.
- 프로세스들을 연결해서 함께 종료될 수 있도록 만들 수 있어야 한다.