Метапрограммирование в ES6: часть 2 — отражение

ECMAScript 6

в моемПредыдущий пост в блоге, мы изучили символы и то, как они добавляют полезные функции метапрограммирования в JavaScript. На этот раз мы (наконец-то!) начнем говорить об отражении. если ты не читалЧасть 1: Символы, то я предлагаю вам сначала прочитать его. В прошлой статье я потрудился подчеркнуть один момент:

  • СимволыОтражение в реализации- Вы применяете символы к своим существующим классам и объектам, чтобы изменить их поведение.
  • ОтразитьРефлексия в провинции (рефлексия через самоанализ)- Часто используется для изучения очень низкоуровневой информации о коде.
  • ПроксиОтражение через ходатайство- Обертывание объектов и перехват поведения объектов через ловушки.

Reflectновый глобальный объект (что-то вродеJSONилиMath), который предоставляет ряд полезных методов самоанализа (самоанализ — довольно красивое слово для «посмотри на эту вещь»). Инструменты самоанализа уже существуют в JavaScript, напримерObject.keys,Object.getOwnPropertyNamesи т.п. Итак, почему мы все еще новички в API, а не расширяемся непосредственно на Object?

"встроенный метод"

Все спецификации JavaScript и результирующий движок основаны на наборе «встроенных методов». Эти встроенные методы эффективно сообщают движку JavaScript о выполнении основных операций над объектами, разбросанными по всему коду. Если вы прочитаете спецификацию, вы обнаружите, что эти методы разбросаны повсюду, например.[[Get]],[[Set]],[[HasOwnProperty]]и т. д. (если у вас нет терпения читать все спецификации, список этих встроенных методов находится по адресуES5 Раздел 8.12так же какES6 Раздел 9.1доступный).

Некоторые из этих «встроенных методов» скрыты от кода JavaScript, другие используются в других методах, и даже если эти методы доступны, они все равно скрыты в труднодоступных местах. Например,Object.prototype.hasOwnPropertyда[[HasOwnProperty]]Реализация , но не все объекты наследуются от Object, поэтому иногда вам приходится писать какой-то причудливый код для использованияhasOwnProperty, как показано в следующем примере:

var myObject = Object.create(null); // 这段代码比你想象得更加常见(尤其是在使用了新的 ES6 的类的时候)
assert(myObject.hasOwnProperty === undefined);
// 如果你想在 `myObject` 上使用 hasOwnProperty:
Object.prototype.hasOwnProperty.call(myObject, 'foo');

См. другой пример,[[OwnPropertyKeys]]Этот встроенный метод получает все строковые и символьные ключи объекта и возвращает их в виде массива. Единственный способ получить эти ключи сразу без использования Reflect — подключитьсяObject.getOwnPropertyNamesа такжеObject.getOwnPropertySymbolsрезультат:

var s = Symbol('foo');
var k = 'bar';
var o = { [s]: 1, [k]: 1 };
// 模拟 [[OwnPropertyKeys]]
var keys = Object.getOwnPropertyNames(o).concat(Object.getOwnPropertySymbols(o));
assert.deepEqual(keys, [k, s]);

метод отражения

Reflection — очень полезная коллекция всех внутренних проприетарных движков JavaScript.«Внутренний метод», теперь отображается как единый удобный объект — Reflect. Вы можете спросить: «Звучит здорово, но почему бы просто не привязать встроенный метод к Object?Object.keys,Object.getOwnPropertyNamesИтак». Сейчас я расскажу, почему:

  1. В Reflection есть методы не только для объектов, но и для функций, таких какReflect.apply, ведь звонилObject.apply(myFunction)Это выглядит так странно.
  2. Использование одного объекта для хранения встроенных методов сохраняет чистоту остального JavaScript, что лучше, чем присоединение рефлексивного метода к конструктору или прототипу с помощью оператора точки, и лучше, чем непосредственное использование глобальных переменных.
  3. typeof,instanceofтак же какdeleteУже существует как оператор отражения — добавление нового ключевого слова для той же функциональности было бы бременем для разработчика, в то же время это было бы кошмаром для обратной совместимости и увеличило бы количество зарезервированных слов в JavaScript.

