메타프로그래밍(Metaprogramming) 이란
자기 자신 혹은 다른 컴퓨터 프로그램을 데이터로 취급하며 프로그램을 작성·수정하는 것을 말한다.
넓은 의미에서, 런타임에 수행해야 할 작업의 일부를 컴파일 타임 동안 수행하는 프로그램을 말하기도 한다.
메타 프로그래밍에 이용되는 언어를 메타 언어라고 하고, 메타 프로그래밍의 대상이 되는 언어를 대상 언어라고 한다.
메타프로그래밍-위키백과메타언어(metalanguage) 란
대상을 직접 서술하는 언어 그 자체를 다시 언급하는 언어나 심볼(symbol)로서 고차언어(高次言語)라고도 한다. 메타 언어의 문장이나 절의 구조는 메타문법으로 기술된다.
메타언어-위키백과메타프로그래밍은 다른 코드를 조작하는 코드를 작성한다.
자바스크립트 같은 동적 언어에서는 프로그래밍/메타프로그래밍이 뚜렷이 구분되지는 않지만,
이 책에서 설명하는 메타프로그래밍의 주제(목차) 는 다음과 같다.
- 객체 프로퍼티의 속성 : 열거 가능성(enumerability), 쓰기 가능성(writability), 변경 가능성(configurability)
- 객체의 확장성 제어와 객체 밀봉(seal), 동결(freeze)
- 객체 프로토타입 검색과 설정
- 잘 알려진 심볼로 타입 튜닝
- 템플릿 태그 함수로 DSL(도메인 특정 언어) 만들기
- reflect 메서드로 객체 탐지
- 프록시를 사용한 객체 동작 제어
프로퍼티 속성을 검색하고 설정하는 API는
- 프로토타입 객체에 메서드를 추가하고, (내장 메서드처럼) 열거 불가로 만들 수 있고,
- 변경/삭제 불가한 프로퍼티를 만들어 객체를 잠글 수 있으므로
중요하다.
객체 프로퍼티의 3가지 속성
자바스크립트 객체의 프로퍼티에는 name, value 뿐만 아니라, 프로퍼티가 어떻게 동작하는지 나타내는 세 가지 속성이 있다.
- 쓰기 가능(writable) : 프로퍼티 값을 바꿀 수 있는지 나타낸다.
- 열거 가능(enumerable) : for/in 루프나 Object.keys() 메서드에서 해당 프로퍼티를 열거할 수 있는지 나타낸다.
- 변경 가능(configurable) : 프로퍼티를 삭제할 수 있는지, 프로퍼티 속성을 바꿀 수 있는지 나타낸다.
(이 책의 14장에서는) 데이터 프로퍼티의 값(value)
과
접근자 프로퍼티의 게터
와 세터
메서드도 아래와 같이 프로퍼티의 속성으로 간주한다.
프로퍼티의 속성을 열거하는 객체이다. 아래 메서드들에서 주로 사용된다.
지정된 객체의 프로퍼티 서술자를 호출하는 메서드이다.
자체 프로퍼티에만 동작하므로,
상속된 프로퍼티 속성을 검색하려면 반드시 명시적으로 프로토타입 체인을 검색해야 한다.
/* 객체의 데이터 프로퍼티의 '프로퍼티 서술자 객체' 호출
: 해당 데이터 프로퍼티의 속성을 확인할 수 있다. */
const o = { x: 1 };
Object.getOwnPropertyDescriptor(o, "x");
// { value: 1, writable: true, enumerable: true, configurable: true }
/* 객체의 접근자 프로퍼티의 '프로퍼티 서술자 객체' 호출
: 해당 접근자 프로퍼티의 속성을 확인할 수 있다. */
const random = {
get octet() {
return Math.floor(Math.random() * 256);
},
};
Object.getOwnPropertyDescriptor(random, "octet");
// { get: [Function: get octet], set: undefined, enumerable: true, configurable: true }
프로퍼티 속성을 설정하거나 지정된 속성으로 새로운 프로퍼티를 생성하는 메서드이다.
파라미터로 ⑴수정할 객체, ⑵생성하거나 변경할 프로퍼티 이름, ⑶프로퍼티 서술자 객체를 전달해야 한다.
let a = [];
Object.getOwnPropertyDescriptor(a, "length");
// { value: 0, writable: true, enumerable: false, configurable: false }
/* 상속된 프로퍼티는 변경할 수 없고, TypeError가 발생한다.*/
Object.defineProperty(a, "length", { value: 10, enumerable: true });
// TypeError: Cannot redefine property: length
생성, 수정이 허용되지 않는 프로퍼티를 생성, 수정하면 TypeError가 일어난다.
// 프로퍼티가 전혀 없는 상태에서 시작한다.
let o = {};
// 값이 1인 열거 불가 데이터 프로퍼티 x를 추가한다.
Object.defineProperty(o, "x", {
value: 1,
writable: true,
enumerable: false,
configurable: true,
}); /* o 는 {x: 1} 이 된다. */
// enumerable: false → x 프로퍼티가 존재하지만, 열거되지 않는다.
o.x; // 1
Object.keys(o); // []
// 프로퍼티 x를 읽기 전용으로 수정한다.
Object.defineProperty(o, "x", { writable: false });
// 프로퍼티 값 변경을 시도한다.
o.x = 2; // 조용히 실패하거나 스트릭트 모드에서는 TypeError를 일으킨다.
o.x; // 1
// configurable: true → 프로퍼티는 여전히 변경 가능이므로 다음과 같이 값을 바꿀 수 있다.
Object.defineProperty(o, "x", { value: 2 });
o.x; // 2
// x를 데이터 프로퍼티에서 접근자 프로퍼티로 바꿀 수도 있다.
Object.defineProperty(o, "x", {
get: function () {
return 0;
},
});
o.x; // 0
둘 이상의 프로퍼티를 한 번에 생성하거나 수정하는 메서드이다.
let p = Object.defineProperties(
{},
{
x: { value: 1, writable: true, enumerable: true, configurable: true },
y: { value: 2, writable: true, enumerable: true, configurable: true },
}
);
p; // { x: 1, y: 2 }
Object.defineProperty()
와Object.defineProperties()
의 규칙
- 객체가 확장 불가이면 기존의 자체 프로퍼티를 수정할 수는 있지만 새로운 프로퍼티를 추가할 수는 없다.
- 프로퍼티가 변경 불가이면 변경 가능 속성이나 열거 가능 속성을 바꿀 수 없다.
- 접근자 프로퍼티가 변경 불가이면 게터나 세터 메서드를 바꿀 수 없고, 데이터 프로퍼티로 바꿀 수도 없다.
- 데이터 프로퍼티가 변경 불가이면 접근자 프로퍼티로 바꿀 수 없다.
- 쓰기 가능 속성을 false → true로 바꾸는 것은 불가능하지만,
- true → false로 바꾸는 것은 가능하다.
- 변경 불가이고 읽기 전용이면 값을 바꿀 수 없다.
- 읽기 전용이더라도 변경 가능이면 프로퍼티의 값을 바꿀 수 있다.
(쓰기 가능으로 바꾸고 → 값을 바꾼 다음 → 다시 읽기 전용으로 바꾸는 것이나 마찬가지이기 때문이다.)
지정된 프로토타입 객체 및 속성(property)을 갖는 새 객체를 만드는 메서드이다.
첫 번째 파라미터로 새로 만든 객체의 프로토타입이어야 할 객체를 받는다.
옵셔널한 두 번째 파라미터로 Object.defineProperties의 두 번째 파라미터와 같은 프로퍼티 서술자 객체를 받는다.
// Shape 함수 생성 - 상위클래스
function Shape() {
this.x = 10;
this.y = 10;
}
// Rectangle 함수 생성 - 하위클래스
function Rectangle() {
Shape.call(this); // class의 super와 같은 역할, 상위 클래스의 this를 하위 클래스로 확장한다.
}
// Rectangle 함수의 프로토타입으로 Shape 함수를 지정함
Rectangle.prototype = Object.create(Shape.prototype);
// Shape - Rectangle - rect로 프로토타입 체이닝으로 이어져 있다.
let rect = new Rectangle();
console.log(rect.x); // 10
console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Shape); // true
let o;
// 프로토타입이 null인 객체 생성, Object와도 체이닝되어 있지 않다.
o = Object.create(null);
// o = {}; 와 같다. Object와 체이닝 되어 있다.
o = Object.create(Object.prototype);
// 생략한 속성은 false나 undefined로 간주한다.
o = Object.create({}, { p: { value: 42 } });
o.p; // 42
// writable: false로 수정할 수 없다.
o.p = 24;
o.p; // 42
// let obj = {p: 100}과 같다.
let obj = Object.create( {}, {
p: {
value: 100,
writable: true,
enumerable: true,
configurable: true,
},
});
// 두 번째 파라미터의 키는 *속성 설명자*에 맵핑된다.
o = Object.create(Object.prototype, {
// o.visible는 데이터 프로퍼티
visible: {
value: "hello",
writable: true,
enumerable: true,
configurable: true,
},
// o.invisible는 접근자(게터와 세터) 프로퍼티
invisible: {
configurable: false,
get: function () {
return 10;
},
set: function (value) {
console.log(`can't set 'o.invisible' to ${value}`);
},
/* ES5 접근자로 코드는 key를 생략할 수 있다
get function() { return 10; },
set function(value) { `can't set 'o.invisible' to ${value}`; } */
},
});
console.log(o); // { visible: 'hello' }
o.invisible = 100; // can't set 'o.invisible' to 100
console.log(o.invisible); // 10
소스 객체의 모든 열거 가능한 자체 프로퍼티를 복사해 타겟 객체에 붙여넣는 메서드이다.
/* 데이터 프로퍼티만 있는 경우 */
let target = { a: 1, b: 2 };
let source = { b: 4, c: 5 };
let copiedTarget = Object.assign(target, source);
console.log(target); // { a: 1, b: 4, c: 5 }
console.log(copiedTarget === target); // true
/* source 객체에 접근자 프로퍼티가 있는 경우 */
target = { a: 1, b: 2 };
source = {
b: 4,
get c() {
return 5;
},
set c(value) {
console.log(`can't change a to ${value}`);
},
};
console.log(source); // { b: 4, c: [Getter/Setter] }
copiedTarget = Object.assign(target, source);
// 접근자 프로퍼티(게터와 세터)는 복사되지 않는다.
console.log(target); // { a: 1, b: 4, c: 5 }
console.log(copiedTarget === target); // true
확장 가능 속성은 객체에 새로운 프로퍼티를 추가할 수 있는지를 결정한다.
Object.isExtensible()
로 확장 가능 여부를 확인할 수 있다.
자바스크립트의 객체는 기본적으로 확장 가능이지만,
Object.preventExtensions()
로 객체를 확장 불가로 만들 수 있다.
const obj = {};
// 객체는 기본적으로 확장 가능하지만
Object.isExtensible(obj); // true
// 확장 불가로 만들 수 있다.
Object.preventExtensions(obj);
Object.isExtensible(obj); // false
// 새로운 프로퍼티를 추가할 수 없다.
Object.defineProperty(obj, "property1", { value: 42 });
// TypeError: Cannot define property property1, object is not extensible
// 프로토타입 확장시도도 TypeError가 발생한다.
const upperObj = { x: 1, y(){ return 2; } };
obj.__proto__ = upperObj; // TypeError: #<Object> is not extensible
객체를 확장 불가로 만들면, 다시 확장 가능으로 되돌릴 수 없다.
확장 불가 객체에 새로운 프로퍼티를 추가하면,
확장 불가인 객체의 프로토타입을 변경하려는 시도도 항상 TypeError를 일으킨다.
const parentObj = { x: 1, y(){ return 2; } };
// parentObj 의 프로토타입 체인에 연결된 childObj
let childObj = Object.create(parentObj);
// childObj 객체를 확장 불가로 설정
Object.preventExtensions(childObj);
// 프로토타입에 새로운 프로퍼티를 추가하면, 그대로 상속된다.
parentObj.z = 3;
childObj.z; // 3
단, 객체 자체의 확장성만 제어하므로,
확장 불가인 객체의 프로토타입에 새로운 프로퍼티를 추가하는 경우, 새로운 프로퍼티는 그대로 상속된다.
객체를 확장 불가로 만드는 동시에 자체 프로퍼티를 변경 불가로 바꾼다.
즉, 새로운 프로퍼티를 추가할 수 없고 기존 프로퍼티를 삭제할 수도 없다.
밀봉 후 풀 수 있는 방법은 없다.
let obj = { func() {}, x: "hello", };
// 객체를 밀봉함
Object.seal(obj);
Object.isSealed(obj); // true
// 밀봉한 객체의 속성값은 밀봉 전과 마찬가지로 변경할 수 있음
obj.x = "hi";
obj.x; // hi
// 데이터 속성과 접근자 속성 사이의 전환은 불가
Object.defineProperty(obj, "x", {
get: function () {
return "g";
},
}); // TypeError: Cannot redefine property: x
// 속성값의 변경을 제외한 어떤 변경도 적용되지 않음
obj.y = "nice to meet you"; // 에러가 나지는 않지만 속성은 추가되지 않음
delete obj.x; // 에러가 나지는 않지만 속성이 삭제되지 않음
obj; // { func: [Function: func], x: 'hi' }
// strict mode 에서는 속성값의 변경을 제외한 모든 변경은 TypeError 발생
function fail() {
"use strict";
delete obj.x; // TypeError: Cannot delete property 'x' of #<Object>
obj.sparky = "arf"; // TypeError: Cannot add property sparky, object is not extensible
}
fail();
// Object.defineProperty() 메서드를 이용한 프로퍼티의 추가도 TypeError 발생
Object.defineProperty(obj, "ohai", { value: 17 }); // TypeError: Cannot define property ohai, object is not extensible
// 속성값의 변경은 가능함
Object.defineProperty(obj, "func", { value: "not function anymore" });
console.log(obj); // { func: 'not function anymore', x: 'hi' }
스트릭트 모드에서는 속성값의 변경을 제외한 모든 변경에서 TypeError가 발생한다.
일반 모드에서는
객체를 확장 불가, 프로퍼티는 변경 불가로 바꾸는 동시에 객체 자체 데이터 프로퍼티를 읽기 전용으로 바꾼다.
let obj = { _name: "Joy",
get name() { return obj._name; },
set name(value) {
obj._name = value;
console.log(`You request to change name to ${value}!`);
},
greeting: "hello",
};
// 동결 : 새 속성을 추가할 수 없고, 기존 속성을 변경하거나 제거할 수 없다.
Object.freeze(obj);
console.log(Object.isFrozen(obj)); // true
// 이제 모든 변경 시도는 조용히 실패한다.
obj.greeting = "hi";
// setter 함수가 실행되지만, 프로퍼티 값이 변경되지는 않는다.
obj.name = "Jessica"; // You request to change name to Jessica!
console.log(obj.name); // Joy
obj.friend = "Jenny";
console.log(obj); // { _name: 'Joy', name: [Getter/Setter], greeting: 'hello' }
// 엄격 모드에서는 이러한 시도에 대해 TypeError가 발생한다.
function fail() {
"use strict";
obj.greeting = "hi"; // TypeError: Cannot assign to read only property 'greeting' of object '#<Object>'
delete obj.greeting; // TypeError: Cannot delete property 'greeting' of #<Object>
delete obj.quaxxor; // 'quaxxor' 속성은 추가된 적이 없으므로 true 반환
obj.y = 100; // TypeError: Cannot add property y, object is not extensible
}
fail();
// Object.defineProperty를 통한 변경 시도도 모두 TypeError가 발생한다.
Object.defineProperty(obj, "ohai", { value: 17 }); // TypeError: Cannot define property ohai, object is not extensible
Object.defineProperty(obj, "greeting", { value: 100 }); // TypeError: Cannot redefine property: greeting
// 프로토타입을 변경하는 것 또한 불가하므로 TypeError가 발생한다.
obj.__proto__ = { greeting: 20 }; // TypeError: #<Object> is not extensible
객체에 세터 메서드가 있는 접근자 프로퍼티가 있는 경우,
프로퍼티에 새로운 값을 할당할 때 호출 되지만, 프로퍼티 값이 변경되지는 않는다.
기본적으로 얕은 동결이 이루어지며, 깊은 동결을 위해서는 각 객체 타입의 프로퍼티를 재귀적으로 동결해야한다.
Object.freeze(obj)
의 얕은 동결
obj의 프로퍼티 값이 객체라면, 그 객체는 동결되지 않으며 속성 추가, 제거, 재할당의 대상이 될 수 있다.
객체의 완전 잠금
Object.seal()
Object.freeze()
모두 전달받은 객체에만 효과가 있으며 그 객체의 프로토타입은 변경하지 않는다.
객체를 완전히 잠그려면 프로토타입 체인에 있는 객체 역시 밀봉 또는 동결해야한다.
객체의 prototype 속성은 객체가 프로퍼티를 상속하는 '부모' 객체이고, 객체가 생성될 때 설정된다.
객체 리터럴
로 생성된 객체의 프로토타입은 Object.prototype이다.new
로 생성한 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티 값이다.Object.create()
로 생성된 객체의 프로토타입은 Object.create()의 첫 번째 파라미터이다.getPrototypeOf()
지정된 객체의 프로토타입을 확인할 수 있는 메서드이다.
isPrototypeOf
해당 객체가 다른 객체의 프로토타입 체인에 속한 객체인지 확인할 수 있는 메소드이다.
const parentObj = { x: 1, y() { return 2; }, };
// parentObj 의 프로토타입 체인에 연결된 childObj
let childObj = Object.create(parentObj);
Object.getPrototypeOf(childObj); // { x: 1, y: [Function: y] }
parentObj.isPrototypeOf(childObj); // true
Object.prototype.isPrototypeOf(childObj); // true
초기 브라우저 일부에서는 객체의 prototype 속성에
__proto__
라는 이름을 사용했고, 아직 많은 환경에서 지원하지만 JavaScript 엔진의 성능 저하 등의 문제로 사용을 지양해야 한다.(자세한 내용은 링크된 MDN 참조)
심벌 타입은 웹에 이미 배포된 코드와 호환성을 유지하면서 자바스크립트를 안전하게 확장하려는 목적으로 ES6에서 추가되었다.
Symbol.iterator
, Symbol.asynclterator
심벌은 객체나 클래스를 이터러블이나 비동기 이터러블로 만든다.
표현식obj instanceof func
에서 func는 반드시 생성자 함수여야한다.
obj의 프로토타입 체인에서 값 func.prototype
을 찾는 방식으로 평가한다.
func 자리에 Symbol.hasInstance
메서드가 있는 객체가 있으면, obj을 인자로 Symbol.hasInstance
메서드를 호출하며, 메서드의 반환값을 boolean으로 변환한 값이 결과값(=instanceof
의 연산 값)이다.
// 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: 정수가 아닙니다.
// 기본 자바스크립트 객체에서 호출
{}.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]"
ES6 이전에는 내장 타입의 인스턴스에만 사용할 수 있었다.
→ 그 외 직접 정의한 클래스 인스턴스에서는 "Object"만 반환
// typeof 연산자보다 정확한 타입 확인 가능
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"
ES6 이후 파라미터에서 심벌 이름 Symbol.toStringTag를 가진 프로퍼티를 찾고, 그런 프로퍼티가 존재하면 그 값을 반환한다.
→ 따라서 클래스를 직접 정의한 경우에도 타입에 대한 반환값이 object
가 아니고, toStringTag에 정의된 값을 확인할 수 있다.
// 첫 번째와 마지막 요소에 게터를 추가하는 서브클래스
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 이후 .concat()
메서드의 내부 로직이 전달된 this 객체 또는 파라미터에 Symbol.isConcatSpreadable이 있는 프로퍼티가 있는지 여부에 따라 달라졌다.
ES6 이후 Symbol.toPrimitive가 객체를 기본 값으로 변환하는 기본 동작을 덮어 쓸 수 있게 하여, 클래스 인스턴스가 기본 값으로 변환되는 방법을 완전히 제어할 수 있다.
Symbol.toPrimitive 메서드는 반드시 객체를 표현하는 기본 값을 반환해야 한다. 이 메서드는 문자열 인자를 하나 받는데, 각 인자는 자바스크립트가 객체를 어떤 값으로 변환하려 하는지 나타낸다.
인자가 string이면 자바스크립트가 문자열을 예상하거나 선호하지만 필수는 아닌 컨텍스트에 있다는 뜻이다.
인자가 number면 자바스크립트가 숫자 값을 예상하거나 선호하지만 필수는 아닌 컨텍스트에 있다는 뜻이다.
인자가 default면 자바스크립트가 숫자나 문자열이 모두 가능한 컨텍스트에 있다는 뜻이다.
대부분의 클래스가 이 인자를 무시하고 항상 똑같은 기본 값을 반환한다.
클래스 인스턴스를 <, >와 함께 사용해야 한다면 Symbol.toPrimitive
메서드를 정의할 수 있다.
with 문
때문에 발생한 호환성 문제를 해결하기 위해 도입되었다.
` 백틱 ` 안에 포함된 문자열을 템플릿 리터럴이라고 부른다.
태그 함수는 일반적인 자바스크립트 함수일 뿐 이들을 정의하는 특별한 문법이 있는 것은 아니다.
함수 표현식 뒤에 템플릿 리터럴이 있으면 함수가 호출된다.
태그는 최종 문자열을 만들기 전에 각 값을 HTML에 알맞게 이스케이프한다.
function html(strings, ...values) {
// 각 값을 문자열로 반환하고 HTML 특수 문자를 이스케이프한다.
let escaped = values.map(v =>
String(v)
.replace('&', '&')
.replace('<', '<')
.replace('>', '>')
.replace('"', '"')
.replace("'", '''),
);
// 이스케이프 결과를 병합한 문자열을 반환한다.
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 < y</b>"
let kind = 'game',
name = 'D&D';
html`<div class="${kind}">${name}</div>`; //'<div class="game">D&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
같은 이스케이프를 더 이상 사용할 수 없다.Reflect 객체는 클래스가 아닌 함수를 모은 집합이다.(e.g. Math 객체)
객체와 그 프로퍼티를 반영(reflect) 하는 API이다.
함수 네임스페이스를 하나로 모은 편리한 세트이며, 이 함수들은 자바스크립트 코어의 동작을 흉내 내고 기존 객체 메서드를 복사한다.
프록시(Proxy) 클래스를 사용하면 자바스크립트 객체의 기본적인 동작을 직접 구현하며 바꿀 수 있고, 일반적인 객체에서는 불가능한 방법으로 동작하는 객체를 만들 수 있다.
✨ 14장 메타프로그래밍 요약
- 자바스크립트 객체에는 확장 가능 속성이 있고, 객체 프로퍼티에는 값과 게터, 세터 외에도 쓰기 가능, 열거 가능, 변경 가능 속성이 있다. 이 속성을 이용해 객체를 다양한 방법으로 잠글 수 있다. 객체의 밀봉과 동결도 잠그는 방법에 속한다.
- 자바스크립트에는 객체의 프로토타입 체인을 탐색하는 함수가 존재하며 심지어 객체의 프로토타입을 바꿀 수도 있다.
(하지만 이렇게 하면 코드가 느려질 수 있다.)
- Symbol 객체의 프로퍼티에는
잘 알려진 심벌
이 있다. 객체나 클래스를 직접 정의할 때 이 심벌을 프로퍼티나 메서드 이름으로 쓸 수 있다.
- 심벌을 이름으로 사용하면 객체가 자바스크립트의 핵심 특징, 코어 라이브러리와 상호작용하는 방식을 바꿀 수 있다.
- 예를 들어 잘 알려진 심벌을 써서 클래스를 이터러블로 만들 수 있고, 인스턴스를 Object.prototype.toString()에 전달했을 때 반환되는 문자열도 바꿀 수 있다.
- ES6 전에는 실행 환경에 내장된 네이티브 클래스에서만 이런 기능을 사용할 수 있었다.
태그된 탬플릿 리터럴
은 함수 호출 문법이며, 새로운 태그 함수를 정의하는 것은 자바스크립트에 새로운 리터럴 문법을 추가하는 것과 같다. 템플릿 문자열 인자를 분석하는 태그 함수를 만들어 자바스크립트 코드에 DSL을 임베드할 수 있다. 태그 함수는 역슬래시에 특별한 의미가 없는 있는 그대로의, 이스케이프되지 않은 문자열 리터럴에 접근할 수 있다.
- 프록시 클래스와 관련 리플렉트 API는 자바스크립트 객체의 기본적인 동작에 대한 저수준 제어를 허용한다. 프록시 객체를 취소할 수 있는 래퍼로 사용해 코드를 더 잘 캡슐화할 수 있고, 초기 웹 브라우저에서 사용되던 특이한 API 같은 비표준 동작도 구현할 수 있다.