Object.defineProperty()‎

من موسوعة حسوب

الدالة Object.defineProperty()‎ تُعرِّف خاصيةً جديدةً على كائنٍ مباشرةً، أو تُعدِّل خاصية موجودةً مسبقًا في كائنٍ ثم تُعيد هذا الكائن.

البنية العامة

Object.defineProperty(obj, prop, descriptor)

obj

الكائن الذي نريد تعريف الخاصية فيه.

prop

اسم الخاصية التي نريد تعريفها أو تعديلها.

descriptor

واصف الخاصية (property descriptor) التي ستُضاف أو تُعدَّل.

القيمة المعادة

الكائن الذي مُرِّرَ إلى الدالة.

الوصف

هذه الدالة تسمح بإضافة أو تعديل خاصية من خاصيات كائنٍ ما بدقة، إذ إنَّ إضافة الخاصيات عبر معامل الإسناد سيُنشِئ خاصياتٍ تظهر عند إحصاء خاصيات العنصر (property enumeration، مثل حلقة for...in أو الدالة Object.keys)، ويمكن تعديل قيمة تلك الخاصيات أو حذفها عبر المعامل delete؛ أما هذه الدالة فتُضيف تفاصيل جديدة إلى الخاصيات؛ إذ إنَّ القيمة المُضافة عبر الدالة Object.defineProperty()‎ تكون غير قابلة للتعديل (immutable) افتراضيًا.

هنالك نوعان من واصفات الخاصيات (property descriptors): واصفات البيانات (data descriptors) وواصفات الوصول (accessor descriptors). واصفات البيانات هي خاصية لها قيمة والتي قد يُسمَح أو يُمنَع تعديلها؛ أما واصفات الوصول فهي خاصيةٌ موصوفةً بزوجين من دوال getter و setter؛ ويجب أن تكون الواصفات من أحد النوعين السابقين ولا يجوز استعمالهما معًا.

واصفات البيانات وواصفات الوصول هما كائنات، ويتشاركان المفاتيح المطلوبة الآتية:

  • configurable: إذا كانت قيمة هذا المفتاح هي true فيمكن تعديل واصف الخاصية (وليس قيمة الخاصية) ويمكن حذف الخاصية من الكائن. القيمة الافتراضية هي false.
  • enumerable: إذا كانت قيمة هذا المفتاح هي true فستظهر هذه الخاصية عند إحصاء خاصيات (enumeration) الكائن. القيمة الافتراضية هي false.

يمكن أن تتواجد المفاتيح الاختيارية الآتية في واصفات البيانات:

  • value: تحديد القيمة المرتبطة مع الخاصية، ويمكن أن تكون أيّ قيمة مسموحة في JavaScript (مثل الأعداد أو الدوال أو الكائنات ...إلخ.). القيمة الافتراضية هي undefined.
  • writable: إذا كانت قيمة هذا المفتاح هي true فسيمكن تعديل القيمة المرتبطة بهذا الخاصية باستخدام معاملات الإسناد. القيمة الافتراضية هي false.

يمكن أن تتواجد المفاتيح الاختيارية الآتية في واصفات الوصول:

  • get: الدالة التي ستُعدّ كدالة getter لهذا الخاصية، أو القيمة undefined إذا لم تكن هنالك دالة getter مرتبطة مع هذه الخاصية؛ القيمة التي ستُعيدها هذه الدالة ستُستخدَم كقيمة للخاصية. القيمة الافتراضية هي undefined.
  • set: الدالة التي ستُعدّ كدالة setter لهذا الخاصية، أو القيمة undefined إذا لم تكن هنالك دالة setter مرتبطة مع هذه الخاصية؛ تقبل هذه الدالة وسيطًا واحدًا هو القيمة الجديدة التي حاول المستخدم إسنادها إلى هذه الخاصية. القيمة الافتراضية هي undefined.

ابقَ في ذهنك أنَّ هذه الخيارات ليست بالضرورة تابعةً للكائن الذي يصف الخاصيات (descriptor's own properties) إذ ستؤخذ الخاصيات الموروثة عبر سلسلة prototype بالحسبان أيضًا:

var obj = {};
var descriptor = Object.create(null); // لن تورث أيّة خاصيات
// ستكون الخاصيات غير قابلة للكتابة أو الإحصاء أو الضبط افتراضيًا
descriptor.value = 'static';
Object.defineProperty(obj, 'key', descriptor);

// ذكر ذلك صراحةً
Object.defineProperty(obj, 'key', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: 'static'
});

