Перейти к основному содержанию

Введение в JavaScript, часть III: Объекты и массивы, подробный обзор


Введение в JavaScript, часть III: Объекты и массивы, подробный обзор

Автоматически переведённая страница

К сожалению, на полный ручной перевод у нас не хватает ресурсов.
Если вы увидели ошибку — отправьте пул-риквест с исправлениями (ссылка для редактирования в конце страницы).

Объекты

Всё в JavaScript — это объекты! За исключением true, false, простых чисел и строк, null и undefined. Да. Что это значит для вас? Это означает, что большинство вещей JS имеют свойства — те вещи, о которых мы говорили в первой части этого введения в JavaScript.

Так как вы можете создать и хранить новый объект? Синтаксис прост: вы создаете список свойств внутри { этих }, разделяя предметы запятыми (,), и разделяя имена свойств и значения точкой с запятой (:). Вы, возможно, видели такие конструкции, используя некоторые котомоды, например, tween:

var myObject = {
    name: 'Молот Bug-Killing',
    description: 'Купите этот молот, и вы сможете сокрушить этих насекомых с легкостью!',
    damage: 100500,
    price: NaN
};

Мы можем позже читать свойства объектов с помощью точки доступителя — того, который вы, наверное, видели везде, например, myObject.name.

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

this.weapon = myObject;

// Позже...

console.log(this.weapon.price); // Это NaN! Это не хорошо, давайте исправим это!
myObject.price = 777; // Обратите внимание, что здесь мы не ссылаемся на `this.weapon`.
console.log(this.weapon.price); // Теперь это 777. Ура!

Вложенные объекты

Вы можете хранить объекты внутри других объектов. Вы можете использовать ссылки на другие объекты в свойствах объекта или ввести их:

this.weapon = {
    name: 'The hammer of bug killing',
    description: 'Buy this hammer and you will crush these bugs with ease!',
    damage: 100500,
    price: 777
};
this.helmet = {
    name: 'The helmet of system thinking',
    description: 'Allows you to get a whole picture',
    wit: 5,
    price: 100
};
this.gear = {
    hands: this.weapon,
    head: this.helmet,
    body: { // You can inline new objects inside new objects!
        name: 'The chestplate of ignorance',
        description: 'Protects your mind from the outer world',
        wit: -100,
        mood: 5,
        price: 3
    }
};

console.log(this.gear.body.name); // Will return 'The chestplate of ignorance'.

Удаление свойств объектов полностью

Вы можете написать this.enemy = undefined, и в большинстве случаев все будет хорошо, но если вы работаете с localStorage или другими хранимыми данными, или если вы перебираете свойства объекта, вам скорее всего понадобится удалить свойство без следов — иначе оно все еще есть, хотя и без определенного значения.

Вы можете использовать ключевое слово delete, чтобы удалить любое свойство из объекта:

if (!templates.isValid(this.enemy)) {
    delete this.enemy;
}

Если строки и числа являются константами, почему мы можем использовать методы с ними?

Потому что JavaScript умный! На самом деле, существуют объекты, основанные на простых строках и числах, и вы можете создать такие с помощью new String('Divine sausage'), new Number(42), и даже new Boolean(true). Но эти методы не рекомендуются, потому что 99,99% времени вам эта функциональность не нужна. И это очень странная функциональность, которая выходит за рамки этой страницы введения.

Что вам действительно нужно, так это методы, которые есть у Number и String; чтобы форматировать эти значения и манипулировать строками. И JavaScript предоставляет их, когда вы пишете ' oh no '.trim() или (99.9).toFixed(2).

Массивы

Массивы можно представить как объекты с числовыми и упорядоченными свойствами, с большим количеством вспомогательных функций для них.

Объявление нового массива заметно отличается от объявления нового объекта:

var groceryList = ['картофель', 'морковь', 'тимьян'];
this.waveEnemyAmount = [10, 10, 15, 15, 20, 25];

console.log(groceryList[0]); // Вывод: картофель
console.log(groceryList[1]); // Вывод: морковь
console.log(this.waveEnemyAmount); // Вывод: весь массив

