ES2015 most useful features

作者 Guanghui Wang 日期 2017-05-19

Declaring Variables in ES6

const

A constant is a variable that cannot be changed. Like other languages had done before it, JavaScript introduced constants with ES6.

Before constants, all we had were variables, and variables could be overwritten:

1
2
3
var pizza = true
pizza = false
console.log(pizza) // false

We cannot reset the value of a constant variable, and it will generate a console error if we try to overwrite the value:

1
2
const pizza = true
pizza = false

let

JavaScript now has lexical variable scoping. In JavaScript, we create code blocks with curly braces ({}). With functions, these curly braces block off the scope of variables. On the other hand, think about if/else statements. If you’re coming from other languages, you might assume that these blocks would also block variable scope. This is not the case.

If a variable is created inside of an if/else block, that variable is not scoped to the block:

1
2
3
4
5
6
7
8
var topic = "JavaScript"

if (topic) {
var topic = "React"
console.log('block', topic) // block React
}

console.log('global', topic) // global React

The topic variable inside the if block resets the value of topic.

With the let keyword, we can scope a variable to any code block. Using let protects the value of the global variable:

1
2
3
4
5
6
7
8
var topic = "JavaScript"

if (topic) {
let topic = "React"
console.log('block', topic) // React
}

console.log('global', topic) // JavaScript

The value of topic is not reset outside of the block.

Another area where curly braces don’t block off a variable’s scope is in for loops:

1
2
3
4
5
6
7
8
9
10
var div, 
container = document.getElementById('container')

for (var i=0; i<5; i++) {
div = document.createElement('div')
div.onclick = function() {
alert('This is box #' + i)
}
container.appendChild(div)
}

In this loop, we create five divs to appear within a container. Each div is assigned an onclick handler that creates an alert box to display the index. Declaring i in the for loop creates a global variable named i, and then iterates it until its value reaches 5. When you click on any of these boxes, the alert says that i is equal to 5 for all divs, because the current value for the global i is 5.

Declaring the loop counter i with let instead of var does block off the scope of i. Now clicking on any box will display the value for i that was scoped to the loop iteration:

1
2
3
4
5
6
7
8
var div, container = document.getElementById('container')
for (let i=0; i<5; i++) {
div = document.createElement('div')
div.onclick = function() {
alert('This is box #: ' + i)
}
container.appendChild(div)
}

Template Strings

Template strings provide us with an alternative to string concatenation. They also allow us to insert variables into a string.

Traditional string concatenation uses plus signs or commas to compose a string using variable values and strings:

1
console.log(lastName + ", " + firstName + " " + middleName)

With a template, we can create one string and insert the variable values by surrounding them with ${ }:

1
console.log(`${lastName}, ${firstName} ${middleName}`)

Any JavaScript that returns a value can be added to a template string between the ${ } in a template string.

Template strings honor whitespace, making it easier to draft up email templates, code examples, or anything else that contains whitespace. Now you can have a string that spans multiple lines without breaking your code.

Example: Template strings honor whitespace
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
`
Hello ${firstName},

Thanks for ordering ${qty} tickets to ${event}.

Order Details
${firstName} ${middleName} ${lastName}
${qty} x $${price} = $${qty*price} to ${event}

You can pick your tickets up at will call 30 minutes before
the show.

Thanks,

${ticketAgent}
`

Previously, using an HTML string directly in our JavaScript code was not so easy to do because we’d need to run it together on one line. Now that the whitespace is recognized as text, you can insert formatted HTML that is easy to understand:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
document.body.innerHTML = `
<section>
<header>
<h1>The HTML5 Blog</h1>
</header>
<article>
<h2>${article.title}</h2>
${article.body}
</article>
<footer>
<p>copyright ${new Date().getYear()} | The HTML5 Blog</p>
</footer>
</section>
`

Notice that we can include variables for the page title and article text as well.

Default Parameters

Languages including C++ and Python allow developers to declare default values for function arguments. Default parameters are included in the ES6 spec, so in the event that a value is not provided for the argument, the default value will be used.

For example, we can set up default strings:

1
2
3
function logActivity(name="Shane McConkey", activity="skiing") {
console.log( `${name} loves ${activity}` )
}

