JavaScript nuggets

Helpful JavaScript code snippets.

Related: Angular nuggets

Remove comments

A well-named function replaces comments

1
2
3
4
5
6
7
8
9
authService.checkLogin(null, null, function() {
// there was an error, default to "anonymous"
$scope.authState = "anonymous";
});
// becomes
function onLoginError() {
$scope.authState = "anonymous";
}
authService.checkLogin(null, null, onLoginError);

Use comments in your json files (really)

This is valid JSON syntax

1
2
3
4
5
6
7
{
"name": "short project name",
"name": "app-server",

"engine": "minimum nodejs version",
"engine": "0.8"
}

Refactor conditions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ($scope.selectedProduct &&
$scope.selectedProduct.id !== selectedProductId) {
$location.path("/products/" +
$scope.selectedProduct.id + "/details");
}
// becomes
function selectedNewProduct() {
return $scope.selectedProduct &&
$scope.selectedProduct.id !== selectedProductId;
}
if (selectedNewProduct()) {
$location.path("/products/" +
$scope.selectedProduct.id + "/details");
}

Avoid exceptions using short circuit expressions

If we have a complex condition, for example if an argument is an object and has a certain property, the condition can cause an exception if an object is undefined.

1
2
3
4
5
function isValid(person) {
return person && person.name
}
isValid()
// false

JavaScript uses short circuit evaluation for expressions in the conditions to avoid evaluating expressions if the entire value is known, which is often the case with Boolean expressions. In the above code, only the left hand of the logical AND is evaluated. Since it is false (there is no person) the condition returns without evaluating person.name.

This often fails if we change the condition to OR. Then all parts might be evaluated, until at least one is true.

1
2
3
4
5
function isValid(person) {
return person && person.name || person.age
}
isValid()
// TypeError: Cannot read property 'age' of undefined

To properly divide the condition we need to make the entire OR into its own expression. Then it will be evaluated only if the left hand of the AND condition passes.

1
2
3
4
5
function isValid(person) {
return person && (person.name || person.age)
}
isValid()
// false

In the above code the value of (person.name || person.age) will not be computed if the person is false because the short circuit principle stops it.

Cast result to Boolean

Avoid undefined values by casting the falsy values to real Booleans. This will help when converting objects to JSON for example.

1
2
3
4
5
6
7
8
function isValid(person) {
return person && (person.name || person.age)
}
var status = {
hasPerson: isValid()
}
JSON.stringify(status)
// '{}''

Notice the empty object - the JSON.stringify does NOT output properties with undefined values by default. You can use either a custom serializer function or ensure that the function isValid returns a true boolean.

1
2
3
4
5
6
7
8
9
10
11
12
13
function bool(fn) {
return function () {
return Boolean(fn.apply(this, arguments))
}
}
const isValid = bool(function (person) {
return person && (person.name || person.age)
})
var status = {
hasPerson: isValid()
}
JSON.stringify(status)
// '{"hasPerson":false}'

The output now has the desired property.

Name functions consistently

Helps with debugging the stack trace

1
2
3
4
5
issues.config(function config() {});
controller('DashboardCtrl', function HomeController($scope) {});
// becomes
issues.config(function issuesConfig() {});
controller('DashboardCtrl', function DashboardCtrl($scope) {});

Visual code patterns

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$scope.archive = function (issue) {
if (!issue || !issue.productId || !issue.type || !issue.id) {
return;
}
// ...
};
// becomes
$scope.archive = function (issue) {
if (!issue ||
!issue.productId ||
!issue.type ||
!issue.id) {
return;
}
// ...
};

Avoid required default arguments

1
2
3
authService.checkLogin(null, null, onLoginError);
// becomes
authService.checkLogin(onLoginError);

Adjust checkLogin if necessary

1
2
3
4
5
6
7
8
function checkLogin(username, onSuccess, onError) {
if (typeof username === 'function' &&
!onSuccess) {
onError = username;
username = null;
}
...
}