Reflect.apply ( target, thisArgument [, argumentList] )

Reflect.applyа такжеFunction#applyАналогично — принимает функцию, контекст для вызова функции и массив аргументов. с этого момента тыМогудуматьFunction#call/Function#applyустарел. Это не кардинальное изменение, но это имеет большой смысл. Показано нижеReflect.applyПрименение:

var ages = [11, 33, 12, 54, 18, 96];

// Function.prototype 风格:
var youngest = Math.min.apply(Math, ages);
var oldest = Math.max.apply(Math, ages);
var type = Object.prototype.toString.call(youngest);

// Reflect 风格:
var youngest = Reflect.apply(Math.min, Math, ages);
var oldest = Reflect.apply(Math.max, Math, ages);
var type = Reflect.apply(Object.prototype.toString, youngest);

Настоящим преимуществом перехода от Function.prototype.apply к Reflect.apply является защита: любой код может попытаться изменить значение функции.callилиapplyметод, из-за которого вы можете застрять из-за сбоя кода или какой-то плохой ситуации. В реальном мире это не имело бы большого значения, но код, подобный следующему, действительно мог бы существовать:

function totalNumbers() {
  return Array.prototype.reduce.call(arguments, function (total, next) {
    return total + next;
  }, 0);
}
totalNumbers.apply = function () {
  throw new Error('Aha got you!');
}

totalNumbers.apply(null, [1, 2, 3, 4]); // 抛出 Error('Aha got you!');

// ES5 中保证防御性的代码看起来很糟糕:
Function.prototype.apply.call(totalNumbers, null, [1, 2, 3, 4]) === 10;

// 你也可以这样做,但看起来还是不够整洁:
Function.apply.call(totalNumbers, null, [1, 2, 3, 4]) === 10;

// Reflect.apply 会是救世主!
Reflect.apply(totalNumbers, null, [1, 2, 3, 4]) === 10;

Reflect.construct ( target, argumentsList [, constructorToCreateThis] )

похожий наReflect.apply——Reflect.constructПозволяет передать последовательность аргументов для вызова конструктора. Он может обслуживать класс и устанавливать правильный объект для конструктора, чтобы иметь правильныйthisссылка для соответствия соответствующему прототипу. В дни ES5 вы бы использовалиObject.create(Constructor.prototype)режиме, затем передайте объект вConstructor.callилиConstructor.apply.Reflect.constructРазница в том, что вам нужно передать только конструктор, а не объект --Reflect.constructПозаботьтесь обо всем (если вы опустите третий параметр, созданный прототип объекта будет привязан по умолчанию кtargetпараметр). В предыдущем стиле завершение построения объекта было сложной задачей, но в новом стиле это можно сделать так же просто, как всего одна строка кода:

class Greeting {

    constructor(name) {
        this.name = name;
    }

    greet() {
      return Hello ${this.name};
    }

}

// ES5 风格的工厂函数:
function greetingFactory(name) {
    var instance = Object.create(Greeting.prototype);
    Greeting.call(instance, name);
    return instance;
}

// ES6 风格的工厂函数:
function greetingFactory(name) {
    return Reflect.construct(Greeting, [name], Greeting);
}

// 如果省略第三个参数,那么默认绑定对象原型到第一个参数
function greetingFactory(name) {
  return Reflect.construct(Greeting, [name]);
}

// ES6 下顺滑无比的线性工厂函数:
const greetingFactory = (name) => Reflect.construct(Greeting, [name]);

Reflect.defineProperty ( target, propertyKey, attributes )

Reflect.definedPropertyВ основном из-заObject.defineProperty- Это позволяет вам определить метаинформацию об атрибуте. В сравнении сObject.defineProperty,Reflect.definePropertyЭто более уместно, потому что Obejct.* подразумевает, что он действует на литерал объекта (в конце концов, Object является конструктором литерала объекта), тогда как Reflect.defineProperty подразумевает только то, что вы выполняете отражение, что более семантично.

На что следует обратить внимание, так это на то, чтоReflect.defineProperty- так какObject.definePropertyто же - для инвалидаtarget, такие как числовые или строковые примитивные значения (Reflect.defineProperty(1, 'foo')), выкинетTypeError. Вместо того, чтобы молча терпеть неудачу, лучше выдать ошибку, чтобы привлечь ваше внимание, когда параметр имеет неправильный тип.

