Categories
Modern JavaScript

Best of Modern JavaScript — Array Holes and Operations

Since 2015, JavaScript has improved immensely.

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

In this article, we’ll look at some array operations with holes.

Array Operations and Holes

Various operations treat array holes differently.

Iteration is done with the Array.prototype[Symbol.iterator] call.

It treats holes as if the entry is undefined .

We can see this if we call Symbol.iterator directly.

For instance, we can write:

const arr = [, 'foo'];
const iter = arr[Symbol.iterator]();

console.log(iter.next())
console.log(iter.next())

We return the iterator from to let us get the elements of the array sequentially with an iterator.

If we call next the first time, we get:

{ value: undefined, done: false }

And if we call next again, we get:

{ value: "foo", done: false }

As we can see, the value is undefined is returned from the first next call.

The spread operator also treats holes the same way.

For instance, we can write:

const arr = [, 'foo'];
console.log([...arr]);

After we spread the arr array, we can see the logged value is:

[undefined, "foo"]

The for-of loop also treats holes the same way.

For instance, if we write:

const arr = [, 'foo'];

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

then we get undefined and 'foo' .

Array.from() uses iteration to convert an iterable object to an array.

This works exactly like the spread operator.

For instance, if we have:

const arr = [, 'foo'];

console.log(Array.from(arr));

Then we get:

[undefined, "foo"]

If we pass in an array-like object that isn’t iterable into the Array.from method, the missing entry is still treated as undefined .

If we have:

const arr = Array.from({
  1: 'foo',
  length: 2
})

then we get the same result.

With a second argument, Array.from works like Array.prototype.map .

For example, if we write:

const arr = Array.from([, 'foo'], x => x)

console.log(arr);

Then arr is [undefined, “foo”] .

We can also use it to get the index. To do that, we write:

const arr = Array.from([, 'foo'], (x, i) => i)

then arr is [0, 1] .

Array.prototype.map skip holes, but preserve them.

For instance, if we have:

const arr = [, 'foo'].map(x => x)

console.log(arr);

Then we get:

[empty, "foo"]

The hole stays as an empty slot after mapping.

It acts the same way if we map an array with holes to an array of indexes.

If we write:

const arr = [, 'foo'].map((x, i) => i)

we get [empty, 1] .

The behavior of array holes is treated differently by different array instance methods.

forEacg , filter , and some ignore holes.

map skips but preserves holes.

join and toString treat holes like they’re undefined ,

null and undefined are treated as empty strings.

copyWithin creates a hole when copying holes.

entries , keys , values, find, and findIndex treat each hole as if they’re undefined .

fill doesn’t care whether there are holes or not.

concat , map , push , reverse , slice , sort , splice and unshift all preserve holes.

pop and shift treat them as elements.

Conclusion

Different operators and methods treat array holes differently.

Categories
Modern JavaScript

Best of Modern JavaScript — Array.from and Getting Items

Since 2015, JavaScript has improved immensely.

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

In this article, we’ll look at the Array.from method and some instance methods.

Array.from and Map

Array.from takes a second argument that takes a function that maps the entry to the values we want.

For example, we can write:

const divs = document.querySelectorAll('div');
const texts = Array.from(divs, d => d.textContent);

We get the div elements with querySelectorAll method.

Then we call Array.from with the NodeList with the divs as the first argument.

The 2nd argument is a callback to let us map a value.

This is shorter than writing:

const divs = document.querySelectorAll('div');
const texts = Array.prototype.map.call(divs, s => s.textContent);

The from method is static, so it’ll be inherited if we create a subclass of an array.

For instance, we can write;

const obj = {
  length: 1,
  0: 'foo'
};

class FooArray extends Array {
  //...
}
const arr = FooArray.from(obj);

We can pass in an array-like object to FooArray.from the way we do with any array.

The mapping functionality also works.

So we can write:

class FooArray extends Array {
  //...
}

const divs = document.querySelectorAll('div');
const arr = FooArray.from(divs, d => d.textContent);

