(13장) 메타프로그래밍 [자바스크립트 완벽 가이드 7판]

iberis2·2023년 3월 6일
0

14장 메타프로그래밍

메타프로그래밍(Metaprogramming) 이란
자기 자신 혹은 다른 컴퓨터 프로그램을 데이터로 취급하며 프로그램을 작성·수정하는 것을 말한다.
넓은 의미에서, 런타임에 수행해야 할 작업의 일부를 컴파일 타임 동안 수행하는 프로그램을 말하기도 한다.
메타 프로그래밍에 이용되는 언어를 메타 언어라고 하고, 메타 프로그래밍의 대상이 되는 언어를 대상 언어라고 한다.
메타프로그래밍-위키백과

메타언어(metalanguage)
대상을 직접 서술하는 언어 그 자체를 다시 언급하는 언어나 심볼(symbol)로서 고차언어(高次言語)라고도 한다. 메타 언어의 문장이나 절의 구조는 메타문법으로 기술된다.
메타언어-위키백과

메타프로그래밍은 다른 코드를 조작하는 코드를 작성한다.

자바스크립트 같은 동적 언어에서는 프로그래밍/메타프로그래밍이 뚜렷이 구분되지는 않지만,
이 책에서 설명하는 메타프로그래밍의 주제(목차) 는 다음과 같다.

  1. 객체 프로퍼티의 속성 : 열거 가능성(enumerability), 쓰기 가능성(writability), 변경 가능성(configurability)
  2. 객체의 확장성 제어와 객체 밀봉(seal), 동결(freeze)
  3. 객체 프로토타입 검색과 설정
  4. 잘 알려진 심볼로 타입 튜닝
  5. 템플릿 태그 함수로 DSL(도메인 특정 언어) 만들기
  6. reflect 메서드로 객체 탐지
  7. 프록시를 사용한 객체 동작 제어

🐰 1.객체 프로퍼티 속성

프로퍼티 속성을 검색하고 설정하는 API는

  • 프로토타입 객체에 메서드를 추가하고, (내장 메서드처럼) 열거 불가로 만들 수 있고,
  • 변경/삭제 불가한 프로퍼티를 만들어 객체를 잠글 수 있으므로
    중요하다.

객체 프로퍼티의 3가지 속성
자바스크립트 객체의 프로퍼티에는 name, value 뿐만 아니라, 프로퍼티가 어떻게 동작하는지 나타내는 세 가지 속성이 있다.

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

(이 책의 14장에서는) 데이터 프로퍼티의 값(value)
접근자 프로퍼티의 게터세터 메서드도 아래와 같이 프로퍼티의 속성으로 간주한다.

  • 데이터 프로퍼티 네 가지 속성 :
    • value, writable, enumerable, configurable
  • 접근자 프로퍼티 네 가지 속성 :
    (value, writable 속성이 없다)
    • get, set, enumerable, configurable

🥕 프로퍼티 서술자(property descriptor) 객체

프로퍼티의 속성을 열거하는 객체이다. 아래 메서드들에서 주로 사용된다.

🥕 Object.getOwnPropertyDescriptor()

지정된 객체의 프로퍼티 서술자를 호출하는 메서드이다.
자체 프로퍼티에만 동작하므로,
상속된 프로퍼티 속성을 검색하려면 반드시 명시적으로 프로토타입 체인을 검색해야 한다.

/* 객체의 데이터 프로퍼티의 '프로퍼티 서술자 객체' 호출
: 해당 데이터 프로퍼티의 속성을 확인할 수 있다. */
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 }

🥕 Object.defineProperty()

프로퍼티 속성을 설정하거나 지정된 속성으로 새로운 프로퍼티를 생성하는 메서드이다.
파라미터로 ⑴수정할 객체, ⑵생성하거나 변경할 프로퍼티 이름, ⑶프로퍼티 서술자 객체를 전달해야 한다.

  • 세 번째 파라미터로 프로퍼티 서술자 객체를 전달할 때, 네 가지 속성을 전부 전달할 필요는 없다.
    • 생략된 속성은 false나 undefined로 간주한다.
    • 기존 프로퍼티를 수정하더라도 생략된 속성이 다시 생기지는 않는다.
    • 자체 프로퍼티만 변경(또는 생성)하고, 상속된 프로퍼티를 변경할 순 없다.
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

🥕 Object.defineProperties

둘 이상의 프로퍼티를 한 번에 생성하거나 수정하는 메서드이다.

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로 바꾸는 것은 가능하다.
    • 변경 불가이고 읽기 전용이면 값을 바꿀 수 없다.
    • 읽기 전용이더라도 변경 가능이면 프로퍼티의 값을 바꿀 수 있다.
      (쓰기 가능으로 바꾸고 → 값을 바꾼 다음 → 다시 읽기 전용으로 바꾸는 것이나 마찬가지이기 때문이다.)

