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 성능이 점점 수가 많아지고 있어서 결국 병렬성을 통해 성능을 향상시킬 수 있을 것이다.
- 시작, 통신, 파괴와 같은 프로세스에 대한 관리를 지원해야 한다.
- 프로세스들을 연결해서 함께 종료될 수 있도록 만들 수 있어야 한다.