انت الان في اول فصل من جافا سكريبت يحتوي كل فصل علي 10 دروس يوجد 20 فصل من avascript بمعدل 200 درس في نهاية كل فصل قم بالتعليق وكتابة ما استفد بة وما لم تستفد منة واذا وجهتك اي مشال لا تتردة في مراسلاتنة
ما الجديد في الإصدار القادم من جافاسكربت (ECMAScript 6)##
Harmony هو الاسم الرمزي لـ ECMAScript 6 وهي اللغة القياسية التي تقوم عليها JavaScript، والإصدار الجديد يأتي بميزات جديدة تتناول العديد من جوانب اللغة بما فيها الصياغة (syntax) وأسلوب البناء وأنواع جديدة من المكونات المدمجة في اللغة. في هذا المقال نتعرف على بعض من المميزات التي ستجعل كتابة شيفرة جافاسكربت أكثر اختصاراً وفعالية.
متغيرات نطاقها القطعة البرمجية (Block-scoped Variables)
في الإصدار الحالي من JavaScript، تُعامل كل المتغيرات المفروضة ضمن دالة (
function) على أنها تابعة لهذه الدالة (Function-scoped) أي يمكن الوصول إليها من أي موقع ضمن هذه الدالة، حتى وإن كانت هذه المتغيرات قد فُرضِت ضمن قطعة برمجية فرعية ضمن هذه الدالة (كحلقة for أو جملة شرطية if)، وهذا يخالف ما تتبناه بعض من أشهر لغات البرمجة، وقد يسبب بعض الارتباك لمن لم يعتد عليه.لنوضح أكثر في هذا المثال:
var numbers = [1, 2, 3];
var doubles = [];
for (var i = 0; i < numbers.length; i++) {
var num = numbers[i];
doubles[i] = function() {
console.log(num * 2);
}
}
for (var j = 0; j < doubles.length; j++) {
doubles[j]();
}
عند تنفيذ هذا المثال، سنحصل على الرقم
6 ثلاث مرات، وهو أمر غير متوقع ما لم نكن على معرفة بطبيعة مجالات JavaScript، ولو طبق ما يشبه هذا المثال في لغة أخرى، لحصلنا على النتيجة 2 ثم 4 ثم 6، وهو ما يبدو النتيجة المنطقية لشيفرة كهذه.ما الذي يحدث هنا؟ يتوقع المبرمج أن المتغير
num محصور ضمن حلقة for وعليه فإن الدالة التي ندخلها في المصفوفة doubles ستعطي عند استدعائها القيمة التي ورثتها عن مجال حلقة for إلا أن الحقيقة هي أن المتغير num يتبع للمجال العام، لأن حلقة for لا تُنشئ مجالًا فرعيًّا وعليه فإن القيمة العامة num تتغير ضمن حلقة for من 2 إلى 4 إلى 6 وعند استدعاء أي دالة ضمن المصفوفة doubles فإنها ستعيد إلينا القيمة العامة num، وبما أن الاستدعاء يحدث بعد إسناد آخر قيمة للمتغير num، فإن قيمته في أي لحظة بعد انتهاء الحلقة الأولى ستكون آخر قيمة أسندت إليه ضمن هذه الحلقة، وهي القيمة 6.يعطينا الإصدار القادم طريقة لحل هذا الارتباك باستخدام الكلمة المفتاحية
let بدلاً عن var، وهي تقوم بخلق مجال ضمن القطعة البرمجية التي تُستخدم فيها، بمعنى آخر: ستكون let هي بديلنا عن var من الآن فصاعدًا، لأنها ببساطة تعطينا النتائج البديهية التي نتوقعها. لنُعِد كتابة المثال السابق باستبدال var num بـlet num:
var numbers = [1, 2, 3];
var doubles = [];
for (var i = 0; i < numbers.length; i++) {
let num = numbers[i];
doubles[i] = function() {
console.log(num * 2);
}
}
for (var j = 0; j < doubles.length; j++) {
doubles[j]();
}
عند تطبيق هذا المثال (يمكنك تطبيقه في Firefox وChrome لأن كلا المتصفحين شرعا في دعم
let) سنحصل على النتيجة البديهية 2 ثم 4 ثم 6. بالطبع بإمكاننا تحسين الشيفرة باعتماد let عند التصريح عن كل المتغيرات السابقة، وهو الأمر الذي يجب أن تعتاد فعله من اليوم!شيفرة أقصر وأسهل للقراءة
لعل أكثر ما أُحبّه في JavaScript مرونتها الفائقة، وبالذات القدرة على إمرار دوال مجهولة (Anonymous Functions) لدوال أخرى، الأمر الذي يسمح لنا بكتابة شيفرة ما كان من الممكن كتابتها بلغات أخرى إلا بضعفي عدد الأسطر وربما أكثر. لاحظ هذا المثال:
var people = ['Ahmed', 'Samer', 'Khaled'];
var greetings = people.map(function(person) { return 'Hello ' + person + '!'; });
console.log(greetings); // ['Hello Ahmed!', 'Hello Samer!', 'Hello Khaled!'];
لو أردنا تنفيذ هذه المهمة في لغة أخرى، فلربما احتجنا إلى حلقة
for لنمرّ من خلالها على كل عنصر ضمن المصفوفة ثم إدخال العبارات الجديدة ضمن مصفوفة أخرى، وهذا يعني أن مهمة يمكن كتابتها بسطرين في JavaScript قد تتطلب 5 سطور في لغة أخرى. لو لم تمتلك JavaScript القدرة على إمرار الدالة المجهولة function(person) {...} أعلاه، لفقدت جزءًا كبيرة من مرونتها.لكن الإصدار القادم من JavaScript تذهب أبعد من ذلك، وتختصر علينا كتابة الكثير من النص البرمجي. لُنعد كتابة المثال السابق:
let people = ['Ahmed', 'Samer', 'Khaled'];
let greetings = people.map(person => 'Hello ' + person + '!');
console.log(greetings); // ['Hello Ahmed!', 'Hello Samer!', 'Hello Khaled!'];
في هذا المثال استخدمنا ما اصطلح على تسميته دوال الأسهم (Arrow Functions)، وهي طريقة أكثر اختصارًا لكتابة الدوال المجهولة، لن تحتاج لكتابة
return، فهي ستضاف تلقائيًا عند التنفيذ. من الآن فصاعداً اعتمد دوال الأسهم عندما تريد تنفيذ دالة مجهولة بسيطة بسطر واحد.بمناسبة الحديث عن الشيفرة المختصرة... ما رأيكم لو جعلنا الشيفرة أعلاه أكثر اختصارًا؟!
let people = ['Ahmed', 'Samer', 'Khaled'];
let greetings = ['Hello ' + person + '!' for (person of people)];
console.log(greetings); // ['Hello Ahmed!', 'Hello Samer!', 'Hello Khaled!'];
قد تبدو الصياغة غريبة بعض الشيء، لكنها تتيح لنا فهم النص بسهولة أكبر، وتغنينا عن الحاجة لدالة مجهولة (الأمر الذي قد يؤثر على الأداء، وإن كان بأجزاء من الثواني). الصياغة التي استخدمناها أعلاه تُسمى Array Comprehensions، وإن كنت قادرًا على ترجمتها إلى العربية بطريقة واضحة، فلا تبخل بها علينا!
لكن... ألا ترون أنه يمكن تحسين هذه الشيفرة قليلاً؟
let people = ['Ahmed', 'Samer', 'Khaled'];
let greetings = [`Hello ${ person }!` for (person of people)];
console.log(greetings); // ['Hello Ahmed!', 'Hello Samer!', 'Hello Khaled!'];
هنا استبدلنا إشارات الاقتباس (
' أو ") بالإشارة ` الأمر الذي أتاح لنا إحاطة المتغير person بقوسين معكوفين مسبوقين بإشارة $، وهذه الصياغة تدعى "السلاسل النصية المقولبة" أو Template Strings، والتي تسمح -بالإضافة إلى القولبة- بالعديد من الأشياء الرائعة، كالعبارات على عدة أسطر:
let multilineString = `I am
a multiline
string`;
console.log(multilineString);
// I am
// a multiline
// string
. تحديث: بدأ Firefox Nightly باعتمادها.
من المميزات الجديدة كذلك إمكانية اختصار بناء الكائنات ذات الخصائص بالشكل التالي:
- حاليًا، نقوم بكتابة شيفرة مثل هذه:
var createPerson = function(name, age, location) {
return {
name: name,
age: age,
location: location,
greet: function() {
console.log('Hello, I am ' + name + ' from ' + location + '. I am ' + age + '.');
}
}
};
var fawwaz = createPerson('Fawwaz', 21, 'Syria');
console.log(fawwaz.name); // 'Fawwaz'
fawwaz.greet(); // "Hello, I am Fawwaz from Syria. I am 21."
- في الإصدار القادم، سيكون بالإمكان كتابة الشيفرة كالتالي:
let createPerson = function(name, age, location) {
return {
name,
age,
location,
greet() {
console.log('Hello, I am ' + name + ' from ' + location + '. I am ' + age + '.');
}
}
};
let fawwaz = createPerson('Fawwaz', 21, 'Syria');
console.log(fawwaz.name); // 'Fawwaz'
fawwaz.greet(); // "Hello, I am Fawwaz from Syria. I am 21."
بما أن اسم المُعامل (parameter) يماثل اسم الخاصة (property)، فإن هذا يتم تفسيره على أن قيمة الخاصة توافق قيمة المعامل، بمعنى:
name: name، بالإضافة إلى كتابة greet() {...} بدل greet: function() {...}.كذلك سيكون بإمكاننا تحسين هذا النص أكثر من ذلك باستخدام الأصناف (Classes)، نعم! سيكون لدينا أصناف أخيرًا! (سنستعرضها لاحقاً)
الثوابت (Constants)
سيداتي وسادتي... رحبوا بالثوابت... نعم إنها أخيرًا متوفرة في JavaScript، إحدى المكونات الأساسية لأي لغة برمجية التي لم تكن متوفرة في JavaScript، أصبحت الآن متوفرة. والآن نأتي للسؤال البديهي: لماذا أحتاج للثوابت؟ أليس بإمكاني التصريح عن متغير دون أن أغير قيمته بعد إعطاءه القيمة الأولية؟ نعم بالطبع بإمكانك ذلك، لكن هذا لا يعني بالضرورة أن المستخدم أو نصاً برمجيًا من طرف ثالث ليس بإمكانه تغيير قيمة هذا المتغير في سياق التنفيذ، وطالما أن المتغير "متغير" بطبيعته، فإننا دومًا بحاجة إلى شيء من أصل اللغة يحمينا من تغييره خطأ. عند التصريح عن ثابت فإننا نعطيه قيمة أولية ثم ستتولى الآلة البرمجية لـJavaScript حماية هذا الثابت من التغيير، وسُيرمى خطأ عند محاولة إسناد قيمة جديدة لهذا الثابت.
const myConstant = 'Never change this!';
myConstant = 'Trying to change your constant';
// TypeError: redeclaration of const myConstant
console.log(myConstant); // "Never change this!"
المُعاملات الافتراضية (Default Parameters)
غياب دعم المُعاملات الافتراضية في JavaScript واحد من أكثر الأشياء التي تزعجني، لأنها تجبرني على كتابة شيفرة مثل هذه:
function SayHello (user) {
if (typeof user == 'undefined') {
user = 'User';
}
console.log('Hello ' + user);
}
console.log(SayHello('Fawwaz')); // Hello Fawwaz!
console.log(SayHello()); // Hello User!
لو كان عندي 3 متغيرات غير إجبارية، فهذا يعني أنني سأحتاج 3 جمل شرطية، الأمر الذي يتطلب الكثير من الكتابة المُملة. بفضل الإصدار القادم من JavaScript، سيكون بالإمكان كتابة شيفرة أبسط بكثير:
function SayHello (user='User') {
console.log('Hello ' + user);
}
SayHello('Fawwaz'); // Hello Fawwaz!
SayHello(); // Hello User!
الوعود (Promises)
الوعود هي الحل الذي تأتينا به JavaScript لحل مشكلة هرم الموت (Pyramid of Death) الذي نواجهه عند تنفيذ مهمات غير متزامنة تعتمد إحداها على الأخرى:
function getFullPost(url, callback) {
var getAuthor = function(post, callback) {
$.ajax({ method: 'GET', url: '/author/' + post.author_id }, callback);
};
var getRelatedPosts = function(post, callback) {
$.ajax({ method: 'GET', url: '/related/' + post.id }, callback);
};
$.ajax({ method: 'GET', url: url }, function(post) {
getAuthor(post, function(res) {
post.author = res.data.author;
getRelatedPosts(post, function(res) {
post.releated = res.data.releated;
callback(post);
});
});
});
}
هل تلاحظ أن الشيفرة تتجه نحو اليمين؟ لو أردنا تنفيذ هذه المهمات غير المتزامنة واحدة بعد الأخرى وكان عددها 10 مثلًا فستصبح الشيفرة شديدة التعقيد، كما أن هذه الطريقة ليست بديهية، ولا يمكن لك أن تفهم ماذا تفعل هذه الدالة المجهولة (المعامل الثاني في كل دالة) ما لم تألفها. ماذا لو أمكننا كتابة هذه الشيفرة بصورة أفضل؟
function getFullPost(url) {
var post = { };
var getPost = function(url) {
return $http.get(url);
};
var getAuthor = function(post) {
return $http.get('/author/' + post.author_id).then(function(res) {
post.author = res.data.author;
});
};
var getRelatedPosts = function(post) {
return $http.get('/related/' + post.id).then(function(res) {
post.related = res.data.related;
});
};
return getPost().then(getAuthor).then(getRelatedPosts).catch(function(err) {
console.log('We got an error:', err);
});
}
ما الجديد في الإصدار القادم من جافاسكربت (ECMAScript 6) القسم الثاني
ذكرنا في الجزء السابق أن اهتمامًا كبيرًا أُوليَ لتسهيل كتابة الشفرة وقراءتها في ECMAScript 6، والإسناد بالتفكيك (Destructuring assignment) لا يخرج عن هذا السياق، وهو ليس بالمفهوم الجديد في عالم البرمجة، فهو معروف في Python وفي Ruby. بعيدًا عن تعقيدات المصطلحات، إليك هذا المثال:
var [a, b, c] = [1, 2, 3];
a == 1 // true
b == 2 // true
c == 3 // true
ما الذي يحدث هنا؟ بكل بساطة تسمح ECMAScript 6 بصياغة جديدة للتعريف عن المتغيرات أو إسناد قيم جديدة إليها جُملةً واحدة من خلال جمعها ضمن قوسي مصفوفة (Array) وسيقوم مُفسّر اللغة بإسناد قيمة مقابلة لكل متغيّر من المصفوفة الواقعة على يمين مُعامل الإسناد (
=). الأمر لا يقتصر على إسناد المصفوفات، بل يمكن أيضًا إسناد خصائص العناصر:let person = { firstName: "John", lastName: "Smith", Age: 42, Country: "UK" };
let { firstName, lastName } = person;
console.log(`Hello ${ firstName } ${ lastName }!`); // Hello John Smith!
في هذا المثال لدينا متغيّرات تتبع للنطاق العامّ
firstName وlastName، وقد أسندنا لها قيمًا من خصائص الكائن person، حيث يبحث مفسّر اللّغة عن خصائص في الكائن person يماثل اسمها اسم المتغيّر المفروض ويُسندها إلى المُتغيّرات. يمكن توضيح المقصود بصورة أفضل إذا أعدنا كتابة الشفرة لتتوافق مع الإصدار الحالي من JavaScript:var person = { firstName: "John", lastName: "Smith", Age: 42, Country: "UK" };
var firstName = person.firstName;
var lastName = person.lastName;
console.log("Hello " + firstName + " " + lastName + "!"); // Hello John Smith!
يشيع استخدام التفكيك في CoffeeScript (وهي لغة أقر Brendan Eich مُخترع JavaScript بأنّ الإصدار الأخير من JavaScript استوحى الكثير منها)، وخصوصًا عندما تُنظّم البرامج في وحدات كما في Node.js ويكون اهتمامًا مُقتصرًا على استيراد جزء مُحدّد من الوحدة المعنيّة:
{ EventEmitter } = require 'events'
{ EditorView } = require 'atom'
{ compile } = require 'coffee-script'
compile('# coffeescript code here');
عند تحويل هذا النص إلى JavaScript الحالية، سنحصل على:
var EventEmitter = require('events').EventEmitter;
var EditorView = require('atom').EditorView;
var compile = require('events').compile;
compile('# coffeescript code here');
من الاستخدامات المفيدة للإسناد بالتفكيك التبديل بين قيمتي متغيّرين بصورة سهلة، سنقتبس المثال من توثيق CoffeeScript ونُعيد كتابته بـJavaScript:
var theBait = 1000;
var theSwitch = 0;
[theBait, theSwitch] = [theSwitch, theBait];
قبل ES6 كنا لنحتاج لكتابة مُتغيّر مؤقّت نخزن فيه قيمة إحدى المتغيّرين للاحتفاظ بها قبل التبديل بين القيمتين، وهو ما يفعله محوّل CoffeeScript بالفعل ليعطينا شفرة JavaScript متوافقة مع الإصدار الحالي (مع أنه يقوم بتخزين كلا القيمتين في مصفوفة، إلا أنّ الفكرة تبقى ذاتها):
var theBait, theSwitch, _ref;
theBait = 1000;
theSwitch = 0;
_ref = [theSwitch, theBait], theBait = _ref[0], theSwitch = _ref[1];
المُكرِّرات (Iterators) وحلقة for... of
ما من لغة برمجة تخلو من وسيلة للمرور على عدد من القيم وتكرار تنفيذ عمليّة معيّنة على هذه القيم، من أبسط هذه الوسائل حلقة
for التقليديّة الشّهيرة، وفي JavaScript يشيع استخدام حلقة for... in إلى جانبها للمرور على أسماء خصائص العناصر، إذ يمكننا معرفة كل خصائص العنصر document بسطرين فقط:for (var propertyName in document) {
console.log(propertyName);
}
// "body"
// "addEventListener"
// "getElementById"
// ...
لاحظ أن حلقة
for... in تُعيد أسماء خصائص العنصر (كسلسة نصيّة String)، والأمر لا يستثني المصفوفات، فهي ليست سوى كائنات بأسماء خصائص توافق رقم الفهرس (Index):for (var i in [1, 2, 3]) {
console.log(i);
}
// "0"
// "1"
// "2"
من عيوب حلقة
for... in أن لا شيء في تعريف اللغة يُجبر مُفسّر اللّغة على إخراج العناصر بترتيب ثابت بالضّرورة، وهذا يعني أنها تصبح مباشرة غير صالحة للمرور على المصفوفات - التي تستخدم لحفظ عناصر مُرتّبة - بطريقة بديهيّة، ويحلّ محلّها حلقة for التقليديّة عندئذٍ، وأمّا عند استخدامها للمرور على الكائنات، فإنّها لا تُعيد إلّا الخصائص الّتي تُعرّف على أنها قابلة للتعداد (enumerable)، وهو شيء يُحدّد عند تعريف الخاصّة، كما أنّها تُعيد الخصائص القابلة للتعداد التي ورثها الكائن عن "آباءه" ضمن سلسلة الوراثة، وهو تصرّف قد لا يكون مرغوبًا دومًا، وغالبًا سترى المطوّرين يُجرون فحصًا للخاصّة قبل متابعة تنفيذ الشفرة لمعرفة ما إذا كانت تخصّ العنصر ذاته أمّ أنّه ورثها:var obj = { a: 1, b: 2, c: 3 }; // كائن جديد لا يرث سوى النموذج Object
for (var prop in obj) {
console.log("o." + prop + " = " + obj[prop]);
}
// "o.a = 1"
// "o.b = 2"
// "o.c = 3"
في هذا المثال (المنقول عن شبكة مطوّري موزيلا) فرضنا كائنًا جديدًا بثلاث خصائص، وعند المرور عليه بحلقة
for... in فإنّنا حصلنا على النتيجة المتوقّعة، ولم نحصل على خصائص إضافيّة لأنّ الكائن الذي فرضناه لا يرث أي كائن آخر سوى Object (الذي ترثه كل الكائنات افتراضًا). أما في المثال التالي، فقد احتجنا لإجراء اختبار hasOwnProperty على العنصر الوارث لكي لا تظهر سوى الخاصة color التي يملكها بذاته ولم يرثها:var triangle = {a: 1, b: 2, c: 3};
function ColoredTriangle() {
this.color = "red";
}
ColoredTriangle.prototype = triangle;
var obj = new ColoredTriangle();
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
console.log("o." + prop + " = " + obj[prop]);
}
}
// Output:
// "o.color = red"
حسنًا، لقد أطلنا الحديث عن حلقة
for... in وهي ليست بالجديدة؛ لكنّنا أصبحنا نرى الحاجة لشيء جديد أكثر بساطة ومرونة، فهذا ما تبتغيه ES6 في النهاية، ولهذا نشأت فكرة المُكرّرات؛ التي تسمح لأي عنصر بأن يختار لنفسه الطّريقة التي يتصرّف بها عند المرور به في حلقة، ومع المُكرّرات لا بدّ من نوع جديد من الحلقات لتلبية هذه الحاجة والمحافظة على حلقة for... in للتوافق مع الإصدارات القديمة. من هنا نشأت حلقة for... of الجديدة.for (var num of [1, 2, 3]) {
console.log(num);
}
// 1
// 2
// 3
for (var node of document.querySelectorAll('a')) {
console.log(node);
}
//
//
حصلنا في المثالين السابقين على قيمة الخاصّة وليس اسم الخاصّة، لكنّ هذا لا يعني أنّ حلقة
for... of تُعيد قيم الخصائص دومًا، بل إنّها تستدعي مُكرّر الكائن (Iterator) وتطلب منه في كلّ دورة للحلقة تزويدها بشيء ما، وتترك للمُكرّر الحُريّة بإعادة أي قيمة يرغب بها، ولكن ولأنّنا نستدعي في مثالنا مصفوفة أولاً، وعنصر من نوع NodeList ثانيًا، وكلا النّوعين يُعيد مُكرُّرهما قيمَ العناصر في المصفوفة، فإنّنا نحصل على تلك النتيجة البديهيّة. بإمكاننا إنشاء أصناف بمُكرّرات خاصّة نُنشئها بأنفسنا، ولنفترض أن لدينا نوعًا لصفّ ضمن مدرسة ابتدائية، ونريد أن نحصل على تفاصيل الطلّاب على هيئة نص مُنسّق عند المرور على الصّفّ في حلقة for... of:function SchoolClass(students) {
this.students = students;
}
SchoolClass.prototype[Symbol.iterator] = function*() {
for (let i = 0; i < this.students.length; i++) {
let student = this.students[i];
yield `#${i+1} ${student.name} (${student.age} years old)`;
}
}
var ourClass = new SchoolClass([ { name: "Ahmed", age: 10 }, { name: "Alaa", age: 9 }/*, ...*/ ]);
for (student of ourClass) {
console.log(student);
}
// "#1 Ahmed (10 years old)"
// "#2 Alaa (9 years old)"
// "#3 ..." ...
استخدمنا الرمز الخاص
Symbol.iterator لإسناد دالّة مُولّد (Generator function) التي تُعطينا عند استدعائها نسخة من مُكرّر الصنف المُخصّص الذي أنشأناه. سنتعرف بعد قليل على المُولّدات (Generators) وكذلك على الرموز (Symbols) في وقت لاحق. لاحظ أنّنا استخدمنا حلقة for... of للمرور على محتويات ourClass.تذكّر أنّنا استخدمنا هذه الحلقة في الجزء السابق مع Array Comprehension، كما في المثال:
let people = ["Samer", "Ahmed", "Khalid"];
console.log([`Hello ${person}` for (person of people)]);
إن كانت الفقرة الأخيرة غامضة بعض الشيء فلا تقلق، سنتوسّع بشرح المولّدات بعد قليل.
لكن دعونا نتوقّف قليلاً ولننتقل إلى الجانب الفلسفي لهذه الإضافات في JavaScript، قد تبدو للوهلة الأولى تعقيدات بلا طائل، خصوصًا وأنّ كثيرًا منها لا يهدف سوى للتسهيل، ولا يقدّم شيئًا يستحيل إنجازه بالإصدارات السابقة من اللغة؛ هنا يمكن الرّدّ بأنّ تطوّر اللغة متعلّق بكيفيّة استخدامها والخبرة التي تُكتسب مع مرور السّنين، حيث تظهر للمطوّرين حاجات جديدة وأفكار تطبّق مرارًا لدرجة أنها ترتقي لتصبح ضمن أساسات اللغة. سهولة كتابة الشفرة لم تعد رفاهية، بل هي ضرورة لإنجاز المشاريع الكبيرة لأنّها تتيح اختصار الوقت الذي كان سيضيع في كتابة متكرّرة ومُملّة، كما أنّها تُلبّي ما يتوقّعه المطوّرون من لغة أصبحت تؤخذ على محمل الجدّ وتُستخدم في تطوير تطبيقات ضخمة ومُعقّدة بعد أن كان جُلّ استخدامها تنفيذ بعض المهام البسيطة.
المُولِّدات (Generators)
المولدات (Generators) ببساطة هي دوال يمكن إيقافها والعودة إليها في وقت لاحق مع الاحتفاظ بسياقها دون تغيير، صياغة دوال المولدات لا تختلف كثيرًا عن صياغة الدوال التقليدية، كل ما عليك هو إضافة إشارة * بعد
function واستخدام yield بدل return، المثال التالي سيوضح فكرة المولدات أكثر:function* getName() {
let names = ['Muhammad', 'Salem', 'Abdullah'];
for (name of names) {
yield name;
}
}
let nameGenerator = getName();
nameGenerator.next().value; // 'Muhammad'
nameGenerator.next().value; // 'Salem'
nameGenerator.next().value; // 'Abdullah'
nameGenerator.next().value; // undefined
}
ما الذي يحدث هنا؟ فرضنا دالّة مولّد (Generator function) (والتي تُميّز بإشارة النجمة
*) سمّيناها getName، وفيها صرحنا عن مصفوفة فيها أسماء، وظيفة هذه الدالة أن تعطينا عند استدعائها نسخة من مُكرّر (Iterator) (الذي شرحناه لتوّنا)، يزوّدنا بالأسماء بالترتيب في كل مرة نستدعيه فيها ليعطينا النتيجة التالية (next())، أولاً يجب حفظ نسخة المُكرّر ضمن متغير لكي نسمح له بحفظ حالته، ودون ذلك سيعطينا استدعاء دالّة المولد مباشرةً getName().next() دوماً النتيجة الأولى لأننا عملياً نُنشئ نسخة جديدة عنه في كل مرة نستدعيه، أما استدعاء نسخة عنه وحفظها في متغير مثل myGenerator فيسمح لنا باستدعاء .next() عليها كما هو متوقع. لا ترجع الدالة .next() القيمة التي نرسلها عبر yield فقط، بل ترجع كائناً يحوي القيمة المطلوبة ضمن الخاصة value، وخاصة أخرى done تسمح لنا بمعرفة ما إذا كان المولد قد أعطانا كل شيء.لنُعِدْ ترتيب أفكارنا:
- المولّدات تسمح بتوقّف تنفيذها مع الاحتفاظ بحالة التنفيذ (يحدث توقّف التنفيذ عند كلّ كلمة
yield). فلو أنّنا كتبنا دالّة تقليديّة في المثال أعلاه معreturnبدلyieldلحصلنا في كلّ مرّة على الاسم الأول (Muhammad). وهذه الميزة في المولّدات يمكن استغلالها لإنشاء حلقات لا نهائية دون إعاقة متابعة البرنامج:function* numberGenerator() {
for (let i = 0; true; i++) {
yield i;
}
}
let numGen = numberGenerator();
numGen.next(); // { value: 0, done: false }
numGen.next(); // { value: 1, done: false }
numGen.next(); // { value: 2, done: false }
numGen.next(); // { value: 3, done: false }
// ... - دوالّ المولّدات تُعطي عند استدعاءها مُكرّرات، وهذا يعني إمكانيّة استخدامها في حلقة
for... of(احذر من تطبيق مثال كهذا على مولّد غير منتهٍ كما في المثال السابق!):for (let name of getName()) {
console.log(name);
}
// "Muhammad"
// "Salem"
// "Abdullah" - لكلّ مُكرّر وظيفة
.next()مهمّتها بدء تنفيذ الدّالّة أو متابعة تنفيذها ثم إيقافها مؤقّتًا عند كلّ كلمةyield. - استدعاء
next()على المُكرّر يعيد لنا في كلّ مرة كائنًا ذا خاصّتين: الأولىvalueوهي أيّ شيء نُعيده بكلمةyield، والثّانيةdoneوهي قيمة منطقيّة (Boolean) تشير إلى حالة انتهاء تنفيذ الدّالة.
تقبل الوظيفة
.next() للمُكرّرات مُعاملاً اختياريًّا تستقبله وتُرسله لدالّة المولّد بعد متابعة التنفيذ، ويمكن استخدامها لإرسال رسائل لدالّة المولّد بحيث نؤثّر في تنفيذه:function* numberGenerator() {
for (let i = 0; true; i++) {
var reset = yield i;
if (reset) i = -1;
}
}
let numGen = numberGenerator();
numGen.next(); // { value: 0, done: false }
numGen.next(); // { value: 1, done: false }
numGen.next(); // { value: 2, done: false }
numGen.next(); // { value: 3, done: false }
numGen.next(true); // { value: 0, done: false }
في هذا المثال مرّرنا القيمة
true إلى الوظيفة .next() على المُكرّر، والذي بدوره يُرسلها لدالّة المولّد كنتيجة yeild i في الدّورة الموافقة للحلقة، لنقومَ بحفظها في متغيّر reset ونُجريَ فحصًا عند متابعة التنفيذ لإعادة تعيين قيمة i، التي ستزداد بمقدار واحد مع بدء الدورة التالية لحلقة for جاعلةً قيمة i مساوية للصّفر.خصائص المولّدات تجعلها مناسبة جدًا لكتابة شيفرة غير متزامنة بصورة أسهل تكاد تبدو فيها وكأنها شيفرة متزامنة خالية من الاستدعاءات الراجعة المتداخلة (Nested callbacks)؛ هذه الفكرة تحتاج إلى تركيز لأنها أساس لعدد من المكتبات مثل co وsuspend التي ظهرت مؤخّرًا وتصاعدت شعبيّتها بسرعة لأنّها تحلّ مشكلة جوهرية في استخدام JavaScript، ألا وهي التعامل مع الدوال غير المتزامنة (asynchronous functions) وذلك بالاعتماد كُليًّا على المُولّدات.
لنفترض أنّ لدينا موقعًا لقراءة الكتب يعرض ملفّ المستخدم الشّخصيّ مع عدد الكتب التي قرأها وعنوان آخر كتاب مع تقييم المستخدم له:
var list = document.querySelector("#book-list");
getJSON("http://reading-website.com/users/fwz.json", function(err, user) {
if (err) return; // افعل شيئًا بما بخصوص الخطأ
var num_books = user.books.length;
var most_recent_book_id = user.books[num_books - 1];
getJSON("http://reading-website.com/users/fwz/ratings/" + most_recent_book_id + ".json", function(err, user_rating) {
getJSON("http://reading-website.com/books/" + most_recent_book_id + ".json", function(err, book) {
var fragment = document.createDocumentFragment();
var h2 = document.createElement("h2");
h2.textContent = user.full_name;
var h3 = document.createElement("h3");
h3.textContent = "الكتب التي قرأها";
for (let book of books) {
let li = document.createElement("li");
li.textContent = book.title + (book.id == most_recent_book_id ? " " + user_rating : "");
fragment.appendChild(li);
}
list.appendChild(fragment);
});
});
})
في المثال السابق احتجنا إلى إرسال 3 طلبات AJAX يعتمد أحدها على الآخر، ولأنّنا لا نستطيع إرسال طلب بتقييم المستخدم للكتاب قبل معرفة مُعرّف الكتاب، فلا بدّ من أن يرسل الطلب الخاصّ بتقييم الكتاب ضمن الاستدعاء الرّاجع لطلب معلومات المستخدم، ثمّ يمكن جلب عنوان الكتاب ضمن الاستدعاء الرّاجع للطلب السّابق، وهذا يعني زيادة تعقيد الشفرة مع تداخل الاستدعاءات الرّاجعة لتبدو أشبه بسباغيتي لا تُعرف بدايته من نهايته.
تخيّلوا -لغرض التّخيّل- لو أمكننا كتابة هذه الشفرة (وهي غير متزامنة) لتبدو لقارئها وكأنها نص برمجي يسير بترتيب متزامن وبديهيّ... ألن يكون هذا أعظم شيء منذ اختراع JavaScript؟
var list = document.querySelector("#book-list");
try {
var user = getJSON("http://reading-website.com/users/fwz.json");
var num_books = user.books.length;
var most_recent_book_id = user.books[num_books - 1];
var user_rating = getJSON("http://reading-website.com/users/fwz/ratings/" + most_recent_book_id + ".json");
var book = getJSON("http://reading-website.com/books/" + most_recent_book_id + ".json");
var fragment = document.createDocumentFragment();
var h2 = document.createElement("h2");
h2.innerText = user.full_name;
var h3 = document.createElement("h3");
h3.innerText = "الكتب التي قرأها";
for (let book of books) {
let li = document.createElement("li");
li.innerText = book.title + (book.id == most_recent_book_id ? " " + user_rating : "");
fragment.appendChild(li);
}
list.appendChild(fragment);
} catch (e) {
// افعل شيئًا بما بخصوص الخطأ
// Error
}
نحن نعلم أن الأمور لا يمكن أن تكون بهذه الروعة، وأنّ الشفرة أعلاه لن تعمل... نحن نعلم أن شيفرتنا تحتاج تفاصيل المستخدم للحصول على الكتب، وأننا نحتاج للكتاب لجلب عنوانه وتقييمه، وتنفيذ هذه المهمّات بشكل غير متزامن لا يعني أنّه ليس علينا انتظار المهمّة الأولى قبل إطلاق الثانية - بل يعني فقط أن المتصفح يمكنه تنفيذ رسم العناصر الأخرى وعرض الصفحات وإرسال طلبات أخرى في هذا الوقت.
حسنًا، لدي خبر جيّد وآخر سيئ: أمّا الجيّد فهو أنّنا كتابة شيفرة شبيه بهذه أصبحت قريبة المنال مع الدّوالّ غير المتزامنة (Async Functions)، وأمّا الخبر السيّئ فهو أنّ علينا الانتظار إلى الإصدار 7 من ECMAScript لنستطيع كتابتها! (مع العلم أن المتصفّحات لم تنتهِ من تطبيق ES6!).
لكن هذا لا يعني أن نقف مكتوفي الأيدي إلى أن تصدر ES7، بل بإمكاننا إيجاد حلّ وسط لهذه المشكلة؛ لماذا نضطّر إلى تعقيد الأمور بالاستدعاءات الرّاجعة المتداخلة؟ ألا يتوفّر في اللّغة بنية برمجيّة تسمح بإيقاف شيفرتنا ريثما يتمّ أمر ما غير متزامن (الانتظار لإكمال طلب AJAX) ثمّ المتابعة بعد انتهاءه؟ يبدو هذا الحديث مألوفًا!
نعلم حتى الآن أننا بحاجة لاستخدام مولّد، ولذلك سنحيط شيفرتنا بدالّة مولّد كخطوة أولى:
var list = document.querySelector("#book-list");
function* displayUserProfile() {
// شيفرتنا هنا
}
الآن نحتاج لتنفيذ طلب AJAX الأوّل والانتظار إلى انتهاءه قبل الانتقال إلى الطّلب الثّاني، نعلم أنّ yield توقف تنفيذ المولّد:
var list = document.querySelector("#book-list");
function* displayUserProfile() {
yield getJSON("http://reading-website.com/users/fwz.json");
// ...
}
عظيم! لكن كيف نُخبر المولّد بأنّ عليه متابعة التنفيذ؟
var list = document.querySelector("#book-list");
function* displayUserProfile() {
yield getJSON("http://reading-website.com/users/fwz.json", resume);
// ...
}
سنمرر دالة اسمها resume للدّالة getJSON، وهذه الدالة ستُستدعى عند انتهاء جلب جواب الطّلب الذي أرسلناه، وهي فرصتنا لإخبار المولّد بمتابعة التنفيذ... فكيف سيكون محتواها؟
var list = document.querySelector("#book-list");
var resume = function(err, response) {
displayIterator.next(response);
}
function* displayUserProfile() {
var user = yield getJSON("http://reading-website.com/users/fwz.json", resume);
var num_books = user.books.length;
var most_recent_book_id = user.books[num_books - 1];
var user_rating = yield getJSON("http://reading-website.com/users/fwz/ratings/" + most_recent_book_id + ".json", resume);
var book = yield getJSON("http://reading-website.com/books/" + most_recent_book_id + ".json", resume);
var fragment = document.createDocumentFragment();
var h2 = document.createElement("h2");
h2.innerText = user.full_name;
var h3 = document.createElement("h3");
h3.innerText = "الكتب التي قرأها";
for (let book of books) {
let li = document.createElement("li");
li.innerText = book.title + (book.id == most_recent_book_id ? " " + user_rating : "");
fragment.appendChild(li);
}
list.appendChild(fragment);
}
var displayIterator = displayUserProfile();
displayIterator.next();
حفظنا نسخة عن المكرّر في متغيّر ثم استدعينا وظيفته
next() في دالّة المتابعة، ممرّرين لها جواب الطّلب ليمكننا تخزينه ضمن المتغيّر user. الدّالة resume تستطيع الوصول إلى displayIterator لأنّه يكون معرّفًا قبل استدعاءها حتمًا، ولا ننسَ أن تعريف المتغيّرات في JavaScript يخضع لعملية الرّفع إلى أعلى النّطاق (variable hoisting) ممّا يجعل المتغيّر displayIterator موجودًا (وإن كان بلا قيمة) منذ بداية تنفيذ الشيفرة.للتأكّد من فهم هذه الشيفرة، سنعيد تحليلها خطوة بخطوة: في طلب AJAX الأوّل تستدعى الدالة
resume ويمرّر إليها جواب الطّلب (response)، الذي يمرّر بدوره إلى المُكرّر ليُحفظ في المتغيّر user الذي سيُستخدم في الخطوة التّالية للمولّد لإرسال الطّلب الثّاني. تُكرّر العمليّة ذاتها للطلبين الآخرين ثمّ تُعرض النتائج في الصّفحة. الفائدة التي جنيناها من استخدام المولّدات هي التخلّص من تعقيد الاستدعاءات الرّاجعة نهائيًّا وتحويل شيفرة غير متزامنة وجعلها تبدو وكأنّها متزامنة. ذكرنا القليل عن مكتبات مثل co وsuspend، لكنّها باختصار تعمل بطريقة مماثلة جدًا لمثالنا الأخير:var suspend = require('suspend'),
resume = suspend.resume;
suspend(function*() {
var data = yield fs.readFile(__filename, 'utf8', resume());
console.log(data);
})();
هذه المكتبات خطوة نحو مستقبل JavaScript، الذي بدأ يتشكل مع مشروع الدّوال غير المتزامنة باستخدام الكلمتين المفتاحيتين الجديدتين
async وawait اللّتان ستتوفّران في الإصدار السّابع وتستندان في عملهما إلى أرضيّة الوعود (Promises) الّتي تتوفّر اليوم في ES6. سيكون بإمكاننا كتابة هذه الشيفرة بدل الاعتماد على المولّدات:async function displayUserProfile() {
var user = await getJSON("http://reading-website.com/users/fwz.json");
var num_books = user.books.length;
var most_recent_book_id = user.books[num_books - 1];
var user_rating = await getJSON("http://reading-website.com/users/fwz/ratings/" + most_recent_book_id + ".json");
var book = await getJSON("http://reading-website.com/books/" + most_recent_book_id + ".json");
var fragment = document.createDocumentFragment();
var h2 = document.createElement("h2");
h2.innerText = user.full_name;
var h3 = document.createElement("h2");
h2.innerText = "الكتب التي قرأها";
for (let book of books) {
let li = document.createElement("li");
li.innerText = book.title + (book.id == most_recent_book_id ? " " + user_rating : "");
fragment.appendChild(li);
}
list.appendChild(fragment);
}
في هذا المثال يجب على
getJSON أن تُعيد وعدًا Promise ليستطيع مُفسّر اللّغة انتظاره إلى أن يُحقّق (resolve) أو يُرفض (reject)، والقيمة الّتي تُحقّق تُحفظ ضمن المُتغيّر user، وأما عند رفض الوعد يُرمى خطأ (throw) يمكن تلقّيه (catch) كما في الشيفرة غير المتزامنة.مُعامِل البقيّة (Rest parameter) والناشرة (Spread)
بعد كلّ هذا الكلام المُعقّد عن الأشياء غير المتزامنة التي نريد جعلها تبدو متزامنة وما إلى ذلك، سنختم الجزء الثّاني بفكرتين بسيطتين أُضيفتا إلى ECMAScript في الإصدار السّادس وتحلّان مشكلتين شائعتين في كثير من اللّغات البرمجيّة:
أمّا الأولى فهي الحاجة إلى تنفيذ نصّ برمجيّ ضمن دالة على عدد غير معروف من المُعاملات، فلنفترض أنّ لدينا دالة تجمع عددين:
function add(n1, n2) {
return n1 + n2;
}
add(1)
// 1
add(1, 2)
// 3
ونظرًا لكوننا مبرمجين أذكياء فقد قرّرنا جعل الدّالة تقبل أي عددين أو ثلاثة أو أكثر... لنجعلها تقبل عددًا لا نهائيًّا من الأعداد؛ في الإصدار الحالي سنلجأ إلى استخدام الكائن الخاصّ
arguments المتوفّر ضمن نطاق كلّ دالّة تلقائيًا:function add() {
return [].reduce.call(arguments, function(memo, n) { return memo + n; });
}
add(1)
// 1
add(1, 2)
// 3
add(1, 2, 3)
// 6
حسنًا لقد اضطررنا إلى "استعارة" دالة الاختزال من مصفوفة فارغة لتطبيقها على الكائن الخاص
arguments الذي يُعتبر "شبيه مصفوفة" ولا يملك ما تمتلكه المصفوفة من دوالّ، لماذا لا يمكننا كتابة هذا فحسب:function add(...numbers) {
return numbers.reduce(function(memo, n) { return memo + n; });
}
add(1)
// 1
add(1, 2)
// 3
add(1, 2, 3)
// 6
وأمّا الفكرة الثّانية فهي تكاد تكون عكس السّابقة، فإذا كانت الأولى تجمع بقيّة المعاملات في كائن مُفرَد، فإنّ هذه "تَنشر" محتويات المصفوفة إلى عناصرها المكوّنة لها، ماذا لو لم نكن أذكياء وعجزنا عن الإتيان بدالة تجمع عددًا غير منتهٍ من الأرقام:
function addThreeNumbers(n1, n2, n3) {
return n1 + n2 + n3;
}
var myNumbers = [1, 2, 3];
addThreeNumbers(...myNumbers);
// 6
لاحظ أن صياغة النشر (Spread) تطابق تمامًا صياغة البقيّة (Rest)، والاختلاف في السّياق فقط. لاحظ أيضًا أنّ معامل البقيّة، وكما يوحي اسمه، يمكن استخدامه لتجميع ما تبقى من مُعاملات الدّالة فقط:
function addThreeOrMoreNumbers(n1, n2, ...numbers) {
return n1 + n2 + numbers.reduce(function(memo, n) { return memo + n; });
}
addThreeOrMoreNumbers(1, 2, 3);
// 6
في الجزء القادم سنتعرف بمشيئة الله على الوحدات (Modules) التي تُعتبر طريقة جديدة لتنظيم الشفرة اُستلهمَت من عالم Node.js وrequire.js، وسُنلقي نظرة على الأصناف (Classes)، المكوّن البرمجيّ الذي وجد طريقه أخيرًا إلى JavaScriptّ!
JavaScript تستطيع أن تُنظّف أطباقك أيضا##
- كما هو عليه الحال كل بضع عقود، تطفو إلى السطح لغة برمجة ما ويعدنا المتعصبون لها بأنها ستفعل لنا كل شيء، بدءًا من تطبيقات الحاسوب مرورًا بالهواتف الذكية وليس انتهاءً بالتعامل مع الجمادات من حولنا بطرق رائعة كالتحكم بطائرة بلا طيّار باستخدام قبضة Xbox 360! لم يكن هذا الحماس يومًا أشدّ منه مع JavaScript، والأمر يعود لعدّة أسباب:
- كونها تعمل في المتصفح جعلها عابرة للمنصات، فكل حاسوب وكلّ هاتف ذكي (أو حتى متوسّط الذكاء!) اليوم يأتي مزوّدًا بمتصفّح قادر على تشغيل JavaScript،
- وبما أنّها لغة الويب الوحيدة التي يمكن استعمالها لبرمجة المواقع من جهة المتصفّح، فلك أن تتخيّل عدد مطوّري الويب الذي يتقنونها حول العالم!
- كذلك كون JavaScript بطبيعتها لغة فائقة المرونة لدرجة أن كل شيء فيها هو في الحقيقة كائن
Objectحتى الدّوال (functions)! ولا يوجد شيء اسمه أصناف (classes) بالمعنى التّقليديّ، وإنما توجد وراثة أنموذجية (Prototypal inheritance) فكل كائن يستطيع أن يرث أي كائن آخر، ولا أنواع محدّدة للكائنات، فما تفرضه في البداية كسلسلة نصيّة يمكنك أن تغيّره فيما بعد ليصبح رقمًا، وبإمكانك توسعة الأنماط البدئية. هذه المرونة التي يألفها من يبتدئ البرمجة بـJavaScript تجعله يُصاب بصدمة عندما ينتقل إلى لغة أخرى تفرض عليه قيودًا في التصريح عن الأنواع ووراثة الأصناف... - الأمر الثالث الذي يجعل JavaScript متفوّقة هو التحسّن الممتاز في أدائها الذي لا يبدو أنه سيتوقّف عند حدّ ما قريبًا، منذ بضع سنوات عندما ظهر Google Chrome مع محرّك JavaScript الجديد V8 والذي اعتمد على JIT compilation بدأت ثورة في عالم التطوير للويب جعلت JavaScript موضع اهتمام وأخذ المطوّرون ينظرون في إمكانيّة استعمالها في تطوير "تطبيقات ويب" بدل "مواقع ويب"، ثم توسّع الأمر مع ظهور Node.js التي قامت على محرّك V8 ذاته لتصبح JavaScript مواطنًا من الدّرجة الأولى على الخواديم مثلها مثل Ruby on Rails وPHP. الأمور ليست ورديّة تمامًا لكنك تستطيع استيعاب سرعة التطوّر الذي تشهده JavaScript وخاصّة أنّه أصبح لدينا أنظمة تشغيل ليست سوى متصفّح ويب في حقيقتها (Chrome OS وFirefox OS). بإمكانك استضافة تطبيقات Node.js مجّانًا على منصّة Heroku أو OpenShift من RedHat مثلها مثل تطبيقات Java وPython وRuby.
باختصار، تحوّلت JavaScript إلى لغة عامّة الأغراض (general-purpose) بعد أن كانت تستخدم بشكل بسيط لإضفاء القليل من التأثيرات السخيفة (Blink! Blink!) على صفحات الويب.
الآن أريد أن أوضّح شيئًا، في الحقيقة أنا لست مُبرمجًا مُختصًّا، وJavaScript هي اللغة الوحيدة التي أزعم أنّني متوسّط إلى خبير بها، ومنذ عام أو أكثر لم أكتب تقريبًا أي شيء بلغة أخرى (بالطّبع CoffeeScript لا تُعتبر لغة مستقلّة، هي فقط لغة تُحوّل إلى JavaScript ومهمّتها تبسيط الكتابة)، مع أنّني بدأت تعلّم البرمجة مع PHP، إلا أنّني كرهت كل إشارات الدولار تلك (
$variable) وعدم انسجام الواجهات البرمجيّة فيها. لا شيء يمنعك من استخدام PHP، وفي الحقيقة إطار العمل Laravel ممتاز ومنسّق بشكل جيّد، لكنّ ما يعيبها هو أنها تحاول أن تفعل كلّ شيء من الوصول لنظام الملفّات إلى دوال للتعامل مع مُعاملات طلبات HTTP إلخ... وهذا بالضّبط ما تحاول بيئات البرمجة الجديدة أن تتجنّبه، ففي Node.js، وعلى الرّغم من أنّه باستطاعتك أن تصل إلى نظام الملفّات وأن تُنشئ خادمًا يستمع إلى الطلبات على أحد المنافذ؛ إلّا أنّ كلّ شيء مُنظّم في وحدات (modules) مستقلّة وعليك أن تُصرح علانيًّة برغبتك باستعمال وحدة نظام الملفّات مثلاً، وكذلك الأمر بالنسبة للوحدات التي يكتبها مبرمجون آخرون. نظام الوحدات هذا والتصريح عنها ضمن ملفّ وعدم تلويث نطاق الأسماء العامّ (Global scope) هي بعضٌ من الأشياء التي نفّذها مُطوّرو Node.js على وجه صحيح، وأزعم أنه واحد من الأشياء التي جعلت JavaScript تُؤخذ على محمل الجدّ. هناك الكثير من المحاولات لتقليد هذا النظام بعد أن أثبت تفوّقه، انظر مثلاً إلى Composer بالنسبة لـPHP، وPip مع virtualenv في Python؛ لكنّ محاولة إدخال هذه الأنظمة على لغات ناضجة لا تبدو موفّقة كثيرًا، وأما في لغة Go فتعتبر الحُزم شيئًا من أساس اللغة. أيًّا يكن، لقد وصلنا إلى مرحلة يمكن بها إنجاز أيّة تطبيق بأيّة لغة، ويبقى الفارق هو التنظيم والسرعة والأمان وأنماط التّصميم المُتّبعة، وأهمّ من ذلك كلّه المجتمع الّذي يوفّر الدّعم والمساعدة للمبتدئين (في النّقطة الأخيرة لا شكّ أن JavaScript متفوّقة على كلّ اللّغات).لكن دعونا لا نُهين قدرتنا العقلية ونتجاهل وجود لغات برمجة أخرى فقط لأنّنا ألِفنا لغة برمجة واحدة، مهما كانت محبوبة! هناك أشياء في JavaScript لا يمكن التّغاضي عنها ولا يُمكن في أحسن الأحوال أن نعتبرها مزايا:
- فهي أوّلاً بطيئة رغم تحسّن أدائها بأضعاف ما كانت عليه منذ سنوات، وما تزال أبطأ بكثير من لغات أخرى. هناك من يجادل (وأنا أؤيّد هذا الرأي) أن السّرعة ليست كلّ شيء، فيكفي لتطبيقات الويب أن تكون سريعة بما يكفي، وليس عليها أن تكون خارقة السرعة، الفارق بين كتابة تطبيق بسيط يؤدي مهمّة محدّدة بـJavaScript وتوزيعه ليعمل على كلّ منصّات الهواتف الذكية أمر يستحق التضحية بالقليل (والقليل فقط، أي إلى حدّ معقول) من السّرعة في مقابل كتابة تطبيق منفصل بـJava (لن تتخيّل عدد السّطور المُرعب الذي تحتاجه!) وآخر بـObjective C (أو Swift) وآخر بلغة ما لـWindows Phone (إن كان هناك من يُطوّر لهذا النظام! ونعم أنا جاهل به لدرجة أنّني لا أعرف شيئًا عن اللّغة التي تُستخدم لتطوير تطبيقاته!). لكن بعد تجاوز هذا الحد المعقول من التضحية، يجدر بك أن تُعيد النظر في صلاحية هذه اللّغة إذا ما أردت تطبيقًا ينفّذ مهمّة تتطلّب سرعة فائقة، دعك من أنّ هناك تطبيقات تحتاج إلى التعامل مع النظام بطريقة لا توفّرها بيئة تطبيقات الويب على هذه الأجهزة.
- وثانيًا JavaScript هي عالم من الفوضى إن تغاضينا عن التنظيم الممتاز في Node.js ونظرنا إلى بيئة المتصفّحات... كم مرّة عانى مطوّرو الويب من عدم التوافق... المتصفّح الفلاني يوفّر الميزة الفلانية... رائع! هذا بالضبط ما أحتاجه! لكن للأسف المتصفح الآخر لا يوفّرها، وستحتاج إلى مكتبة بديلة (polyfill) لسدّ هذا الفراغ، حسنًا سأضيف هذا الـpolyfill وسيمكنني التطوير لكل المتصفّحات... نعم، باستثناء أنّ هذا الـpolyfill يسدّ الفراغ بنسخة قديمة من معيار هذه الميزة التي تحتاجها - فكما تعرف أعضاء منظّمة W3C لا يتوقّفون عن تغيير الواجهات البرمجيّة للأشياء التجريبية في المتصفّحات... ألم نُحذّرك من استخدام هذه الميزة غير المُستقرّة؟ كان يجدر بك أن تبحث عن حلّ بديل!... وهذا هو بالضّبط السّبب الذي يجعلني أكره التطوير للواجهات (front-end development) ولهذا قرّرت الاعتزال في عالم Node.js والتطوير للنهاية الخلفيّة (backend)! لا شيء من أحلامك الورديّة يتحقّق بسهولة في عالم المتصفّحات! أعرف أن الأمور في تحسّن دائم، وهناك الكثير من الأشياء الرائعة القادمة... مثل
applicationCacheوindexedDbوObject.observeوWeb Components وHTML Imports... لكنّ المشكلة أنها جميعها ليست مستقرّة أو غير مُتبنّاة في كلّ المتصفّحات بعد؛ ويبدو أن هذه الفوضى لن تنتهي يومًا، ولا حلّ لها سوى المزيد من فوضى المكتبات البديلة، هل قلت يومًا إنّ الويب هو مستقبل التطوير الموحّد لكلّ المنصّات؟ لقد كنت ساذجًا! - هناك الكثير من عيوب التصميم في JavaScript، بعضها يعود لكونها لغة صُمّمت لإنجاز مهام بسيطة وعلى عجَلَ (في الحقيقة Brendan Eich صمّمها خلال 10 أيام لمتصفّح Netscape)، فمثلاً لا يُمكنك إنشاء خيوط (threads) لأنّها تعمل ضمن خيط واحد (single-threaded)، هناك الكثير من الحلول الالتفافيّة (workarounds) في Node.js والمتصفّحات تعدنا بحل محدود الفعاليّة (Web Workers) لكنّ إيّاك أن تُخبر مُبرمج Java بأنّه لا يُمكنك إنشاء threads في JavaScript ثمّ تدعي أنّها لغة قويّة! أيضًا لا تستغرب إن قضيت ساعات تشرح لمطوّري اللّغات الأخرى عن حلقة الأحداث (event loop) وكيف تعمل ولماذا عليك تمرير استدعاءات راجعة (callbacks) عندما تُنفّذ طلبات
XMLHttpRequestوما هو جحيم الاستدعاءات (callback hell) ولماذا ظهرت بدائل عنها مثل الوعود (Promise) التي تحوّل جحيم الاستدعاءات إلى جحيمthen!
asynctask1(function(err1, data1) {
if (err1) {
throw err1;
return;
}
asynctask2(data1, function(err2, data2) {
if (err2) {
throw err2;
return;
}
asynctask3(data2, function(err3, data3) {
// ... WELCOME TO JAVASCRIPT!
})
})
});
- أخيرًا أرجوك لا تنسى بديهيّة أن JavaScript بحدّ ذاتها لغة تُفسّر بمحرّك مكتوب بـC++ أو لغة أخرى أعرق وأقوى أداءً، النّواة Linux مكتوبة بخليط من C وC++ وبرامج تشغيل الأجهزة (drivers) غالبًا تُكتب بـAssembly. نعم، صدّقني JavaScript ليست اللّغة الوحيدة ضمن المجموعة الشّمسيّة!
ضحكت كثيرًا عندما شاهدت هذه الصورة منذ بضعة أسابيع، التي تُعبّر بالضّبط عن موضوع هذه التدوينة:
تسخر هذه الصّورة بشدّة من تعصّب بعض مبرمجي JavaScript الذي يدعوهم إلى الظنّ بأنّ على كل تطبيق جديد في الكرة الأرضية أن يستخدم JavaScript من اليوم فصاعدًا، وأنّ لغات أخرى ستصبح طيّ النسيان ولا يستخدمها إلّا المبرمجون القدامى الذين عفى عليهم الدهر؛ الأمر ليس مقتصرًا على متعصّبي JavaScript وحدهم، فلكلّ لغة برمجة أنصارها ومتعصّبوها، لكن JavaScript بالذّات هي أكثر اللغات التي تترافق بهذه الظاهرة، للأسباب التي ذكرتها سابقًا.
3/تصميم النّصوص البرمجيّة: تنظيم JavaScript##
التّصميم الجيّد للمُنتجات يعني الاعتناء بالنواحي المهمّة للمنتج وإيلاءها اهتمامًا أكبر لتكونَ النتيجة واجهة استخدام جميلة ومُفيدة ومفهومة، لكن إياك أن تظنّ أنّ التصميم يقع على عاتق المُصمّمين فقط!
التصميم مطلوب في النصوص البرمجية، ليس فقط في النصوص البرمجية المكتوبة لبناء الواجهات المرئيّة للمستخدم — بل المقصود تصميم النّصوص البرمجيّة ذاتها.
النّصوص المُصمّمة بإتقان أسهل صيانةً وإمكانيّة تطويرها والبناء عليها أكبر، ممّا يجعل المُطوّرين أكثر كفاءة، لأنّها تسمح بتوجيه الاهتمام والجهد نحو بناء مُنتجات أفضل، والنتيجة شعورٌ عامّ بالراحة - راحة المطوّرين والمستخدمين وكلّ من يهمّه الأمر!
لتصميم النّصوص البرمجيّة نواحٍ ثلاث عامّة غير مرتبطة بلغة برمجة مُعيّنة:
- بنية النظام: المُخطّط العامّ لمجموعة ملفّات المشروع، والقواعد الّتي تُحدد كيف تتواصل مكوّناته المختلفة فيما بينها، سواء كانت نماذج البيانات أم واجهات العرض أم المُتحكّمات.
- إمكانية الصيانة: إلى أي حدّ يمكن تحسين النصّ البرمجيّ والبناء عليه؟
- إعادة الاستخدام: هل يمكن إعادة استخدام مكوّنات المشروع؟ هل يسهُل تخصيص أحد هذه المكوّنات لإعادة استخدامه؟
التصميم المُتقن للنّصوص البرمجية في اللّغات المرنة مثل JavaScript يفرض على المطوّر إلزام نفسه بمجموعة من القواعد، لأنّ بيئة JavaScript المُتسامحة قد تسمح للمشروع أن يعمل وإن كانت أجزاؤه مبعثرة هنا وهناك. لذلك فإن تأسيس بنية المشروع والالتزام بها منذ البداية يضمنان انسجامه حتّى تمامه.
نمط الوحدات (module pattern) هو إحدى الطّرق المُجرّبة والّتي أثبتت فائدتها في تصميم البرمجيّات، إذ تتلاءم بنيتها القابلة للتوسيع مع ما يتطلّبه بناء مشروع برمجيّ متماسك وسهل الصّيانة. أعتمدُ هذا النّمط في بناء إضافات jQuery لأنّه يسمح بإعادة استخدام الوحدات وضبطها من خلال مجموعة من الخيارات عبر واجهة برمجيّة مُتقنة التّصميم.
سنستعرض فيما يلي كيف يُكتبُ نصّ برمجيّ موزّع في مكوّنات يمكن استخدامها في مشاريع أخرى لاحقة.
نمط الوحدات
تكثر أنماط التّصميم، وتكثر معها مصادر تعلّمها. كتب Addy Osmani كتابًا ممتازًا (ومجّانيًّا!) عن أنماط التّصميم في JavaScript، أنصح كلّ المطوّرين من كلّ المستويات بالاطّلاع عليه.
يٌسهّل نمط الوحدات تنظيم المشروع ويضمن بقاءه نظيفًا ومرتّبًا، والوحدة التي نقصدها ليست سوى كائن JavaScript حرفيّ تقليديّ (object literal) يتضمّن وظائف (methods) وخصائص (properties)، وهذه البنية البسيطة هي أفضل ما في نمط الوحدات لأنّها تسمح حتى لغير المتمرّسين ممّن يطّلعون على النّصّ البرمجيّ أن يقرؤوه ويفهموا بسرعة كيف يعمل.
في التّطبيقات التي تتبنّى هذا النّمط، يكون لكلّ مكوّن فيه وحدة منفصلة تتبع له؛ فمثلاً، يمكن لحقل نصّ في الواجهة المرئيّة يسمح بالإكمال التلقائيّ للكلمات أن يُبنى من وحدتين، الأولى للحقل ذاته، والأخرى لقائمة الكلمات المُقترحَة. تتواصل الوحدتان فيما بينهما دون أن يتداخل نصّاهما البرمجيّان معًا.
هذا العزل بين المكوّنات هو ما يجعلها مناسبة لإقامة بنية متماسكة للمشاريع، حيث تكون العلاقات بين أجزاء التطبيق محدّدة بدقّة؛ ويتولّى حقل النّصّ ما يرتبط به ضمن وحدته، ولا تُترك هذه الأمور مبعثرة بين الملفّات — لنحصل بالنّتيجة على نصّ برمجيّ واضح.
توفّر البنية القائمة على الوحدات فائدة أخرى، إذ تسمح طبيعتها بتسهيل صيانة المشروع، وذلك بتطوير كلّ وحدة بصورة منفصلة دون أن تتأثّر بقيّة أجزاء التّطبيق.
استخدمت نمط الوحدات لإنشاء إضافة jPanelMenu، وهي إضافة لـjQuery تسمح بإنشاء قائمة للموقع خارج الشاشة (off-canvas menu). سأستخدم هذه الإضافة كمثال عن بناء وحدة وفق هذا النمط.
بناء الوحدة
بدايةً سأفرض ثلاث وظائف وخاصّة واحدة تُستخدم جميعها للتفاعل مع القائمة.
var jpm = {
animated: true,
openMenu: function( ) {
…
this.setMenuStyle( );
},
closeMenu: function( ) {
…
this.setMenuStyle( );
},
setMenuStyle: function( ) { … }
};
الفكرة هنا هي تجزئة النصّ إلى أصغر أجزاء مُفيدة ويمكن إعادة استخدامها. كان بإمكاننا كتابة وظيفة واحدة
toggleMenu( ) لتبديل وضع القائمة بين الظهور والخفاء، ولكن إنشاء وظيفتين منفصلتين openMenu( ) وcloseMenu( ) يعطينا تحكّمًا أكبر ويسمح بإعادة استخدامهما ضمن الوحدة.لاحظ أنّ استدعاء وظائف الوحدة وخواصّها من ضمن الوحدة ذاتها (مثل استدعاء
setMenuStyle( )) يُسبق بالكلمة المفتاحية this، فهذه هي طريقة وصول الوحدات إلى أعضائها.هذه هي البنية الأساسيّة لكلّ وحدة. يمكنك إضافة خواص ووظائف أخرى عند الحاجة إليها، ولكنّ الوحدة ستبقى بالبساطة ذاتها. بعد إقامة الأساسات السابقة، يمكن بناء طبقة فوقها لإعادة استخدام الوحدة، وذلك من خلال إتاحة ضبط خيارات الوحدة والتحكم بها.
إضافات jQuery
قد يكون الجانب الثالث للتصميم المُتقن هو الجانب الأهمّ: إعادة الاستخدام. هذه الفقرة يرافقها تحذير؛ فعلى الرغم من وجود طرق لكتابة مكوّنات يمكن إعادة استخدامها بـJavaScript "ساده"، إلّا أنّني أفضّل بناء الوحدة على صورة إضافة jQuery عندما تكون الأمور مُعقّدة، ولهذا أسباب عدّة.
أهمّ هذه الأسباب: بناء الوحدة بصورة إضافة jQuery يوصل للمُطوّرين رسالة ضمنيّة بأنّ وحدتنا تعتمد على وجود jQuery، فهذا يجب أن يكون واضحًا منذ البداية لمن يُريد استخدامها.
يُضاف إلى السّبب السّابق أنّ النّص البرمجيّ الّذي سنكتبه سيكون مُنسجمًا مع بقيّة أجزاء المشروع المُقامة على jQuery، وهذا جيّد لاعتبارات جماليّة أولًا، ولأنّه يسمح للمطوّرين بفهم استخدام الإضافة إلى حدٍّ ما دون كثير عناء، فهي إذًا طريقة أخرى لتحسين الواجهة البرمجيّة الّتي سيتخدمها المطوّرون.
قبل البدء ببناء إضافة jQuery، تأكّد من أنّها لا تتعارض مع مكتبات JavaScript أخرى تستخدم الرّمز
$. لا تقلق فالأمر أبسط ممّا يبدو، كلّ ما عليك هو إحاطة نصّ الإضافة بما يلي:(function($) {
// نص الإضافة البرمجي هنا
})(jQuery);
بعد ذلك سنُعدُّ الإضافة ونُلقي بالوحدة الّتي كتبناها سابقًا ضمن الإضافة. إضافة jQuery ليس إلا وظيفة مُعرِّفة على الكائن
$ (كائن jQuery).(function($) {
$.jPanelMenu = function( ) {
var jpm = {
animated: true,
openMenu: function( ) {
…
this.setMenuStyle( );
},
closeMenu: function( ) {
…
this.setMenuStyle( );
},
setMenuStyle: function( ) { … }
};
};
})(jQuery);
كل ما علينا لاستخدام الإضافة استدعاء الدّالة الّتي أنشأناها للتّوّ.
var jpm = $.jPanelMenu( );
الخيارات
لا تكون الإضافة صالحةً لإعادة الاستخدام إن لم تُتِح تخصيصها وفق متطلّبات المشروع، فكلّ مشروع له أسلوبه الخاصّ وبنيته الخاصّة، وإتاحة ضبط الوحدة بالخيارات يسمح للإضافة بالتكيّف مع حاجات المشاريع المختلفة.
تزويد الوحدة بقيم مبدئية للخيارات أمرٌ مفضَّل. أسهل طريقة لفعل ذلك استخدام وظيفة jQuery
$.extend() الّتي تستقبل على الأقل مُعاملين اثنين (arguments).اجعل المُعامل الأول لهذه الوظيفة كائنًا يوفّر كل الخيارات المتاحة وقيهما المبدئيّة، واجعل المعامل الثاني الخيارات الّتي يُقدّمها المُطور الّذي يستخدم الإضافة. ستقوم الوظيفة بدمج الكائنين مع إحلال الخيارات الّتي قدّمها المّطوّر مكان مقابلاتها المبدئيّة.
(function($) {
$.jPanelMenu = function(options) {
var jpm = {
options: $.extend({
'animated': true,
'duration': 500,
'direction': 'left'
}, options),
openMenu: function( ) {
…
this.setMenuStyle( );
},
closeMenu: function( ) {
…
this.setMenuStyle( );
},
setMenuStyle: function( ) { … }
};
};
})(jQuery);
إضافةً إلى توفير القيم المبدئيّة، تشرح الخيارات نفسها بنفسها، أي يستطيع من يقرأ النّصّ البرمجيّ رؤية كلّ الخيارات المُتاحة مباشرةً.
أتِح أكبر عددٍ ممكن من الخيارات، فهذا قد يكون مُفيدًا في المستقبل، والمرونة لن تضرّ!
الواجهة البرمجيّة
الخيارات الّتي تتيحها الإضافة وسيلة رائعة لتخصيص عملها، وأمّا الواجهة البرمجيّة، فهي تتيح توسيع وظيفة الإضافة بكشف محتوى الإضافة من خصائص ووظائف ليستطيع المشروع الاستفادة منها.
مع أنّ إتاحة أكبر ما يمكن من الوحدة عبر الواجهة البرمجيّة أمرٌ حسن، إلا أنّه ليس على من يستخدمها من الخارج الوصول إلى كلّ المكوّنات الدّاخليّة. أتِح في الواجهة البرمجيّة العناصر الّتي ستُستخدم فقط.
في مثالنا، يجب على الإضافة أن تُتيح الوصول إلى وظائف فتح القائمة وإغلاقها فقط، أمّا الوظيفة الداخليّة
setMenuStyle( ) فيجب أن تُستدعى عندما تُفتح القائمة أو تغلق، ولكن ليس على الأجزاء الخارجيّة من المشروع أن تصل إليها.لكشف الواجهة البرمجيّة، أعِد كائنًا يحوي الوظائف والخصائص المرغوب كشفها في نهاية نصّ الإضافة، بإمكانك أيضًا ربط الكائنات المُعادة مع تلك المُحتواة ضمن الوحدة؛ وفي هذا التنظيم يكمن جمال نمط الوحدات.
(function($) {
$.jPanelMenu = function(options) {
var jpm = {
options: $.extend({
'animated': true,
'duration': 500,
'direction': 'left'
}, options),
openMenu: function( ) {
…
this.setMenuStyle( );
},
closeMenu: function( ) {
…
this.setMenuStyle( );
},
setMenuStyle: function( ) { … }
};
return {
open: jpm.openMenu,
close: jpm.closeMenu,
someComplexMethod: function( ) { … }
};
};
})(jQuery);
ستكون خصائص الواجهة ووظائفها متاحةً عبر هذا الكائن الّذي أُعيد بعد تهيئة الإضافة.
var jpm = $.jPanelMenu({
duration: 1000,
…
});
jpm.open( );
"صقل" واجهة المُطوّرين
باتّباع الخطوط العامّة البسيطة والقليل من المفاهيم، أنشأنا لأنفسنا إضافة صالحةً لإعادة الاستخدام والتّوسعة، وهذا سيُسهّل عملنا كثيرًا، وككلّ ما نُنجزه تبقى التّجربة العامل الحاسم قبل اعتماد هذا النّمط على مستوى فريقك، وبما يُناسب سياق عملك.
كلّ ما وجدت نفسي أكتب شيئًا يمكن إعادة استخدامه لاحقًا، بادرتُ لفصله في إضافة jQuery مبنيّة كوحدة. أفضل ما في هذا الأسلوب أنّه يجبرك على استخدام النّصّ الّذي تكتبه وتجربته، وعندما تستخدم شيئًا وأنت تُنشئه، يكون تحديد مواضع القوّة ومواضع الضّعف أسرع، الأمر الّذي يسمح لك بتغيير خطّة العمل باكرًا.
هذه العمليّة تُعطي في النّهاية نصًّا برمجيًّا مُجرّبًا وجاهزًا لإتاحته كمشروع مفتوح المصدر يستقبل المساهمات، وقد قمت بالفعل بنشر إضافاتي (الّتي أعتبرها شبه ممتازة) على GitHub.
وحتّى إن لم ترغب بنشر ما تكتبه للعموم، فإنّ تصميم النّصّ البرمجيّ أمر شديد الأهمّيّة، وستشكر نفسك بسببه في المستقبل!
كتابة JavaScript يسهل اختبارها##
مررنا جميعًا بموقف كهذا: بضع سطور JavaScript كُتبت لتؤدّي وظيفة صغيرة ثمّ تضخمت مع الوقت لتبلغ بضع عشرات من السطور، ثم مئات السطور... أصبحت تقبل معاملات أكثر، وتختبر شروطًا أكثر، ثم أتى ذلك اليوم: تقرير بعلّة، التطبيق لا يعمل كما ينبغي! وحينئذ يكون فكّ غموض هذه السطور المتشابكة مسؤوليّتنا.
مع تزايد الأعباء الّتي نُلقيها على مشروعنا من جهة المتصفّح، يتّضح أمامنا أمران: أوّلهما: تجربة التطبيق بالنقر على الأزرار والروابط سريعًا ليس حلًّا عمليًّا، بل لا بدّ من الاختبارات المُؤتمتة حتّى نثق باستقرار مشروعنا؛ وثانيهما: علينا (غالبًا) أن نُغيّر كيفيّة كتابة برامجنا بما يُتيح لنا كتابة الاختبارات.
هل أنا جادّة؟ نعم! لأنّنا وإن علمنا فائدة الاختبارات المؤتمتة، فإنّ أكثرنا اليوم لا يكتبون سوى اختبارات التكامل (integration tests). لا يُقلّل هذا من أهمّيّة اختبارات التكامل، فهي تُركّز على كيفيّة عمل أجزاء التّطبيق سويّةً، إلّا أنّها لا تستطيع إخبارنا إن كانت الوحدات المنفصلة لتطبيقنا تعمل كما ينبغي لها.
هنا تبرز ضرورة اختبارات الوحدات (unit tests)، وستكون كتابة اختبارات الوحدات صعبة جدًّا إن لم يكن نصّ JavaScript الّذي كتبناه ملائمًا للاختبار.
ما الفرق بين اختبارات الوحدات واختبارات التكامل؟
كتابة اختبارات التكامل أمر بسيط عادةً: كل ما نفعله كتابة برنامج بسيط يصف كيف يتفاعل المُستخدم مع تطبيقنا، وما يجب أن يتوقّعه المستخدم عندئذٍ. إحدى برامج أتمتة المُتصفّحات الشّهيرة Selenium. وفي Ruby يُسهل Capybara التّعامل مع Selenium، وتتوفّر أدوات مشابهة كثيرة للغات الأخرى.
فيما يلي اختبار تكامل لجزء من تطبيق بحث:
def test_search
fill_in('q', :with => 'cat')
find('.btn').click
assert( find('#results li').has_content?('cat'), 'Search results are shown' )
assert( page.has_no_selector?('#results li.no-results'), 'No results is not shown' )
end
إذن فاختبار التكامل يهتمّ بتفاعل المستخدم مع التّطبيق، أمّا اختبار الوحدات فتركيزه ضيّق ويقتصر على جزء صغير من البرنامج:
عندما أستدعي دالّةً (function) بمُدخلات معيّنة، فما المُخرجات المُتوقّعة؟
يصبح إخضاع التّطبيقات المكتوبة بأسلوب إجرائي (procedural) تقليديّ لاختبار الوحدات أمرًا شديد الصعوبة، وكذلك صيانتها وتنقيحها والبناء عليها. ولكن عندما نكتب نصوص برامجنا وفي ذهننا الحاجة لإجراء اختبارات الوحدات في المستقبل، فسنجد أنّ كتابة هذه الاختبارات أمر يسير، وسينعكس ذلك بالإيجاب على نصوصنا البرمجية كذلك.
لنفهم ما أتحدّث عنه، دعنا نُلقِ نظرةً على تطبيق بحث بسيط:
عندما يكتب المستخدم عبارة للبحث عنها، يُرسل التّطبيق طلب
XMLHttpRequest للخادوم يطلب منه البيانات المُوافقة. يُجيبه الخادوم بالبيانات، مُصاغاة بـJSON، ليتلقّاها التطبيق ويعرضها في الصّفحة مُستخدمًا أدوات القولبة المُتوفّرة للمتصفّحات. يستطيع المستخدم أن ينقر على إحدى نتائج البحث ليُشير "بإعجابه" بها؛ وعندما يفعل ذلك، يُضاف اسم الشّخص الّذي أعجبه إلى قائمة "الإعجابات" على يمين الصّفحة.يمكن كتابة هذا البرنامج بأسلوب تقليدي كما يلي:
var tmplCache = {};
function loadTemplate (name) {
if (!tmplCache[name]) {
tmplCache[name] = $.get('/templates/' + name);
}
return tmplCache[name];
}
$(function () {
var resultsList = $('#results');
var liked = $('#liked');
var pending = false;
$('#searchForm').on('submit', function (e) {
e.preventDefault();
if (pending) { return; }
var form = $(this);
var query = $.trim( form.find('input[name="q"]').val() );
if (!query) { return; }
pending = true;
$.ajax('/data/search.json', {
data : { q: query },
dataType : 'json',
success : function (data) {
loadTemplate('people-detailed.tmpl').then(function (t) {
var tmpl = _.template(t);
resultsList.html( tmpl({ people : data.results }) );
pending = false;
});
}
});
$(''
, {
'class' : 'pending',
html : 'Searching …'
}).appendTo( resultsList.empty() );
});
resultsList.on('click', '.like', function (e) {
e.preventDefault();
var name = $(this).closest('li').find('h2').text();
liked.find('.no-results').remove();
$(''
, { text: name }).appendTo(liked);
});
});
يحلو لصديقنا Adam Sontag أن يُسمّي هذا الأسلوب "اختر مغامرتك الخاصّة"، ويحقّ له ذلك! ففي أيّ سطر ممّا سبق، قد نتعامل مع كيفيّة عرض البيانات، أو مع البيانات ذاتها، أو مع تفاعل المستخدم مع الصّفحة، أو مع حالة التّطبيق في لحظة معيّنة! من يدري!؟ هذا الأسلوب لا يجعل كتابة اختبارات التكامل صعبًا، ولكنّ اختبار المهامّ المختلفة للبرنامج تصبح معه غايةً في التعقيد.
كيف ذلك؟ لنرَ:
- غياب كامل لبنية واضحة؛ فكلّ شيء يحدث ضمن
$(document).ready()ثمّ في دوالّ مجهولة لا يمكن اختبارها لأنّها مُغلّفة ضمن الدّالة السّابقة ولا يمكن الوصول إليها. - دوالّ معقدة؛ فعندما تتجاوز دالّة مُفردة 10 سطور، كما في دالّة إرسال النّموذج، فهذا يعني غالبًا أنّها تفعل أشياء أكثر مما ينبغي لها أن تفعل!
- حالة (state) مخفيّة أو مُشتركة؛ كالمتغيّر
pendingالمُعرّف ضمن دالّة مُغلقة (closure)، لا سبيل لاختبار قيمته والتأكّد من كونها عُينت إلى قيمة صحيحة في لحظة ما. - اعتماد شديد للأجزاء بعضها على بعض؛ فمثلاً: لا تحتاج دالّة نجاح طلب
$.ajaxوصولًا مباشرًا إلى عناصر الصّفحة.
تنظيم النّصّ البرمجيّ
الخطوة الأولى لحلّ المشكلة السابقة هي إزالة هذا التّشابك، وفصل المهامّ السابقة إلى عدّة أجزاء:
- العرض والتفاعل
- إدارة البيانات وتخزينها
- حالة التطبيق العامّة
- ربط الأجزاء السّابقة مع بعضها لتعمل سويّةً!
في الأسلوب التّقليديّ الّذي بيّناه سابقًا، تتشابك هذه المهمّات الأربع، ففي سطرٍ نتعامل مع العرض، وبعده ببضع سطور نتواصل مع الخادوم.
يمكننا كتابة اختبار تكامل للبرنامج السابق بلا شكّ (بل ينبغي أن نفعل!)، ولكن كتابة اختبارات الوحدات أمرٌ صعب. في اختبارات التكامل، يمكننا افتراض عبارات من نحو "عندما يبحث المستخدم عن شيءٍ ما، يُتوقّع أن يرى النّتائج المُناسبة"، ولكن لا يمكننا التعمّق في التّفاصيل، وإن حدثت مشكلةٌ ما، ينبغي علينا تحديد موقع المشكلة بالضّبط، ولن تساعدنا اختبارات التكامل في ذلك.
ولكنْ، إن أعدنا التفكير في كيفيّة كتابة نصّ البرنامج يمكننا كتابة اختبارات للوحدات تُسهِّل لنا تحديد موقع المشكلة، وتسمح بتسهيل صيانة النّصّ النهائي للبرنامج وتحسينه واستخدامه ثانيةً.
سيتَّبع برنامجنا الجديد المبادئ التّالية:
- مثّل كلّ مهمّة ضمن كائن مُنفصل يقع تنصيفه في واحدة من المناطق الأربعة الّتي سبق ذكرها، دون أن يكون هذا الكائن في حاجة لمعرفة وجود الكائنات الأخرى. هذا يُجنّنبا تشابك النّص البرمجيّ.
- ادعم إمكانية الضّبط، بدلًا من كتابة قيم ثابتة. هذا سيُجنّبنا الحاجة لمحاكاة كامل بيئة HTML عند إجراء الاختبارات.
- أبقِ وظائف الكائنات بسيطة ومُوجزة. سيؤدّي هذا إلى نصّ برمجيّ أبسط وأسهل قراءةً.
- استخدم دوالّ مُشيّدة (constructors) لإنشاء نسخ من الكائنات، هذا يُتيح إنشاء نُسخ "نظيفة" من كلّ جزء من البرنامج لغرض الاختبار.
بدايةً، علينا أن نكتشف كيف سنفصل تطبيقنا لعدّة أجزاء. سيكون لدينا 3 أجزاء مُخصّصة للعرض والتفاعل: نموذج البحث ونتائج البحث ومساحة الإعجابات.
سيكون لدينا أيضًا جزء مُخصّص لجلب البيانات من الخادوم، وجزء آخر يربط بين تلك الأجزاء كلها.
لنبدأ بالنّظر في واحد من أبسط أجزاء التطبيق: مساحة الإعجابات. في الإصدار السّابق من برنامجنا، كان النّصّ التّالي مسؤولًا عن تحديث هذه المساحة:
var liked = $('#liked');
var resultsList = $('#results');
// ...
resultsList.on('click', '.like', function (e) {
e.preventDefault();
var name = $(this).closest('li').find('h2').text();
liked.find( '.no-results' ).remove();
$(''
, { text: name }).appendTo(liked);
});
لاحظ كيف يتداخل جزء نتائج البحث في جزء مساحة الإعجابات وكيف يحتاج أن يعلم الكثير عن عناصره في الصّفحة. إن أردنا كتابة هذا الجزء بأسلوب أسهل اختبارًا، فعلينا إنشاء كائن لمساحة الإعجابات مسؤولٍ عن تعديل عناصر الصّفحة المُتعلّقة بقائمة الإعجابات:
var Likes = function (el) {
this.el = $(el);
return this;
};
Likes.prototype.add = function (name) {
this.el.find('.no-results').remove();
$(''
, { text: name }).appendTo(this.el);
};
يوفّر هذا النّصّ دالّة تشييد (constructor function) تُعطي نسخة جديدة من مساحة الإعجابات. للنسخة المُنشأة وظيفة
.add() يمكن استخدامها لإضافة نتائج جديدة. ولإثبات صحّة عملها، يمكننا كتابة بضعة اختبارات:var ul;
setup(function(){
ul = $('
');
});
test('constructor', function () {
var l = new Likes(ul);
assert(l);
});
test('adding a name', function () {
var l = new Likes(ul);
l.add('Brendan Eich');
assert.equal(ul.find('li').length, 1);
assert.equal(ul.find('li').first().html(), 'Brendan Eich');
assert.equal(ul.find('li.no-results').length, 0);
});
ليس الأمر صعبًا كما ترى! استخدمنا Mocha كإطار عمل للاختبار وChai كمكتبة لإنشاء الافتراضات (assesrtions). Mocha توفّر
test وsetup، وChai توفّر assert. تتوفّر أطر عمل ومكتبات للافتراضات كثيرة، ولكنّ السابقتين ملائمتان تمامًا للمُبتدئين. عليك اختيار ما يناسبك ويناسب مشروعك. اطّلع على QUnit وIntern (الأخيرة تبدو واعدة).يبدأ برنامج الاختبا بإنشاء عنصر في الصّفحة سنستخدمه لاحتواء مساحة الإعجابات، ثمّ ينطلق اختباران: الأول للتأكّد من إمكانيّة إنشاء مساحة الإعجابات، والآخر للتأكد من أن وظيفة
.add() تعطينا النتيجة المرغوبة. بعد إنشاء هذين الاختبارين، يمكننا إعادة صياغة النصّ البرمجي لمساحة الإعجابات، مطمئنين إلى أنّ الأخطاء الّتي نرتكبها عندئذٍ ستُكتشف فورًا في الاختبار.يبدو تطبيقنا الآن كما يلي:
var liked = new Likes('#liked');
var resultsList = $('#results');
// ...
resultsList.on('click', '.like', function (e) {
e.preventDefault();
var name = $(this).closest('li').find('h2').text();
liked.add(name);
});
جزء نتائج البحث أشدّ تعقيدًا من مساحة الإعجابات، لكن لنجرّب إعادة صياغته أيضًا. نريد أن نُنشئ وظائف للتفاعل مع نتائج البحث كما أنشأنا الوظيفة
.add() لمساحة الإعجابات. نريد طريقةً لإضافة نتائج جديدة، وكذلك طريقة "لبثّ" التّغيرات الّتي تطرأ على النّتائج لبقيّة أجزا التّطبيق (كأن يُعجب المستخدم بنتيجة مثلاً).var SearchResults = function (el) {
this.el = $(el);
this.el.on( 'click', '.btn.like', _.bind(this._handleClick, this) );
};
SearchResults.prototype.setResults = function (results) {
var templateRequest = $.get('people-detailed.tmpl');
templateRequest.then( _.bind(this._populate, this, results) );
};
SearchResults.prototype._handleClick = function (evt) {
var name = $(evt.target).closest('li.result').attr('data-name');
$(document).trigger('like', [ name ]);
};
SearchResults.prototype._populate = function (results, tmpl) {
var html = _.template(tmpl, { people: results });
this.el.html(html);
};
الآن يمكن لنصّنا البرمجيّ القديم المسؤول عن إدارة التفاعلات بين نتائج البحث ومساحة الإعجابات أن يصبح كما يلي:
var liked = new Likes('#liked');
var resultsList = new SearchResults('#results');
// ...
$(document).on('like', function (evt, name) {
liked.add(name);
})
وهذا أبسط وأقل تشابكًا، لأنّنا نستخدم
document كممرّ عامّ للرّسائل المُتبادلة بين الأجزاء بحيث لا يضطرّ أحدها إلى أن يعلم وجود الآخر. (ملاحظة: في تطبيق حقيقيّ، ربّما يُستخدم شيء مثل Backbone أو مكتبة RSVP لإدارة الأحداث، استخدمنا document للتبسيط.) كذلك أخفينا كل التّفاصيل المُعفّدة (كإيجاد اسم الشّخص الّذي أُعجب المستخدم به) داخل كائن نتائج البحث، بدل أن نترك هذه التّفاصيل تُلوّث نصّ تطبيقنا. أفضل ما في الأمر هو أنّ بإمكاننا الآن كتابة اختبارات للتأكّد من عمل كائن نتائج البحث كما يجب:var ul;
var data = [ /* fake data here */ ];
setup(function () {
ul = $('
');
});
test('constructor', function () {
var sr = new SearchResults(ul);
assert(sr);
});
test('display received results', function () {
var sr = new SearchResults(ul);
sr.setResults(data);
assert.equal(ul.find('.no-results').length, 0);
assert.equal(ul.find('li.result').length, data.length);
assert.equal(
ul.find('li.result').first().attr('data-name'),
data[0].name
);
});
test('announce likes', function() {
var sr = new SearchResults(ul);
var flag;
var spy = function () {
flag = [].slice.call(arguments);
};
sr.setResults(data);
$(document).on('like', spy);
ul.find('li').first().find('.like.btn').click();
assert(flag, 'event handler called');
assert.equal(flag[1], data[0].name, 'event handler receives data' );
});
التفاعل مع الخادم جزء آخر يجب إنشاؤه، وقد احتوى النّص البرمجيّ القديم على طلب
$.ajax() فيه استدعاء راجع (callback) يتعامل مباشرةً مع عناصر الصّفحة:$.ajax('/data/search.json', {
data : { q: query },
dataType : 'json',
success : function( data ) {
loadTemplate('people-detailed.tmpl').then(function(t) {
var tmpl = _.template( t );
resultsList.html( tmpl({ people : data.results }) );
pending = false;
});
}
});
مُجدّدًا أقول: من الصعب كتابة اختبار وحدات للنّصّ السابق لأن أشياء كثيرةً تحدث في بضع سطور. يمكننا إعادة صياغة القسم المسؤول عن البيانات في صورة كائن مُنفصل:
var SearchData = function () { };
SearchData.prototype.fetch = function (query) {
var dfd;
if (!query) {
dfd = $.Deferred();
dfd.resolve([]);
return dfd.promise();
}
return $.ajax( '/data/search.json', {
data : { q: query },
dataType : 'json'
}).pipe(function( resp ) {
return resp.results;
});
};
وعندها يمكن تعديل النّصّ البرمجيّ لجلب النّتائج إلى الصّفحة:
var resultsList = new SearchResults('#results');
var searchData = new SearchData();
// ...
searchData.fetch(query).then(resultsList.setResults);
لاحظ كم بسّطنا نصّ تطبيقنا، وكيف عزلنا التّعقيدات ضمن كائن بيانات البحث، بدلًا من تركه حُرًّا ضمن نصّ تطبيقنا العامّ. قمنا أيضًا بجعل واجهة البحث قابلةً للاختبار. ولكن هناك بضعة أمور يجب النّظر فيها عندما نختبر جزءًا من برنامج يتفاعل مع الخواديم.
أوّل هذه الأمور: لا نريد أنّ نتفاعل مع الخادوم حقيقةً، ففعل ذلك يعني العودة إلى عالم اختبارات التّكامل، ولأنّنا مُطوّرون أكفاء، فقد انتهينا من كتابة تلك، صحيح؟ ما نريده الآن هو "محاكاة" التّفاعل مع الخادوم، الأمر الّذي يمكن إنجازه بمكتبة Sinon. ثانيًا: يجب أيضًا اختبار المسارات غير المُتوقّعة للتطبيق، كعبارة بحث فارغة مثلًا.
class="lang-javascript">test('constructor', function () {
var sd = new SearchData();
assert(sd);
});
suite('fetch', function () {
var xhr, requests;
setup(function () {
requests = [];
xhr = sinon.useFakeXMLHttpRequest();
xhr.onCreate = function (req) {
requests.push(req);
};
});
teardown(function () {
xhr.restore();
});
test('fetches from correct URL', function () {
var sd = new SearchData();
sd.fetch('cat');
assert.equal(requests[0].url, '/data/search.json?q=cat');
});
test('returns a promise', function () {
var sd = new SearchData();
var req = sd.fetch('cat');
assert.isFunction(req.then);
});
test('no request if no query', function () {
var sd = new SearchData();
var req = sd.fetch();
assert.equal(requests.length, 0);
});
test('return a promise even if no query', function () {
var sd = new SearchData();
var req = sd.fetch();
assert.isFunction( req.then );
});
test('no query promise resolves with empty array', function () {
var sd = new SearchData();
var req = sd.fetch();
var spy = sinon.spy();
req.then(spy);
assert.deepEqual(spy.args[0][0], []);
});
test('returns contents of results property of the response', function () {
var sd = new SearchData();
var req = sd.fetch('cat');
var spy = sinon.spy();
requests[0].respond(
200, { 'Content-type': 'text/json' },
JSON.stringify({ results: [ 1, 2, 3 ] })
);
req.then(spy);
assert.deepEqual(spy.args[0][0], [ 1, 2, 3 ]);
});
});
سأترك ما تبقّى من إعادة صياغة نموذج البحث لغرض الاختصار، وقد قمت بتبسيط بعض الاختبارات والنّصوص أعلاه مُجدّدًا، ويمكنك الاطّلاع على الإصدار النّهائي للتطبيق هنا إن كنت مُهتمًّا.
عندما ننتهي من كتابة تطبيقنا بأسلوب يسمح بإجراء الاختبارات، سيكون مشروعنًا أكثر نظافةً ممّا بدأنا به:
$(function() {
var pending = false;
var searchForm = new SearchForm('#searchForm');
var searchResults = new SearchResults('#results');
var likes = new Likes('#liked');
var searchData = new SearchData();
$(document).on('search', function (event, query) {
if (pending) { return; }
pending = true;
searchData.fetch(query).then(function (results) {
searchResults.setResults(results);
pending = false;
});
searchResults.pending();
});
$(document).on('like', function (evt, name) {
likes.add(name);
});
});
والأهمّ من ذلك أنّنا ننتهي ولدينا مشروعٌ أُخضع لاختبارات دقيقة، وهذا يعني أنّ بإمكاننا إعادة كتابة الأجزاء ونحن مطمئنون إلى أنّ ذلك لن يسيئ إلى عمل التطبيق. بإمكاننا أيضًا إضافة اختبارات جديدةً عندما نكتشف مشكلات جديدة، ثمّ كتابة النّصّ الّذي يجعل هذه الاختبار تنجح.
اختبار البرامج يُسهّل حياتك على المدى البعيد
من السهولة أن تنظر إلى ما سبق وتقول لنفسك: "مهلًا! أتريدين منّي كتابة برامج أطول لأداء المهمّة ذاتها؟"
عندما يتعلّق الأمر ببناء المشاريع على الإنترنت، فإنّه لا مفرّ من هذه الحقيقة: لا بدّ من وقتٍ كافٍ لتصميم منهجٍ لحلّ مشكلة. لا بدّ من تجربة الحلّ، سواءٌ كانت التّجربة يدويّة تقوم بها بنفسك في المتصفّح، أو مؤتمتة، أو أن تترك مستخدميك يجرّبوا مشروعك بعد نشره (فكرة مرعبة!). ستُغيّر نصوصك البرمجيّة، ثمّ سيستخدمها آخرون، وفي النّهاية ستبقى العلل موجودة مهما بلغ عدد الاختبارات الّتي تكتبها.
صحيحٌ أنّ الاختبارات تتطلّب وقتًا عند كتابتها أوّل مرّة، ولكنّها ستوفّر وقتك في المدى البعيد، سترضى عن نفسك عندما يفشل أحد الاختبارات الّتي كتبتها كاشفًا عن علّة قبل أن يصل النّص البرمجيّ إلى مرحلة الإنتاج، وكذلك عندما يكون لديك نظامٌ يُثبت أنّ العلل قد حلّت بالفعل قبل أن تجد طريقها إلى المُستخدمين!
أيّهما أفضل للتحريك Animations؟ جافاسكربت أم CSS؟##
هناك اعتقاد خاطئ بين العديد من مطوّري الويب أن CSS هي الطريقة الوحيدة للقيام بالتحريك Animations. برزت CSS بقوة كأكثر نظام مُدلّل خلال سنوات وحتى الآن، وتحدّث المطوّرون باستمرار حول متانتها وتوافقها مع الهواتف. CSS جيدة لكنها ليست أفضل من جافاسكربت. هناك بعض الأساطير حول CSS قادت المطوّرين بشكل استباقي لتبنّي هذا النظام (أي CSS) والتخلي عن كلٍّ من جافاسكربت والتحريك الخاصّ بها.
ما يحيّر المطوّرين هو الاستخدام الفعّال لـ CSS، فبينما يقومون بإدارة التفاعل بين العناصر من داخل CSS، يُجبرون أنفسهم أيضًا على جعل مشروعهم متوافقًا مع Internet Explorer 8 و 9، وأخيرًا، يتجنّبون التحريك الجاهز الذي لا يتوفر إلا عن طريق جافاسكربت.
يهدف هذا المقال لدحض بعض الأساطير حول CSS وكشف جميع المشكلات الأساسية التي بالكاد يتحدث الناس عنها بسبب إعجابهم بـ CSS. يهدف المقال إلى رفع معرفتك ووعيك بفوائد استخدام جافاسكربت، حتى يمكنك بسهولة التخلص من المواقف التي تثير غضبك.
لذا، دون مزيد من الإطالة دعونا نتناقش حول كل منهم بالتفصيل.
1. jQuery
دعونا نبدأ بالأساسيات، يتم الربط بين جافاسكربت ومكتبة jQuery بشكل خاطئ . فالتحريك المُصمَّم بجافاسكربت سريع ومرن، بينما المُصمَّم بـ jQuery بطيء. السبب أنه -وبرغم مواصفات jQuery القوية- إلّا أنها لم تهدف بشكل رئيسي لتنفيذ التحريك. هناك العديد من الأسباب لدعم هذا:
- لا يمكن لمكتبة jQuery ببساطة أن تتوقف عن تحطيم التنسيق (layout thrashing) نظرًا لهيكلتها البرمجية التي تخدم عددًا من الأغراض غير التحريك. هذا بشكل عام يسبب تقطّعًا في المراحل الأولى من التحريك.
- الذاكرة التي تستهلكها jQuery دومًا تُحفّز جمع القمامة (Garbage Collection) مما يؤدي إلى تجمّد التحريك. ونظرًا لـ جمع القمامة، يمكن للمرء أن يواجه تقطّعًا أثناء التحريك.
- تم تصميم jQuery لتستخدم set_interval() وليس طلب إطار الحركة (Request Animation Frame - RAF). عندما لا يكون طلب إطار الحركة موجودًا فيمكن أن ينتج عن ذلك تحريكات بعدد إطارات منخفض (جودة ودقة الحركة تعتمد على عدد الإطارات التي تظهر في الثانية، العدد المثالي هو 60 إطار في الثانية).
2. فقر التحكم بالتدوير وتحديد الموقع
من الميزات الأساسية الضرورية أثناء تنفيذ التحريك استخدام المُتحكّمات لـ: التحريك animation، التدوير rotation، وتحديد المواقع positioning. في CSS، تم تكديس جميع هذه الدوالّ في خاصية معروفة هي tranform. هذه الخاصية تُسبب مشاكل عند تحريك شيء ما بطريقة فريدة عن طريق عنصر مشترك. على سبيل المثال، إن أردت تحريك “التدوير” rotation و”التكبير” scaling بشكل منفصل، وباستخدام أزمنة مختلفة، فهذا مُمكن فقط عن طريق جافاسكربت ﻷنها تُمكّنك من استخدام حِيل متنوعة، وهذه غير مسموحة في CSS. هذه إحدى مساوئ CSS. هي جيدة من أجل مشاريع تتطلب تحريكاتٍ بسيطة وليس من أجل المشاريع التي تتطلب اندماج تصميمٍ بتحريكاتٍ كبيرة.
3. الأداء مع مكتبتيّ Velocity وGSAP
Velocity وGSAP هما المكتبتان الرائدتان والأكثر شعبية في جافاسكربت. كلاهما يعمل مع jQuery أو بدونها. عندما يتم استخدام أيّ من هاتين المكتبتين جنبًا إلى جنب مع jQuery فلا يحدث أي تدهورأو تباطؤ في الأداء لأنهما تعملان بشكل منفصل عن تحريكات jQuery الأساسية.
ويمكن أيضًا استخدام هاتين المكتبتين بسهولة عندما لا تكون مكتبة jQuery موجودة في الصفحة. وهذا يبين بوضوح أنه بدلًا من ربط جميع أنواع استدعاءات التحريك في عنصر jQuery مشترك يمكنك بشكل مباشر تمرير العنصر المراد إلى استدعاء الحركة كما هو مبين أدناه.
/* Working without jQuery */
Velocity(element, { opacity: 0.4 }, 900); // Velocity
TweenMax.to(element, 1, { opacity: 0.4 }); // GSAP
في المثال السابق، يمكنك ملاحظة أن Velocity تستخدم نفس الصيغة حتى عندما لا يتم استخدام jQuery. قامت فقط بإزاحة جميع العناصر تجاه اليمين لترك مساحة للعناصر المستهدفة.
بالمقابل، في GSAP تم استخدام تصميم كائني التوجه (عن طريق استدعاء كائن يمثّل صنف Class) جنبًا إلى جنب مع دالّة بسيطة ثابتة. بهذه الطريقة المستخدم يملك تحكم كامل بعملية الحركة.
4. معامل وحدة معالجة الرسوم (GPU)
عندما تكون وحدة معالجة رسوم GPU مُحسّنة بشكل كامل، عندها ستكون عظيمة لأداء مهام متنوعة كالتعامل مع مصفوفات التحويل transform والشفافية opacity، لهذا السبب أول ما تفعله جميع المتصفحات المتطورة هو تفريغ هذه المهام من وحدة المعالجة المركزية CPU إلى وحدة معالجة الرسوم GPU. الهدف الأساسي هو فصل جميع العناصر المتحركة إلى طبقات خاصة بها في وحدة معالجة الرسوم ﻷنه فور انتهاء النظام من إنشاء طبقات وحدة معالجة الرسوم لا يُظهر النظام أي اهتمام بتحريك البكسلات وجمعهم معًا. لهذا لا يوجد حاجة بتاتًا لحساب كل بكسل بشكل مستمر. بدلًا من ذلك يمكن تحريك بكسل واحد فوق الآخر وتوفير كثير من الوقت.
ملاحظة: لا حاجة لإعطاء كل عنصر طبقته الخاصة نظرًا لذاكرة الفيديو المحدودة في وحدة معالجة الرسوم. فإذا نفدت الذاكرة سيفسد كل شيء.
من ناحية أخرى، إذا قمتَ بتعريف التحريك في CSS عندئذ ستكون مهمة المتصفح أن يحدد طبقة وحدة معالجة الرسوم لكل عنصر وسيحصل انقسام بسبب ذلك.
مع ذلك، يمكنك عمل هذا الأمر عن طريق جافاسكربت أيضًا. كل ما تحتاجه هو تحديد التحويل transform مع خاصية 3D (تمامًا مثل translate3d() و matrix3d()) لجعل المُتصفح يعلم عن عملية إنشاء طبقات وحدة معالجة الرسوم للعنصر المستهدف. لذا، فمن الواضح أن وحدة معالجة الرسوم لا توفّر زيادة السرعة لـ CSS فقط بل لجافاسكربت أيضًا.
5. قوة تحريك جافاسكربت
لدى جافاسكربت كل الإمكانيات للفوز بمقارنة الأداء مع CSS. جافاسكربت هي أسرع. لكن إلى أي حدّ يمكن أن تكون أسرع؟ بدايةً هي مرنة بما فيه الكفاية لإنشاء عرض تحريك ثلاثيّ الأبعاد 3D ملفت للنظر، الذي يمكن أن تراه عبر WebGL. وجافاسكربت سريعة بشكلٍ كافٍ لبناء دعاية وسائط متعددة، والتي كان من الممكن تطويرها باستخدام Flash أو After Effects. وبالتأكيد بناء عالم افتراضي باستخدام جافاسكربت بمساعدة Canvas.
بقدر ما تكون مكتبات التحريك هي المعنية في هذا المقال، فإن الحديث الآن عن توثيق مكتبتيّ Transit و Velocity غير مبرر على الإطلاق. لمزيد من التوضيح لهذه النقطة، قمنا بطرح قائمة تحسينات يمكن فقط للتحريك المبنيّ بجافاسكربت أن يؤديها:
- مزامنة DOM
- التخزين المؤقت للخصائص عبر سلسلة من الاستدعاءات، للتخفيف من استعلامات DOM
- التخزين المؤقت لنسب تحويل الواحداتا
التصميم في JavaScript: الوحدات##
في سلسلة من عدة أجزاء سنناقش موضوعًا نظريًّا يُعتبر من أساسيّات هندسة البرامج، وهو أنماط التصميم (Design Patterns)، وسنعتمد لغة JavaScript في نقاشنا لتصاعد شعبيّتها ومرونتها التي تسمح لنا ببناء مشاريعنا وفق أنماط متنوّعة مما سيُسهّل علينا شرح موضوع السّلسلة
ما هي أنماط التصميم؟
عندما تبدأ بتعلّم البرمجة، فغالبًا ما يكون اهتمامك مُنصبًّا على أن يكون البرنامج قادرًا على إنجاز المهمّة الّتي تريدها قبل كل شيء، أمّا بعد أن تتطوّر مهاراتك، فسينتقل اهتمامك إلى مواضيع أكثر عمقًا، وستبدأ بطرح بعض الأسئلة على نفسك، حتّى قبل أن تبدأ بكتابة البرنامج، من هذه الأسئلة:
- كيف أبني برنامجي بحيث يسهُل تحسينه فيما بعد؟
- كيف أتأكّد أن برنامجي سيبقى يؤدّي ما يُتوقّع منه حتّى وإن قمت بتعديل أجزاء منه بعد زيادة تعقيده؟
- كيف أبني برنامجي بحيث أستطيع إعادة استخدام أجزاء منه في برامج أخرى في المستقبل؟
- كيف أجعل برنامجي يستخدم أجزاء من مشاريع أخرى كتبها مطوّرون آخرون؟
الإجابة على هذه الأسئلة هي واحدة دومًا: اختر نمط التصميم المناسب لمشروعك.
لم نُعرِّف بعدُ مفهوم نمط التّصميم، لكنّنا بدأنا نُكوّن فكرة عنه. نمط التّصميم هو وصف لطريقة مُعيّنة في حلّ مشكلة برمجيّة ما، فالعديد من المُشكلات البرمجيّة يمكن حلّها بأكثر من طريقة، ولكلّ طريقة مساوئ ومحاسن، وبعضها قد يكون أكثر مُناسبةً للمشروع الحاليّ، واختيار نمط التصميم المُناسب سيضمن استمرار تطوّر المشروع بسهولة وربّما يُفيدنا في عزل أجزاء منه لإعادة استخدامها في مشاريع أخرى بحيث لا نُضطَّر لكتابتها مرارًا.
أغلب الظنّ أنّك تستخدم واحدًا أو أكثر من أنماط التّصميم وإن لم تعرف ما هي أنماط التّصميم بمعناها النّظريّ، فإنشاء أصناف (classes) لمفاهيم مُجرّدة في اللّغات الكائنيّة التّوجّه (object-oriented) وإنشاء نُسخ عنها (instances) وتوزيع هذه الأصناف في ملفّات مستقلّة، هو في الواقع نمط من أنماط التّصميم.
يُرجى الانتباه إلى أن التّطبيق العمليّ لأنماط التّصميم يفرض على المطوّر دمج أكثر من نمط معًا وشرحنا لأحدها لا يمكن أن يخلو من استخدام لأنماط أخرى كما سيتبيّن لك بعد انتهاء السّلسلة، إذ يمكن مثلًا إنشاء وحدة (module) تُصدِّر صنفًا (class) وهذا يعني أنّنا استخدمنا نمطين اثنين (نمط الوحدات، ونمط مُشيّد الكائنات constructor) في وقت واحد.
نمط الوحدات (Module Pattern)
بغرض تبسيط الأمور، سنبدأ بتوضيح أحد أنماط التّصميم الشّائعة في JavaScript، وهو ما يُعرف بنمط الوحدات (module pattern)، والتي ازدادت شعبيّة بعد ظهور Node.js وما أحدثته من تأثير انتقل حتّى إلى أساليب بناء وتصميم المكتبات البرمجيّة الّتي تستهدف المُتصفّحات.
الوحدات هي أجزاء مُستقلّة ومعزولة من النّص البرمجيّ للمشروع توفّر مهمّة مُعيّنة، الأمر الّذي يجعل الهدف من كل وحدة واضحًا ومحدّدًا ويُجنّب المشروع الفوضى التي تنتج عن كتابة كامل النّصّ البرمجيّ في كتلة واحدة متداخلة يصعب معها تنقيحه وصيانته.
فصل الأجزاء هذا ليس الفائدة الوحيدة الّتي يُقدّمها نمط الوحدات، إذ من خلاله يمكن مُحاكاة مفهوم المكوّنات السّرّيّة (private) والعلنيّة (public) وحماية بعض محتويات الوحدة من الوصول إليها من خارجها في JavaScript، وذلك بإخفائها ضمن الوحدة والامتناع عن تصديرها الأمر الذي يجعل الوصول إليها من خارج الوحدة مستحيلًا كما سنوضّح بعد قليل.
تتوفّر في عالم JavaScript أشكال مختلفة لتصميم الوحدات، منها:
- الكائنات الحرفيّة (object literals)
- الدّوالّ المغلقة المجهولة (anonymous closures)
- وحدات CommonJS
- وحدات AMD
- وحدات ECMAScript 6
الشّكل الأول ربّما هو أبسط الأشكال وأكثرها بدائيّة، وهو يعني ببساطة إنشاء كائن باستخدام صياغة القوسين المعكوفين
{} يضمّ خصائص ووظائف متعلّقة بمهمّة واحدة لعزلها وتسهيل استخدامها:var userSettings = {
preferences: {
privacy: "strict",
language: "ar",
showEmail: false,
available: true,
},
updatePreferences: function(newPrefs) {
this.preferences = newPrefs;
}
}
بعض خبراء JavaScript لا يعتبرون هذا النّمط وحدةً حقيقيّة لبساطته الشّديدة، فمن الواضح أنّ هذا النّمط أبسط من حاجات التّطبيقات المعقّدة، فغالبًا ما يكون توزيع الوحدات على ملفّات منفصلة أمرًا مرغوبًا أثناء تطوير التّطبيقات ولهذا نحتاج إلى وسيلة لاستيراد هذه الملفّات وتصديرها بما يسمح باستخدام وحدة واقعة في ملفّ من ملفّ آخر، ولهذا الغرض طوّر مجتمع JavaScript خلال الأعوام الماضية أساليب قياسيّة اتّفق على استخدامها على الرّغم من أن اللّغة ذاتها لم تقدّم مفهوم الوحدات إلّا في الإصدار الأخير (ES6)، والذي استلهم من الأساليب السّابقة أصلًا؛ كما أنّنا قد نرغب بحماية كائن مثل
preferences في مثالنا السّابق من تعديله بصورة مباشرة.الشّكل الثّاني هو أسلوب أكثر تطوّرًا لإنشاء الوحدات، ويحتاج فهمه إلى فهم معنى الدّوال المُغلقة (closures)، فإذا كانت لدينا دالّة تُعيد عند استدعائها دالّة أخرى، فإنّنا ندعو الدّالة الأخيرة دالّة مُغلقة، ويتاح لهذه الدّالة الوصول إلى المتّغيّرات الّتي كانت مفروضة في الدّالة الأولى حتّى عندما تُستدعى من خارجها:
function addNumberToN(n) {
return function(number) {
return n + number;
}
}
var addTo2 = addNumberToN(2);
var five = addTo2(3); // 5
هذه الخاصيّة في JavaScript تسمح لنا بعزل المتّغيّرات (encapsulation) ضمن الدّالة الخارجيّة مع الاحتفاظ بإمكانيّة الوصول إليها من الدّوال والكائنات الفرعيّة، الأمر الذي يحاكي مفهوم خصوصيّة المتغيّرات في لغات البرمجة الأخرى (access modifiers).
لنفترض مثلًا أنّنا نريد أن نقوم بإنشاء عدّاد لعدد النّقرات على زرّ معيّن في تطبيقنا، ولا نريد الاحتفاظ بهذه القيمة في النّطاق العامّ لأنّ هذا قد يعرّضها للتّعارض من أسماء مُتغيّرات أخرى أو يجعلها قابلة للتّعديل من إضافات خارجيّة في المتصفّح، لهذا نقوم بإنشاء دالّة مُغلقة تُحيط بهذه القيمة:
function() {
var counter = 0;
return {
increaseCounter: function() {
counter++;
}
}
}
وبهذا نكون قد قيّدنا إمكانيّة تعديل قيمة المتغيّر بزيادته فقط، وعبر الدّالة
increaseCounter() فقط. لا يمكن استخدام الدّالّة increaseCounter() إلا بعد استدعاء الدّالة المجهولة (anonymous) الّتي تُحيط بها، ويتمّ هذا كما يلي:(function() {
var counter = 0;
return {
increaseCounter: function() {
counter++;
}
}
})()
لتُصبح الدّالة
increaseCounter() مُتاحة في النّطاق العامّ، وبهذا نكون حصلنا على شكل بدائي لفكرة "تصدير الوحدات" (module exports).لنستعرض الآن الأساليب الأخرى لإنشاء الوحدات، ولعلّ أكثر هذه الأساليب شيوعًا أسلوب CommonJS المُعتمد في Node.js، والذي يُتيح استيراد الوحدات باستخدام الدّالّة
require():var UrlMaker = require("./url-maker");
var url_maker = new UrlMaker("Hello World!");
var url = url_maker.make();
console.log(url); // hello-world;
وأمّا تصدير الوحدات لإتاحة استخدامها، فيتم بإسناد الخصائص إلى الكائن
module.exports، كما في الملفّ url-maker.js الموجود في مسار العمل الحالي:module.exports = function UrlMaker(string) {
return {
make: function() {
return string.toLowerCase().replace(/\s+/gi, "-").replace(/[!?*%$#@`]/gi, "")
}
}
}
لا يقتصر استخدام أسلوب CommonJS على بيئة Node.js، بل يمكن نقله إلى المتصفّحات باستخدام برامج مثل Browserify.
من الأساليب الشائعة لإنشاء الوحدات كذلك نمط وحدات AMD (اختصارًا لـAsynchronous Module Definition) ولعلّه يُستخدم بكثرة مع مكتبة require.js في المتصفّحات والّتي تسمح بتحميل الوحدات (الموزّعة كلّ منها على ملفّ منفصل) عند الحاجة إليها، إذ يتمّ التّصريح عن كلّ وحدة وما تعتمد عليه من وحدات أخرى وتقوم require.js بتلبية هذه المتطلبات بتحميل ملفّات الوحدات المنفصلة. في المثال التّالي، نُصرّح عن حاجة موقعنا لمكتبتي jQuery وUnderscore ووحدة أخرى قمنا بإنشائها بأنفسنا:
require(["jquery", "underscore", "user_profile"], function($, _, UserProfile) {
var user = new UserProfile();
user.firstName = $("#form input[name='first']").text();
user.lastName = $("#form input[name='last']").text();
user.username = $("#form input[name='username']").text();
// ...
})
ويتمّ التّصريح عن الوحدة
user_profile في ملفّ منفصل باسم موافق:define(function() {
function Profile() {
/* ... */
}
return Profile;
})
يوفّر الإصدار الجديد من JavaScript (ES6) دعمًا أساسيًّا لتعريف الوحدات واستيرادها وتصديرها، وسنتطرّق له بالتّفصيل في الجزء القادم من سلسلة التّعريف بميزات ES6 الجديدة، لكنّ لا بأس من أن نتعرّف عليه سريعًا:
import { jQuery as $ } from "/jquery.js";
import { Profile as UserProfile } from "/user.js";
var user = new UserProfile();
user.firstName = $("#form input[name='first']").text();
// ...
وتُنشئ الوحدة وتُصدَّر في الملفّ
user.js:class Profile {
constructor() {
// ...
}
validate() {
// ...
}
}
export { Profile };
فوائد استخدام الوحدات
لنُلخِّص إذن فوائد الوحدات:
- تنظيم النّصّ البرمجيّ للمشاريع الضّخمة بحيث يسهل فهم بنية البرنامج وتنقيحه ومتابعة صيانته في المستقبل.
- إدارة المُتطلّبات (dependencies): توضيح العلاقة بين مكوّنات المشروع، بحيث نستطيع إدارة ما تتطلّبه كلّ وحدة وتحميل هذه المتطلّبات آليًّا بالتّرتيب الصّحيح بدل الحاجة إلى التّصريح عن روابط المكتبات الخارجيّة في الصّفحة الرئيسيّة للموقع وإعادة ترتيبها كلّ ما تطلّب الأمر إضافة مكتبة جديدة تعتمد على أخرى.
- العزل (encapsulation): حماية أجزاء من المشروع من العبث بها سهوًا أو من نصوص برمجيّة خارجيّة، وذلك بعزلها ضمن نطاق فرعيّ خلافًا لتركها في النّطاق العامّ. ففي الحالة الطّبيعيّة تكون متغّيرات JavaScript المفروضة في النّطاق العامّ مُتاحة لأي دالّة، وأما عند فرض هذه المُتغيّرات ضمن الوحدات، فإنّ الوصول إليها يُصبح محدودًا بما هو داخل الوحدة ذاتها، ويتمّ ذلك باستغلال مفهوم النّطاقات (scopes) في JavaScript وإحاطة هذه المتغيّرات بدالّة تُغلّفها بنطاق فرعيّ ثمّ إعادة كائن يحوي فقط الخصائص الّتي نريد إتاحتها للعموم.
أنماط التصميم في JavaScript: نمط المُشيِّد (Constructor)##
في سلسلة من عدة أجزاء سنناقش موضوعًا نظريًّا يُعتبر من أساسيّات هندسة البرامج، وهو أنماط التصميم (Design Patterns)، وسنعتمد لغة JavaScript في نقاشنا لتصاعد شعبيّتها ومرونتها التي تسمح لنا ببناء مشاريعنا وفق أنماط متنوّعة مما سيُسهّل علينا شرح موضوع السّلسلة
نمط المُشيِّد (Constructor)
يشيع استخدام المُشيّدات في اللغات الكائنيّة التّوجّه، حيث تُستخدم لإنشاء نُسخ (instances) من الأصناف (classes)، ومع أنّ JavaScript ليست لغةً كائنيّة التّوجّه بالمعنى التّقليديّ، إلّا أنّها تسمح بإنشاء نُسخ عن كائنات باستخدام بالمُشيّدات، ويمكن لأيّ دالّة أن تُستخدم كمُشيّد، وذلك بأن نُسبقها بالكلمة
new، ولتوضيح هذا النّمط سنقوم بإنشاء مُنبّه (كالّذي تضبطه للاستيقاظ في هاتفك) يمكن ضبطه إلى تاريخ ووقت معيّنين ثمّ تفعيله أو تعطيله حسب الرّغبة:function Alarm(when) {
this.setAt = when;
this.enable = function() {
var startAfter = new Date(this.setAt) - new Date;
console.log("Alarm will wake you up after " + startAfter/1000 + " seconds");
this.timeout = setTimeout(function() {
console.log("Wake up!");
}, startAfter);
}
this.disable = function() {
if (this.timeout) {
clearTimeout(this.timeout);
delete this.timeout;
console.log("Alarm diabled");
}
}
}
var a = new Alarm("2015-03-19 5:58 PM");
a.enable()
// Alarm will wake you up after 8.982 seconds
// After a few seconds:
// Wake up!
في المثال السّابق نُسمّي الدّالة
Alarm() مُشيّدًا (constructor)، والكائن a نُسخة (instance).لاحظ أنّ
Alarm في المثال السّابق ليست سوى دالّة (function)، فهي ليست صنفًا كما في لغات أخرى مثل Java وC++، إذ تُعتبر الدّوال في JavaScript مكوّنًا من الدّرجة الأولى وتُعامل كما يُعامل أيّ كائن، وهكذا يمكن استخدامها كمشيّد لكائن آخر ممّا يسمح بمحاكاة مفهوم الأصناف الّذي لم يُضَف إلّا مؤخّرًا في JavaScript. من عيوب المِثال السّابق إسناد الدّوال الّتي ستعمل عمل الوظائف (methods) إلى النُسخة ذاتها عند إنشائها، وهذا يعني تكرار محتوى الدّوال في الذّاكرة مع كلّ نسخة جديدة من الكائن Alarm، بينما يمكننا توفير هذا الاستهلاك غير المُبرّر للذّاكرة بإسناد الدّوال إلى النّموذج البدئيّ للكائن (أي إلى Alarm.prototype) مما يسمح بمشاركتها بين كل نسخ الكائن، لتقوم الآلة الافتراضيّة بتنفيذ النّصّ البرمجيّ للدّالّة ذاتها بسياق النّسخة (instance context) الّتي استدعت الدّالة، أي إنّ this تُشير ضمن الدّالة عند تنفيذها إلى النُسخةَ المنشأة وليس الصّنف؛ بالطّبع ليس من المرغوب تطبيق الفكرة ذاتها على المُتغيّرات الأخرى مثل setAt، لأنّه من البديهيّ أن تختلف قيمتها بين نسخة وأخرى. لنُعد كتابة المثال السّابق بصورة أفضل:function Alarm(when) {
this.setAt = when;
}
Alarm.prototype.enable = function() {
var startAfter = new Date(this.setAt) - new Date;
console.log("Alarm will wake you up after " + startAfter/1000 + " seconds");
this.timeout = setTimeout(function() {
console.log("Wake up!");
}, startAfter);
}
Alarm.prototype.disable = function() {
if (this.timeout) {
clearTimeout(this.timeout);
delete this.timeout;
console.log("Alarm diabled");
}
}
var a = new Alarm("2015-03-19 6:21 PM");
a.enable();
// Alarm will wake you up after 30.243 seconds
// After 30 seconds...
// Wake up!
هذا الأسلوب في إنشاء الأصناف شائع جدًّا، وهو يتطلّب فهمًا دقيقًا لآليّة الوراثة في JavaScript؛ إذ يُبنى كلّ كائنٍ فيها على كائن آخر يُسمّى النّموذج البدئيّ (prototype)، ويقوم هذا الكائن الأخير على كائن ثالث أعلى منه في السّلسلة هو نموذجه البدئيّ، وهكذا حتّى نصل إلى
null الّذي ليس له نموذج بدئيّ بحسب تعريف اللّغة. في مثالنا السّابق الكائن Alarm.prototype هو النّموذج البدئيّ للكائن a، وهذا يعني أنّ كل الخواصّ المُسندة إلى Alarm.prototype وما فوقه ستكون مُتاحة للكائن a، ولو كتابنا برنامجًا مُشابهًا بـJava لقُلنا إنّ Alarm صنفٌ وإنّ a نُسخة عن هذا الصّنف (instance). عندما نحاول الوصول إلى الخاصّة a.setAt، فإنّ مُفسِّر JavaScript يبدأ بالبحث عن هذه الخاصّة من أدنى سلسلة الوراثة، أي من الكائن a ذاته، فإنّ وجدها قرأها وأعاد قيمتها، وإلّا تابع البحث صعودًا إلى النّموذج البدئيّ وهكذا... وطبيعة JavaScript هذه هي ما سمح لنا بإسناد الوظيفتين enable وdisable إلى Alarm.prototype مطمئنّين إلى أنّها ستكون مُتاحة عند قراءة a.enable() وa.disable(). يمكن التأكّد من النّموذج البدئيّ للكائن a كما يلي:Object.getPrototypeOf(a) == Alarm.prototype;
// true
الأصناف في ECMAScript 6
يُقدّم الإصدار الأحدث من JavaScript مفهوم الأصناف (classes) بصورته التّقليديّة المعروفة في اللّغات الأخرى، إلّا أنّه ليس سوى أسلوب آخر لصياغة النّماذج البدئيّة (أو ما يُسمّى syntactic sugar)، وهذا يعني أنّه نموذج الوراثة في JavaScript لم يتغيّر. يمكننا إعادة كتابة المثال السّابق بصياغة الأصناف في ES6 كما يلي:
class Alarm {
constructor(when) {
this.startAt = when;
}
enable() {
var startAfter = new Date(this.setAt) - new Date;
console.log("Alarm will wake you up after " + startAfter/1000 + " seconds");
this.timeout = setTimeout(function() {
console.log("Wake up!");
}, startAfter);
}
disable() {
if (this.timeout) {
clearTimeout(this.timeout);
delete this.timeout;
console.log("Alarm diabled");
}
}
}
وستُسند الدّوال
enable() وdisable() إلى Alarm.prototype تمامًا كما في المثال الذي سبقه.يُذكر أنّ استخدام
new ليست الطّريقة الوحيدة لتشييد الكائنات، إذ يمكن استخدام الوظيفة Object.create() لتُعطي النّتيجة ذاتها:var a = Object.create(Alarm.prototype);
Object.getPrototypeOf(a) == Alarm.prototype;
// true
إسناد الخواصّ إلى الكائنات
JavaScript لغة ديناميكية، وهذا يعني أنّه يمكن إضافة وحذف الخواصّ من الكائنات وتعديل نماذجها البدئيّة أثناء التّنفيذ، وهذا ما يمنحها القسم الأكبر من مرونتها ويجعلها مناسبة للاستخدام في بيئة مُعقّدة مثل بيئة الويب، وليس من الغرابة أن توفّر اللّغة وسائل متعدّدة لإسناد الخصائص إلى الكائنات لتلبية الحاجات المتنوّعة لتطبيقات الويب.
ماذا لو أردنا تغيير قيمة المنبّه في مثالنا السّابق بعد تفعيله؟ لربّما ترادونا للوهلة الأولى إمكانيّة تغيير قيمة الخاصّة
setAt بالطّريقة التّقليدية:a.setAt = "2016-03-03 03:03 PM";
// or
a["setAt"] = "2016-03-03 03:03 PM";
لكنّ نتيجة هذا الفعل لن تكون كما يُتوقّع، فلو عدنا للمثال السابق وتمعّنا في خواصّه، للاحظنا عيبًا في كيفيّة عمل المُنبّه، إذ إنّ الخاصّة
setAt مكشوفة ويمكن تغيير قيمتها في أيّ وقت، حتى بعد تفعيل المُنبّه، إلّا أنّ تغييرها بعدئذٍ لن يغيّر اللّحظة الحقيقيّة الّتي سينطلق فيها المنبّه كما يتضّح لنا عند قراءة النّصّ البرمجيّ، ولذا فنحن هنا أمام حلّين: إمّا منع تغيير قيمة الخاصّة setAt وجعلها للقراءة فقط بعد إنشاء المُنبّه، أو إيقاف المنبّه وإعادة ضبطه في كلّ مرّة تُغيّر فيها قيمة الخاصّة setAt، وكلا الحلّين متاحان إذا كنّا على علم بأساليب إسناد الخصائص في JavaScript.توفّر اللّغة الوظيفة
Object.defineProperty() الّتي تسمح بتعريف خواصّ لكائن ما مع إمكانيّة التّحكم بتفاصيل هذه الخاصّة، ومن هذه التّفاصيل:- هل الخاصّة قابلة للكتابة؟ (
writable) - هل يجب المرور على هذه الخاصّة عند سرد خواصّ الكائن؟ (
enumerable) - ما الذي يحدث عند إسناد قيمة للخاصّة؟ (
set) - ما الذي يحدث عند قراءة قيمة الخاصّة؟ (
get)
وبهذا يمكننا بسهولة منع تغيير قيمة الخاصّة
setAt بعد إسنادها:function Alarm(when) {
Object.defineProperty(this, "setAt", {
value: when,
writable: false
})
}
Alarm.prototype.enable = function() {
var startAfter = new Date(this.setAt) - new Date;
console.log("Alarm will wake you up after " + startAfter/1000 + " seconds");
this.timeout = setTimeout(function() {
console.log("Wake up!");
}, startAfter);
}
Alarm.prototype.disable = function() {
if (this.timeout) {
clearTimeout(this.timeout);
delete this.timeout;
console.log("Alarm diabled");
}
}
var a = new Alarm("2015-03-19 7:51 PM");
a.setAt = new Date("2016-03-19");
console.log(a.setAt);
// "2015-03-19 7:51 PM"
لاحظ أنّ قيمة
setAt لم تتغيّر. هذا حلّ جيّد، لكن سيكون من الأفضل السّماح للمُستخدم بتعديل قيمة المنبّه، وعندها سنلجأ لإيقاف المنّبه وإعادة ضبطه كما يلي:function Alarm(when) {
var _hidden_value = new Date(when);
Object.defineProperty(this, "setAt", {
set: function(newValue) {
_hidden_value = new Date(newValue);
if (this.timeout) {
this.disable();
console.log("Alarm changed to " + newValue);
console.log("You need to re-enable the alarm for changes to take effect");
}
},
get: function() {
return _hidden_value;
}
})
}
Alarm.prototype.enable = function() {
var startAfter = new Date(this.setAt) - new Date;
console.log("Alarm will wake you up after " + startAfter/1000 + " seconds");
this.timeout = setTimeout(function() {
console.log("Wake up!");
}, startAfter);
}
Alarm.prototype.disable = function() {
if (this.timeout) {
clearTimeout(this.timeout);
delete this.timeout;
console.log("Alarm diabled");
}
}
var a = new Alarm("2016-03-03 03:03 PM")
a.enable();
// Alarm will wake you up after 30221933.66 seconds
a.setAt = "2015-03-19 8:05 PM";
// Alarm changed to 2015-03-19 8:05 PM
// You need to re-enable the alarm for changes to take effect
a.enable()
// Alarm will wake you up after 20.225 seconds
// After 20 seconds...
// Wake up!
لاحظ أنّنا سنحتاج إلى مُتغيّر سرِّيِّ (
_hidden_value) نُخزّن فيه القيمة الفعليّة لوقت التّنبيه.


ليست هناك تعليقات:
إرسال تعليق