ArrayBuffer

The ArrayBuffer object, TypedArray view and DataView view are an interface for JavaScript to manipulate binary data. These objects have long existed and belong to independent specifications (released in February 2011). ES6 incorporates them into the ECMAScript specification and adds new methods. They all process binary data in the syntax of arrays, so they are collectively called binary arrays.

The original design purpose of this interface is related to the WebGL project. The so-called WebGL refers to the communication interface between the browser and the graphics card. In order to satisfy the massive, real-time data exchange between JavaScript and the graphics card, the data communication between them must be binary instead of the traditional text format. A 32-bit integer is passed in the text format, and the JavaScript script and the graphics card at both ends must be formatted, which will be very time-consuming. At this time, if there is a mechanism that can directly manipulate bytes like the C language, and send 4-byte 32-bit integers to the graphics card in binary form intact, the performance of the script will be greatly improved.

Binary arrays were born under this background. It is very similar to an array in C language, allowing developers to directly manipulate memory in the form of array subscripts, greatly enhancing JavaScript's ability to process binary data, and making it possible for developers to communicate with the native interface of the operating system through JavaScript.

The binary array is composed of three types of objects.

(1) ArrayBuffer object: represents a piece of binary data in the memory, which can be manipulated through the "view". The "view" deploys an array interface, which means that the memory can be manipulated in an array method.

(2) TypedArray view: includes 9 types of views, such as Uint8Array (unsigned 8-bit integer) array view, Int16Array (16-bit integer) array view, Float32Array (32 Bit floating point number) array view and so on.

(3) DataView view: You can customize the view in compound format, for example, the first byte is Uint8 (unsigned 8-bit integer), the second and third bytes are Int16 (16-bit integer) , The fourth byte starts with Float32 (32-bit floating point number) and so on. In addition, the endianness can be customized.

Simply put, the ArrayBuffer object represents the original binary data, the TypedArray view is used to read and write simple types of binary data, and the DataView view is used to read and write complex types of binary data.

The TypedArray view supports a total of 9 data types (the DataView view supports 8 other than Uint8C).

Data TypeByte LengthMeaningCorresponding C Language Type
Int818-bit signed integersigned char
Uint818-bit unsigned integerunsigned char
Uint8C18-bit unsigned integer (automatically filter overflow)unsigned char
Int16216-bit signed integershort
Uint16216-bit unsigned integerunsigned short
Int32432-bit signed integerint
Uint32432-bit unsigned integerunsigned int
Float32432-bit floating point numberfloat
Float64864-bit floating point numberdouble

Note that the binary array is not a real array, but an array-like object.

Many browser operation APIs use binary arrays to manipulate binary data. Here are a few of them.

ArrayBuffer Object

Overview

The ArrayBuffer object represents a section of memory that stores binary data. It cannot be directly read or written, but can only be read and written through views (TypedArray view and DataView view). The function of the view is to interpret binary data in a specified format.

ArrayBuffer is also a constructor, which can allocate a continuous memory area where data can be stored.

const buf = new ArrayBuffer(32);

The above code generates a 32-byte memory area, and the value of each byte is 0 by default. As you can see, the parameter of the ArrayBuffer constructor is the required memory size (in bytes).

In order to read and write this content, you need to specify a view for it. To create a DataView view, you need to provide an instance of the ArrayBuffer object as a parameter.

const buf = new ArrayBuffer(32);
const dataView = new DataView(buf);
dataView.getUint8(0); // 0

The above code creates a DataView view for a section of 32-byte memory, and then reads 8-bit binary data from the beginning in an unsigned 8-bit integer format, and the result is 0, because the original memory ArrayBuffer object defaults to all The bits are all 0.

Another TypedArray view, one difference from the DataView view is that it is not a constructor, but a set of constructors, representing different data formats.

const buffer = new ArrayBuffer(12);

const x1 = new Int32Array(buffer);
x1[0] = 1;
const x2 = new Uint8Array(buffer);
x2[0] = 2;

x1[0]; // 2

The above code creates two views for the same memory: 32-bit signed integer (Int32Array constructor) and 8-bit unsigned integer (Uint8Array constructor). Since the two views correspond to the same memory, the modification of the underlying memory by one view will affect the other view.

The constructor of the TypedArray view, in addition to accepting the ArrayBuffer instance as a parameter, can also accept an ordinary array as a parameter, directly allocate memory to generate the underlying ArrayBuffer instance, and complete the assignment of this memory at the same time.

const typedArray = new Uint8Array([0, 1, 2]);
typedArray.length; // 3

typedArray[0] = 5;
typedArray; // [5, 1, 2]

The above code uses the Uint8Array constructor of the TypedArray view to create an unsigned 8-bit integer view. As you can see, Uint8Array directly uses ordinary arrays as parameters, and the assignment to the underlying memory is completed at the same time.

ArrayBuffer.prototype.byteLength

The byteLength property of the ArrayBuffer instance returns the byte length of the allocated memory area.

const buffer = new ArrayBuffer(32);
buffer.byteLength;
// 32

If the memory area to be allocated is large, the allocation may fail (because there is not so much continuous free memory), so it is necessary to check whether the allocation is successful.

if (buffer.byteLength === n) {
  // success
} else {
  // failed
}

ArrayBuffer.prototype.slice()

The ArrayBuffer instance has a slice method, which allows a part of the memory area to be copied to generate a new ArrayBuffer object.

const buffer = new ArrayBuffer(8);
const newBuffer = buffer.slice(0, 3);

The above code copies the first 3 bytes of the buffer object (starting from 0 and ending before the third byte), and generates a new ArrayBuffer object. The slice method actually consists of two steps. The first step is to allocate a piece of new memory, and the second step is to copy the original ArrayBuffer object.

The slice method accepts two parameters. The first parameter indicates the byte sequence number of the copy start (including this byte), and the second parameter indicates the byte sequence number of the copy end (excluding the byte). If the second parameter is omitted, it will default to the end of the original ArrayBuffer object.

Except for the slice method, the ArrayBuffer object does not provide any method to read and write memory directly, and only allows the establishment of a view on it, and then read and write through the view.

ArrayBuffer.isView()

ArrayBuffer has a static method isView, which returns a boolean value indicating whether the parameter is a view instance of ArrayBuffer. This method is roughly equivalent to judging whether the parameter is a TypedArray instance or a DataView instance.

const buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false