We called FooArray.from like we do with Array.from and get the same result.

Array.of

Array.of is another method of an array.

It takes a list of arguments to let us create an array.

This is an alternative to the Array constructor.

Instead of writing:

const arr = new Array(1, 2, 3);

We can write:

const arr = Array.of(1, 2, 3);

Array.of is better since it returns an array with the arguments even if there’s only one argument.

This isn’t the case with the Array constructor.

If we pass one argument, then it’ll try to create an array with the given length.

This also works with subclasses of Array.

For instance, we can write:

class FooArray extends Array {
  //...
}

const arr = FooArray.of(1, 2, 3);

Then we can check if an arr is an instance of FooArray by writing:

console.log(arr instanceof FooArray);
console.log(arr.length === 3);

Array.prototype Methods

There are also new Array.prototype methods added with ES6.

They include the Array.prototype.entries() , Array.prototype.keys() , and Array.prototype.entries() .

Array.prototype.entries() returns an array with arrays of index and element as entries.

For example, we can write:

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

for (const [index, element] of arr.entries()) {
  console.log(index, element);
}

index will have the index of each entry and element has the element for each index.

Array.prototype.keys() have the index of the array.

For instance, we can write:

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

for (const index of arr.keys()) {
  console.log(index);
}

Then we get the index value for each entry.

Array.prototype.values returns an array of values.

So we can write:

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

for (const element of arr.values()) {
  console.log(element);
}

Searching for Array Elements

We can search for array elements with the Array.prototype.find method.

It takes the predicate which is a callback that returns the condition we’re looking for.

The 2nd argument is a value for this we use in our callback.

It returns the first item that’s found.

If nothing’s found, then undefined is returned.

For example, we can write:

const num = [2, -1, 6].find(x => x < 0)

then num is -1.

If we write:

const num = [2, 1, 6].find(x => x < 0)

then num is undefined .

Conclusion

Array.from can be used to map items the way we want.

Also, we can get indexes and elements and find elements in various ways.

Categories
Modern JavaScript

Best of Modern JavaScript — Array Fill, Concat, and Spread

Since 2015, JavaScript has improved immensely.

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

In this article, we’ll look at various array operations.

Creating Arrays Filled with Values

We can create an array filled with values with the Array.prototype.fill method.

It replaces all array elements including holes with a fixed value.

For instance, we can write:

const arr = new Array(3).fill('foo');

Then arr is [“foo”, “foo”, “foo”] .

new Array(3) creates an array with 3 holes and fill replaces each hole with 'foo' .

We can fill an array with ascending numbers by calling the keys method with an empty array.

For example, we can write:

const arr = [...new Array(10).keys()]

Then arr is [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] .

We can also fill an array with some computed value.

To do that, we can use the Array.from method.

For instance, we write:

const arr = Array.from(new Array(10), (x, i) => i ** 2)

then we return an array with the first 10 square numbers.

So arr is [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] .

To fill an array with onlyundefined , we can spread an array created with the Array constructor.

For example, we can write:

const arr = [...new Array(10)];

Then arr is:

[undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

Configuring Which Objects are Spread by concat()

We can configure which objects are spreadable by the Array.prototype.concat method.

To do this, we can override the Symbol.isConcatSpreadable value.

By default, the Array.prototype.concat method spread an array into the array it’s called on.

For instance, we can use it by writing:

const arr = [3, 4, 5];
const merged = [1, 2].concat(arr, 6);

Then merged is [1, 2, 3, 4, 5, 6] .

We pass in an array or a value into the method and it’ll be spread into the array it’s called on and returned.

To change how concatenation is done, we can define our own Symbol.isConcatSpreadable value to let change this behavior.

We can write:

const arr = [3, 4, 5];
arr[Symbol.isConcatSpreadable] = false;

const merged = [1, 2].concat(arr, 6);

Then merged is:

[
  1,
  2,
  [
    3,
    4,
    5
  ],
  6
]

No Spreading for Non-Arrays

Non-array values won’t be spread into the array that concat is called with.

However, we can change this behavior with the Symbol.isConcatSoreadabke value.

For instance, we can spread an array-like object into the array that we return with concat by writing:

const arrayLike = {
  length: 2,
  0: 'c',
  1: 'd'
};
arrayLike[Symbol.isConcatSpreadable] = true;

const merged = Array.prototype.concat.call(['a', 'b'], arrayLike, 'e', 'f');

We set arrayLike[Symbol.isConcatSpreadable] to true so that we can spread the properties with integer indexes into the array returned by Array.prototype.concat .

Therefore, we get that value of merged is:

[
  "a",
  "b",
  "c",
  "d",
  "e",
  "f"
]

Detecting Arrays

Array.prototype.concat detects an array the same way as Array.isArray .

It doesn’t matter whether Array.prototype is in the prototype chain.

This ensures that various hacks that were used to create Array subclasses still work with the array check.

Conclusion

We can fill and combine arrays to manipulate arrays.

The Symbol.isConcatSpreadable property lets us set if an array is spreadable into another array.

Categories
Modern JavaScript

Best of Modern JavaScript — Well-Known Symbols

Since 2015, JavaScript has improved immensely.

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

In this article, we’ll look at new OOP features in JavaScript.

How to use __proto__?

To get the prototype of an object, we use Object.getPrototypeOf .

To create an object with a given prototype, we can use Object.create .

Object.setPrototypeOf is deprecate and it prevents optimizations in many browsers.

__proto__ can be used to get and set the prototype of an object.

Enumerability

The for-in loop traverses the string keys of an own and inherited enumerable properties.

Object.keys returns the string keys of an enumerable own properties.

JSON.stringify only stringified enumerable own properties and string keys.

In ES6, Object.assign only copies enumerable own string and symbol properties.

There’re many non-enumerable properties in a JavaScript object.

All prototype properties of built-in classes are non-enumerable.

For instance, if we have:

const desc = Object.getOwnPropertyDescriptor.bind(Array);
console.log(desc(Object.prototype, 'toString').enumerable)

We get the property descriptors in the Array.prototype.toString method.

And we get the enumerable property, and it’ll log false .

Marking Properties as not to be Copied

We can mark a property not to be copied if we mark the property as being enumerable.

For instance, we can write:

const obj = Object.defineProperty({}, 'foo', {
  value: 'bar',
  enumerable: false
});

We set the enumerable property to false so that it won’t be picked up by Object.assign or the spread operator.

**Object.assign()**

Object.assign can be used to merge object sources into the target.

All own enumerable properties from the sources will be copied to the target.

It doesn’t consider inherited properties.

Hiding Own Properties from JSON.stringify()

Hiding own properties from JSON.stringify can be done by making properties non-enumerable.

This will make JSON.stringify skip the properties.

We can also specify the toJSON method to return the object we want to stringify.

For example, we can write:

const obj = {
  foo: 'bar',
  toJSON() {
    return {
      bar: 'baz'
    };
  },
};

console.log(JSON.stringify(obj))

Then the console log will log {“bar”:”baz”} .

Customizing Basic Language Operations via Well-Known Symbols

We can customize basic language operations with well-known symbols.

The Symbol.hasInstance method lets an object customize the behavior of the instanceof operator.

Symbol.toPrimitive is a method that lets us customize how it’s converted to a primitive value.

The Symbol.toStringTag method lets us call Object.prototype.toString to compute the string description of an object.

Symbol.unscopables lets us hide some properties from the with statement.

obj instanceof C works by doing some checks.

If C isn’t an object, then it throws a TypeError .

If the method exists, then it calls C[Symbol.hasInstance](obj) .

It coerces the result to boolean and returns it.

Otherwise, it computes and returns the result according to the regular algorithm by checking for scalability, C.prototype in the prototype chain of obj , etc.

The only method in the standard library that has Symbol.hasInstance is Function.prototype .

We can check whether a value in an object by writing:

const ObjType = {
  [Symbol.hasInstance](value) {
    return (value !== null &&
      (typeof value === 'object' ||
        typeof value === 'function'));
  }
};

We create our own ObjType object with the Symbol.hasInstance method.

It takes the value we want to check.

Then it checks whether it’s not bull , the type of value is 'object' or 'function' .

Then we can use it by writing:

{} instanceof ObjType

which returns true .

Conclusion

Enumerabnility of objects can be changed and checked.

Also, we can change the behavior or instanceof and other operators by overriding methods with well-known symbols.

Categories
Modern JavaScript

Best of Modern JavaScript — Symbols and Base Classes

Since 2015, JavaScript has improved immensely.

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

In this article, we’ll look at new OOP features in JavaScript.

Symbol.toPrimitive Method

The Symbol.toPrimitive lets an object customize how it’s converted to a pritmive value.

Many JavaScript operators coverts operands.

For instance, the multiplication operator converts operands to numbers.

The Date constructor converts parameters to numbers.

parseInt also does the same conversion.

The most common kind of values is converted to boolean, number, string, or object.

Numbers and string conversions are converted by the ToPrimitive operation.

There’re other methods used for coercion.

They include the obj.valueOf method to convert it primitive wrapper object to a primitive value.

To convert strings, the obj.toString method is returned if it’s primitive.

valueOf is called as an alternative.

The default mode is to convert to a number.

Date.prototype[Symbol.toPrimitive] deviates from the default algorithm for conversion.

It converts the Date instance to a timestamp.

For instance, we can override the Symbol.toPrimitive method by writing:

const obj = {
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return 100;
      case 'string':
        return 'foo';
      case 'default':
        return 'default';
      default:
        throw new Error();
    }
  }
};

