Расширение на Ответ Эндера , давайте рассмотрим наши варианты с улучшениями от ES2015.
Во-первых, проблема в коде Аскера заключается в том, что 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 раз.
И такon.
Вы можете решить эту проблему, изменив его на:
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);
}
Давайте рассмотрим, как это работает для начинающих (обратите внимание, что не все это напрямуюсвязан с вопросом):
format
function format
Очень полезно иметь printf-подобную функцию форматирования строк на любом языке.Я не понимаю, почему в JavaScript его нет.
format(str, ...args)
...
- это шикарная функция, добавленная в ES6, которая позволяет вам делать множество вещей.Я считаю, что это называется оператором спреда.Синтаксис: ...identifier
или ...array
.В заголовке функции вы можете использовать его для указания аргументов переменных, и он будет принимать каждый аргумент в позиции и после позиции указанного аргумента переменной и помещать их в массив.Вы также можете вызвать функцию с массивом, например, так: args = [1, 2, 3]; i_take_3_args(...args)
, или вы можете взять объект, похожий на массив, и преобразовать его в массив: ...document.querySelectorAll("div.someclass").forEach(...)
.Это было бы невозможно без оператора распространения, потому что querySelectorAll
возвращает «список элементов», который не является истинным массивом.
str.split(/(%)/)
Я не очень хорошо объясняю, как работает регулярное выражение.JavaScript имеет два синтаксиса для регулярных выражений.Есть ОО-путь (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]
.См. Также: фильтр, поиск, уменьшение, 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
, хотя большинство людей называют его «троичным» оператором, поскольку на каждом языке, на котором он появляется, это единственный оператор с 3 операндами, каждый другой оператор является двоичным (+, &&, |, =) или унарным (++, ..., &, *).Интересный факт: некоторые языки (и расширения языков поставщиков, такие как GNU C) реализуют двухоперационную версию оператора ?:
с синтаксисом 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
.Тернарный оператор может образовывать выражение или выражение-оператор, поэтому обе эти вещи:
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"
slide
return setInterval(slideLoop(0, "#container"), 3000);
Прежде всего, мы возвращаем возвращаемое значение setInterval
, чтобы оно могло быть использовано позже при вызове clearInterval
.Это важно, потому что JavaScript не будет очищать это сам по себе.Я настоятельно не рекомендую использовать setTimeout
для создания цикла.Это не то, для чего setTimeout
предназначен, и, делая это, вы возвращаетесь к GOTO.Прочтите статью Дейкстры за 1968 год, Перейти к утверждению, которое считается вредным, , чтобы понять, почему циклы GOTO - плохая практика.
Во-вторых, вы заметите, что я сделал некоторые вещи по-другому.Повторяющийся интервал очевиден.Это будет продолжаться вечно, пока интервал не будет очищен, и с задержкой 3000 мс.Значение для callback является возвращаемым значением другой функции, которую я передал аргументам 0
и "#container"
.Это создаст замыкание, и вы вскоре поймете, как это работает.
slideLoop
function slideLoop(margin, selector)
Мы берем margin (0) и селектор ("#container") в качестве аргументов.Поля - это начальное значение поля, а селектор - это селектор CSS, используемый для поиска изменяемого элемента.Довольно просто.
const multiplier = -600;
let contStyle = document.querySelector(selector).style;
Я переместил некоторые жестко закодированные элементы вверх.Поскольку поля кратны -600, у нас есть четко помеченный постоянный множитель с этим базовым значением.
Я также создал ссылку на свойство элемента style с помощью селектора 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) +
"%).",
edit: Я рад отметить, что я сделал ошибку в приведенном выше фрагменте, что, на мой взгляд, является отличной демонстрацией того, насколько подвержен ошибкам этот метод построения строк.
visits + "% times today"
^ whoops
Это хорошая демонстрация, потому что единственная причина, по которой я совершил эту ошибку и не замечал ее так долго (как не делал), в том, что код чертовски труден для чтения.
Всегда окружайте аргументы своих троичных выражений паренами.Он улучшает читабельность и предотвращает ошибки.
Я позаимствовал это правило из лучших практик, связанных с макросами препроцессора C.Но мне не нужно объяснять это;убедитесь сами:
let myValue = someValue < maxValue ? someValue * 2 : 0;
let myValue = (someValue < maxValue) ? (someValue * 2) : (0);
Мне все равно, насколько хорошо вы думаете, что понимаете синтаксис вашего языка, последний ВСЕГДА будет легче читать, чем первый, и удобочитаемость - единственный необходимый аргумент,Вы читаете в тысячи раз больше кода, чем пишете.Не будьте придурком в будущем, только так вы можете похлопать себя по спине, чтобы быть умным в краткосрочной перспективе.