أمثلة

إنشاء خاصية

عندما تكون الخاصية المُحدَّد غير موجودة في الكائن، فستُنشِئ الدالة Object.defineProperty()‎ خاصيةً جديدةً، ويمكن حذف الحقول الاختيارية من واصف تلك الخاصية وستُستخدَم القيم الافتراضية لها؛ تذكّر أنَّ جميع مفاتيح الواصفات التي تأخذ قيمًا منطقيةً ستكون false، أما المفاتيح value و get و set فستكون undefined.

سنُنشِئ في المثال الآتي كائنًا جديدًا باسم o ثم نستخدم الدالة Object.defineProperty()‎ لإضافة الخاصية a مع تحديد قيم لمختلف المفاتيح التي تصفها:

var o = {}; // إنشاء كائن جديد

// إضافة خاصية جديدة إلى الكائن
Object.defineProperty(o, 'a', {
  value: 37,
  writable: true,
  enumerable: true,
  configurable: true
});
// أصبح الخاصية موجودةً في الكائن وقيمتها تساوي 37

أما هذا المثال فهو يُضيف دالةً عبر واصفات الوصول، مما يُحدِّد قيمةً لدالة getter و setter فيها:

// مثال عن إضافة خاصية عبر واصفات الوصول
var bValue = 38;
Object.defineProperty(o, 'b', {
  get: function() { return bValue; },
  set: function(newValue) { bValue = newValue; },
  enumerable: true,
  configurable: true
});
o.b; // 38

لاحظ أنَّ من غير الممكن استخدام نوعَي الواصفات معًا، فالمثال الآتي سيرمي الخطأ TypeError:

Object.defineProperty(o, 'conflict', {
  value: 0x9f91102,
  get: function() { return 0xdeadbeef; }
});
// TypeError: Invalid property descriptor.

تعديل خاصية

عندما تكون الخاصية موجودةً من قبل، فستحاول الدالة Object.defineProperty()‎ تعديلها وفقًا للقيم الموجودة في واصف الخاصية وضبط الكائن الحالي؛ فلو كان واصف الخاصية الموجود في الكائن قد ضبط قيمة المفتاح configurable إلى false، فهذا يعني أنَّ الخاصية غير قابلة للضبط ولا يمكن تعديلها (إلا تعديل قيمة المفتاح writable إلى false [والعكس غير صحيح])؛ من غير الممكن التبديل بين أنواع الخاصيات (خاصيات وصول وخاصيات بيانات) عندما تكون الخاصية غير قابلةٍ للضبط.

سيُرمى الخطأ TypeError عند محاولة تغيير الخاصيات غير القابلة للضبط (باستثناء قيمة writable كما بيّنا أعلاه).

المفتاح writable

عندما تُضبَط قيمة المفتاح writable إلى false فستكون الخاصية غير قابلةٍ للكتابة، أي لا يمكن إعادة إسناد القيم إليها:

var o = {}; // إنشاء كائن جديد

Object.defineProperty(o, 'a', {
  value: 37,
  writable: false
});

console.log(o.a); // 37
o.a = 25; // لم يرمَ خطأٌ
// لكن سيرمى خطأٌ في نمط strict
console.log(o.a); // 37. لم تنجح عملية الإسناد

// strict mode
(function() {
  'use strict';
  var o = {};
  Object.defineProperty(o, 'b', {
    value: 2,
    writable: false
  });
  o.b = 3; // TypeError: "b" is read-only
  return o.b; // كان هذا التعبير سيعيد 2 إذا لم يرمَ الخطأ السابق
}());

كما هو واضح في المثال السابق، محاولة الكتابة على خاصية غير قابلة للكتابة لن يؤدي إلى تغييرها، وإنما سيؤدي إلى رمي خطأ.

المفتاح enumerable

