Можно ли реализовать в JavaScript динамические геттеры / сеттеры?

133

Я знаю, как создавать геттеры и сеттеры для свойств, имена которых уже известны, делая что-то вроде этого:

// A trivial example:
function MyObject(val){
    this.count = 0;
    this.value = val;
}
MyObject.prototype = {
    get value(){
        return this.count < 2 ? "Go away" : this._value;
    },
    set value(val){
        this._value = val + (++this.count);
    }
};
var a = new MyObject('foo');

alert(a.value); // --> "Go away"
a.value = 'bar';
alert(a.value); // --> "bar2"

Теперь мой вопрос: можно ли определить такие универсальные геттеры и сеттеры? Т.е. создайте методы получения и установки для любого имени свойства, которое еще не определено.

Понятие возможно в PHP с использованием __get()и __set()методов магии (см РНР документации для получения информации о них), так что я действительно спрашивает, есть эквивалент JavaScript для них?

Излишне говорить, что в идеале мне хотелось бы кроссбраузерное решение.

daiscog
источник
Мне удалось это сделать, смотрите мой ответ здесь, как.

Ответы:

217

Обновление 2013 и 2015 гг. (См. Ниже исходный ответ от 2011 г.) :

Это изменилось в спецификации ES2015 (также известной как «ES6»): теперь у JavaScript есть прокси . Прокси-серверы позволяют создавать объекты, которые являются настоящими заместителями (фасадами) других объектов. Вот простой пример, который при извлечении переводит все значения свойств, которые являются строками, заглавными буквами:

"use strict";
if (typeof Proxy == "undefined") {
    throw new Error("This browser doesn't support Proxy");
}
let original = {
    "foo": "bar"
};
let proxy = new Proxy(original, {
    get(target, name, receiver) {
        let rv = Reflect.get(target, name, receiver);
        if (typeof rv === "string") {
            rv = rv.toUpperCase();
        }
        return rv;
      }
});
console.log(`original.foo = ${original.foo}`); // "original.foo = bar"
console.log(`proxy.foo = ${proxy.foo}`);       // "proxy.foo = BAR"

Операции, которые вы не отменяете, имеют поведение по умолчанию. Все, что мы переопределяем выше, есть get, но есть целый список операций, к которым вы можете подключиться.

В getсписке аргументов функции-обработчика:

  • targetпроксируемый объект ( originalв нашем случае).
  • name - это (конечно) имя извлекаемого свойства, которое обычно является строкой, но также может быть символом.
  • receiver- это объект, который следует использовать, как thisв функции получения, если свойство является средством доступа, а не свойством данных. В обычном случае это прокси или что-то, что от него наследуется, но это может быть что угодно, поскольку ловушка может быть вызвана Reflect.get.

Это позволяет вам создать объект с желаемой функцией универсального получения и установки:

"use strict";
if (typeof Proxy == "undefined") {
    throw new Error("This browser doesn't support Proxy");
}
let obj = new Proxy({}, {
    get(target, name, receiver) {
        if (!Reflect.has(target, name)) {
            console.log("Getting non-existent property '" + name + "'");
            return undefined;
        }
        return Reflect.get(target, name, receiver);
    },
    set(target, name, value, receiver) {
        if (!Reflect.has(target, name)) {
            console.log(`Setting non-existent property '${name}', initial value: ${value}`);
        }
        return Reflect.set(target, name, value, receiver);
    }
});

console.log(`[before] obj.foo = ${obj.foo}`);
obj.foo = "bar";
console.log(`[after] obj.foo = ${obj.foo}`);

Результат вышеизложенного:

Получение несуществующего свойства 'foo'
[до] obj.foo = undefined
Установка несуществующего свойства 'foo', начальное значение: bar
[после] obj.foo = bar

Обратите внимание на то, как мы получаем сообщение «несуществующее», когда пытаемся извлечь, fooкогда оно еще не существует, и снова, когда мы его создаем, но не после этого.


Ответ от 2011 года (см. Выше обновления за 2013 и 2015 годы) :

