`DataView` ב-TypeScript
מאמר זה מסביר את השימוש ב-DataView ב-TypeScript.
נסביר את DataView ב-TypeScript, מהבסיס ועד לשימושים מעשיים, שלב אחר שלב.
YouTube Video
DataView ב-TypeScript
בעזרת DataView ב-TypeScript, ניתן לבצע פעולות קריאה וכתיבה ברמת בייטים על ArrayBuffer באופן מדויק. DataView שימושי מאוד לעיבוד בינארי ברמה נמוכה, כמו מימוש פרוטוקולים, ניתוח קבצים בינאריים ושליחה/קבלה של נתונים בינאריים דרך WebSocket.
מושגים בסיסיים: ההבדלים בין ArrayBuffer, TypedArray ו-DataView
ArrayBuffer הוא מבנה נתונים בסיסי לאחסון רצף בייטים באורך קבוע. TypedArrayים כמו Uint8Array הם תצוגות שמאפשרות לראות את ה-buffer כמערך של טיפוס מסוים, כאשר כל איבר בטיפוס קבוע.
מצד שני, DataView הוא תצוגה גמישה המאפשרת לקרוא ולכתוב ערכים בכל סוג נתון בכל מיקום אופסט. מכיוון שהוא לא מניח סוגים קבועים ומאפשר שליטה מפורטת ברמת הבייט, הוא מתאים לניתוח פרוטוקולים בינאריים ולעיבוד ברמה נמוכה.
הנה דוגמה ליצירת ArrayBuffer וממנו ליצור גם Uint8Array וגם DataView.
1// Create an ArrayBuffer (8 bytes)
2const buffer = new ArrayBuffer(8);
3
4// Create a Uint8Array view (easy to manipulate individual bytes)
5const u8 = new Uint8Array(buffer);
6u8[0] = 0x12;
7u8[1] = 0x34;
8
9// Create a DataView (allows reading/writing any type at any offset)
10const dv = new DataView(buffer);
11
12// Read a 16-bit unsigned integer (big-endian assumed if second argument is omitted)
13console.log(dv.getUint16(0)); // 0x1234
- בקוד זה, נעשה שימוש בשני סוגי תצוגות שונים —
Uint8Arrayו-DataView— בו-זמנית על אותו buffer. באמצעותDataViewניתן לקרוא ולכתוב ערכים בצורה גמישה על ידי בחירת מיקום (offset) וסדר בתים (endianness).
השיטות המרכזיות של DataView
DataView הוא ממשק לעבודה עם ArrayBuffer ברמת בייטים, ומספק שיטות לקריאה וכתיבה של טיפוסים שונים כגון מספרים שלמים ומספרים עשרוניים.
השיטות העיקריות הן:.
- קריאה:
getUint8,getInt8,getUint16,getInt16,getUint32,getInt32,getFloat32,getFloat64 - כתיבה:
setUint8,setInt8,setUint16,setInt16,setUint32,setInt32,setFloat32,setFloat64
כל שיטה כזו מקבלת 'מיקום בייט' (offset) כפרמטר ראשון: get קורא את הערך במיקום הזה ו-set כותב שם ערך. ניתן גם לציין את סדר הבתים (endianness) כפרמטר שני כאשר עובדים עם נתוני 16, 32 או 64 ביט. בפועל, הכי בטוח תמיד לציין את סדר הבתים לפי המפרט של הנתונים.
בדוגמה הבאה נראה איך כותבים מספר שלם של 32 ביט ומספר עשרוני של 32 ביט בפורמט little endian ל-buffer ואז קוראים אותם חזרה באותו פורמט.
1const buf = new ArrayBuffer(12); // Enough space for integer + float
2const view = new DataView(buf);
3
4// Write a 32-bit unsigned integer at offset 0 (little-endian)
5view.setUint32(0, 305419896, true); // 305419896 = 0x12345678
6
7// Read the same 32-bit unsigned integer back from offset 0 (little-endian)
8const intVal = view.getUint32(0, true);
9console.log(intVal); // 305419896
10
11// Write a 32-bit floating number at offset 4 (little-endian)
12view.setFloat32(4, 3.1415926, true);
13
14// Read the 32-bit floating number back from offset 4 (little-endian)
15const floatVal = view.getFloat32(4, true);
16console.log(floatVal); // 3.1415926 (approx)
- בכך שתקבע במפורש את סדר הבתים, תבטיח תאימות בין פלטפורמות שונות ומבני נתונים בינאריים.
אודות סדר בתים (Endianness)
חלק מהפרוטוקולים, כמו בפרוטוקולי רשת, משתמשים ב-big-endian, בעוד שברוב המעבדים ופורמטי קבצים נהוג little-endian. ב-DataView, אם הפרמטר השני הוא true הנתון נחשב little endian; אם הוא false או מושמט — big endian.
בדוגמה הבאה, ניתן לראות כיצד מערך הבייטים בזיכרון משתנה בעת כתיבת אותו מספר באמצעות פורמטים של Big-Endian ו-Little-Endian.
1const b = new ArrayBuffer(4);
2const a = new Uint8Array(b);
3const dv = new DataView(b);
4
5// Write in big-endian order
6dv.setUint32(0, 0x0A0B0C0D, false); // big-endian
7console.log([...a]); // [10, 11, 12, 13]
8
9// Write the same value in little-endian order
10dv.setUint32(0, 0x0A0B0C0D, true); // little-endian
11console.log([...a]); // [13, 12, 11, 10]
- הבנת ההבדל בין big endian ל-little endian תסייע לך מאוד לנתח נתוני תקשורת או פורמטים בינאריים.
קלט ופלט של מחרוזות (באמצעות TextEncoder ו-TextDecoder)
DataView מעולה לקריאה וכתיבה של מספרים אך לא תומך ישירות במחרוזות. נהוג לקודד מחרוזות באמצעות TextEncoder או TextDecoder (כגון לקידוד UTF-8), ואז להעתיקן ל-buffer בעזרת Uint8Array. להלן דוגמה לכתיבת מחרוזת UTF-8 ל-buffer וקריאה שלה חזרה.
1const encoder = new TextEncoder();
2const decoder = new TextDecoder();
3
4const str = "\u3053\u3093\u306b\u3061\u306f";
5const encoded = encoder.encode(str); // Uint8Array (UTF-8 encoded bytes)
6
7// Create a buffer and write the encoded string bytes into it
8const buf2 = new ArrayBuffer(encoded.length);
9const u8v = new Uint8Array(buf2);
10u8v.set(encoded);
11
12// Read the bytes and decode them back into a string
13const decodedStr = decoder.decode(new Uint8Array(buf2));
14console.log(decodedStr);- אם נמיר מחרוזות לבינארי ונוסיף (אם צריך) את האורך מראש, נוכל לשמור מחרוזות באורך משתנה.
דוגמה מעשית: קידוד/פענוח של פורמט בינארי מותאם אישית
להלן, אנו מגדירים פורמט הודעה פשוט המכיל מספר גירסה, מזהה ושם. אנו מממשים תהליך קידוד להמרת הודעות לנתונים בינאריים, ותהליך פענוח לשחזור האובייקט המקורי מהבינארי. בפורמט הודעה זה, הבייט הראשון מכיל את הגירסה, ארבעת הבייטים הבאים מכילים את המזהה בפורמט Little-Endian, הבייט הבא מכיל את אורך השם, ולבסוף, שם המקודד ב-UTF-8 מאוחסן.
1// Encode an object into a binary message
2function encodeMessage(msg: { version: number; id: number; name: string }): ArrayBuffer {
3 const encoder = new TextEncoder();
4 const nameBytes = encoder.encode(msg.name);
5 const total = 1 + 4 + 1 + nameBytes.length;
6 const buf = new ArrayBuffer(total);
7 const dv = new DataView(buf);
8 let offset = 0;
9
10 dv.setUint8(offset, msg.version); offset += 1; // write version
11 dv.setUint32(offset, msg.id, true); offset += 4; // write ID (little-endian)
12 dv.setUint8(offset, nameBytes.length); offset += 1; // write name length
13
14 // write name bytes
15 new Uint8Array(buf, offset).set(nameBytes);
16 return buf;
17}
18
19// Decode a binary message back into an object
20function decodeMessage(buf: ArrayBuffer) {
21 const dv = new DataView(buf);
22 let offset = 0;
23
24 const version = dv.getUint8(offset); offset += 1; // read version
25 const id = dv.getUint32(offset, true); offset += 4; // read ID (little-endian)
26 const nameLen = dv.getUint8(offset); offset += 1; // read name length
27
28 // extract name bytes
29 const nameBytes = new Uint8Array(buf, offset, nameLen);
30 const decoder = new TextDecoder();
31 // decode UTF-8 string
32 const name = decoder.decode(nameBytes);
33
34 return { version, id, name };
35}
36
37// Example usage
38const packed = encodeMessage({ version: 1, id: 42, name: "Alice" });
39const unpacked = decodeMessage(packed);
40console.log(unpacked); // { version: 1, id: 42, name: "Alice" }
- זהו מימוש טיפוסי לטיפול בפורמט הודעה בסיסי הכולל מחרוזת באורך משתנה, וניתן ליישום במצבים פרקטיים כמו תקשורת רשת או עיצוב קבצים בינאריים ייעודיים.
זהירות ונוהגים מומלצים
בעת עבודה עם נתונים בינאריים באמצעות DataView, ישנם מספר דגשים חשובים מעבר לקריאה וכתיבה פשוטים — כגון בטיחות, שמירה על קביעות ה-endianness וטיפול נכון בטיפוסים. במיוחד כאשר עובדים עם נתונים בינאריים שמגיעים ממקורות חיצוניים או עם מספרים שלמים גדולים — חשוב לתכנן את הקוד כך שימנע קריאות שגויות וחריגה מגבולות הזיכרון. להלן מספר נקודות שימושיות שכדאי לזכור לשימוש מעשי.
-
בדיקת גבולות (Boundary Check)
DataViewיגרום ל-exception אם המיקום או הגודל חורגים מהגבול של ה-buffer. תמיד בדוק את האורכים בעת עבודה עם נתונים בינאריים לא מהימנים. -
תמיד לציין סדר בתים (Endianness) יש להקפיד לציין במפורש ועקבית little endian או big endian בקוד שלך.
-
עקביות טיפוסים מספרים ב-JavaScript הם מסוג נקודה צפה (floating point) 64 ביט לפי תקן IEEE-754. שיטות כמו
getUint32מתבצעות כראוי, אבל איןgetUint64, ולכן נדרשת טיפול מיוחד במספרים שלמים בגודל 64 ביט. -
טיפול במספרים שלמים בגודל 64 ביט כדי לטפל במספרים שלמים 64 ביט, יש להשתמש ב-
BigIntאו לחלק ידנית את הערך ל-32 ביט גבוהים ו-32 נמוכים. להלן דוגמה פשוטה לקריאת מספר שלם 64 ביט לא חתום.
1function getUint64(dv: DataView, offset: number, littleEndian = true): bigint {
2 const low = BigInt(dv.getUint32(offset, littleEndian));
3 const high = BigInt(dv.getUint32(offset + 4, littleEndian));
4 return littleEndian ? (high << 32n) | low : (low << 32n) | high;
5}- באמצעות
BigIntאפשר בבטחה לטפל גם במספרים שלמים גדולים מ-64 ביט.
שימוש ב-Node.js (עבודה עם Buffer ו-DataView)
למרות ש-Buffer נמצא בשימוש נפוץ ב-Node.js, קל גם להמיר בין ArrayBuffer ל-DataView. השתמש במאפיין buffer של אובייקטי Buffer או ב-Uint8Array לצורך ההמרה.
1// Node.js: Buffer -> ArrayBuffer
2const nodeBuf = Buffer.from([1,2,3,4]);
3const arrBuf = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.byteLength);
4const dvNode = new DataView(arrBuf);
5console.log(dvNode.getUint16(0));- אפשרות זו יכולה לשמש ככלי להחלפת מידע בינארי בין Node.js לדפדפנים.
סיכום
DataView הוא מנגנון עוצמתי לגישה חופשית לקריאה וכתיבה של נתונים בינאריים — במיוחד כשיש צורך בשליטה גמישה כמו בחירת סדר בתים וגישה בעמדות שונות בזיכרון. בשילוב של ArrayBuffer, TypedArray ו-DataView תוכל לטפל בנתונים בינאריים ב-TypeScript בצורה גמישה ומדויקת, מה שמאפשר מימוש פרוטוקולים, ניתוח קבצים ועוד. על ידי הוספת קידוד מחרוזות וטיפול במספרים שלמים בגודל 64 ביט, תוכל לבצע פעולות בינאריות שימושיות אף יותר.
תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.