Categories
Modern JavaScript

Best of Modern JavaScript — Closable Iterators

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at JavaScript iterable objects.

Closable Iterators

An iterator is closable if it has the return method.

This is an optional feature.

Array iterators aren’t closable.

For instance, if we write:

const arr = ['foo', 'bar', 'baz'];
const iterator = arr[Symbol.iterator]();
console.log('return' in iterator)

We see that there’s no return method.

On the other hand, generator objects are closable by default.

For instance, if we have:

function* genFn() {
  yield 'foo';
  yield 'bar';
  yield 'baz';
}

const gen = genFn();

We can finish calling the generator gen by calling the return method on it.

For instance, we can write:

function* genFn() {
  yield 'foo';
  yield 'bar';
  yield 'baz';
}

const gen = genFn();
console.log(gen.next());
gen.return();
console.log(gen.next());
console.log(gen.next());

After we called return , then the generator is done.

So the last 2 return calls give us:

{value: undefined, done: true}

If an iterator isn’t closable, we can continue iterating over it after an exit from a for-of loop.

So if we have an iterable with a non-closable iterator like an array, we can continue looping through it:

const arr = ['foo', 'bar', 'baz'];

for (const x of arr) {
  console.log(x);
  break;
}
for (const x of arr) {
  console.log(x);
}

After we used break to break out of the first loop, the 2nd loop still iterate from start to end.

Prevent Iterators from Being Closed

We can prevent iterators from being closed by changing the return method.

If we return done set to false , then it can’t be closed.

For example, we can write:

class NoReturn {
  constructor(iterator) {
    this.iterator = iterator;
  }
  [Symbol.iterator]() {
    return this;
  }
  next() {
    return this.iterator.next();
  }
  return (value) {
    return {
      done: false,
      value
    };
  }
}

function* genFn() {
  yield 'foo';
  yield 'bar';
  yield 'baz';
}
const gen = genFn();
const noReturn = new NoReturn(gen);

for (const x of noReturn) {
  console.log(x);
  break;
}
for (const x of noReturn) {
  console.log(x);
}

Since we passed our generator, which is closeable, to the NoReturn constructor, it’ll continue iterating through the items.

So we get:

foo
bar
baz

logged.

It just continues the iteration from where it left off since it hasn’t been closed.

So it can continue iteration from where it left off.

We can also make generators unclosable by setting the prototype.return method to undefined .

However, we should be aware that this doesn’t work with all transpilers.

For instance, we can write:

function* genFn() {
  yield 'foo';
  yield 'bar';
  yield 'baz';
}

genFn.prototype.return = undefined;

We set the genFn.prototype.return property to be undefined .

Then we can confirm that it’s unclosable by writing:

const gen = genFn();

for (const x of gen) {
  console.log(x);
  break;
}
for (const x of gen) {
  console.log(x);
}

We get:

foo
bar
baz

from the 2 loops.

Using break didn’t close the generator.

Conclusion

We can adjust whether we can close an iterator.

If closing is disabled, this will let us loop through the iterator items after breaking and similar operations.

Categories
Modern JavaScript

Best of Modern JavaScript — Async and Promises

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at JavaScript async programming.

Sync and Async

The synchronous code always runs before the async code.

So if we have:

setTimeout(function() {
  console.log(2);
}, 0);
console.log(1);

Then we get:

1
2

The console log outside is synchronous, so it runs first.

The callback’s console log runs after the synchronous code, since it’s async.

The event loop may be blocked if we run something that’s synchronous since the whole app is managed by a single process.

Both the user interface and other computations are all in one thread.

So if one thing runs, then whatever comes after it can’t run until that thing is done.

For instance, if we have a synchronous sleep function, then that’ll pause the execution of the whole program.

If we have:

function sleep(ms) {
  const start = Date.now();
  while ((Date.now() - start) < ms);
}

console.log('start');
sleep(2000);
console.log('end');

Then we see 'start' logged first, then wait 2 seconds, then 'end' is logged.

The sleep function uses a while loop, so it’s synchronous.

This means it’ll hold up the whole program from running.

Avoiding Blocking

To avoid blocking the UI thread, we can use different kinds of async code like web workers, or setTimeout or Promises.

One example of async code is the Fetch API.

We can get data by making HTTP requests without blocking the whole thread.

For instance, we can write:

fetch('https://api.agify.io/?name=michael')
  .then(res => res.json())
  .then(res => {
    console.log(res);
  })

to use the fetch function to get the data.

It returns a promise so it’s async.

Then then callbacks are called only when the results are ready.

When it’s not the promise is paused until a result is obtained.

Asynchronous Results

Other examples, include Node.js async callbacks.

For example, the readFile method takes a callback that’s run asynchronously when the file is read.

We can write:

fs.readFile('foo.txt', {
    encoding: 'utf8'
  },
  function(error, text) {
    if (error) {
      // ...
    }
    console.log(text);
  });

Then we read file.txt from with readFile .

