نمط Strict
نمط Strict أتى في معيار ECMAScript 5 لإضافة «صرامة» في تطبيق شيفرات JavaScript، لاحظ أنَّ نمط strict لا يُشكِّل قسمًا فرعيًا من JavaScript، وإنما له بنية خاصة به تختلف عن الشيفرات العادية. فالمتصفحات التي لا تدعم نمط strict ستُشغِّل الشيفرات بطريقة مختلفة عن المتصفحات التي تدعمه، لذا لا تعتمد على نمط strict دون اختبار دعمه أولًا من المتصفح.
نمط Strict يُضيف عددًا من التغييرات على بنية JavaScript، فلم تعد تسكت JavaScript عن الأخطاء وإنما أصبحت ترمي استثناءات عند حدوثها؛ ونمط strict يحّل الأخطاء التي كانت تُصعِّب عملية تحسين أداء الشيفرات على محركات JavaScript، أي أنَّ نمط strict يساعد في بعض الأحيان بتسريع تنفيذ الشيفرات. أضف إلى ما سبق أنَّ نمط strict يمنع استخدام بعض البنى التي قد تُعرَّف في إصدارات قادمة من معيار ECMAScript.
تفعيل نمط Strict
يمكن تطبيق نمط strict على كامل السكربت أو على الدوال، لكن لا يمكن تطبيقه على الكتل البرمجية الموجودة بين قوسين معقوفين {}، ومحاولة فعل ذلك لن تؤدي إلى فعل أيّ شيء.
تفعيل نمط Strict في السكربتات
لتفعيل هذا النمط لكامل السكربت، عليك أن تضع العبارة الآتية (بحذافيرها) قبل أيّة تعليمة في الملف: "use strict";
أو 'use strict';
:
// تفعيل النمط في كامل الملف
'use strict';
var v = "Hi! I'm a strict mode script!";
تفعيل نمط Strict في الدوال
لتفعيل هذا النمط داخل دالة، عليك أن تضع العبارة الآتية (بحذافيرها) قبل أيّة تعليمة في الدالة: "use strict";
أو 'use strict';
:
function strict() {
// تفعيل النمط في الدالة
'use strict';
function nested() { return 'And so am I!'; }
return "Hi! I'm a strict mode function! " + nested();
}
function notStrict() { return "I'm not strict."; }
التغيرات التي جرت في نمط Strict
يؤدي نمط strict إلى تغييرات في البنية والسلوك. والتغييرات تنضوي عادةً تحت أحد التصنيفات الآتية: تغيير المشكلات الصغيرة إلى أخطاء، وتبسيط طريقة حساب أسماء المتغيرات، وتبسيط eval
و arguments
، وتبسيط طريقة كتابة شيفرات آمنة، وتغيرات لها علاقة بمستقبل معيار ECMAScript.
تحويل المشكلات إلى أخطاء
نمط strict يحوِّل المشكلات التي كانت مقبولةً فيما سبق إلى أخطاء؛ ضع في ذهنك أنَّ لغة JavaScript قد صُمِّمَت فيما قد سلف للمطورين المبتدئين، وكانت تسمح ببعض المشكلات ولا تعدّها أخطاءً؛ لكن هذه المشكلات الصغيرة قد تتسبب بمشاكل عميقة في المستقبل، ونمط Strict يُعامِل تلك المشكلات كأخطاء لكي لا تُسبِّب بسلوكٍ غير متوقع وليحّلها المبرمج فورًا.
بدايةً، من المستحيل إنشاء متغيرات عامة في نمط strict بالغلط، فكتابة اسم المتغير خطأً سيُنشِئ متغيرًا عامًا جديدًا وسيستمر السكربت «بالعمل» (مع أنَّ من المحتمل أن يحتوي على أخطاء وعلل)؛ أما عمليات الإسناد التي تُنشِئ المتغيرات العامة خطأً سترمي استثناءً في نمط strict:
'use strict';
// على فرض وجود متغير باسم mistypedVariable
mistypeVariable = 17; // ReferenceError
ثانيًا، إنَّ عمليات الإسناد التي كانت تفشل بصمت سترمي استثناءً في نمط strict، فمثلًا NaN
هو متغيرٌ عامٌ غير قابل للكتابة، وفي الشيفرة العادية لن تؤدي محاولة إسناد قيمة إلى NaN
إلى شيء، فلن تظهر للمبرمج أيّة رسالة خطأ؛ أما في نمط strict فمحاولة إسناد قيمة إلى NaN
سيؤدي إلى رمي استثناء. الخلاصة أنَّ جميع محاولات الإسناد التي كانت ترمي أخطاءً في الشيفرات العادية (مثل محاولة الإسناد إلى خاصية أو متغير عام غير قابل للكتابة، أو الإسناد إلى خاصية getter، أو الإسناد إلى خاصية جديدة في كائن غير قابل للتوسعة) ستؤدي إلى رمي خطأ في نمط strict:
'use strict';
// الإسناد إلى متغير عام غير قابل للكتابة
var undefined = 5; // TypeError
var Infinity = 5; // TypeError
// الإسناد إلى خاصية غير قابلة للكتابة
var obj1 = {};
Object.defineProperty(obj1, 'x', { value: 42, writable: false });
obj1.x = 9; // TypeError
// الإسناد إلى خاصية getter
var obj2 = { get x() { return 17; } };
obj2.x = 5; // TypeError
// الإسناد إلى خاصية جديدة في كائن غير قابل للتوسعة
var fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = 'ohai'; // TypeError
ثالثًا، نمط strict سيؤدي إلى رمي خطأ عند محاولة حذف الخاصيات غير القابلة للحذف (وتلك المحاولات لم تكن تفعل شيئًا فيما قد سلف):
'use strict';
delete Object.prototype; // TypeError
رابعًا، نمط strict يتطلب أن تكون أسماء معاملات الدالة فريدةً، ففي الشيفرة العادية ستُستعمَل قيمة آخر معامل مكرر بدل بقية القيم. لكن لاحظ أنَّ بإمكاننا الوصول إلى الوسائط الأخرى عبر الكائن arguments
، فهي ما تزال متاحةً، لكن إخفاء قيمها بهذه الطريقة هو أمرٌ غير منطقي وليس مستحسنًا وقد يُسبِّب أخطاءً، لذا سيؤدي تكرار أسماء المعاملات إلى رمي أخطاء من النوع SyntaxError
:
function sum(a, a, c) { // SyntaxError
'use strict';
return a + b + c; // خطأ!
}
خامسًا، معيار ECMAScript 5 يمنع استخدام شكل تعريف الأعداد بالنظام الثماني، لاحظ أنَّ هذا الشكل ليس مدعومًا في معيار ECMAScript 5 لكنه مدعوم في جميع المتصفحات بإضافة الرقم 0 قبل الأعداد الثمانية: 0644 === 420
و "\045" === "%"
؛ أما في معيار ECMAScript 2015 (أي ES6) فأصبح هذا الشكل مدعومًا بإسباق الرقم بالمحرفين 0o
، أي:
var a = 0o10; // ES2015: Octal
يعتقد بعض المطورين المبتدئين أنَّ إضافة صفر قبل العدد ليس له معنى وإنما يستخدم لمحاذاة الأعداد مع بعضها، لكنهم لا يدركون أنَّ ذلك سيغيّر معنى العدد كليًا. لاحظ أنَّ استخدام صفر قبل العدد تحويله إلى النظام الثماني ليس مفيدًا في الأعم الأغلب من الحالات ويمكن إساءة استخدامه، لذا سيرمى الخطأ SyntaxError
عند فعله في نمط strict:
'use strict';
var sum = 015 + // SyntaxError
197 +
142;
var sumWithOctal = 0o10 + 8;
console.log(sumWithOctal); // 16
سادسًا، نمط strict في ECMAScript 2015 (أي ES6) يمنع ضبط الخاصيات على القيم الأولية، فإذا حاولنا ذلك ولم يكن نمط strict مفعلًا فلن يحدث شيء، أما إذا كان مُفعّلًا فسيرمى الخطأ TypeError
:
(function() {
'use strict';
false.true = ''; // TypeError
(14).sailing = 'home'; // TypeError
'with'.you = 'far away'; // TypeError
})();
تبسيط استخدام المتغيرات
نمط strict يُبسِّط طريقة ارتباط أسماء المتغيرات إلى تعريفاتها، فعددٌ كبيرٌ من التحسينات التي يجريها مُحرِّك JavaScript تعتمد على أننا نستطيع أن نقول أنَّ المتغير X مُخزِّن في موضعٍ معيّن؛ لكن JavaScript قد تجعل من عملية ربط أسماء المتغيرات إلى تعريفاتها أمرًا مستحيلًا إلا عند زمن التنفيذ؛ لذا يأتي نمط strict ويزيل أغلبية الحالات التي يحدث فيها ذلك، مما يساعد محرِّك JavaScript على تحسين الأداء.
أوّلًا، يمنع نمط strict استخدام التعبير with
، ويرمي استثناءً من النوع SyntaxError
:
'use strict';
var x = 17;
with (obj) { // SyntaxError
x;
}
ثانيًا، الدالة eval في نمط strict لا تؤدي إلى إضافة متغيرات جديدة في المجال (scope) المحيط بها، ففي الشيفرة العادية، سيؤدي تنفيذ الدالة eval("var x;")
إلى إضافة المتغير x في الدالة التي تحتوي على eval
أو في المجال العام، وهذا يعني أنَّه عمومًا في الدوال التي تستدعي eval
لن ترتبط أسماء المتغيرات إلى تعريفتها إلا في زمن التنفيذ (باستثناء أسماء المتغيرات التي تُشير إلى المعاملات أو المتغيرات المحلية) وذلك لأنَّ الدالة eval
قد تُسبِّب إضافة متغير جديد قد يؤدي إلى إخفاء المتغير الخارجي. أما في نمط strict فستُنشِئ الدالة eval
المتغيرات للشيفرة التي يجري تنفيذها داخلها فقط، أي أنَّ eval
ليس لها دخل إن كان اسم المتغير المُعرّف داخلها يُشير إلى متغير خارجي أو متغير محلي:
var x = 17;
var evalX = eval("'use strict'; var x = 42; x;");
console.assert(x === 17);
console.assert(evalX === 42);
لاحظ أنَّه إذا استدعيت الدالة eval
باستخدام تعبيرٍ من الشكل eval(...)
في نمط strict فستُنفَّذ الشيفرة الموجودة داخلها في نمط strict أيضًا:
function strict1(str) {
'use strict';
return eval(str); // النمط strict
}
function strict2(f, str) {
'use strict';
return f(str); // not eval(...): لن تكون في نمط strict
// إلا إذا حددت ذلك الشيفرة بوضوح
}
function nonstrict(str) {
return eval(str); // لن تكون في نمط strict
// إلا إذا صرّحنا عن ذلك
}
strict1("'Strict mode code!'");
strict1("'use strict'; 'Strict mode code!'");
strict2(eval, "'Non-strict code.'");
strict2(eval, "'use strict'; 'Strict mode code!'");
nonstrict("'Non-strict code.'");
nonstrict("'use strict'; 'Strict mode code!'");
ثالثًا، نمط strict يمنع حذف أسماء المتغيرات، فالتعبير delete name
في النمط strict سيؤدي إلى خطأ من النوع SyntaxError
:
'use strict';
var x;
delete x; // SyntaxError
eval('var y; delete y;'); // SyntaxError
تبسيط eval
و arguments
نمط strict يبسِّط استخدام الكائن arguments
والدالة eval
، فالدالة eval
يمكنها أن تضيف أو تحذف ارتباطات الأسماء مع القيم، والكائن arguments
يؤدي إلى إضافة أسماء بديلة للوسائط؛ فالنمط strict
يحاول جعل eval
و arguments
شبيهين بالكلمات المحجوزة، لكنه لا يحل جميع الإشكاليات ويترك ذلك لإصدار قادم من معيار ECMAScript.
أولًا، لا يمكن اسناد الأسماء eval
و arguments
في سياقٍ مختلف، فجميع التعابير البرمجية الآتية سترمي الخطأ SyntaxError
:
'use strict';
eval = 17;
arguments++;
++eval;
var obj = { set p(arguments) { } };
var eval;
try { } catch (arguments) { }
function x(eval) { }
function arguments() { }
var y = function eval() { };
var f = new Function('arguments', "'use strict'; return 17;");
ثانيًا، إذا كان لدينا معاملٌ باسم arg
في الشيفرات العادية ضمن دالةً فعملية ضبط قيمة arg
ستؤدي إلى ضبط قيمة arguments[0]
أيضًا (والعكس بالعكس)، أما الكائن arguments
في نمط strict سيُخزِّن القيم الأصلية للوسائط فقط، أي أنَّ التغيرات التي تحدث لقيمة المعامل لن تنعكس على الكائن arguments
. انظر إلى المثال الآتي للتوضيح:
function f(a) {
'use strict';
a = 42;
return [a, arguments[0]];
}
var pair = f(17);
console.assert(pair[0] === 42);
console.assert(pair[1] === 17);
ثالثًا، لم تعد الخاصية arguments.callee
مدعومةً، ففي الشيفرات العادية ستُشير إلى الدالة التي استدعت الدالة الحالية، وهي غير مفيدة عمليًا؛ لكنها ستؤدي إلى منع القيام ببعض عمليات تحسين الأداء:
'use strict';
var f = function() { return arguments.callee; };
f(); // TypeError
تأمين JavaScript
نمط strict يسهِّل من كتابة شيفرات «آمنة»، فبعض الموقع توفِّر طرائق للمستخدمين لكتابة شيفرات JavaScript التي ستعمل على أجهزة زوار الصفحة، ويمكن للغة JavaScript في المتصفحات أن تصل إلى معلومات المستخدم الخاصة... يحاول النمط strict توفير طرائق آمنة لكتابة الشيفرات.
أولًا، القيمة المُمرَّرة كالقيمة this
في دالة تعمل في النمط strict ليس من الضرورة أن تكون كائنًا، ففي الدوال العادية تكون قيمة this
كائنًا دومًا، فهي إما كان Boolean
أو String
أو Number
، أو الكائن العام؛ لاحظ أنَّ الوصول إلى الكائن العام سيسبب مخاطر أمنية لأنَّ الكائن العام يملك وصولًا إلى وظائف لا يُسمَح بها في بيئات JavaScript الآمنة:
'use strict';
function fun() { return this; }
console.assert(fun() === undefined);
console.assert(fun.call(2) === 2);
console.assert(fun.apply(null) === null);
console.assert(fun.call(undefined) === undefined);
console.assert(fun.bind(true)() === true);
هذا يعني أنَّ المتصفحات لم تعد قادرةً على الوصول إلى الكائن window
عبر this
داخل دالة في النمط strict.
ثانيًا، لم يعد بالإمكان الوصول إلى مكدس الاستدعاءات في JavaScript، فلو كانت الدالة fun
مستدعاةً في شيفرة عادية فالخاصية fun.caller
تُشير إلى الدالة fun
و fun.arguments
تُشير إلى الوسائط التي مُرِّرَت إلى الدالة fun
، وكلا الأمرين يُسبِّب مشاكل أمنية، لذا سيرمى الخطأ TypeError
عند محاولة الوصول إلى تلك الخاصيتين في الشيفرات التي تعمل بنمط strict:
function restricted() {
'use strict';
restricted.caller; // TypeError
restricted.arguments; // TypeError
}
function privilegedInvoker() {
return restricted();
}
privilegedInvoker();
ثالثًا، لم يعد الكائن arguments
-المتوافر ضمن الدوال- يسمح بالوصول إلى متغيرات الدالة، ففي إصدارات ECMAScript القديمة كانت الخاصية arguments.caller
هي كائنٌ له خاصياتٌ تُمثِّل المتغيرات الموجودة ضمن الدالة، وهذا يُسبِّب مشكلةً أمنية لأنَّه أصبح من غير الممكن إخفاء القيم الحساسة أمنيًا بوضعها ضمن دالة، ولذا السبب سيرمى الخطأ TypeError
عند محاولة الوصول إلى الخاصية arguments.caller
:
'use strict';
function fun(a, b) {
'use strict';
var v = 12;
return arguments.caller; // TypeError
}
fun(1, 2);
تمهيد الطريق لإصدارات ECMAScript القادمة
من المرجح أن تضيف إصدارات ECMAScript القادمة بنى جديدة إلى اللغة، ويُطبِّق نمط strict في ECMAScript 5 بعض المحدوديات لتسهيل عملية الانتقال.
بدايةً، هنالك قائمة من المعرفات التي أصبحت كلمات محجوزة، وهي implements
و interface
و let
و package
و private
و protected
و public
و static
و yield
، أي لا تستطيع تسمية متغيرات أو معاملات بهذه الأسماء:
function package(protected) { // خطأ
'use strict';
var implements; // خطأ
interface: // خطأ
while (true) {
break interface; // خطأ
}
function private() { } // خطأ
}
function fun(static) { 'use strict'; } // خطأ
نمط Strict في المتصفحات
المتصفحات الرئيسية تدعم حاليًا نمط strict، لكن لا تعتمد على ذلك لأنَّ هنالك عددٌ كبيرٌ من إصدارات المتصفحات التي فيها دعمٌ جزئيٌ لنمط strict أو أنها لا تدعمه إطلاقًا (مثلًا: إصدارات IE الأقل من 10).
تذكر أنَّ نمط strict يجري تغييرات في بنية اللغة، والاعتماد على هذه التغييرات سيؤدي إلى أخطاء في المتصفحات التي لا تدعم نمط strict.
احرص أيضًا على اختبار الشيفرة في المتصفحات التي لا تدعم نمط strict إضافة إلى المتصفحات التي تدعمه، ذلك لأنَّ اختبارك للميزات في أحد نوعَي المتصفحات فقط قد يُسبِّب مشاكل في المتصفحات الأخرى، والعكس بالعكس.
مصادر ومواصفات
- مسودة المعيار ECMAScript Latest Draft.
- معيار ECMAScript 2015 (6th Edition).
- معيار ECMAScript 5.1.