在Ender's answer 上进行扩展,让我们通过 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 秒执行。 Asker 预计这 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)));
}
}
但这只是一个懒惰的解决方案,并没有解决此实现的其他问题。这里有很多硬编码和一般的草率,应该修复。
十年经验的教训
正如本答案顶部所述,Ender 已经提出了一个解决方案,但我想补充一点,以考虑 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
在 any 语言中具有类似 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 有两种用于正则表达式的语法。有 OO 方式 (new RegExp("regex", "gi")) 和字面方式 (/insert regex here/gi)。我对 regex 深恶痛绝,因为它鼓励的简洁语法往往弊大于利(也因为它们极不便携),但在某些情况下 regex 很有帮助,比如这个。通常,如果您使用"%" 或/%/ 调用split,则生成的数组将从数组中排除“%”分隔符。但是对于这里使用的算法,我们需要将它们包括在内。 /(%)/ 是我尝试的第一件事,它奏效了。我猜是幸运的猜测。
.map(...)
map 是一个函数式成语。您使用 map 将函数应用于列表。语法:array.map(function)。功能:必须返回一个值并接受 1-2 个参数。第一个参数将用于保存数组中的每个值,而第二个参数将用于保存数组中的当前索引。示例:[1,2,3,4,5].map(x => x * x); // returns [1,4,9,16,25]。另请参阅:filter、find、reduce、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,虽然大多数人称它为“三元”运算符,因为在它出现的每种语言中,它都是唯一的三元运算符,其他所有运算符都是二元(+、&&、|、=)或一元( ++, ..., &, *)。有趣的事实:一些语言(以及语言的供应商扩展,如 GNU C)实现了?: 运算符的双操作数版本,语法为value ?: fallback,相当于value ? value : fallback,如果@,将使用fallback 987654360@ 评估为假。他们称之为猫王接线员。
我还应该提到expression 和expression-statement 之间的区别,因为我意识到这对所有程序员来说可能并不直观。 expression 代表一个值,可以分配给l-value。表达式可以填充在括号内,不会被视为语法错误。表达式本身可以是 l-value,尽管大多数语句是 r-values,因为唯一的左值表达式是由标识符或(例如在 C 中)由引用/指针形成的那些。函数可以返回左值,但不要指望它。表达式也可以由其他较小的表达式合成。 (1, 2, 3) 是由三个 r 值表达式由两个逗号运算符连接而成的表达式。表达式的值为 3。另一方面,expression-statements 是由单个表达式组成的语句。 ++somevar 是一个表达式,因为它可以用作赋值表达式语句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 的镜像版本,表面上继承自shell 语言,您可以在其中调用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。阅读 Dijkstra 1968 年的论文,Go To Statement Considered Harmful,了解为什么 GOTO 循环是不好的做法。
其次,你会注意到我做了一些不同的事情。重复间隔是显而易见的。这将永远运行,直到间隔被清除,并且延迟 3000 毫秒。 callback 的值是另一个函数的返回值,我已经提供了参数0 和"#container"。这会创建一个闭包,您很快就会明白它是如何工作的。
幻灯片循环
function slideLoop(margin, selector)
我们将边距 (0) 和选择器 ("#container") 作为参数。边距是初始边距值,选择器是用于查找我们正在修改的元素的 CSS 选择器。很简单。
const multiplier = -600;
let contStyle = document.querySelector(selector).style;
我已经上移了一些硬编码元素。由于边距是 -600 的倍数,因此我们有一个带有该基值的明确标记的常数乘数。
我还通过 CSS 选择器创建了对元素的 style 属性的引用。因为style 是一个对象,所以这样做是安全的,因为它将被视为引用而不是副本(阅读通过共享 来理解这些语义)。
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 函数轻松设置容器的marginLeft CSS 属性。它设置为当前边距值乘以乘数,您还记得它设置为 -600。 -600 -> -1200 -> -1800 -> 0 -> -600 -> ...
我的版本和 Ender 的版本之间存在一些重要差异,我在 a comment 的回答中提到了这一点。我现在要复习一下推理:
使用document.querySelector(css_selector) 而不是document.getElementById(id)
querySelector 是在 ES6 中添加的,如果我没记错的话。 querySelector(返回第一个找到的元素)和 querySelectorAll(返回所有找到的元素的列表)是 all DOM 元素原型链的一部分(不仅仅是document),并采用 CSS 选择器,所以除了通过 ID 查找元素之外,还有其他方法可以查找元素。您可以按 ID(#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)返回以形成闭包
您不想像 Ender 那样将状态和回调混为一谈。这很容易出现混乱,并且可能变得难以维护。状态应该与匿名函数来自同一个函数,以便清楚地将其与世界其他部分区分开来。 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();
很多初学者都这样做。包括 2009 年 14 岁的我,直到 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) +
"%).",
编辑:我很高兴地指出我在上面的 sn-p 中犯了错误,我认为这很好地证明了这种字符串构建方法是多么容易出错。
visits + "% times today"
^ whoops
这是一个很好的演示,因为我犯这个错误的全部原因,而且我一直没有注意到它(没有),是因为代码非常难以阅读。
始终用括号括住三元表达式的参数。它有助于提高可读性并防止错误。
我从围绕 C 预处理器宏的最佳实践中借用了这条规则。但是我真的不需要解释这一点。自己看:
let myValue = someValue < maxValue ? someValue * 2 : 0;
let myValue = (someValue < maxValue) ? (someValue * 2) : (0);
我不在乎您认为自己对语言语法的理解程度,后者总是比前者更容易阅读,而可读性是唯一必要的论据。您阅读的代码比编写的代码多数千倍。从长远来看,不要对未来的自己做个混蛋,这样你就可以在短期内称赞自己聪明。