const v = new Int32Array(buffer);
ArrayBuffer.isView(v) ​​// true

TypedArray view

Overview

The ArrayBuffer object is used as a memory area and can store many types of data. In the same memory, different data have different interpretation methods, which is called "view". There are two views of ArrayBuffer, one is TypedArray view, and the other is DataView view. The array members of the former are all of the same data type, and the array members of the latter can be of different data types.

Currently, the TypedArray view includes a total of 9 types, and each view is a constructor.

  • Int8Array: 8-bit signed integer, 1 byte in length.
  • Uint8Array: 8-bit unsigned integer, 1 byte in length.
  • Uint8ClampedArray: 8-bit unsigned integer, length 1 byte, overflow handling is different.
  • Int16Array: 16-bit signed integer, 2 bytes in length.
  • Uint16Array: 16-bit unsigned integer, 2 bytes in length.
  • Int32Array: 32-bit signed integer, 4 bytes in length.
  • Uint32Array: 32-bit unsigned integer, 4 bytes in length.
  • Float32Array: 32-bit floating point number, 4 bytes in length.
  • Float64Array: 64-bit floating point number, 8 bytes in length.

The arrays generated by these 9 constructors are collectively referred to as TypedArray views. They are very similar to ordinary arrays. They have a length property, and can use the square bracket operator ([]) to get a single element. All array methods can be used on them. The differences between ordinary arrays and TypedArray arrays are mainly in the following aspects.

  • All members of the TypedArray array are of the same type.
  • The members of the TypedArray array are continuous and there will be no vacancies.
  • The default value of TypedArray array members is 0. For example, new Array(10) returns a normal array with no members in it, just 10 empty positions; new Uint8Array(10) returns a TypedArray array, where 10 members are all 0.
  • The TypedArray array is only a layered view and does not store data itself. Its data is stored in the underlying ArrayBuffer object. To obtain the underlying object, the buffer property must be used.

Constructor

TypedArray array provides 9 kinds of constructors to generate array instances of corresponding types.

There are multiple uses of the constructor.

(1) TypedArray(buffer, byteOffset=0, length?)

On the same ArrayBuffer object, multiple views can be created according to different data types.

// Create an 8-byte ArrayBuffer
const b = new ArrayBuffer(8);

// Create an Int32 view pointing to b, starting at byte 0 and ending at the end of the buffer
const v1 = new Int32Array(b);

// Create a Uint8 view pointing to b, starting at byte 2 and ending at the end of the buffer
const v2 = new Uint8Array(b, 2);

// Create an Int16 view pointing to b, starting at byte 2 and length 2
const v3 = new Int16Array(b, 2, 2);

The above code generates three views: v1, v2, and v3 on a section of memory (b) with a length of 8 bytes.

The view's constructor can accept three parameters:

-The first parameter (required): the underlying ArrayBuffer object corresponding to the view. -The second parameter (optional): the byte sequence number of the start of the view, starting from 0 by default. -The third parameter (optional): the number of data contained in the view, by default until the end of this memory area.

Therefore, v1, v2 and v3 overlap: v1[0] is a 32-bit integer, pointing to byte 0 to byte 3; v2[0] is an 8-bit unsigned Integer, pointing to byte 2; v3[0] is a 16-bit integer, pointing to byte 2 to byte 3. As long as any one view modifies the memory, it will be reflected in the other two views.

Note that byteOffset must be consistent with the data type to be created, otherwise an error will be reported.

const buffer = new ArrayBuffer(8);
const i16 = new Int16Array(buffer, 1);
// Uncaught RangeError: start offset of Int16Array should be a multiple of 2

In the above code, an 8-byte ArrayBuffer object is newly generated, and then a signed 16-bit integer view is created in the first byte of this object, and an error is reported. Because a signed 16-bit integer requires two bytes, the byteOffset parameter must be divisible by 2.

If you want to interpret the ArrayBuffer object from any byte, you must use the DataView view, because the TypedArray view only provides 9 fixed interpretation formats.

(2) TypedArray(length)

The view can also be generated by directly allocating memory without using the ArrayBuffer object.

const f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

The above code generates an 8-member Float64Array array (64 bytes in total), and then assigns a value to each member in turn. At this time, the parameter of the view constructor is the number of members. As you can see, the assignment operation of the view array is no different from the operation of the ordinary array.

(3) TypedArray(typedArray)

The constructor of TypedArray array can accept another TypedArray instance as a parameter.

const typedArray = new Int8Array(new Uint8Array(4));

In the above code, the Int8Array constructor accepts an instance of Uint8Array as a parameter.

Note that the new array generated at this time just copies the value of the parameter array, and the corresponding underlying memory is different. The new array will open up a new section of memory to store data, and will not create a view on the memory of the original array.

const x = new Int8Array([1, 1]);
const y = new Int8Array(x);
x[0]; // 1
y[0]; // 1

x[0] = 2;
y[0]; // 1

In the above code, the array y is generated using the array x as a template. When x changes, y does not change.

If you want to construct different views based on the same memory, you can use the following wording.

const x = new Int8Array([1, 1]);
const y = new Int8Array(x.buffer);
x[0]; // 1
y[0]; // 1

x[0] = 2;
y[0]; // 2

(4) TypedArray(arrayLikeObject)

The parameter of the constructor can also be an ordinary array, and then the TypedArray instance is directly generated.

const typedArray = new Uint8Array([1, 2, 3, 4]);

Note that the TypedArray view will reopen the memory at this time, and the view will not be built on the memory of the original array.

The above code generates an 8-bit unsigned integer TypedArray instance from an ordinary array.

TypedArray arrays can also be converted back to ordinary arrays.

const normalArray = [...typedArray];
// or
const normalArray = Array.from(typedArray);
// or
const normalArray = Array.prototype.slice.call(typedArray);

Array method

