نتعامل في تطوير الويب مع البيانات بالصيغة الثنائية لما نتعامل مع الملفات، سواءً عند إنشائها أو رفعها أو تنزيلها، كما يمكن استخدامها في عمليات معالجة الصور، ويمكن إنجاز كل ما سبق في 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 بايتات لكل منهما).
فالكائن 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();
-
إذا أضيف وسيط من النوع
ArrayBuffer
فسيتشكل كائن عرض موافق له، وقد استخدمنا هذه الصيغة سابقًا.يمكن أن نختار إزاحةً
byteOffset
لنبدأ منها (ولها القيمة 0 افتراضيًا)، وأن نحدد حجمًا محددًاlength
من المخزن (وسيكون حجم المخزن الكلي هو القيمة الافتراضية)، وبهذا سنغطي جزءًا من المخزن المؤقتbuffer
فقط. -
عندما نضع وسيطًا على شكل مصفوفة
Array
أو أي كائن شبيه بها، فستُنشِئ مصفوفةَ نوع لها نفس الحجم وتنسخ المحتويات إليها، ويمكن استخدام ذلك لملء المصفوفة مسبقًا بالبيانات:
let arr = new Uint8Array([0, 1, 2, 3]); alert( arr.length ); // 4, ينشئ مصفوفة ثنائية لها الحجم نفسه alert( arr[1] ); // 1,لها القيم نفسها (unsigned 8-bit integers) يملؤها بـ 4 بايتات من النوع
-
لو استخدمنا مصفوفة نوع
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)
-
يُنشئ الوسيط العددي
length
مصفوفة نوع تحتوي عدد عناصر مساويًا لقيمته، أما طولها بالبايت فسيكون مساويًا للقيمةlength
مضروبةً بعدد البايتات اللازم لتمثيل أحد عناصرهاTypedArray.BYTES_PER_ELEMENT
، مثلًا:
let arr = new Uint16Array(4); // integer إنشاء مصفوفة نوع تحوي 4 أعداد من النوع alert( Uint16Array.BYTES_PER_ELEMENT ); // بايتان لكل عدد alert( arr.byteLength ); // حجمها 8 بايت
- عندما لا نستخدم أي وسائط، فستتشكل مصفوفة نوع طولها 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.
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
، ومن هذه الكائنات: -
مصفوفة النوع، والتي يمكن أن تكون:
-
Uint8Array
وUint16Array
وUint32Array
: وهي أعداد بلا إشارة من 8 أو 16 أو 32 بت. -
Uint8ClampedArray
: أعداد صحيحة من 8 بت، مع اجتزاء البتات التي تزيد عن 8. -
Int8Array
وInt16Array
وInt32Array
: أعداد صحيحة ذات إشارة. -
Float32Array
وFloat64Array
: أعداد بفاصلة عائمة من 32 أو 64 بت، وذات إشارة.
-
-
الكائن
DataView
: ويستخدم التوابع لتحديد تنسيق الأعداد في مخزن مؤقت للبيانات، مثل(getUint8(offset
.
في معظم الأحيان يمكن العمل مباشرةً على مصفوفات النوع محتفظين بالكائن ArrayBuffer
خلف الستار، ويمكن الوصول إليه باستخدام الأمر buffer.
، كما يمكن إنشاء كائن عرض جديد عند الحاجة.
يوجد مصطلحان إضافيان يستخدَمان في وصف التوابع التي تتعامل مع البيانات الثنائية:
-
ArrayBufferView
: وهو مصطلح يغطي كل أنواع كائنات العرض. -
BufferSource
: مصطلح يغطي كلًا منArrayBuffer
أوArrayBufferView
، وسنرى هذه المصطلحات في الفصول القادمة،BufferSource
من المصطلحات الشائعة، ويعني "أي نوع من البيانات الثنائية" سواءً كانت الكائنArrayBuffer
أو أي كائن عرض له.
مهمات للإنجاز
ضم مصفوفات النوع 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.