Чи можливо реалізувати динамічні геттери / сетери в JavaScript?


132

Мені відомо, як створити геттери та сетери для властивостей, імена яких уже відомі, зробивши щось подібне:

// 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()магії ( для отримання інформації про них див . Документацію PHP ), тому я справді запитую, чи існує JavaScript, еквівалентний цим?

Потрібно сказати, що в ідеалі я хотів би рішення, яке сумісне між браузерами.


Мені вдалося це зробити, дивіться тут свою відповідь як.

Відповіді:


216

Оновлення 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у функції getter, якщо властивість є аксесуаром, а не властивістю даних. У звичайному випадку це проксі-сервер або щось, що від нього успадковується, але це може бути будь-що, оскільки можливе спрацювання пастки 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 = невизначений
Встановлення неіснуючого властивості 'foo', початкове значення: bar
[після] obj.foo = бар

Зверніть увагу, як ми отримуємо "неіснуюче" повідомлення, коли ми намагаємося отримати 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...
};

Але знову ж таки, я не можу уявити, що ви дійсно хочете це зробити через те, як це змінюється як ви використовуєте об'єкт.


1
Там є альтернатива Proxy: Object.defineProperty(). Я вкладаю деталі в новій відповіді .
Андрій

@Andrew - Боюся, ви неправильно прочитали питання, дивіться мій коментар до вашої відповіді.
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'));

Ви можете побачити його тут .


Це не працює. Ви не можете визначити getter, не вказавши назву ресурсу.
Іван Курлак

@JohnKurlak Перевірте це jsFiddle: jsfiddle.net/oga7ne4x Це працює. Вам потрібно передавати лише назви властивостей як рядки.
clami219

3
Ах, дякую за уточнення. Я думав, що ви намагаєтеся використовувати функцію мови get () / set (), а не писати власний get () / set (). Я все ще не люблю це рішення, хоча воно не вирішує оригінальну проблему.
Джон Курлак

@JohnKurlak Ну, я написав, що це оригінальний підхід. Він надає інший спосіб вирішення проблеми, навіть якщо він не вирішує проблему там, де у вас є існуючий код, який використовує більш традиційний підхід. Але добре, якщо ви починаєте з нуля. Безумовно, не гідний анонсу ...
clami219

@JohnKurlak Подивіться, чи зараз це виглядає краще! :)
clami219

0

Передмова:

У відповіді TJ Crowder згадується а Proxy, що знадобиться для загальнодоступного геттера / сеттера для властивостей, яких не існує, як вимагала ОП. Залежно від того, яка поведінка насправді потрібна динамічним геттерам / сетерам, 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
Боюся, ви неправильно прочитали питання. ОП спеціально просила спіймати всіх, як PHP __getта__set . definePropertyне справляється з цим випадком. З питання: "Тобто, створюйте getters та setters для будь-якого імені властивості, яке ще не визначено." (їх наголос). definePropertyзаздалегідь визначає властивості. Єдиний спосіб зробити те, про що вимагає ОП, - це проксі.
TJ Crowder

@TJCrowder Я бачу. Ви технічно правильні, хоча питання було не дуже зрозумілим. Я відповідно скоригував свою відповідь. Також деякі можуть насправді хочуть поєднання наших відповідей (я особисто це роблю).
Андрій

@Andrew, коли я задав це питання ще в 2011 році, я мав на увазі те, що я мав на увазі - це бібліотека, яка може повернути об'єкт, на який користувач може викликати obj.whateverPropertyтаке, що бібліотека може перехопити це загальним гетьтером і надати ім'я властивості Користувач намагався отримати доступ. Звідси випливає вимога до "загальнодоступних гетерів та сетерів".
дайског

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

це працює для мене


13
Використовувати Function()подібне - це як використовувати eval. Просто поставте безпосередньо функції як параметри defineProperty. Або якщо ви чомусь наполягаєте на динамічному створенні getта set, то використовуйте функцію високого порядку, яка створює функцію та повертає її, як-отvar get = (function(propName) { return function() { return this[propName]; };})('value');
chris-l
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.