Обратите внимание, как мы обращаемся к элементам массива: мы используем число в квадратных скобках, начиная с [0], чтобы получить значение.

Вы также можете хранить сложные объекты в массивах:

this.shopItems = [{
    name: 'The hammer of bug killing',
    description: 'Buy this hammer and you will crush these bugs with ease!',
    damage: 100500,
    price: 777
}, {
    name: 'The helmet of system thinking',
    description: 'Allows you to get a whole picture',
    wit: 5,
    price: 100
}, {
    name: 'The chestplate of ignorance',
    description: 'Protects your mind from the outer world',
    wit: -100,
    mood: 5,
    price: 3
}];

console.log(this.shopItems[0].name); // Will output 'The hammer of bug killing'
console.log(this.shopItems[2].price); // Will output `3`, the price of the chestplate of ignorance

Здесь мы обращаемся к всему объекту с помощью [0], [1], [2] и т. д., а затем читаем свойство этого объекта, добавляя .name и .price. Будьте внимательны к этой синтаксической конструкции!

Получение длины массива

Массивы имеют несколько функций для упрощения обработки любых игровых данных, которые вам могут понадобиться.

Во-первых, есть свойство length, которое указывает количество элементов в массиве.

Как его можно использовать? Допустим, вы хотите ограничить количество мусора в инвентаре игрока:

this.inventory = ['sword', 'sword', 'sword', 'sword', 'sword', 'sword', 'sword', 'sword', 'sword', 'apple'];
this.maxInventorySize = 10;
// позже…

if (this.inventory.length < this.maxInventorySize) {
    this.inventory.push('gold ingot'); // Добавить новый элемент
}

К сожалению, они никогда не получают этот золотой слиток.

Добавление новых элементов в массивы

Существует три метода для добавления нового элемента в массив:

var pizza = ['tomato sauce'];

pizza.push('pepperoni'); // Этот метод добавляет новый элемент в конец массива
pizza.unshift('dough'); // Этот метод добавляет элемент в начало массива
pizza.splice(2, 0, 'cheese'); // Этот метод добавляет новый элемент после pizza[2]. Пока игнорируем аргумент 0.

console.log(pizza); // Выводит массив с dough, tomato sauce, cheese и pepperoni. Мм... вкусняшка! 🍕

Удаление элементов из массива

Давайте съедим нашу пиццу!

var pizza = ['дрожжи', 'соус', 'сыр', 'перец'];

pizza.pop('дрожжи'); // Удалить последний элемент из массива
pizza.splice(1, 2); // Удалить два элемента, начиная с pizza[1]
pizza.splice(0, 1, 'корочка'); // Удалить один элемент, начиная с pizza[0], и заменить его на 'корочку'
pizza.shift(); // Удалить первый элемент массива

console.log(pizza); // Выводит пустой массив!

Дополнительная информация о методе array.splice

Вы можете увидеть, что pizza.splice использовался в трех разных случаях: для добавления, удаления и замены значений. Как работает этот метод?

Этот метод используется для замены одного набора элементов на другой. Его полная форма — .splice(startFromIndex, deleteCount, addOne, addTwo, addThree, …). Но вы можете удалить несколько элементов и не добавлять ничего, чтобы удалить элементы из массива, или сделать наоборот — не удалять ничего и добавлять новые элементы:

  • Когда вы пишете .splice(3, 0, 'колбасу'), вы добавляете новые элементы после третьего элемента массива.
  • Когда вы пишете .splice(3, 1), вы удаляете один элемент.
  • Когда вы пишите .splice(3, 1, 'колбасу'), вы заменяете один элемент на другой.
  • Вы можете написать .splice(3, 2) для удаления нескольких элементов одновременно.

Функции для поиска, фильтрации, сортировки и сокращения массивов

Фильтрация с помощью array.filter

Функция array.filter — это удобный инструмент, который создает новую массив из существующего. Вы передаете ему в аргумент функцию фильтрации, называемую «предикатом», написанную вами самими.

Давайте выберем всех нейтральных и дружелюбных существ в нашем bestiary:

var bestiary = [
    { name: 'Свинья', aggressiveness: 'нейтральный' },
    { name: 'Кошка', agressiveness: 'дружелюбная' },
    { name: 'Волк', aggressiveness: 'вражеская' },
    { name: 'Медведь', aggressiveness: 'вражеская' },
    { name: 'Волшебная пони', aggressiveness: 'нейтральный' }
];

var neutralAnimals = bestiary.filter( beast => {
    if (beast.aggressiveness === 'вражеская') {
        return false;
    }
    return true; // Этот блок будет выполнен только в том случае, если предыдущий блок не выполняется,
                 // потому что `return` останавливает выполнение функции.
});
console.log(neutralAnimals);

В данном примере все существа, возвращающие false, будут исключены из массива neutralAnimals. А те, которые возвращают true, включатся в этот массив.

Давайте рассмотрим еще один пример: получение списка оружия, которое герой может купить сейчас:

this.money = 1230;
var shop = [
    { name: 'Искусство Измерения Констант', price: 130, type: 'book' },
    { name: 'Молот для борьбы с насекомыми', price: 100500, type: 'weapon' },
    { name: 'Ржавый топор отравления', price: 853, type: 'weapon' },
    { name: 'Свиток молний', price: 167, type: 'book' }
];
// здесь мы пропускаем `return` и скобки — это более короткая синтаксическая конструкция, а результат автоматически возвращается!
var purchaseable = shop.filter(item => item.price <= this.money);
var purchaseableWeapon = purchaseable.filter(item => item.type === 'weapon');
console.log(purchaseableWeapon);

Метод sort массива

Метод array.sort может работать без изменений с текстовыми элементами:

var groceryList = [
    'морковь',
    'картошка',
    'салат',
    'сосиски'
];
groceryList.sort();
console.log(groceryList); // картошка и морковь будут поменяны местами

Вы можете заметить, что в отличие от других подобных методов, сортировка не создает новый массив, а модифицирует существующий.

Вы также можете передать предикат, который будет возвращать, как два объекта сравниваются друг с другом. Давайте попробуем использовать список товаров из магазина выше и отсортировать его по цене в порядке возрастания:

var shop = [{
    name: 'Искусство Царства Постоянных',
    price: 130,
    type: 'книга'
}, {
    name: 'Молот для убийства насекомых',
    price: 100500,
    type: 'оружие'
}, {
    name: 'Топор для уничтожения насекомых',
    price: 853,
    type: 'оружие'
}, {
    name: 'Свиток молний',
    price: 167,
    type: 'книга'
}];

shop.sort((a, b) => {
    return a.price - b.price;
});

Здесь мы берем два элемента и возвращаем разницу между первым (a) и последним (b). Если полученный вами результат отрицательный, первый элемент переместится ближе к индексу 0, а второй элемент поднимется на более высокий индекс. Если результат вашего расчета положительный, происходит противоположное. Если возвращается 0, элементы не меняются местами.

Совет

Вы можете вызвать метод sort только один раз — JavaScript будет продолжать сортировать массив до тех пор, пока он не станет стабильным.

Методы поиска и проверки элементов массива

Существует множество методов для поиска элементов в массиве!

Проверка наличия элемента в массиве с помощью array.includes

Функция array.includes(value) позволяет быстро проверить, присутствует ли определенный элемент в текущем массиве. Она возвращает логическое значение (true или false).

var buffs = ['vigor', 'rested', 'rage'];
// Добавить новый элемент в массив только если он еще не присутствует
if (!buffs.includes('blessed')) {
    buffs.push('blessed');
}
Проверьте, удовлетворяют ли некоторые элементы в массиве определенному условию с помощью array.some

Это функция высшего порядка для проверки того, удовлетворяет ли по крайней мере один из элементов вашему условию. Она принимает предикат, который проходит через каждый элемент, и возвращает true, только если по крайней мере одно вызовы этой функции вернули true.

