개요
ES2018부터 적용되는 Object Rest/Spread Properties는 미묘한 특징이 있습니다.
Array로 Object Rest/Spread를 할 때
우선 아래 코드를 보죠.
//Rest로 해체될 때 const {...a} = [1,2,3]; console.log(a); //{"0":1, "1":2, "2":3} //Spread로 사용될 때 const b = {...[1,2,3]}; console.log(b); //{"0":1, "1":2, "2":3}
이 결과만 보면 배열의 경우 Object Rest와 Spread가 잘 작동합니다. 따라서 Iterable인터페이스가 수용되는 것처럼 보일 수 있습니다.
Iterable로 Object Rest/Spread를 할 때
위에서 배열이 작동하는 걸 확인했으니 간단히 iterable을 만들어서 실험해보죠.
const iterable = { [Symbol.iterator](){ let cursor = 0; return {next(){ return {done:cursor++ > 2, value:cursor}; }}; } }; const {...a} = iterable; //{[Symbol.iterator]:f} const b = {...iterable}; //{[Symbol.iterator]:f}
배열일 때는 Object Rest와 Spread가 작동하던 게 직접 작성한 iterable은 적용되지 않는다는 사실을 알 수 있습니다. 말 그대로 객체로 해체되어 갖고 있던 [Symbol.iterator]키만 복사되었습니다. 그럼 배열인척 하는 iterable을 만들어볼까요.
const iterableArr = new (class extends Array{ [Symbol.iterator](){ let cursor = 0; return {next(){ return {done:cursor++ > 2, value:cursor}; }}; } })(); const {...a} = iterableArr; //{} const b = {...iterableArr}; //{}
클래스 문이 되어 hasOwnProperty에 [Symbol.iterator]가 안걸리게 되었을 뿐 증상은 같습니다. 혹시 new.target 탓일 수도 있으니 [Symbol.species]로 구상해버리죠.
const iterableArr = new (class A extends Array{ static get [Symbol.species](){ return A; } [Symbol.iterator](){ let cursor = 0; return {next(){ return {done:cursor++ > 2, value:cursor}; }}; } })(); const {...a} = iterableArr; //{} const b = {...iterableArr}; //{}
소용없습니다. 즉 리터럴로 생성된 배열의 경우는 이터러블과 완전히 무관하게 인덱스가 자신의 키로 잡히기 때문에 Object Rest나 Spread에 반응한다는 사실을 알 수 있습니다.
사실 이는 이미 스펙문서에 자세히 기술되어있습니다 ^^(괜히 브라우저 구현체를 확인하고 싶어서 뻘 짓을 해봤습니다)
Object Rest나 Spread는 내부적으로 CopyDataProperties라는 함수를 사용하는데 그 정의는 다음과 같습니다.
CopyDataProperties ( target, source, excludedItems )
- target은 객체다
- excludedItems은 속성키의 리스트다.
- source가 undefined나 null이면 target을 반환한다.
- 속성키리스트를 돌면서
- 그 키가 source의 hasOwnProperty인지 체크한다.
- 맞으면 target에 CreateDataProperty로 그 속성과 값을 셋팅해준다.
- 다했으면 target을 반환한다.
결국 이 작동에 iterable을 인식하는 동작은 없는 셈입니다.
iterable을 배열처럼 Object Rest/Spread에 쓰려면?
결국 방법은 Array Rest/Spread를 한 뒤에 다시 Object Rest/Spread를 할 수 밖에 없습니다.
처음 코드로 돌아와서
const iterable = { [Symbol.iterator](){ let cursor = 0; return {next(){ return {done:cursor++ > 2, value:cursor}; }}; } };
요런 간단한 iterable을 대상으로 처리한다면 아래와 같은 구문이 될 것입니다.
const {...[...a]} = iterable; const b = {...[...iterable]};
이게 잘되면 좋겠지만 미묘하게 Rest와 Spread는 문법이 다릅니다.
우선 위에서 b에 해당되는 Spread의 문법은 정확하게는 Object literal에서 지원하는 assignment 중 destructuring assignment라는 사양에 의존합니다.
근데 이 사양을 잘 보고 있으면
1. AssignmentRestProperty 부분이 DestructuringAssignmentTarget을 가리키고
2. DestructuringAssignmentTarget은 다시 LeftHandSideExpression을 가리킵니다.
3. LeftHandSideExpression은 다시 destructuring assignment을 포함하기 때문에 재귀가 성립됩니다.
그런 이유로 문법 상 const b = {…[…iterable]}; 가 성립하게 되어 문제 없이 처리됩니다.
즉 b는 const b = {…[1,2,3]} 처럼 {“0”:1, “1”:2, “2”:3}이 잘 출력됩니다.
하지만 a는 Uncaught SyntaxError: ...
must be followed by an identifier in declaration contexts 라는 에러를 내고 죽어버립니다.
에러메시지는 Rest표현 뒤에는 반드시 식별자가 와야한다는 거죠. destructuring은 문법적인 변수와 그 할당에 대한 내용이라 별도로 정해져 있습니다(스펙문서에는 너무 많은 참조로 타고 내려가야해서 생략했습니다)
따라서 a가 정상이 되려면 const {…a} = […iterable] 이 될 수 밖에 없습니다. 이렇게 하면 a도 {“0”:1, “1”:2, “2”:3}이 출력됩니다.
const iterable = { [Symbol.iterator](){ let cursor = 0; return {next(){ return {done:cursor++ > 2, value:cursor}; }}; } }; const {...a} = [...iterable] const b = {...[...iterable]};
이렇게 귀결지을 수 있습니다.
결론
코드스피츠 77에서 해당 내용이 나왔는데 간단히 배열로 작동하는 것을 확인한 후 Iterable도 된다고 생각해 내용을 포함시켰는데 완전히 틀렸네요.
재확인할 겸 차근차근 살펴보았습니다. 역시 ECMAScript는 계속 스펙이 추가되어가고 있고 거기에 따라 더욱 공부하고 확인 해봐야하는 내용도 많아지네요.
recent comment