Опять же, вы можете думать, чтоObject.definePropertyОтныне устарело, и используйтеReflect.definePropertyзаменять:

function MyDate() {
  /*…*/
}

// 老的风格下,我们使用 Object.defineProperty 来定义一个函数的属性,显得很奇怪
// (为什么我们不用 Function.defineProperty ?)
Object.defineProperty(MyDate, 'now', {
  value: () => currentms
});

// 新的风格下,语义就通畅得多,因为 Reflect 只是在做反射。
Reflect.defineProperty(MyDate, 'now', {
  value: () => currentms
});

Reflect.getOwnPropertyDescriptor ( target, propertyKey )

Как и выше, мы предпочитаем использоватьReflect.getOwnPropertyDescriptorзаменятьObject.getOwnPropertyDescriptorдля получения метаинформации дескриптора атрибута. а такжеObject.getOwnPropertyDescriptor(1, 'foo')молча потерпит неудачу, возвращаясьundefinedразные,Reflect.getOwnPropertyDescriptor(1, 'foo')броситTypeErrorошибка - сReflect.definePropertyТипа, ошибка дляtargetброшена пустота. Вы также знаете, что мы можем использоватьReflect.getOwnPropertyDescriptorзаменятьObject.getOwnPropertyDescriptorсейчас:

var myObject = {};
Object.defineProperty(myObject, 'hidden', {
  value: true,
  enumerable: false,
});
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');
assert.deepEqual(theDescriptor, { value: true, enumerable: true });

// 老的风格
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');
assert.deepEqual(theDescriptor, { value: true, enumerable: true });

assert(Object.getOwnPropertyDescriptor(1, 'foo') === undefined)
Reflect.getOwnPropertyDescriptor(1, 'foo'); // throws TypeError

Reflect.deleteProperty ( target, propertyKey )

очень очень интересно,Reflect.deletePropertyВозможность удалить атрибут целевого объекта. До ES6 вы обычно проходилиdelete obj.foo, теперь вы можете использоватьReflect.deleteProperty(obj, 'foo')для удаления свойств объекта.Reflect.deletePropertyСлегка многословный, семантически такой же, какdeleteКлючевое слово несколько разное, но для удаления объекта есть такая же роль. Оба называются встроеннымtarget[[Delete]](propertyKey)Метод - ноdeleteОперация также может «работать» со ссылками, не относящимися к объектам (такими как переменные), поэтому она выполняет больше проверок переданных ей операндов и, возможно, выдает ошибку:

var myObj = { foo: 'bar' };
delete myObj.foo;
assert(myObj.hasOwnProperty('foo') === false);

myObj = { foo: 'bar' };
Reflect.deleteProperty(myObj, 'foo');
assert(myObj.hasOwnProperty('foo') === false);

Опять же, если вы хотите, вы можете использовать этот «новый способ» для удаления свойств. Этот метод, очевидно, предназначен для более явного использования, т. е. для удаления атрибута.

Reflect.getPrototypeOf ( target )

Замена/прекращение поддержки методов Object продолжается — на этот раз пришло времяObject.getPrototypeOf. Как и его родственные методы, если вы передаете литерал, такой как Number и String ,nullилиundefinedэто недействительноtarget,Reflect.getPropertyOfброситTypeErrorошибка, покаObject.getPropertyOfпринудительное преобразованиеtargetдля объекта - так'a'сталObject('a'). Помимо синтаксиса, они почти идентичны:

var myObj = new FancyThing();
assert(Reflect.getPrototypeOf(myObj) === FancyThing.prototype);

// 老的风格
assert(Object.getPrototypeOf(myObj) === FancyThing.prototype);

Object.getPrototypeOf(1); // undefined
Reflect.getPrototypeOf(1); // TypeError

Reflect.setPrototypeOf ( target, proto )