If no arguments are provided to the favoriteActivity function, it will run correctly using the default values. Default arguments can be any type, not just strings:

1
2
3
4
5
6
7
8
9
10
11
var defaultPerson = {
name: {
first: "Shane",
last: "McConkey"
},
favActivity: "skiing"
}

function logActivity(p=defaultPerson) {
console.log(`${p.name.first} loves ${p.favActivity}`)
}

Arrow Functions

Arrow functions are a useful new feature of ES6. With arrow functions, you can create functions without using the function keyword. You also often do not have to use the return keyword.

Example: As a traditional function
1
2
3
4
5
6
var lordify = function(firstname) {
return `${firstname} of Canterbury`
}

console.log( lordify("Dale") ) // Dale of Canterbury
console.log( lordify("Daryle") ) // Daryle of Canterbury

With an arrow function, we can simplify the syntax tremendously.

Example: As an arrow function
1
var lordify = firstname => `${firstname} of Canterbury`

With the arrow, we now have an entire function declaration on one line. The function keyword is removed. We also remove return because the arrow points to what should be returned. Another benefit is that if the function only takes one argument, we can remove the parentheses around the arguments.

More than one argument should be surrounded by parentheses:

1
2
3
4
5
6
7
8
9
10
// Old
var lordify = function(firstName, land) {
return `${firstName} of ${land}`
}

// New
var lordify = (firstName, land) => `${firstName} of ${land}`

console.log( lordify("Dale", "Maryland") ) // Dale of Maryland
console.log( lordify("Daryle", "Culpeper") ) // Daryle of Culpeper

We can keep this as a one-line function because there is only one statement that needs to be returned.

More than one line needs to be surrounded with brackets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Old
var lordify = function(firstName, land) {

if (!firstName) {
throw new Error('A firstName is required to lordify')
}

if (!land) {
throw new Error('A lord must have a land')
}

return `${firstName} of ${land}`
}

// New
var _lordify = (firstName, land) => {

if (!firstName) {
throw new Error('A firstName is required to lordify')
}

if (!land) {
throw new Error('A lord must have a land')
}

return `${firstName} of ${land}`
}

console.log( lordify("Kelly", "Sonoma") ) // Kelly of Sonoma
console.log( lordify("Dave") ) // ! JAVASCRIPT ERROR

These if/else statements are surrounded with brackets but still benefit from the shorter syntax of the arrow function.

Arrow functions do not block this. For example, this becomes something else in the setTimeout callback, not the tahoe object:

1
2
3
4
5
6
7
8
9
10
11
12
var tahoe = {
resorts: ["Kirkwood","Squaw","Alpine","Heavenly","Northstar"],
print: function(delay=1000) {

setTimeout(function() {
console.log(this.resorts.join(","))
}, delay)

}
}

tahoe.print() // Cannot read property 'join' of undefined

This error is thrown because it’s trying to use the .join method on what this is. In this case, it’s the window object. Alternatively, we can use the arrow function syntax to protect the scope of this:

1
2
3
4
5
6
7
8
9
10
11
12
var tahoe = {
resorts: ["Kirkwood","Squaw","Alpine","Heavenly","Northstar"],
print: function(delay=1000) {

setTimeout(() => {
console.log(this.resorts.join(","))
}, delay)

}
}

tahoe.print() // Kirkwood, Squaw, Alpine, Heavenly, Northstar

This works correctly and we can .join the resorts with a comma. Be careful, though, that you’re always keeping scope in mind. Arrow functions do not block off the scope of this

1
2
3
4
5
6
7
8
9
10
11
12
var tahoe = {
resorts: ["Kirkwood","Squaw","Alpine","Heavenly","Northstar"],
print: (delay=1000) => {

setTimeout(() => {
console.log(this.resorts.join(","))
}, delay)

}
}

tahoe.print() // Cannot read property resorts of undefined

Changing the print function to an arrow function means that this is actually the window.

To verify, let’s change the console message to evaluate whether this is the window:

1
2
3
4
5
6
7
8
9
10
11
12
var tahoe = {
resorts: ["Kirkwood","Squaw","Alpine","Heavenly","Northstar"],
print: (delay=1000)=> {

setTimeout(() => {
console.log(this === window)
}, delay)

}
}

