[완벽 가이드] 14장. 메타 프로그래밍

Ninto·2023년 3월 5일
0

학습 기록

목록 보기
1/17

일반적인 프로그래밍이 데이터를 조작하는 코드를 작성한다면
메타프로그램은 다른 코드를 조작하는 코드를 작성합니다.

자바스크립트와 같은 동적 언어에서는 프로그래밍과 메타 프로그래밍이 뚜렷이 구분되지 않습니다.

하지만 더 정적인 언어에 익숙한 프로그래머에게는 for/in루프와 같은 객체 프로퍼티를 순회하는 단순한 작업도 메타 프로그래밍으로 느껴질 수 있습니다.

📌 프로퍼티 속성

자바스크립트 객체의 프로퍼티에는 이름과 값이 있지만,
각 프로퍼티에는 그 프로퍼티가 어떻게 동작하는지 나타내는 세 가지 속성이 있습니다.

  • 쓰기 가능 (writable) : 프로퍼티 값을 바꿀 수 있는지 나타내는 속성입니다.
  • 열거 가능 (enumerable) : for/in 루프나 Object.keys()메서드에서 해당 프로퍼티를 열거할 수 있는지 나타내는 속성입니다.
  • 변경 가능 (configurable) : 프로퍼티를 삭제할 수 있는지 혹은 프로퍼티 속성을 바꿀 수 있는지 나타내는 속성입니다.

객체 리터럴이나 일반적인 할당으로 정의된 프로퍼티는 쓰기 가능, 열거 가능, 변경 가능입니다.

그러나 자바스크립트 표준 라이브러리에서 정의한 프로퍼티는 그렇지 않은 것이 많습니다.

프로퍼티의 속성을 검색하고 설정하는 API는 아래와 같은 이유로 라이브러리 제작자에게 특히 중요합니다.

  • 프로토타입 객체에 메서드를 추가하고 내장 메서드처럼 열거 불가로 만들 수 있습니다.
  • 변경하거나 삭제할 수 없는 프로퍼티를 만들어 객체를 잠글 수 있습니다.

데이터 프로퍼티의 네 가지 속성 : value, writable, enumerable, configurable
접근자 프로퍼티의 네 가지 속성 : get, set, enumerable, configurable

// 반환 값: {value: 1, writable, enumerable: true, configurable:true}
Object.getOwnPropertyDescriptor({x: 1}, 'x');

// 읽기 전용 접근자 프로퍼티가 있는 객체
const random = {
	get octet() {
    	return Math.floor(Math.random() * 256);
    }
}

// 반환 값: {get:/*함수*/, set:undefined, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(random, 'octet');

// 상속된 프로퍼티, 존재하지 않는 프로퍼티는 undefined를 반환
Object.getOwnPropertyDescriptor({}, 'x'); // undefined 존재하지 않습니다
Object.getOwnPropertyDescriptor({}, 'toString'); // undefined 상속되었습니다.
let o = {}; // 프로퍼티가 전혀 없는 상태에서 시작
Object.defineProperty(o, 'x', {
 value: 1,
 writable: true,
 enmerable: false,
  configurable: true,
});

// 프로퍼티가 존재하고 열거 불가인지 체크
o.x; // 1
Object.keys(o); // []

// 프로퍼티 x를 읽기 전용으로 수정
Object.defineProperty(o, 'x', {
 writable: false,
});

// 프로퍼티 값 변경을 시도
o.x = 2; // 조용히 실패하거나 스트릭모드에서 TypeError 발생
o.x; // 1

// 프로퍼티는 여전히 변경 가능이므로 다음과 같이 값을 바꿀 수 있음
Object.defineProperty(o, 'x', {
 value: 2,
});
o.x; // 2

// x를 데이터 프로퍼티에서 접근자 프로퍼티로 변경
Object.defineProperty(o, 'x', {
 get: function() {
  return 0;
 }
});
o.x; // 0

Object.defineProperty()에 전달하는 프로퍼티 서술자에 네 가지 속성이 전부 있을 필요는 없습니다.
새 프로퍼티를 생성할 때 생략된 속성은 false나 undefined로 간주합니다.
기존 프로퍼티를 수정하더라도 생략된 속성이 다시 생기지는 않습니다.

Object.defineProperty()메서드는 기존의 자체 프로퍼티를 변경하거나 새로운 자체 프로퍼티를 생성할 뿐 상속된 프로퍼티를 변경하지 않습니다.