🥕 Object.create()

지정된 프로토타입 객체 및 속성(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

🥕 Object.assign()

소스 객체의 모든 열거 가능한 자체 프로퍼티를 복사해 타겟 객체에 붙여넣는 메서드이다.

  • 프로퍼티의 속성은 복사할 수 없다.
    • 복사되는 원본 객체에 접근자 프로퍼티가 있다면, 게터 메서드가 반환하는 값만 복사된다. (게터 메서드는 복사되지 않는다.)
/* 데이터 프로퍼티만 있는 경우 */
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

🐰 2.객체의 확장성

확장 가능 속성은 객체에 새로운 프로퍼티를 추가할 수 있는지를 결정한다.
Object.isExtensible() 로 확장 가능 여부를 확인할 수 있다.

🥕 Object.preventExtensions()

자바스크립트의 객체는 기본적으로 확장 가능이지만,
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가 발생하고
  • 일반 모드에서는 에러 없이 조용히 실패한다.

확장 불가인 객체의 프로토타입을 변경하려는 시도도 항상 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

단, 객체 자체의 확장성만 제어하므로,
확장 불가인 객체의 프로토타입에 새로운 프로퍼티를 추가하는 경우, 새로운 프로퍼티는 그대로 상속된다.

🥕 Object.seal()

객체를 확장 불가로 만드는 동시에 자체 프로퍼티를 변경 불가로 바꾼다.
즉, 새로운 프로퍼티를 추가할 수 없고 기존 프로퍼티를 삭제할 수도 없다.

  • 쓰기 가능{writable: true}인 프로퍼티의 값은 수정할 수 있다.

밀봉 후 풀 수 있는 방법은 없다.

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가 발생한다.

일반 모드에서는

  • 데이터 프로퍼티와 접근자 프로퍼티 사이의 변경과 Object.defineProperty를 이용한 프로퍼티 추가에서 TypeError가 발생하고,
  • 새로운 프로퍼티를 할당하여 추가하거나 delete를 이용한 자체 프로퍼티 삭제는 에러 없이 조용히 실패한다.

🥕 Object.freeze()

객체를 확장 불가, 프로퍼티는 변경 불가로 바꾸는 동시에 객체 자체 데이터 프로퍼티를 읽기 전용으로 바꾼다.

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() 모두 전달받은 객체에만 효과가 있으며 그 객체의 프로토타입은 변경하지 않는다.
객체를 완전히 잠그려면 프로토타입 체인에 있는 객체 역시 밀봉 또는 동결해야한다.

🐰 3. 프로토타입 속성

객체의 prototype 속성은 객체가 프로퍼티를 상속하는 '부모' 객체이고, 객체가 생성될 때 설정된다.

  • 객체 리터럴로 생성된 객체의 프로토타입은 Object.prototype이다.
  • new로 생성한 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티 값이다.
    • 생성자 함수의 prototype 프로퍼티는 그 생성자로 생성된 객체의 prototype 속성을 가리킨다.
  • Object.create()로 생성된 객체의 프로토타입은 Object.create()의 첫 번째 파라미터이다.

🥕 getPrototypeOf() / isPrototypeOf()

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 참조)

🐰 4. 잘 알려진 심볼(Symbol)

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

🥕 Symbol.iterator와 Symbol.asyncIterator

Symbol.iterator, Symbol.asynclterator 심벌은 객체나 클래스를 이터러블이나 비동기 이터러블로 만든다.

🥕 Symbol.hasInstance

  • ES6 에서 도입

표현식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: 정수가 아닙니다.

🥕 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]"

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에 정의된 값을 확인할 수 있다.

🥕 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입니다.

🥕 Symbol.isConcatSpreadable

ES6 이후 .concat() 메서드의 내부 로직이 전달된 this 객체 또는 파라미터에 Symbol.isConcatSpreadable이 있는 프로퍼티가 있는지 여부에 따라 달라졌다.

🥕 패턴 매칭 심벌

🥕 Symbol.toPrimitive