The operation methods and properties of ordinary arrays are completely applicable to TypedArray arrays.

  • TypedArray.prototype.copyWithin(target, start[, end = this.length])
  • TypedArray.prototype.entries()
  • TypedArray.prototype.every(callbackfn, thisArg?)
  • TypedArray.prototype.fill(value, start=0, end=this.length)
  • TypedArray.prototype.filter(callbackfn, thisArg?)
  • TypedArray.prototype.find(predicate, thisArg?)
  • TypedArray.prototype.findIndex(predicate, thisArg?)
  • TypedArray.prototype.forEach(callbackfn, thisArg?)
  • TypedArray.prototype.indexOf(searchElement, fromIndex=0)
  • TypedArray.prototype.join(separator)
  • TypedArray.prototype.keys()
  • TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)
  • TypedArray.prototype.map(callbackfn, thisArg?)
  • TypedArray.prototype.reduce(callbackfn, initialValue?)
  • TypedArray.prototype.reduceRight(callbackfn, initialValue?)
  • TypedArray.prototype.reverse()
  • TypedArray.prototype.slice(start=0, end=this.length)
  • TypedArray.prototype.some(callbackfn, thisArg?)
  • TypedArray.prototype.sort(comparefn)
  • TypedArray.prototype.toLocaleString(reserved1?, reserved2?)
  • TypedArray.prototype.toString()
  • TypedArray.prototype.values()

For the usage of all the above methods, please refer to the introduction of array methods, which will not be repeated here.

Note that TypedArray arrays do not have a concat method. If you want to merge multiple TypedArray arrays, you can use the following function.

function concatenate(resultConstructor, ...arrays) {
  let totalLength = 0;
  for (let arr of arrays) {
    totalLength += arr.length;
  }
  let result = new resultConstructor(totalLength);
  let offset = 0;
  for (let arr of arrays) {
    result.set(arr, offset);
    offset += arr.length;
  }
  return result;
}

concatenate(Uint8Array, Uint8Array.of(1, 2), Uint8Array.of(3, 4));
// Uint8Array [1, 2, 3, 4]

In addition, TypedArray arrays are the same as ordinary arrays, with the Iterator interface deployed, so they can be traversed.

let ui8 = Uint8Array.of(0, 1, 2);
for (let byte of ui8) {
  console.log(byte);
}
// 0
// 1
// 2

Byte order

Endianness refers to the way a value is represented in memory.

const buffer = new ArrayBuffer(16);
const int32View = new Int32Array(buffer);

for (let i = 0; i < int32View.length; i++) {
  int32View[i] = i * 2;
}

The above code generates a 16-byte ArrayBuffer object, and then builds a 32-bit integer view based on it. Since each 32-bit integer occupies 4 bytes, a total of 4 integers can be written, which are 0, 2, 4, and 6 in turn.

If you build a 16-bit integer view on this piece of data, you can read completely different results.

const int16View = new Int16Array(buffer);

for (let i = 0; i < int16View.length; i++) {
  console.log("Entry " + i + ":" + int16View[i]);
}
// Entry 0: 0
// Entry 1: 0
// Entry 2: 2
// Entry 3: 0
// Entry 4: 4
// Entry 5: 0
// Entry 6: 6
// Entry 7: 0

Since each 16-bit integer occupies 2 bytes, the entire ArrayBuffer object is now divided into 8 segments. Then, because the computers of the x86 system all adopt little endian, the relatively important byte is arranged in the memory address behind, and the relatively unimportant byte is arranged in the front memory address, so the above result is obtained. .

For example, a hexadecimal number 0x12345678 that occupies four bytes, the most important byte that determines its size is "12", and the least important is "78". Little-endian byte order puts the least important bytes first, and the storage order is 78563412; big-endian byte order is completely opposite, puts the most important bytes first, and the storage order is 12345678. At present, almost all personal computers are in little-endian byte order, so the TypedArray array also uses little-endian byte order to read and write data, or more accurately, read and write data according to the byte order set by the operating system of the machine.

This does not mean that big-endian byte order is not important. In fact, many network devices and certain operating systems use big-endian byte order. This brings about a serious problem: if a piece of data is big-endian, the TypedArray array will not be parsed correctly, because it can only handle little-endian! To solve this problem, JavaScript introduces the DataView object, which can set the endianness, which will be described in detail below.

Here is another example.

// Suppose a buffer contains the following bytes [0x02, 0x01, 0x03, 0x07]
const buffer = new ArrayBuffer(4);
const v1 = new Uint8Array(buffer);
v1[0] = 2;
v1[1] = 1;
v1[2] = 3;
v1[3] = 7;

const uInt16View = new Uint16Array(buffer);

// The computer uses little endian
// So the first two bytes are equal to 258
if (uInt16View[0] === 258) {
  console.log("OK"); // "OK"
}

// Assignment operation
uInt16View[0] = 255; // byte becomes [0xFF, 0x00, 0x03, 0x07]
uInt16View[0] = 0xff05; // byte becomes [0x05, 0xFF, 0x03, 0x07]
uInt16View[1] = 0x0210; // byte becomes [0x05, 0xFF, 0x10, 0x02]

The following function can be used to determine whether the current view is little-endian or big-endian.

const BIG_ENDIAN = Symbol("BIG_ENDIAN");
const LITTLE_ENDIAN = Symbol("LITTLE_ENDIAN");

function getPlatformEndianness() {
  let arr32 = Uint32Array.of(0x12345678);
  let arr8 = new Uint8Array(arr32.buffer);
  switch (arr8[0] * 0x1000000 + arr8[1] * 0x10000 + arr8[2] * 0x100 + arr8[3]) {
    case 0x12345678:
      return BIG_ENDIAN;
    case 0x78563412:
      return LITTLE_ENDIAN;
    default:
      throw new Error("Unknown endianness");
  }
}

In short, compared with ordinary arrays, the biggest advantage of TypedArray arrays is that they can directly manipulate the memory without data type conversion, so the speed is much faster.

BYTES_PER_ELEMENT attribute

The constructor of each view has a BYTES_PER_ELEMENT attribute, which indicates the number of bytes occupied by this data type.

Int8Array.BYTES_PER_ELEMENT; // 1
Uint8Array.BYTES_PER_ELEMENT; // 1
Uint8ClampedArray.BYTES_PER_ELEMENT; // 1
Int16Array.BYTES_PER_ELEMENT; // 2
Uint16Array.BYTES_PER_ELEMENT; // 2
Int32Array.BYTES_PER_ELEMENT; // 4
Uint32Array.BYTES_PER_ELEMENT; // 4
Float32Array.BYTES_PER_ELEMENT; // 4
Float64Array.BYTES_PER_ELEMENT; // 8

This property can also be obtained on the TypedArray instance, that is, there is TypedArray.prototype.BYTES_PER_ELEMENT.

ArrayBuffer and string conversion

To convert between ArrayBuffer and string, use native TextEncoder and TextDecoder methods. In order to explain the usage, the following codes are in accordance with the usage of TypeScript, and the type signature is given.