둘 이상의 프로퍼티를 한번에 생성하거나 수정하려면 Object.defineProperties()를 사용합니다.

let p = Object.defineProperties({}, {
 x: { value: 1, writable: true, enumerable: true, configurable: true },
 y: { value: 1, writable: true, enumerable: true, configurable: true },
 r: { get() {
  return Math.sqrt(this.x * this.x + this.y * this.y);
  enumerable : true,
  configurable: true,
 }},
})

p.r // Math.SQRT2

다음 규칙을 위반하는 Object.defineProperty()Object.defineProperties()호출은 TypeError를 일으킵니다.

  • 객체가 확장불가이면 기존의 자체 프로퍼티를 수정할 수 있지만 새로운 프로퍼티를 추가할 수 없다.
  • 프로퍼티가 변경불가이면 변경 가능 속성이나 열거 가능 속성을 바꿀 수 없다.
  • 접근자 프로퍼티가 변경불가이면 게터나 세터 메서드를 바꿀 수 없고, 데이터 프로퍼티로 바꿀 수도 없다.
  • 데이터 프로퍼티가 변경불가이면 접근자 프로퍼티로 바꿀 수 없다.
  • 데이터 프로퍼티가 변경불가이면 쓰기 가능 속성을 false에서 true로 바꾸는 것은 불가능하지만 true에서 false로 바꾸는 것은 가능하다.
  • 데이터 프로퍼티가 변경 불가이고 읽기 전용이면 값을 바꿀 수 없다.
    읽기 전용이더라도 변경 가능이면 프로퍼티의 값을 바꿀 수 있다.
    (쓰기 가능으로 바꾸고 값을 바꾼 다음 다시 읽기 전용으로 바꾸는 것이나 마찬가지이기 때문이다.)

Object.assign()은 열거 가능 프로퍼티와 값은 복사할 수 있지만 프로퍼티 속성은 복사할 수 없습니다.

보통은 이런 결과를 원하긴 하지만, 소스 객체 중 하나에 접근자 프로퍼티가 있다면 대상 객체에 복사되는 것은 게터 메서드가 반환하는 값이지 게터 메서드 자체가 아닙니다.

Object.defineProperty(Object, 'assignDescriptors', {
//Object.assign()과 속성을 일치시킵니다.
  writable: true,
  enumerable: false,
  configurable: true,
  // assignDescriptors 프로퍼티의 값인 함수입니다.
  value: function (target, ...sources) {
   for (let source of sources) {
    for (let name of Object.getOwnPropertyNames(source)) {
     let desc = Object.getOwnPropertyDescriptor(source, name);
      Object.defineProperty(target, name, desc);
    }
     
     for (let symbol of Object.getOwnPropertySymbols(source)) {
      let desc = Object.getOwnPropertyDescriptor(source, symbol);
       Object.defineProperty(target, symbol, desc);
     }
   }
    return target;
  }
});

let o = {c:1, get count() {return this.c++}}; //게터가 있는 객체를 정의합니다.
let p = Object.assign({}, o); // 프로퍼티 값을 복사합니다.
let q = Object.assignDescriptors({}, o); // 프로퍼티 서술자를 복사합니다.

p.count
p.count
q.count
q.count
// 1 : count는 이제 데이터 프로퍼티이므로
// 1 : 늘어나지 않습니다.
// 2 : 처음 복사할 때 증가하며
// 3 : 게터 메서드를 복사했으므로 계속 증가합니다.


📌 객체 확장성

확장 가능 속성은 객체에 새로운 프로퍼티를 추가할 수 있는지 결정합니다.
일반적인 자바스크립트 객체는 기본적으로 확장 가능이지만 이 절에서 설명하는 함수로 바꿀 수 있습니다.

객체가 확장 가능인지 확인하려면 Object.isExtensible()에 전달합니다.

객체를 확장 불가로 만들려면 Object.preventExtensions()에 전달합니다.
이렇게 하면 객체에 새로운 프로퍼티를 추가하려 할 때 스트릭트 모드에서는 TypeError가 일어나고 일반 모드에서는 에러 없이 조용히 실패하게 됩니다.

또한 확장 불가인 객체의 프로토타입을 변경하려는 시도는 항상 TypeError를 일으킵니다.

