February 21st, 2022
I've had multiple junior developers and interns join the team over the years and I found one thing that they all have in common (most of the time): They end up unnecessarily writing longer ways to do something in code when there are shorter (as well as performant) ways to do the same thing.
With that said this post will mostly be aimed for JavaScript developers who are learning the language as well as those who are just curious to potentially find out what's more out there in JavaScript. There are some tips here that I didn't know about until the later stages of my development career.
Without further ado, here are 10 JavaScript Practices You Should Know Before Tomorrow:
ES6 introduced features that influenced the majority of our code base today. There is the spread operator as well as the Set object. These two can be combined to a one liner that filters an array of primitive values to be fully unique:
const symb1 = Symbol('hello')
const symb2 = Symbol('hello')
// prettier-ignore
const arr = [1, '1',1, 3, 'morning', null, '', 3, 121,true,symb1,symb1, 121, true, symb1, symb2]
const uniqueArr = [...new Set(arr)]
Result:
;[1, '1', 3, 'morning', null, '', 121, true, Symbol(hello), Symbol(hello)]
Note: This JavaScript trick is the cleanest solution for arrays containing only primitive data types such as boolean
, string
, undefined
, null
, number
. For the Symbol
type, each constructed Symbol
is unique so Symbol(hello)
and Symbol(hello)
are actually two different instances (declared as variable symb1
and symb2
)
JavaScript treats all values as truthy or falsey except for the true
and false
values themselves. This is intended by the semantics of the language:
if ('true') {
// Truthy
}
if ('false') {
// Truthy
}
if (null) {
// Falsy
}
if (0) {
// Falsy
} else if (function () {}) {
// Truthy
}
if (new Array().fill(null)) {
// Truthy
}
if (Object.create(null)) {
// Truthy
}
if (1) {
// Truthy
}
All values in JavaScript are truthy except (when otherwise specified):, false
, ''
, null
, NaN
, undefined
, and 0
which JavaScript interprets as all falsy.
The most concise way to convert values to boolean (true
or false
) is by negating values with !
as shown below:
const isTrue = !0
const isFalse = !1
const alsoFalse = !!0
console.log(isTrue) // Result: true
console.log(typeof true) // Result: "boolean"
This type of coercion can become really useful when handling functions that take multiple input especially when you're looking to create simple and smaller functions:
Also, with 0
and 1
we can also convert them to true
and false
just as easily:
console.log(+true) // Result: 1
console.log(+false) // Result: 0
true
is the equivalent to 1
and false
is the equivalent of 0
:
console.log(-true) // Result: -1
console.log(-false) // Result: -0
We can quickly convert values to strings by a simple +
operator immediately following quotes:
const val = 1 + ''
console.log(val) // Result: "1"
console.log(typeof val) // Result: "string"
Similarly to the previous example, we can also convert strings to numbers with the same +
operator by prefixing strings with it:
let age = '80'
age = +age
// Result: 80
We can use this to increment numbers by true
or false
as well (remember that true
is equivalent to 1
and false
as 0
):
let age = 30
age = age + true
// Result: 31
age = 30
age = age + false
// Result: 30
&&
)Instead of this:
const john = { id: null, age: 30, email: 'john@gmail.com' }
const mike = { id: '456', age: 31, email: 'mike@gmail.com' }
const sally = { id: '789', age: 32, email: 'sally@gmail.com' }
const kelly = { id: null, age: 25, email: 'kelly@gmail.com' }
const george = { id: '131415', age: 40, email: 'george@gmail.com' }
const getProfileOfAgeWithSubmittedEmail = (minAge, ...profiles) => {
for (const profile of profiles) {
let age
if (profile.email && profile.age) {
age = profile.age
}
if (typeof age === 'number' && age >= minAge) {
return profile
}
}
}
// prettier-ignore
const profile = getProfileOfAgeWithSubmittedEmail(32, john, mike, sally, kelly, george)
// Result: sally
We can use the logical and (&&
) operator:
const getProfileOfAgeWithSubmittedEmail = (minAge, ...profiles) => {
for (const profile of profiles) {
const age = profile.email && profile.age
if (typeof age === 'number' && age >= minAge) {
return profile
}
}
}
// prettier-ignore
const profile = getProfileOfAgeWithSubmittedEmail(32, john, mike, sally, kelly, george)
// Result: sally
This does three separate things in just one line:
profile.email
profile.email
) is falsy, then return profile.email
profile.age
It is shorter but also when you use it a lot in your code you will start to realize you are naturally writing const
instead of let
(or var
) more, which is a best practice if you expect assigned values will never be changing after evaluating which is key in writing cleaner code.
||
)Similarly to the above, instead of this:
const getProfileOfAgeWithMissingId = (maxAge, ...profiles) => {
for (const profile of profiles) {
let id
if (profile.age < maxAge) {
id = profile.id
}
if (!id) return profile
}
}
// prettier-ignore
const profile = getProfileOfAgeWithMissingId(30, john, mike, sally, kelly, george)
// Result: kelly
We can use the logical and (||
) operator as a shorter alternative:
const getProfileOfAgeWithMissingId = (maxAge, ...profiles) => {
for (const profile of profiles) {
const id = profile.age < maxAge || profile.id
if (!id) return profile
}
}
// prettier-ignore
const profile = getProfileOfAgeWithMissingId(30, john, mike, sally, kelly, george)
// Result: kelly
This does three separate things in just one line:
profile.age < maxAge
false
profile.id
The advantage of short circuiting this way is that you can hit two conditons (#2
and #3
) in one line of code (profile.age < maxAge || profile.id
) which is great!
Instead of this:
const john = { id: null, age: 30, email: 'john@gmail.com' }
const mike = { id: '456', age: 31, email: 'mike@gmail.com' }
let person1
let person2
person1 = person1 || (person1 = john)
console.log(person1)
// Result: { id: null, age: 30, email: 'john@gmail.com' }
We can instead do this:
const john = { id: null, age: 30, email: 'john@gmail.com' }
const mike = { id: '456', age: 31, email: 'mike@gmail.com' }
let person1
let person2
person1 ||= john
console.log(person1)
// Result: { id: null, age: 30, email: 'john@gmail.com' }
There is hardly a difference in syntax but the benefit of person1 ||= john
is short circuiting where the assignment of john
is only evaluated if person
is falsy.
Other equivalents to the short circuiting logical assignment operator are:
person1 &&= john // If person1 is truthy then assign john to person1
person1 ??= john // If person1 is null or undefined then assign john to person1
An interesting trick is to customize how objects coerce to strings when assigned to another object as a property to that object.
Normally when we assign objects as properties we get '[object Object]'
:
const john = {
id: null,
age: 30,
email: 'john@gmail.com',
}
const obj = {}
obj[john] = john
console.log(obj)
result:
{ "[object Object]": { "id": null, "age": 30, "email": "john@gmail.com" } }
This is not very ideal because this is how it would turn out if we wanted to use JSON.stringify
to save data to some data source:
const output = JSON.stringify(obj, null, 2)
console.log(output)
// Result:
// { "[object Object]": { "id": null, "age": 30, "email": "john@gmail.com" } }
Since all objects by default coerce to [object Object]
and objects can only keep 1 unique key in an object at a time we also lose other data when we want to use them as keys (converting Map
objects to their JSON representation when they can hold JavaScript objects for example).
To solve this issue we can define the toString()
method and return the value we want as the key when assigned as a property on objects:
const john = {
id: null,
age: 30,
email: 'john@gmail.com',
toString: () => `John_${john.age}_${john.email}`,
}
const obj = { [john]: john }
// Or --> obj[john] = john
console.log(obj)
Output:
{
"John_30_john@gmail.com": {
"id": null,
"age": 30,
"email": "john@gmail.com"
}
}
Now we can even create Map
instances and easily serialize them into a format we want:
const createPerson = (options) => {
const person = {}
for (const [key, value] of Object.entries(options.props)) {
person[key] = value
}
if (typeof options.key === 'function') {
Object.defineProperty(person, 'toString', {
value: function getPropertyKey() {
return options.key(person)
},
})
}
return person
}
const john = createPerson({
props: { id: null, email: 'john@gmail.com' },
key: (values) => {
return `John_${values.age}_${values.email}`
},
})
const mike = createPerson({
props: { id: '456', age: 31, email: 'mike@gmail.com' },
key: (values) => `Mike_${values.email}`,
})
const map = new Map()
map.set(john, john)
map.set(mike, mike)
map.toJSON = function () {
const output = Object.assign(
{},
[...this.entries()].reduce((acc, [key, value]) => {
acc[key] = value
return acc
}, {}),
)
return output
}
john.age = 30
console.log(JSON.stringify(map, null, 2))
Result:
{
"John_30_john@gmail.com": {
"id": null,
"email": "john@gmail.com",
"age": 30
},
"Mike_mike@gmail.com": {
"id": "456",
"age": 31,
"email": "mike@gmail.com"
}
}
In the previous example we learned how to customize the JSON stringified output of objects. But we can also customize the tag part of the coerced [object Object]
value:
function Warrior() {
Object.defineProperty(this, Symbol.toStringTag, {
value: 'Warrior',
})
this.hp = 100
}
const sally = new Warrior()
console.log(sally.toString()) // Result: [object Warrior]
We can use this[Symbol.toStringTag] = 'Warrior'
, but it can show up in the console when we log our instance:
Warrior { hp: 100, [Symbol(Symbol.toStringTag)]: 'Warrior' }
By using Object.defineProperty
we prevent it from being displayed to the console. (We can configure that with the enumerable
property)
function Warrior() {
Object.defineProperty(this, Symbol.toStringTag, {
enumerable: true, // Include it in log output if true. Defaults to false
value: 'Warrior',
})
this.hp = 100
}
A good use case for this is to have some helper function that easily checks if an object is an instance of something like our Warrior
:
function isWarrior(value) {
return String(value) === '[object Warrior]'
}
const sally = new Warrior()
console.log(isWarrior(sally)) // Result: true
We can create functions that take prepended arguments which can be called later with the prepended arguments in addition to new arguments.
For example:
function isOlder(person, targetPerson) {
return targetPerson.age > person.age
}
console.log(isOlder(john, mike))
// Result: true
We can easily make a new re-usable function that compares the result on a prepended value:
const isOlderThanJohn = isOlder.bind(null, john)
console.log(isOlderThanJohn(mike))
// Result: true
console.log(isOlderThanJohn({ age: 11 }))
// Result: false
One way we can retrieve the last item in arrays is the more commonly used:
const people = [john, mike, kelly, sally]
const lastPerson = people[people.length - 1]
// Result --> sally
A trick we can use is using the .slice
method on arrays. Since JavaScript can take negative integers in .slice
it will begin taking values at the end of the array which can quickly be used to grab the last item if we start from -1
:
const people = [john, mike, kelly, sally]
const lastPerson = people.slice(-1)
// Result --> sally
And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!
Tags
© jsmanifest 2023