اذهب إلى المحتوى

نتعامل في تطوير الويب مع البيانات بالصيغة الثنائية لما نتعامل مع الملفات، سواءً عند إنشائها أو رفعها أو تنزيلها، كما يمكن استخدامها في عمليات معالجة الصور، ويمكن إنجاز كل ما سبق في JavaScript التي تتميز العمليات الثنائية فيها بأنها عالية الأداء على الرغم من وجود بعض الالتباسات، نظرًا لوجود العديد من الأصناف، منها ArrayBuffer وUint8Array وDataView وBlob وFile، وغيرها.

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

let buffer = new ArrayBuffer(16); // إنشاء مخزن مؤقت حجمه 16
alert(buffer.byteLength); // 16

حيث تحجز الشيفرة السابقة حجمًا من الذاكرة مقداره 16 بايت، ويُملأ بالأصفار.

اقتباس

لايمثل الكائن ArrayBuffer مصفوفةً من الأشياء.

لتوضيح الأمور لا بدّ من الإشارة أن ArrayBuffer لا يشترك مع المصفوفات Array بأي شيء:

  • له حجم ثابت، لا يمكن زيادته أو إنقاصه.
  • يحتل المقدار المحدد من الذاكرة تمامًا.
  • نحتاج إلى كائن عرض view للوصول إلى بايت محدد، وليس [buffer[index.

فالكائن ArrayBuffer هو مساحة من الذاكرة التي لا يمكن معرفة ما يُخزّن فيها، بل هي سلسلة خام من البايتات، وللتعامل مع ArrayBuffer، سنحتاج إلى كائن عرض لا يخزن أي بيانات بمفرده، بل تنحصر مهمته في تفسير البايتات المخزنة في ArrayBuffer، ومن بين هذه الكائنات الآتي:

  • "Uint8Array": يعامِل كل بايت من ArrayBuffer مثل عدد مستقل يأخذ قيمةً بين 0 و255، وتُمثِّل عددًا صحيحًا بلا إشارة مؤلفًا من 8-بت.
  • "Uint16Array": يعامِل كل بايتين مثل عدد صحيح يأخذ قيمةً بين 0 و65535، وتمثِّل عددًا صحيحًا بلا إشارة مؤلفًا من 16-بت.
  • "Uint32Array": يعامِل كل 4 بايتات مثل عدد صحيح يأخذ قيمةً بين 0 و4294967295، وتمثل عددًا صحيحًا بلا إشارة مؤلفًا من 16-بت.
  • "Float64Array": يعامل كل 8 بايتات مثل عدد عشري ذي فاصلة عائمة floating-point، ويأخذ قيمةً بين 5.0x 10^-324 و1.8x 10^308.

وبالتالي سيفسَّر الكائن المؤلف من 16 بايت -وفق الكائنات السابقة- إلى 16 عدد صغير، أو 8 أرقام أكبر (بايتان لكل عدد)، أو 4 أعداد أضخم (4 بايتات لكل عدد)، أو عددين بالفاصلة العائمة عاليي الدقة (8 بايتات لكل منهما).

array_buffer_16bit_01.png

فالكائن ArrayBuffer هو الكائن البنيوي وأساس البيانات الخام.

ولا بدّ من استخدام كائن عرض عند محاولة الكتابة ضمنه أو المرور على قيمه وكذلك لأي عملية بالأساس، مثلًا:

let buffer = new ArrayBuffer(16); // إنشاء مخزن مؤقت من 16 بايت

let view = new Uint32Array(buffer); // تعامل مع المخزن كعدد صحيح من 32-بت

alert(Uint32Array.BYTES_PER_ELEMENT); //  أربعة بايت لكل عدد 

alert(view.length); // 4
alert(view.byteLength); // 16

// كتابة قيمة
view[0] = 123456;

// المرور على قيمه
for(let num of view) {
  alert(num); // 123456, then 0, 0, 0 (أربعة قيم ككل)
}

الكائن TypedArray

إنّ المصطلح الرئيسي لكائنات العرض السابقة هو مصفوفات النوع TypedArray، والتي تشترك جميعها بمجموعة التوابع والخصائص، مع ملاحظة عدم وجود دالة بانية اسمها TypedArray، فهو مجرد مصطلح يمثِّل كائنات العرض السابقة التي تطبق على ArrayBuffer، وهي Int8Array وUint8Array، وغيرها من الكائنات، وسنتابع بقية القائمة لاحقًا. عندما نرى أمرًا وفق الصيغة new TypedArray، فقد يعني ذلك أيًا من new Int8Array أو new Uint8Array وغيرها، إذ تسلك مصفوفات النوع سلوك بقية المصفوفات، فلها فهارس indexes ويمكن المرور على عناصرها، كما يختلف سلوكها باختلاف نوع الوسائط التي تقبلها، وهناك 5 حالات ممكنة للوسائط:

new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();
  1. إذا أضيف وسيط من النوع ArrayBuffer فسيتشكل كائن عرض موافق له، وقد استخدمنا هذه الصيغة سابقًا.

    يمكن أن نختار إزاحةً byteOffset لنبدأ منها (ولها القيمة 0 افتراضيًا)، وأن نحدد حجمًا محددًا length من المخزن (وسيكون حجم المخزن الكلي هو القيمة الافتراضية)، وبهذا سنغطي جزءًا من المخزن المؤقت buffer فقط.

  2. عندما نضع وسيطًا على شكل مصفوفة Array أو أي كائن شبيه بها، فستُنشِئ مصفوفةَ نوع لها نفس الحجم وتنسخ المحتويات إليها، ويمكن استخدام ذلك لملء المصفوفة مسبقًا بالبيانات:

   let arr = new Uint8Array([0, 1, 2, 3]);
   alert( arr.length ); // 4, ينشئ مصفوفة ثنائية لها الحجم نفسه
   alert( arr[1] ); // 1,لها القيم نفسها  (unsigned 8-bit integers)  يملؤها بـ 4 بايتات من النوع 
  1. لو استخدمنا مصفوفة نوع TypedArray من نوع مختلف، فستُنشَأ مصفوفة نوع لها نفس الحجم وتنسخ المحتويات إليها، وستحوَّل القيم إلى النوع الجديد خلال العملية عند الحاجة لذلك، مثلًا:
   let arr16 = new Uint16Array([1, 1000]);
   let arr8 = new Uint8Array(arr16);
   alert( arr8[0] ); // 1
   alert( arr8[1] ); // 232, tried to copy 1000, but can't fit 1000 into 8 bits (explanations below)
  1. يُنشئ الوسيط العددي length مصفوفة نوع تحتوي عدد عناصر مساويًا لقيمته، أما طولها بالبايت فسيكون مساويًا للقيمة length مضروبةً بعدد البايتات اللازم لتمثيل أحد عناصرها TypedArray.BYTES_PER_ELEMENT، مثلًا:
   let arr = new Uint16Array(4); // integer إنشاء مصفوفة نوع تحوي 4 أعداد من النوع
   alert( Uint16Array.BYTES_PER_ELEMENT ); // بايتان لكل عدد
   alert( arr.byteLength ); // حجمها 8 بايت
  1. عندما لا نستخدم أي وسائط، فستتشكل مصفوفة نوع طولها 0.

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

سنستخدم الخصائص التالية للوصول إلى الكائن ArrayBuffer:

  • arr.buffer: يعيد مرجعًا إلى الكائن ArrayBuffer.
  • arr.byteLength: يعيد طول الكائن ArrayBuffer.

وبالتالي يمكننا الانتقال من كائن عرض إلى آخر، مثلًا:

let arr8 = new Uint8Array([0, 1, 2, 3]);

// كائن عرض آخر للمعطيات نفسها
let arr16 = new Uint16Array(arr8.buffer);

إليك قائمةً بمصفوفات النوع:

  • "‘Uint32Array" و"Uint16Array" و"Uint8Array": تستخدم للأعداد الصحيحة 8 و16 و32 بت.
  • "Uint8ClampedArray": لتمثيل الأعداد الصحيحة ذات 8-بت، وتبقي القيمة 0 أو 255 إن كانت خارج المجال.
  • "‘Int32Array" و"Int16Array" و"Int8Array": لتمثيل الأعداد الصحيحة السالبة أو الموجبة.
  • "Float32Array" و"Float64Array": لتمثيل الأعداد العشرية ذات 32 و64 بت والتي تحمل إشارةً بطريقة الفاصلة العائمة.
اقتباس

لايوجد النوع int8 أو أي نوع أحادي القيمة single-valued مشابه على الرغم من وجود أسماء مثل Int8Array، فلا يوجد نوع أحادي القيمة مثل int أو int8 في JavaScript، والأمر منطقي هنا، حيث لا يمثل الكائن Int8Array مصفوفةً من هذه القيم المفردة، بل هو عرض وتمثيل للكائن ArrayBuffer.

سلوك المصفوفات عند خروج القيم عن حدودها

عندما نحاول كتابة قيمة خارج حدود مصفوفة النوع فلن يظهر لنا خطأ، لكن ستُحذف البتات الزائدة.

لنحاول مثلًا وضع القيمة 256 ضمن Uint8Array، فإذا حولنا 256 إلى الصيغة الثنائية، فستكون بالشكل "100000000" (9 بتات)، لكن النوع الذي استخدمناه يحوي 8 بتات فقط، أي قيمًا بين 0 و255، وفي حال أدخلنا رقمًا أكبر من النطاق السابق فستُخزن الخانات اليمينية الثمان (الأقل قيمة) فقط ويُحذف الباقي، وبالتالي سنحصل على القيمة 0 في هذا المثال.

لنأخذ العدد 257 مثالًا آخر لذلك، أينما سنحصل ثنائيًا على تسع بتات "100000001"، ستُخزّن منها الخانات اليمينية الثمان الأولى، وسنحصل بالنتيجة على القيمة 1.

binary_representation_of_ 8bit_integer_2.png

let uint8array = new Uint8Array(16);

let num = 256;
alert(num.toString(2)); // 100000000 (التمثيل الثنائي)

uint8array[0] = 256;
uint8array[1] = 257;

alert(uint8array[0]); // 0
alert(uint8array[1]); // 1

للنوع Uint8ClampedArray ميزة خاصة، فهو يعطي القيمة 255 لأي عدد أكبر من 255، والقيمة 0 لأي عدد سالب، وهذا مفيد في معالجة الصور.

توابع الصنف TypedArray

لهذا الصنف نفس توابع المصفوفات مع بعض الاستثناءات الواضحة، حيث يمكننا مسح عناصره، وتنفيذ التوابع map وslice وfind وreduce؛ إلا أنه لا يمكن تنفيذ عدة أشياء، منها الآتي:

  • لا يمكن استخدام التابع splice: لا يمكن "حذف" قيمة ما، لأنّ مصفوفات النوع هي كائنات تعرض مساحات ثابتةً متجاورةً من الذاكرة مؤقتًا، وكل ما يمكننا فعله هو إسناد القيمة 0 لها.
  • لا يمكن استخدام التابع concat.

كما يوجد تابعان إضافيان:

  • ([arr.set(fromArr, [offset: ينقل عناصر المصفوفة fromArr إلى arr ابتداءً من الموقع offset، والذي يأخذ القيمة 0 افتراضيًا.
  • ([arr.subarray([begin, end: يُنشئ كائن عرض جديد من نفس النوع بين القيمتين begin وend ضمنًا، وهذا يشبه التابع slice المدعوم أيضًا، لكنه لا ينسخ أي قيم، بل ينشئ كائن عرض جديد للتعامل مع جزء محدد من البيانات.

تتيح لنا التوابع السابقة نسخ مصفوفات النوع ودمجها وإنشاء مصفوفات جديدة من مصفوفات موجودة مسبقًا وغيرها.

الكائن DataView

يمثل الكائن DataView كائن عرض عالي المرونة بلا نوع untyped للكائن ArrayBuffer، حيث يسمح بالوصول إلى البيانات ابتداءً من أي موضع وبأي تنسيق.

  • تحدِّد الدالة البانية تنسيق البيانات في مصفوفات النوع، ويُفترض أن تكون الدالة منتظمةً ككل، وأن نحصل على العنصر الذي ترتيبه "i" باستخدام التعليمة [arr[i.
  • يمكن الوصول إلى البيانات عند استخدام DataView من خلال توابع مثل (getUint8(i. أو (getUint16(i.، ونختار التنسيق عند استدعاء التابع وليس عند إنشائه.

نستخدم DataView كالتالي:

new DataView(buffer, [byteOffset], [byteLength])
  • buffer: ويمثل الكائن ArrayBuffer الأساسي، ولا تنشئ DataView مخزنًا مؤقتًا خاصًا بها على عكس مصفوفات النوع، وإنما ينبغي أن يكون موجودًا مسبقًا.
  • byteOffset: ويمثل بايت البداية لكائن العرض (القيمة الافتراضية 0).
  • byteLength: عدد بايتات كائن العرض (إلى نهاية المخزن افتراضيًا).

في المثال التالي نستخرج أعدادًا بتنسيقات مختلفة من المخزن ذاته:

// مصفوفة ثنائية مكونة من 4 بايتات، وستكون أكبر قيمة 255
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;

let dataView = new DataView(buffer);

// الحصول على رقم 8 بت عند  الإزاحة 0
alert( dataView.getUint8(0) ); // 255

// الحصول على رقم 16 بت عند  الإزاحة 0
alert( dataView.getUint16(0) ); // 65535 (biggest 16-bit unsigned int)

// الحصول على رقم 32 بت عند  الإزاحة 0
alert( dataView.getUint32(0) ); // 4294967295 (biggest 32-bit unsigned int)

dataView.setUint32(0, 0); // إعطاء القيمة 0 لرقم من 4 بايت، وبالتالي تصفير المخزن

تَظهر قدرة DataView عند تخزين أرقام ذات تنسيق مختلف في المخزن المؤقت نفسه، فلو خزّنّا مثلًا سلسلةً من البيانات مؤلفةً من الأزواج (عدد صحيح 16 بت، عدد عشري عائم 32 بت)، فسيسمح لنا DataView بالوصول إليها بكل سهولة.

الخلاصة

  • يمثل الكائن الأساسي ArrayBuffer مرجعًا إلى حجم ثابت ومتجاور في الذاكرة.
  • نحتاج إلى كائن عرض لتنفيذ أي عملية على ArrayBuffer، ومن هذه الكائنات:
  • مصفوفة النوع، والتي يمكن أن تكون:
    1. Uint8Array وUint16Array وUint32Array: وهي أعداد بلا إشارة من 8 أو 16 أو 32 بت.
    2. Uint8ClampedArray: أعداد صحيحة من 8 بت، مع اجتزاء البتات التي تزيد عن 8.
    3. Int8Array وInt16Array وInt32Array: أعداد صحيحة ذات إشارة.
    4. Float32Array وFloat64Array: أعداد بفاصلة عائمة من 32 أو 64 بت، وذات إشارة.
  • الكائن DataView: ويستخدم التوابع لتحديد تنسيق الأعداد في مخزن مؤقت للبيانات، مثل (getUint8(offset.

في معظم الأحيان يمكن العمل مباشرةً على مصفوفات النوع محتفظين بالكائن ArrayBuffer خلف الستار، ويمكن الوصول إليه باستخدام الأمر buffer.، كما يمكن إنشاء كائن عرض جديد عند الحاجة.

يوجد مصطلحان إضافيان يستخدَمان في وصف التوابع التي تتعامل مع البيانات الثنائية:

  • ArrayBufferView: وهو مصطلح يغطي كل أنواع كائنات العرض.
  • BufferSource: مصطلح يغطي كلًا من ArrayBuffer أو ArrayBufferView، وسنرى هذه المصطلحات في الفصول القادمة، BufferSource من المصطلحات الشائعة، ويعني "أي نوع من البيانات الثنائية" سواءً كانت الكائن ArrayBuffer أو أي كائن عرض له.

buffre_source_3.png

مهمات للإنجاز

ضم مصفوفات النوع Concatenate typed arrays

لنفترض وجود مصفوفة من النوع Uint8Array، يمكن استخدام الدالة (concat(arrays لضمّ عدة مصفوفات في مصفوفة واحدة.

الحل

إليك الحل المتمثل بالشيفرة التالية:

function concat(arrays) {
  // مجموع أطوال كل مصفوفة
  let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);

  if (!arrays.length) return null;

  let result = new Uint8Array(totalLength);

  // ‫انسخ محتوى كل مصفوفة إلى الناتج result
  // يُنسخ محتوى المصفوفة اللاحقة على الجانب الأيمن للمصفوفة السابقة
  let length = 0;
  for(let array of arrays) {
    result.set(array, length);
    length += array.length;
  }

  return result;
}

افتح الحل في تجربة حية

ترجمة -وبتصرف- للفصل Array buffer, Binary data من سلسلة The Modern JavaScript Tutorial.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...