JavaScript

شرح تفويض الأحداث (event delegation)

تفويض الأحداث هي تقنية تتضمن إضافة مستمعي الأحداث إلى عنصر أب بدلاً من إضافتهم إلى العناصر الفرعية. سيتم تشغيل المستمع كلما تم تشغيل الحدث على العناصر الفرعية بسبب فقاعة الحدث (event bubbling) صعودًا في DOM. فوائد هذه التقنية هي:

  • تقل بصمة الذاكرة لأنه يلزم معالج واحد فقط على العنصر الأصلي، بدلاً من الاضطرار إلى إرفاق معالجات الأحداث على كل عنصر تابع.
  • لا توجد حاجة لإلغاء ربط المعالج من العناصر التي تتم إزالتها وربط الحدث للعناصر الجديدة.

اشرح كيف تعمل `this` في JavaScript

لا يوجد تفسير بسيط لـ this؛ إنه أحد أكثر المفاهيم إرباكًا في JavaScript. التفسير التقريبي هو أن قيمة this تعتمد على كيفية استدعاء الدالة. لقد قرأت العديد من التفسيرات حول this عبر الإنترنت، ووجدت أن شرح [Arnav Aggrawal] هو الأكثر وضوحًا. يتم تطبيق القواعد التالية:

  1. إذا تم استخدام الكلمة المفتاحية new عند استدعاء الدالة، فإن this داخل الدالة هو كائن جديد تمامًا.
  2. إذا تم استخدام apply أو call أو bind لاستدعاء/إنشاء دالة، فإن this داخل الدالة هو الكائن الذي تم تمريره كحجة.
  3. إذا تم استدعاء دالة كطريقة، مثل obj.method() - فإن this هو الكائن الذي تكون الدالة خاصية له.
  4. إذا تم استدعاء دالة كاستدعاء دالة حرة (free function invocation)، مما يعني أنها تم استدعاؤها دون وجود أي من الشروط المذكورة أعلاه، فإن this هو الكائن العام (global object). في المتصفح، هو الكائن window. إذا كان في الوضع الصارم ('use strict')، فسيكون this هو undefined بدلاً من الكائن العام.
  5. إذا انطبقت عدة من القواعد المذكورة أعلاه، فإن القاعدة الأعلى تفوز وتحدد قيمة this.
  6. إذا كانت الدالة هي دالة سهم ES2015، فإنها تتجاهل جميع القواعد المذكورة أعلاه وتتلقى قيمة this لنطاقها المحيط في وقت إنشائها.

لشرح متعمق، تحقق من [مقالته على Medium].

هل يمكنك إعطاء مثال على إحدى الطرق التي تغير بها العمل مع this في ES6؟

يسمح لك ES6 باستخدام [دوال السهم] التي تستخدم [النطاق اللغوي المحيط]. هذا عادة ما يكون مريحًا، ولكنه يمنع المتصل من التحكم في السياق عبر .call أو .apply - وتكون العواقب هي أن مكتبة مثل jQuery لن تربط this بشكل صحيح في دوال معالجة الأحداث الخاصة بك. وبالتالي، من المهم أن تضع هذا في الاعتبار عند إعادة هيكلة التطبيقات القديمة الكبيرة.

اشرح كيف يعمل التوارث الأولي (prototypal inheritance).

تحتوي جميع كائنات JavaScript على خاصية __proto__ باستثناء الكائنات التي تم إنشاؤها باستخدام Object.create(null)، وهي مرجع لكائن آخر، يسمى "نموذج" الكائن (prototype). عندما يتم الوصول إلى خاصية على كائن وإذا لم يتم العثور على الخاصية في هذا الكائن، يبحث محرك JavaScript في __proto__ الخاص بالكائن، ثم في __proto__ الخاص بـ __proto__ وهكذا، حتى يجد الخاصية معرفة على أحد __proto__s أو حتى يصل إلى نهاية سلسلة النماذج. يحاكي هذا السلوك الوراثة الكلاسيكية، ولكنه في الواقع أقرب إلى [التفويض من الوراثة].

مثال على التوارث الأولي

// مُنشئ الكائن الأب. function Animal(name) { this.name = name; } // أضف طريقة إلى نموذج الكائن الأب. Animal.prototype.makeSound = function () { console.log('The ' + this.constructor.name + ' makes a sound.'); }; // مُنشئ الكائن الابن. function Dog(name) { Animal.call(this, name); // استدعاء مُنشئ الأب. } // تعيين نموذج الكائن الابن ليكون نموذج الأب. Object.setPrototypeOf(Dog.prototype, Animal.prototype); // أضف طريقة إلى نموذج الكائن الابن. Dog.prototype.bark = function () { console.log('Woof!'); }; // أنشئ مثيلًا جديدًا من Dog. const bolt = new Dog('Bolt'); // استدعاء الطرق على الكائن الابن. console.log(bolt.name); // "Bolt" bolt.makeSound(); // "The Dog makes a sound." bolt.bark(); // "Woof!"

الأشياء التي يجب ملاحظتها هي:

  • makeSound. غير معرف على Dog، لذلك ينتقل المحرك صعودًا في سلسلة النماذج ويجد makeSound. من Animal الموروثة.
  • لم يعد يوصى باستخدام Object.create لبناء سلسلة الوراثة. استخدم Object.setPrototypeOf بدلاً من ذلك.

ما رأيك في AMD مقابل CommonJS؟

كلاهما طريقتان لتطبيق نظام وحدات، لم يكن موجودًا بشكل أصلي في JavaScript حتى ظهور ES2015. CommonJS متزامن بينما AMD (Asynchronous Module Definition) غير متزامن بشكل واضح. تم تصميم CommonJS مع وضع تطوير جانب الخادم في الاعتبار بينما AMD، مع دعمها للتحميل غير المتزامن للوحدات، مخصص للمتصفحات بشكل أكبر.

أجد بناء جملة AMD مطولًا للغاية و CommonJS أقرب إلى النمط الذي تكتب به عبارات الاستيراد في لغات أخرى. في معظم الأوقات، أجد AMD غير ضروري، لأنه إذا قمت بتقديم جميع JavaScript الخاص بك في ملف حزمة واحد مدمج، فلن تستفيد من خصائص التحميل غير المتزامن. كما أن بناء جملة CommonJS أقرب إلى نمط Node لكتابة الوحدات وهناك تقليل في تكلفة تبديل السياق عند التبديل بين تطوير JavaScript على جانب العميل وجانب الخادم.

أنا سعيد لأنه مع وحدات ES2015، التي تدعم كلاً من التحميل المتزامن وغير المتزامن، يمكننا أخيرًا الالتزام بنهج واحد. على الرغم من أنه لم يتم طرحه بالكامل في المتصفحات وفي Node، يمكننا دائمًا استخدام محولات اللغة لتحويل الكود الخاص بنا.

اشرح لماذا لا يعمل ما يلي كـ IIFE: `function foo(){ }();`. ما الذي يجب تغييره لجعله IIFE بشكل صحيح؟

IIFE هو اختصار لـ "Immediately Invoked Function Expressions" (تعبيرات الدالة المستدعاة فورًا). يقرأ محلل JavaScript function foo(){ }(); كـ function foo(){ } و ();، حيث الأول هو إعلان دالة والأخير (زوج من الأقواس) هو محاولة استدعاء دالة ولكن لا يوجد اسم محدد، وبالتالي يرمي Uncaught SyntaxError: Unexpected token ).

إليك طريقتان لإصلاح ذلك تتضمن إضافة المزيد من الأقواس: (function foo(){ })() و (function foo(){ }()). تعتبر العبارات التي تبدأ بـ function إعلانات دالة؛ عن طريق تضمين هذه الدالة داخل ()، تصبح تعبير دالة يمكن بعد ذلك تنفيذه باستخدام () اللاحقة. لا يتم كشف هذه الدوال في النطاق العام ويمكنك حتى حذف اسمها إذا لم تكن بحاجة إلى الإشارة إليها داخل جسم الدالة.

يمكنك أيضًا استخدام عامل التشغيل void: void function foo(){ }();. لسوء الحظ، هناك مشكلة واحدة في هذا النهج. تقييم التعبير المعطى هو دائمًا undefined، لذلك إذا كانت دالة IIFE الخاصة بك ترجع أي شيء، فلا يمكنك استخدامه. مثال:

const foo = void (function bar() { return 'foo'; })(); console.log(foo); // undefined

ما الفرق بين المتغير الذي يكون: `null`، `undefined`، أو غير مُعلن (undeclared)؟ كيف يمكنك التحقق من أي من هذه الحالات؟

يتم إنشاء المتغيرات غير المُعلنة عندما تقوم بتعيين قيمة لمعرّف لم يتم إنشاؤه مسبقًا باستخدام var أو let أو const. سيتم تعريف المتغيرات غير المُعلنة بشكل عام، خارج النطاق الحالي. في الوضع الصارم، سيتم رمي ReferenceError عندما تحاول تعيين قيمة لمتغير غير مُعلن. المتغيرات غير المُعلنة سيئة تمامًا مثل المتغيرات العامة سيئة. تجنبها بأي ثمن! للتحقق منها، قم بتضمين استخدامها في كتلة try/catch.