this.gear = [
    {
        name: 'Кувалда для истребления насекомых',
        description: 'Купите эту кувалду, и вы сможете легко раздавить этих насекомых!',
        damage: 100500,
        enchantment: 'благословенный'
    },
    {
        name: 'Шлем системного мышления',
        description: 'Позволяет получить общее представление',
        wit: 5,
        enchantment: 'нет'
    },
    {
        name: 'Щиты игнорирования',
        description: 'Защищает ваш разум от внешнего мира',
        wit: -100,
        mood: 5,
        enchantment: 'проклятый'
    }
];

// Добавить деффлаб только если одно из предметов проклятые
if (this.gear.some(item => item.enchantment === 'проклятый')) {
    this.debuffs.push('проклятый');
}

Получаем элемент, удовлетворяющий условию с помощью array.find и array.findIndex

Они аналогичны по своей концепции методу array.some, но возвращают первый элемент, который соответствует вашим требованиям. array.find возвращает само элемент (или undefined), если ничего не найдено, а array.findIndex возвращает позицию подходящего элемента в массиве (или -1, если такого элемента не существует).

this.gear = [{
    name: 'The hammer of bug killing',
    type: 'weapon'
}, {
    name: 'The helmet of system thinking',
    type: 'head'
}, {
    name: 'The chestplate of ignorance',
    type: 'torso'
}];

// Установка нанесенного урона на значение урона текущего элемента снаряжения
var weapon = this.gear.find(item => item.type === 'weapon');
this.damage = weapon.damage;

// Удаление шлемы
var helmetIndex = this.gear.findIndex(item => item.type === 'head');
if (helmetIndex !== -1) { // Убедитесь, что шлем был найден
    this.gear.splice(helmetIndex, 1);
}

Найти индекс известного элемента в массиве с помощью array.indexOf

Аналогично array.findIndex, array.indexOf возвращает индекс указанного элемента в массиве. Это полезно, если вы храните числа или строки в массиве или у вас есть ссылка на искомый объект.

var groceryList = [
    'картофель',
    'морковь',
    'огурец',
    'банан',
    'черешня'
];
var carrotIndex = array.indexOf('морковь');
if (carrotIndex !== -1) { // Убедитесь, что мы действительно нашли элемент
    groceryList.splice(carrotIndex, 1);
}

Метод 'reduce' массива

Метод 'array.reduce' обходит каждый элемент массива, выполняя свою функцию (предикат), и передавая результат этой функции следующему вызову. Он обычно используется для быстрого сбора различных статистических данных по массиву, и написав предикат с другой логикой, можно рассчитывать различные статистические значения.

Предположим, вы разрабатываете игру в жанре "башня защиты", и у вас есть финальный босс в последнем раунде. Вы хотите рассчитать время до появления босса, суммируя задержки всех раундов.

Учитывая следующую структуру данных, мы можем использовать метод 'reduce' массива для получения суммы всех задержек:

var waves = [
  {
    delay: 30,
    monsters: [
      {
        type: "Monster_Flyer",
        health: 10,
        amount: 10
      }
    ]
  },
  {
    delay: 10,
    monsters: [
      {
        type: "Monster_Flyer",
        health: 15,
        amount: 12
      }
    ]
  },
  {
    delay: 12,
    monsters: [
      {
        type: "Monster_Flyer",
        health: 15,
        amount: 20
      },
      {
        type: "Monster_Tank",
        health: 15,
        amount: 20
      }
    ]
  },
  {
    delay: 20,
    monsters: [
      {
        type: "Monster_Boss",
        health: 1000,
        amount: 1
      }
    ]
  }
];

var timeTillBoss = waves.reduce((currentSum, wave) => {
  return currentSum + wave.delay;
}, 0); // Здесь 0 является начальным значением.

Теперь давайте используем тот же массив, чтобы получить количество всех врагов в раундах по их типу. Это будет более сложный пример, поскольку нам нужно обрабатывать массивы монстров внутри основного массива:

var monstersInTheLevel = waves.reduce((currentStats, wave) => {
  for (const monsterGroup of wave.monsters) {
    // Инициализируем группу монстров, если она еще не была добавлена в статистику
    if (!currentStats[monsterGroup.type]) {
      currentStats[monsterGroup.type] = 0;
    }
    currentStats[monsterGroup.type] += monsterGroup.amount;
  }
}, {}); // Начальное значение - пустой объект