The hint has the string with the mode for conversion.

And we can return what we want based on that.

Symbol.toStringTag

The Symbol.toStringTag is a string-valued property that’s used to create the default string description of an object.

It’s used internally by the Object.prototype.toString method.

For instance, we can create our own description by writing:

class Foo {
  get[Symbol.toStringTag]() {
    return 'bar';
  }
}

Then when we call:

console.log(Object.prototype.toString.call(new Foo()));

Then we get:

'[object bar]'

logged.

The default return values for Symbol.toStringTag for various kinds of objects are the following:

  • undefined' — Undefined'
  • null — 'Null'
  • array — 'Array'
  • string — 'String'
  • arguments — 'Arguments'
  • something callable — 'Function'
  • error object — 'Error'
  • boolean object — 'Boolean'
  • number object — 'Number'
  • date object — 'Date'
  • regular expression object — 'RegExp'
  • everything else — 'Object'

Overriding the Default toString Tag

We can override the default toString tag by overriding the Symbo.toStringTag method with our own method.

Built-in classes also have their own string tags.

Objects like JSON , Math , ArrayBuffer , DataView , Map , Promise , Set , TypedArray , WeakMap , WeakSet , etc. all have their own string tags.

The Symbol.toStringTag methods are all non-writable, non-enumerable, but it’s configurable.

Symbol.unscopables

Symbol.unscopables lets an object hide some properties from the with statement.

It’s only used by the Array.prototype in the standard library.

We shouldn’t use the with statement, so we don’t have to worry about this.

Base Classes

Class syntax is introduced with ES6 to let us create constructors easier.

For instance, we can write:

class Person {
  constructor(name) {
    this.name = name;
  }

  toString() {
    return `(${this.name})`;
  }
}

We can then create the object from it by writing:

const person = new Person('james');

And we can call the toString method by writing:

person.toString()

and get 'james' .

If we check the type of the class with typeof :

typeof Person

We get 'function' .

However, if we try to call it as a function, we can write:

Person()

We’ll get the error ‘TypeError: Classes can’t be function-called’.

Conclusion

We can override common well-known symbols to change the behavior of objects.

Also, we can use the class syntax to create constructors.