tahoe.print()

It evaluates as true. To fix this, we can use a regular function:

1
2
3
4
5
6
7
8
9
10
11
12
var tahoe = {
resorts: ["Kirkwood","Squaw","Alpine","Heavenly","Northstar"],
print: function(delay=1000) {

setTimeout(() => {
console.log(this === window)
}, delay)

}
}

tahoe.print() // false

Transpiling ES6

Not all web browsers support ES6, and even those that do don’t support everything. The only way to be sure that your ES6 code will work is to convert it to ES5 code before running it in the browser. This process is called transpiling. One of the most popular tools for transpiling is Babel.

In the past, the only way to use the latest JavaScript features was to wait weeks, months, or even years until browsers supported them. Now, transpiling has made it possible to use the latest features of JavaScript right away. The transpiling step makes JavaScript similar to other languages. Transpiling is not compiling: our code isn’t compiled to binary. Instead, it’s transpiled into syntax that can be interpreted by a wider range of browsers. Also, JavaScript now has source code, meaning that there will be some files that belong to your project that don’t run in the browser.

Below shows some ES6 code. We have an arrow function, already covered, mixed with some default arguments for x and y.

Example: ES6 code before Babel transpiling
1
const add = (x=5, y=10) => console.log(x+y);

After we run the transpiler on this code, here is what the output will look like:

1
2
3
4
5
6
7
8
9
"use strict";

var add = function add() {
var x = arguments.length <= 0 || arguments[0] === undefined ?
5 : arguments[0];
var y = arguments.length <= 1 || arguments[1] === undefined ?
10 : arguments[1];
return console.log(x + y);
};

The transpiler added a “use strict” declaration to run in strict mode. The variables x and y are defaulted using the arguments array, a technique you may be familiar with. The resulting JavaScript is more widely supported.

You can transpile JavaScript directly in the browser using the inline Babel transpiler. You just include the browser.js file, and any scripts with type="text/babel" will be converted (even though Babel 6 is the current version of Babel, only the CDN for Babel 5 will work):

1
2
3
4
5
<script 
src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.js">
</script>
<script src="script.js" type="text/babel">
</script>

TRANSPILING IN THE BROWSER

This approach means that the browser does the transpiling at runtime. This is not a good idea for production because it will slow your application down a lot. For now, the CDN link will allow us to discover and use ES6 features.

You may be thinking to yourself: “Great! When ES6 is supported by all browsers, we won’t have to use Babel anymore!” However, by the time this happens, we will want to use features of the next version of the spec. Unless a tectonic shift occurs, we’ll likely be using Babel in the foreseeable future.

ES6 Objects and Arrays

ES6 gives us new ways for working with objects and arrays and for scoping the variables within these datasets. These features include destructuring, object literal enhancement, and the spread operator.

Destructuring Assignment

The destructuring assignment allows you to locally scope fields within an object and to declare which values will be used.

Consider this sandwich object. It has four keys, but we only want to use the values of two. We can scope bread and meat to be used locally:

1
2
3
4
5
6
7
8
9
10
var sandwich =  {
bread: "dutch crunch",
meat: "tuna",
cheese: "swiss",
toppings: ["lettuce", "tomato", "mustard"]
}

var {bread, meat} = sandwich

console.log(bread, meat) // dutch crunch tuna

The code pulls bread and meat out of the object and creates local variables for them. Also, the bread and meat variables can be changed:

1
2
3
4
5
6
7
8
9
var {bread, meat} = sandwich

bread = "garlic"
meat = "turkey"

console.log(bread) // garlic
console.log(meat) // turkey

console.log(sandwich.bread, sandwich.meat) // dutch crunch tuna

We can also destructure incoming function arguments. Consider this function that would log a person’s name as a lord:

1
2
3
4
5
6
7
8
9
10
var lordify = regularPerson => {
console.log(`${regularPerson.firstname} of Canterbury`)
}

var regularPerson = {
firstname: "Bill",
lastname: "Wilson"
}

lordify(regularPerson) // Bill of Canterbury

Instead of using dot notation syntax to dig into objects, we can destructure the values that we need out of regularPerson:

1
2
3
4
5
var lordify = ({firstname}) => {
console.log(`${firstname} of Canterbury`)
}

lordify(regularPerson) // Bill of Canterbury

Destructuring is also more declarative, meaning that our code is more descriptive about what we are trying to accomplish. By destructuring firstname, we declare that we will only use the firstname variable. We’ll cover more on declarative programming in the next chapter.

Values can also be destructured from arrays. Imagine that we wanted to assign the first value of an array to a variable name:

1
2
3
var [firstResort] = ["Kirkwood", "Squaw", "Alpine"]

console.log(firstResort) // Kirkwood

We can also pass over unnecessary values with list matching using commas. List matching occurs when commas take the place of elements that should be skipped. With the same array, we can access the last value by replacing the first two values with commas:

1
2
3
var [,,thirdResort] = ["Kirkwood", "Squaw", "Alpine"]

console.log(thirdResort) // Alpine

Later in this section, we’ll take this example a step further by combining array destructuring and the spread operator.

Object Literal Enhancement

Object literal enhancement is the opposite of destructuring. It is the process of restructuring or putting back together. With object literal enhancement, we can grab variables from the global scope and turn them into an object:

1
2
3
4
5
6
var name = "Tallac"
var elevation = 9738

var funHike = {name,elevation}

console.log(funHike) // {name: "Tallac", elevation: 9738}

name and elevation are now keys of the funHike object.

We can also create object methods with object literal enhancement or restructuring:

1
2
3
4
5
6
7
8
9
var name = "Tallac"
var elevation = 9738
var print = function() {
console.log(`Mt. ${this.name} is ${this.elevation} feet tall`)
}

var funHike = {name,elevation,print}

funHike.print() // Mt. Tallac is 9738 feet tall

Notice we use this to access the object keys.

When defining object methods, it is no longer necessary to use the function keyword.

Example: Old versus new: Object syntax
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// OLD
var skier = {
name: name,
sound: sound,
powderYell: function() {
var yell = this.sound.toUpperCase()
console.log(`${yell} ${yell} ${yell}!!!`)
},
speed: function(mph) {
this.speed = mph
console.log('speed:', mph)
}
}

// NEW
const skier = {
name,
sound,
powderYell() {
let yell = this.sound.toUpperCase()
console.log(`${yell} ${yell} ${yell}!!!`)
},
speed(mph) {
this.speed = mph
console.log('speed:', mph)
}
}

Object literal enhancement allows us to pull global variables into objects and reduces typing by making the function keyword unnecessary.

The Spread Operator

The spread operator is three dots (...) that perform several different tasks. First, the spread operator allows us to combine the contents of arrays. For example, if we had two arrays, we could make a third array that combines the two arrays into one:

1
2
3
4
5
var peaks = ["Tallac", "Ralston", "Rose"]
var canyons = ["Ward", "Blackwood"]
var tahoe = [...peaks, ...canyons]

console.log(tahoe.join(', ')) // Tallac, Ralston, Rose, Ward, Blackwood

All of the items from peaks and canyons are pushed into a new array called tahoe.

Let’s take a look at how the spread operator can help us deal with a problem. Using the peaks array from the previous sample, let’s imagine that we wanted to grab the last item from the array rather than the first. We could use the Array.reverse method to reverse the array in combination with array destructuring:

1
2
3
4
5
var peaks = ["Tallac", "Ralston", "Rose"]
var [last] = peaks.reverse()

console.log(last) // Rose
console.log(peaks.join(', ')) // Rose, Ralston, Tallac

See what happened? The reverse function has actually altered or mutated the array. In a world with the spread operator, we don’t have to mutate the original array; we can create a copy of the array and then reverse it:

1
2
3
4
5
var peaks = ["Tallac", "Ralston", "Rose"]
var [last] = [...peaks].reverse()

console.log(last) // Rose
console.log(peaks.join(', ')) // Tallac, Ralston, Rose

Since we used the spread operator to copy the array, the peaks array is still intact and can be used later in its original form.

The spread operator can also be used to get some, or the rest, of the items in the array:

1
2
3
4
5
var lakes = ["Donner", "Marlette", "Fallen Leaf", "Cascade"]

var [first, ...rest] = lakes