Конечно,getProtopertyOfне может уйтиsetPropertyOf. Сейчас,Object.setPrototypeOfБудет выброшена ошибка для входящих аргументов, не являющихся объектами, но он попытается преобразовать входящий аргумент в Object, и если встроенный[[SetPrototype]]Операция не удалась и выкинетTypeError, и в случае успеха вернетtargetпараметр.Reflect.setPrototypeOfПростые основы - если он получает аргумент, не являющийся объектом, он выдаетTypeErrorошибка, но кроме этого он возвращает[[SetPrototypeOf]]Результат — это логическое значение, указывающее, была ли операция ошибкой. Это полезно, потому что вы можете напрямую сказать, если операция неверна, без необходимости использоватьtry/catch, который будет обнаруживать другие ошибки, вызванные ошибками передачи параметров.TypeErrors.

var myObj = new FancyThing();
assert(Reflect.setPrototypeOf(myObj, OtherThing.prototype) === true);
assert(Reflect.getPrototypeOf(myObj) === OtherThing.prototype);

// 老的风格
assert(Object.setPrototypeOf(myObj, OtherThing.prototype) === myObj);
assert(Object.getPrototypeOf(myObj) === FancyThing.prototype);

Object.setPrototypeOf(1); // TypeError
Reflect.setPrototypeOf(1); // TypeError

var myFrozenObj = new FancyThing();
Object.freeze(myFrozenObj);

Object.setPrototypeOf(myFrozenObj); // TypeError
assert(Reflect.setPrototypeOf(myFrozenObj) === false);

Reflect.isExtensible (target)

Опять же, это заменаObject.isExtensibleДа, но это сложнее, чем последнее. До ES6 (скажем, ES5), если вы передали параметр, не являющийся объектом (typeof target !== object),Object.isExtensibleброситTypeError. ES6 изменился семантически (о боже! Он фактически изменил существующий API!), так что, когда передается параметр, не являющийся объектом,Object.isExtensibleвернутьfalse- Потому что не-объекты действительно не расширяемы. Итак, в ES6 этот оператор, который ранее выдавал ошибку:Object.isExtensible(1) === falseТеперь ведет себя так, как и следовало ожидать, с более точной семантикой.

Ключевой момент приведенного выше краткого исторического обзора заключается в том, чтоReflect.isExtensibleИспользует старое поведение выдачи ошибки при передаче необъектного параметра. Я не совсем уверен, почему он это делает, но это так. Так техническиReflect.isExtensibleизмененныйObject.isExtensibleсемантика, ноObject.isExtensibleСемантика также изменилась. Следующий код иллюстрирует это:

var myObject = {};
var myNonExtensibleObject = Object.preventExtensions({});

assert(Reflect.isExtensible(myObject) === true);
assert(Reflect.isExtensible(myNonExtensibleObject) === false);
Reflect.isExtensible(1); // 抛出 TypeError
Reflect.isExtensible(false);  // 抛出 TypeError

// 使用 Object.isExtensible
assert(Object.isExtensible(myObject) === true);
assert(Object.isExtensible(myNonExtensibleObject) === false);

// ES5 Object.isExtensible 语义
Object.isExtensible(1); // 在老版本的浏览器下,会抛出 TypeError
Object.isExtensible(false);  // 在老版本的浏览器下,会抛出 TypeError

// ES6 Object.isExtensible 语义
assert(Object.isExtensible(1) === false); // 只工作在新的浏览器
assert(Object.isExtensible(false) === false); // 只工作在新的浏览器

Reflect.preventExtensions ( target )

Это последний метод, который объекты отражения заимствуют у Object. это иReflect.isExtensibleЕсть похожая история; ES5Object.preventExtensionsРаньше он выдавал ошибку для аргументов, не являющихся объектами, но теперь, в ES6, он возвращает переданное значение, аReflect.preventExtensionsСледует старому поведению ES5 по выдаче ошибок для аргументов, не являющихся объектами. Кроме того, в случае успешной операцииObject.preventExtensionsможет выдать ошибку, ноReflect.preventExtensionПростое возвращение true или false позволяет изящно обрабатывать сценарии отказа:

var myObject = {};
var myObjectWhichCantPreventExtensions = magicalVoodooProxyCode({});