/**
 * Convert ArrayBuffer/TypedArray to String via TextDecoder
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
 */
function ab2str(
  input:
    | ArrayBuffer
    | Uint8Array
    | Int8Array
    | Uint16Array
    | Int16Array
    | Uint32Array
    | Int32Array,
  outputEncoding: string = "utf8"
): string {
  const decoder = new TextDecoder(outputEncoding);
  return decoder.decode(input);
}

/**
 * Convert String to ArrayBuffer via TextEncoder
 *
 * @see https://developer.mozilla.org/zh-CN/docs/Web/API/TextEncoder
 */
function str2ab(input: string): ArrayBuffer {
  const view = str2Uint8Array(input);
  return view.buffer;
}

/** Convert String to Uint8Array */
function str2Uint8Array(input: string): Uint8Array {
  const encoder = new TextEncoder();
  const view = encoder.encode(input);
  return view;
}

In the above code, the second parameter outputEncoding of ab2str() gives the encoding of the output encoding, generally keep the default value (utf-8), for other optional values, please refer to [Official Document](https:/ /encoding.spec.whatwg.org) or Node.js documentation.

Overflow

The range of values ​​that can be accommodated for different view types is determined. Beyond this range, overflow will occur. For example, an 8-bit view can only hold an 8-bit binary value, and if you put a 9-bit value, it will overflow.

The overflow handling rule for TypedArray arrays, in simple terms, is to discard the overflow bits, and then explain them according to the view type.

const uint8 = new Uint8Array(1);

uint8[0] = 256;
uint8[0]; // 0

uint8[0] = -1;
uint8[0]; // 255

In the above code, uint8 is an 8-bit view, and the binary form of 256 is a 9-bit value 100000000, then overflow will occur. According to the rules, only the last 8 bits are reserved, which is 00000000. The interpretation rule of the uint8 view is an unsigned 8-bit integer, so 00000000 is 0.

Negative numbers are represented by "2's complement" in the computer, that is to say, the corresponding positive value is negated, and then 1 is added. For example, the positive value corresponding to -1 is 1, after performing the negation operation, 11111110 is obtained, and adding 1 is the complement form 11111111. uint8 interprets 11111111 according to an unsigned 8-bit integer, and the return result is 255.

A simple conversion rule can be expressed like this.

-Positive overflow (overflow): When the input value is greater than the maximum value of the current data type, the result is equal to the minimum value of the current data type plus the remaining value, and then minus 1. -Underflow: When the input value is less than the minimum value of the current data type, the result is equal to the maximum value of the current data type minus the absolute value of the residual value, plus 1.

The "residual value" above is the result of the modular operation, that is, the result of the % operator in JavaScript.

12 % 4; // 0
12 % 5; // 2

In the above code, 12 divided by 4 has no residual value, and divided by 5 will give a residual value of 2.

Please see the example below.

const int8 = new Int8Array(1);

int8[0] = 128;
int8[0]; // -128

int8[0] = -129;
int8[0]; // 127

In the above example, int8 is a signed 8-bit integer view. Its maximum value is 127 and its minimum value is -128. When the input value is 128, it is equivalent to positive overflow of 1. According to the rule of "minimum value plus residual value (remaining value of 128 divided by 127 is 1), then subtract 1" rule, it will return - 128; when the input value is -129, it is equivalent to negative overflow 1, according to "maximum minus the absolute value of the residual value (the absolute value of the residual value of -129 divided by -128 is 1), and then Adding the rule of 1" will return 127.

The overflow rule of the Uint8ClampedArray view is different from the above rule. It stipulates that whenever a positive overflow occurs, the value is equal to the maximum value of the current data type, which is 255; if a negative overflow occurs, the value is always equal to the minimum value of the current data type, which is 0.

const uint8c = new Uint8ClampedArray(1);

uint8c[0] = 256;
uint8c[0]; // 255

uint8c[0] = -1;
uint8c[0]; // 0

In the above example, uint8C is a view of Uint8ClampedArray, and it returns 255 when it overflows in the positive direction, and it returns 0 when it overflows in the negative direction.

TypedArray.prototype.buffer

The buffer property of the TypedArray instance returns the ArrayBuffer object corresponding to the entire memory area. This attribute is read-only.

const a = new Float32Array(64);
const b = new Uint8Array(a.buffer);

The a view object and the b view object in the above code correspond to the same ArrayBuffer object, that is, the same memory.

TypedArray.prototype.byteLength, TypedArray.prototype.byteOffset

The byteLength property returns the memory length occupied by the TypedArray array, in bytes. The byteOffset property returns which byte of the underlying ArrayBuffer object the TypedArray array starts. Both of these attributes are read-only.

const b = new ArrayBuffer(8);

const v1 = new Int32Array(b);
const v2 = new Uint8Array(b, 2);
const v3 = new Int16Array(b, 2, 2);

v1.byteLength; // 8
v2.byteLength; // 6
v3.byteLength; // 4

v1.byteOffset; // 0
v2.byteOffset; // 2
v3.byteOffset; // 2

TypedArray.prototype.length

The length property indicates how many members the TypedArray array contains. Note that the length attribute is distinguished from the byteLength attribute. The former is the member length and the latter is the byte length.

const a = new Int16Array(8);

a.length; // 8
a.byteLength; // 16

TypedArray.prototype.set()

The set method of TypedArray arrays is used to copy arrays (ordinary arrays or TypedArray arrays), that is, to copy a piece of content completely to another piece of memory.

const a = new Uint8Array(8);
const b = new Uint8Array(8);

b.set(a);

The above code copies the contents of the a array to the b array. It is a copy of the entire memory, which is much faster than the copy of the members one by one.

The set method can also accept a second parameter, which indicates which member of the b object to start copying the a object.

const a = new Uint16Array(8);
const b = new Uint16Array(10);

b.set(a, 2);

The b array in the above code has two more members than the a array, so start copying from b[2].

TypedArray.prototype.subarray()

The subarray method is to create a new view for a part of the TypedArray array.

const a = new Uint16Array(8);
const b = a.subarray(2, 3);

a.byteLength; // 16
b.byteLength; // 2

The first parameter of the subarray method is the starting member number, and the second parameter is the ending member number (excluding this member). If omitted, all remaining members are included. Therefore, a.subarray(2,3) in the above code means that b contains only one member of a[2], and the byte length is 2.