console.log(rest.join(", ")) // "Marlette, Fallen Leaf, Cascade"

We can also use the spread operator to collect function arguments as an array. Here, we build a function that takes in n number of arguments using the spread operator, and then uses those arguments to print some console messages:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function directions(...args) {
var [start, ...remaining] = args
var [finish, ...stops] = remaining.reverse()

console.log(`drive through ${args.length} towns`)
console.log(`start in ${start}`)
console.log(`the destination is ${finish}`)
console.log(`stopping ${stops.length} times in between`)
}

directions(
"Truckee",
"Tahoe City",
"Sunnyside",
"Homewood",
"Tahoma"
)

The directions function takes in the arguments using the spread operator. The first argument is assigned to the start variable. The last argument is assigned to a finish variable using Array.reverse. We then use the length of the arguments array to display how many towns we’re going through. The number of stops is the length of the arguments array minus the finish stop. This provides incredible flexibility because we could use the directions function to handle any number of stops.

The spread operator can also be used for objects. Using the spread operator with objects is similar to using it with arrays. In this example, we’ll use it the same way we combined two arrays into a third array, but instead of arrays, we’ll use objects:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var morning = {
breakfast: "oatmeal",
lunch: "peanut butter and jelly"
}

var dinner = "mac and cheese"

var backpackingMeals = {
...morning,
dinner
}

console.log(backpackingMeals) // {breakfast: "oatmeal",
lunch: "peanut butter and jelly",
dinner: "mac and cheese"}

Promises

Promises give us a way to make sense out of asynchronous behavior. When making an asynchronous request, one of two things can happen: everything goes as we hope or there’s an error. There may be several different types of successful or unsuccessful requests. For example, we could try several ways to obtain the data to reach success. We could also receive multiple types of errors. Promises give us a way to simplify back to a simple pass or fail.

Let’s create an asynchronous promise for loading data from the randomuser.me API. This API has information like email address, name, phone number, location, and so on for fake members and is great to use as dummy data.

The getFakeMembers function returns a new promise. The promise makes a request to the API. If the promise is successful, the data will load. If the promise is unsuccessful, an error will occur:

1
2
3
4
5
6
7
8
9
10
11
const getFakeMembers = count => new Promise((resolves, rejects) => {
const api = `https://api.randomuser.me/?nat=US&results=${count}`
const request = new XMLHttpRequest()
request.open('GET', api)
request.onload = () =>
(request.status === 200) ?
resolves(JSON.parse(request.response).results) :
reject(Error(request.statusText))
request.onerror = (err) => rejects(err)
request.send()
})

With that, the promise has been created, but it hasn’t been used yet. We can use the promise by calling the getFakeMembers function and passing in the number of members that should be loaded. The then function can be chained on to do something once the promise has been fulfilled. This is called composition. We’ll also use an additional callback that handles errors:

1
2
3
4
5
getFakeMembers(5).then(
members => console.log(members),
err => console.error(
new Error("cannot load members from randomuser.me"))
)

Promises make dealing with asynchronous requests easier, which is good, because we have to deal with a lot of asynchronous data in JavaScript. You’ll also see promises used heavily in Node.js, so a solid understanding of promises is essential for the modern JavaScript engineer.

Classes

Previously in JavaScript, there were no official classes. Types were defined by functions. We had to create a function and then define methods on the function object using the prototype:

1
2
3
4
5
6
7
8
9
10
11
12
function Vacation(destination, length) {
this.destination = destination
this.length = length
}

Vacation.prototype.print = function() {
console.log(this.destination + " | " + this.length + " days")
}

var maui = new Vacation("Maui", 7);

maui.print(); // Maui | 7

If you were used to classical object orientation, this probably made you mad.

ES6 introduces class declaration, but JavaScript still works the same way. Functions are objects, and inheritance is handled through the prototype, but this syntax makes more sense if you come from classical object orientation:

1
2
3
4
5
6
7
8
9
10
11
12
class Vacation {

constructor(destination, length) {
this.destination = destination
this.length = length
}

print() {
console.log(`${this.destination} will take ${this.length} days.`)
}

}

CAPITALIZATION CONVENTIONS