Use more Jasmine matchers

The Jasmine testing framework comes with a few matchers expect(foo).toBeDefined(), etc. Jasmine-Matchers adds a LOT more, making your unit tests a lot more descriptive.

Focus / skip single suite or test

See my post on how to run / skip individual suite or test.

Use console.debug

There is grunt-remove-logging that removes console.debug or other logging calls after concatenation, but before minification step.

1
2
console.log('something really, really important');
console.debug('useful during testing and development');

In general, if I see log messages in the DevTools on websites, I think lame.

Use inspect to find the function

If you have a function definition (for example from Angular1 scope object) you can quickly jump to its definition in Chrome DevTools

1
inspect($($0).scope().saveDescription)

Use console.time

For simple timing in Node and browsers use

1
2
3
4
5
const label = 'foo'
console.time(label)
...
console.timeEnd(label)
// foo: 200.51ms

Array concatenation

JavaScript flattens items during Array concatenation

1
2
3
4
[].concat('a', 'b')
> ["a", "b"]
[].concat('a', ['b', 'c'])
> ["a", "b", "c"]

Concatenating states

Use simple objects for product states, no need to have arrays with single object

1
2
3
.config(function (productStates, anotherProductStates) {
var states = [].concat(productStates, anotherProductStates);
)})

Exclude files from build

When using grunt to build the front-end, you can easily skip files using ! syntax

1
2
3
4
5
// grabs only .js files, but not .spec.js
js: [
'src/**/*.js',
'!src/**/*.spec.js'
]

Know your code's complexity

Use grunt-complexity plugin to measure source complexity. You can even fail the build if complexity is higher than a set limit.

$ grunt complexity
Running "complexity:all" (complexity) task
✓ Gruntfile.js                   ████ 88.600
✓ build.config.js                ████ 98.062
✓ src/app/footer/footer.js       ███████ 140.54
✓ src/app/footer/footer.spec.js  ███████ 137.06
✓ src/app/header/header.js       ██████ 127.84
✓ src/app/header/header.spec.js  █████ 119.55

// higher number = simpler code

Simplify your Gruntfile.js

Gruntfiles grow in size and complexity with time. Luckily they can be easily refactored to bring them under control. A simple method is to move parts of config for different plugins into separate json files

1
2
// Gruntfile.js
complexity: grunt.file.readJSON('build.complexity.json')
1
2
3
4
5
6
7
8
9
10
11
12
// build.complexity.json
{
"all": {
"src": ["*.js", "src/app/**/*.js"],
"options": {
"errorsOnly": false,
"cyclomatic": 10,
"halstead": 20,
"maintainability": 80
}
}
}

Each plugin an loads its configuration from a separate simple file.

Optional logging without extra code

I often turn on optional logging depending on the boolean flag.

1
2
3
4
5
6
7
8
9
function foo(verbose) {
if (verbose) {
console.log('started foo');
}
...
if (verbose) {
console.log('another message');
}
}

These small if statements increase Cyclomatic complexity, leading to code that is harder to read and test. The following code is simpler:

1
2
3
4
5
6
function foo(verbose) {
var log = verbose ? console.log : angular.noop;
log('started foo');
...
log('another message');
}

Append array to existing array

We can easily concatenate JavaScript arrays, but this returns a new array

1
2
3
var a = ['foo'];
a.concat(['bar', 'baz']); // returns ['foo', 'bar', 'baz']
console.log(a); // ['foo']

Sometimes we want to append second array to the existing one. JavaScript Array.prototype.push method can take multiple items to append. You can pass the entire second array via apply call

1
2
3
var a = ['foo'];
a.push.apply(a, ['bar', 'baz']);
console.log(a); // ['foo', 'bar', 'baz']

Remove element from array

Lodash has two useful methods for removing items from an array. Both return new array. _.without removes items by value, and _.remove removes items by callback.