TypedArray.prototype.slice()

The slice method of TypeArray instance can return a new TypedArray instance at a specified position.

let ui8 = Uint8Array.of(0, 1, 2);
ui8.slice(-1);
// Uint8Array [2]

In the above code, ui8 is an instance of an 8-bit unsigned integer array view. Its slice method can return a new view instance from the current view.

The parameter of the slice method indicates the specific position of the original array, and the new array is generated. Negative value indicates the reverse position, that is, -1 is the penultimate position, -2 is the penultimate position, and so on.

TypedArray.of()

All constructors of TypedArray arrays have a static method of, which is used to convert the parameters into a TypedArray instance.

Float32Array.of(0.151, -8, 3.7);
// Float32Array [0.151, -8, 3.7]

The following three methods will generate the same TypedArray array.

// method one
let tarr = new Uint8Array([1, 2, 3]);

// Method Two
let tarr = Uint8Array.of(1, 2, 3);

// Method three
let tarr = new Uint8Array(3);
tarr[0] = 1;
tarr[1] = 2;
tarr[2] = 3;

TypedArray.from()

The static method from accepts a traversable data structure (such as an array) as a parameter and returns a TypedArray instance based on this structure.

Uint16Array.from([0, 1, 2]);
// Uint16Array [0, 1, 2]

This method can also convert one type of TypedArray instance to another.

const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
ui16 instanceof Uint16Array; // true

The from method can also accept a function as the second parameter to traverse each element. The function is similar to the map method.

Int8Array.of(127, 126, 125).map((x) => 2 * x);
// Int8Array [-2, -4, -6]

Int16Array.from(Int8Array.of(127, 126, 125), (x) => 2 * x);
// Int16Array [254, 252, 250]

In the above example, the from method did not overflow, which shows that the traversal is not for the original 8-bit integer array. In other words, from will copy the TypedArray array specified by the first parameter to another memory, and then convert the result into the specified array format after processing.

Composite view

Since the view's constructor can specify the starting position and length, different types of data can be stored in the same memory in sequence, which is called a "composite view".

const buffer = new ArrayBuffer(24);

const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);

The above code divides an ArrayBuffer object with a length of 24 bytes into three parts:

-Byte 0 to Byte 3: 1 32-bit unsigned integer -Byte 4 to Byte 19: 16 8-bit integers -Byte 20 to Byte 23: 1 32-bit floating point number

This data structure can be described in the following C language:

struct someStruct {
  unsigned long id;
  char username[16];
  float amountDue;
};

DataView View

If a piece of data includes multiple types (such as HTTP data from the server), in addition to creating a composite view of the ArrayBuffer object, you can also operate it through the DataView view.

The DataView view provides more operation options and supports setting endianness. Originally, for design purposes, the various TypedArray views of the ArrayBuffer object are used to transmit data to local devices such as network cards and sound cards, so the endianness of the machine can be used; and DataView The design purpose of the view is to process the data from the network device, so the big-endian or little-endian can be set by yourself.

The DataView view itself is also a constructor, accepting an ArrayBuffer object as a parameter to generate the view.

new DataView(ArrayBuffer buffer [, byte starting position [, length]]);

Below is an example.

const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);

The DataView instance has the following properties, which have the same meaning as the method of the same name of the TypedArray instance.

-DataView.prototype.buffer: returns the corresponding ArrayBuffer object -DataView.prototype.byteLength: returns the byte length of memory occupied -DataView.prototype.byteOffset: Returns which byte of the corresponding ArrayBuffer object the current view starts from

The DataView instance provides 8 methods to read memory.

-getInt8: Read 1 byte and return an 8-bit integer. -getUint8: Read 1 byte and return an unsigned 8-bit integer. -getInt16: Read 2 bytes and return a 16-bit integer. -getUint16: Read 2 bytes and return an unsigned 16-bit integer. -getInt32: Read 4 bytes and return a 32-bit integer. -getUint32: Read 4 bytes and return an unsigned 32-bit integer. -getFloat32: Read 4 bytes and return a 32-bit floating point number. -getFloat64: Read 8 bytes and return a 64-bit floating point number.

The parameters of this series of get methods are all a byte sequence number (cannot be a negative number, otherwise an error will be reported), indicating the byte from which to start reading.

const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);

// Read an 8-bit unsigned integer from the first byte
const v1 = dv.getUint8(0);

// Read a 16-bit unsigned integer from the second byte
const v2 = dv.getUint16(1);

// Read a 16-bit unsigned integer from the 4th byte
const v3 = dv.getUint16(3);

The above code reads the first 5 bytes of the ArrayBuffer object, which contains an 8-bit integer and two 16-bit integers.

If you read two or more bytes at a time, you must clarify how the data is stored, whether it is little-endian or big-endian. By default, the get method of DataView uses big-endian byte order to interpret the data. If you need to use little-endian byte order to interpret the data, you must specify true in the second parameter of the get method.

// little endian
const v1 = dv.getUint16(1, true);

// Big endian
const v2 = dv.getUint16(3, false);

// Big endian
const v3 = dv.getUint16(3);

The DataView view provides 8 methods to write to memory.

-setInt8: Write a 1-byte 8-bit integer. -setUint8: Write a 1-byte 8-bit unsigned integer. -setInt16: Write a 2-byte 16-bit integer. -setUint16: Write 2 bytes of 16-bit unsigned integer. -setInt32: Write a 4-byte 32-bit integer. -setUint32: Write a 4-byte 32-bit unsigned integer. -setFloat32: Write 4 bytes of 32-bit floating point numbers. -setFloat64: Write 8 bytes of 64-bit floating point numbers.

This series of set methods accept two parameters. The first parameter is the byte sequence number, which indicates the byte from which to start writing, and the second parameter is the data to be written. For those methods that write two or more bytes, you need to specify the third parameter, false or undefined means to write in big-endian byte order, and true means to write in little-endian byte order Into.

// In the first byte, write a 32-bit integer with a value of 25 in big-endian byte order
dv.setInt32(0, 25, false);

// In the 5th byte, write a 32-bit integer with a value of 25 in big-endian byte order
dv.setInt32(4, 25);

// In the 9th byte, write a 32-bit floating point number with a value of 2.5 in little-endian byte order
dv.setFloat32(8, 2.5, true);

If you are not sure of the endianness of the computer you are using, you can use the following judgment method.

