개발/아티클

ECMAScript 스펙을 읽는 법 (How to Read the ECMAScript Specification)

devmomori 2022. 3. 14. 08:30

이 글은 Timothy Gu의 How to Read the ECMAScript Specification를 번역한 글 입니다.
혼자 번역한 글로 의역과 오역이 있을 수 있습니다. 원문과 함께 읽는 것을 추천드립니다.
수정사항은 댓글이나 somedaycode@gmail.com 으로 알려주시면 감사하겠습니다.

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License, which is available at https://creativecommons.org/licenses/by-sa/4.0/. Parts of this work may be from another specification document. If so, those parts are instead covered by the license of that specification document.


요약

흔히 자바스크립트 스펙 문서 (ECMA-262)라고 읽는 ECMAScript 언어 스펙 문서는 자바스크립트의 복잡한 동작 방식을 이해하는 데 훌륭한 도구입니다. 그러나, 처음에 마주하게 되는 방대한 양의 글이 우리를 더욱 혼란스럽게 만듭니다.

이 문서는 가장 좋은 자바스크립트 참고 문헌인 ( ECMAScript )를 읽기 쉽게 하는 것을 목표로 합니다.

목차

1. 소개
1.1 ECMAScript 스펙을 읽어야 하는 이유
1.2 ECMAScript 스펙에 속하는 것과 속하지 않는 것
1.3 진행하기에 앞서, ECMAScript 스펙은 어디에 있나?
1.4 스펙 훑어보기

2. 런타임 의미 ( Runtime semantics)
2.1 알고리즘 단계
2.2 추상연산 ( Abstract operations )
2.3. [[This]] 란

  • 2.3.1 A field of a Record
  • 2.3.2 자바스크립트 객체의 내부 슬롯 ( An internal slot of a JavaScript Object )
  • 2.3.3 자바스크립트 객체의 내부 메소드 ( An internal method of a JavaScript Object )

2.4 Completion Records; ? and !

2.5 자바스크립트 객체 ( JavaScript Objects )
2.6 Example: String.prototype.substring()
2.7 Example: Can Boolean() and String() ever throw exceptions?
2.8 Example: typeof operator

용어 사전
Common abstract operations
색인
Terms defined by this specification
Terms defined by reference
참고 문헌
참고자료 - Informative References
이슈 인덱스 - Issues Index


1. 목차

ECMAScript를 매일 하루 건강을 위해 읽자고 다짐했습니다. 신년 목표 혹은 의사의 처방일 수도 있겠죠. 어찌 됐든, 환영합니다.

참고: 이 문서에서 "ECMAScript"라는 용어만 Specification 자체를 나타내고 "JavaScript"는 곳곳에서 사용됩니다. 두 용어는 동일한 것을 나타냅니다. (ECMAScript와 JavaScript 사이에는 몇 가지 역사적 차이점이 있지만 이에 대해 논의하는 것은 이 문서가 전달하고자 하는 범위를 벗어납니다. 이와 관련한 내용은 Google에서 쉽게 참고할 수 있습니다 .)

 

1.1 ECMAScript 스펙을 읽어야 하는 이유

