Как создать бесконечный цикл в JavaScript? Я пытаюсь сделать слайд-шоу, которое у меня работает, но я не могу заставить его зацикливаться. Я даже не могу его зацикливать дважды.
Без этого, он проходит один раз. Когда я вставляю, он либо заставляет Firefox блокироваться, либо просто циклы один раз. Я уверен, что это действительно простая вещь, и даже если она должна быть петлей 1 000 000 раз или что-то вместо бесконечного, это будет хорошо работать для меня.
Кроме того, я не хочу использовать jQuery или что-то, что кто-то создал. Я изучаю JavaScript, и это частично помогает мне учиться, и частично потому, что я пытаюсь сделать так много систем на базе HTML5, насколько смогу.
EDIT: Я думаю, что причина его замораживания заключается в том, что он выполняет код сразу, а затем просто хранит его в кеше или что-то в этом роде. То, что я хочу сделать, это пройти через это один раз, а затем снова начать сверху, и это то, что я всегда думал о том, где. В сценарии "пакетная" (командная строка) это можно сделать с помощью команды " GOTO
". Я не знаю, есть ли в JS эквивалент или нет, но это действительно моя цель.
Ответ 8
В продолжение ответа Ender, давайте рассмотрим наши варианты с улучшениями от ES2015.
Во-первых, проблема в коде asker заключается в том, что setTimeout
является асинхронным, а циклы - синхронными. Таким образом, логический недостаток заключается в том, что они записали несколько вызовов асинхронной функции из синхронного цикла, ожидая, что они будут выполняться синхронно.
function slide() {
var num = 0;
for (num=0;num<=10;num++) {
setTimeout("document.getElementById('container').style.marginLeft='-600px'",3000);
setTimeout("document.getElementById('container').style.marginLeft='-1200px'",6000);
setTimeout("document.getElementById('container').style.marginLeft='-1800px'",9000);
setTimeout("document.getElementById('container').style.marginLeft='0px'",12000);
}
}
В действительности же происходит то, что...
- Цикл "одновременно" создает 44 асинхронных тайм-аута, настроенных на выполнение 3, 6, 9 и 12 секунд в будущем. Аскер ожидал, что 44 вызова будут выполняться один за другим, но вместо этого все они будут выполняться одновременно.
- Через 3 секунды после завершения цикла
container
marginLeft устанавливается в "-600px"
11 раз. - Через 3 секунды после этого для marginLeft устанавливается значение
"-1200px"
11 раз. - 3 секунды спустя,
"-1800px"
, 11 раз.
И так далее.
Вы можете решить это, изменив его на:
function setMargin(margin){
return function(){
document.querySelector("#container").style.marginLeft = margin;
};
}
function slide() {
for (let num = 0; num <= 10; ++num) {
setTimeout(setMargin("-600px"), + (3000 * (num + 1)));
setTimeout(setMargin("-1200px"), + (6000 * (num + 1)));
setTimeout(setMargin("-1800px"), + (9000 * (num + 1)));
setTimeout(setMargin("0px"), + (12000 * (num + 1)));
}
}
Но это просто ленивое решение, которое не решает другие проблемы с этой реализацией. Здесь много жесткого кодирования и общего разгильдяйства, которое должно быть исправлено.
Уроки, извлеченные из десятилетнего опыта
Как упоминалось в верхней части этого ответа, Эндер уже предложил решение, но я хотел бы добавить к нему, чтобы учесть передовой опыт и современные инновации в спецификации ECMAScript.
function format(str, ...args){
return str.split(/(%)/).map(part => (part == "%") ? (args.shift()) : (part)).join("");
}
function slideLoop(margin, selector){
const multiplier = -600;
let contStyle = document.querySelector(selector).style;
return function(){
margin = ++margin % 4;
contStyle.marginLeft = format("%px", margin * multiplier);
}
}
function slide() {
return setInterval(slideLoop(0, "#container"), 3000);
}
Давайте рассмотрим, как это работает для начинающих (обратите внимание, что не все это напрямую связано с вопросом):
формат
function format
Чрезвычайно полезно иметь функцию форматирования строк, похожую на printf, на любом языке. Я не понимаю, почему в JavaScript его нет.
format(str, ...args)
...
это шикарная функция, добавленная в ES6, которая позволяет вам делать множество вещей. Я считаю, что это называется оператором спреда. Синтаксис: ...identifier
или ...array
. В заголовке функции вы можете использовать его для указания аргументов переменных, и он будет принимать каждый аргумент в позиции и после позиции указанного аргумента переменной и помещать их в массив. Вы также можете вызвать функцию с массивом, например, так: args = [1, 2, 3]; i_take_3_args(...args)
args = [1, 2, 3]; i_take_3_args(...args)
, или вы можете взять похожий на массив объект и преобразовать его в массив: ...document.querySelectorAll("div.someclass").forEach(...)
. Это было бы невозможно без оператора распространения, потому что querySelectorAll
возвращает "список элементов", который не является истинным массивом.
str.split(/(%)/)
Я не очень хорошо объясняю, как работает регулярное выражение. JavaScript имеет два синтаксиса для регулярных выражений. Там путь OO (new RegExp("regex", "gi")
) и там буквальный путь (/insert regex here/gi
). Я испытываю глубокую ненависть к регулярным выражениям, потому что лаконичный синтаксис, который он поощряет, часто приносит больше вреда, чем пользы (а также потому, что они чрезвычайно непереносимы), но есть некоторые случаи, когда регулярное выражение полезно, как этот. Обычно, если вы вызываете split с помощью "%"
или /%/
, результирующий массив исключает разделители "%" из массива. Но для алгоритма, используемого здесь, мы должны включить их. /(%)/
было первое, что я попробовал, и это сработало. Я думаю, мне повезло.
.map(...)
map
является функциональной идиомой. Вы используете карту, чтобы применить функцию к списку. Синтаксис: array.map(function)
. Функция: должна возвращать значение и принимать 1- 2 аргумента. Первый аргумент будет использоваться для хранения каждого значения в массиве, а второй будет использоваться для хранения текущего индекса в массиве. Пример: [1,2,3,4,5].map(x => x * x);//returns [1,4,9,16,25]
[1,2,3,4,5].map(x => x * x);//returns [1,4,9,16,25]
. Смотрите также: фильтр, поиск, уменьшение, forEach.
part => ...
Это альтернативная форма функции. Синтаксис: argument-list => return-value
, например (x, y) => (y * width + x)
, что эквивалентно function(x, y){return (y * width + x);}
.
(part == "%") ? (args.shift()) : (part)
Пара операторов ?:
- это операндный оператор 3-, называемый троичным условным оператором. Синтаксис: condition? if-true: if-false
condition? if-true: if-false
, хотя большинство людей называют его "троичным" оператором, поскольку в каждом языке, в котором он присутствует, это единственный оператор-операнд 3-, каждый другой оператор является двоичным (+, &&, |, =) или унарный (++,..., &, *). Интересный факт: некоторые языки (и расширения языков поставщиков, такие как GNU C) реализуют двухоперационную версию оператора ?:
С синтаксическим value?: fallback
, который эквивалентен value? value: fallback
value? value: fallback
, и будет использовать fallback
если value
оценивается как ложное. Они называют это оператором Элвиса.
Я должен также упомянуть разницу между expression
и expression-statement
, поскольку я понимаю, что это может быть не интуитивно понятно для всех программистов. expression
представляет значение и может быть присвоено l-value
. Выражение может быть заключено в круглые скобки и не может считаться синтаксической ошибкой. Выражение само может быть l-value
, хотя большинство операторов являются r-values
, поскольку единственными выражениями l-значения являются выражения, сформированные из идентификатора или (например, в C) из ссылки/указателя. Функции могут возвращать l-значения, но не рассчитывают на это. Выражения также могут быть составлены из других, меньших выражений. (1, 2, 3)
- это выражение, сформированное из трех выражений r-значения, соединенных двумя запятыми операторами. Значение выражения - 3. expression-statements
, с другой стороны, являются операторами, сформированными из одного выражения. ++somevar
является выражением, поскольку его можно использовать как значение r в выражении-выражении присваивания newvar = ++somevar;
(значение выражения newvar = ++somevar
, например, является значением, которое присваивается newvar
). ++somevar;
также выражение-утверждение.
Если троичные операторы вообще смущают вас, примените то, что я только что сказал, к троичному оператору: expression? expression: expression
expression? expression: expression
. Тернарный оператор может образовывать выражение или выражение-оператор, поэтому обе эти вещи:
smallest = (a < b) ? (a) : (b);
(valueA < valueB) ? (backup_database()) : (nuke_atlantic_ocean());
допустимы использования оператора. Пожалуйста, не делайте последнее, хотя. Это что, if
это для. Есть случаи для такого рода вещей, например, в макросах препроцессора C, но мы говорим о JavaScript здесь.
args.shift()
Array.prototype.shift
. Это зеркальная версия pop
, якобы унаследованная от языков оболочки, где вы можете вызвать shift
для перехода к следующему аргументу. shift
"выталкивает" первый аргумент из массива и возвращает его, изменяя массив в процессе. Обратное не unshift
. Полный список:
array.shift()
[1,2,3] -> [2,3], returns 1
array.unshift(new-element)
[element, ...] -> [new-element, element, ...]
array.pop()
[1,2,3] -> [1,2], returns 3
array.push(new-element)
[..., element] -> [..., element, new-element]
Смотрите также: ломтик, сплайс
.join("")
Array.prototype.join(string)
. Эта функция превращает массив в строку. Пример: [1,2,3].join(", ") → "1, 2, 3"
горка
return setInterval(slideLoop(0, "#container"), 3000);
Во-первых, мы возвращаем возвращаемое значение setInterval
чтобы его можно было использовать позже при вызове clearInterval
. Это важно, потому что JavaScript не будет очищать это сам по себе. Я настоятельно не рекомендую использовать setTimeout
для создания цикла. Это не то, для чего предназначен setTimeout
, и, делая это, вы возвращаетесь к GOTO. Прочитайте статью Дейкстры 1968 года "Перейти к утверждению, которое считается вредным", чтобы понять, почему циклы GOTO - плохая практика.
Во-вторых, вы заметите, что я сделал некоторые вещи по-другому. Повторяющийся интервал очевиден. Это будет продолжаться вечно, пока интервал не будет очищен, и с задержкой 3000 мс. Значением для обратного вызова является возвращаемое значение другой функции, которую я "#container"
аргументам 0
и "#container"
. Это создает закрытие, и вы поймете, как это работает в ближайшее время.
slideLoop
function slideLoop(margin, selector)
Мы берем margin (0) и селектор ("#container") в качестве аргументов. Поля - это начальное значение поля, а селектор - это селектор CSS, используемый для поиска изменяемого элемента. Довольно просто.
const multiplier = -600;
let contStyle = document.querySelector(selector).style;
Я переместил некоторые из жестко закодированных элементов вверх. Поскольку поля кратны -600, у нас есть четко помеченный постоянный множитель с этим базовым значением.
Я также создал ссылку на свойство стиля элемента с помощью селектора CSS. Поскольку style
- это объект, это безопасно сделать, так как он будет рассматриваться как ссылка, а не как копия (прочитайте статью Pass By Sharing, чтобы понять эту семантику).
return function(){
margin = ++margin % 4;
contStyle.marginLeft = format("%px", margin * multiplier);
}
Теперь, когда у нас есть определенная область, мы возвращаем функцию, которая использует указанную область. Это называется закрытием. Вы должны прочитать об этом тоже. Понимание, по общему признанию, странных правил определения содержания JavaScript сделает язык намного менее болезненным в долгосрочной перспективе.
margin = ++margin % 4;
contStyle.marginLeft = format("%px", margin * multiplier);
Здесь мы просто увеличиваем поле и модулируем его на 4. Последовательность значений, которые это произведет, будет 1->2->3->0->1->...
, которая точно имитирует поведение вопрос без какой-либо сложной или жестко закодированной логики.
После этого мы используем определенную ранее функцию format
чтобы безболезненно установить CSS-свойство marginLeft контейнера. Он установил текущее значение маржи, умноженное на множитель, который, как вы помните, был установлен на -600. -600 → -1200 → -1800 → 0 → -600 ->...
Между моей версией и версией Эндера есть некоторые важные различия, о которых я упоминал в комментарии к их ответу. Я сейчас перейду к рассуждениям:
Используйте document.querySelector(css_selector)
вместо document.getElementById(id)
querySelector был добавлен в ES6, если я не ошибаюсь. querySelector (возвращает первый найденный элемент) и querySelectorAll (возвращает список всех найденных элементов) являются частью цепочки прототипов всех элементов DOM (не только document
) и используют селектор CSS, поэтому есть другие способы найти элемент, кроме просто по его идентификатору. Вы можете выполнять поиск по идентификатору (#idname
), классу (.classname
), отношениям (div.container div div span
, p:nth-child(even)
) и атрибутам (div[name]
, a[href=https://google.com]
), между прочим.
Всегда отслеживайте возвращаемое значение setInterval(fn, interval)
чтобы впоследствии его можно было закрыть с помощью clearInterval(interval_id)
Это не хороший дизайн, чтобы оставить интервал, работающий навсегда. Это также не очень хороший дизайн, чтобы написать функцию, которая вызывает себя через setTimeout
. Это ничем не отличается от цикла GOTO. Возвращаемое значение setInterval
должно быть сохранено и использовано для очистки интервала, когда он больше не нужен. Думайте об этом как о форме управления памятью.
Поместите интервальный обратный вызов в его собственную формальную функцию для удобочитаемости и удобства обслуживания.
Конструкции как это
setInterval(function(){
...
}, 1000);
Может довольно легко стать неуклюжим, особенно если вы храните возвращаемое значение setInterval. Я настоятельно рекомендую поместить функцию вне вызова и дать ей имя, чтобы оно было четким и самодокументированным. Это также позволяет вызывать функцию, которая возвращает анонимную функцию, в случае, если вы делаете что-то с замыканиями (особый тип объекта, который содержит локальное состояние, окружающее функцию).
Array.prototype.forEach
в порядке.
Если состояние сохраняется с обратным вызовом, обратный вызов должен быть возвращен из другой функции (например, slideLoop
), чтобы сформировать замыкание
Вы не хотите смешивать состояние и обратные вызовы вместе, как это сделал Эндер. Это склонно к беспорядку и может быть трудно поддерживать. Состояние должно быть в той же функции, что и анонимная функция, чтобы четко отделить его от остального мира. Лучшим названием для slideLoop
может быть makeSlideLoop
, чтобы сделать его более понятным.
Используйте правильные пробелы. Логические блоки, которые делают разные вещи, должны быть разделены одной пустой строкой
Это:
print(some_string);
if(foo && bar)
baz();
while((some_number = some_fn()) !== SOME_SENTINEL && ++counter < limit)
;
quux();
намного легче читать, чем это:
print(some_string);
if(foo&&bar)baz();
while((some_number=some_fn())!==SOME_SENTINEL&&++counter<limit);
quux();
Многие начинающие делают это. Включая маленького 14-летнего меня из 2009 года, и я не отучился от этой дурной привычки до, вероятно, 2013 года. Перестаньте пытаться сокрушить ваш код настолько маленьким.
Избегайте "string" + value + "string" +...
Создайте функцию форматирования или используйте String.prototype.replace(string/regex, new_string)
Опять же, это вопрос читабельности. Это:
format("Hello %! You've visited % times today. Your score is %/% (%%).",
name, visits, score, maxScore, score/maxScore * 100, "%"
);
намного легче читать, чем это ужасное чудовище
"Hello " + name + "! You've visited " + visits + "% times today. " +
"Your score is " + score + "/" + maxScore + " (" + (score/maxScore * 100) +
"%).",
редактировать: я рад отметить, что я сделал ошибку в приведенном выше фрагменте, который, на мой взгляд, является отличной демонстрацией того, насколько подвержен ошибкам этот метод построения строк.
visits + "% times today"
^ whoops
Это хорошая демонстрация, потому что единственная причина, по которой я сделал эту ошибку и не замечал ее так долго (как не делал), в том, что код чертовски труден для чтения.
Всегда окружайте аргументы своих троичных выражений паренами. Это способствует удобочитаемости и предотвращает ошибки.
Я позаимствовал это правило из лучших практик, связанных с макросами препроцессора С. Но мне не нужно объяснять это; посмотреть на себя:
let myValue = someValue < maxValue ? someValue * 2 : 0;
let myValue = (someValue < maxValue) ? (someValue * 2) : (0);
Мне все равно, насколько хорошо вы думаете, что понимаете синтаксис вашего языка, последний будет ВСЕГДА легче читать, чем первый, и удобочитаемость - единственный необходимый аргумент. Вы читаете в тысячи раз больше кода, чем пишете. Не будьте придурком к своему будущему я в долгосрочной перспективе, просто чтобы похлопать себя по спине за то, что вы умны в краткосрочной перспективе.