يُعرِّف المفتاح enumerable إذا كانت الخاصية قابلةً للإحصاء، أي هل ستظهر في حلقة for...in وفي الدالة Object.keys أم لا. لاحظ أنَّ القيمة الافتراضية للمفتاح enumerable عند استخدام الدالة Object.defineProperty()‎ هي false، لكن قيمة هذا المفتاح هي true إذا أنشأنا خاصيةً بإسناد قيمة إليها:

var o = {};
Object.defineProperty(o, 'a', {
  value: 1,
  enumerable: true
});
Object.defineProperty(o, 'b', {
 value: 2,
 enumerable: false
});
Object.defineProperty(o, 'c', {
  value: 3
}); // خاصية غير قابلة للإحصاء
o.d = 4; // خاصية قابلة للإحصاء لأننا أنشأناها عبر
         // إسناد قيمة إلى خاصية مباشرةً

for (var i in o) {
  console.log(i);
}
// 'a' و 'd' (دون ترتيب)

Object.keys(o); // ['a', 'd']

o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
o.propertyIsEnumerable('c'); // false

المفتاح configurable

المفتاح configurable يتحكم إذا كان بالإمكان حذف الخاصية من الكائن (عبر المعامل delete)، وهل يمكن تغيير قيمة بقية المفاتيح (باستثناء تغيير قيمة المفتاح writable إلى false):

var o = {};
Object.defineProperty(o, 'a', {
  get: function() { return 1; },
  configurable: false
});

Object.defineProperty(o, 'a', {
  configurable: true
}); // throws a TypeError
Object.defineProperty(o, 'a', {
  enumerable: true
}); // throws a TypeError
Object.defineProperty(o, 'a', {
  set: function() {}
}); // throws a TypeError (set was undefined previously)
Object.defineProperty(o, 'a', {
  get: function() { return 1; }
}); // throws a TypeError
Object.defineProperty(o, 'a', {
  value: 12
}); // throws a TypeError

console.log(o.a); // 1
delete o.a; // لن يحدث شيء
console.log(o.a); // 1

إذا كانت قيمة المفتاح configurable للخاصية o.a تساوي true، فلن يرمى أيُّ خطأٍ من الأخطاء السابقة، وستُحذَف الخاصية في النهاية.

إضافة الخاصيات والقيم الافتراضية للمفاتيح

من المهم أن نضع بالحسبان القيم الافتراضية للمفاتيح، فهنالك فرقٌ كبيرٌ بين إسناد قيمة إلى خاصية من خاصيات الكائن مباشرةً واستخدام الدالة Object.defineProperty()‎ كما هو مبيّن في المثال الآتي:

var o = {};

o.a = 1;
// يكافئ التعبير
Object.defineProperty(o, 'a', {
  value: 1,
  writable: true,
  configurable: true,
  enumerable: true
});


// لكن في المقابل
Object.defineProperty(o, 'a', { value: 1 });
// يكافئ
Object.defineProperty(o, 'a', {
  value: 1,
  writable: false,
  configurable: false,
  enumerable: false
});

دوال getter و setter مخصصة

المثال الآتي يوضِّح كيفية إنشاء كائن يأرشِف القيم المسندة إليه تلقائيًا، فعند ضبط قيمة للخاصية temperature فستُضاف إلى المصفوفة archive:

function Archiver() {
  var temperature = null;
  var archive = [];

  Object.defineProperty(this, 'temperature', {
    get: function() {
      console.log('get!');
      return temperature;
    },
    set: function(value) {
      temperature = value;
      archive.push({ val: temperature });
    }
  });

  this.getArchive = function() { return archive; };
}

var arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]

مثالٌ آخر عن دوال getter و setter:

var pattern = {
    get: function () {
        return 'I always return this string, ' +
               'whatever you have assigned';
    },
    set: function () {
        this.myname = 'this is my name string';
    }
};


function TestDefineSetAndGet() {
    Object.defineProperty(this, 'myproperty', pattern);
}


var instance = new TestDefineSetAndGet();
instance.myproperty = 'test';
console.log(instance.myproperty);
// I always return this string, whatever you have assigned

console.log(instance.myname); // this is my name string

دعم المتصفحات

الميزة Chrome Firefox Internet Explorer Opera Safari
الدعم الأساسي 5 4 9 11.6 5.1

مصادر ومواصفات