Автор фото — Jesus Kiteque.

Доброго времени суток, друзья!

Представляю Вашему вниманию перевод статьи jsmanifest «9 Ways to Work With Objects in JavaScript in 2020».

9 приемов работы с объектами в JavaScript в 2020 году


JS, как и прочие языки программирования, имеет множество инструментов для выполнения как простых, так и сложных задач. Давайте познакомимся с 9 приемами работы с объектами в JS в 2020 году.

Замечание: некоторые из представленных приемов покажутся Вам интересными, некоторые очевидными (потому что хорошо известны), некоторые приведены просто для информации.

Как бы то ни было, если Вам нравится писать код на JS, Вы, пожалуй, согласитесь со мной, что работать с объектами намного интересней, чем с другими типами.

1. Создать пустой объект


Знаете ли Вы, что можете создавать объекты в JS? Конечно, знаете.

А знаете ли Вы, что можете создавать пустые объекты?

Вот пример:

const myEmptyObject = {}

Так обычно создаются пустые объекты. Однако такой объект не совсем пустой, поскольку на самом деле происходит Object.create(Object.prototype). Поэтому Ваш объект будет иметь доступ к свойствам Object.prototype, который находится на вершине цепочки прототипов.

Это означает, что Вы можете использовать метод myEmptyObject.toString().

Для того, чтобы создать по-настоящему пустой объект, необходимо сделать следующее:

const myTrullyEmptyObject = Object.create(null)

У такого объекта не будет свойств, пока Вы их ему не добавите.

В 99,99% случаев у нас нет причины «отказываться от услуг» базового прототипа.

2. Объединение объектов (Object.assign)


const novice = {
    username: 'henry123',
    level: 10,
    hp: 100
}

function transform(target) {
    return Object.assign(target, {
        fireBolt(player) {
            player.hp -= 15
            return this
        },
    })
}

const sorceress = transform(novice)
const lucy = {
    username: 'iamlucy',
    level: 5,
    hp: 100
}

sorceress.fireBolt(lucy)

Когда Вы используете Object.assign, Вам нужен целевой объект (target), к которому будут добавляться свойства и/или методы.

Целевой объект передается Object.assign в качестве первого аргумента. Во втором и последующих аргументах указываются свойства и/или методы, добавляемые к target.

Вот как определяет этот метод MDN:
Метод Object.assign() используется для копирования значений всех собственных перечисляемых свойств из одного или более исходных объектов в целевой объект. После копирования он возвращает целевой объект.

Объединение объектов (Spread Syntax)


const novice = {
    username: 'henry123',
    level: 10,
    hp: 100
}

function transform(target) {
    return {
        ...target,
        fireBolt(player) {
            player.hp -= 15
            return this
        },
    }
}

const sorceress = transform(novice)
const lucy = {
    username: 'iamlucy',
    level: 5,
    hp: 100
}

sorceress.fireBolt(lucy)

Здесь мы используем оператор spread («троеточие») в объектном литерале.

Такой способ использования spread был введен стандартом ECMAScript2018.

Это невероятно простой способ объединения объектов, который позволяет Вашему коду оставаться аккуратным и читабельным.

Помните, что правила объединения объектов остаются прежними, поэтому Вы можете использовать такие странные конструкции, как эта (с Вашего позволения, я опущу часть статьи, в которой автор восхищается функциями как объектами первого класса — прим. пер.):

// объединение IIFE
import React from 'react'
import {
    EditIcon,
    DeleteIcon,
    ResetIcon,
    TrashIcon,
    UndoIcon,
} from '../lib/icons'
import * as utils from '../utils'

export const ausioExts = ['mp3', 'mpa', 'ogg', 'wav']

const icons = {
    edit: {
        component: EditIcon,
        onClick: () => alert('You clicked the edit component'),
        name: 'edit',
    },
    delete: {
        component: DeleteIcon,
        name: 'delete',
    },
    // audio icons
    // IIFE возвращает объект
    ...(function() {
        return audioExts.reduce((acc, ext) => {
            acc[ext] = {
                component: MdAudioTrack,
                title: 'Audio Track'
            }
            return acc
        })(),
    })
}

IIFE возвращает объект, который объединяется с объектом icons. На выходе получаем следующее:

export const audioExts = ['mp3', 'mpa', 'ogg', 'wav']

const icons = {
    edit: {
        component: EditIcon,
        onClick: () => alert('You clicked the edit component'),
        name: 'edit',
    },
    delete: {
        component: DeleteIcon,
        name: 'delete',
    },
    // результат объединения
    mp3: {
        component: MdAudiotrack,
        title: 'Audio Track',
    },
    mpa: {
        component: MdAudiotrack,
        title: 'Audio Track',
    },
    ogg: {
        component: MdAudiotrack,
        title: 'Audio Track',
    },
    wav: {
        component: MdAudiotrack,
        title: 'Audio Track',
    },
}

Проверка свойств объекта


Одна из фич, приведших в восторг сообщество JS-разработчиков (я в этом уверен), это опциональная цепочка. Этот новый оператор, который выглядит как вопросительный знак с точкой (?.), позволяет читать свойства, которые находятся глубоко в цепочке объектов, без необходимости проверять работоспособность каждой ссылки (хорошая статья на Хабре, посвященная операторам ?., ?? и |>; работаю над переводом статьи, посвященной опциональной цепочке и объединению с null, следите за обновлениями — прим. пер.).