The rule of thumb with capitalization is that all types should be capitalized. Due to that, we will capitalize all class names.

Once you’ve created the class, you can create a new instance of the class using the new keyword. Then you can call the custom method on the class:

1
2
3
const trip = new Vacation("Santiago, Chile", 7);

console.log(trip.print()); // Chile will take 7 days.

Now that a class object has been created, you can use it as many times as you’d like to create new vacation instances. Classes can also be extended. When a class is extended, the subclass inherits the properties and methods of the superclass. These properties and methods can be manipulated from here, but as a default, all will be inherited.

You can use Vacation as an abstract class to create different types of vacations. For instance, an Expedition can extend the Vacation class to include gear:

1
2
3
4
5
6
7
8
9
10
11
12
class Expedition extends Vacation {

constructor(destination, length, gear) {
super(destination, length)
this.gear = gear
}

print() {
super.print()
console.log(`Bring your ${this.gear.join(" and your ")}`)
}
}

That’s simple inheritance: the subclass inherits the properties of the superclass. By calling the printDetails method of Vacation, we can append some new content onto what is printed in the printDetails method of Expedition. Creating a new instance works the exact same way—create a variable and use the new keyword:

1
2
3
4
5
6
7
const trip = new Expedition("Mt. Whitney", 3, 
["sunglasses", "prayer flags", "camera"])

trip.print()

// Mt. Whitney will take 3 days.
// Bring your sunglasses and your prayer flags and your camera

CLASSES AND PROTOTYPAL INHERITANCE

Using a class still means that you are using JavaScript’s prototypal inheritance. Log Vacation.prototype, and you’ll notice the constructor and printDetails methods on the prototype.

We will use classes a bit in this book, but we’re focusing on the functional paradigm. Classes have other features, like getters, setters, and static methods, but this book favors functional techniques over object-oriented techniques. The reason we’re introducing these is because we’ll use them later when creating React components.

ES6 Modules

A JavaScript module is a piece of reusable code that can easily be incorporated into other JavaScript files. Until recently, the only way to work with modular JavaScript was to incorporate a library that could handle importing and exporting modules. Now, with ES6, JavaScript itself supports modules.

JavaScript modules are stored in separate files, one file per module. There are two options when creating and exporting a module: you can export multiple JavaScript objects from a single module, or one JavaScript object per module.

In text-helpers.js, the module and two functions are exported.

Example: ./text-helpers.js
1
2
3
4
export const print(message) => log(message, new Date())

export const log(message, timestamp) =>
console.log(`${timestamp.toString()}: ${message}`}

export can be used to export any JavaScript type that will be consumed in another module. In this example the print function and log function are being exported. Any other variables declared in text-helpers.js will be local to that module.

Sometimes you may want to export only one variable from a module. In these cases you can use export default.

Example: ./mt-freel.js
1
2
3
const freel = new Expedition("Mt. Freel", 2, ["water", "snack"])

export default freel

export default can be used in place of export when you wish to export only one type. Again, both export and export default can be used on any JavaScript type: primitives, objects, arrays, and functions.

Modules can be consumed in other JavaScript files using the import statement. Modules with multiple exports can take advantage of object destructuring. Modules that use export default are imported into a single variable:

1
2
3
4
5
6
7
import { print, log } from './text-helpers'
import freel from './mt-freel'

print('printing a message')
log('logging a message')

freel.print()

You can scope module variables locally under different variable names:

1
2
3
4
import { print as p, log as l } from './text-helpers'

p('printing a message')
l('logging a message')

You can also import everything into a single variable using *:

1
import * as fns from './text-helpers

ES6 modules are not yet fully supported by all browsers. Babel does support ES6 modules, so we will be using them throughout this book.

CommonJS

CommonJS is the module pattern that is supported by all versions of Node.js. You can still use these modules with Babel and webpack. With CommonJS, JavaScript objects are exported using module.exports.

Example: ./txt-helpers.js
1
2
3
4
5
6
const print(message) => log(message, new Date())

const log(message, timestamp) =>
console.log(`${timestamp.toString()}: ${message}`}

module.exports = {print, log}

CommonJS does not support an import statement. Instead, modules are imported with the require function:

1
const { log, print } = require('./txt-helpers')