객체를 일단 확장불가로 만든 후에는 다시 확장 가능으로 되돌릴 방법이 없습니다.
또한 Object.preventExtensions()는 객체 자체의 확정성만 제어합니다.
확장 불가인 객체의 프로토타입에 새로운 프로퍼티를 추가하면 새로운 프로퍼티는 그대로 상속됩니다.

확장 가능 속성의 목적은 객체를 현재 상태로 유지하고 바깥에서 손댈 수 없도록 잠그는 것입니다.
확장 가능 속성을 변경 가능, 쓰기 가능 속성과 함께 사용할 때가 많으므로 자바스크립트에는 이 속성들을 한꺼번에 다루는 함수가 존재합니다.

  • Object.seal()Object.preventExtensions()와 비슷하지만 객체를 확장 불가로 만드는 동시에 자체 프로퍼티를 모두 변경 불가로 바꿉니다.
    따라서 새로운 프로퍼티를 추가할 수 없고 기존 프로퍼티를 삭제할 수 도 없습니다.
    쓰기 가능인 기존 프로퍼티는 여전히 그대로 남습니다.
    밀봉된 객체의 밀봉을 풀 수 있는 방법은 없습니다.
    객체가 밀봉됐는지는 Object.isEealed()로 파악할 수 있습니다.

  • Object.freeze()는 객체를 더 단단히 잠급니다.
    객체는 확장불가, 프로퍼티는 변경 불가로 바뀌는 동시에 객체의 자체 데이터 프로퍼티는 모두 읽기 전용으로 바뀝니다.
    (객체에 세터 메서드가 있는 접근자 프로퍼티가 있다면 이들은 영향을 받지 않으며 프로퍼티에 할당할 때 여전히 호출됩니다.)
    객체가 동결되었는지는 Object.isFrozen()으로 판단합니다.

Object.seal()Object.freeze()는 전달받은 객체에만 효과가 있으며, 그 객체의 프로토타입은 변경하지 않습니다.
객체를 완전히 잠그려면 프로토아비 체인에 있는 객체 역시 밀봉 또는 동결해야만 합니다.

Object.preventExtentions(), Object.seal(), Object.freeze()는 모두 전달받은 객체를 반환하므로 아래와 같이 중첩해서 호출할 수 있습니다.

// 프로토타입이 동결되었으며 열거 불가한 프로퍼티를 하나 가진 밀봉된 객체를 생성합니다.
let o = Object.seal(
 Object.create(
  Object.freeze({x: 1}),
   {y: {value:2, writable: true}}
 )
);

라이브러리 사용자가 작성한 콜백 함수에 객체를 전달하는 라이브러리를 제작한다면 객체에 Object.freeze()를 사용해서 사용자의 코드에서 라이브러리를 수정하지 못하도록 막을 수 있습니다.
예를 들어, 동결된 객체는 널리 쓰이는 자바스크립트 테스트 스위트를 방해할 수 있습니다.



📌 프로토타입 속성

프로토타입 속성은 객체가 생성될 때 결정됩니다.
객체 리터럴로 생성된 객체의 프로토타입은 Object.prototype입니다.
new로 생성한 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티 값입니다.
Object.create()로 생성된 객체의 프로토타입은 Object.create()의 첫 번째 인자입니다.(null 일 수 있음)

Object.getPrototypeOf({}); // Object.prototype
Object.getPrototypeOf([]); // Array.prototype
Object.getPrototypeOf(()=>{}); // Function.prototype

객체가 다른 객체의 프로토타입인지 (또는 프로토타입 체인에 속해 있는지)는 isPrototypeOf()메서드로 파악합니다.

let p = {x: 1}; // 프로토타입 객체를 정의합니다.
let o = Object.create(p); // 그 프로토타입으로 객체를 생성합니다.
p.isPrototypeOf(o); // true: o는 p를 상속합니다.
Object.prototype.isPrototypeOf(p); // true: p는 Object.prototype을 상속합니다.
Object.prototype.isPrototypeOf(o); // true: o도 마찬가지입니다.

Object.setPrototypeOf()로 객체의 프로토타입을 바꿀 수 있습니다.

let o = {x: 1};
let p = {y: 2};
Object.setPrototypeOf(o, p); // o의 프로토타입을 p로 정합니다.
o.y; // 2: o는 이제 프로퍼티 y를 상속합니다.
let a = [1,2,3];
Object.setPrototypeOf(a, p); // 배열 a의 프로토타입을 p로 바꿉니다.
a.join; // undefined: 이제 a에는 join() 메서드가 없습니다.