1
2
3
4
var a = ['foo', 'bar'];
_.without(a, 'bar'); // returns ['foo']
_.remove(a, function (item) { return /^f/.test(item); }); // returns ['bar']
console.log(a); // ['foo', 'bar']

_.remove is more versatile and can be used to remove items by property value

1
2
var a = [{ foo: 2 }, { foo: 3 }];
_.remove(a, { foo: 3 }); // returns [{ foo: 2 }]

Think of _.remove as inverse of Array.prototype.filter

Quickly evaluate properties on the object

If you have nested property names as strings and an object, you can quickly evaluate them without prepending object name. For example, all the console log statements below print "baz".

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var foo = {
bar: {
baz: 'baz'
}
};
// explicit dot notation
console.log(foo.bar.baz);
// set this = foo, explicit dot notation
with (foo) {
console.log(bar.baz);
}
// evaluate explicit dot notation string
console.log(eval('foo.bar.baz'));
// evaluate dot notation STRING against an object
with (foo) {
console.log(eval('bar.baz'));
}

This could be useful to evaluate expressions against a model object

1
2
3
4
5
6
function $update(model, expression) {
with(model) { eval(expression); }
}
$update(foo, 'bar.baz = 42;');
console.log(foo);
// { bar: { baz: 42 } }

While there are very serious downsides to with, there are some neat tricks where it can be used, like tiny spreadsheet in vanilla js.

Deep clone an object

To quickly deep clone an object, you can use this command JSON.parse(JSON.stringify(foo));

Ajax files from files

It is a pain to run a web server locally just so you can Ajax $.get(path). Chrome browser allows you to enable files to make ajax requests to other local files. Just close the browser, then open it with command line switch --allow-file-access-from-files. For example

alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
alias chromef="chrome --allow-file-access-from-files"
chromef index.html

Inside index.html you can fetch files!

1
2
3
$.get('foo.txt').done(function (data) {
alert('foo.txt has\n' + data);
});

The full list of Chrome command line switches is available here.

Avoid boilerplate when calling methods

In nodejs I often use path.join to form full paths. You can avoid always using path object - the methjod .join does not use this keyword (see Useful module pattern), thus we can use it directly. We can also remove passing __dirname every time using partial application

1
2
3
4
5
6
7
8
9
// boilerplate
var path = require('path');
var fullPath = path.join(__dirname, 'foo.js');
// without boilerplate
var join = require('path').join;
var fullPath = join(__dirname, 'foo.js');
// plus partial application
var toFull = require('path').join.bind(null, __dirname);
var fullPath = toFull('foo.js');

Wrap streams into promises

The most efficient way in Node to copy / send a file is to pipe a read stream into a write stream and let the system process data in chunks.

1
2
3
4
5
6
7
const fs = require('fs')
const readStream = fs.createReadStream('input.file')
const writeStream = fs.createWriteStream('output.file') // could be HTTP response, etc
readStream.pipe(writeStream)
readStream.on('end', function () {
console.log('all done')
})

Often we can just abstract the above operation into a promise-returning function for convenience.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function letItFlow(read, write) {
return new Promise(function (resolve) {
read.pipe(write)
read.on('end', function () {
resolve()
})
})
}
// use example
const fs = require('fs')
const readStream = fs.createReadStream('input.file')
const writeStream = fs.createWriteStream('output.file') // could be HTTP response, etc
letIfFlow(readStream, writeStream)
.then(function () {
console.log('all done')
})

If you run the above examples, you will notice a curious thing. The Node process exits several seconds after printing "all done" message. We have a bug in the code - we think the operation is finished after the read stream emits "end" event. But the write stream has not finished yet. The correct solution would wait for write stream's finish event.

1
2
3
4
5
6
7
8
function letItFlow(read, write) {
return new Promise(function (resolve) {
read.pipe(write)
write.on('finish', function () {
resolve()
})
})
}

The above promise resolves only when the write operation has been finished.