const littleEndian = (function () {
  const buffer = new ArrayBuffer(2);
  new DataView(buffer).setInt16(0, 256, true);
  return new Int16Array(buffer)[0] === 256;
})();

If it returns true, it is little-endian; if it returns false, it is big-endian.

Application of Binary Array

A lot of Web API uses the ArrayBuffer object and its view object.

AJAX

Traditionally, the server can only return text data through AJAX operations, that is, the responseType attribute defaults to text. The second edition of XMLHttpRequest, XHR2, allows the server to return binary data, which is divided into two cases. If you know the returned binary data type, you can set the return type (responseType) to arraybuffer; if you don't know, set it to blob.

let xhr = new XMLHttpRequest();
xhr.open("GET", someUrl);
xhr.responseType = "arraybuffer";

xhr.onload = function () {
  let arrayBuffer = xhr.response;
  // ···
};

xhr.send();

If you know that it is a 32-bit integer, you can handle it as follows.

xhr.onreadystatechange = function () {
  if (req.readyState === 4) {
    const arrayResponse = xhr.response;
    const dataView = new DataView(arrayResponse);
    const ints = new Uint32Array(dataView.byteLength / 4);

    xhrDiv.style.backgroundColor = "#00FF00";
    xhrDiv.innerText = "Array is " + ints.length + "uints long";
  }
};

Canvas

The binary pixel data output by the Canvas element of the webpage is a TypedArray array.

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const uint8ClampedArray = imageData.data;

It should be noted that although the uint8ClampedArray in the above code is a TypedArray array, its view type is a proprietary type Uint8ClampedArray for the elements of Canvas. The feature of this view type is that it interprets each byte as an unsigned 8-bit integer specifically for color, that is, it can only take values ​​from 0 to 255, and it automatically filters high-order overflows when operations occur. This brings great convenience to image processing.

For example, if the color value of the pixel is set to the Uint8Array type, when multiplying by a gamma value, it must be calculated as follows:

u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));

Because the Uint8Array type will automatically change to 0x00 for calculation results greater than 255 (such as 0xFF+1), the image processing must be calculated as above. This is troublesome and affects performance. If the color value is set to the Uint8ClampedArray type, the calculation is much simplified.

pixels[i] *= gamma;

The Uint8ClampedArray type ensures that values ​​less than 0 are set to 0, and values ​​greater than 255 are set to 255. Note that IE 10 does not support this type.

WebSocket

WebSocket can send or receive binary data through ArrayBuffer.

let socket = new WebSocket("ws://127.0.0.1:8081");
socket.binaryType = "arraybuffer";

// Wait until socket is open
socket.addEventListener("open", function (event) {
  // Send binary data
  const typedArray = new Uint8Array(4);
  socket.send(typedArray.buffer);
});

// Receive binary data
socket.addEventListener("message", function (event) {
  const arrayBuffer = event.data;
  // ···
});

Fetch API

The data retrieved by the Fetch API is the ArrayBuffer object.

fetch(url)
  .then(function (response) {
    return response.arrayBuffer();
  })
  .then(function (arrayBuffer) {
    // ...
  });

File API

If you know the binary data type of a file, you can also read the file as an ArrayBuffer object.

const fileInput = document.getElementById("fileInput");
const file = fileInput.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function () {
  const arrayBuffer = reader.result;
  // ···
};

The following is an example of processing bmp files. Assuming that the file variable is a file object pointing to a bmp file, the file is read first.

const reader = new FileReader();
reader.addEventListener("load", processimage, false);
reader.readAsArrayBuffer(file);

Then, define the callback function for processing the image: first create a DataView view on top of the binary data, then create a bitmap object to store the processed data, and finally display the image in the Canvas element.

function processimage(e) {
  const buffer = e.target.result;
  const datav = new DataView(buffer);
  const bitmap = {};
  // Specific processing steps
}

When processing image data, first process the header of the bmp file. For the specific format and definition of each file header, please refer to the relevant information.

bitmap.fileheader = {};
bitmap.fileheader.bfType = datav.getUint16(0, true);
bitmap.fileheader.bfSize = datav.getUint32(2, true);
bitmap.fileheader.bfReserved1 = datav.getUint16(6, true);
bitmap.fileheader.bfReserved2 = datav.getUint16(8, true);
bitmap.fileheader.bfOffBits = datav.getUint32(10, true);

Then process the image meta information part.

bitmap.infoheader = {};
bitmap.infoheader.biSize = datav.getUint32(14, true);
bitmap.infoheader.biWidth = datav.getUint32(18, true);
bitmap.infoheader.biHeight = datav.getUint32(22, true);
bitmap.infoheader.biPlanes = datav.getUint16(26, true);
bitmap.infoheader.biBitCount = datav.getUint16(28, true);
bitmap.infoheader.biCompression = datav.getUint32(30, true);
bitmap.infoheader.biSizeImage = datav.getUint32(34, true);
bitmap.infoheader.biXPelsPerMeter = datav.getUint32(38, true);
bitmap.infoheader.biYPelsPerMeter = datav.getUint32(42, true);
bitmap.infoheader.biClrUsed = datav.getUint32(46, true);
bitmap.infoheader.biClrImportant = datav.getUint32(50, true);

Finally, the pixel information of the image itself is processed.

const start = bitmap.fileheader.bfOffBits;
bitmap.pixels = new Uint8Array(buffer, start);

At this point, all the data of the image file has been processed. In the next step, the image can be deformed or converted as needed, or displayed in the Canvas web page element.

SharedArrayBuffer

JavaScript is single-threaded, and Web worker introduces multi-threading: the main thread is used to interact with the user, and the worker thread is used to undertake computing tasks. The data of each thread is isolated and communicated via postMessage(). Below is an example.

// main thread
const w = new Worker("myworker.js");

In the above code, the main thread creates a new Worker thread. There will be a communication channel between this thread and the main thread. The main thread sends a message to the Worker thread through w.postMessage, and at the same time listens to the response of the Worker thread through the message event.

// main thread
w.postMessage("hi");
w.onmessage = function (ev) {
  console.log(ev.data);
};

In the above code, the main thread sends a message hi first, and then prints it out after listening to the response of the Worker thread.

The Worker thread also obtains the message sent by the main thread by listening to the message event, and reacts to it.

// Worker thread
onmessage = function (ev) {
  console.log(ev.data);
  postMessage("ho");
};