Object.setPrototypeOf()를 사용하는 일은 거의 없습니다.
자바스크립트 실행 환경은 객체의 프로토타입이 고정됐다고 가정하고 적극적으로 최적화합니다.
따라서 Object.setPrototypeOf()를 호출하면 변경된 객체를 사용한 코드는 훨씬 느리게 동작할 수 있습니다.

초기 브라우저 일부에서는 객체의 prototype속성에 __proto__라는 이름을 사용했습니다. 이 프로퍼티는 오래전에 폐기됐지만 웹에는 여전히 __proto__가 쓰인 코드가 많이 남아 있으므로 ECMAScript 표준에서 자바스크립트 실행 환경은 모두 __proto__를 지원하도록 정했습니다. (표준에서 요구한 것은 아니지만 노드도 __proto__를 지원합니다.)

최신 자바스크립트에서 __proto__는 읽고 쓸 수 있는 프로퍼티이고, Object.getPrototypeOf()Object.setPrototypeOf()를 대체할 수 도 있지만, 그렇게 해서는 안됩니다.

다음과 같이 객체 리터럴에서 프로토타입을 지정하는 용도로는 쓸 수 있습니다.

let p = {z: 3};
let o = {
 x: 1,
 y: 2,
 __proto__: p
};
o.z; // 3 : o는 p를 상속합니다.


📌 잘 알려진 심벌

심벌 타입은 웹에 이미 배포된 코드와 호환성을 유지하면서 자바스크립트를 안전하게 확장하려는 목적으로 ES6에서 추가되었습니다.

🔗 Symbol.iterator와 Symbol.asynclterator

Symbol.iteratorSymbol.asynclterator 심벌은 객체나 클래스를 이터러블이나 비동기 이터러블로 만듭니다.

🔗 Symbol.haslnstance

표현식 o instanceof f는 o의 프로토타입 체인에서 값 f.prototype을 찾는 방식으로 평가한다고 설명했습니다.
ES6이후에는 Symbol.haslnstance도 사용 할 수 있습니다.

// instanceof와 함께 사용할 수 있도록 타입 객체를 정의합니다.
let uint8 = {
	[Symbol.haslnstance](x) {
		return Number.islnteger(x) && x >= 0 && x <= 255;
	}
};
128 instanceof uint8 // => true
256 instanceof uint8 // => false: 너무 큽니다.
Math.PI instanceof uint8 // => false: 정수가 아닙니다.

🔗 Symbol.toStringTag

{}.toString() // => "[object Object]"
Object.prototype.toString.call([]) // => "[object Array]"
Object.prototype.toString.call(/./) // => " [object RegExp]"
Object.prototype.toString.call(()=>{}) // => "[object Function]"
Object.prototype.toString.call("") // => "[object String]"
Object.prototype.toString.call(0) // => "[object Number]"
Object.prototype.toString.call(false) // => "[object Boolean]"
     
function classof(o) {
	return Object.prototype.toString.call(o).slice(8,-1);
}

classof(null) // => "Null"
classof(undefined) // => "Undefined"
classof(1) // => "Number"
classof(10n**100n) // => "Biglnt"
classof("") // => "String"
classof(false) // => "Boolean"
classof(Symbol()) // => "Symbol"
classof({}) // => "Object"
classof([]) // => "Array"
classof(/./) // => "RegExp"
classof(()=>{}) // => "Function"
classof(new Map()) // => "Map”
classof(new Set()) // => "Set”
classof(new Date()) // => "Date"

class Range {
	get [Symbol.toStringTag]() { return "Range"; }
	// 나머지 클래스 정의는 생략합니다.
}
let r = new Ranged, 10);
Object.prototype.toString.call(r) // => "[object Range]"
classof(r) // => "Range"

🔗 Symbol.species

// 첫 번째와 마지막 요소에 게터를 추가하는 서브클래스
class EZArray extends Array {
	get first() { return this[0]; }
	get last() { return this[this.length-1]; }
}
let e = new EZArray(1,2,3);
let f = e.map(x => x * x);
e.last // => 3: EZArray e의 마지막 요소
f.last // => 9: f도 last 프로퍼티가 있는 EZArray입니다.

