Apakah mungkin menerapkan getter / setter dinamis dalam JavaScript?

132

Saya mengetahui cara membuat getter dan setter untuk properti yang namanya sudah diketahui, dengan melakukan sesuatu seperti ini:

// 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"

Sekarang, pertanyaan saya adalah, apakah mungkin untuk menentukan jenis catch-all getter dan setter seperti ini? Yaitu, buat getter dan setter untuk nama properti apa pun yang belum ditentukan.

Konsep ini mungkin dalam PHP menggunakan metode __get()dan __set()sihir (lihat dokumentasi PHP untuk informasi tentang ini), jadi saya benar-benar bertanya apakah ada JavaScript yang setara dengan ini?

Tak perlu dikatakan, saya idealnya menginginkan solusi yang kompatibel lintas-browser.

daiscog
sumber
Saya berhasil melakukannya, lihat jawaban saya di sini untuk caranya.

Jawaban:

216

Pembaruan 2013 dan 2015 (lihat di bawah untuk jawaban asli dari 2011) :

Ini berubah pada spesifikasi ES2015 (alias "ES6"): JavaScript sekarang memiliki proxy . Proxy memungkinkan Anda membuat objek yang benar-benar proxy untuk (fasad) objek lain. Berikut adalah contoh sederhana yang mengubah nilai properti apa pun yang merupakan string ke semua batas saat pengambilan:

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

Operasi yang tidak Anda timpa memiliki perilaku default. Di atas, semua yang kami timpa adalah get, tetapi ada seluruh daftar operasi yang dapat Anda lakukan.

Dalam getdaftar argumen fungsi pawang:

  • targetadalah objek yang sedang diproksikan ( original, dalam kasus kami).
  • name adalah (tentu saja) nama properti yang diambil, yang biasanya berupa string tetapi juga bisa menjadi Simbol.
  • receiveradalah objek yang harus digunakan seperti thispada fungsi pengambil jika properti adalah accessor daripada properti data. Dalam kasus normal ini adalah proksi atau sesuatu yang mewarisi darinya, tetapi bisa berupa apa saja karena perangkap mungkin dipicu oleh Reflect.get.

Ini memungkinkan Anda membuat objek dengan fitur catch-all getter dan setter yang Anda inginkan:

"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}`);

Output di atas adalah:

Mendapatkan properti 'foo' yang tidak ada
[sebelum] obj.foo = tidak ditentukan
Mengatur properti 'foo' yang tidak ada, nilai awal: bilah
[setelah] obj.foo = bar

Perhatikan bagaimana kita mendapatkan pesan "tidak ada" ketika kita mencoba mengambilnya fooketika belum ada, dan lagi ketika kita membuatnya, tetapi tidak setelah itu.


Jawaban dari 2011 (lihat pembaruan di atas untuk 2013 dan 2015) :

Tidak, JavaScript tidak memiliki fitur properti tangkap semua. Sintaks accessor yang Anda gunakan tercakup dalam Bagian 11.1.5 dari spesifikasi, dan tidak menawarkan wildcard atau sesuatu seperti itu.

Anda tentu saja dapat mengimplementasikan fungsi untuk melakukannya, tetapi saya rasa Anda mungkin tidak ingin menggunakan f = obj.prop("foo");daripada f = obj.foo;dan obj.prop("foo", value);bukannya obj.foo = value;(yang akan diperlukan untuk fungsi untuk menangani properti yang tidak diketahui).

FWIW, fungsi pengambil (saya tidak repot-repot dengan logika setter) akan terlihat seperti ini:

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...
};

Tetapi sekali lagi, saya tidak bisa membayangkan Anda benar-benar ingin melakukan itu, karena cara itu mengubah cara Anda menggunakan objek.

TJ Crowder
sumber
1
Ada alternatif untuk Proxy: Object.defineProperty(). Saya memasukkan detail dalam jawaban baru saya .
Andrew
@Andrew - Saya khawatir Anda salah membaca pertanyaan, lihat komentar saya pada jawaban Anda.
TJ Crowder
4

Berikut ini bisa menjadi pendekatan asli untuk masalah ini:

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;
  }
}

Untuk menggunakannya properti harus dilewatkan sebagai string. Jadi inilah contoh cara kerjanya:

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

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

Sunting: Pendekatan yang ditingkatkan, lebih berorientasi objek berdasarkan apa yang saya usulkan adalah sebagai berikut:

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

Anda dapat melihatnya bekerja di sini .

clami219
sumber
Ini tidak berhasil. Anda tidak dapat menentukan pengambil tanpa menentukan nama properti.
John Kurlak
@JohnKurlak Periksa jsFiddle ini: jsfiddle.net/oga7ne4x Berhasil . Anda hanya perlu memberikan nama properti sebagai string.
clami219
3
Ah, terima kasih sudah menjelaskan. Saya pikir Anda mencoba menggunakan fitur bahasa get () / set (), bukan menulis get () / set () Anda sendiri. Saya masih tidak menyukai solusi ini karena itu tidak benar-benar menyelesaikan masalah aslinya.
John Kurlak
@ JohnKurlak Ya, saya menulis ini pendekatan yang orisinal. Ini memberikan cara berbeda untuk menyelesaikan masalah, meskipun tidak menyelesaikan masalah di mana Anda memiliki kode yang ada yang menggunakan pendekatan yang lebih tradisional. Tapi ada baiknya jika Anda memulai dari awal. Tentunya tidak layak downvote ...
clami219
@JohnKurlak Lihat apakah sekarang terlihat lebih baik! :)
clami219
0