The data exchange between threads can be in various formats, not only strings, but also binary data. This exchange uses a replication mechanism, that is, one process copies the data that needs to be shared and passes it to another process through the postMessage method. If the amount of data is relatively large, the efficiency of this communication is obviously relatively low. It is easy to think that a memory area can be set aside at this time, shared by the main thread and the Worker thread, and both parties can read and write, then the efficiency will be greatly improved, and the collaboration will be relatively simple (not as troublesome as postMessage) .

ES2017 introduced SharedArrayBuffer, allowing the Worker thread to share the same memory with the main thread. The API of SharedArrayBuffer is exactly the same as that of ArrayBuffer, the only difference is that the latter cannot share data.

// main thread

// Create a new 1KB shared memory
const sharedBuffer = new SharedArrayBuffer(1024);

// The main thread sends out the address of the shared memory
w.postMessage(sharedBuffer);

// Establish a view on shared memory for writing data
const sharedArray = new Int32Array(sharedBuffer);

In the above code, the parameter of the postMessage method is the SharedArrayBuffer object.

The Worker thread gets the data from the data attribute of the event.

// Worker thread
onmessage = function (ev) {
  // The data shared by the main thread is 1KB of shared memory
  const sharedBuffer = ev.data;

  // Establish a view on shared memory for easy reading
  const sharedArray = new Int32Array(sharedBuffer);

  // ...
};

Shared memory can also be created in the Worker thread and sent to the main thread.

SharedArrayBuffer is the same as ArrayBuffer, it cannot read and write by itself. You must create a view on it, and then read and write through the view.

// allocate memory space occupied by 100,000 32-bit integers
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);

// Create a 32-bit integer view
const ia = new Int32Array(sab); // ia.length == 100000

// Create a new prime number generator
const primes = new PrimeGenerator();

// Write 100,000 prime numbers into this memory space
for (let i = 0; i < ia.length; i++) ia[i] = primes.next();

// Send this shared memory to the Worker thread
w.postMessage(ia);

The processing of the worker thread after receiving the data is as follows.

// Worker thread
let ia;
onmessage = function (ev) {
  ia = ev.data;
  console.log(ia.length); // 100000
  console.log(ia[37]); // Output 163, because this is the 38th prime number
};

Atomics Object

Multi-threaded shared memory, the biggest problem is how to prevent two threads from modifying an address at the same time, or in other words, when a thread modifies the shared memory, there must be a mechanism to synchronize other threads. The SharedArrayBuffer API provides the Atomics object to ensure that all shared memory operations are "atomic" and can be synchronized in all threads.

What is "atomic operation"? In modern programming languages, after a common command is processed by the compiler, it will become multiple machine instructions. If it is a single-threaded operation, this is no problem; in a multi-threaded environment and shared memory, there will be problems, because during the operation of this group of machine instructions, instructions from other threads may be inserted, resulting in errors in the operation results. Please see the example below.

// main thread
ia[42] = 314159; // the original value is 191
ia[37] = 123456; // the original value is 163

// Worker thread
console.log(ia[37]);
console.log(ia[42]);
// possible outcome
// 123456
// 191

In the above code, the original order of the main thread is to first assign a value to position 42 and then to position 37. However, for optimization, the compiler and CPU may change the execution order of these two operations (because they do not depend on each other), first assign a value to position 37, and then assign a value to position 42. In the middle of the execution, the Worker thread may come to read the data, resulting in the printout of 123456 and 191.

Here is another example.

// main thread
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);
const ia = new Int32Array(sab);

for (let i = 0; i < ia.length; i++) {
  ia[i] = primes.next(); // put prime numbers into ia
}

// worker thread
ia[112]++; // error
Atomics.add(ia, 112, 1); // correct

In the above code, it is incorrect for the Worker thread to directly rewrite the shared memory ia[112]++. Because this line of statements will be compiled into multiple machine instructions, there is no guarantee that instructions from other processes will not be inserted between these instructions. Imagine that if two threads are ia[112]++ at the same time, it is likely that they will get incorrect results.

The Atomics object is proposed to solve this problem. It can ensure that multiple machine instructions corresponding to an operation must run as a whole without being interrupted in the middle. In other words, all the operations involved can be regarded as atomic single operations, which can avoid thread competition and improve the safety of operations when multi-threaded shared memory. Therefore, ia[112]++ should be rewritten as Atomics.add(ia, 112, 1).

The Atomics object provides multiple methods.

(1) Atomics.store(), Atomics.load()

The store() method is used to write data to the shared memory, and the load() method is used to read data from the shared memory. Compared with direct read and write operations, their advantage is to ensure the atomicity of read and write operations.

In addition, they are also used to solve a problem: multiple threads use a certain location in shared memory as a switch (flag), and once the value of the location changes, perform a specific operation. At this time, it must be ensured that the assignment operation at this position must be performed after all operations that may rewrite the memory in front of it are completed; and the value operation at this position must be all operations that may read the position after it. Execute before you start. The store() method and the load() method can do this, and the compiler will not disrupt the execution order of machine instructions for optimization.

Atomics.load(typedArray, index);
Atomics.store(typedArray, index, value);

The store() method accepts three parameters: typedArray object (View of SharedArrayBuffer), position index and value, and returns the value of typedArray[index]. The load() method only accepts two parameters: the typedArray object (View of SharedArrayBuffer) and the position index, which also returns the value of typedArray[index].

// main thread main.js
ia[42] = 314159; // the original value is 191
Atomics.store(ia, 37, 123456); // the original value is 163

// Worker thread worker.js
while (Atomics.load(ia, 37) == 163);
console.log(ia[37]); // 123456
console.log(ia[42]); // 314159

In the above code, the assignment of the main thread's Atomics.store() to position 42 must be earlier than the assignment of position 37. As long as position 37 is equal to 163, the Worker thread will not terminate the loop, and the value of position 37 and position 42 must be after the Atomics.load() operation.

Here is another example.

// main thread
const worker = new Worker("worker.js");
const length = 10;
const size = Int32Array.BYTES_PER_ELEMENT * length;
// Create a new shared memory
const sharedBuffer = new SharedArrayBuffer(size);
const sharedArray = new Int32Array(sharedBuffer);
for (let i = 0; i < 10; i++) {
  // write 10 integers to shared memory
  Atomics.store(sharedArray, i, 0);
}
worker.postMessage(sharedBuffer);

In the above code, the main thread uses the Atomics.store() method to write data. Below is the Worker thread using the Atomics.load() method to read data.