assert(Reflect.preventExtensions(myObject) === true);
assert(Reflect.preventExtensions(myObjectWhichCantPreventExtensions) === false);
Reflect.preventExtensions(1); // 抛出 TypeError
Reflect.preventExtensions(false);  // 抛出 TypeError

// 使用 Object.preventExtensions
assert(Object.preventExtensions(myObject) === true);
Object.preventExtensions(myObjectWhichCantPreventExtensions); // throws TypeError

// ES5 Object.preventExtensions 语义
Object.preventExtensions(1); // 抛出 TypeError
Object.preventExtensions(false);  // 抛出 TypeError

// ES6 Object.preventExtensions 语义
assert(Object.preventExtensions(1) === 1);
assert(Object.preventExtensions(false) === false);

Reflect.enumerate ( target )

Обновление: в ES2016 (он же ES7) это было удалено.myObject[Symbol.iterator]()это единственный способ перебрать ключ или значение объекта.

Наконец, будет представлен совершенно новый метод Reflect!Reflect.enumerateподержанные и новыеSymbol.iteratorфункции (обсуждаемые в предыдущей главе) имеют одинаковый синтаксис, обе используют скрытый, известный только движку JavaScript[[Enumerate]]метод. другими словами,Reflect.enumerateЕдинственная альтернатива простоmyObject[Symbol.iterator()], только последний может быть переопределен, а первый - нет. Пример использования следующий:

var myArray = [1, 2, 3];
myArray[Symbol.enumerate] = function () {
  throw new Error('Nope!');
}
for (let item of myArray) { // error thrown: Nope!
}
for (let item of Reflect.enumerate(myArray)) {
  // 1 then 2 then 3
}

Reflect.get ( target, propertyKey [ , receiver ])

Reflect.getТоже новый метод. Это очень простой метод, который эффективно вызываетtarget[propertyKey]. еслиtargetне является объектом, вызов функции вызовет ошибку - это полезно, потому что в настоящее время, если вы пишете1['foo']Такой код, он просто молча возвращаетсяundefined,а такжеReflect.get(1, 'foo')броситTypeErrorошибка!Reflect.getинтересная часть в том, что егоreceiverпараметр, еслиtarget[propertyKey]является функцией-получателем, которая используется как this функции, как показано в следующем примере:

var myObject = {
  foo: 1,
  bar: 2,
  get baz() {
    return this.foo + this.bar;
  },
}

assert(Reflect.get(myObject, 'foo') === 1);
assert(Reflect.get(myObject, 'bar') === 2);
assert(Reflect.get(myObject, 'baz') === 3);
assert(Reflect.get(myObject, 'baz', myObject) === 3);

var myReceiverObject = {
  foo: 4,
  bar: 4,
};
assert(Reflect.get(myObject, 'baz', myReceiverObject) === 8);

// 非对象将抛出错误
Reflect.get(1, 'foo'); // 抛出 TypeError
Reflect.get(false, 'foo'); // 抛出 TypeError

// 老的风格下,静默返回 `undefined`:
assert(1['foo'] === undefined);
assert(false['foo'] === undefined);

Reflect.set ( target, propertyKey, V [ , receiver ] )

Вы можете примерно догадаться, что делает этот метод. этоReflect.getРодственный метод , который принимает еще один параметр — значение, которое необходимо установить. Такие какReflect.getТакой же,Reflect.setвыдаст ошибку при передаче параметра, не являющегося объектом, а также будет иметьreceiverспецификация параметраtarget[propertyKey]используется для функций установкиthis. Должен быть предыдущий пример кода:

var myObject = {
  foo: 1,
  set bar(value) {
    return this.foo = value;
  },
}

assert(myObject.foo === 1);
assert(Reflect.set(myObject, 'foo', 2));
assert(myObject.foo === 2);
assert(Reflect.set(myObject, 'bar', 3));
assert(myObject.foo === 3);
assert(Reflect.set(myObject, 'bar', myObject) === 4);
assert(myObject.foo === 4);

var myReceiverObject = {
  foo: 0,
};
assert(Reflect.set(myObject, 'bar', 1, myReceiverObject));
assert(myObject.foo === 4);
assert(myReceiverObject.foo === 1);