Callbacks

Callbacks are problematic because error handling is complicated.

We’ve to handle errors at each callback.

The signatures are also less elegant because there’s no separation of concerns between inputs and outputs.

Async functions use callback functions, which can’t return anything.

Composition is also more complicated.

Node style callbacks some problems.

We’ve to check for errors with if statements.

Reusing error handlers is harder.

And providing a default handler is also harder.

Promises

Promises are a pattern of async programming where a single result is returned asynchronously.

They’re better than callbacks since they can be chained.

Promises are returned by various functions and serve as the placeholder for the final result.

For instance, a promise chain may be written by writing:

asyncFunction(arg)
  .then(result => {
    console.log(result);
  });

We can have more than one then call if the then callback returns a promise.

For instance, we can write:

asyncFunction(arg)
  .then(result1 => {
    console.log(result1);
    return asyncFunction2(x, y);
  })
  .then(result2 => {
    console.log(result2);
  });

Conclusion

Promises are a better way to write async code in JavaScript.

Categories
Modern JavaScript

Best of Modern JavaScript — Typed Arrays

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at JavaScript typed arrays.

Clamped Conversion

Clamped conversions work differently than module conversion.

JavaScript provides us with constructors with typed arrays that does clamp conversions such as the Uint8ClampedArray constructor.

It works differently from modulo conversion in that all underflowing values are converted to the lowest value.

And all overflowing values are converted to the highest value.

For example, we can create one by writing:

const uint8c = new Uint8ClampedArray(1);

Then if we write:

uint8c[0] = 255;

We get 255 as the value of uint8c[0] .

If we write:

uint8c[0] = 256;

We still get 255.

On the other hand, if we write:

uint8c[0] = 0;

We get 0.

And if we write:

uint8c[0] = -1;

We still get 0.

Endianness

The endianness matters if we store multiple bytes in our typed arrays.

Big-endian means the most significant byte comes first.

So if we have 2 bytes like 0xABCD , then 0xAB comes first and then 0xCD .

Little-endian means the least significant byte comes first.

This means the order of the digits is stored opposite of the way that they’re a store in a big-endian array.

Endianness different between CPU architectures and are consistent across native APIs.

Typed arrays kets us to communicate with those APIs.

This is why their endianness can’t be changed.

The endianness of binary files and protocols are fixed across platforms.

DataViews lets us specify the endianness so that we can transport files and communicate with different protocols across multiple platforms.

Negative Indexes

Negative index can be used with the slice method.

Index -1 means the last element of the typed array.

For instance, we can write:

const arr = Uint8Array.of(0, 1, 2);
const last = arr.slice(-1);

We created an Uint8Array with some numbers.

Then we called slice with -1 to return a typed array with the last entry of arr .

Offsets must be non-negative integers.

So if we pass in a negative number to the DataView.prototype.getInt8 method, we’ll get a RangeError.

ArrayBuffers

ArrayBuffers store the data and views let us read and change them.

To create a DataView, we got to provide the constructor with an ArrayBuffer.

Typed array constructors can optionally create ArrayBuffers for us.

The ArrayBuffer constructor takes a number with the length of the ArrayBuffers.

The ArrayBuffer constructor has the isView static method that returns true if the argument we pass in is a view for an ArrayBuffers.

Only type arrays and DataViews have the [[ViewedArrayBuffer]] internal slot which makes them views.

The ArrayBuffer.prototype.byteLength is an instance method that returns the capacity of the ArrayBuffer in bytes.

It’s a getter method.

ArrayBuffer.prototype.slice(start, end) is an instance method that returns a new ArrayByffer with the index greater than or equal to start and less than end .

start and end can be negative.

Conclusion

Typed arrays can wrap values in various ways.

Also, we’ve to have the correct endianness to store and communicate the data properly.

ArrayBuffers let us store binary data and slice them.

Categories
Modern JavaScript

Best of Modern JavaScript — Typed Arrays and Map/Set Issues

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at issues with maps, sets, and typed arrays.

Why do Maps and Sets have the size Property?

Maps and sets have the size property instead of length because they aren’t sequential data structures, unlike arrays.

length is meant for sequential data structures like arrays.

Configure How Maps and Sets Compare Keys and Values

There’s no way to configure how maps and sets compare their keys and values.

This feature is hard to implement efficiently and properly.

Specify a Default Value when Getting Something out of a Map

There’s no short to specify a default value when we get something out of a map.

Maps don’t let us set the default value directly.

But we can use the || operator to do this since get returns undefined when the entry doesn’t exist.

Therefore, we can write:

const map = new Map();
//...
const prevCount = map.get('foo') || 0;

We get the value with the key 'foo' .

If it’s undefined , we return 0.

Map vs Object

Maps are good for holding keys other than string keys.

Otherwise, objects can give us equivalent functionality.

If we’re mapping arbitrary data, then a map is probably a better choice.

If there’s a fixed set of keys, then objects are a good choice.