ES6 이후의 Array() 생성자에는 심벌 이름 Symbol.species를 가진 프로퍼티가 있습니다.
(이 심벌은 생성자 함수의 프로퍼티 이름으로 사용된다.)
여기서 설명하는 다른 잘 알려진 심벌 대부분은 프로토타입 객체의 메서드 이름으로 쓰입니다.

extends로 서브클래스를 만들면 그 서브클래스 생성자는 슈퍼클래스 생성자에서 프로퍼티를 상속합니다.
(서브클래스 인스턴스가 슈퍼 클래스에서 메서드를 상속하는 일반적인 상속에 추가된 것)

따라서 배열의 서브클래스 생성자에 는 모두 상속된 Symbol.species 프로퍼티가 있습니다.
(필요하다면 서브클래스 에서 같은 이름의 자체 프로퍼티를 정의할 수도 있습니다.)

map()slice()처럼 새로운 배열을 생성해 반환하는 메서드는 ES6 이후 조금 바뀌었습니다.
이들은 일반적인 배열을 생성하지 않고, new this.constructorSymbol.species를 호출한 것과 같은 새로운 배열을 생성합니다.

Array[Symbol.species]는 읽기 전용 접근자 프로퍼티이며, 그 게터 함수는 단순히 this를 반환합니다.

서브클래스 생성자는 이 게터 함수를 상속하므로, 기본적으로 모든 서브 클래스 생성자는 독립적인 ‘종족’입니다.

배열을 반환하는 메서드 가 EZArray에서 일반적인 배열을 반환하길 원한다면 EZArray[Symbol.species]를 Array로 설정하기만 하면 됩니다.

하지만 상속된 프로퍼티가 읽기 전용 접근자이므로 그냥 할당 연산자를 써서 설정할 수는 없고 defineProperty()를 사용해야 합니다.

EZArray[Symbol, species] = Array; // 읽기 전용 프로퍼티를 설정하려는 시도는 실패합니다.
// 대신 defineProperty() 사용 가능합니다.
Object.defineProperty(EZArray, Symbol.species, {value: Array});

class EZArray extends Array {
	static get [Symbol.species]() { return Array; }
	get first() { return this[0]; }
	get last() { return this[this.length-1]; }
}
let e = new EZArray(1,2,3);
let f = e.map(x => x - 1);
e.last // => 3
f.last // => undefined: f는 last 게터가 없는 일반적인 배열입니다.

Symbol.species를 도입한 설계 의도는 원래 배열의 유용한 서브클래스를 만들 수 있게 하려는 것이었지만 이 잘 알려진 심벌을 배열에만 쓸 수 있는 것은 아닙니다.

형식화 배열 클래스에도 배열 클래스와 같은 방법으로 이 심벌을 사용할 수 있습니다.

ArrayBuffer의 slice() 메서드는 단순히 새로운 ArrayBuffer를 생성하지 않고 this.constructor의 Symbol.species 프로퍼티를 참조합니다.

then() 같은 프라미스 메서드는 이 프로토콜을 통해 새로운 프라미스 객체를 생성해 반환합니다.

맵의 서브클래스를 만들고, 새로운 Map 객체를 반환하는 메서드를 정의한다면 서브클래스의 서브클래스를 만들 때 Symbol.species가 유용할 것입니다.

🔗 Symbol.isConcatSpreadable

ES6 전의 concat()은 값이 배열인지 아닌지 판단할 때 Array.isArray()를 사용했습니다.

ES6에서는 이 알고리즘이 조금 바뀌었는데, concat()의 인자나 this 값 이 객체이고 심벌 이름 Symbol.isConcatSpreadable이 있는 프로퍼티가 있다면 이 프로퍼티의 불 값을 사용해 인자를 ‘분해(spread)’할지 판단합니다.

만약 그런 프로퍼티 가 없다면 이전 버전과 마찬가지로 Array.isArray()를 사용합니다.

배열 비슷한 객체를 생성하면서 이 객체를 concat()에 전달할 때 배열처럼 동작하길 원한다면 Symbol.isConcatSpreadable 프로퍼티를 객체에 추가합니다.

let arraylike = { 
	length: 1, 
	0: 1, 
	[Symbol.isConcatSpreadable]: true 
};
[].concat(arraylike) // => [1]: (분해되지 않았다면 [[1]]이었음)

