البرمجة فوق العادية - هل من الممكن تنفيذ دوال الحصول/التعيين الديناميكية في جافاسكريبت؟
أنا على دراية بكيفية إنشاء getters و setters للخصائص التي يعرف الشخص أسمائها مسبقًا، عن طريق القيام بشيء مثل هذا:
// 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 لهذه؟
لا داعي للقول، أود بشكل مثالي حلاً يكون متوافقاً مع جميع المتصفحات.
الإجابة رقم 1
تغير هذا اعتبارًا من مواصفة ES2015 (المعروفة أيضًا باسم "ES6"): أصبح لدى JavaScript الآن الوكلاء (proxies). تتيح لك الوكلاء إنشاء كائنات تكون وكالات حقيقية (واجهات) لكائنات أخرى. إليك مثالًا بسيطًا يقوم بتحويل أي قيم خواص هي سلاسل نصية إلى حروف كبيرة بالكامل عند الاسترجاع، ويعيد "missing" بدلًا من undefined لأي خاصية غير موجودة:
"use strict";
if (typeof Proxy == "undefined") {
throw new Error("This browser doesn't support Proxy");
}
let original = {
example: "value",
};
let proxy = new Proxy(original, {
get(target, name, receiver) {
if (Reflect.has(target, name)) {
let rv = Reflect.get(target, name, receiver);
if (typeof rv === "string") {
rv = rv.toUpperCase();
}
return rv;
}
return "missing";
}
});
console.log(`original.example = ${original.example}`); // "original.example = value"
console.log(`proxy.example = ${proxy.example}`); // "proxy.example = VALUE"
console.log(`proxy.unknown = ${proxy.unknown}`); // "proxy.unknown = missing"
original.example = "updated";
console.log(`original.example = ${original.example}`); // "original.example = updated"
console.log(`proxy.example = ${proxy.example}`); // "proxy.example = UPDATED"العمليات التي لا تقوم بإعادة تعريفها لها سلوكها الافتراضي. في المثال أعلاه، كل ما نقوم بإعادة تعريفه هو get، لكن هناك قائمة كاملة من العمليات التي يمكنك التوصّل إليها.
في قائمة معالجي الدالة get للوسائط:
targetهو الكائن الذي يتم عمل بروكسي له (original، في حالتنا).nameهو (بالطبع) اسم الخاصية التي يتم استرجاعها، والتي عادة ما تكون سلسلة نصية ولكن يمكن أن تكون أيضًا رمزاً.المستقبلهو الكائن الذي يجب استخدامه كـ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.example = ${obj.example}`);
obj.example = "value";
console.log(`[after] obj.example = ${obj.example}`);الناتج مما ورد أعلاه هو:
Getting non-existent property 'example'
[before] obj.example = undefined
Setting non-existent property 'example', initial value: value
[after] obj.example = valueلاحظ كيف نحصل على رسالة "غير موجود" عندما نحاول استرجاع example عندما لا يكون موجودًا بعد، ومرة أخرى عندما نقوم بإنشائه، لكن ليس بعد ذلك.
إجابة من عام 2011 (أصبحت قديمة وفقًا لما سبق، لكنها لا تزال ذات صلة بالبيئات المحدودة بميزات ES5 مثل إنترنت إكسبلورر):
لا، جافاسكريبت لا تمتلك ميزة خاصية تشمل كل شيء. بناء جملة الوصول الذي تستخدمه مغطى في القسم 11.1.5 من المواصفة، ولا يقدم أي بديل شامل أو شيء من هذا القبيل.
بالطبع، يمكنك تنفيذ دالة للقيام بذلك، لكنني أفترض أنك على الأرجح لا تريد استخدام f=obj.prop("example"); بدلاً من f=obj.example; و obj.prop("example", value); بدلاً من obj.example=value; (والذي سيكون ضرورياً لكي تتعامل الدالة مع الخصائص غير المعروفة).
للعلم، دالة الجيتّر (لم أهتم بمنطق السِتر) ستبدو شيئًا مثل هذا:
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...
};
لكن مرة أخرى، لا أستطيع أن أتخيل أنك تريد فعل ذلك حقًا، بسبب كيف يغير ذلك الطريقة التي تستخدم بها الشيء.
الإجابة رقم 2
المقدمة:
إجابة T.J. Crowder تشير إلى Proxy، الذي سيكون ضرورياً لالتقاط جميع الخصائص التي لا توجد، كما كان يسأل OP. اعتماداً على السلوك المطلوب فعلياً مع الملتقطات/المحدّدات الديناميكية، قد لا يكون Proxy ضرورياً فعلياً؛ أو، من المحتمل، قد ترغب في استخدام مزيج من Proxy مع ما سأعرضه لك أدناه.
(ملاحظة: لقد قمت مؤخرًا بتجربة Proxy بشكل شامل في متصفح فايرفوكس على لينوكس ووجدته قادرًا جدًا، لكنه أيضًا مربك/صعب بعض الشيء في التعامل معه والحصول على الإعداد الصحيح. والأهم من ذلك، لقد وجدته أيضًا بطيئًا إلى حد كبير (على الأقل مقارنة بكيفية تحسين جافاسكريبت في الوقت الحاضر) - أتحدث عن بطء يصل لأضعاف عشرية.)
لتنفيذ المَهْمَلَات (getters) والمُعَيِّنَات (setters) التي تم إنشاؤها ديناميكياً بشكل محدد، يمكنك استخدام 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؛ من الوثائق:تأتي مواصفات الخصائص الموجودة في الكائنات بنوعين رئيسيين: مواصفات البيانات ومواصفات الوصول. مواصفة البيانات هي خاصية لها قيمة، التي قد تكون قابلة للكتابة أو قد لا تكون كذلك. مواصفة الوصول هي خاصية يتم وصفها بواسطة زوج من دوال الحصول والتعيين (getter-setter). يجب أن تكون المواصفة واحدة من هذين النوعين؛ لا يمكن أن تكون كليهما.
- لذلك، ستلاحظ أنني أنشأت خاصية
valخارج استدعاء/موصوف الخاصيةObject.defineProperty(). هذا سلوك قياسي. - وفقًا للخطأ هنا، لا تقم بتعيين
writableإلىtrueفي واصف الخاصية إذا كنت تستخدمgetأوset. قد ترغب في التفكير في ضبط
configurableوenumerable، ومع ذلك، يعتمد الأمر على ما تسعى إليه؛ من الوثائق:قابل للتكوين
trueإذا وفقط إذا كان يمكن تغيير نوع واصف الخاصية هذا وإذا كان يمكن حذف الخاصية من الكائن المقابل.
القيمة الافتراضية هي خطأ.
اقتباس>
قابل للتعداد
trueإذا وفقط إذا ظهرت هذه الخاصية أثناء تعداد الخصائص على الكائن المقابل.القيمة الافتراضية هي خطأ.
وعلى هذه الملاحظة، قد تكون هذه أيضًا محل اهتمام:
Object.getOwnPropertyNames(obj): يجلب جميع خصائص الكائن، بما في ذلك الخصائص غير القابلة للتعداد (حسب معلوماتي، هذه هي الطريقة الوحيدة للقيام بذلك!).Object.getOwnPropertyDescriptor(obj, prop): يحصل على واصف الخاصية لكائن، وهو الكائن الذي تم تمريره إلىObject.defineProperty()أعلاه.obj.propertyIsEnumerable(prop);: بالنسبة لخاصية فردية على نسخة محددة من الكائن، قم باستدعاء هذه الدالة على نسخة الكائن لتحديد ما إذا كانت الخاصية المحددة قابلة للتعداد أم لا.