Это означает, что если мы имеем такой объект:

const food = {
    fruits: {
        apple: {
            dates: {
                expired: '2020-01-25'
            },
        },
    },
}

… нам больше не нужно писать такой скучный код:

function getAppleExpirationDate(obj) {
    if (food.fruits && food.fruits.apple && food.fruits.apple.dates) {
        return food.fruits.apple.dates.expired
    }
}

Вместо этого, мы может использовать опциональную цепочку:

function getAppleExpirationDate(obj) {
    return food?.fruits?.apple?.dates?.expired
}

Использование данного оператора делает код более чистым.

Функция вроде этой:

function findFatDogs(dog, result = []) {
    if (dog && dog.children) {
        return dog.children.reduce((acc, child) => {
            if (child && child.weight > 100) {
                return acc.concat(child)
            } else {
                return acc.concat(findFatDogs(child))
            }
        }, result)
    }
    return result
}

… может быть преобразована так (с повышением читабельности):

function findFatDogs(dog, result = []) {
    if (dog?.children) {
        return dog.children.reduce((acc, child) => {
            return child?.weight > 100
            ? acc.concat(child)
            : acc.concat(findFatDogs(child))
        }, result)
    }
}

Запомните: в настоящее время не все браузеры поддерживают эту возможность. Однако вы можете использовать TypeScript (или Babel — прим. пер.).

5. Вызов объектов посредством переопределения метода .toString()


Когда объекты назначаются в качестве ключей литералов объекта, они становятся строками. Это предоставляет нам некоторые интересные возможности.

Давайте рассмотрим это на примере:

function Command(name, execute){
    this.name = name
    this.execute = execute
}

Command.prototype.toString = function(){
    return this.name
}

const createCommandHub = function(){
    const commands = {}
    return {
        add(command){
            commands[command.name] = command
        },
        execute(command, ...args){
            return commands[command].execute(...args)
        },
    }
}

const cmds = createCommandHub()
const talkCommand = new Command('talk', function(wordsToSay){
    console.log(wordsToSay)
})
const destroyEverythingCommand = new Command('destroyEverything', function(){
    throw new Error('Destroying everything')
})

cmds.add(talkCommand)
cmds.add(destroyEverythingCommand)
cmds.execute(talkCommand, 'Is talking a talent?')

Если Вы запустите этот сниппет, то убедитесь в том, что код работает, а результатом будет «Is talking a talent?»

Если присмотреться внимательнее к тому, как мы добавляем команду, можно подумать, что будет выброшена ошибка.

Причина, по которой этого не происходит, заключается в том, что мы определили конструктор Command и переопределили метод toString.

Когда «непримитив» назначается в качестве свойства объекта, JS пытается преобразовать его в строку перед назначением в качестве ключа, обращаясь к методу .toString прототипа.

6. Деструктуризация


Среди других замечательных возможностей языка следует назвать деструктуризацию (парочку интересных примеров деструктуризации можно посмотреть здесь — прим. пер.):

const obj = {
    foods: {
        fruits: ['apple', 'orange'],
    },
}

const { foods } = obj

console.log(foods) // fruits: ['apple', 'orange']

7. Изменение названия деструктурируемых свойств


const obj = {
    foods: {
        fruits: ['apple', 'orange'],
    },
}

const { foods: myFoods } = obj

console.log(myFoods) // fruits: ['apple', 'orange']

8. Перебор ключей объекта (for in)


Легким способом перечислить ключи объекта является for in:

const obj = {
    foods: {
        fruits: ['apple', 'orange'],
    },
    water: {
        f: '',
    },
    tolupa: function(){
        return this.name
    },
}

const { foods } = obj

for(let k in obj){
    console.log(k) // foods water tolupa
}

9. Перебор ключей объекта (Object.keys)


Другой способ перебрать ключи объекта заключается в использовании метода Object.keys:

const obj = {
    foods: {
        fruits: ['apple', 'orange'],
    },
    water: {
        f: '',
    },
    tolupa: function(){
        return this.name
    },
}

const { foods } = obj

const keys = Object.keys(obj)
console.log(keys)

Разница состоит в том, что мы получаем массив с ключами, с которым можно работать дальше.

Например, это очень удобно, если мы собираемся использовать данный массив для фильтрации:

const people = {
    bob: {
        age: 15,
        gender: 'male',
    },
    jessica: {
        age: 24,
        gender: 'female',
    },
    lucy: {
        age: 11,
        gender: 'female',
    },
    sally: {
        age: 14,
        gender: 'female',
    },
}

const { males, females } = Object.keys(people).reduce(
    (acc, name) => {
        const person = people[name]
        if(person.gender === 'male'){
            acc.males.push(name)
        } else{
            acc.females.push(name)
        }
        return acc
    },
    { males: [], females: [] },
)

console.log(males) // ['bob']
console.log(females) // ['jessica', 'lucy', 'sally']

На этом у меня все. Надеюсь, статья была Вам полезной. До новых встреч!