Kata pengantar:

Jawaban TJ Crowder menyebutkan a Proxy, yang akan dibutuhkan untuk semua pengambil / penyetel untuk properti yang tidak ada, seperti yang diminta OP. Bergantung pada perilaku apa yang sebenarnya diinginkan dengan getter / setters yang dinamis, Proxymungkin saja sebenarnya tidak diperlukan; atau, berpotensi, Anda mungkin ingin menggunakan kombinasi a Proxydengan apa yang akan saya tunjukkan di bawah ini.

(PS Saya telah bereksperimen dengan Proxytuntas di Firefox di Linux baru-baru ini dan telah menemukan itu sangat mampu, tetapi juga agak membingungkan / sulit untuk dikerjakan dan diperbaiki. Lebih penting lagi, saya juga menemukan itu sangat lambat (setidaknya di kaitannya dengan seberapa optimalnya JavaScript saat ini) - Saya berbicara lebih lambat di ranah deca-kelipatan.)


Untuk menerapkan pengambil dan setter yang dibuat secara dinamis secara khusus, Anda dapat menggunakan Object.defineProperty()atau Object.defineProperties(). Ini juga cukup cepat.

Intinya adalah Anda dapat mendefinisikan pengambil dan / atau penyetel pada objek seperti:

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.

Beberapa hal yang perlu diperhatikan di sini:

  • Anda tidak dapat menggunakan valueproperti di deskriptor properti ( tidak ditampilkan di atas) secara bersamaan dengan getdan / atau set; dari dokumen:

    Deskriptor properti hadir dalam objek datang dalam dua rasa utama: deskriptor data dan deskriptor accessor. Sebuah descriptor Data adalah properti yang memiliki nilai, yang mungkin atau mungkin tidak dapat ditulis. Sebuah descriptor accessor adalah properti yang dijelaskan oleh sepasang pengambil-setter fungsi. Seorang deskriptor harus merupakan salah satu dari dua rasa ini; tidak mungkin keduanya.

  • Dengan demikian, Anda akan perhatikan bahwa saya membuat sebuah valproperti di luar dari Object.defineProperty()descriptor call / properti. Ini adalah perilaku standar.
  • Sesuai kesalahan di sini , jangan setel writableke truedeskriptor properti jika Anda menggunakan getatau set.
  • Anda mungkin ingin mempertimbangkan pengaturan configurabledan enumerable, bagaimanapun, tergantung pada apa yang Anda cari; dari dokumen:

    dapat dikonfigurasi

    • true jika dan hanya jika jenis deskriptor properti ini dapat diubah dan jika properti dapat dihapus dari objek yang sesuai.

    • Default ke false.


    tak terhitung

    • true jika dan hanya jika properti ini muncul selama enumerasi properti pada objek yang sesuai.

    • Default ke false.


Pada catatan ini, ini mungkin juga menarik:

Andrew
sumber
2
Saya khawatir Anda salah membaca pertanyaan. OP secara khusus meminta semua tangkapan seperti PHP __getdan__set . definePropertytidak menangani kasus itu. Dari pertanyaan: "Yaitu, buat getter dan setter untuk nama properti apa pun yang belum ditentukan." (Penekanan mereka). definePropertymendefinisikan properti terlebih dahulu. Satu-satunya cara untuk melakukan apa yang diminta OP adalah proxy.
TJ Crowder
@TJCrowder saya mengerti. Secara teknis Anda benar, meskipun pertanyaannya tidak terlalu jelas. Saya telah menyesuaikan jawaban saya sesuai dengan itu. Juga, beberapa orang mungkin sebenarnya menginginkan kombinasi jawaban kami (saya pribadi suka).
Andrew
@Andrew ketika saya menanyakan pertanyaan ini pada tahun 2011, use-case yang saya pikirkan adalah perpustakaan yang dapat mengembalikan objek tempat pengguna dapat memanggil obj.whateverPropertysehingga perpustakaan dapat mencegatnya dengan pembuat getar generik dan diberi nama properti pengguna mencoba mengakses. Oleh karena itu persyaratan untuk 'menangkap semua getter dan setter'.
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)

ini bekerja untuk saya

Bruno
sumber
13
Menggunakan Function()seperti itu seperti menggunakan eval. Langsung saja fungsinya sebagai parameter defineProperty. Atau, jika karena alasan tertentu Anda bersikeras untuk membuat getdan set, kemudian menggunakan fungsi tingkat tinggi yang membuat fungsi dan mengembalikannya, sepertivar get = (function(propName) { return function() { return this[propName]; };})('value');
chris-l