// worker.js
self.addEventListener(
  "message",
  (event) => {
    const sharedArray = new Int32Array(event.data);
    for (let i = 0; i < 10; i++) {
      const arrayValue = Atomics.load(sharedArray, i);
      console.log(`The item at array index ${i} is ${arrayValue}`);
    }
  },
  false
);

(2) Atomics.exchange()

If the Worker thread wants to write data, you can use the above Atomics.store() method, or use the Atomics.exchange() method. The difference between them is that Atomics.store() returns the written value, while Atomics.exchange() returns the replaced value.

// Worker thread
self.addEventListener(
  "message",
  (event) => {
    const sharedArray = new Int32Array(event.data);
    for (let i = 0; i < 10; i++) {
      if (i % 2 === 0) {
        const storedValue = Atomics.store(sharedArray, i, 1);
        console.log(`The item at array index ${i} is now ${storedValue}`);
      } else {
        const exchangedValue = Atomics.exchange(sharedArray, i, 2);
        console.log(
          `The item at array index ${i} was ${exchangedValue}, now 2`
        );
      }
    }
  },
  false
);

The above code changes the value of the even-numbered location of the shared memory to 1, and the value of the odd-numbered location to 2.

(3) Atomics.wait(), Atomics.notify()

Using the while loop to wait for the notification of the main thread is not very efficient. If it is used in the main thread, it will cause a stall. The Atomics object provides two methods of wait() and notify() for waiting Notice. These two methods are equivalent to locking memory, that is, when a thread is operating, let other threads sleep (establish a lock), wait until the operation is over, and then wake up those sleeping threads (unlock).

The Atomics.notify() method was formerly called Atomics.wake(), but was later renamed.

// Worker thread
self.addEventListener(
  "message",
  (event) => {
    const sharedArray = new Int32Array(event.data);
    const arrayIndex = 0;
    const expectedStoredValue = 50;
    Atomics.wait(sharedArray, arrayIndex, expectedStoredValue);
    console.log(Atomics.load(sharedArray, arrayIndex));
  },
  false
);

In the above code, the Atomics.wait() method is equivalent to telling the worker thread that as long as the given condition is met (the position of 0 of sharedArray is equal to 50), the worker thread will go to sleep in this line.

Once the main thread changes the value of the specified position, it can wake up the Worker thread.

// main thread
const newArrayValue = 100;
Atomics.store(sharedArray, 0, newArrayValue);
const arrayIndex = 0;
const queuePos = 1;
Atomics.notify(sharedArray, arrayIndex, queuePos);

In the above code, the 0 position of sharedArray is changed to 100, and then the Atomics.notify() method is executed to wake up a thread in the sleep queue at the 0 position of sharedArray.

The usage format of the Atomics.wait() method is as follows.

Atomics.wait(sharedArray, index, value, timeout);

The meanings of its four parameters are as follows.

-sharedArray: View array of shared memory. -index: The position of the view data (starting from 0). -value: The expected value of the position. Once the actual value is equal to the expected value, it goes to sleep. -timeout: Integer, which means it will wake up automatically after this time, in milliseconds. This parameter is optional. The default value is Infinity, that is, sleep indefinitely. It can only be awakened by the Atomics.notify() method.

The return value of Atomics.wait() is a string with three possible values. If sharedArray[index] is not equal to value, it returns the string not-equal, otherwise it goes to sleep. If the Atomics.notify() method wakes up, it returns the string ok; if it wakes up due to a timeout, it returns the string timed-out.

The usage format of the Atomics.notify() method is as follows.

Atomics.notify(sharedArray, index, count);

The meanings of its three parameters are as follows.

-sharedArray: View array of shared memory. -index: The position of the view data (starting from 0). -count: The number of Worker threads that need to be awakened, the default is Infinity.

Once the Atomics.notify() method wakes up the sleeping Worker thread, it will continue to run.

Please see an example.

// main thread
console.log(ia[37]); // 163
Atomics.store(ia, 37, 123456);
Atomics.notify(ia, 37, 1);

// Worker thread
Atomics.wait(ia, 37, 163);
console.log(ia[37]); // 123456

In the above code, the 37th position of the view array ia, the original value is 163. The Worker thread uses the Atomics.wait() method to specify that as long as ia[37] is equal to 163, it will enter the sleep state. The main thread uses the Atomics.store() method to write 123456 into ia[37], and then uses the Atomics.notify() method to wake up the Worker thread.

In addition, the implementation of lock memory based on the two methods of wait and notify can be seen in Lars T Hansen's [js-lock-and-condition](https://github.com/lars-t-hansen/js -lock-and-condition) this library.

Note that the main thread of the browser should not be set to sleep, which will cause the user to lose response. Moreover, the main thread will actually refuse to go to sleep.

(4) Operation method

Certain operations on the shared memory cannot be interrupted, that is, other threads cannot be allowed to rewrite the value on the memory during the operation. The Atomics object provides some arithmetic methods to prevent data from being rewritten.

Atomics.add(sharedArray, index, value);

Atomics.add is used to add value to sharedArray[index] and return the old value of sharedArray[index].

Atomics.sub(sharedArray, index, value);

Atomics.sub is used to subtract value from sharedArray[index] and return the old value of sharedArray[index].

Atomics.and(sharedArray, index, value);

Atomics.and is used to perform bit operation and between value and sharedArray[index], put it in sharedArray[index], and return the old value.

Atomics.or(sharedArray, index, value);

Atomics.or is used to perform bit operation or between value and sharedArray[index], put it in sharedArray[index], and return the old value.

Atomics.xor(sharedArray, index, value);

Atomic.xor is used to perform bitwise operation xor between vaule and sharedArray[index], put it into sharedArray[index], and return the old value.

(5) Other methods

The Atomics object also has the following methods.

-Atomics.compareExchange(sharedArray, index, oldval, newval): If sharedArray[index] is equal to oldval, write newval and return oldval. -Atomics.isLockFree(size): returns a boolean value indicating whether the Atomics object can handle a memory lock of a size. If false is returned, the application needs to implement the lock itself.

One purpose of Atomics.compareExchange is to read a value from SharedArrayBuffer, and then perform an operation on the value. After the operation is over, check whether the original value in SharedArrayBuffer has changed (that is, it has been overwritten by other threads). If it has not been rewritten, write it back to the original location, otherwise read the new value and perform the operation again.