ES6 이후 Symbol.toPrimitive가 객체를 기본 값으로 변환하는 기본 동작을 덮어 쓸 수 있게 하여, 클래스 인스턴스가 기본 값으로 변환되는 방법을 완전히 제어할 수 있다.
Symbol.toPrimitive 메서드는 반드시 객체를 표현하는 기본 값을 반환해야 한다. 이 메서드는 문자열 인자를 하나 받는데, 각 인자는 자바스크립트가 객체를 어떤 값으로 변환하려 하는지 나타낸다.

  • 인자가 string이면 자바스크립트가 문자열을 예상하거나 선호하지만 필수는 아닌 컨텍스트에 있다는 뜻이다.

    • 예를 들어 템플릿 리터럴에 객체를 사용하는 경우가 이에 해당한다.
  • 인자가 number면 자바스크립트가 숫자 값을 예상하거나 선호하지만 필수는 아닌 컨텍스트에 있다는 뜻이다.

    • 예를 들어 객체를 <, > 연산자 또는 * 같은 산 술 연산자와 함께 사용하는 경우이다.
  • 인자가 default면 자바스크립트가 숫자나 문자열이 모두 가능한 컨텍스트에 있다는 뜻이다.

    • +,==, !=가 이에 해당한다.

대부분의 클래스가 이 인자를 무시하고 항상 똑같은 기본 값을 반환한다.
클래스 인스턴스를 <, >와 함께 사용해야 한다면 Symbol.toPrimitive 메서드를 정의할 수 있다.

🥕 Symbolunscopables

with 문 때문에 발생한 호환성 문제를 해결하기 위해 도입되었다.

🐰 5. 템플릿 태그

` 백틱 ` 안에 포함된 문자열을 템플릿 리터럴이라고 부른다.

태그 함수는 일반적인 자바스크립트 함수일 뿐 이들을 정의하는 특별한 문법이 있는 것은 아니다.
함수 표현식 뒤에 템플릿 리터럴이 있으면 함수가 호출된다.

  • 첫 번째 인자는 문자열 배열이며 그 뒤 에 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 같은 이스케이프를 더 이상 사용할 수 없다.

🐰 6. 리플렉트 API

Reflect 객체는 클래스가 아닌 함수를 모은 집합이다.(e.g. Math 객체)
객체와 그 프로퍼티를 반영(reflect) 하는 API이다.
함수 네임스페이스를 하나로 모은 편리한 세트이며, 이 함수들은 자바스크립트 코어의 동작을 흉내 내고 기존 객체 메서드를 복사한다.

🐰 7. 프록시 객체

프록시(Proxy) 클래스를 사용하면 자바스크립트 객체의 기본적인 동작을 직접 구현하며 바꿀 수 있고, 일반적인 객체에서는 불가능한 방법으로 동작하는 객체를 만들 수 있다.

  • 자바스크립트에서 가장 강력한 메타프로그래밍 기능이다.
  • ES6에서 도입되었다.

✨ 14장 메타프로그래밍 요약

  • 자바스크립트 객체에는 확장 가능 속성이 있고, 객체 프로퍼티에는 값과 게터, 세터 외에도 쓰기 가능, 열거 가능, 변경 가능 속성이 있다. 이 속성을 이용해 객체를 다양한 방법으로 잠글 수 있다. 객체의 밀봉동결도 잠그는 방법에 속한다.

  • 자바스크립트에는 객체의 프로토타입 체인을 탐색하는 함수가 존재하며 심지어 객체의 프로토타입을 바꿀 수도 있다.
    (하지만 이렇게 하면 코드가 느려질 수 있다.)

  • Symbol 객체의 프로퍼티에는 잘 알려진 심벌이 있다. 객체나 클래스를 직접 정의할 때 이 심벌을 프로퍼티나 메서드 이름으로 쓸 수 있다.
    • 심벌을 이름으로 사용하면 객체가 자바스크립트의 핵심 특징, 코어 라이브러리와 상호작용하는 방식을 바꿀 수 있다.
    • 예를 들어 잘 알려진 심벌을 써서 클래스를 이터러블로 만들 수 있고, 인스턴스를 Object.prototype.toString()에 전달했을 때 반환되는 문자열도 바꿀 수 있다.
    • ES6 전에는 실행 환경에 내장된 네이티브 클래스에서만 이런 기능을 사용할 수 있었다.

  • 태그된 탬플릿 리터럴은 함수 호출 문법이며, 새로운 태그 함수를 정의하는 것은 자바스크립트에 새로운 리터럴 문법을 추가하는 것과 같다. 템플릿 문자열 인자를 분석하는 태그 함수를 만들어 자바스크립트 코드에 DSL을 임베드할 수 있다. 태그 함수는 역슬래시에 특별한 의미가 없는 있는 그대로의, 이스케이프되지 않은 문자열 리터럴에 접근할 수 있다.

  • 프록시 클래스와 관련 리플렉트 API는 자바스크립트 객체의 기본적인 동작에 대한 저수준 제어를 허용한다. 프록시 객체를 취소할 수 있는 래퍼로 사용해 코드를 더 잘 캡슐화할 수 있고, 초기 웹 브라우저에서 사용되던 특이한 API 같은 비표준 동작도 구현할 수 있다.
profile
React, Next.js, TypeScript 로 개발 중인 프론트엔드 개발자

0개의 댓글