배열 서브클래스는 기본적으로 분해 가능하므로,
서브클래스가 concat()에서 배열처럼 동작하지 않길 원한다면 다음과 같은 게터를 서브클래스에 추가합니다.

class NonSpreadableArray extends Array {
	get [Symbol.isConcatSpreadable]() { return false; }
}
let a = new NonSpreadableArray(l,2,3);
[].concat(a).length // => 1; (a가 분해됐다면 3이었음)

🔗 패턴 매칭 심벌

정규 표현식은 범용적이고 아주 강력하지만, 복잡하기도 하고 퍼지 매칭에는 적합하지 않습니다.

범용적인 문자열 메서드에 잘 알려진 심벌 메서드를 써서 패턴 매칭 클래스를 정의할 수 있습니다.

예를 들어 Intl.Collator를 사용해 검색할 때 악센트를 무시한 채 문자열을 비교할 수 있습니다.

또는 사운덱스(Soundex) 알고리즘을 바탕으로 패턴 클래스를 만들어 단어를 유사한 발음으로 검색할 수도 있고, 주어진 레벤슈타인(Levenshtein) 거리에 따라 느슨하게 일치하는 문자열을 찾을 수도 있습니다.

다음과 같이 다섯 가지 문자열 메서드와 패턴 객체를 사용한다고 가정해봅시다.

string.method(pattern, arg)

위 코드는 다음과 같이 패턴 객체의 심벌 이름 메서드를 호출하는 것 과 같습니다.

pattern[symbol](string, arg)

다음은 *?와일드 카드를 사용하는 패턴매칭 클래스입니다.
이런 스타일의 패턴 검색은 유닉스 운영체제 초기부터 널리 쓰였고, 이런 패턴을 글롭(glob)이라 부릅니다.

class Glob {
  constructor(glob) {
    this.glob = glob;

    // 내부적으로 정규 표현식을 사용해 글롭을 검색합니다.
		// ?는 /를 제외한 글자 하나에 일치하고 *는 0개 이상의 글자에 일치합니다.
		// 각 와일드카드를 캡처 그룹으로 캡처합니다.
		// 'docs/([^/]*).txt'
    let regexpText = glob.replace('?', '([^/])').replace('*', '([^/]*)');

    // 유니코드를 인식하도록 u 플래그를 썼습니다.
		// 글롭은 문자열 전체에 일치하도록 만들어졌으므로 ”와 $ 앵커를 사용합니다.
		// search()나 matchAUO은 이런 패턴에 적합하지 않으므로 쓰지 않았습니다.
    this.regexp = new RegExp(`^${regexpText}$`, 'u');
  }

  toString() {
    return this.glob;
  }

  [Symbol.search](s) {
    return s.search(this.regexp);
  }
  [Symbol.match](s) {
    return s.match(this.regexp);
  }
  [Symbol.replace](s, replacement) {
    return s.replace(this.regexp, replacement);
  }
}

let pattern = new Glob('docs/*.txt');
'docs/js.txt'.search(pattern); // => 0: 인덱스 0에 일치합니다.
'docs/js.htm'.search(pattern); // => -1: 일치하지 않습니다.
let match = 'docs/js.txt'.match(pattern);
match[0]; // => "docs/js.txt"
match[1]; // => "js"
match.index; // => 0
'docs/js.txt'.replace(pattern, 'web/$1.htm'); // => "web/js.htm"

🔗 Symbol.toPrimitive

ES6 이후 Symbol.toPrimitive가 객체를 기본 값으로 변환하는 기본 동작을 덮어 쓸 수 있게 하여, 클래스 인스턴스가 기본 값으로 변환되는 방법을 완전히 제어할 수 있게 되었습니다.

Symbol.toPrimitive 메서드는 반드시 객체를 표현하는 기본 값을 반환해야 합니다.

이 메서드는 문자열 인자를 하나 받는데, 각 인자는 자바스크립트가 객체를 어떤 값으로 변환하려 하는지 나타냅니다.

인자가 string이면 자바스크립트가 문자열을 예상하거나 선호하지만 필수는 아닌 컨텍스트에 있다는 뜻입니다.
(예를 들어 템플릿 리터럴에 객체를 사용하는 경우가 이에 해당합니다.)

인자가 number면 자바스크립트가 숫자 값을 예상하거나 선호하지만 필수는 아닌 컨텍스트에 있다는 뜻입니다.
(예를 들어 객체를 <> 연산자 또는 * 같은 산술 연산자와 함께 사용하는 경우를 말합니다.)

