Тестирование JavaScript кода с Jest для чайников. Часть 1
Здравствуй, Хабр! Данное руководство является первой частью в запланированном цикле статей про такой замечательный фреймворк для тестирования как Jest. Материал будет полезен новичкам и тем, кто только знакомится с тестированием, и хотел бы изучить этот фреймворк. В первой части мы разберём: как начать работу с jest, как написать простой тест, и какие есть методы для сопоставления проверяемых значение с ожидаемыми. Кому интересно — добро пожаловать под кат!
Что такое Jest?
Как указано на домашней странице проекта:
Jest — это восхитительная среда тестирования JavaScript с упором на простоту.
И действительно, Jest очень простой. Он не требует дополнительных настроек, легкий в понимании и применении, а так же имеет довольно хорошую документацию. Отлично подходит для проектов использующих Node, React, Angular, Vue, Babel, TypeScript и не только.
Также он имеет открытый исходный код и поддерживается компанией Facebook.
Установка
Для установки Jest в ваш проект выполните:
npm install --save-dev jest
Если вы используете yarn:
yarn add --dev jest
После установки можете обновить секцию scripts вашего package.json:
“scripts” :
С помощью такого простого вызова мы уже можем запустить наши тесты (на самом деле jest потребует существование хотя бы одного теста).
Также можно установить глобально (но так делать я бы не рекомендовал, так как по мне глобальная установка модулей является плохой практикой):
npm install jest --global
И соответственно для yarn:
yarn global add jest
После этого вы можете использовать jest непосредственно из командной строки.
При помощи вызова команды jest —init в корне проекта, ответив на несколько вопросов, вы получите файл с настройками jest.config.js. Или можно добавить конфигурацию прямиком в ваш package.json. Для этого добавьте в корень json ключ «jest» и в соответствующем ему объекте можете добавлять необходимые вам настройки. Сами опции мы разберем позже. На данном этапе в этом нет необходимости, поскольку jest можно использовать «сходу», без дополнительных конфигураций.
Первый тест
Давайте создадим файл first.test.js и напишем наш первый тест:
//first.test.js test('My first test', () => < expect(Math.max(1, 5, 10)).toBe(10); >);
И запустим наши тесты с помощью npm run test или непосредственно командой jest (если он установлен глобально). После запуска мы увидим отчет о прохождении тестов.
PASS ./first.test.js ✓ My first test (1 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.618 s, estimated 1 s
Давайте «сломаем» наш тест и запустим jest повторно:
//first.test.js test('My first test', () => < expect(Math.max(1, 5, 10)).toBe(5); >);
Как мы видим, теперь наш тест не проходит проверки. Jest отображает подробную информацию о том, где возникла проблема, какой был ожидаемый результат, и что мы получили вместо него.
Теперь давайте разберём код самого теста. Функция test используется для создания нового теста. Она принимает три аргумента (в примере мы использовали вызов с двумя аргументами). Первый — строка с названием теста, его jest отобразит в отчете. Второй — функция, которая содержит логику нашего теста. Также можно использовать 3-й аргумент — таймаут. Он является не обязательным, а его значение по умолчанию составляет 5 секунд. Задаётся в миллисекундах. Этот параметр необходим когда мы работаем с асинхронным кодом и возвращаем из функции теста промис. Он указывает как долго jest должен ждать разрешения промиса. По истечению этого времени, если промис не был разрешен — jest будет считать тест не пройденным. Подробнее про работу с асинхронными вызовами будет в следующих частях. Также вместо test() можно использовать it(). Разницы между такими вызовами нету. it() это просто алиас на функцию test().
Внутри функции теста мы сначала вызываем expect(). Ему мы передаем значение, которое хотим проверить. В нашем случае, это результат вызова Math.max(1, 5, 10). expect() возвращает объект «обертку», у которой есть ряд методов для сопоставления полученного значения с ожидаемым. Один из таких методов мы и использовали — toBe.
Давайте разберем основные из этих методов:
- toBe() — подходит, если нам надо сравнивать примитивные значения или является ли переданное значение ссылкой на тот же объект, что указан как ожидаемое значение. Сравниваются значения при помощи Object.is(). В отличие от === это дает возможность отличать 0 от -0, проверить равенство NaN c NaN.
- toEqual() — подойдёт, если нам необходимо сравнить структуру более сложных типов. Он сравнит все поля переданного объекта с ожидаемым. Проверит каждый элемент массива. И сделает это рекурсивно по всей вложенности.
test('toEqual with objects', () => < expect(< foo: 'foo', subObject: < baz: 'baz' >>) .toEqual( < foo: 'foo', subObject: < baz: 'baz' >>); // Ок expect( < foo: 'foo', subObject: < num: 0 >>) .toEqual( < foo: 'foo', subObject: < baz: 'baz' >>); // А вот так ошибка. >); test('toEqual with arrays', () => < expect([11, 19, 5]).toEqual([11, 19, 5]); // Ок expect([11, 19, 5]).toEqual([11, 19]); // Ошибка >);
const arr = ['apple', 'orange', 'banana']; expect(arr).toContain('banana'); expect(new Set(arr)).toContain('banana'); expect('apple, orange, banana').toContain('banana');
expect([, ]).toContainEqual();
expect([1, 2, 3, 4]).toHaveLength(4); expect('foo').toHaveLength(3); expect(< length: 1 >).toHaveLength(1);
const num = 0.1 + 0.2; // 0.30000000000000004 expect(num).toBeCloseTo(0.3); expect(Math.PI).toBeCloseTo(3.14, 2);
expect('Banana').toMatch(/Ba/);
function funcWithError() < throw new Error('some error'); >expect(funcWithError).toThrow(); expect(funcWithError).toThrow(Error); expect(funcWithError).toThrow('some error'); expect(funcWithError).toThrow(/some/);
expect(true).not.toBe(false); expect(< foo: 'bar' >).not.toEqual(<>); function funcWithoutError() <> expect(funcWithoutError).not.toThrow();
// src/circle.js const area = (radius) => Math.PI * radius ** 2; const circumference = (radius) => 2 * Math.PI * radius; module.exports = < area, circumference >;
Далее добавим тесты:
// tests/circle.test.js const circle = require('../src/circle'); test('Circle area', () => < expect(circle.area(5)).toBeCloseTo(78.54); expect(circle.area()).toBeNaN(); >); test('Circumference', () => < expect(circle.circumference(11)).toBeCloseTo(69.1, 1); expect(circle.circumference()).toBeNaN(); >);
В этих тестах мы проверили результат работы 2-х методов — area и circumference. При помощи метода toBeCloseTo мы сверились с ожидаемым результатом. В первом случае мы проверили или вычисляемая площадь круга с радиусом 5 приблизительно равна 78.54, при этом разница с полученым значением (оно составит 78.53981633974483) не большая и тест будет засчитан. Во втором мы указали, что нас интересует проверка с точностью до 1 знака после запятой. Также мы вызвали наши методы без аргументов и проверили результат с помощью toBeNaN. Поскольку результат их выполнения будет NaN, то и тесты будут пройдены успешно.
Разберём ещё один пример. Создадим функцию, которая будет фильтровать массив продуктов по цене:
// src/productFilter.js const byPriceRange = (products, min, max) => products.filter(item => item.price >= min && item.price ;
// tests/product.test.js const productFilter = require('../src/producFilter'); const products = [ < name: 'onion', price: 12 >, < name: 'tomato', price: 26 >, < name: 'banana', price: 29 >, < name: 'orange', price: 38 >]; test('Test product filter by range', () => < const FROM = 15; const TO = 30; const filteredProducts = productFilter.byPriceRange(products, FROM, TO); expect(filteredProducts).toHaveLength(2); expect(filteredProducts).toContainEqual(< name: 'tomato', price: 26 >); expect(filteredProducts).toEqual([< name: 'tomato', price: 26 >, < name: 'banana', price: 29 >]); expect(filteredProducts[0].price).toBeGreaterThanOrEqual(FROM); expect(filteredProducts[1].price).toBeLessThanOrEqual(TO); expect(filteredProducts).not.toContainEqual(< name: 'orange', price: 38 >); >);
В этом тесте мы проверям результат работы функии byRangePrice. Сначала мы проверили соответствие длины полученого массива ожидаемой — 2. Следующая проверка требует, чтобы в массиве находился элемент — < name: 'tomato', price: 26 >. Объект в массиве и объект переданный toContainEqual — это два разных объекта, а не ссылка на один и тот же. Но toContainEqual сверит каждое свойство. Так как оба объекта идентичные — проверка пройдет успешно. Далее мы используем toEqual для провеки структуры всего массива и его элементов. Методы toBeGreaterThanOrEqual и toBeLessThanOrEqual помогут нам проверить price первого и второго элемента массива. И, наконец, вызов not.toContainEqual сделает проверку, не содержится ли в массиве элемент — < name: 'orange', price: 38 >, которого по условию там быть не должно.
В данных примерах мы написали несколько простых тестов используя функции проверки описанные выше. В следующих частях мы разберём работу с асинхронным кодом, функции jest которые не затрагивались в этой части туториала, поговорим о его настройке и многое другое.
- JavaScript
- Тестирование веб-сервисов
Функции
Зачастую нам надо повторять одно и то же действие во многих частях программы.
Например, необходимо красиво вывести сообщение при приветствии посетителя, при выходе посетителя с сайта, ещё где-нибудь.
Чтобы не повторять один и тот же код во многих местах, придуманы функции. Функции являются основными «строительными блоками» программы.
Примеры встроенных функций вы уже видели – это alert(message) , prompt(message, default) и confirm(question) . Но можно создавать и свои.
Объявление функции
Для создания функций мы можем использовать объявление функции.
Пример объявления функции:
function showMessage()
Вначале идёт ключевое слово function , после него имя функции, затем список параметров в круглых скобках через запятую (в вышеприведённом примере он пустой) и, наконец, код функции, также называемый «телом функции», внутри фигурных скобок.
function имя(параметры)
Наша новая функция может быть вызвана по своему имени: showMessage() .
function showMessage() < alert( 'Всем привет!' ); >showMessage(); showMessage();
Вызов showMessage() выполняет код функции. Здесь мы увидим сообщение дважды.
Этот пример явно демонстрирует одно из главных предназначений функций: избавление от дублирования кода.
Если понадобится поменять сообщение или способ его вывода – достаточно изменить его в одном месте: в функции, которая его выводит.
Локальные переменные
Переменные, объявленные внутри функции, видны только внутри этой функции.
function showMessage() < let message = "Привет, я JavaScript!"; // локальная переменная alert( message ); >showMessage(); // Привет, я JavaScript! alert( message ); //
Внешние переменные
У функции есть доступ к внешним переменным, например:
let userName = 'Вася'; function showMessage() < let message = 'Привет, ' + userName; alert(message); >showMessage(); // Привет, Вася
Функция обладает полным доступом к внешним переменным и может изменять их значение.
let userName = 'Вася'; function showMessage() < userName = "Петя"; // (1) изменяем значение внешней переменной let message = 'Привет, ' + userName; alert(message); >alert( userName ); // Вася перед вызовом функции showMessage(); alert( userName ); // Петя, значение внешней переменной было изменено функцией
Внешняя переменная используется, только если внутри функции нет такой локальной.
Если одноимённая переменная объявляется внутри функции, тогда она перекрывает внешнюю. Например, в коде ниже функция использует локальную переменную userName . Внешняя будет проигнорирована:
let userName = 'Вася'; function showMessage() < let userName = "Петя"; // объявляем локальную переменную let message = 'Привет, ' + userName; // Петя alert(message); >// функция создаст и будет использовать свою собственную локальную переменную userName showMessage(); alert( userName ); // Вася, не изменилась, функция не трогала внешнюю переменную
Глобальные переменные
Переменные, объявленные снаружи всех функций, такие как внешняя переменная userName в вышеприведённом коде – называются глобальными.
Глобальные переменные видимы для любой функции (если только их не перекрывают одноимённые локальные переменные).
Желательно сводить использование глобальных переменных к минимуму. В современном коде обычно мало или совсем нет глобальных переменных. Хотя они иногда полезны для хранения важнейших «общепроектовых» данных.
Параметры
Мы можем передать внутрь функции любую информацию, используя параметры.
В нижеприведённом примере функции передаются два параметра: from и text .
function showMessage(from, text) < // параметры: from, text alert(from + ': ' + text); >showMessage('Аня', 'Привет!'); // Аня: Привет! (*) showMessage('Аня', "Как дела?"); // Аня: Как дела? (**)
Когда функция вызывается в строках (*) и (**) , переданные значения копируются в локальные переменные from и text . Затем они используются в теле функции.
Вот ещё один пример: у нас есть переменная from , и мы передаём её функции. Обратите внимание: функция изменяет значение from , но это изменение не видно снаружи. Функция всегда получает только копию значения:
function showMessage(from, text) < from = '*' + from + '*'; // немного украсим "from" alert( from + ': ' + text ); >let from = "Аня"; showMessage(from, "Привет"); // *Аня*: Привет // значение "from" осталось прежним, функция изменила значение локальной переменной alert( from ); // Аня
Значение, передаваемое в качестве параметра функции, также называется аргументом.
- Параметр – это переменная, указанная в круглых скобках в объявлении функции.
- Аргумент – это значение, которое передаётся функции при её вызове.
Мы объявляем функции со списком параметров, затем вызываем их, передавая аргументы.
Рассматривая приведённый выше пример, мы могли бы сказать: "функция showMessage объявляется с двумя параметрами, затем вызывается с двумя аргументами: from и "Привет" ".
Значения по умолчанию
Если при вызове функции аргумент не был указан, то его значением становится undefined .
Например, вышеупомянутая функция showMessage(from, text) может быть вызвана с одним аргументом:
showMessage("Аня");
Это не приведёт к ошибке. Такой вызов выведет "*Аня*: undefined" . В вызове не указан параметр text , поэтому предполагается, что text === undefined .
Если мы хотим задать параметру text значение по умолчанию, мы должны указать его после = :
function showMessage(from, text = "текст не добавлен") < alert( from + ": " + text ); >showMessage("Аня"); // Аня: текст не добавлен
Теперь, если параметр text не указан, его значением будет "текст не добавлен"
В данном случае "текст не добавлен" это строка, но на её месте могло бы быть и более сложное выражение, которое бы вычислялось и присваивалось при отсутствии параметра. Например:
function showMessage(from, text = anotherFunction()) < // anotherFunction() выполнится только если не передан text // результатом будет значение text >
Вычисление параметров по умолчанию
В JavaScript параметры по умолчанию вычисляются каждый раз, когда функция вызывается без соответствующего параметра.
В приведённом выше примере, функция anotherFunction() не будет вызвана вообще, если указан параметр text .
С другой стороны, функция будет независимо вызываться каждый раз, когда text отсутствует.
Использование параметров по умолчанию в ранних версиях JavaScript
Ранние версии JavaScript не поддерживали параметры по умолчанию. Поэтому существуют альтернативные способы, которые могут встречаться в старых скриптах.
Например, явная проверка на undefined :
function showMessage(from, text) < if (text === undefined) < text = 'текст не добавлен'; >alert( from + ": " + text ); >
…Или с помощью оператора || :
function showMessage(from, text) < // Если значение text ложно, тогда присвоить параметру text значение по умолчанию // заметим, что при этом пустая строка text === "" будет также считаться отсутствующим значением text = text || 'текст не добавлен'; . >
Альтернативные параметры по умолчанию
Иногда имеет смысл присваивать значения по умолчанию для параметров не в объявлении функции, а на более позднем этапе.
Во время выполнения функции мы можем проверить, передан ли параметр, сравнив его с undefined :
function showMessage(text) < // . if (text === undefined) < // если параметр отсутствует text = 'пустое сообщение'; >alert(text); > showMessage(); // пустое сообщение
…Или мы можем использовать оператор || :
function showMessage(text) < // если значение text ложно или равняется undefined, тогда присвоить text значение 'пусто' text = text || 'пусто'; . >
Современные движки JavaScript поддерживают оператор нулевого слияния ?? . Его использование будет лучшей практикой, в случае, если большинство ложных значений, таких как 0 , следует расценивать как «нормальные».
function showCount(count) < // если count равен undefined или null, показать "неизвестно" alert(count ?? "неизвестно"); >showCount(0); // 0 showCount(null); // неизвестно showCount(); // неизвестно
Возврат значения
Функция может вернуть результат, который будет передан в вызвавший её код.
Простейшим примером может служить функция сложения двух чисел:
function sum(a, b) < return a + b; >let result = sum(1, 2); alert( result ); // 3
Директива return может находиться в любом месте тела функции. Как только выполнение доходит до этого места, функция останавливается, и значение возвращается в вызвавший её код (присваивается переменной result выше).
Вызовов return может быть несколько, например:
function checkAge(age) < if (age >= 18) < return true; >else < return confirm('А родители разрешили?'); >> let age = prompt('Сколько вам лет?', 18); if ( checkAge(age) ) < alert( 'Доступ получен' ); >else
Возможно использовать return и без значения. Это приведёт к немедленному выходу из функции.
function showMovie(age) < if ( !checkAge(age) ) < return; >alert( "Вам показывается кино" ); // (*) // . >
В коде выше, если checkAge(age) вернёт false , showMovie не выполнит alert .
Результат функции с пустым return или без него – undefined
Если функция не возвращает значения, это всё равно, как если бы она возвращала undefined :
function doNothing() < /* пусто */ >alert( doNothing() === undefined ); // true
Пустой return аналогичен return undefined :
function doNothing() < return; >alert( doNothing() === undefined ); // true
Никогда не добавляйте перевод строки между return и его значением
Для длинного выражения в return может быть заманчиво разместить его на нескольких отдельных строках, например так:
return (some + long + expression + or + whatever * f(a) + f(b))
Код не выполнится, потому что интерпретатор JavaScript подставит точку с запятой после return . Для него это будет выглядеть так:
return; (some + long + expression + or + whatever * f(a) + f(b))
Таким образом, это фактически стало пустым return .
Если мы хотим, чтобы возвращаемое выражение занимало несколько строк, нужно начать его на той же строке, что и return . Или, хотя бы, поставить там открывающую скобку, вот так:
return ( some + long + expression + or + whatever * f(a) + f(b) )
И тогда всё сработает, как задумано.
Выбор имени функции
Функция – это действие. Поэтому имя функции обычно является глаголом. Оно должно быть кратким, точным и описывать действие функции, чтобы программист, который будет читать код, получил верное представление о том, что делает функция.
Как правило, используются глагольные префиксы, обозначающие общий характер действия, после которых следует уточнение. Обычно в командах разработчиков действуют соглашения, касающиеся значений этих префиксов.
Например, функции, начинающиеся с "show" обычно что-то показывают.
Функции, начинающиеся с…
- "get…" – возвращают значение,
- "calc…" – что-то вычисляют,
- "create…" – что-то создают,
- "check…" – что-то проверяют и возвращают логическое значение, и т.д.
Примеры таких имён:
showMessage(..) // показывает сообщение getAge(..) // возвращает возраст (получая его каким-то образом) calcSum(..) // вычисляет сумму и возвращает результат createForm(..) // создаёт форму (и обычно возвращает её) checkPermission(..) // проверяет доступ, возвращая true/false
Благодаря префиксам, при первом взгляде на имя функции становится понятным, что делает её код, и какое значение она может возвращать.
Одна функция – одно действие
Функция должна делать только то, что явно подразумевается её названием. И это должно быть одним действием.
Два независимых действия обычно подразумевают две функции, даже если предполагается, что они будут вызываться вместе (в этом случае мы можем создать третью функцию, которая будет их вызывать).
Несколько примеров, которые нарушают это правило:
- getAge – будет плохим выбором, если функция будет выводить alert с возрастом (должна только возвращать его).
- createForm – будет плохим выбором, если функция будет изменять документ, добавляя форму в него (должна только создавать форму и возвращать её).
- checkPermission – будет плохим выбором, если функция будет отображать сообщение с текстом доступ разрешён/запрещён (должна только выполнять проверку и возвращать её результат).
В этих примерах использовались общепринятые смыслы префиксов. Конечно, вы в команде можете договориться о других значениях, но обычно они мало отличаются от общепринятых. В любом случае вы и ваша команда должны чётко понимать, что значит префикс, что функция с ним может делать, а чего не может.
Сверхкороткие имена функций
Имена функций, которые используются очень часто, иногда делают сверхкороткими.
Например, фреймворк jQuery определяет функцию с помощью $ . В библиотеке Lodash основная функция представлена именем _ .
Это исключения. В основном имена функций должны быть в меру краткими и описательными.
Функции == Комментарии
Функции должны быть короткими и делать только что-то одно. Если это что-то большое, имеет смысл разбить функцию на несколько меньших. Иногда следовать этому правилу непросто, но это определённо хорошее правило.
Небольшие функции не только облегчают тестирование и отладку – само существование таких функций выполняет роль хороших комментариев!
Например, сравним ниже две функции showPrimes(n) . Каждая из них выводит простое число до n .
Первый вариант использует метку nextPrime :
function showPrimes(n) < nextPrime: for (let i = 2; i < n; i++) < for (let j = 2; j < i; j++) < if (i % j == 0) continue nextPrime; >alert( i ); // простое > >
Второй вариант использует дополнительную функцию isPrime(n) для проверки на простое:
function showPrimes(n) < for (let i = 2; i < n; i++) < if (!isPrime(i)) continue; alert(i); // простое >> function isPrime(n) < for (let i = 2; i < n; i++) < if ( n % i == 0) return false; >return true; >
Второй вариант легче для понимания, не правда ли? Вместо куска кода мы видим название действия ( isPrime ). Иногда разработчики называют такой код самодокументируемым.
Таким образом, допустимо создавать функции, даже если мы не планируем повторно использовать их. Такие функции структурируют код и делают его более понятным.
Итого
Объявление функции имеет вид:
function имя(параметры, через, запятую) < /* тело, код функции */ >
- Передаваемые значения копируются в параметры функции и становятся локальными переменными.
- Функции имеют доступ к внешним переменным. Но это работает только изнутри наружу. Код вне функции не имеет доступа к её локальным переменным.
- Функция может возвращать значение. Если этого не происходит, тогда результат равен undefined .
Для того, чтобы сделать код более чистым и понятным, рекомендуется использовать локальные переменные и параметры функций, не пользоваться внешними переменными.
Функция, которая получает параметры, работает с ними и затем возвращает результат, гораздо понятнее функции, вызываемой без параметров, но изменяющей внешние переменные, что чревато побочными эффектами.
- Имя функции должно понятно и чётко отражать, что она делает. Увидев её вызов в коде, вы должны тут же понимать, что она делает, и что возвращает.
- Функция – это действие, поэтому её имя обычно является глаголом.
- Есть много общепринятых префиксов, таких как: create… , show… , get… , check… и т.д. Пользуйтесь ими как подсказками, поясняющими, что делает функция.
Функции являются основными строительными блоками скриптов. Мы рассмотрели лишь основы функций в JavaScript, но уже сейчас можем создавать и использовать их. Это только начало пути. Мы будем неоднократно возвращаться к функциям и изучать их всё более и более глубоко.
Задачи
Обязателен ли "else"?
важность: 4
Следующая функция возвращает true , если параметр age больше 18 .
В ином случае она запрашивает подтверждение через confirm и возвращает его результат:
function checkAge(age) < if (age >18) < return true; >else < // . return confirm('Родители разрешили?'); >>
Будет ли эта функция работать как-то иначе, если убрать else ?
function checkAge(age) < if (age >18) < return true; >// . return confirm('Родители разрешили?'); >
Есть ли хоть одно отличие в поведении этого варианта?
Оба варианта функций работают одинаково, отличий нет.
Перепишите функцию, используя оператор '?' или '||'
важность: 4
Следующая функция возвращает true , если параметр age больше 18 .
В ином случае она задаёт вопрос confirm и возвращает его результат.
function checkAge(age) < if (age >18) < return true; >else < return confirm('Родители разрешили?'); >>
Перепишите функцию, чтобы она делала то же самое, но без if , в одну строку.
Сделайте два варианта функции checkAge :
- Используя оператор ?
- Используя оператор ||
function checkAge(age) < return (age >18) ? true : confirm('Родители разрешили?'); >
Используя оператор || (самый короткий вариант):
function checkAge(age) < return (age >18) || confirm('Родители разрешили?'); >
Обратите внимание, что круглые скобки вокруг age > 18 не обязательны. Они здесь для лучшей читаемости кода.
Методы RegExp и String
В этой главе мы рассмотрим все детали методов для работы с регулярными выражениями.
str.match(regexp)
Метод str.match(regexp) ищет совпадения с regexp в строке str .
У него есть три режима работы:
-
Если у регулярного выражения нет флага g , то он возвращает первое совпадение в виде массива со скобочными группами и свойствами index (позиция совпадения), input (строка поиска, равна str ):
let str = "I love JavaScript"; let result = str.match(/Java(Script)/); alert( result[0] ); // JavaScript (всё совпадение) alert( result[1] ); // Script (первые скобки) alert( result.length ); // 2 // Дополнительная информация: alert( result.index ); // 7 (позиция совпадения) alert( result.input ); // I love JavaScript (исходная строка)
let str = "I love JavaScript"; let result = str.match(/Java(Script)/g); alert( result[0] ); // JavaScript alert( result.length ); // 1
let str = "I love JavaScript"; let result = str.match(/HTML/); alert(result); // null alert(result.length); // Ошибка: у null нет свойства length
Если хочется, чтобы результатом всегда был массив, можно написать так:
let result = str.match(regexp) || [];
str.matchAll(regexp)
Новая возможность
Эта возможность была добавлена в язык недавно. В старых браузерах может понадобиться полифил.
Метод str.matchAll(regexp) – «новый, улучшенный» вариант метода str.match .
Он используется, в первую очередь, для поиска всех совпадений вместе со скобочными группами.
У него 3 отличия от match :
- Он возвращает не массив, а перебираемый объект с результатами, обычный массив можно сделать при помощи Array.from .
- Каждое совпадение возвращается в виде массива со скобочными группами (как str.match без флага g ).
- Если совпадений нет, то возвращается не null , а пустой перебираемый объект.
let str = 'Hello, world!
'; let regexp = /<(.*?)>/g; let matchAll = str.matchAll(regexp); alert(matchAll); // [object RegExp String Iterator], не массив, а перебираемый объект matchAll = Array.from(matchAll); // теперь массив let firstMatch = matchAll[0]; alert( firstMatch[0] ); // alert( firstMatch[1] ); // h1 alert( firstMatch.index ); // 0 alert( firstMatch.input ); // Hello, world!
При переборе результатов matchAll в цикле for..of вызов Array.from , разумеется, не нужен.
str.split(regexp|substr, limit)
Разбивает строку в массив по разделителю – регулярному выражению regexp или подстроке substr.
Обычно мы используем метод split со строками, вот так:
alert('12-34-56'.split('-')) // массив [12, 34, 56]
Но мы можем разделить по регулярному выражению аналогичным образом:
alert('12, 34, 56'.split(/,\s*/)) // массив [12, 34, 56]
str.search(regexp)
Метод str.search(regexp) возвращает позицию первого совпадения с regexp в строке str или -1 , если совпадения нет.
let str = "Я люблю JavaScript!"; let regexp = /Java.+/; alert( str.search(regexp) ); // 8
Важное ограничение: str.search умеет возвращать только позицию первого совпадения.
Если нужны позиции других совпадений, то следует использовать другой метод, например, найти их все при помощи str.matchAll(regexp) .
str.replace(str|regexp, str|func)
Это универсальный метод поиска-и-замены, один из самых полезных. Этакий швейцарский армейский нож для поиска и замены в строке.
Мы можем использовать его и без регулярных выражений, для поиска-и-замены подстроки:
// заменить тире двоеточием alert('12-34-56'.replace("-", ":")) // 12:34-56
Хотя есть подводный камень.
Когда первый аргумент replace является строкой, он заменяет только первое совпадение.
Вы можете видеть это в приведённом выше примере: только первый "-" заменяется на ":" .
Чтобы найти все дефисы, нам нужно использовать не строку "-" , а регулярное выражение /-/g с обязательным флагом g :
// заменить все тире двоеточием alert( '12-34-56'.replace( /-/g, ":" )) // 12:34:56
Второй аргумент – строка замены. Мы можем использовать специальные символы в нем:
| Спецсимволы | Действие в строке замены |
|---|---|
| $& | вставляет всё найденное совпадение |
| $` | вставляет часть строки до совпадения |
| $' | вставляет часть строки после совпадения |
| $n | если n это 1-2 значное число, то вставляет содержимое n-й скобки (см. главу Скобочные группы) |
| $ | вставляет содержимое скобки с указанным name (см. главу Скобочные группы) |
| $$ | вставляет "$" |
let str = "John Smith"; // поменять местами имя и фамилию alert(str.replace(/(\w+) (\w+)/i, '$2, $1')) // Smith, John
Для ситуаций, которые требуют «умных» замен, вторым аргументом может быть функция.
Она будет вызываться для каждого совпадения, и её результат будет вставлен в качестве замены.
Функция вызывается с аргументами func(match, p1, p2, . pn, offset, input, groups) :
- match – найденное совпадение,
- p1, p2, . pn – содержимое скобок (см. главу Скобочные группы).
- offset – позиция, на которой найдено совпадение,
- input – исходная строка,
- groups – объект с содержимым именованных скобок (см. главу Скобочные группы).
Если скобок в регулярном выражении нет, то будет только 3 аргумента: func(match, offset, input) .
Например, переведём выбранные совпадения в верхний регистр:
let str = "html and css"; let result = str.replace(/html|css/gi, str => str.toUpperCase()); alert(result); // HTML and CSS
Заменим каждое совпадение на его позицию в строке:
alert("Хо-Хо-хо".replace(/хо/gi, (match, offset) => offset)); // 0-3-6
В примере ниже две скобки, поэтому функция замены вызывается с 5-ю аргументами: первый – всё совпадение, затем два аргумента содержимое скобок, затем (в примере не используются) индекс совпадения и исходная строка:
let str = "John Smith"; let result = str.replace(/(\w+) (\w+)/, (match, name, surname) => `$, $`); alert(result); // Smith, John
Если в регулярном выражении много скобочных групп, то бывает удобно использовать остаточные аргументы для обращения к ним:
let str = "John Smith"; let result = str.replace(/(\w+) (\w+)/, (. match) => `$, $`); alert(result); // Smith, John
Или, если мы используем именованные группы, то объект groups с ними всегда идёт последним, так что можно получить его так:
let str = "John Smith"; let result = str.replace(/(?\w+) (?\w+)/, (. match) => < let groups = match.pop(); return `$, $`; >); alert(result); // Smith, John
Использование функции даёт нам максимальные возможности по замене, потому что функция получает всю информацию о совпадении, имеет доступ к внешним переменным и может делать всё что угодно.
regexp.exec(str)
Метод regexp.exec(str) ищет совпадение с regexp в строке str . В отличие от предыдущих методов, вызывается на регулярном выражении, а не на строке.
Он ведёт себя по-разному в зависимости от того, имеет ли регулярное выражение флаг g .
Если нет g , то regexp.exec(str) возвращает первое совпадение в точности как str.match(regexp) . Такое поведение не даёт нам ничего нового.
Но если есть g , то:
- Вызов regexp.exec(str) возвращает первое совпадение и запоминает позицию после него в свойстве regexp.lastIndex .
- Следующий такой вызов начинает поиск с позиции regexp.lastIndex , возвращает следующее совпадение и запоминает позицию после него в regexp.lastIndex .
- …И так далее.
- Если совпадений больше нет, то regexp.exec возвращает null , а для regexp.lastIndex устанавливается значение 0 .
Таким образом, повторные вызовы возвращают одно за другим все совпадения, используя свойство regexp.lastIndex для отслеживания текущей позиции поиска.
В прошлом, до появления метода str.matchAll в JavaScript, вызов regexp.exec использовали для получения всех совпадений с их позициями и группами скобок в цикле:
let str = 'Больше о JavaScript на https://javascript.info'; let regexp = /javascript/ig; let result; while (result = regexp.exec(str)) < alert( `Найдено $на позиции $` ); // Найдено JavaScript на позиции 9, затем // Найдено javascript на позиции 31 >
Это работает и сейчас, хотя для современных браузеров str.matchAll , как правило, удобнее.
Мы можем использовать regexp.exec для поиска совпадения, начиная с нужной позиции, если вручную поставим lastIndex .
let str = 'Hello, world!'; let regexp = /\w+/g; // без флага g свойство lastIndex игнорируется regexp.lastIndex = 5; // ищем с 5-й позиции (т.е с запятой и далее) alert( regexp.exec(str) ); // world
Если у регулярного выражения стоит флаг y , то поиск будет вестись не начиная с позиции regexp.lastIndex , а только на этой позиции (не далее в тексте).
В примере выше заменим флаг g на y . Ничего найдено не будет, поскольку именно на позиции 5 слова нет:
let str = 'Hello, world!'; let regexp = /\w+/y; regexp.lastIndex = 5; // ищем ровно на 5-й позиции alert( regexp.exec(str) ); // null
Это удобно в тех ситуациях, когда мы хотим «прочитать» что-то из строки по регулярному выражению именно на конкретной позиции, а не где-то далее.
regexp.test(str)
Метод regexp.test(str) ищет совпадение и возвращает true/false , в зависимости от того, находит ли он его.
let str = "Я люблю JavaScript"; // эти два теста делают одно и то же alert( /люблю/i.test(str) ); // true alert( str.search(/люблю/i) != -1 ); // true
Пример с отрицательным ответом:
let str = "Ля-ля-ля"; alert( /люблю/i.test(str) ); // false alert( str.search(/люблю/i) != -1 ); // false
Если регулярное выражение имеет флаг g , то regexp.test ищет, начиная с regexp.lastIndex и обновляет это свойство, аналогично regexp.exec .
Таким образом, мы можем использовать его для поиска с заданной позиции:
let regexp = /люблю/gi; let str = "Я люблю JavaScript"; // начать поиск с 10-й позиции: regexp.lastIndex = 10; alert( regexp.test(str) ); // false (совпадений нет)
Одно и то же регулярное выражение, использованное повторно на другом тексте, может дать другой результат
Если мы применяем одно и то же регулярное выражение последовательно к разным строкам, это может привести к неверному результату, поскольку вызов regexp.test обновляет свойство regexp.lastIndex , поэтому поиск в новой строке может начаться с ненулевой позиции.
Например, здесь мы дважды вызываем regexp.test для одного и того же текста, и второй раз поиск завершается уже неудачно:
let regexp = /javascript/g; // (regexp только что создан: regexp.lastIndex=0) alert( regexp.test("javascript") ); // true (теперь regexp.lastIndex=10) alert( regexp.test("javascript") ); // false
Это именно потому, что во втором тесте regexp.lastIndex не равен нулю.
Чтобы обойти это, можно присвоить regexp.lastIndex = 0 перед новым поиском. Или вместо методов на регулярном выражении вызывать методы строк str.match/search/. , они не используют lastIndex .
Оператор нулевого слияния (??)
Оператор нулевого слияния ( ?? ) — это логический оператор, возвращающий значение правого операнда, если значение левого операнда содержит null или undefined , в противном случае возвращается значение левого операнда.
Этот оператор можно рассматривать как частный случай логического оператора ИЛИ ( || ), который возвращает правый операнд, если в левом операнде хранится любое ложноподобное значение, а не только null или undefined . Другими словами, если вы используете оператор || для присваивания значения по умолчанию другой переменной foo , вы можете столкнуться с неожиданным поведением, если считаете некоторые ложноподобные значения пригодными для использования (например, '' или 0 ). Ниже вы увидите примеры.
Оператор нулевого слияния вместе с || находится на четвёртом по приоритетности месте, непосредственно перед ним следует условный (тернарный) оператор.
Интерактивный пример
Синтаксис
?? rightExpr;
Примеры
Использование оператора нулевого слияния
В этом примере показано как устанавливать значения по умолчанию, отличные от null или undefined .
const nullValue = null; const emptyText = ""; // ложноподобное const someNumber = 42; const valA = nullValue ?? "значение для A"; const valB = emptyText ?? "значение для B"; const valC = someNumber ?? 0; console.log(valA); // "значение для A" console.log(valB); // "" (поскольку пустая строка не приравнивается к null или undefined) console.log(valC); // 42
Присваивание переменной значения по умолчанию
Ранее, когда нужно было присвоить переменной значение по умолчанию, часто использовался оператор ИЛИ ( || ):
let foo; // foo ещё не было присвоено значение, поэтому оно undefined let someDummyText = foo || "Hello!";
Однако поскольку || является логическим оператором, левый операнд был принудительно приведен к булеву значению, игнорируя любое ложноподобное значение ( 0 , '' , NaN , null , undefined ). Это может привести к некорректной работе кода, если нужно учитывать такие значения, как 0 , '' , или NaN .
let count = 0; let text = ""; let qty = count || 42; let message = text || "hi!"; console.log(qty); // 42, но не 0 console.log(message); // "hi!", но не ""
Оператор нулевого слияния предотвращает попадание в такую ловушку, возвращая второй операнд, только если первый равен null или undefined (и не любым другим ложноподобным значением):
let myText = ""; // Пустая строка (ложноподобное значение) let notFalsyText = myText || "Привет, мир"; console.log(notFalsyText); // Привет, мир let preservingFalsy = myText ?? "Привет, сосед"; console.log(preservingFalsy); // '' (т.к. myText не ни undefined, ни null)
Сокращённое вычисление
Так же как при использовании операторов логического И и ИЛИ, правое выражение не вычисляется, если значение в левом выражении не равняется null или undefined
function A() console.log("вызвана A"); return undefined; > function B() console.log("вызвана B"); return false; > function C() console.log("вызвана C"); return "foo"; > console.log(A() ?? C()); // в консоли будет будет выведено "вызвана A", после "вызвана C", и затем "foo", // т.к. A() возвращает undefined, поэтому оба выражения были вычислены console.log(B() ?? C()); // в консоли будет выведено "вызвана B", затем "false", // так B() возвращает false (не null или undefined), то // правое выражение не вычисляется
Использование вместе с операторами И или ИЛИ
Не получится использовать оператор И ( && ) и ИЛИ ( || ) вместе с оператором ?? в одном выражении. В таких случаях будет вызвана ошибка SyntaxError .
null || undefined ?? "foo"; // вызывает SyntaxError true || undefined ?? "foo"; // вызывает SyntaxError
Но если явно указать приоритет, создав подвыражение через круглые скобки, то всё заработает:
(null || undefined) ?? "foo"; // вернёт "foo"
Использование совместно с оператором опциональной последовательности ( ?. )
Оператор нулевого слияния учитывает значения undefined и null , по аналогии как и оператор опциональной последовательности ( ?. )(/ru/docs/Web/JavaScript/Reference/Operators/Optional_chaining), который позволяет избежать ошибок при обращении к свойству объекта, которое может быть null или undefined .
let foo = someFooProp: "привет" >; console.log(foo.someFooProp?.toUpperCase() ?? "нет значения"); // "ПРИВЕТ" console.log(foo.someBarProp?.toUpperCase() ?? "нет значения"); // "нет значения"
Спецификации
| Specification |
|---|
| ECMAScript Language Specification # prod-CoalesceExpression |
Совместимость с браузерами
BCD tables only load in the browser
Смотрите также
- Оператор опциональной последовательности
- Оператор логического ИЛИ ( || )
- Параметры по умолчанию функций
Found a content problem with this page?
- Edit the page on GitHub.
- Report the content issue.
- View the source on GitHub.
This page was last modified on 6 янв. 2024 г. by MDN contributors.
Your blueprint for a better internet.
MDN
Support
- Product help
- Report an issue
Our communities
Developers
- Web Technologies
- Learn Web Development
- MDN Plus
- Hacks Blog
- Website Privacy Notice
- Cookies
- Legal
- Community Participation Guidelines
Visit Mozilla Corporation’s not-for-profit parent, the Mozilla Foundation.
Portions of this content are ©1998– 2024 by individual mozilla.org contributors. Content available under a Creative Commons license.