Learning ES6: Strings and Destructuring

EcmaScript 6 is one of the newer versions of Javascript that we're now using in a few projects across our teams, and most significantly in our New Design Lab project! As we start to iterate on new features for the lab, we're reviewing and ramping up on the cool shiny features ES6 brings to the table. This week, we learned about some new string methods and destructuring.

New String Methods

With these new string methods, we won't need to rely as much on using crazy regular expressions when trying to parse out strings. This also makes our ES6 code much more readable!

.startsWith()

For this example, let's pretend we are trying to parse through a collection of flight codes and find the ones which are flights on United Airlines.

Let's say we have a group of flight codes: ['UA871', 'UA872', 'ATT314', 'ATT093', 'JBL123']

With the new .startsWith() method, we can easily go through each of the flight codes and see if it starts with 'UA' so we know that it's from United Airlines. It would look something like this:

const flightCodes = ['UA871', 'UA872', 'ATT314', 'ATT093', 'JBL123'];
for(let flight of flightCodes) {
    console.log(flight.startsWith('UA'));
}

This code would print to the console:

true
true
false
false
false

(More on the for(let x of y) loops from ES6 in a later blog post!)

It's important to note that whatever string you pass to the .startsWith() method is case sensitive. That means if we used flight.startsWith('ua') we would get all falses. If you need to be able to find out if a string starts with some substring, regardless of casing, you will still need to use regular expressions.

You can also specify to the .startsWith() method a second parameter: the index that the function considers the 'start' of the string. For example, if I only want to see that the numerical part of the United Airlines flight codes starts with '87', I could pass the index 3 because I know that there will always be the two characters 'UA' before the number.

const unitedAirlines = ['UA871', 'UA872', 'UA123', 'UA324'];
for(let flight of unitedAirlines) {
    console.log(flight.startsWith('87', 3))
}

This prints:

true
true
false
false

.endsWith()

.endsWith() works in a very similar way as .startsWith() , but it checks the end of the string rather than the beginning.

const color1 = 'Light Blue';
const color2 = 'Light Pink';
console.log(color1.endsWith('Blue'));
console.log(color2.endsWith('Blue'));

This would print:

true
false

You can also specify a second parameter to .endsWith() which is the length of the substring you'd like to check the end of. For example, if I want to check that the first 5 characters of my string ends with 'ght', I would have to pass the length 5 like so: color1.endsWith('ght', 5)

Which would return true.

.includes()

.includes() is a pretty straightforward method - it returns true or false if the string includes the substring. Like the above two methods, it is case sensitive, so if you need to be able to know if the string includes the substring regardless of casing, you would still need to use regular expressions or modify the prototype.

const color = 'Light Blue';
color.includes('Blu');

returns true.

.repeat()

.repeat() is also pretty straightforward! It takes one parameter: how many times you would like the string to be repeated, and it returns that string.

const repeatMe = 'Repeat This';
repeatMe.repeat(5);

This returns: "Repeat ThisRepeat ThisRepeat ThisRepeat ThisRepeat This"

One interesting application of the .repeat() method is for a function that can add a uniform amount of left padding to some lines of text, such that they look right-aligned. We can write a function which takes in the string, and the amount of padding that we would like to add.

function leftPad(str, padLength) {
    return `${' '.repeat(padLength - str.length)}${str}`;
}

We subtract the length of the string from our padding so every string will always be right aligned regardless of its length. Then, we can call our leftPad method like so:

console.log(leftPad('Hello', 10);
console.log(leftPad('World', 10);
console.log(leftPad('Yay', 10);

which would output the three strings nicely aligned to the right:

     Hello
     World
       Yay

Destructuring

Destructuring is an interesting new feature of ES6 which lets us write less lines of code when trying to extract data from objects, arrays, maps and sets. This is especially useful when we need to extra lots of data from one object/array/map/set.

Destructuring an Object

Let's say we are dealing with the flights again, but we have some more information about each flight like so:

const flight = {
    airline: 'United Airlines',
    code: 'UA872',
    origin: 'San Fransisco',
    destination: 'Taipei',
    gate: 'D72'
}

Without destructuring, in order to get each piece of data into its own variable, we have to write something like this:

const airline = flight.airline;
const code = flight.code;
const origin = flight.origin;
// etc. etc.

This is a lot of repeated code! With destructuring, we can do this in a much simpler way:

const { airline, code, origin, destination, gate } = flight;

Now, each of these variables will match up with the keys in theflight object, and assign to it the value. So, if we console.log(destination) then we will get "Taipei". We can pick and choose which keys we care about too, so if we left out gate, the rest of the variables would still work.

This can be especially useful for when we have deeply nested objects like in many JSON responses!

But what if we don't want to keep the original key names and still use destructuring? For example, what if we are already using the variable code for something else? Well, we can specify what the new variables should be named.

const { code: flightCode } = flight;

Now, we can console.log(flightCode) and that will return what was in flight.code , or 'UA872'.

What if we want to be able to pick out some specific data from an object, but the key doesn't exist in the object? To avoid a code breaking error, we can set a default value when destructuring.

const { airline, code, plane = 'Boeing 757' } = flight;

Even though there is no flight.plane, we can still use the variable plane which will be holding the default value 'Boeing 757'. If there is a plane key in the flight object, then the variable plane will be holding flight.plane.

We can also combine these two in our destructuring expression for maximum syntactical sugar!

const { airline = 'JetBlue', code: flightCode = 'JBL123', plane = 'Boeing 757' } = flight;
console.log(airline);
console.log(flightCode);
console.log(plane);

This will print out:

United Airlines
UA872
Boeing 757

Destructuring an Array

We can also use destructuring on an array, which assigns the variables based on the index of the value in the array. Let's look at a list of student names:

const students = ['Mary', 'John', 'Sue', 'Miranda']

Instead of assigning our variables like this...

const student1 = students[0];
const student2 = students[1];
// etc.

We can use destructuring! const [student1, student2, student3, student4] = students

Then when we console.log(student4) we get 'Miranda'.

This is especially useful for CSV (comma separated values) files.

Another interesting ES6 feature we can use with array destructuring is the splat operator. For example, if we have an array which has the name of the school as the first value, and the rest of the values are the students, we can write:

const schoolAndStudents = ['University Of Virginia', 'Mary', 'John', 'Sue', 'Miranda'];
const [school, ...students] = schoolAndStudents;

Then console.log(students) will return an array of the rest of the values: ['Mary', 'John', 'Sue', 'Miranda']. Without the ... operator, like this: const [school, students] = schoolAndStudents, console.log(students) would only return the first string, 'Mary'.

Sweet Unlocks: Swapping variables, multiple returns, and named defaults

With destructuring, we can get some sweet unlocks! One cool example was being able to swap variables in one line. Before ES6, in order to swap variables, we would need to use a temporary variable like this:

let var1 = 'A';
let var2 = 'B';
const tmp = var1;
var1 = var2;
var2 = tmp;

But with destructuring, we can do this in one line: [var1, var2] = [var2, var1]

And now, var1 holds 'B' and var2 holds 'A'!

We can also take advantage of destructuring to get multiple returns from a function. Let's say we wrote a function that builds flight codes for different airlines, assuming that the only thing that needs to change is the letter code in the begining of the string.

function flightCode(num) {
    const airlineCode = {
        United: `${UA}${num}`,
        JetBlue: `${JBL}${num}`,
        American: `${AA}${num}`
    }
    return airlineCode;
}

Now, we can easily convert our number into all of the airline codes. But, we still have to specify each of the different airlines to get the specific code for that airline.

const flightCodes = flightCode(872);
const United = flightCodes.United;
const JetBlue = flightCodes.JetBlue;
const American = flightCodes.American;

With destructuring, we can get the value of each of the flight codes into its own variable in one line: const { United, JetBlue, American } = flightCode(872)

Because the function flightCode() returns an object, we can directly set our variables with destructuring.

Another cool unlock we get with destructuring is the ability to use named defaults in our method parameters. Let's take a look at an example that calculates flight costs.

function costOfFlight(distance, basePrice = 5, tax = 0.15) {
    const priceBeforeTax = distance * 0.25 + basePrice;
    return priceBeforeTax + (priceBeforeTax * tax);
}

This function expects that, when it is called, the three parameters will be passed to it in a specific order. distance first, then basePrice, and then tax.

However with destructuring, we can list our variables in any order we'd like and we don't need to include all of them if there are default values. We just need to add in our destructuring syntax!

function costOfFlight({distance = 50, basePrice = 5, tax = 0.15} = {}) {
    const priceBeforeTax = distance * 0.25 + basePrice;
    return priceBeforeTax + (priceBeforeTax * tax);
}

console.log(costOfFlight({basePrice: 1000, distance: 100}));

This returns 1178.75 . Man, flights can be pricy!

One important thing to note is that we do need to set a default value for the parameter object. This is to avoid any errors when calling the method with no parameters - in that case, it will use the default values of each.

Notes and Thoughts

During my team discussion, we agreed that we liked how readable the new String methods were. As someone that's pretty comfortable with Ruby, I liked how these methods reminded me of the same kind of readability that you get in Ruby. However, we did notice that with destructuring, things can get unreadable quickly if we use it excessively. We talked about how there is a fine line between writing less lines of code and readability of the code. It reminded me of the ternary operator for conditions - sometimes it is very useful, like when assigning a variable, but when it gets nested or does more than just assignment, ternaries can really block up your mental model of the code.

Although it IS super cool to use all the fun syntax and feels clever, we need to remember that an important aspect of programming is the code's maintainability, and the ease at which another programmer can read and modify your code! We should be conscious of when and why we are using shorthand syntax. Sometimes, even if there is one or two extra lines of code, it's worth it to make the code more readable.

Another interesting discussion which was brought up was if we should take advantage of Javascript's prototype feature and add new methods to String that are similar to the new ones from ES6. One of the best parts of the new String methods is that our code is more readable, and we don't need to use regular expressions. Should we also modify the String prototype to include new methods for other types of regular expressions? For example, in our code base, we have a file of regular expression constants including a huge one that checks if there is an emoji in your string. Should we create a new method on the String prototype called .hasEmoji() ?

We came to the agreement that modifying a core object like String is scary and unreliable because it exists in the global namespace and we could be overriding something without knowing it. However, we did make the distinction that modifying the String prototype in application code is less risky than doing it when writing a library, since you have more of a guarantee that your application code is the "last" part so you can rely on that function doing what you expect it to do.

Well, I hope this post gave you some food for thought!

Thanks for reading!

by Miranda Wang