If the keys change, then maps are better.

When to use Objects as Map Keys?

We can use objects as map keys if we externally attach data to objects.

But this is better done with WeakMaps since garbage collection happens if we remove the reference to the key object.

Typed Arrays

Typed arrays are a special kind of array that lets us hold binary data.

They let us manipulate image data, canvas elements, process binary files, etc.

Also, we can use them to interact with native APIs like WebGL for graphics manipulation.

There’s no way to manipulate the binary data from theses APIs without a new data type.

2 kinds of objects work together with Typed Arrays.

They include buffers and views.

Buffers are instances of ArrayBuffer and holds binary data.

Views provide methods for accessing binary data.

There’re 2 kinds of views.

One is an instance of the Typed Array constructor such as Uint8Array , Floar64Array , etc.

These work like abnormal arrays, but only allows one type of elements and don’t have holes.

Another is the instance of DataVBiew that lets us access the byte offsets in the buffer.

Handling Overflow and Underflow

When a value is out of range, modular arithmetic is used to convert it to a value in range.

This means that the highest value plus one is converted to the lowest value.

And the lowest value minus one is converted to the highest value.

This is the case with Typed Arrays.

For instance, if we have:

const uint8 = new Uint8Array(1);
uint8[0] = 255;

uint8[0] is 255.

On the other hand, if we have:

const uint8 = new Uint8Array(1);
uint8[0] = 256;

uint8[0] is 0.

If we have:

const uint8 = new Uint8Array(1);
uint8[0] = -1;

uint8[0] is 255.

This applies to all types of Typed Arrays.

Conclusion

Maps and sets have their limitations. They’re also suitable for some applications.

Typed Arrays are used for storing binary data.

Categories
Modern JavaScript

Best of Modern JavaScript — Typed Arrays and its Uses

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at JavaScript typed arrays usage.

Static Typed Array Properties

Typed arrays have the BYTES_PER_ELEMENT property to count how many bytes are needed to store a single element.

The same property is also available in the typed array’s prototype.

Concatenating Typed Arrays

Typed arrays don’t have a concat method like normal arrays.

We can use the set method with a typed array as the first argument and the offset index as the 2nd argument.

For example, we can write:

const arr = Int8Array.of(1, 2, 3);
const arr2 = Int8Array.of(4, 5, 6);

const arrays = [arr, arr2];

let totalLength = 0;
for (const arr of arrays) {
  totalLength += arr.length;
}

const result = new Int8Array(totalLength);

let offset = 0;
for (const arr of arrays) {
  result.set(arr, offset);
  offset += arr.length;
}

console.log(result);

to create a new result typed array with the length being the total length of all the arrays.

Then we can add each typed array’s entry to the result typed array with the set method.

And we update the offet after each array to add the entries.

DataViews

The DataView constructor lets us create a DataView whose data is stored in a given ArrayBuffer.

Its signature is (buffer, byteOffset, byteLength) .

buffer is the buffer object.

byteOffset is the offset of the buffer in bytes,

byteLength is the length the DataView in bytes.

DataView Instance Properties

DataView has the following instance properties.

The DataView.prototype.buffer is a getter than returns the ArrayBuffer for the DataView.

DataView.prototype.length returns how many bytes can be accessed by the DataView.

DataView.prototype.byteOffset returns the offset of which DataView starts accessing the bytes in its buffer.

DataView.prototype.get returns a value from the buffer of the DataView.

It takes the byte offset to start accessing the array as its first argument.

The 2nd argument is whether to set littleEndian to be true or not.

It’s false by default.

DataView.prototype.set lets us writes a value to the buffer of the DataView.

Its signature is (byteOffset, value, littleEndian=false) , byteOffset is the offset to start writing.

value us the value to write.

littleEndian lets us set the endianness of our DataView.

Typed Arrays Usage

Typed aerays are used in various places.

The File API uses it to store file data ready from a file input.

For example, we can get a typed array by writing:

const fileInput = document.getElementById('fileInput');

fileInput.onchange = () => {
  const file = fileInput.files[0];
  const reader = new FileReader();
  reader.readAsArrayBuffer(file);
  reader.onload = function() {
    const arrayBuffer = reader.result;
    //...
  };
}

We get the file object with fileInput.files[0] .

Then we used the FileReader instance’s readAsArrayBuffer function to read the file’s contents into the array buffer.

reader.result has the result.

Fetch API

The Fetch API also lets us get data as an ArrayBuffer.

For instance, we can write:

fetch('https://file-examples-com.github.io/uploads/2017/10/file-sample_150kB.pdf')
  .then(request => request.arrayBuffer())
  .then(arrayBuffer => {
    console.log(arrayBuffer);
  });

to get a PDF files from a URL and convert it to an ArrayBuffer with the arrayBuffer method on the result.

Other APIs that use typed arrays include canvas and WebSockets.

Conclusion

Typed arrays can be manipulated like arrays.

They’re used in many native browser APIs.