Было ли это понятно? Возможно. Можем ли мы сделать то же самое с обычными циклами? Конечно. Однако если у вас есть разные массивы, которые необходимо обработать, написание циклов для каждого массива может быть утомительным. С помощью 'reduce', 'forEach', 'filter' и 'find' вы можете хранить всю логику предиката в переменной или свойстве, а затем использовать его несколько раз при необходимости, что делает ваш код более чистым.

Тёмные знания о массивах в JavaScript 🕸🕷

Двухмерные массивы

В JavaScript массивы по умолчанию являются одновременными: это просто список. Но если вы создадите список списков, он вдруг станет похожим на двухмерные массивы в других языках программирования!

var myMap = [
    [1, 1, 1, 1, 0],
    [0, 0, 1, 0, 1],
    [2, 0, 1, 1, 1],
    [1, 0, 1, 0, 0],
    [1, 1, 1, 0, 0],
];

В этом примере вы можете получить значение myMap[2][0] (которое вернет 2 — элемент во втором ряду и в первой колонке).

Специальный, массивоподобный доступ к свойствам объектов

Вы помните, что почти всё в JavaScript — это объект? А массивы как насчёт них? Почему вы думаете, что у массивов есть специальная синтаксическая конструкция для доступа к их элементам?

Потому что это на самом деле не ограничено только массивами! Каждый объект может читать, удалять и изменять свои свойства с помощью массивоподобного доступора:

var myObject = {
    name: 'Убийца багов',
    description: 'Купите этот молоток, и вы сможете легко сокрушить этих насекомых!',
    damage: 100500,
    price: NaN
};
myObject['price'] = 1000;

Любое значение, которое может быть преобразовано в строки, может использоваться в качестве ключа:

// Это действительно странный пример, но что, если так и представляете настраиваемую систему способностей в ARPG?
var abilities = {
    '0': 'Лунный удар',
    '1': 'Разрезающий прыжок',
    '5': 'Лечить'
};
// Переместите навык:
var ability = abilities[1];
delete abilities[1];
abilities[2] = ability;

И самое лучшее в том, что ключи не должны быть статичными! Вы можете вычислять значения, объединяя строки или совершая другие магические вещи:

this.stats = {
    resistanceFire: 5,
    resistanceIce: 0
};
this.armor = {
    name: 'Плащ Холода',
    resistanceType: 'Ice',
    resistanceBoost: 15
};
// `'resistance' + this.armor.resistanceType` будет 'resistanceIce',
// поэтому свойство this.stats.resistanceIce будет изменено.
this.stats['resistance' + this.armor.resistanceType] += this.armor.resistanceBoost;

Особый случай: строки являются… массивами?!

Строки имеют свойство .length, которое возвращает длину строки, конечно. Они также могут возвращать символы в конкретных позициях, если вы попытаетесь использовать массивный аксessor:

var string = 'Привет, ct.js!';
console.log(string.length); // вернёт 14
console.log(string[1]); // вернёт 'р'

Но у строк нет всех этих умных методов, таких как .forEach, .map или .filter. На самом деле у них есть свои собственная методыopen in new window, чтобы упростить преобразование строк, такие как .trim(), .search, .replace и другиеopen in new window. Они также имеют свой собственный .slice. Не нужно использовать строки как настоящие массивы, но если вам действительно нужен этот режим работы, вы можете использовать Array.from(yourString), чтобы создать новый массив, элементы которого будут символами строки.

Заключение

Объекты и массивы невероятно мощные — они в основном свободные структурные формы, которые могут вместить все, что вы в них кладете. Знание того, как эффективно управлять ими, позволит вам писать код быстрее и легче. Но не волнуйтесь, если вы сейчас не можете вспомнить всё — совершенство приходит с практикой, а практика требует времени. Не так ли? Я, например, использую каждый из этих методов в лучшем случае раз в месяц или около того, хехех.

Удачи в кодинге!
CoMiGo