function foo() { x = 1; // يرمي ReferenceError في الوضع الصارم } foo(); console.log(x); // 1

المتغير الذي هو undefined هو متغير تم تعريفه، ولكن لم يتم تعيين قيمة له. نوعه هو undefined. إذا لم ترجع دالة أي قيمة كنتيجة لتنفيذها تم تعيينها لمتغير، فإن المتغير يكون له أيضًا قيمة undefined. للتحقق من ذلك، قارن باستخدام عامل المساواة الصارمة (===) أو typeof الذي سيعطي السلسلة 'undefined'. لاحظ أنه لا ينبغي عليك استخدام عامل المساواة التجريدية للتحقق، لأنه سيعود true أيضًا إذا كانت القيمة null.

var foo; console.log(foo); // undefined console.log(foo === undefined); // true console.log(typeof foo === 'undefined'); // true console.log(foo == null); // true. خطأ، لا تستخدم هذا للتحقق! function bar() {} var baz = bar(); console.log(baz); // undefined

المتغير الذي هو null سيكون قد تم تعيينه صراحةً إلى القيمة null. يمثل لا قيمة ويختلف عن undefined بمعنى أنه تم تعيينه صراحةً. للتحقق من null، قارن ببساطة باستخدام عامل المساواة الصارمة. لاحظ أنه كما في السابق، لا ينبغي عليك استخدام عامل المساواة التجريدية (==) للتحقق، لأنه سيعود true أيضًا إذا كانت القيمة undefined.

var foo = null; console.log(foo === null); // true console.log(typeof foo === 'object'); // true console.log(foo == undefined); // true. خطأ، لا تستخدم هذا للتحقق!

كعادة شخصية، لا أترك أبدًا متغيراتي غير معلنة أو غير معينة. سأقوم بتعيين null لها صراحةً بعد الإعلان إذا لم أكن أنوي استخدامها بعد. إذا كنت تستخدم أداة Lint في سير عملك، فسوف تكون عادةً قادرة أيضًا على التحقق من أنك لا تشير إلى متغيرات غير معلنة.

ما هو الإغلاق (closure)، وكيف/لماذا تستخدمه؟

الإغلاق هو مزيج من دالة والبيئة المعجمية التي تم الإعلان عن تلك الدالة ضمنها. تشير كلمة "معجمية" إلى حقيقة أن النطاق المعجمي يستخدم الموقع الذي يتم فيه الإعلان عن متغير داخل الكود المصدري لتحديد مكان توفر هذا المتغير. الإغلاقات هي دوال لديها وصول إلى متغيرات الدالة الخارجية (المحيطة) – سلسلة النطاقات حتى بعد أن تكون الدالة الخارجية قد عادت.

لماذا تستخدمها؟

  • خصوصية البيانات / محاكاة الطرق الخاصة باستخدام الإغلاقات. تستخدم عادة في [نمط الوحدة (module pattern)].
  • [التطبيقات الجزئية (Partial applications) أو التكليف (currying)].

هل يمكنك وصف الفرق الرئيسي بين حلقة `.forEach` وحلقة `.map()` ولماذا تختار أحدهما على الآخر؟

لفهم الفروق بين الاثنين، دعنا ننظر إلى ما تفعله كل دالة.

forEach

  • تقوم بالمرور عبر العناصر في مصفوفة.
  • تنفذ دالة رد نداء لكل عنصر.
  • لا ترجع قيمة.
const a = [1, 2, 3]; const doubled = a.forEach((num, index) => { // قم بشيء ما مع num و/أو index. }); // doubled = undefined

map

  • تقوم بالمرور عبر العناصر في مصفوفة.
  • "تُسقط" كل عنصر إلى عنصر جديد عن طريق استدعاء الدالة على كل عنصر، مما يؤدي إلى إنشاء مصفوفة جديدة.
const a = [1, 2, 3]; const doubled = a.map((num) => { return num * 2; }); // doubled = [2, 4, 6]

الفرق الرئيسي بين .forEach و .map() هو أن .map() تُرجع مصفوفة جديدة. إذا كنت بحاجة إلى النتيجة، ولكن لا ترغب في تغيير المصفوفة الأصلية، فإن .map() هو الخيار الواضح. إذا كنت بحاجة ببساطة إلى التكرار على مصفوفة، فإن forEach خيار جيد.

ما هي حالة الاستخدام النموذجية للدوال مجهولة الاسم (anonymous functions)؟

يمكن استخدامها في تعبيرات الدالة المستدعاة فوراً (IIFEs) لتغليف بعض التعليمات البرمجية ضمن نطاق محلي بحيث لا تتسرب المتغيرات المعلنة فيها إلى النطاق العام.

(function () { // بعض التعليمات البرمجية هنا. })();

كدالة رد نداء تُستخدم مرة واحدة ولا تحتاج إلى استخدامها في أي مكان آخر. سيبدو الكود أكثر احتواءً على نفسه وقابلية للقراءة عندما تُعرّف المعالجات داخل الكود الذي يستدعيها مباشرةً، بدلاً من الاضطرار إلى البحث في مكان آخر للعثور على جسم الدالة.

setTimeout(function () { console.log('Hello world!'); }, 1000);

حجج لبُنى البرمجة الوظيفية أو Lodash (مشابهة لدوال رد النداء).

const arr = [1, 2, 3]; const double = arr.map(function (el) { return el * 2; }); console.log(double); // [2, 4, 6]

كيف تنظم الكود الخاص بك؟ (نمط الوحدة النمطية (module pattern)، الوراثة الكلاسيكية؟)

في الماضي، استخدمت Backbone لنماذجي التي تشجع على نهج أكثر توجهاً للكائنات (OOP)، بإنشاء نماذج Backbone وإرفاق الدوال بها.

لا يزال نمط الوحدة النمطية رائعًا، ولكن في هذه الأيام، أستخدم React/Redux التي تستخدم تدفق بيانات أحادي الاتجاه يعتمد على بنية Flux. سأمثل نماذج تطبيقي باستخدام كائنات عادية وأكتب دوال نقية مساعدة لمعالجة هذه الكائنات. يتم التعامل مع الحالة باستخدام الإجراءات والمختزلات كما هو الحال في أي تطبيق Redux آخر.

أتجنب استخدام الوراثة الكلاسيكية حيثما أمكن. وإذا فعلت ذلك، فإني ألتزم بـ [هذه القواعد].

ما الفرق بين الكائنات المضيفة (host objects) والكائنات الأصلية (native objects)؟

الكائنات الأصلية هي كائنات جزء من لغة JavaScript معرفة بمواصفات ECMAScript، مثل String, Math, RegExp, Object, Function, إلخ.

الكائنات المضيفة يتم توفيرها بواسطة بيئة التشغيل (المتصفح أو Node)، مثل window, XMLHTTPRequest, إلخ.

الفرق بين: `function Person(){}` و `var person = Person()` و `var person = new Person()`؟

هذا السؤال غامض جدًا. أعتقد أن قصده هو السؤال عن المنشئات (constructors) في JavaScript. من الناحية الفنية، function Person(){} هو مجرد إعلان دالة عادي. الاتفاقية هي استخدام PascalCase للدوال التي يُقصد استخدامها كمنشئات.

var person = Person() يستدعي Person كدالة، وليس كمنشئ. الاستدعاء بهذه الطريقة هو خطأ شائع إذا كان المقصود من الدالة استخدامها كمنشئ. عادةً، لا يُرجع المنشئ أي شيء، وبالتالي فإن استدعاء المنشئ كدالة عادية سيعود undefined ويتم تعيين ذلك للمتغير المقصود كالمثيل.

var person = new Person() ينشئ مثيلًا لكائن Person باستخدام عامل التشغيل new، والذي يرث من Person.prototype. بديل آخر سيكون استخدام Object.create، مثل: Object.create(Person.prototype).

function Person(name) { this.name = name; } var person = Person('John'); console.log(person); // undefined console.log(person.name); // Uncaught TypeError: Cannot read property 'name' of undefined var person = new Person('John'); console.log(person); // Person { name: "John" } console.log(person.name); // "john"

ما الفرق بين `.call` و `.apply`؟

يُستخدم كلاهما .call و .apply لاستدعاء الدوال، وسيتم استخدام المعامل الأول كقيمة لـ this داخل الدالة. ومع ذلك، يستقبل .call معاملات مفصولة بفاصلات كالحجج التالية بينما يستقبل .apply مصفوفة من الحجج كالحجة التالية. طريقة سهلة لتذكر ذلك هي C لـ call و comma-separated (مفصول بفاصلات) و A لـ apply و array of arguments (مصفوفة من الحجج).

function add(a, b) { return a + b; } console.log(add.call(null, 1, 2)); // 3 console.log(add.apply(null, [1, 2])); // 3

اشرح `Function.prototype.bind`.

مأخوذة كلمة بكلمة من [MDN]:

تنشئ الدالة bind() دالة جديدة، عند استدعائها، يتم تعيين الكلمة المفتاحية this الخاصة بها إلى القيمة المقدمة، مع تسلسل معين من الوسائط يسبق أي وسائط مقدمة عند استدعاء الدالة الجديدة.

في تجربتي، إنها مفيدة للغاية لربط قيمة this في طرق الفئات التي تريد تمريرها إلى دوال أخرى. يتم ذلك بشكل متكرر في مكونات React.

متى تستخدم `document.write()`؟

تكتب document.write() سلسلة نصية إلى تدفق مستند مفتوح بواسطة document.open(). عندما يتم تنفيذ document.write() بعد تحميل الصفحة، فإنها ستستدعي document.open الذي يمسح المستند بأكمله (تمت إزالة <head> و <body>!) ويستبدل المحتويات بقيمة المعامل المعطى. وبالتالي، عادة ما يعتبر خطيرًا ومعرضًا لسوء الاستخدام.

هناك بعض الإجابات عبر الإنترنت التي توضح أن document.write() يتم استخدامها في رمز التحليلات أو [عندما تريد تضمين أنماط يجب أن تعمل فقط إذا كانت JavaScript ممكّنة]. حتى أنها تستخدم في قالب HTML5 لتحميل [السكريبتات بالتوازي والحفاظ على ترتيب التنفيذ]! ومع ذلك، أظن أن تلك الأسباب قد تكون قديمة، وفي العصر الحديث، يمكن تحقيقها دون استخدام document.write(). يرجى تصحيحي إذا كنت مخطئًا في هذا.

ما الفرق بين اكتشاف الميزات (feature detection) واستنتاج الميزات (feature inference) واستخدام سلسلة وكيل المستخدم (UA string)؟

اكتشاف الميزات (Feature Detection)

يتضمن اكتشاف الميزات معرفة ما إذا كان المتصفح يدعم كتلة معينة من التعليمات البرمجية، وتشغيل تعليمات برمجية مختلفة اعتمادًا على ما إذا كان يدعمها (أم لا)، بحيث يمكن للمتصفح دائمًا توفير تجربة عمل بدلاً من التعطل/الخطأ في بعض المتصفحات. على سبيل المثال:

if ('geolocation' in navigator) { // يمكن استخدام navigator.geolocation } else { // التعامل مع نقص الميزة }

[Modernizr] هي مكتبة رائعة للتعامل مع اكتشاف الميزات.

استنتاج الميزات (Feature Inference)

يتحقق استنتاج الميزات من ميزة تمامًا مثل اكتشاف الميزات، ولكنه يستخدم دالة أخرى لأنه يفترض أنها ستكون موجودة أيضًا، على سبيل المثال:

if (document.getElementsByTagName) { element = document.getElementById(id); }

هذا لا ينصح به حقًا. اكتشاف الميزات أكثر أمانًا.

سلسلة وكيل المستخدم (UA String)

هذه سلسلة يبلغ عنها المتصفح تسمح لنظيرات بروتوكول الشبكة بتحديد نوع التطبيق أو نظام التشغيل أو بائع البرامج أو إصدار البرامج لوكيل المستخدم البرمجي الذي يطلب. يمكن الوصول إليها عبر navigator.userAgent. ومع ذلك، فإن السلسلة صعبة التحليل ويمكن تزويرها. على سبيل المثال، يعرض Chrome نفسه على أنه Chrome و Safari. لذا لاكتشاف Safari، يجب عليك التحقق من سلسلة Safari وعدم وجود سلسلة Chrome. تجنب هذه الطريقة.

اشرح Ajax بأكبر قدر ممكن من التفصيل.

Ajax (اختصار لـ JavaScript و XML غير المتزامنة) هي مجموعة من تقنيات تطوير الويب تستخدم العديد من تقنيات الويب على جانب العميل لإنشاء تطبيقات ويب غير متزامنة. باستخدام Ajax، يمكن لتطبيقات الويب إرسال البيانات إلى خادم واستردادها منه بشكل غير متزامن (في الخلفية) دون التدخل في عرض وسلوك الصفحة الموجودة. عن طريق فصل طبقة تبادل البيانات عن طبقة العرض، يسمح Ajax لصفحات الويب، وبالتالي لتطبيقات الويب، بتغيير المحتوى ديناميكيًا دون الحاجة إلى إعادة تحميل الصفحة بأكملها. عمليًا، تستخدم التطبيقات الحديثة عادةً JSON بدلاً من XML، نظرًا لمزايا JSON التي تعد جزءًا أصيلًا من JavaScript.

يتم استخدام واجهة برمجة تطبيقات XMLHttpRequest بشكل متكرر للاتصال غير المتزامن أو في هذه الأيام، واجهة برمجة تطبيقات fetch().

ما هي مزايا وعيوب استخدام Ajax؟

المزايا

  • تفاعلية أفضل. يمكن تغيير المحتوى الجديد من الخادم ديناميكيًا دون الحاجة إلى إعادة تحميل الصفحة بأكملها.
  • تقليل الاتصالات بالخادم نظرًا لأن السكريبتات وأوراق الأنماط تحتاج إلى طلب مرة واحدة فقط.
  • يمكن الحفاظ على الحالة في الصفحة. ستستمر متغيرات JavaScript وحالة DOM لأن صفحة الحاوية الرئيسية لم يتم إعادة تحميلها.
  • بشكل أساسي معظم مزايا تطبيقات الصفحة الواحدة (SPA).

العيوب

  • صفحات الويب الديناميكية أصعب في الإشارة المرجعية.
  • لا تعمل إذا تم تعطيل JavaScript في المتصفح.
  • بعض برامج زحف الويب لا تنفذ JavaScript ولن ترى المحتوى الذي تم تحميله بواسطة JavaScript.
  • صفحات الويب التي تستخدم Ajax لجلب البيانات سيتعين عليها على الأرجح دمج البيانات البعيدة التي تم جلبها مع قوالب جانب العميل لتحديث DOM. لكي يحدث هذا، سيتعين تحليل وتنفيذ JavaScript في المتصفح، وقد تواجه الأجهزة المحمولة منخفضة الجودة صعوبة في ذلك.
  • بشكل أساسي معظم عيوب تطبيقات الصفحة الواحدة (SPA).

اشرح كيف يعمل JSONP (وكيف أنه ليس Ajax حقًا).

JSONP (JSON with Padding) هي طريقة شائعة الاستخدام لتجاوز سياسات النطاقات المتعارضة في متصفحات الويب لأن طلبات Ajax من الصفحة الحالية إلى نطاق خارجي غير مسموح بها.

يعمل JSONP عن طريق إجراء طلب إلى نطاق خارجي عبر علامة <script> وعادةً ما يكون ذلك باستخدام معلمة استعلام callback، على سبيل المثال: https://example.com?callback=printData. سيقوم الخادم بعد ذلك بتغليف البيانات داخل دالة تسمى printData وإعادتها إلى العميل.

<script> function printData(data) { console.log(`My name is ${data.name}!`); } </script> <script src="[https://example.com?callback=printData](https://example.com?callback=printData)"></script>
printData({ name: 'Yang Shun' });

يجب أن يكون لدى العميل دالة printData في نطاقه العام، وسيتم تنفيذ الدالة بواسطة العميل عند تلقي الاستجابة من النطاق الخارجي.

يمكن أن يكون JSONP غير آمن وله بعض الآثار الأمنية. نظرًا لأن JSONP هو في الواقع JavaScript، فإنه يمكنه فعل كل ما يمكن أن تفعله JavaScript، لذا يجب أن تثق في موفر بيانات JSONP.

في هذه الأيام، [CORS] هو النهج الموصى به ويعتبر JSONP بمثابة اختراق.

هل سبق لك استخدام قوالب JavaScript؟ إذا كان الأمر كذلك، فما هي المكتبات التي استخدمتها؟

نعم. Handlebars, Underscore, Lodash, AngularJS, و JSX. لم أحب استخدام القوالب في AngularJS لأنها كانت تعتمد بشكل كبير على السلاسل في التوجيهات وكان من الممكن أن تمر الأخطاء المطبعية دون اكتشافها. JSX هو المفضل لدي الآن لأنه أقرب إلى JavaScript ولا يوجد تقريبًا أي بناء جملة لتعلمه. في هذه الأيام، يمكنك حتى استخدام قوالب السلاسل الحرفية ES2015 كوسيلة سريعة لإنشاء القوالب دون الاعتماد على كود طرف ثالث.

const template = `<div>My name is: ${name}</div>`;

ومع ذلك، كن على دراية باحتمالية وجود XSS في النهج أعلاه حيث أن المحتويات لا يتم فك ترميزها لك، على عكس مكتبات القوالب.

اشرح "الرفع" (hoisting).

الرفع هو مصطلح يستخدم لشرح سلوك إعلانات المتغيرات في الكود الخاص بك. المتغيرات المعلنة أو المهيأة باستخدام الكلمة المفتاحية var سيتم "نقل" إعلانها إلى أعلى نطاق الوحدة/الدالة الخاصة بها، والذي نشير إليه بالرفع. ومع ذلك، يتم رفع الإعلان فقط، بينما تبقى القيمة المعينة (إذا كانت موجودة) في مكانها.

لاحظ أن الإعلان لا يتم نقله فعليًا - فمحرك JavaScript يحلل الإعلانات أثناء الترجمة ويصبح على دراية بالإعلانات ونطاقاتها. من الأسهل فهم هذا السلوك عن طريق تصور الإعلانات على أنها مرفوعة إلى أعلى نطاقها. دعنا نشرح ذلك ببعض الأمثلة.

console.log(foo); // undefined var foo = 1; console.log(foo); // 1

يتم رفع جسم إعلانات الدالة بينما يتم رفع إعلان المتغير فقط في تعبيرات الدالة (المكتوبة على شكل إعلانات متغيرات).

// إعلان دالة console.log(foo); // [Function: foo] foo(); // 'FOOOOO' function foo() { console.log('FOOOOO'); } console.log(foo); // [Function: foo] // تعبير دالة console.log(bar); // undefined bar(); // Uncaught TypeError: bar is not a function var bar = function () { console.log('BARRRR'); }; console.log(bar); // [Function: bar]

يتم رفع المتغيرات المعلنة عبر let و const أيضًا. ومع ذلك، على عكس var و function، لا يتم تهيئتها وسيؤدي الوصول إليها قبل الإعلان إلى استثناء ReferenceError. يكون المتغير في "منطقة زمنية ميتة" من بداية الكتلة حتى تتم معالجة الإعلان.

صف فقاعة الحدث (event bubbling).

عندما يتم تشغيل حدث على عنصر DOM، فإنه سيحاول التعامل مع الحدث إذا كان هناك مستمع مرفق، ثم ينتقل الحدث إلى العنصر الأصلي ويحدث نفس الشيء. يحدث هذا التراص صعودًا عبر أسلاف العنصر وصولًا إلى document. فقاعة الحدث هي الآلية التي تقف وراء تفويض الأحداث.

ما الفرق بين "السمة" (attribute) و "الخاصية" (property)؟

السمات تُعرف في ترميز HTML ولكن الخصائص تُعرف في DOM. لتوضيح الفرق، تخيل أن لدينا حقل النص هذا في HTML الخاص بنا: <input type="text" value="Hello">.

const input = document.querySelector('input'); console.log(input.getAttribute('value')); // Hello console.log(input.value); // Hello

ولكن بعد تغيير قيمة حقل النص بإضافة "World!" إليه، يصبح هذا:

console.log(input.getAttribute('value')); // Hello console.log(input.value); // Hello World!

لماذا لا يُعد تمديد كائنات JavaScript المدمجة فكرة جيدة؟

تمديد كائن JavaScript مدمج/أصلي يعني إضافة خصائص/دوال إلى prototype الخاص به. بينما قد تبدو هذه فكرة جيدة في البداية، إلا أنها خطيرة في الممارسة العملية. تخيل أن التعليمات البرمجية الخاصة بك تستخدم بعض المكتبات التي تقوم كلتاهما بتمديد Array.prototype عن طريق إضافة نفس دالة contains، فإن التطبيقات ستلغي بعضها البعض وسيتعطل التعليمات البرمجية الخاصة بك إذا لم يكن سلوك هاتين الدالتين هو نفسه.

المرة الوحيدة التي قد ترغب فيها في تمديد كائن أصلي هي عندما تريد إنشاء polyfill، وذلك بتوفير تطبيق خاص بك لدالة جزء من مواصفات JavaScript ولكن قد لا تكون موجودة في متصفح المستخدم نظرًا لأنه متصفح أقدم.

الفرق بين حدث `load` للمستند وحدث `DOMContentLoaded` للمستند؟

يتم تشغيل حدث DOMContentLoaded عندما يتم تحميل وتحليل مستند HTML الأولي بالكامل، دون انتظار انتهاء تحميل أوراق الأنماط والصور والإطارات الفرعية.

يتم تشغيل حدث load لـ window فقط بعد تحميل DOM وجميع الموارد والأصول التابعة.

ما الفرق بين `==` و `===`؟

إن == هو عامل المساواة المجردة بينما === هو عامل المساواة الصارمة. سيقارن عامل التشغيل == من أجل المساواة بعد إجراء أي تحويلات ضرورية للأنواع. لن يقوم عامل التشغيل === بتحويل النوع، لذلك إذا كانت قيمتان ليستا من نفس النوع، فإن === سيعيد ببساطة false. عند استخدام ==، يمكن أن تحدث أشياء غريبة، مثل:

1 == '1'; // true 1 == [1]; // true 1 == true; // true 0 == ''; // true 0 == '0'; // true 0 == false; // true

نصيحتي هي عدم استخدام عامل التشغيل == أبدًا، باستثناء الراحة عند المقارنة بـ null أو undefined، حيث سيعود a == null بـ true إذا كانت a هي null أو undefined.

var a = null; console.log(a == null); // true console.log(a == undefined); // true

اشرح سياسة نفس الأصل (same-origin policy) فيما يتعلق بـ JavaScript.

تمنع سياسة نفس الأصل JavaScript من إجراء طلبات عبر حدود النطاقات. يُعرف الأصل بأنه مزيج من مخطط URI واسم المضيف ورقم المنفذ. تمنع هذه السياسة أي نص برمجي ضار على صفحة واحدة من الوصول إلى البيانات الحساسة على صفحة ويب أخرى من خلال نموذج كائن المستند الخاص بتلك الصفحة.

اجعل هذا يعمل:

duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]
function duplicate(arr) { return arr.concat(arr); } duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]

أو مع ES6:

const duplicate = (arr) => [...arr, ...arr]; duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]

لماذا يُطلق عليها "تعبير ثلاثي" (Ternary expression)، وماذا تشير كلمة "Ternary"؟

تشير كلمة "Ternary" إلى ثلاثة، والتعبير الثلاثي يقبل ثلاثة معاملات: شرط الاختبار، تعبير "ثم" (then)، وتعبير "وإلا" (else). التعبيرات الثلاثية ليست خاصة بـ JavaScript ولست متأكدًا لماذا هي مدرجة في هذه القائمة.

ما هو `"use strict";`؟ ما هي مزايا وعيوب استخدامه؟

'use strict' هي عبارة تستخدم لتمكين الوضع الصارم (strict mode) للنصوص البرمجية بأكملها أو للدوال الفردية. الوضع الصارم هو طريقة للاختيار في شكل مقيد من JavaScript.

المزايا:

  • يجعل من المستحيل إنشاء متغيرات عامة عن طريق الخطأ.
  • يجعل التعيينات التي كانت ستفشل بصمت في السابق ترمي استثناءً.
  • يجعل محاولات حذف الخصائص غير القابلة للحذف ترمي استثناءً (حيث كانت المحاولة في السابق ببساطة لا تحدث أي تأثير).
  • يتطلب أن تكون أسماء معلمات الدالة فريدة.
  • this يكون undefined في السياق العام.
  • يلتقط بعض الأخطاء البرمجية الشائعة، ويرمي استثناءات.
  • يعطل الميزات المربكة أو سيئة التصميم.

العيوب:

  • العديد من الميزات المفقودة التي قد يكون بعض المطورين معتادين عليها.
  • لا مزيد من الوصول إلى function.caller و function.arguments.
  • قد يسبب دمج النصوص البرمجية المكتوبة في أوضاع صارمة مختلفة مشاكل.

بشكل عام، أعتقد أن الفوائد تفوق العيوب، ولم أضطر أبدًا إلى الاعتماد على الميزات التي يحجبها الوضع الصارم. أوصي باستخدام الوضع الصارم.

أنشئ حلقة `for` تتكرر حتى `100` مع إخراج "**fizz**" عند مضاعفات `3`، و"**buzz**" عند مضاعفات `5`، و"**fizzbuzz**" عند مضاعفات `3` و `5`.

تحقق من هذا الإصدار من FizzBuzz بواسطة [Paul Irish].

for (let i = 1; i <= 100; i++) { let f = i % 3 == 0, b = i % 5 == 0; console.log(f ? (b ? 'FizzBuzz' : 'Fizz') : b ? 'Buzz' : i); }

لا أنصحك بكتابة ما ورد أعلاه أثناء المقابلات. التزم بالنهج الطويل ولكن الواضح. لمزيد من الإصدارات الغريبة من FizzBuzz، تحقق من رابط المرجع أدناه.

لماذا من الجيد، بشكل عام، ترك النطاق العام لموقع الويب كما هو وعدم لمسه أبدًا؟

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

لماذا تستخدم شيئًا مثل حدث `load`؟ هل لهذا الحدث عيوب؟ هل تعرف أي بدائل، ولماذا تستخدمها؟

يتم تشغيل حدث load في نهاية عملية تحميل المستند. في هذه النقطة، تكون جميع الكائنات في المستند موجودة في DOM، وقد انتهت جميع الصور والنصوص البرمجية والروابط والإطارات الفرعية من التحميل.

سيتم تشغيل حدث DOM DOMContentLoaded بعد بناء DOM للصفحة، ولكنه لا ينتظر انتهاء تحميل الموارد الأخرى. يُفضل هذا في حالات معينة عندما لا تحتاج إلى تحميل الصفحة بالكامل قبل التهيئة.

اشرح ما هو تطبيق الصفحة الواحدة (single page app) وكيفية جعله صديقًا لمحركات البحث (SEO-friendly).

ما يلي مأخوذ من [Grab Front End Guide] الرائع، والذي كتبته أنا بالمناسبة!

يشير مطورو الويب في هذه الأيام إلى المنتجات التي يبنونها كتطبيقات ويب، بدلاً من مواقع ويب. بينما لا يوجد فرق صارم بين المصطلحين، تميل تطبيقات الويب إلى أن تكون تفاعلية وديناميكية للغاية، مما يسمح للمستخدم بأداء الإجراءات وتلقي استجابة لإجرائه. تقليديًا، يتلقى المتصفح HTML من الخادم ويعرضه. عندما يتنقل المستخدم إلى عنوان URL آخر، يتطلب ذلك تحديثًا كاملًا للصفحة ويرسل الخادم HTML جديدًا للصفحة الجديدة. وهذا ما يسمى العرض من جانب الخادم (server-side rendering).

ومع ذلك، في تطبيقات الصفحة الواحدة الحديثة، يتم استخدام العرض من جانب العميل (client-side rendering) بدلاً من ذلك. يقوم المتصفح بتحميل الصفحة الأولية من الخادم، بالإضافة إلى النصوص البرمجية (الأطر، المكتبات، كود التطبيق) وأوراق الأنماط المطلوبة للتطبيق بأكمله. عندما يتنقل المستخدم إلى صفحات أخرى، لا يتم تشغيل تحديث للصفحة. يتم تحديث عنوان URL للصفحة عبر [HTML5 History API]. يتم استرداد البيانات الجديدة المطلوبة للصفحة الجديدة، عادةً بتنسيق JSON، بواسطة المتصفح عبر طلبات [AJAX] إلى الخادم. يقوم تطبيق الصفحة الواحدة بعد ذلك بتحديث الصفحة ديناميكيًا بالبيانات عبر JavaScript، التي قام بتنزيلها بالفعل عند التحميل الأولي للصفحة. هذا النموذج مشابه لكيفية عمل تطبيقات الهاتف المحمول الأصلية.

الفوائد:

  • يبدو التطبيق أكثر استجابة ولا يرى المستخدمون الوميض بين التنقلات بين الصفحات بسبب تحديثات الصفحة الكاملة.
  • يتم إرسال طلبات HTTP أقل إلى الخادم، حيث لا يتعين تنزيل نفس الأصول مرة أخرى لكل تحميل صفحة.
  • فصل واضح للاهتمامات بين العميل والخادم؛ يمكنك بسهولة بناء عملاء جدد لمنصات مختلفة (مثل الأجهزة المحمولة، روبوتات الدردشة، الساعات الذكية) دون الحاجة إلى تعديل كود الخادم. يمكنك أيضًا تعديل مكدس التكنولوجيا على العميل والخادم بشكل مستقل، طالما أن عقد API لم يتم خرقه.

السلبيات:

  • تحميل أولي أثقل للصفحة بسبب تحميل الإطار وكود التطبيق والأصول المطلوبة لصفحات متعددة.
  • هناك خطوة إضافية يجب القيام بها على الخادم الخاص بك وهي تهيئته لتوجيه جميع الطلبات إلى نقطة دخول واحدة والسماح للتوجيه من جانب العميل بتولي المسؤولية من هناك.
  • تعتمد تطبيقات الصفحة الواحدة على JavaScript لعرض المحتوى، ولكن لا تقوم جميع محركات البحث بتنفيذ JavaScript أثناء الزحف، وقد ترى محتوى فارغًا على صفحتك. وهذا يضر عن غير قصد بتحسين محركات البحث (SEO) لتطبيقك. ومع ذلك، في معظم الأحيان، عند بناء التطبيقات، لا يكون تحسين محركات البحث هو العامل الأكثر أهمية، حيث لا يحتاج كل المحتوى إلى أن يكون قابلاً للفهرسة بواسطة محركات البحث. للتغلب على ذلك، يمكنك إما عرض تطبيقك من جانب الخادم أو استخدام خدمات مثل [Prerender] لـ "عرض JavaScript الخاص بك في متصفح، وحفظ HTML الثابت، وإعادته إلى برامج الزحف".

ما هو مدى خبرتك في الوعود (Promises) و/أو polyfills الخاصة بها؟

أمتلك معرفة عملية بها. الوعد هو كائن قد ينتج قيمة واحدة في وقت ما في المستقبل: إما قيمة تم حلها أو سبب لعدم حلها (على سبيل المثال، حدث خطأ في الشبكة). قد يكون الوعد في إحدى الحالات الثلاث الممكنة: تم الوفاء به (fulfilled)، تم رفضه (rejected)، أو معلق (pending). يمكن لمستخدمي الوعد إرفاق دوال رد نداء للتعامل مع القيمة التي تم الوفاء بها أو سبب الرفض.

بعض الـ polyfills الشائعة هي $.deferred و Q و Bluebird ولكن ليس كلها تتوافق مع المواصفات. يدعم ES2015 الوعود بشكل مباشر، وعادة ما لا تكون هناك حاجة إلى polyfills في هذه الأيام.

ما هي إيجابيات وسلبيات استخدام الوعود (Promises) بدلاً من دوال رد النداء (callbacks)؟

الإيجابيات

  • تجنب جحيم رد النداء (callback hell) الذي يمكن أن يكون غير قابل للقراءة.
  • يجعل من السهل كتابة تعليمات برمجية غير متزامنة متسلسلة وقابلة للقراءة باستخدام .then().
  • يجعل من السهل كتابة تعليمات برمجية غير متزامنة متوازية باستخدام Promise.all().
  • مع الوعود، لن تحدث هذه السيناريوهات الموجودة في البرمجة التي تعتمد على رد النداء فقط:
    • استدعاء رد النداء مبكرًا جدًا
    • استدعاء رد النداء متأخرًا جدًا (أو أبدًا)
    • استدعاء رد النداء عددًا قليلًا جدًا أو كثيرًا جدًا من المرات
    • الفشل في تمرير أي بيئة/معلمات ضرورية
    • إخفاء أي أخطاء/استثناءات قد تحدث

السلبيات

  • كود أكثر تعقيدًا قليلاً (محل نقاش).
  • في المتصفحات القديمة التي لا تدعم ES2015، تحتاج إلى تحميل polyfill لاستخدامه.

ما هي بعض مزايا/عيوب كتابة كود JavaScript بلغة تُجمّع إلى JavaScript؟

تتضمن بعض أمثلة اللغات التي تُجمع إلى JavaScript: CoffeeScript، Elm، ClojureScript، PureScript، و TypeScript.

المزايا:

  • تُصلح بعض المشاكل الطويلة الأمد في JavaScript وتُثبط أنماط JavaScript السيئة (anti-patterns).
  • تمكنك من كتابة كود أقصر، من خلال توفير بعض التيسيرات النحوية (syntactic sugar) فوق JavaScript، والتي أعتقد أن ES5 يفتقر إليها، ولكن ES2015 رائع.
  • الأنواع الثابتة رائعة (في حالة TypeScript) للمشاريع الكبيرة التي تحتاج إلى صيانه بمرور الوقت.

العيوب:

  • تتطلب عملية بناء/تجميع حيث تعمل المتصفحات فقط على JavaScript، وسيحتاج الكود الخاص بك إلى التجميع إلى JavaScript قبل تقديمه للمتصفحات.
  • قد يكون التصحيح مؤلمًا إذا لم تتطابق خرائط المصدر (source maps) بشكل جيد مع المصدر المُجمع مسبقًا.
  • معظم المطورين ليسوا على دراية بهذه اللغات وسيحتاجون إلى تعلمها. هناك تكلفة صاعدة متضمنة لفريقك إذا استخدمتها لمشاريعك.
  • مجتمع أصغر (يعتمد على اللغة)، مما يعني أن الموارد والبرامج التعليمية والمكتبات والأدوات سيكون من الصعب العثور عليها.
  • قد يكون دعم IDE/المحرر ضعيفًا.
  • ستكون هذه اللغات دائمًا متأخرة عن أحدث معيار JavaScript.
  • يجب أن يكون المطورون مدركين لما يتم تجميع الكود الخاص بهم إليه - لأن هذا هو ما سيتم تشغيله بالفعل، وهذا هو ما يهم في النهاية.

عمليًا، لقد حسّن ES2015 JavaScript بشكل كبير وجعله أجمل بكثير للكتابة. لا أرى حقًا الحاجة إلى CoffeeScript في هذه الأيام.

ما هي الأدوات والتقنيات التي تستخدمها لتصحيح أخطاء كود JavaScript؟

  • React و Redux
    • [React Devtools]
    • [Redux Devtools]
  • Vue
    • [Vue Devtools]
  • JavaScript
    • [Chrome Devtools]
    • عبارة debugger
    • تصحيح الأخطاء باستخدام console.log القديم الجيد

ما هي هياكل اللغة التي تستخدمها للتكرار على خصائص الكائن وعناصر المصفوفة؟

للكائنات:

  • حلقات for-in - for (var property in obj) { console.log(property); }. ومع ذلك، ستقوم هذه الحلقات أيضًا بالتكرار عبر خصائصها الموروثة، وستضيف فحص obj.hasOwnProperty(property) قبل استخدامها.
  • Object.keys() - Object.keys(obj).forEach(function (property) { ... }). Object.keys() هي دالة ثابتة ستدرج جميع الخصائص القابلة للتعداد للكائن الذي تمرره إليها.
  • Object.getOwnPropertyNames() - Object.getOwnPropertyNames(obj).forEach(function (property) { ... }). Object.getOwnPropertyNames() هي دالة ثابتة ستدرج جميع الخصائص القابلة للتعداد وغير القابلة للتعداد للكائن الذي تمرره إليها.

للمصفوفات:

  • حلقات for - for (var i = 0; i < arr.length; i++). المشكلة الشائعة هنا هي أن var يكون في نطاق الدالة وليس نطاق الكتلة، وفي معظم الأحيان سترغب في متغير مكرر بنطاق كتلة. يقدم ES2015 let الذي له نطاق كتلة ويوصى باستخدامه بدلاً من ذلك. لذا يصبح هذا: for (let i = 0; i < arr.length; i++).
  • forEach - arr.forEach(function (el, index) { ... }). يمكن أن يكون هذا البناء أكثر ملاءمة في بعض الأحيان لأنك لست مضطرًا لاستخدام index إذا كان كل ما تحتاجه هو عناصر المصفوفة. توجد أيضًا طرق every و some التي ستسمح لك بإنهاء التكرار مبكرًا.
  • حلقات for-of - for (let elem of arr) { ... }. يقدم ES6 حلقة جديدة، وهي حلقة for-of، التي تسمح لك بالتكرار على الكائنات التي تتوافق مع بروتوكول التكرار مثل String و Array و Map و Set وما إلى ذلك. إنها تجمع بين مزايا حلقة for وطريقة forEach(). ميزة حلقة for هي أنه يمكنك الخروج منها، وميزة forEach() هي أنها أكثر إيجازًا من حلقة for لأنك لا تحتاج إلى متغير عداد. مع حلقة for-of، تحصل على كل من القدرة على الخروج من حلقة وبناء جملة أكثر إيجازًا.

في معظم الأحيان، أفضّل طريقة .forEach، ولكن الأمر يعتمد حقًا على ما تحاول فعله. قبل ES6، كنا نستخدم حلقات for عندما كنا بحاجة إلى إنهاء الحلقة مبكرًا باستخدام break. ولكن الآن مع ES6، يمكننا القيام بذلك باستخدام حلقات for-of. سأستخدم حلقات for عندما أحتاج إلى مرونة أكبر، مثل زيادة المكرر أكثر من مرة لكل حلقة.

أيضًا، عند استخدام حلقة for-of، إذا كنت بحاجة إلى الوصول إلى كل من فهرس وقيمة كل عنصر من عناصر المصفوفة، يمكنك القيام بذلك باستخدام طريقة entries() لمصفوفة ES6 وفك التحديد:

const arr = ['a', 'b', 'c']; for (let [index, elem] of arr.entries()) { console.log(index, ': ', elem); }

اشرح الفرق بين الكائنات القابلة للتعديل (mutable) والكائنات غير القابلة للتعديل (immutable).

الخالدة (Immutability) هي مبدأ أساسي في البرمجة الوظيفية، ولها الكثير لتقدمه للبرامج الموجهة للكائنات أيضًا. الكائن القابل للتعديل هو كائن يمكن تعديل حالته بعد إنشائه. الكائن غير القابل للتعديل هو كائن لا يمكن تعديل حالته بعد إنشائه.

ما هو مثال على كائن غير قابل للتعديل في JavaScript؟

في JavaScript، بعض الأنواع المدمجة (الأرقام، السلاسل) غير قابلة للتعديل، ولكن الكائنات المخصصة قابلة للتعديل بشكل عام.

بعض كائنات JavaScript المدمجة غير القابلة للتعديل هي Math، Date.

فيما يلي بعض الطرق لإضافة/محاكاة الخالدة على كائنات JavaScript العادية.

خصائص الكائن الثابتة

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

let myObject = {}; Object.defineProperty(myObject, 'number', { value: 42, writable: false, configurable: false, }); console.log(myObject.number); // 42 myObject.number = 43; console.log(myObject.number); // 42

منع التوسعات

إذا كنت ترغب في منع إضافة خصائص جديدة إلى كائن، ولكن ترك باقي خصائص الكائن كما هي، فاستدعِ Object.preventExtensions(...):

var myObject = { a: 2, }; Object.preventExtensions(myObject); myObject.b = 3; myObject.b; // undefined

في الوضع غير الصارم، يفشل إنشاء b بصمت. في الوضع الصارم، يرمي TypeError.

Seal

Object.seal() ينشئ كائنًا "مختومًا"، مما يعني أنه يأخذ كائنًا موجودًا ويستدعي بشكل أساسي Object.preventExtensions() عليه، ولكنه أيضًا يحدد جميع خصائصه الموجودة على أنها configurable: false.

لذلك، لا يمكنك فقط إضافة المزيد من الخصائص، ولكن لا يمكنك أيضًا إعادة تكوين أو حذف أي خصائص موجودة (على الرغم من أنه لا يزال بإمكانك تعديل قيمها).

Freeze

Object.freeze() ينشئ كائنًا مجمدًا، مما يعني أنه يأخذ كائنًا موجودًا ويستدعي بشكل أساسي Object.seal() عليه، ولكنه أيضًا يحدد جميع خصائص "الوصول إلى البيانات" على أنها writable: false، بحيث لا يمكن تغيير قيمها.

هذا النهج هو أعلى مستوى من الخالدة يمكنك تحقيقه لكائن نفسه، حيث يمنع أي تغييرات على الكائن أو على أي من خصائصه المباشرة (على الرغم من أن محتويات أي كائنات أخرى مشار إليها لا تتأثر، كما ذكر أعلاه).

var immutable = Object.freeze({});

تجميد الكائن لا يسمح بإضافة خصائص جديدة إلى الكائن ويمنع إزالة أو تغيير الخصائص الموجودة. يحافظ Object.freeze() على قابلية التعداد، قابلية التكوين، قابلية الكتابة، ونموذج الكائن. يعيد الكائن الذي تم تمريره ولا ينشئ نسخة مجمدة.

ما هي إيجابيات وسلبيات الخالدة؟

الإيجابيات

  • أسهل في اكتشاف التغييرات - يمكن تحديد مساواة الكائنات بطريقة فعالة وسهلة من خلال المساواة المرجعية. هذا مفيد لمقارنة اختلافات الكائنات في React و Redux.
  • البرامج ذات الكائنات غير القابلة للتعديل أقل تعقيدًا في التفكير، حيث لا داعي للقلق بشأن كيفية تطور الكائن بمرور الوقت.
  • لم تعد النسخ الدفاعية ضرورية عندما يتم إرجاع الكائنات غير القابلة للتعديل من الدوال أو تمريرها إليها، حيث لا يوجد احتمال لتعديل كائن غير قابل للتعديل بواسطتها.
  • سهولة المشاركة عبر المراجع - نسخة واحدة من الكائن جيدة تمامًا مثل الأخرى، لذا يمكنك تخزين الكائنات مؤقتًا أو إعادة استخدام نفس الكائن عدة مرات.
  • آمنة للاستخدام المتعدد الخيوط - يمكن استخدام الكائنات غير القابلة للتعديل بأمان بين الخيوط في بيئة متعددة الخيوط حيث لا يوجد خطر من تعديلها في خيوط أخرى تعمل بالتوازي.
  • باستخدام مكتبات مثل ImmutableJS، يتم تعديل الكائنات باستخدام المشاركة الهيكلية ويحتاج إلى ذاكرة أقل لوجود كائنات متعددة ذات هياكل متشابهة.

السلبيات

  • يمكن أن تؤدي التطبيقات البسيطة لهياكل البيانات غير القابلة للتعديل وعملياتها إلى أداء ضعيف للغاية لأنه يتم إنشاء كائنات جديدة في كل مرة. يوصى باستخدام المكتبات لهياكل البيانات غير القابلة للتعديل الفعالة والعمليات التي تستفيد من المشاركة الهيكلية.
  • يمكن أن يتسبب تخصيص (وإلغاء تخصيص) العديد من الكائنات الصغيرة بدلاً من تعديل الكائنات الموجودة في تأثير على الأداء. تعتمد تعقيد المخصص أو جامع القمامة عادةً على عدد الكائنات في الكومة.
  • من الصعب بناء هياكل البيانات الدورية مثل الرسوم البيانية. إذا كان لديك كائنان لا يمكن تعديلهما بعد التهيئة، فكيف يمكنك جعلهما يشيران إلى بعضهما البعض؟

كيف يمكنك تحقيق الخالدة في الكود الخاص بك؟

البديل هو استخدام تصريحات const جنبًا إلى جنب مع التقنيات المذكورة أعلاه للإنشاء. لـ "تغيير" الكائنات، استخدم عامل الانتشار (spread operator)، Object.assign، Array.concat()، وما إلى ذلك، لإنشاء كائنات جديدة بدلاً من تغيير الكائن الأصلي.

أمثلة:

// مثال على المصفوفة const arr = [1, 2, 3]; const newArr = [...arr, 4]; // [1, 2, 3, 4] // مثال على الكائن const human = Object.freeze({ race: 'human' }); const john = { ...human, name: 'John' }; // {race: "human", name: "John"} const alienJohn = { ...john, race: 'alien' }; // {race: "alien", name: "John"}

اشرح الفرق بين الدوال المتزامنة (synchronous) وغير المتزامنة (asynchronous).

الدوال المتزامنة تكون محجوبة (blocking) بينما الدوال غير المتزامنة ليست كذلك. في الدوال المتزامنة، تكتمل العبارات قبل تشغيل العبارة التالية. في هذه الحالة، يتم تقييم البرنامج بالترتيب الدقيق للعبارات ويتوقف تنفيذ البرنامج إذا استغرقت إحدى العبارات وقتًا طويلاً جدًا.

عادةً ما تقبل الدوال غير المتزامنة دالة رد نداء كمعامل ويستمر التنفيذ في السطر التالي فور استدعاء الدالة غير المتزامنة. لا يتم استدعاء دالة رد النداء إلا عندما تكتمل العملية غير المتزامنة وتكون مكدس الاستدعاءات فارغًا. يجب إجراء العمليات الثقيلة مثل تحميل البيانات من خادم الويب أو استعلام قاعدة بيانات بشكل غير متزامن حتى يتمكن الخيط الرئيسي من الاستمرار في تنفيذ عمليات أخرى بدلاً من الحجب حتى تكتمل تلك العملية الطويلة (في حالة المتصفحات، ستتوقف واجهة المستخدم).

ما هي حلقة الأحداث (event loop)؟ ما الفرق بين مكدس الاستدعاءات (call stack) وقائمة انتظار المهام (task queue)؟

حلقة الأحداث هي حلقة أحادية الخيط تراقب مكدس الاستدعاءات وتتحقق مما إذا كان هناك أي عمل يجب القيام به في قائمة انتظار المهام. إذا كان مكدس الاستدعاءات فارغًا وكانت هناك دوال رد نداء في قائمة انتظار المهام، يتم إخراج دالة من قائمة الانتظار ودفعها إلى مكدس الاستدعاءات لتنفيذها.

إذا لم تكن قد شاهدت حديث فيليب روبرت عن حلقة الأحداث، فيجب عليك ذلك. إنه أحد أكثر مقاطع الفيديو مشاهدة حول JavaScript.

اشرح الفروقات في استخدام `foo` بين `function foo() {}` و `var foo = function() {}`

الأول هو إعلان دالة بينما الأخير هو تعبير دالة. الفرق الرئيسي هو أن إعلانات الدالة يتم رفع جسمها بينما أجسام تعبيرات الدالة لا يتم رفعها (فهي تتمتع بنفس سلوك الرفع مثل المتغيرات). لمزيد من الشرح حول الرفع، ارجع إلى السؤال أعلاه حول الرفع. إذا حاولت استدعاء تعبير دالة قبل تعريفه، فستحصل على خطأ Uncaught TypeError: XXX is not a function.

إعلان دالة

foo(); // 'FOOOOO' function foo() { console.log('FOOOOO'); }

تعبير دالة

foo(); // Uncaught TypeError: foo is not a function var foo = function () { console.log('FOOOOO'); };

ما هي الفروقات بين المتغيرات التي تم إنشاؤها باستخدام `let` أو `var` أو `const`؟

المتغيرات المعلنة باستخدام الكلمة المفتاحية var تكون محددة النطاق للدالة التي تم إنشاؤها فيها، أو إذا تم إنشاؤها خارج أي دالة، تكون للكائن العام. let و const هما محددا النطاق للكتلة، مما يعني أنهما يمكن الوصول إليهما فقط داخل أقرب مجموعة من الأقواس المعقوفة (دالة، كتلة if-else، أو حلقة for).

function foo() { // جميع المتغيرات يمكن الوصول إليها داخل الدوال. var bar = 'bar'; let baz = 'baz'; const qux = 'qux'; console.log(bar); // bar console.log(baz); // baz console.log(qux); // qux } console.log(bar); // ReferenceError: bar is not defined console.log(baz); // ReferenceError: baz is not defined console.log(qux); // ReferenceError: qux is not defined
if (true) { var bar = 'bar'; let baz = 'baz'; const qux = 'qux'; } // المتغيرات المعلنة بـ var يمكن الوصول إليها في أي مكان في نطاق الدالة. console.log(bar); // bar // المتغيرات المعرفة بـ let و const لا يمكن الوصول إليها خارج الكتلة التي تم تعريفها فيها. console.log(baz); // ReferenceError: baz is not defined console.log(qux); // ReferenceError: qux is not defined

يسمح var برفع المتغيرات، مما يعني أنه يمكن الإشارة إليها في الكود قبل الإعلان عنها. let و const لا يسمحان بذلك، بل يرميان خطأ.

console.log(foo); // undefined var foo = 'foo'; console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initialization let baz = 'baz'; console.log(bar); // ReferenceError: can't access lexical declaration 'bar' before initialization const bar = 'bar';

إعادة الإعلان عن متغير بـ var لن يرمي خطأ، لكن let و const سيفعلان ذلك.

var foo = 'foo'; var foo = 'bar'; console.log(foo); // "bar" let baz = 'baz'; let baz = 'qux'; // Uncaught SyntaxError: Identifier 'baz' has already been declared

يختلف let و const في أن let يسمح بإعادة تعيين قيمة المتغير بينما const لا يسمح بذلك.

// هذا جيد. let foo = 'foo'; foo = 'bar'; // هذا يسبب استثناء. const baz = 'baz'; baz = 'qux';

ما هي الفروقات بين فئة ES6 ومنشئات دوال ES5؟

دعنا ننظر أولاً إلى مثال لكل منهما:

// مُنشئ دالة ES5 function Person(name) { this.name = name; } // فئة ES6 class Person { constructor(name) { this.name = name; } }

بالنسبة للمنشئات البسيطة، تبدو متشابهة جدًا.

الفرق الرئيسي في المنشئ يأتي عند استخدام الوراثة. إذا أردنا إنشاء فئة Student ترث من Person وإضافة حقل studentId، فهذا ما يتعين علينا القيام به بالإضافة إلى ما سبق.

// مُنشئ دالة ES5 function Student(name, studentId) { // استدعاء مُنشئ الفئة العليا لتهيئة الأعضاء المشتقة من الفئة العليا. Person.call(this, name); // تهيئة أعضاء الفئة الفرعية الخاصة. this.studentId = studentId; } Student.prototype = Object.create(Person.prototype); Student.prototype.constructor = Student; // فئة ES6 class Student extends Person { constructor(name, studentId) { super(name); this.studentId = studentId; } }

استخدام الوراثة في ES5 أكثر إطالة بكثير، وإصدار ES6 أسهل في الفهم والتذكر.

هل يمكنك تقديم حالة استخدام لبنية دالة السهم الجديدة `=>`؟ كيف تختلف هذه البنية الجديدة عن الدوال الأخرى؟

إحدى الفوائد الواضحة لدوال السهم هي تبسيط بناء الجملة اللازم لإنشاء الدوال، دون الحاجة إلى الكلمة المفتاحية function. يتم أيضًا ربط this داخل دوال السهم بالنطاق المحيط، وهو ما يختلف مقارنة بالدوال العادية حيث يتم تحديد this بواسطة الكائن الذي يستدعيها. يكون this المحيط لغويًا مفيدًا عند استدعاء دوال رد النداء خاصة في مكونات React.

ما هي ميزة استخدام بناء دالة السهم (arrow syntax) لطريقة (method) في المُنشئ (constructor)؟

الميزة الرئيسية لاستخدام دالة سهم كطريقة داخل مُنشئ هي أن قيمة this تُحدد وقت إنشاء الدالة ولا يمكن تغييرها بعد ذلك. لذلك، عندما يُستخدم المُنشئ لإنشاء كائن جديد، سيشير this دائمًا إلى هذا الكائن. على سبيل المثال، لنفترض أن لدينا مُنشئ Person يقبل الاسم الأول كحجة ولديه طريقتان لـ console.log هذا الاسم، إحداهما كدالة عادية والأخرى كدالة سهم:

const Person = function (firstName) { this.firstName = firstName; this.sayName1 = function () { console.log(this.firstName); }; this.sayName2 = () => { console.log(this.firstName); }; }; const john = new Person('John'); const dave = new Person('Dave'); john.sayName1(); // John john.sayName2(); // John // يمكن تغيير قيمة 'this' للدالة العادية، لكن لا يمكن تغييرها لدالة السهم john.sayName1.call(dave); // Dave (لأن "this" أصبح الآن كائن dave) john.sayName2.call(dave); // John john.sayName1.apply(dave); // Dave (لأن 'this' أصبح الآن كائن dave) john.sayName2.apply(dave); // John john.sayName1.bind(dave)(); // Dave (لأن 'this' أصبح الآن كائن dave) john.sayName2.bind(dave)(); // John var sayNameFromWindow1 = john.sayName1; sayNameFromWindow1(); // undefined (لأن 'this' أصبح الآن كائن window) var sayNameFromWindow2 = john.sayName2; sayNameFromWindow2(); // John

النقطة الرئيسية هنا هي أن this يمكن تغييره لدالة عادية، ولكن السياق يظل دائمًا كما هو لدالة السهم. لذلك حتى إذا كنت تمرر دالة السهم الخاصة بك إلى أجزاء مختلفة من تطبيقك، فلن تضطر إلى القلق بشأن تغيير السياق.

يمكن أن يكون هذا مفيدًا بشكل خاص في مكونات فئة React. إذا قمت بتعريف طريقة فئة لشيء مثل معالج النقر باستخدام دالة عادية، ثم مررت معالج النقر هذا إلى مكون فرعي كـ prop، فستحتاج أيضًا إلى ربط this في مُنشئ المكون الأب. إذا استخدمت بدلاً من ذلك دالة سهم، فلا داعي لربط "this" أيضًا، حيث ستحصل الطريقة تلقائيًا على قيمة "this" من سياقها المعجمي المحيط.

ما هو تعريف الدالة ذات الترتيب الأعلى (higher-order function)؟

الدالة ذات الترتيب الأعلى هي أي دالة تأخذ دالة واحدة أو أكثر كحجج، والتي تستخدمها للعمل على بعض البيانات، و/أو تُرجع دالة كنتيجة. تهدف الدوال ذات الترتيب الأعلى إلى تجريد عملية يتم تنفيذها بشكل متكرر. المثال الكلاسيكي على ذلك هو map، الذي يأخذ مصفوفة ودالة كحجج. ثم يستخدم map هذه الدالة لتحويل كل عنصر في المصفوفة، مع إرجاع مصفوفة جديدة تحتوي على البيانات المحولة. أمثلة شائعة أخرى في JavaScript هي forEach و filter و reduce. لا يجب أن تكون الدالة ذات الترتيب الأعلى تتلاعب بالمصفوفات فقط حيث توجد العديد من حالات الاستخدام لإرجاع دالة من دالة أخرى. Function.prototype.bind هو أحد هذه الأمثلة في JavaScript.

Map

لنفترض أن لدينا مصفوفة من الأسماء نحتاج إلى تحويل كل سلسلة إلى أحرف كبيرة.

const names = ['irish', 'daisy', 'anna'];

الطريقة الأمرية ستكون كالتالي:

const transformNamesToUppercase = function (names) { const results = []; for (let i = 0; i < names.length; i++) { results.push(names[i].toUpperCase()); } return results; }; transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']

استخدام .map(transformerFn) يجعل الكود أقصر وأكثر تصريحية.

const transformNamesToUppercase = function (names) { return names.map((name) => name.toUpperCase()); }; transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']

هل يمكنك إعطاء مثال لفك هيكلة كائن أو مصفوفة؟

فك الهيكلة (Destructuring) هو تعبير متاح في ES6 يتيح طريقة موجزة ومريحة لاستخراج قيم الكائنات أو المصفوفات ووضعها في متغيرات منفصلة.

فك هيكلة المصفوفة

// تعيين المتغيرات. const foo = ['one', 'two', 'three']; const [one, two, three] = foo; console.log(one); // "one" console.log(two); // "two" console.log(three); // "three"
// تبديل المتغيرات let a = 1; let b = 3; [a, b] = [b, a]; console.log(a); // 3 console.log(b); // 1

فك هيكلة الكائن

// تعيين المتغيرات. const o = { p: 42, q: true }; const { p, q } = o; console.log(p); // 42 console.log(q); // true

توفر قوالب ES6 الحرفية مرونة كبيرة في إنشاء السلاسل النصية، هل يمكنك إعطاء مثال؟

تساعد القوالب الحرفية على تسهيل عملية دمج السلاسل النصية (string interpolation)، أو تضمين المتغيرات في سلسلة نصية. قبل ES2015، كان من الشائع القيام بشيء من هذا القبيل:

var person = { name: 'Tyler', age: 28 }; console.log( 'Hi, my name is ' + person.name + ' and I am ' + person.age + ' years old!', ); // 'Hi, my name is Tyler and I am 28 years old!'

مع القوالب الحرفية، يمكنك الآن إنشاء نفس الإخراج بهذه الطريقة بدلاً من ذلك:

const person = { name: 'Tyler', age: 28 }; console.log(`Hi, my name is ${person.name} and I am ${person.age} years old!`); // 'Hi, my name is Tyler and I am 28 years old!'

لاحظ أنك تستخدم علامات الاقتباس الخلفية (backticks)، وليس علامات الاقتباس العادية، للإشارة إلى أنك تستخدم قالبًا حرفيًا وأنه يمكنك إدراج تعبيرات داخل علامات ${}.

حالة استخدام مفيدة ثانية هي في إنشاء سلاسل نصية متعددة الأسطر. قبل ES2015، يمكنك إنشاء سلسلة نصية متعددة الأسطر بهذا الشكل:

console.log('This is line one.\nThis is line two.'); // This is line one. // This is line two.

أو إذا كنت ترغب في تقسيمها إلى عدة أسطر في الكود الخاص بك حتى لا تضطر إلى التمرير إلى اليمين في محرر النصوص لقراءة سلسلة نصية طويلة، يمكنك أيضًا كتابتها بهذا الشكل:

console.log('This is line one.\n' + 'This is line two.'); // This is line one. // This is line two.

ومع ذلك، تحافظ القوالب الحرفية على أي مسافات تضيفها إليها. على سبيل المثال، لإنشاء نفس الإخراج متعدد الأسطر الذي أنشأناه أعلاه، يمكنك ببساطة القيام بما يلي:

console.log(`This is line one. This is line two.`); // This is line one. // This is line two.

حالة استخدام أخرى للقوالب الحرفية ستكون استخدامها كبديل لمكتبات القوالب لعمليات دمج المتغيرات البسيطة:

const person = { name: 'Tyler', age: 28 }; document.body.innerHTML = ` <div> <p>Name: ${person.name}</p> <p>Age: ${person.age}</p> </div> `;

لاحظ أن الكود الخاص بك قد يكون عرضة لهجمات XSS باستخدام .innerHTML. قم بتنقية بياناتك قبل عرضها إذا كانت قادمة من مستخدم!

هل يمكنك إعطاء مثال لدالة التقسيم (curry function) ولماذا توفر هذه البنية ميزة؟

التقسيم (Currying) هو نمط يتم فيه تقسيم دالة تحتوي على أكثر من معلمة إلى دوال متعددة، والتي عند استدعائها بالتسلسل، ستجمع جميع المعلمات المطلوبة واحدة تلو الأخرى. يمكن أن تكون هذه التقنية مفيدة لجعل الكود المكتوب بأسلوب وظيفي أسهل في القراءة والتركيب. من المهم ملاحظة أنه لكي يتم تقسيم الدالة، يجب أن تبدأ كدالة واحدة، ثم يتم تقسيمها إلى سلسلة من الدوال التي تقبل كل منها معلمة واحدة.

function curry(fn) { if (fn.length === 0) { return fn; } function _curried(depth, args) { return function (newArgument) { if (depth - 1 === 0) { return fn(...args, newArgument); } return _curried(depth - 1, [...args, newArgument]); }; } return _curried(fn.length, []); } function add(a, b) { return a + b; } var curriedAdd = curry(add); var addFive = curriedAdd(5); var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]

ما هي فوائد استخدام بناء الانتشار (spread syntax) وكيف يختلف عن بناء التجميع (rest syntax)؟

يعتبر بناء الانتشار في ES6 مفيدًا جدًا عند البرمجة بأسلوب وظيفي حيث يمكننا بسهولة إنشاء نسخ من المصفوفات أو الكائنات دون اللجوء إلى Object.create أو slice أو دالة مكتبية. تُستخدم هذه الميزة اللغوية غالبًا في مشاريع Redux و RxJS.

function putDookieInAnyArray(arr) { return [...arr, 'dookie']; } const result = putDookieInAnyArray(['I', 'really', "don't", 'like']); // ["I", "really", "don't", "like", "dookie"] const person = { name: 'Todd', age: 29, }; const copyOfTodd = { ...person };

يقدم بناء التجميع في ES6 اختصارًا لتضمين عدد عشوائي من الحجج التي سيتم تمريرها إلى دالة. إنه يشبه معكوس بناء الانتشار، حيث يأخذ البيانات ويضعها في مصفوفة بدلاً من تفريغ مصفوفة من البيانات، ويعمل في حجج الدوال، وكذلك في تعيينات فك هيكلة المصفوفات والكائنات.

function addFiveToABunchOfNumbers(...numbers) { return numbers.map((x) => x + 5); } const result = addFiveToABunchOfNumbers(4, 5, 6, 7, 8, 9, 10); // [9, 10, 11, 12, 13, 14, 15] const [a, b, ...rest] = [1, 2, 3, 4]; // a: 1, b: 2, rest: [3, 4] const { e, f, ...others } = { e: 1, f: 2, g: 3, h: 4, }; // e: 1, f: 2, others: { g: 3, h: 4 }

كيف يمكنك مشاركة الكود بين الملفات؟

يعتمد هذا على بيئة JavaScript.

على العميل (بيئة المتصفح)، طالما أن المتغيرات/الدوال معلنة في النطاق العام (window)، يمكن لجميع السكريبتات الإشارة إليها. بدلاً من ذلك، اعتماد تعريف الوحدة النمطية غير المتزامنة (AMD) عبر RequireJS لنهج أكثر معيارية.

على الخادم (بيئة Node.js)، كانت الطريقة الشائعة هي استخدام CommonJS. يتم التعامل مع كل ملف كوحدة، ويمكنه تصدير المتغيرات والدوال عن طريق إرفاقها بكائن module.exports.

يعرف ES2015 بناء جملة الوحدة الذي يهدف إلى استبدال كل من AMD و CommonJS. سيتم دعم هذا في النهاية في كل من بيئات المتصفح و Node.

لماذا قد ترغب في إنشاء أعضاء فئة ثابتة (static class members)؟

أعضاء الفئة الثابتة (الخصائص/الدوال) لا ترتبط بمثيل معين من الفئة ولها نفس القيمة بغض النظر عن المثيل الذي يشير إليها. عادة ما تكون الخصائص الثابتة متغيرات تهيئة والدوال الثابتة عادة ما تكون دوال مساعدة نقية لا تعتمد على حالة المثيل.