ECMAScript 스펙은 자바스크립트로 구현된 브라우저[What's My Brower], Node.js 서버, 혹은 당신이 사용하는 IoT 기기[JOHNNY-FIVE]의 동작 방식을 이해하기 위한 신뢰있는 문서(source)입니다. 모든 자바스크립트 엔진 개발자들은 다른 자바스크립트 엔진처럼 새로운 기능이 의도한대로 작동하는지 확인하기 위해서 스펙에 의존합니다.

그러나 스펙 문서는 신화적 존재 같은 자바스크립트 엔진 개발자 뿐만 아니라, "평범한 JavaScript 코더인 당신에게도 유용하며 그저 당신이 아직 깨닫지 못한 것 뿐" 이라고 저는 말하고 싶습니다.

어느 날 직장에서 다음과 같은 peculiar juxtaposition을 발견했다고 가정해봅시다.

 

> Array.prototype.push(42)
1
> Array.prototype
[ 42 ]
> Array.isArray(Array.prototype)
true
> Set.prototype.add(42)
TypeError: Method Set.prototype.add called on incompatible receiver #<Set>
    at Set.add (<anonymous>)
> Set.prototype
Set {}

 

그런데 메소드가 프로토타입에서 동작하지만 다른 메소드는 프로토타입에서 동작하지 않는 이유로 매우 혼란스럽습니다.

 

구글은 내가 가장 필요할 때 도움을 주지 않고 항상 도움이 되는 스택오버플로우도 마찬가지죠.

스펙을 읽는 것이 도움이 될 수 있습니다.

또, 당신은 악명높은 loose equality operator (==) 가 실제로 어떻게 동작(function)하는지 궁금할 수 있습니다. 열심히 공부 중인 소프트웨어 엔지니어라면 MDN의 긴 문장을 읽는 것이 도움보다는 눈을 아프게 한다는 걸 알게 될 겁니다.

원문: you might wonder just how the heck does the notorious loose equality operator(==) actually function
(using the word "function" loosely here [WAT])


스펙을 읽는 것이 도움이 될 수 있습니다.

반면에 처음 JavaScript를 공부하는 개발자들에게는 ECMAScript 스펙을 읽는 것을 권하지 않습니다. JavaScript가 처음이라면 Web을 가지고 놀아보고 Web 앱을 만들어보세요. 자바스크립트로 만들어진 nannycams(보모용 몰래카메라)를 만들어도 좋고, 아무거나 다 좋습니다. JavaScript를 충분히 다뤄보았거나, JavaScript에 대해 걱정하지 않을 만큼 부자가 되었다면 이 글을 읽는 것을 고려해보세요.

이제 언어나 플랫폼의 복잡함을 이해하는 데 스펙을 읽는 것이 매우 도움이 될 수 있을 거라는 사실을 알았습니다.

근데 어떤 것들이 정확히 ECMAScript 스펙에 속하는 걸까요? (what exactly falls into in the realm of the ECMAScript specification?)


1.2 ECMAScript 스펙에 속하는 것과 속하지 않는 것

교과서적인 답변은 "언어 기능만 ECMAScript 스펙에 포함된다." 입니다. 이렇게 말하는 것은 "JavaScript 기능은 JavaScript 이다." 라고 말하는 것과 별다를 것 없고 도움이 되지 않습니다. And I’m not one for tautologies [XKCD-703].

tautologies: 불필요하게 같은 뜻의 말을 표현만 달리 하여 되풀이 하는 것 - naver 영어사전\


대신에, 자바스크립트 앱에서 흔히 볼 수 있는 몇가지를 나열하고 어떤 것이 (자바스크립트) 언어 기능인지 아닌지 알려드리겠습니다.

 

Syntax of syntactic elements (i.e., what a valid for..in loop looks like)
Semantics of syntactic elements (i.e., what typeof null, or { a: b } returns)
import a from 'a'; ❓[1]
Object, Array, Function, Number, Math, RegExp, Proxy, Map, Promise, ArrayBuffer, Uint8Array, globalThis, ...
console, setTimeout(), setInterval(), clearTimeout(), clearInterval() ✘[2]
Buffer, process, global* ✘[3]
module, exports, require(), __dirname, __filename ✘[4]
window, alert(), confirm(), the DOM (document, HTMLElement, addEventListener(), Worker, ...) ✘[5]

 

[1] ECMAScript 스펙은 이러한 선언 구문이 어떠한 것을 의미하는지 알려주지만 모듈이 로드 되는 방법은 명시하지 않았습니다.
원문: The ECMAScript spec specifies the syntax of such declarations and what they are supposed to mean, but does not specify how the module is loaded.

[2] 이것들은 모두 브라우저와 Node.js 에서 사용될 수 있지만, 비표준입니다. Node.js의 경우 관련 문서가 있습니다. 브라우저의 경우에는 console콘솔 표준이 있고, 나머지는 HTML 표준이 존재합니다.

[3] Node.js 문서에서 확인 할 수 있는 Node.js(-only) 전역 객체 입니다.
* 참고: global과 달리 globalThis는 ECMAScript에 속하며 브라우저에서도 구현되어 있습니다.

[4] These are Node.js-only module-wide "globals", documented/specified by its documentation.

[5] 모두 브라우저 전용 입니다.

 


1.3 더 자세히 훑어보기 전에, ECMAScript 스펙은 어디에 있을까?


구글에 'ECMAScript Spectification'을 검색하면 많은 결과들을 볼 수 있다. 모두 표준이라고 주장하는데, 어떤 것을 읽어야할까?

짧게 말해, tc39.es/ecma262/ 에 게시된 사양이 당신이 원하는 스펙일 겁니다.

길게 말하면:

ECMAScript Language 스펙은 Ecma International Technical Committee 39 로 알려진 다양한 배경을 가진 사람들이 모인 조직에 의해 개발되었습니다. TC39 는 ECMAScript 언어의 최신 스펙을 tc39.es 에서 유지하고 관리합니다.

*Ecma International Technical Committee 39 는 [TC39]로 더 친숙하게 알려져있습니다.


문제를 복잡하게 만드는 것은, 매년 TC39 이 에디션 번호와 함께 해당 년도 ECMAScript 언어 표준이 될 사양의 스냅샷을 찍을 시점을 선택하는 것 입니다. 예를 들어, 'ECMAScript® 2019 Language Specification (ECMA-262, 10th edition) - ES2019' 는 2019년 6월 tc39.es에 표시된 사양입니다. 이것은 포름알데히드에 넣어 수축포장하고 PDF 파일로 변환하여 영구 보관하고 있습니다.

포름알데히드는 방부제 등의 성분으로 사용한다.


그렇기 때문에 웹 어플리케이션이 2019년 6월부터 방부 처리된 브라우저에서만 작동하길 원하지 않는다면 항상 최신 스펙을 tc39.es에서 확인해야 합니다. 그러나 이전 브라우저나 Node.js 버전을 지원해야하는 경우에는 이전 사양을 참조하는 것이 도움이 될 것 입니다.
* 항상 최신 스펙을 보며 어플리케이션을 관리하고 뒤쳐지지 않아야 한다는 뜻 - 역자

Note: The ISO/IEC also republishes the ECMAScript Language Standard as ISO/IEC 22275 [ISO-22275-2018]. Don’t worry about it though, since the standard is basically a hyperlink to [ECMA-262].
참고: ISO/IEC는 ECMAScript 언어 표준을 ISO/IEC 22275 에 재발행합니다. 걱정마세요. 해당 표준은 ECMA-262 에 대한 하이퍼링크입니다.

 


1.4 스펙 훑어보기

ECMAScript 스펙은 정말 많은 것(HUGE)을 담고 있습니다. 저자들도 이것을 논리적인 말모음(logical chunks)들로 나누기 위해 최선을 다했지만 여전히 많은 것을 담고 있습니다.

개인적으로 스펙을 다섯 부분으로 나누는 것을 좋아합니다.

  • 규칙 및 기본 사항 ( "어떤 것이 Number 인지", "TypeError exception이 의미하는 것이 무엇인지?" )
  • 언어의 문법 생성( " for- in 루프는 어떻게 작성합니까?" )
  • 언어의 정적 의미( "var 에서 변수 이름들은 어떻게 결정됩니까?" )
  • 언어의 런타임 의미론( " for- in루프는 어떻게 실행됩니까?" )
  • APIs ( "String.prototype.substring() 은 무엇을 하나요?" )

 

그러나 이것은 스펙이 구성된 방법이 아닙니다.

대신에, 첫번째 불릿(bullet point)을 §5 Notational Conventions 부터 §9 Ordinary and Exotic Objects Behaviours 까지 찍고, 인터리브 형식의 다음 세부분은 §10 ECMAScript Language: Source Code 부터 §15 ECMAScript Language: Scripts and Modules 까지, 이렇게요.

 

  • §13.6 The if Statement Grammar productions
    • §13.6.1-6 Static semantics
    • §13.6.7 Runtime sematics
  • §13.7 Iteration Statements Grammar productions
    • §13.7.1 Shared static and runtime semantics
    • §13.7.2 The do-while Statement
      • §13.7.2.1-5 Static semantics
      • §13.7.2.6 Runtime semantics
    • §13.7.3 The while Statement
      • ..


APIs 는 §18 The Global Object 부터 §26 Reflection 까지 퍼져있습니다.

이 시점에서 스펙을 처음부터 끝까지 읽는 사람은 없다는 걸 말하고 싶습니다. 찾고자 하는 연관된 섹션을 찾고 또, 그 안에서 필요한 부분만 보세요. 당신이 궁금해하는 특정 질문이 5개의 섹션 중 어느 것과 연관이 되어있는지 확인하세요. 만약 어떤 것과 연관이 되어있는지 판단하기가 힘들다면, "이것이 평가되는 시점이 언제인지 자문하세요". 아마도 도움이 될 겁니다. 걱정하지 마세요. 스펙을 훑어보는 (읽는) 것은 연습을 통해 더 쉬워질 겁니다.


2. 런타임 의미론 (Runtime semantics)

언어의 런타임과 APIs 는 스펙에서 가장 큰 부분이고 사람들이 가장 많이 궁금해하는 부분입니다.

전반적으로 스펙에서 해당 섹션들을 읽는 것은 매우 간단합니다. 하지만, 스펙은 많은 약칭들이 있고 처음 스펙을 읽는 사람들을 불편하게 합니다. 이러한 규칙들을 몇 가지를 설명하려고 합니다. 그리고 그것들이 어떻게 작동하는지 알아내는 과정에서 적용해볼겁니다.

 

2.1. 알고리즘 단계 (Algorithm steps)

ECMAScript 대부분의 런타임은 일련의 알고리즘 단계에 의해 지정되며 의사 코드(pseudocode) 와는 달리 더 정확한 형태로 지정됩니다.

EXAMPLE 1

샘플 알고리즘은 다음과 같습니다.
1. a는 1 이다.
2. b는 a+a 이다.
3. 만약 b가 2라면,
  - 오예! 연산이 깨지지 않았습니다.

4. Else
  - 우우우!
Further reading: §5.2 Algorithm Conventions

2.2. 추상 연산 (Abstract operations)

종종 스펙에서 호출되는 함수와 같은 것을 볼 수 있습니다. Boolean() 함수의 첫 번째 단계는 다음과 같습니다.

 

Boolean 이 인수 value 와 함께 호출되면 다음 단계가 수행됩니다.
  1. Let b be !ToBoolean(value).
  2. ...


"ToBoolean" 함수를 추상 연산 (Abstract operations)이라고 합니다. 실제로 JavaScript 코드에 함수로 노출되지 않기 때문에 추상입니다. 이것은 단순히 동일한 것을 반복해서 쓰지 않도록 하기 위해서 고안되었습니다.

Note: ToBoolean 앞의 느낌표(!)가 무엇인지 현재로썬 걱정하지마세요. § 2.4 Completion Records; ? and !. 에서 이야기 하게 될 겁니다.
추가 참고 자료: §5.2.1 Abstract Operations

 


2.3. [[This]] 란 ( What is [[This]] )

때때로 "Let proto be obj.[[Prototype]]." 와 같이 [[양쪽 대괄호]] 가 사용되는 것을 볼 수 있습니다. 이 표기는 문장의 의미에 따라 여러 다른 의미를 가질 수 있지만, 이 표기법이 JavaScript 코드를 통해 관찰할 수 없는 일부 내부 속성을 참조한다는 것을 이해하면 도움이 될 것 입니다.

정확히는 세 가지 다른 의미를 가질 수 있으며 스펙의 예를 들어 설명하겠습니다. 그러나 읽고 싶지 않다면 지금은 건너 뛰어도 괜찮습니다.

 

2.3.1. 레코드의 필드 (A field of a Record)

ECMAScript 사양은 레코드(Record) 라는 용어를 사용하여 고정된 키 세트가 있는 key-value 맵을 나타냅니다. 이는 C와 같은 언어 구조와 비슷합니다. 레코드의 각 key-value 쌍을 필드(Field) 라고 합니다. 레코드는 스펙에만 나타날 수 있고 실제 JavaScript 코드에는 나타날 수 없으므로 [[표기법]] 을 사용 하여 레코드의 필드 를 참조하는 것이 좋습니다.

 

EXAMPLE 2

특히, Property Descriptors 는 [[Value]], [[Writable]], [[Get]], [[Set]], [[Enumerable]], and [[Configurable]] 필드가 있는 레코드로 모델링 되었습니다. IsDataDescriptor 추상 작업(abstract operation)은 이 표기법을 광범위하게 사용합니다.

추상 작업 isDataDescriptor가 Property Descriptor Desc와 함께 호출 되면 다음 단계가 수행됩니다.
  1. If Desc is undefined, return false.
  2. If both Desc.[[Value]] and Desc.[[Writable]] are absent, return false.
  3. Return true.

 

레코드의 구체적인 예시는 다음 섹션에서 찾을 수 있습니다. - § 2.4 Completion Records; ? and !.
추가 참고 자료: §6.2.1 The List and Record Specification Types

 

2.3.2. 자바스크립트 객체의 내부 슬롯 (An internal slot of a JavaScript Object)

JavaScript 객체에는 내부 슬롯(Internal slot)이라 불리우는 게 있습니다. 이것은 스펙에서 데이터를 보관하는 데 사용하게 됩니다. 레코드 필드(Record fields)와 마찬가지로 JavaScript를 사용하여 관찰 할 수는 없지만 구글 크롬 개발자 도구를 통해 노출 될 수 있습니다. 따라서 내부 슬롯을 설명하기 위해 [[표기법]] 을 사용하는 것도 의미가 있습니다.

내부 슬롯의 세부 사항은 § 2.5 JavaScript Objects 에서 다루게 될 것 입니다. 지금은 어떻게 사용되는지에 대해 너무 걱정할 필요 없습니다. 대신 다음과 같은 예시에 유의하세요.

 

EXAMPLE 3


대부분의 JavaScript 객체에는 상속받은 객체를 참조하는 내부 슬롯 [[Prototype]]이 있습니다. 이 내부 슬롯의 값은 일반적으로 Object.getPrototypeOf() 가 반환하는 값입니다. OrdinaryGetPrototypeOf 추상 연산에서, 내부 슬롯의 값에 엑세스하게 됩니다.

추상 연산 OrdinaryGetPrototypeOf가 Object O 와 함께 호출 되면 다음 단계가 수행됩니다.
  1. Return O.[[Prototype]].
Note: 객체의 내부 슬롯과 레코드 필드가 보기에는 동일합니다. 그러나 표기점을 보면 객체인지 레코드인지 명확하게 구분 될 수 있습니다. (점 앞에 있는 부분). 이것은 보통 주변문맥을 통해 명백해질 수 있습니다.

 

2.3.2. 자바스크립트 객체의 내부 메소드 (An internal method of a JavaScript Object)

JavaScript 객체에는 내부 메소드(Internal method)라 불리우는 게 있습니다. 내부 슬롯과 같이, 직접 관찰할 수 없습니다. 따라서 내부 메소드를 설명하기 위해서 [[표기법]]을 사용하는 것이 의미가 있습니다.

내부 슬롯의 세부 사항은 § 2.5 JavaScript Objects 에서 다루게 될 것 입니다. 지금은 어떻게 사용되는지에 대해 너무 걱정할 필요 없습니다. 대신 다음과 같은 예시에 유의하세요.

EXAMPLE 4


모든 JavaScript 함수에는 해당 함수를 실행하는 내부 메소드[[Call]]이 있습니다. Call 추상 연산은 다음 단계가 있습니다.

추상 연산 OrdinaryGetPrototypeOf가 Object O 와 함께 호출 되면 다음 단계가 수행됩니다.

3. Return ? F.[[Call]](V, argumentsList).

여기서 F 는 JavaScript 함수 객체 입니다. 이 경우에, F 의 [[Call]] 내부 메소드 자체가 인수 V 그리고 argumentList와 함께 호출 됩니다.
Note: This third sense of the [[Notation]] can be distinguished from the rest by looking like a function call.

 


2.4. Completion Records; ? and !

ECMAScript 스펙의 모든 런타임(runtime semantic) 에서 명시적이든 암시적이든 그 결과를 보고하는 Completion Record를 반환합니다. 이 Completion Record는 세 가지 가능한 필드(fields)가 있는 레코드(Record) 입니다.

  • a [[Type]] (normal, return, throw, break, or continue)
  • 만약 [[Type]]이 normal, return 혹은 throw라면, [[Value]]를 가질 수 있습니다. (returned이든 thrown 이든)
  • 만약 [[Type]]이 break 혹은 continue 라면, [[Target]] 이라는 라벨을 선택적으로 전달 할 수 있습니다. 이 라벨은 런타임의 결과로 스크립트를 중단/실행 합니다.
Note: 두 개의 대괄호는 레코드의 필드를 나타내는 데 사용됩니다. See § 2.3.1 A field of a Record for a primer on Records and the notations associated with them.


일반완료(normal completion) 은 Completion Record에서 [[Type]]이 normal 인 것을 말합니다. 모든 normal completion이 아닌 모든 Completion Record는 (갑작스러운 완료)abrupt completion 이라 불립니다.

대부분의 경우 [[Type]]이 throw 인 abrupt completion을 처리하게 될 것입니다. 다른 세가지의 abrupt completion 타입들은 특정 구문 요소가 어떻게 평가되는지 확인할 때만 유용합니다. 사실은 내장 함수의 정의에서 다른 유형은 절대 볼 수 없을 것 입니다. 왜냐하면 break / continue / return 은 함수 경계를 넘어 작동하지 않기 때문입니다.

추가 참고 자료: §6.2.3 The Completion Record Specification Type

Completion Records 의 정의 때문에 try - catch 블록이 스펙에 존재하지 않을 때 까지 버블링과 같은 자바스크립트의 세부 사항들이 에러를 만들었습니다. 실제로 에러는(더 정확하게는 abrupt completion) 명시적으로 처리됩니다.

원문: Because of the definition of Completion Records, niceties in JavaScript like bubbling errors until a try-catch block don’t exist in the spec. In fact, errors (or more precisely abrupt completions) are handled explicitly.


약어가 없다면, 결과를 반환하거나 에러를 던지는 추상 연산에 대한 스펙 문서는 다음과 같을 수 있습니다.

 

EXAMPLE 5

약어 없이 
추상 연산을 호출하는 몇 가지 단계
(A few steps that call an abstract operation that may throw without any shorthands)

  1. resultCompletionRecord는 AbstractOp().
    - resultCompletionRecord는 Completion Record 이다.
  2. 만약 resultCompletionRecord 가 abrupt completion(갑작스러운 완료) 라면, resultCompletionRecord을 반환한다.
    - resultCompletionRecord는 abrupt completion 일 경우 직접 반환 됩니다. 다시 말해, AbstractOp 에서 발생한 에러는 전달 되고, 나머지 단계들은 중단됩니다.
  3. result 는 resultCompletionRecord.[[Value]].
    - normal completion(정상적인 완료)를 확인한 이후, Completion Record를 풀어서 필요한 실제 결과를 얻을 수 있습니다.
  4. result 는 우리에게 필요한 결과입니다. 이제 이것을 가지고 더 많은 작업을 수행할 수 있습니다.

 

이것은 C 언어로 에러 처리하던 것을 생각나게 해줄 수도 있습니다.
int result = abstractOp();              // Step 1
if (result < 0)                         // Step 2
  return result;                        // Step 2 (continued)
                                        // Step 3 is unneeded
// func() succeeded; carrying on...     // Step 4


그러나 이런 많은 보일러 플레이트 단계를 줄이기 위해서 ECMAScript 스펙의 편집자는 몇가지 약어를 추가하였습니다. ES2016 이후로는 동일한 스펙에 다음 두 가지의 방법으로 작성할 수 있습니다.

EXAMPLE 6

ReturnIfAbrupt
를 throw 하는 추상연산을 호출하는 몇가지 단계:

1. result는 AbstractOp() 이다. (앞선 예문의 1번과 같이 result는 Completion Record 이다)

2. ReturnIfAbrupt(result).

3. result 는 필요한 결과 값입니다. 이제 이것을 가지고 더 많은 작업을 수행할 수 있습니다.

 

ReturnIfAbrupt: 예외 처리와 같은 abrupt completion (갑작스런 완료) 인지 확인하고, 만약 그렇다면 abrupt completion을 반환합니다 (예외가 버블링 되도록 허용). 그러나 인수가 normal completion 이라면 Completion Record를 unwrap 후 인수를 값에 넣습니다. ( set argument to argument.[[Value]] )

또는 더 간결하게 물음표 ( ? ) 표기 법을 사용합니다

EXAMPLE 7


물음표를 throw 하는 추상 연산을 호출하는 몇 가지 단계:

1. result 는 ? AbstracOp() 이다.
- 이 표기법에서는 Completion Records를 다루지 않습니다. ? 약어가 모든 것을 처리하며 결과를 즉시 사용할 수 있게 됩니다.

2. result 는 우리에게 필요한 결과입니다. 이제 이것을 가지고 더 많은 작업을 수행할 수 있습니다.


때때로, AbstracOp에 대한 호출이 abrupt completion을 반환하지 않는 다는 것을 알고 있다면, 스펙의 의도에 대해 더 많은 것을 독자에게 전달 할 수 있습니다. 이러한 경우에는 느낌표 ( ! ) 가 사용됩니다.

 

EXAMPLE 8


느낌표와 함께 절대 throw 할 수 없는 추상 연산을 호출하는 몇가지 단계

1. result 는 ! AbstracOp() 이다.
- 물음표 ( ? ) 가 우리가 가진 에러를 전달한다면, 느낌표 ( ! ) 는 호출로부터 어떠한 abrupt completion (갑작스런 완료) 도 받지 못한다고 말합니다. 만약 그랬었다면, 그것은 스펙의 버그가 될 것입니다. 물음표 ( ? ) 와 같은 경우처럼 여기에서도 Completion Records를 다루지 않고 결과 값은 바로 사용 될 수 있습니다.

2. result 는 필요한 결과 값입니다. 이제 이것을 가지고 더 많은 작업을 수행할 수 있습니다.

 

CAUTION


느낌표 ( ! ) 가 만약 유효한 JavaScript 표현처럼 보인다면 꽤 혼란스러울 수 있습니다.

  1. Let b be ! ToBoolean(value).
— Boolean() 에서 발췌.

우리는 여기에서 느낌표 ( ! ) 가 ToBoolean에 대한 호출이 예외를 반환하지 않을 것이며 결과가 반전되지 않는다는 것을 확신할 수 있습니다.
추가 참고 자료: §5.2.3.4 ReturnIfAbrupt Shorthands.

 


2.5. JavaScript 객체

ECMAScript에서 모든 객체는 특정 작업을 수행하기 위해 스펙에서 호출하는 내부 메서드 집합이 있습니다. 모든 객체에 있는 내부 메서드 중 일부는 다음과 같습니다.

  • [[Get]], 객체의 프로퍼티를 가져옵니다. (e.g. obj.prop)
  • [[Set]], 객체의 프로퍼티를 설정합니다. (e.g. obj.prop = 42;)
  • [[GetPrototypeOf]], 객체의 프로토타입을 가져옵니다. (i.e., Object.getPrototypeOf(obj))
  • [[GetOwnProperty]], 객체 자체의 프로퍼티에 대한 Property Descriptor를 가져옵니다. (i.e., Object.getOwnPropertyDescriptor(obj, "prop"))
  • [[Delete]], 객체의 프로퍼티를 삭제합니다. (e.g. delete obj.prop)

 

전체 목록은 §6.1.7.2 Object Internal Methods and Internal Slots에서 확인할 수 있습니다.

이 정의에 따르자면, 함수 객체 (혹은 "함수")는 [[Call]] 내부 메서드와 [[Construct]] 내부 매서드가 있는 단순한 객체 입니다. 이러한 이유로 호출 가능한 객체(callable objects)라고도 불립니다.

스펙은 모든 객체를 ordinary objects (일반 객체)와 exotic objects로 나눕니다. 다루게 되는 대부분의 객체는 ordinary objects 입니다. 이 말은, 해당 객체의 모든 내부 메서드는 §9.1 Ordinary Object Internal Methods and Internal Slots
에 명시되었다는 것을 의미합니다.

그러나 ECMAScript 스펙은 몇몇 exotic objects를 정의하고 있습니다. 이것들은 내부 메서드의 기본 구현을 재정의할 수 있습니다. exotic objects가 수행할 수 있는 작업에는 명확한 최소한의 제약들이 있지만 일반적으로 재정의된 내부메서드들은 사양에 어긋나지 않으며 많은 것을 할 수 있습니다.

 

Example 9

배열 (Array) 객체는 exotic objects의 한 종류 입니다. 배열이 가지는 특별한 의미의 length 프로퍼티는 ordinary object (일반 객체)에서 사용되는 도구(속성)들로 구현될 수 없습니다.
[원문]: Array objects are one kind of these exotic objects. Some special semantics around the length property of Array objects cannot be achieved using the instruments available to ordinary objects.

그 중 하나로는 배열에 length 프로퍼티를 추가하면 객체에서 프로퍼티를 제거할 수 있습니다. 그러나 length 프로퍼티는 일반적인 데이터 프로퍼티로 보인다는 사실입니다. 대조적으로, new Map().size 는 단지 Map.prototype 에 명시된 getter 함수이며 [].length 와 같은 동작을 수행하는 마법같은 프로퍼티가 없다는것 입니다.

 

> const arr = [0, 1, 2, 3];
> console.log(arr);
[ 0, 1, 2, 3 ]
> arr.length = 1;
> console.log(arr);
[ 0 ]
> console.log(Object.getOwnPropertyDescriptor([], "length"));
{ value: 1,
  writable: true,
  enumerable: false,
  configurable: false }
> console.log(Object.getOwnPropertyDescriptor(new Map(), "size"));
undefined
> console.log(Object.getOwnPropertyDescriptor(Map.prototype, "size"));
{ get: [Function: get size],
  set: undefined,
  enumerable: false,
  configurable: true }
이 동작은 [[DefineOwnProperty]] 내부 메서드를 재정의하여 수행됩니다. 자세한 내용은 §9.4.2 Array Exotic Objects를 참조하십시오.

 

ECMAScript 스펙은 다른 스펙이 다른 스펙을 정의할 수 있도록 합니다. 이러한 매커니즘을 통해 브라우저가 교차 출처 API 접근(Cross-origin API access)에 대한 제한이 명시되어있습니다 (참조 WindowProxy) [HTML]. 또한, JavaScript 프로그래머가 Proxy API 를 사용하여 독자적인 객체(own exotic objects)를 만드는 것도 가능하게 합니다.


JavaScript 객체에는 특정 유형의 값을 포함하도록 정의된 내부 슬롯이 있을 수도 있습니다.


저는 내부 슬롯을 Object.getOwnPropertySymbols() 에도 숨겨져 있는 Symbol-named 프로퍼티처럼 생각하는 경향이 있습니다. ordinary objects(일반 객체)와 exotic objects 모두 내부 슬롯을 가질 수 있습니다.

2.3.2 JavaScript 객체의 내부슬롯에서 대부분의 객체는 [[Prototype]] 이라는 내부 슬롯을 가진다고 언급했습니다. (사실 모든 일반 객체, 심지어 배열 객체 같은 exotic objects에도 있습니다.) 그러나 간략하게 설명했던 [[GetPrototypeOf]]와 같은 내부 메서드가 존재하는 것을 알고 있는데, 차이점이 무엇인가요?

핵심은 most (대부분의) 입니다. 대부분의 객체는 내부 슬롯이 있지만 모든 객체는 [[GetPrototypeOf]] 내부 메서드를 구현합니다. 특히 Proxy 객체에는 자체 [[Prototype]]이 없으며 [[GetPrototypeOf]] 내부 메소드가 등록된 핸들러나 대상의 프로토타입을 따르고 Proxy 객체의 [[ProxyTarget]] 내부 슬롯에 저장됩니다.


객체, 내부 메서드 그리고 내부 슬롯 간의 간계에 대해 생각하는 또 다른 방법은 classical object-oriented lens 를 사용하는 것 입니다.

 

"객체"는 구현해야하는 여러 내부 메서드를 구체적으로 명시하는 것과 같습니다. Ordinary objects (일반객체)는 기본 구현 동작을 제공하며 exotic objects는 그것들 부분적으로, 혹은 모두 재정의 할 수 있습니다. 반면에 내부 슬롯은 객체의 인스턴스 변수와 같습니다. 즉, 해당 객체의 세부 구현 정보 입니다.

모든 관계는 UML 다이어그램으로 요약될 수 있습니다. (클릭 시 확대)

UML 다이어그램


2.6. 예시: String.prototype.substring()

이제 스펙이 어떻게 구성되었고 쓰였는지 꽤 이해했으므로 연습해봅시다.
다음과 같은 질문이 있습니다.

 

다음 코드 조각들은 어떤 것을 반환하나요? 코드를 실행하지 말고 맞춰봅시다.

String.prototype.substring.call(undefined, 2, 4)


이건 꽤 까다로운 질문입니다. 두 가지의 그럴듯한 결과가 예상됩니다.

  1. String.prototype.substring() 은 첫번째로 들어온 undefined를 문자열 "undefined"로 바꿉니다. 그리고 각 문자의 위치인 2, 3으로 부터 "de"라는 결과 값을 얻습니다.
  2. 다른 결과로는, String.prototype.substring() 아마 undefined를 입력으로 받았기 때문에 합리적으로 에러를 던지게게 될 것입니다.

불행하게도 MDN이 문자열이 아닐 때 함수가 어떻게 동작하는지에 대해서 알려주지는 않습니다.

도와줘요 스펙! 스펙 문서[ECMA-262], 왼쪽 상단의 검색창에 substring을 입력하면 함수 작동 방식에 대한 규범적 사양인 §21.1.3.22 String.prototype.substring ( start, end )을 확인 할 수 있습니다.

알고리즘 단계를 읽기 전에 우리가 알고 있는 것에 대해 먼저 생각해봅시다. 먼저 str.substring()이 어떻게 동작하는지 기본적인 이해가 있다고 가정합니다. 지금 당장 확실하지 않은 것은, 값이 정의되지 않은 상태에서 어떻게 동작하는지 입니다. 따라서 구체적으로 이 값을 다루는 알고리즘 단계를 찾습니다.

운 좋게도, String.prototype.substring() 의 첫 번째 알고리즘 단계가 이 값을 다루고 있습니다.

 

1. Let O be ? RequireObjectCoercible(this value).

 

물음표 ( ? ) 약어를 사용하면 RequireObjectCoercible 추상 연산이 실제로 예외가 발생(throw exception)될 수 있다는 결론을 얻을 수 있습니다. 그게 아니라면 느낌표 ( ! )가 사용되었을 겁니다. 실제로 오류가 발생하면 위의 두 번째 가설에 해당하게 됩니다. RequireObjectCoercible 를 클릭하여 어떻게 수행되는지 한번 들여다 봅시다.

RequireObjectCoercible 추상 연산은 좀 이상합니다. 다른 추상 연상들과는 달리 테이블을 통해 정의되어 있습니다.

Argument Type Result
Undefined Throw a TypeError exception
... ...


그렇다 하더라도, 행에 있는 Undefined - (위에서 substring()에 인자로 넣어줬던 값의 타입)를 살펴보면 스펙은 RequireObjectCoercible가 예외처리를 해야한다는 것을 알려주고 있습니다. 그리고 물음표 ( ? ) 표기법은 함수의 정의에서 사용되며, 예외가 발생(thrown exception)할 경우에는 함수의 호출자에게 버블링되어야 한다는 것을 알고 있습니다. 빙고!

그리고 우리는 이제 정답을 알았습니다. 주어진 코드 조각에서 TypeError를 발생시킵니다.

스펙은 어떤 메시지를 담고 있는 것이 아닌, 어떤 유형의 에러인지만 명시합니다. 이 말은 다른 오류 메시지를 가질 수 있다는 것을 의미합니다. 메시지가 현지화되어(localized) 바뀔 수도 있습니다.
예를 들어, Google’s V8 6.4 (Google Chrome 64에서 포함) 에서의 메시지는 다음과 같습니다.

TypeError: String.prototype.substring called on null or undefined

Mozilla Firefox 57.0 의 메시지는 도움이 덜 됩니다.

TypeError: can’t convert undefined to object

동시에, ChakraCore version 1.7.5.0 (마이크로소프트 엣지 JavaScript 엔진)은 V8’s route and throws를 사용합니다.

TypeError: String.prototype.substring: 'this' is null or undefined

2.7. 예시: Boolean() 과 String() 은 에외를 발생시킬까? (Can Boolean() and String() ever throw exceptions?)

업무에 필수적이고 중요한 코드를 작성할 때 예외 처리하는 것을 가장 중요하게 생각해야합니다. 따라서, "일부 내장 함수가 예외를 발생시킬 수 있을까?" 라는 질문을 자주 생각할 수 있습니다.

해당 예시에서, 두 가지 Boolean() 과 String() 내장 함수를 사용하여 질문에 대한 답변을 하고자 합니다. 함수에 대한 직접 호출만 살펴보며, new Boolean() new String()과 같은(form boxed objects) 경우는 살펴보지 않습니다. 이것들은 Javascript에서 가장 바람직하지 않은 기능이며 권장하지 않는 방식이기 때문입니다. [[YDKJS]]

스펙에서 Boolean() 섹션을 확인해보면 알고리즘이 상당히 짧은 것을 알 수 있습니다.

Boolean 이 인수 value와 호출되면 다음 단계가 수행됩니다.
  1. Let b be ! ToBoolean(value).
  2. If NewTarget is undefined, return b.
  3. Let O be ? OrdinaryCreateFromConstructor(NewTarget, "%BooleanPrototype%", « [[BooleanData]] »).
  4. Set O.[[BooleanData]] to b.
  5. Return O.

그러나, 이것은 OrdinaryCreateFromConstructor 주변의 여러 복잡한 것들로 인해서 명확하지가 않습니다. 더 중요한 것은, step3의 물음표 ( ? ) 약어는 특정 경우에 에러를 던질 수 있다는 것을 암시하고 있습니다. 자세히 살펴보죠.

step 1 에서 value를 Bool 값으로 변환합니다. 흥미롭게도 여기엔 물음표 또는 느낌표 약어가 없습니다. 일반적으로 Completion Record 약어를 가지고 있지 않다는 것은 느낌표 ( ! ) 와 같습니다. So step 1 cannot throw an exception.

step 2에서는 NewTarget이 undefined 인지 확인합니다. NewTarget 은 ES2015에 처음 추가된 메타 프로퍼티로 new.target과 같은 스펙이며, new Boolean() 호출과 Boolean() 호출을 구분할 수 있게 해줍니다. 우리는 현재 직접 호출인 Boolean()만 확인하고 있기 때문에 NewTarget이 항상 undefined로 나온다는 것을 알고, 알고리즘이 항상 b를 즉시 반환한다는 것을 알 수 있습니다.

new 없이 Boolean()만 호출하면 처음 두 개의 step만 접근이 가능하며 예외 또한 던질 수 없습니다. 입력이 무엇이든 Boolean()은 어떤 예외도 발생시키지 않는다 로 결론을 내릴 수 있습니다.



이번에는 String() 을 한번 봐볼까요.

String이 인수 value 와 함께 호출 되면 다음 단계가 수행됩니다.

  1. If no arguments were passed to this function invocation, let s be "".
  2. Else,
    1. If NewTarget is undefined and Type(value) is Symbol, return SymbolDescriptiveString(value).
    2. Let s be ? ToString(value).
  3. If NewTarget is undefined, return s.
  4. Return ? StringCreate(s, ? GetPrototypeFromConstructor(NewTarget, "%StringPrototype%")).


Boolean() 함수로 동일한 분석을 한 경험으로 NewTarget 이 항상 undefined라는 것을 알고 있으므로 마지막 step 은 고려하지 않아도 됩니다. abrupt completion(갑작스런 완료)가 어디에선가 따로 처리되지는 않았기 때문에 TypeSymbolDescriptiveString 도 안전하다는 것을 알 수 있습니다. (We also know that Type and SymbolDescriptiveString are safe as well, since abrupt completions are not handled for either of them.)

아직 더 봐야합니다. 물음표 ( ? ) 약어가 ToString 추상 연산 호출 앞에 있습니다. 더 자세히 살펴보죠.

앞서 살펴본 RequireObjectCoercible 와 같이 ToString(argument) 또한 테이블로 정의되어 있습니다.

 

Argument Type Result
Undefined Return "undefined".
Null Return "null".
Boolean If argument is true, return "true".If argument is false, return "false".
Number Return NumberToString(argument).
String Return argument.
Symbol Throw a TypeError exception.
Object Apply the following steps:
  1. Let primValue be ? ToPrimitive(argument, hint String).
  2. Return ? ToString(primValue).


String() 에서 ToString 이 호출되는 지점(step 2.2를 말함)에서, Symbol을 제외한 모든 값을 가질 수 있습니다. (Symbol은 앞선 step 에서 필터링 되었습니다. 아직 두개의 물음표 ( ? )가 Object에 남아 있습니다. ToPrimitive 링크를 따라가볼 수 있으며, 실제로 값이 객체인 경우에 에러가 발생할 가능성이 많다는 것을 알 수 있습니다.

EXAMPLE 10
Several examples where String() throws
더보기
// Spec stack trace:
//   OrdinaryGet step 8.
//   Ordinary Object’s [[Get]]() step 1.
//   GetV step 3.
//   GetMethod step 2.
//   ToPrimitive step 2.d.

String({
  get [Symbol.toPrimitive]() {
    throw new Error("Breaking JavaScript");
  }
});
// Spec stack trace:
//   GetMethod step 4.
//   ToPrimitive step 2.d.

String({
  get [Symbol.toPrimitive]() {
    return "Breaking JavaScript";
  }
});
// Spec stack trace:
//   ToPrimitive step 2.e.i.

String({
  [Symbol.toPrimitive]() {
    throw new Error("Breaking JavaScript");
  }
});
// Spec stack trace:
//   ToPrimitive step 2.e.iii.

String({
  [Symbol.toPrimitive]() {
    return { "breaking": "JavaScript" };
  }
});
// Spec stack trace:
//   OrdinaryToPrimitive step 5.b.i.
//   ToPrimitive step 2.g.

String({
  toString() {
    throw new Error("Breaking JavaScript");
  }
});

 

// Spec stack trace:
//   OrdinaryToPrimitive step 5.b.i.
//   ToPrimitive step 2.g.

String({
  valueOf() {
    throw new Error("Breaking JavaScript");
  }
});
// Spec stack trace:
//   OrdinaryToPrimitive step 6.
//   ToPrimitive step 2.g.

String(Object.create(null));

 

따라서 String()은 원시 값에 대해 예외를 던지지는 않지만 객체에 대한 에러를 발생할 수는 있다는 결론입니다.


2.8. 예시: typeof 연산자 (typeof operator)

여기서 원문은 끝이나고, 깃헙 링크가 걸려있습니다.

To be written. <https://github.com/TimothyGu/es-howto/issues/2>



원문 용어 사전 링크
원문 INDEX
원문 레퍼런스

 

somedaycode - Overview

somedaycode has 41 repositories available. Follow their code on GitHub.

github.com