// 非对象将抛出错误
Reflect.set(1, 'foo', {}); // 抛出 TypeError
Reflect.set(false, 'foo', {}); // 抛出 TypeError

// 老的风格下,静默返回 `undefined`:
1['foo'] = {};
false['foo'] = {};
assert(1['foo'] === undefined);
assert(false['foo'] === undefined);

Reflect.has ( target, propertyKey )

Reflect.hasэто очень интересный метод, потому что он по существу такой же, какinОператоры имеют ту же функциональность (вне циклов). Оба используют встроенный[[HasProperty]], и будет вtargetВыдает ошибку, когда не является объектом. Если вы не предпочитаете стиль вызова функции, по сравнению сin, мало использовалсяReflect.has, но он имеет важные применения в других аспектах языка, которые будут разъяснены в следующей главе. В любом случае, давайте сначала посмотрим, как его использовать:

myObject = {
  foo: 1,
};
Object.setPrototypeOf(myObject, {
  get bar() {
    return 2;
  },
  baz: 3,
});

// 不使用 Reflect.has:
assert(('foo' in myObject) === true);
assert(('bar' in myObject) === true);
assert(('baz' in myObject) === true);
assert(('bing' in myObject) === false);

// 使用 Reflect.has:
assert(Reflect.has(myObject, 'foo') === true);
assert(Reflect.has(myObject, 'bar') === true);
assert(Reflect.has(myObject, 'baz') === true);
assert(Reflect.has(myObject, 'bing') === false);

Reflect.ownKeys ( target )

Этот метод уже упоминался в этой статье, вы можете увидетьReflect.ownKeysДостигнуто[[OwnPropertyKeys]], вы вспоминаете вышесказанное, вы знаете, что это связаноObject.getOwnPropertyNamesа такжеObject.getOwnPropertySymbolsрезультат. Это позволяетReflect.ownKeysиграет незаменимую роль. См. использование ниже:

var myObject = {
  foo: 1,
  bar: 2,
  [Symbol.for('baz')]: 3,
  [Symbol.for('bing')]: 4,
};

assert.deepEqual(Object.getOwnPropertyNames(myObject), ['foo', 'bar']);
assert.deepEqual(Object.getOwnPropertySymbols(myObject), [Symbol.for('baz'), Symbol.for('bing')]);

// 不使用 Reflect.ownKeys:
var keys = Object.getOwnPropertyNames(myObject).concat(Object.getOwnPropertySymbols(myObject));
assert.deepEqual(keys, ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);

// 使用 Reflect.ownKeys:
assert.deepEqual(Reflect.ownKeys(myObject), ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);

В заключение

У нас есть тщательное обсуждение каждого метода Reflect. Мы видели новые версии некоторых существующих методов, некоторые измененные, некоторые совершенно новые — выводя отражение JavaScript на новый уровень. Вы можете выбросить все это, если хотитеObject.*/Function.*метод, сReflectАльтернатива, если вы не хотите, не волнуйтесь, не используйте его, ничего не изменится.

Я не хочу, чтобы вы читали это с пустыми руками и ничего не получили. если вы хотите использоватьReflect, мы оказали вам поддержку — в рамках работы над этой статьей я представилпулл-реквест на eslint,существуетv1.0.0Версия,ESlint имеет prefer-reflect правило, который позволяет получать подсказки от ESLint при использовании старых версий методов Reflect. Вы также можете посмотреть на мойeslint-config-strictнастроить, включитьprefer-reflectправил (также добавлено много дополнительных правил). Конечно, если вы решите использовать Reflect, вам может понадобиться полифилл; к счастью, есть несколько хороших доступных полифилов, таких какcore-jsа такжеharmony-reflect.

Что вы думаете о новом Reflect API? Планируете использовать его в своем проекте? Вы можете написать мне сообщение в моем Твиттере, я@keithamus.

И не забывайте, скоро выйдет третья часть этой серии, Прокси, и я не буду откладывать ее еще на два месяца. (Уже опубликовано:nuggets.capable/post/684490…

Наконец, спасибо@mttshwа также@WebReflectionВнимательное изучение моей работы сделало статью более качественной, чем ожидалось.


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,React,внешний интерфейс,задняя часть,товар,дизайнЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.