Нет, в JavaScript нет универсальной функции свойств. Синтаксис средства доступа, который вы используете, описан в Разделе 11.1.5 спецификации и не предлагает подстановочных знаков или чего-то подобного.

Вы, конечно, можете реализовать функцию для этого, но я предполагаю, что вы, вероятно, не хотите использовать f = obj.prop("foo");вместо f = obj.foo;и obj.prop("foo", value);вместо obj.foo = value;(что было бы необходимо для функции для обработки неизвестных свойств).

FWIW, функция получения (я не заморачивался с логикой установки) будет выглядеть примерно так:

MyObject.prototype.prop = function(propName) {
    if (propName in this) {
        // This object or its prototype already has this property,
        // return the existing value.
        return this[propName];
    }

    // ...Catch-all, deal with undefined property here...
};

Но опять же, я не могу представить, что вы действительно захотите это сделать, потому что это меняет способ использования объекта.

TJ Crowder
источник
1
Там есть альтернатива Proxy: Object.defineProperty(). Я изложил подробности в своем новом ответе .
Эндрю
@ Андрей - Боюсь, вы неправильно прочитали вопрос, посмотрите мой комментарий к вашему ответу.
TJ Crowder,
4

Следующее может быть оригинальным подходом к этой проблеме:

var obj = {
  emptyValue: null,
  get: function(prop){
    if(typeof this[prop] == "undefined")
        return this.emptyValue;
    else
        return this[prop];
  },
  set: function(prop,value){
    this[prop] = value;
  }
}

Чтобы использовать его, свойства должны быть переданы в виде строк. Вот пример того, как это работает:

//To set a property
obj.set('myProperty','myValue');

//To get a property
var myVar = obj.get('myProperty');

Изменить: улучшенный, более объектно-ориентированный подход, основанный на том, что я предложил, заключается в следующем:

function MyObject() {
    var emptyValue = null;
    var obj = {};
    this.get = function(prop){
        return (typeof obj[prop] == "undefined") ? emptyValue : obj[prop];
    };
    this.set = function(prop,value){
        obj[prop] = value;
    };
}

var newObj = new MyObject();
newObj.set('myProperty','MyValue');
alert(newObj.get('myProperty'));

Вы можете увидеть это здесь .

clami219
источник
Это не работает. Вы не можете определить получатель без указания имени свойства.
Джон Курлак
@JohnKurlak Проверьте этот jsFiddle: jsfiddle.net/oga7ne4x Он работает. Вам нужно только передать имена свойств в виде строк.
clami219
3
Ах, спасибо за разъяснения. Я думал, вы пытаетесь использовать функцию языка get () / set (), а не пишете свой собственный get () / set (). Мне все еще не нравится это решение, потому что оно не решает исходную проблему.
Джон Курлак
@JohnKurlak Ну, я написал, что это оригинальный подход. Он предоставляет другой способ решения проблемы, даже если он не решает проблему, когда у вас есть существующий код, использующий более традиционный подход. Но хорошо, если вы начинаете с нуля. Конечно, не заслуживает отрицательного голоса ...
clami219 05
@JohnKurlak Посмотрим, теперь это выглядит лучше! :)
clami219 07
-1

Предисловие:

В ответе TJ Crowder упоминается a Proxy, который понадобится для универсального получателя / установщика для свойств, которые не существуют, как просил OP. В зависимости от того, какое поведение на самом Proxyделе требуется от динамических геттеров / сеттеров, в действительности может не потребоваться; или, возможно, вы можете захотеть использовать комбинацию Proxyс тем, что я покажу вам ниже.

(PS Я недавно Proxyтщательно экспериментировал с Firefox на Linux и обнаружил, что он очень эффективен, но также несколько сбивает с толку / труден для работы и исправления. Что еще более важно, я также обнаружил, что он довольно медленный (по крайней мере, в относительно того, насколько оптимизирован JavaScript в наши дни) - я говорю в области дека-кратных медленнее.)


Для конкретной реализации динамически создаваемых геттеров и сеттеров можно использовать Object.defineProperty()или Object.defineProperties(). Это тоже довольно быстро.