인자가 default면 자바스크립트가 숫자나 문자열이 모두 가능한 컨텍스트에 있다는 뜻입니다. (+,==, !=가 이에 해당합니다.)

대부분의 클래스가 이 인자를 무시하고 항상 똑같은 기본 값을 반환합니다.

클래스 인스턴스를 <, >와 함께 사용해야 한다면 [Symbol.toPrimitive] 메서드를 정의해야 합니다.

🔗 Symbol.unscopables

배열 클래스에 새로운 메서드를 추가할 때 with 문 때문에 호환성 문제가 발생했고, 이로 인해 기존 코드가 제대로 동작하지 않았습니다.

이런 문제를 해결하기 위해 Symbol.unscopables가 도입 되었습니다..

ES6 이후 with 문은 조금 수정되었는데,
객체 o가 있을 때 with 문은 Object.keys(o[Symbol.unscopables]||{})를 계산하고 바디의 가상 스코프를 생성할 때 그 결과에 포함된 프로퍼티는 무시합니다.

ES6은 Array.prototype에 이 심벌을 써서 새로운 메서드를 추가했고 웹에 존재하는 기존 코드도 손상시키지 않았습니다.

let newArrayMethods = Object.keys(Array.prototype[Symbol.unscopables]);

📌 탬플릿 태그

태그 함수는 일반적인 자바스크립트 함수일 뿐 이들을 정의하는 특별한 문법이 있는 것도 아닙니다.

함수 표현식 뒤에 템플릿 리터럴이 있으면 함수가 호출됩니다.

첫 번째 인자는 문자열 배열이며 그 뒤 에 0개 이상의 인자를 붙이고, 이 인자들의 값은 타입에 제한이 없습니다.

태그는 최종 문자열을 만들기 전에 각 값을 HTML에 알맞게 이스케이프합니다.

function html(strings, ...values) {
  // 각 값을 문자열로 변환하고 HTML 특수 문자를 이스케이프합니다.
  let escaped = values.map((v) =>
    String(v)
      .replace('&', '&amp;')
      .replace('<', '&lt;')
      .replace('>', '&gt;')
      .replace('"', '&quot;')
      .replace("'", '&#39;')
  );

  // 이스케이프 결과를 병합한 문자열을 반환합니다.
  let result = strings[0];
  for (let i = 0; i < escaped.length; i++) {
    result += escaped[i] + strings[i + 1];
  }
  return result;
}

let operator = '<';
html`<b>x ${operator} y</b>`; // => "<b>x &lt; y</b>"

let kind = 'game',
  name = 'D&D';
html`<div class="${kind}">${name}</div>`; // =>'<div class="game">D&amp;D</div>'
function glob(strings, ...values) {
  // 문자열과 값을 문자열 하나로 합칩니다.
  let s = strings[0];
  for (let i = 0; i < values.length; i++) {
    s += values[i] + strings[i + 1];
  }
  // 합친 문자열을 분석해 반환합니다.
  return new Glob(s);
}

let root = '/tmp';
let filePattern = glob`${root}/*.html`; // 정규 표현식
'/tmp/test.html'.match(filePattern)[1]; // => "test"

태그 함수를 호출할 때, 첫 번째 인자는 문자열 배열입니다.
하지만 이 배열에는 raw라는 프로퍼티가 있는데 그 값은 같은 수의 문자열로 이루어진 다른 배열입니다.

인자 배열에는 이스케이프 시퀀스를 일반적으로 해석한 문자열이 들어 있습니다.

raw 배열에는 이스케이프 시퀀스를 해석하지 않은 문자열이 들어 있습니다.

이 특징은 문법 에서 역슬래시를 사용하는 DSL을 정의할 때 중요합니다.
예를 들어 glob 태그 함수가 슬래시 대신 역슬래시를 사용하는 윈도우 스타일 경로를 지원해야 하고 사용할 때마다 이중 역슬래시를 쓰는 번거로움을 피하고 싶다면 strings[] 대신 strings .raw[]를 사용하도록 함수를 고쳐 쓰면 됩니다.

하지만 이렇게 고치면 글롭 리터럴에서는 \u 같은 이스케이프를 더 이상 사용할 수 없습니다.

profile
성장에는 성장통이 있기 마련이다.

0개의 댓글