Суть в том, что вы можете определить геттер и / или сеттер для объекта следующим образом:

let obj = {};
let val = 0;
Object.defineProperty(obj, 'prop', { //<- This object is called a "property descriptor".
  //Alternatively, use: `get() {}`
  get: function() {
    return val;
  },
  //Alternatively, use: `set(newValue) {}`
  set: function(newValue) {
    val = newValue;
  }
});

//Calls the getter function.
console.log(obj.prop);
let copy = obj.prop;
//Etc.

//Calls the setter function.
obj.prop = 10;
++obj.prop;
//Etc.

Здесь следует отметить несколько моментов:

  • Вы не можете использовать valueсвойство в дескрипторе свойства ( не показано выше) одновременно с getи / или set; из документов:

    Дескрипторы свойств, присутствующие в объектах, бывают двух основных видов: дескрипторы данных и дескрипторы доступа. Дескриптор данных , это свойство , которое имеет значение, которое может быть или не может быть доступны для записи. Дескриптор аксессор это свойство описывается геттер-сеттер пары функций. Дескриптор должен быть одной из этих двух разновидностей; не может быть и того, и другого.

  • Таким образом, вы заметите , что я создал valсвойство вне этого Object.defineProperty()дескриптора вызова / свойства. Это стандартное поведение.
  • По ошибке здесь не установлен , writableчтобы trueв дескрипторе собственности , если вы используете getили set.
  • Вы можете рассмотреть возможность настройки configurableи enumerable, однако, в зависимости от того, что вам нужно; из документов:

    конфигурируемый

    • true тогда и только тогда, когда тип этого дескриптора свойства может быть изменен и если свойство может быть удалено из соответствующего объекта.

    • По умолчанию - false.


    перечислимый

    • true тогда и только тогда, когда это свойство появляется во время перечисления свойств соответствующего объекта.

    • По умолчанию - false.


В этой связи они также могут быть интересны:

  • Object.getOwnPropertyNames(obj): получает все свойства объекта, даже неперечислимые (AFAIK, это единственный способ сделать это!).
  • Object.getOwnPropertyDescriptor(obj, prop): получает дескриптор свойства объекта, объекта, который был передан Object.defineProperty()выше.
  • obj.propertyIsEnumerable(prop);: для отдельного свойства в конкретном экземпляре объекта вызовите эту функцию в экземпляре объекта, чтобы определить, является ли конкретное свойство перечислимым или нет.
Андрей
источник
2
Боюсь, вы неправильно поняли вопрос. OP специально попросил поймать все, например PHP __getи__set . definePropertyне занимается этим делом. Из вопроса: «Т.е. создавать геттеры и сеттеры для любого имени свойства, которое еще не определено». (их курсив). definePropertyзаранее определяет свойства. Единственный способ сделать то, что просил OP, - это прокси.
TJ Crowder,
@TJCrowder Понятно. Технически вы правы, хотя вопрос не совсем ясен. Я соответствующим образом скорректировал свой ответ. Кроме того, некоторым может потребоваться комбинация наших ответов (лично я).
Эндрю
@Andrew, когда я задал этот вопрос еще в 2011 году, я имел в виду вариант использования: библиотека, которая может возвращать объект, который может вызывать пользователь obj.whateverProperty, чтобы библиотека могла перехватить его с помощью универсального метода получения и присвоить имени свойства пользователь пытался получить доступ. Отсюда требование к «универсальным геттерам и сеттерам».
daiscog
-6
var x={}
var propName = 'value' 
var get = Function("return this['" + propName + "']")
var set = Function("newValue", "this['" + propName + "'] = newValue")
var handler = { 'get': get, 'set': set, enumerable: true, configurable: true }
Object.defineProperty(x, propName, handler)

это работает для меня

Bruno
источник
13
Такое использование Function()похоже на использование eval. Просто укажите функции как параметры defineProperty. Или, если по какой-то причине вы настаиваете на динамическом создании getи set, затем используйте функцию высокого порядка, которая создает функцию и возвращает ее, напримерvar get = (function(propName) { return function() { return this[propName]; };})('value');
chris-l