至于何时恢复更新……
咕,咕咕咕?
]]>希望能早点安定下来,静下心来做自己想做的事情……青色的旋律只是进入了短暂的休止,并没有迎来终结。
更多的脑洞…… JavaScript 的教程等等等等,未完待续……
]]>打开吧。
蜡笔的笔尖在雪白的墙上移行。金色的直线,弧线,直线,然后又是直线。
打开吧——
轮廓之后,数笔又添上细节。很快,歪扭的笔画勾勒出一扇门。
打开吧……
眼前只有无尽的荒漠。风卷起沙子,堆砌成一个又一个山丘。
没有路,没有植被,没有足迹。旅人向天际前行,只为寻找一朵花。
残阳浮在沙之海的远方,随时都可能沉落。抬眼望去,目光所至之处皆是余晖染成的金红。红得让人恐慌,金得让人窒息。
不知道来自哪里,不知道要去何方。甚至连寻找本身,都只是无数轮回之后,前世残留的一缕记忆。踏出的脚印很快也被风抹去,一切都存在过,又不曾存在过。
但花一定开在某处。旅人依然前行。感受不到饥饿或者寒冷,只能感受到孤独。
远处,似乎传来了笛声。
门重重地合上,似乎还能听到木头吱嘎的声音,宛如一声叹息。
风和沙和日都褪色了,消隐在稀薄的空气中。白色充斥着视野,很快又组成熟悉的墙。
墙上已然没有了旅人。墙上也已然没有了门。我曾经描绘的,已是曾经。
盯着空白的墙沉思了许久,我又拿起蜡笔,描绘出翠色的痕迹……
潺潺的水声响起,铺天盖地的绿意袭来。阶梯在山中延展着,曲折着。
飞鸟扑着翅膀从头顶飞过。森林散发着清香。一切都是那么熟悉,仿佛回到了起点。
木屐拾级而上,将半座山踩在脚下。此时平静的心情,似乎与山重新融为一体。
远处的那抹红色近了,近了。是几根红色的柱子,支撑起红色的横杆。先前飞过的鸟停在上面,时不时发出吱吱喳喳的叫声。
山顶是几座木屋,简陋的材质做出繁复的式样。轻车熟路地绕过屋子,一棵大树映入眼帘。
这是一颗苍老的古木。其粗壮的枝干、坚硬的树皮、盘错的根系,无不显示着它的年龄。树枝和树叶交错着,墨绿遮蔽了一片天空。
在树根坐下,背靠树干,俯瞰着山中的风景。日光从枝叶之间洒落下来,在午后的时光中洒下调味。
起风了。树叶的沙沙声,鸟鸣声,风声,歌声交织成一曲旋律。一种难以言述的空灵感弥漫开来。不知干渴不知疲倦,就这样唱着、唱着……盼望着这旋律永不终结。
刺眼的光让我不禁闭上眼睛。再睁开时,眼前只剩下那灼目的光。屋不见了,山也不见了,树也不见了。
光仍然没有任何减退。我试着向前走了几步,但马上撞到了一个冰冷坚硬的东西。我用手摸了上去,似乎很平,很宽,很光滑也很冰冷……
这是一堵墙。我这样想着,于是光被打散,重组,然后果然变成了一堵墙。
我摸了摸口袋,想找到一支蜡笔。光于是在口袋聚集,然后果然就有了一支蜡笔。
蜡笔是虹色的。不,这么说并不准确。蜡笔只是发出虹色的光。随着我将蜡笔涂在墙上,墙也似乎被点亮,雪白的墙终于有了色彩。
又一扇灰色的门,逐渐被打开。
这是一片灰色的天空。灰色的月在灰色的云间穿行,向地面投下灰色的影子。
这次的舞台是钢铁的丛林。灰色的路像棋盘般铺散,格子间耸立着灰色的楼。看不到任何行人,看不到任何灯光。
无论走向何处,所见的景色都是那么千篇一律。似乎楼和路,就是这个世界的全部。
甚至,连空气都没有一丝波动。声音好像也不属于这个世界,简直就像旧时代的黑白默片。
手提着一盏灯,穿梭在这个迷宫。也许这个迷宫没有入口,也没有出口。
城市中,只有自己手中的灯亮着——
夜尽了。梦醒了。天亮了。
我看了看手中。之前一直握着的灯,不知何时又变回了一支蜡笔。
眼前雪白的墙上,仍然画着夜和城市。但下一刻,城市中迎来了日出,夜色也就被拂晓所吞噬。整个城市一点点被照亮,然后,变得与雪白的墙壁别无二致。
存在过的,都消失了。但创造出的,都铭刻了。
在梦与现实的狭间,我手握一支虹色的蜡笔,将一扇又一扇门描绘。每一扇门都有不同的颜色、不同的风景、不同的旋律。
这就是我的世界。
这也是没有我的世界。
]]>JS 的控制语句本身并没有太多难点,和 C 语言一系列的很像。如果各位之前没有学习过编程的话,可能会需要一点时间理解一下语句执行机制和控制流。但如果已经学习过的话,应该只需要熟悉下语法即可。 ES2015 (ES6) 以来, JavaScript 也加入了一些新的语句和语法,比如一个新的循环语法,也可以适当熟悉一下。
大家,和青色旋律一起继续愉快的 JavaScript 旅途吧~
(此文经过了彻底重写,和之前学习小组的讨论记录完全不同,看过讨论记录的建议重新读一次本文,相信一定会有收获。这次的各语句介绍更详细,且增加了 ES2015 等内容。)
对了,这次青色旋律特别加了贴心的目录功能,点击即可跳到对应章节。系列之前几篇也加上啦,再也不怕找不到内容在哪里啦!就算整篇太长不看,也不妨跳着看自己想知道的。
上次的作业 中首先布置了预习或者复习控制语句,相信大家应该都已经完成了吧?如果没预习的话也没关系,接下去会有详细的讲解。其次,是抽奖机、 G+ 圈子系统、和金珂拉相关内容。如果大家已经做完了的话,请在 上次文章的评论区 提交作业。
作业并没有截止时间,各位仍然可以继续提交上次的作业,青色旋律仍然会进行评阅。但是,请不要照抄下面的解答哦。
请编写一个抽奖程序,从一组预设的奖项中任意抽取一个结果。当然,玩过抽奖的各位都知道,大部分预设的结果都应该是“谢谢惠顾”,少量其他奖项。
解答:首先,我们需要一个数组来存放所有可能的奖项。
|
|
这个数组可以任意长,根据各位的恶意程度,可以放置 1~100 个'谢谢惠顾'
。
然后正如上次提示的一样,使用 const num = Math.floor(Math.random() * 8);
可以得到一个 0 到 7 之间的随机数。抽取到哪个数,就取出第几个元素。但数组的长度不一定是 8
,比如以上一共只有 5
个元素。怎样做到无论数组长度为多少,都可以正常运行呢?这个就要使用 上次提到的
数组 .length
属性来获取数组长度,然后根据此长度来随机了。例如:
|
|
此时 selectedIndex
会是大于等于 0 且小于数组长度之间的一个随机整数。此处
Math.random() 用于生成一个 [0, 1)
之间的实数,包括 0 但不包括 1. 然后例如 *8
后为
[0, 8)
, 再使用 Math.floor(x) 向下取整。
此后,使用数组的 []
语法取得下标为 selectedIndex
的元素。注意数组元素从 0
开始,所以之前生成的随机数是 0
开始,小于长度。
|
|
总之,这个题目并不是太困难,适合用于复习数组操作。
改进 1: 实际抽奖的时候,大奖的概率总是低一些的。为了达到这个效果,可以在数组里只写一次”一等奖”,而写十次”谢谢惠顾”。多抽几次,试试看自己的运气吧~
解答:这个并没有难度,如果注意使用 results.length
来获取长度的话,修改结果数量时就不需要再更改下面的代码了。刚接触编程的各位,青色旋律在此推荐大家能获取的值不要写死在程序里,尽量使用比较灵活的方式,这样代码也会相对可靠一些。
改进 2: 请做成每次抽取后不返还。也就是说,抽取过的结果不能再次被抽取。
提示:可以把一开始的数组当做抽奖箱,每次抽取后,把抽取的那个结果从箱子中移除,这样下次就抽不到了。这个涉及到数组中删除一个元素的操作。由于 JS 里并没有一个函数直接对应删除元素,所以会需要上网搜索一下等,看看别人的回答。适当利用搜索引擎也是学习中很重要的一部分,甚至说是自学的必备技能之一也不为过,因此如果各位还不清楚要怎么做,不妨先搜索下试试看,然后再看答案。
提示 2: 由于是我们自己抽出的,下标 selectedIndex
是已知的,因此我们只需要根据数组下标来移除元素即可。试试看搜索关键词 JS 根据数组下标移除元素
!或者,也可以试试看在 Google 搜索英文 ,例如
JS remove element from array by index
.
由于英文社区比较活跃,提问的人也比较多,一般情况下更容易找到优质内容。注意搜索结果繁多,打开网页后也会有各种琳琅满目的答案,需要自行培养过滤信息的能力。
解答: 使用 results.splice(selectedIndex, 1);
可以移除那一个元素。其中
selectedIndex
是要移除的下标(开始位置),然后 1
指定删除一个元素。
splice
的用法可以参考 MDN 上的文档 。 特别注意如果省略第二个参数,会从把那里开始到数组结尾的元素全部删掉(又称为“腰斩”),是我们不需要的。由于 splice
函数名字比较怪,而且不止能删除一个,还能删除多个、插入、替换等,所以定位到这个函数会比较难。但适当搜索的情况下,总会有结果提到这个函数的。整体代码如下:
|
|
由于第二次数组长度已经发生了变化,所以需要用 results.length
再获取一次新的长度。当然也别忘了重新生成随机数。但是,第二次还要换不同的变量再写一次非常麻烦,而且难道抽 N 次就要复制和修改 N 遍吗?关于这一点,可以看接下来介绍的内容,使用循环语句连续抽奖 N 次。或者在《JavaScript: 从入门到放弃》系列的下一篇中学习函数,然后把抽奖的代码逻辑放到函数里,随时可以再次抽取。
总之题目太长了,这里就不再贴一次了。请新来的各位自行查看 上次题目的内容 吧。
总之,需要设计一个数据结构保存以下人际关系,并且针对获取所有土豪、获取某人被多少人认为是土豪,和一些圈子操作做优化。
注意实际的操作中,很可能需要条件语句或者循环语句才能达成。因此这里先设计一下数据结构,然后在这次学习过语句后再编写操作吧。
首先是最简单的,最贴近原始数据的设计:
|
|
这里把每一对人际关系都存储为一个对象,然后整体数据结构就是一个数组,里面有各种关系。注意此处 Bob 认为 Alice 是土豪也是菊苣,所以这里使用了两个关系对象。当然,也可以只使用一个对象,比如
{from: 'Bob', to: 'Alice', circleNames: ['土豪', '菊苣']}
. 但是假如这样设计,那么其他的关系元素也应该使用同样的格式,即使只有一个圈子。相同的数据结构有助于使用循环语句统一处理,减少条件判断等,这个后面会讲述。
这个设计方案的优点是直接明了,且插入数据非常方便,只需要
relationships.push(...)
即可。缺点是要进行任何检索、删除等操作,都必须检查一遍整个数组。比如计算 Bob
被多少人认为是土豪,需要逐个元素检查然后计数。
需要注意的是,很多时候假如处理的数据量并不大,可能最直接的方式才是最有效的,不要为了可能无关紧要的性能而忽视了代码复杂性。针对未来可能处理的数据量做出正确预估,然后再确定合适的方案,或者在实际情况下检测性能瓶颈和关键路径然后再优化,这样更为可取。这也是青色旋律在实际项目中汲取的经验教训。
那么,具体怎么样遍历数组和计数呢?其他又有哪些设计方案,支持什么样的操作,各有怎样的优缺点呢?请看后文 使用循环进行数据处理 一节。
JS 掺了金珂拉,码农日薪一万八!语言特性再也不向 PHP 进口啦!
同理,题目太长不贴,新来的各位请看 上次题目的内容 吧。
|
|
总之输出类似上面的顺口溜内容,但要求 金珂拉
, 非洲
, 日本
三个关键词可以替换,
以适应米国圣地亚鸽公司发布新产品时请不同长相的滑稽演员的需求。
首先,举一个输入的例子:
|
|
根据原始题目要求,新产品名字不一定是三个字,因此需要做一些预处理。
|
|
需要特别注意的是,以上的代码取第 X 个字符等操作,对于某些 Unicode 字符 并不能正常工作。这里为了简单起见不再多介绍,有兴趣请阅读链接的文章。
这里使用了 if .. else
条件语句来保证代码清晰可读。具体请看
后文相关章节 。但实际上只使用 ?:
表达式也可以实现,只是语句会相对而言长一点,而且没办法处理错误情况。如果不记得 ?:
操作符了,请复习下上次 布尔值
相关的操作。
|
|
为了让语句稍微精简一点,特意把一些计算放在了其他变量中先执行。这个例子中,由于用于获取子字符串的函数 product.substr(start, len)
即使超出范围也不会出错,只会返回空字符串之类的,所以提前计算也没关系。
有了产品的昵称 nickname
后,就可以输出顺口溜了。最简单的方式是直接用
模版字符串
来直接替换输出。注意 环境必须支持 ES2015 模版字符串 , 因此在一些过时的浏览器或者环境中无法使用。
|
|
模版字符串有一个比较好的特点是可以包括换行,但换行后的代码不能缩进(否则空格也会进入到最终的字符串中),必须顶格,经常会使得代码变得很丑。
另外一种方案是直接拼接字符串,也并不是很难。这次我们也可以试试看分四次输出,就可以避开换行的问题了。
|
|
最后的办法就是用字符串替换,可以直接把 "非洲"
换成 "法国"
之类的。注意这个对原始的字符串有一定的要求,在其他例子中使用可能会替换到不该替换的内容,所以建议原始字符串里用比较特殊的字符串来作为替换目标。以下代码中为了对齐工整,使用了
甲地
, 乙地
, 新产品
三个词,但实战推荐更独特的字符串,例如 "_PLACE1_"
。
|
|
这个方案中用到一个小技巧,先用一个数组保存各行元素,然后用
.join
把四行连接在一起,中间以换行符 '\n'
隔开。这种使用 \
开头的字符序列来代替难以输入在代码里的特殊字符方式,称为转义,具体参见
MDN 上文档 。注意如果不加换行符分隔的话,不会自动换行。
由于 JavaScript 不支持全局替换功能,比较简单的方式是使用
正则表达式
来进行替换。正则表达式的 g
参数指定该正则表达式为全局匹配,从而可以实现全局替换。关于如何编写正则表达式,请查看相关教程。
总之,作业的解答就到这里了。大家明白了吗?如果还有疑惑,可以在下方评论区给青色旋律留言。
如果您已经比较熟悉 C 系列的语言,那么 if
语句, switch
语句, for
语句都可以跳过不看,直接从 for .. of 循环 开始看即可。但请注意
let
和 const
变量作用域为块作用域,条件会
隐式转换为布尔值。
首先是 if
语句,其基本语法如下:
|
|
其中 condition
可以是一个计算结果为
布尔值 的表达式,比如 a > 5
等等。如果值为 true
, 执行 statements_1;
(称为真-分支),如果为 false
执行
statements_2;
(假-分支). 值如果是其他的话,会
转换为布尔值 然后再执行。两个分支都可以是零条到多条语句,例如
console.log(a);
之类的,甚至可以嵌套其他 if
语句。 JS 语言运行的时候,正常情况一定会执行两个分支其中之一,不会执行另一个,所以称为分支。
注意 if (...)
的圆括号是语法的一部分,不能省略。如果条件不成立时不需要执行任何语句,那么 else
开始的部分可以全部省略,变成
if (condition) { statements_1; }
. 换行不影响语法。
如果某个分支语句只有一条,那么可以省略花括号,但青色旋律强烈反对这种写法,除了
if (aaa) bbb;
这种极为简单,可以写在一行内且没有 else
分支的时候。有的项目更严格,这种情况也必须换行写花括号,所以在参与项目时请尽可能先了解其编码规范。
理论上空格和换行全省, if(a)b;else c
也可以执行,但对人类太不友好,不可取。在源代码中少按几下键盘,或者说为了减少传输的字节量之类的理由是不成立的,因为正常情况下如果有这个需求,都是通过自动工具来最小化代码 (例如
UglifyJS
),然后再经过网页传输压缩等再到达客户端的。至于不需要在网络上传输的代码,例如 Node.js 等场合,一般而言并不需要最小化。如果您看到其他网站有类似这种完全没有格式的代码,那么您可以试着用工具来
格式化代码 然后再分析。不过注意最小化的过程中,很可能变量名也被改成单字符的 a
, b
之类的了,会很难阅读,所以还是尽量建议找源代码来看。
if .. else
语句一种常见的嵌套形式如下,可用于判断多个条件。(此处使用了 上次作业题的解答 为例。)
|
|
这里实际上就是依次判断第一个条件,如果满足执行第一段,否则继续判断第二个条件,依此类推,最后如果都不满足就执行最后一个 else
之后的内容。常用于比较复杂的逻辑。这里实际上就是省略了前几个 else
语句的花括号,如果加上的话类似这样:
|
|
但这样太繁琐了,而且后面的要缩进好多层,所以一般不这样写。但初学者可以把上面的想像成这样,这样对嵌套关系和执行逻辑的理解会更清楚一些。
如果大部分判断都是针对同一个表达式的不同可能值的话,还有一种 switch
语句会更简洁一些:
|
|
可以看到 switch
语句不关注布尔值,而是只针对括号内的表达式的可能取值进行判断。如果表达式计算后值等于 0
, 那么 case 0:
后的内容会执行。否则继续判断下一个
case
是否相等……如果都不成立,那么 default:
后的内容会执行。由于只会检查相等不相等,自然没法写出 product.length < 2
这个逻辑,所以这里使用了 case 0:
和
case 1:
两条语句来分别判断长度为 0
和 1
。
执行时,遇到 break;
语句才会终止,直接跳到 switch
语句之后。如果没有写
break;
, 那么即使遇到其他 case xxx:
也不会做任何判断,而是继续执行下去。例如注意到 case 0:
后面紧接着 case 1:
, 这样使得 0
或 1
情况下都输出错误。这个用法没什么问题,但如果在 case 2:
或者 case 3:
之后忘记写 break;
了,那么就很危险了,代码会继续执行到 default:
下面的语句,从而导致最终结果和一眼看上去的不一样。这个现象称为
fallthrough,
如果不小心写错会导致 bug, 而且滥用会导致代码不可读。青色旋律推荐使用代码风格检查工具来 检查这样的问题 。
能用 switch
表达的逻辑,都能使用类似
if (x === 1) { ... } else if (x === 2) { ... } else { ... }
的方式来表达,运行结果完全相同(除了 fallthough 的情况)。但反过来,用 if .. else
能表达的,不一定都能用 switch
表达,尤其是和相等无关的判断。实际编码时,根据实际情况,选用比较简明的方式吧。
最简单的循环,是 while (condition) { ... }
类型的循环。以抽奖为例,以下代码可以不断地抽取奖项,直到所有奖项都抽取完为止:
|
|
这里遇到 while
时,首先判断条件是否为 true
或者能转换为 true
,如果不成立直接跳到结束。如果成立,会执行花括号内的所有代码,然后再跳回到条件那里,再次判断和执行,如此反复。这里的条件和强制转换和 if
相同。
注意由于开始之前就先判断,如果条件一开始就不是 true
, 那么一次都不会执行,直接跳过整段。但假如条件一直是 true
, 那么程序就停不下来了,称为死循环。例如
while (true) { console.log("Let's make the world a better place!"); }
会不停地打印,不会终止。这种时候需要用外部手段停掉 JS 解释器本身,比如关闭网页、关闭浏览器,在 Node.js 等环境使用 Ctrl-C
组合键,关闭终端,使用任务管理器等方式结束进程,甚至关机、拔电源等。打印大量内容本身也很可能会造成设备卡顿,妨碍操作。
但总的来说,只要执行的操作不是很危险,那不必太害怕死机等,假如出现了再处理就好。我们都曾是新手,通过犯错来学习也没什么问题。
在 while
函数体内使用 break;
语句可以中断执行,不进行任何条件判断,直接跳到结束。通常是配合 if
语句来使用,例如:
|
|
以上示例中,如果运气不好,随机不到 60
以上的分数,就无法退出循环。但假如随机到了,执行到 if (score >= 60) break;
就会退出了。此时是直接跳到结束,所以不会再多打印一次 'Sorry. Try again!'
.
同样,滥用 break;
语句也会降低代码可读性。一般而言,把循环条件写在一开始会让人类更容易理解终止条件,但也不可一概而论,还是需要酌情判断。
最后,需要注意的是 const score
这个变量只在 { ... }
之内有效,而且每次执行时会创建一个新变量,只是名字相同,所以不是改变了变量值。
{ ... }
称为一个
语句块 ,
而变量的这个现象称为块作用域。其他用到语句块的场合,比如上述的 if (x) { ... }
,
也适用这个规则。 let
语句声明的可再次赋值的变量,也遵守块作用域。语句块执行完毕后,这样声明的变量名就不能访问了,所以如果需要把值带出来,需要在块之外提前声明变量,例如:
|
|
注意 var
不同,使用的是“函数作用域”,也就是说每次函数执行中有效,具体情况在本系列下一篇函数相关的内容中详细叙述。
do .. while
语句与 while
语句类似,但主要区别是判断条件写在最后,所以是先执行再判断。例如上面那个例子如果用 do .. while
语句来写的话:
|
|
注意这里省略了打印 "Try again"
的部分。可以看到同样的逻辑使用 do .. while
方式表达,无论如何都会先进入一次,生成随机数然后再进行判断,这大大简化了代码书写。如果需要先执行再判断的场合,可以考虑这种写法。
特别注意,由于块作用域的变量只在 { ... }
中有效,执行完毕后就不能再访问了,所以以下写法的最后一行会找不到 score
变量,无法编译执行。
|
|
for
语句可以看做是一种比较特殊的 while
循环,一般用于限定次数的场合。例如:
|
|
这个例子会输出 "Hello 0"
, "Hello 1"
, "Hello 2"
, "Hello 3"
, "Hello 4"
.
对于没有接触过 C 系列语言的人来说,可能 for
语句看起来很复杂,但实际上还算比较简单,语法是 for (initial; condition; next) { body; }
。首先,在循环前一定会执行 initial
语句,而且只执行一次,后续不再执行。这一部分通常用于声明和初始化变量。 condition
部分是循环条件,通常是布尔表达式,与
while (condition)
的情况相同,为 true
或者能转换为 true
则继续循环。接下去执行 body;
部分的一条或者多条语句。最后的 next
部分则是在每次循环
body;
执行完毕后执行,通常用于前进到下一步。这个例子中我们使用了 i++
, 表示将变量 i
的值增加 1
(等同于 i = i + 1
), 然后再判断 condition
, 开始下一步。
也就是说,for (initial; condition; next) { body; }
可以粗略当成下面的 while
循环写法(除了 continue;
的处理以外,后述):
|
|
如果 initial
部分有变量声明,作用域按照特殊规则,变量只在 for
语句内(包括
condition
, body
, next
三部分)才能访问,for
结束之后的下一条语句就无法访问这个变量了。这个特性非常方便,使得 initial
部分的变量可以真正做到用完就丢,完全无污染。如果改用 while
语句,可以看到以上的代码需要整个用 { ... }
包起来,才能实现同样的效果。另外, for
循环的 let
变量对于其中声明的函数也有特殊处理,这个在《JavaScript: 从入门到放弃》的下一篇,关于函数部分会进行说明。
注意 initial
, condition
, next
三部分都可以省略。其中 initial
和 next
省略就是什么都不执行了,而 condition
省略的话相当于 true
, 也就是不退出,这种时候需要在里面使用 break
退出以免造成死循环。省略时不能省略分号,所以假如三部分都没有,就是写作 for ( ; ; )
或者 for(;;)
(不推荐,不如用 while (true)
意图更清楚)。如果 body;
只有一条语句,花括号可以省略,但除非很短可以写在同一行,否则不推荐这样写。
另外,在 for
循环中使用 continue;
语句时,会中断本次循环,而是直接执行
next;
部分,然后判断 condition
进行下一次循环。这个常用于需要跳过这次的处理而直接开始处理下一次的情况。要注意在 for
循环中 continue;
还是会执行
next;
, 这一点上和前一个示例使用 while
实现的代码不相同。
for
语句常见用于 for (let i = 0; i < 5; i++)
这样限定次数的循环。或者说,只要是事先知道循环多少次的情况, JS 程序员大多会用这样的 for
语句实现。这个范式当然也可以用来遍历数组,比如以下代码打印所有可能的抽奖结果:
|
|
但如果遍历数组的时候只需要数组元素,而不需要下标 i
的话,还有一种更简便的写法,使用 for .. of
循环。注意此语法在
支持 ES2015 语法的环境
方可使用,在不支持的环境可能会导致语法错误。
|
|
这样会输出 results
中每个元素。优点是非常简单直观,缺点是循环中没有数组的下标,比如说这个例子中无法输出是第几个结果。这个语法不仅对数组有用,对
Map,
Set
等集合,以及所有其他遵守 iterable 协议的对象都可以使用。
如果声明了新的变量,这个变量每次循环的时候都会相当于重新声明了一次,而且遵守块作用域,只有当前循环的 { ... }
中有效,下次循环的时候会是一个名字相同但其他都不同的新变量。因此可以用也建议用 const
来声明。这一点和 for
使用同一个变量但改变值的行为完全不同。当然,也不一定非要声明新变量,也可以用外面已经有的变量,不过不推荐。
例如,字符串也是可以遍历的,其中的“元素”是每一个符号:
|
|
这个例子依次输出字符串 "a"
, "b"
, "c"
. 由于 JavaScript 没有字符类型,所以每个“元素”是一个只含有一个“符号”的字符串。这个例子的一个显著优点是能正确处理
Unicode 符号,包括 U+010000 以上的符号。它会整个符号当做一个元素一起,而不像是大部分其他处理方式会把某些符号拆成一对 surrogate pair 当做两个字符处理。
遍历 Map
对象时,每次循环会获得一个数组,有且仅有两个元素,分别是键和值。
|
|
利用 解构表达式 ,上面的例子也可以更简洁地写成:
|
|
但假如有一个对象 const obj = {a: 1, b: 2};
, 怎么遍历对象的属性和值呢?也很简单,先想办法找到对象的所有属性,然后遍历。查阅文档或者搜索后发现
Object.keys(obj)
可以返回所有对象自身的可枚举属性,于是直接用:
|
|
这里顺便复习下 obj[key]
可以返回属性 key
对应的值。 ES2017 还新增了
Object.values(obj)
只获取值,或者
Object.entries(obj) 来获取键值对。在用最新的功能前,请务必
检查运行环境是否兼容 。
特别注意,对象的 有些属性不可枚举 ,不会出现在这些函数的返回结果里。
for .. in
循环也可以用于遍历对象的属性,但注意与 Object.keys(obj)
不同,这个方法会 遍历所有可枚举属性,包括原型链上继承的属性 。关于原型链,请关注青色旋律博客,此教程的后续系列篇。此处只需要知道,有时候会出现“不属于自己”的属性,从而会比
Object.keys(obj)
返回的属性多。
由于
Map
和
Set
的使用,新写的 JS 代码不应该再把对象当做字典使用了,因此 for .. in
循环的使用频率也会降低很多。如果一定要这样用,请三思 。如果是为了打印或者拷贝对象的话,也请最好不要自己强行做,而是找个比较可靠的库来使用。
如果一定要用,建议过滤掉不属于自己的属性:
|
|
基本上来说,只有在没有 ES2015 的环境比较有用。注意即使没有 for .. of
支持,遍历数组也不应该使用 for .. in
, 而是应该使用最典型的 for
循环,例如
for (var i = 0; i < arr.length; i++)
.
给定一个由对象和数组组成的数据结构,就可以轻松地使用各种循环语句来处理了。以之前 作业解答 中设计的数据结构为例:
|
|
首先,最简单的数据结构中最外层是数组,使用 for .. of
即可轻松遍历。让我们先来完成作业要求的第一个操作,获取所有被人认为是土豪的用户。
|
|
接下来,第二个任务是给定一个人,例如 const user = 'Carol';
,
快速获取有多少人认为这位用户是土豪,而多少人认为这位人是菊苣。先来试试看最直接的做法。
|
|
很简单吧?但是,时代在变化,目前大触、学霸等多种超级生物也加入了 G+, 假如要统计所有的种类,恐怕代码会严重膨胀。让我们试试看用更精巧的设计,让代码可以处理所有圈子的类型。
|
|
这样就没问题了!不过这次的代码有些复杂,让我们首先看看整体思路吧。首先这个程序使用 circleNames
记录所有发现的圈子名称,不重复,然后 circleCounts
与其一一对应(下标相同),记录各个圈子的关系数量。然后对于每个关系,如果是见过的圈子就更新关系数量,没见过的就新增记录下来。最后一并打印输出。这里用到了嵌套循环,如果各位还不适应的话请尽快理解和适应,因为以后可能会很常用。总之嵌套循环也是按照顺序严格执行的,内层的先循环完才会循环外层的,并不存在两个变量一起动之类的事情。
注意里面还用到了 continue;
和 break;
来简化逻辑。前者结束此次处理而直接进行下一个处理,而后者直接终止循环。具体用途见代码注释。
但是维护 circleNames
和 circleCounts
两个不同的数组实在是太麻烦了,还需要用下标进行对应。我们能不能合而为一呢?答案当然是能,但需要把两个值合在一起塞到一个数组里。这时就可以使用对象来表示。
|
|
思路和上述相同,但这次只用一个数组,数组中每个元素是一个对象,形如
{name: '菊苣', count: 1}
. 见标记 // 1
的那一行。注意由于 JS 本身是动态类型语言,不需要事先声明对象的结构即可使用。但这里使用者如果不看到比较后面的 // 1
那一行,就不知道这个对象的结构是什么样的。如果结构很重要,可以提前在数组那里就加上注释说明结构,如以上代码第二行所示。
那么,有没有办法干脆不用数组来表示圈子名,从而彻底避免嵌套循环呢?当然也是可以的,这就需要使用字典结构,例如 Map.
|
|
字典结构在这种需要查询和去重复的场合有明显的优势,比如可以直接用
countOfCircle.get("菊苣")
直接获取之前的数量,避免了一层循环。由于此函数在不存在时返回 undefined
, 而 undefined
转换为布尔值
是 false
, 所以会进入 else
分支,新设置一个值。存在的场合当然就是重新设置为 (以前的值 + 1) 啦。注意最后用到 for .. of
遍历字典,每次返回一个数组,第一个元素是键,第二个是值,这里用
解构表达式 直接拆成两个变量。
注:如果您的目标执行环境没有 Map, 建议找一个带字典功能的 JS 库,而不要自己强行去用对象来实现。否则万一有一个用户真的起名叫
toString
,
__proto__
之类的,和继承的隐藏属性发生冲突,代码会被坑得很惨。
可以看到,经过几次重构,最终的代码因为使用了合适的数据结构而变得非常简洁易懂。顺带性能也提高了一些,但在这么小的数据集看不出差别。可读性和可维护性差距更显著。
有没有办法干脆不用循环,直接获取到想要的结果?在这个例子中,因为需要动态处理各种不同的圈子,所以是没办法的。但如果只限“土豪”和“菊苣”,那么我们完全可以设计一种这样的数据结构:
|
|
这样的话,只需 countsOfUser.get('Carol').get('土豪')
即可获得 3
, 免除了循环的烦恼。但前提是——数据在创建的时候就必须按照这个结构来存储。因此,我们就需要修改添加和删除圈子的代码:
|
|
|
|
也就是说,必须在修改操作的时候做更多操作,来使得查询操作的时候更简单更快。根据具体的业务不同,假如查询很多很多,但修改很少,那么这样的取舍也许是值得的。总之,数据结构和访问的设计也并非总是有明确的优劣之分,很多时候必须耐心分析做取舍。如果性能上很难做决定或者差距不大,那么建议选比较简单易懂的方式,以便之后维护。
好的,终于到了激动人心的作业环节啦!各位,上次的作业做了吗?如果没做的话,真是太遗憾了呢,不过不要灰心,从这次开始做也没有问题的啦。考虑到大家都学业/工作繁忙,这次作业量比较少,花不了多少时间。如果有空余时间的话,请试着预习下函数吧?
嗯哼,总之作业内容全部为可选,供您自学时作为参考。如果您对于以上的概念是第一次接触,建议还是尽量完成作业。
作业完成后,可以上传至 https://gist.github.com/ ,并将链接贴在下方评论区中。青色旋律会对作业进行批改和反馈。如果有任何问题也可以使用下方评论区提问。
作业并不是一定要完成的,也没有截止日期,但作业的参考答案将会在下一次的系列博文中公布,您可以在那时自行核对答案,检验自己的学习成果。
请复习或补习 JS 的所有控制语句。有不清楚的地方可以在评论区问,也可以自行查找 MDN 等相关资料进行学习。
对于没有学习过编程或者不了解控制语句的各位,建议在 Codecademy 进行简单的训练。 Codewars 也有很多不错的 关于控制语句的题目,可以用于训练。
对于给定的任意数组,编写代码对数组中的元素进行随机排序,就像洗牌一样。
可以上网搜索洗牌(随机排序)的实现思路,也可以自己考虑。但是青色旋律当然不建议大家搜索现成的代码,那样起不到任何练习的效果。
如果自己考虑了很久也没有思路的话……那用最简单的方法,随机交换多次就行了。
最后,多运行几次自己的程序,进行下测试。
针对 上次题目 中您设计的数据结构,撰写一些处理代码,实现以下功能:
查询功能:
更新操作:
a
新建一个圈子,名字为 "foo"
.a
将用户 b
添加到 "土豪"
圈。b
原先在 a
的 "土豪"
圈中,然后用户 a
将其移除。如果您上次没有设计数据结构,请结合今天学到的内容,现在做一下设计。反正很快的。假如您设计的结构碰巧和今天举例的完全一样,那么您也可以试试看再设计一种不同的结构然后再开始写处理代码。
各位,作业请加油~如果完成了别忘记在评论区提交作业哦。
]]>不,也许是见过的吧。只是他从来没有注意过,从来没有记得过。
这一天的早晨,他在城市东郊的一间房中醒来、洗漱、着衣,然后汇入城市的人潮。当他再一次从梦里醒来的时候,他乘坐的公交已经摇摇晃晃地抵达了办公室的楼门口。
夜晚 10 时,他又从电梯中走出,招手与陌生的出租车司机相遇,然后在一个小时的共同旅程后,返回西郊的家中。
他已经换过很多份工作了,然而每一份工作都是那么的索然无味。坐在舒适或者不舒适的椅子上,桌子前,闷头敲打着键盘,时不时抬头看一下屏幕。是的,他的工作离不开名为计算机的设备。每一份都是。
今天的天是灰蒙蒙的,时不时还落下一些雨点。他感受到突如其来的一种孤独,但转眼又觉得自己一定是疯了。
是的,他的每一天都过得很平凡。平凡得令人压抑。平凡得使人疯狂。
接下去的清晨,他又被闹钟唤醒。今天是一个重要会议的日子,他急忙穿好西装离开了家门。十五分钟后,他已经步行抵达了公司。他抬起手腕—— 9 点 25 分,离开会还有 5 分钟。他连忙走进会议室。
不出他所料,会议的内容仍然是沉闷的。一位领导在不急不缓地讲述着公司今年的发展规划,空谈一些不切实际的目标。随后是突如其来的人事变更,周围的同事怨声载道。这和我又有什么关系呢,他想,并做出一副厌倦的表情。就算是听到他被降职时,他都没有变化过表情,仿佛这只不过是别国总统选举之类的无足轻重的小事。
与他熟识的同事默默地为他叹息,有的还递来同情的眼神。他熟视无睹。与他平日不合的手下几乎掩盖不住内心的窃喜,有的甚至轻轻地哼了一声。他恍若未闻。他已经太累了。
会议结束了,他只是默默地走到他的计算机前,开始处理起工作交接的一些琐事。时间就在键盘的敲打声中默默地走到了黄昏,这也意味着考勤时间的结束。大部分职员都会选择在这个时间离开,除非加班。他注销用户,披上西装外套,头也不回地径直走出了同一楼层的大门。
此时此刻,他疲惫的心里只剩下一个念头——赶紧返回位于城市西侧的家。
下一天他一觉睡到了接近正午。他懒散地套上一件T恤衫,下楼骑上了自行车,向公司赶去。
他现在的工作是一名艺术家。也许听起来很时尚吧,他默默地想着。但其实他工作的内容也无非只是按部就班地拼接一些图片,按照几个固定的模式来机械操作罢了。哪怕是一点点想要创新的想法,就会遭到同事的一致反对,更不用说跳出创作的条条框框。
他缓缓拾级而上,在楼梯与他的几位同事相遇。今天怎么来这么早?一位同事发问,带着深深的疑惑。另一位同事则笑着说,头一次见到你在午饭之前来上班,今天难道太阳从西边出来了?他抽动了一下嘴角,勉强挤出一个比哭都难看的笑容。同事默默地离开了。
他在属于自己的“方块”前坐下。两侧的隔离板形成一个直角,阻断了来自其他同事的声音和视线,更阻断了思维的交汇与艺术的传播。所有工作都在计算机上完成,想要纸笔颜料根本无从谈起。都 21 世纪了,哪有人还在用那种古老的工具?午餐时,一位同事说道,引起了几位其他同事哑铃般的笑声。
过了下午 3 时,开始陆续有人起身从他背后走过然后离开公司。公司并没有固定的上班和下班时间,但该交的任务还是要完成的。因此,很多人也会选择在家中继续作业。也有的同事喊他一起出去喝杯咖啡,但他拒绝了。没有人能真正懂得他的孤独,他心想,手中一边熟练地敲打键盘,保存今日的第 42 幅所谓的“艺术作品”。
结束了今天的任务,他骑上自行车,车轮碾过熟悉而陌生的路面。他听到有人在歌唱,他也听到有人在叹息。
他不由自主地慢了下来,最后左脚拄地,停下了车。不为别的,他的面前有一个穿着红色衣服的小女孩。那一抹红是那么鲜艳,几乎将周围灰色的世界都染上了色彩。
小女孩就静静地站在路中间,手里还拿着一束不知名的花。
此刻,连周围车辆轰鸣的声音都静了下来。时间也仿佛在这一刻凝固。他注视着那个女孩,正像是注视着自己的人生。女孩向他招了招手。
繁忙的马路仍然车来车往。甚至没有一辆车在这里停下。他和她两个人,几乎被世界所遗忘。
不知道是第多少回,他又从睡梦中清醒过来,脑中对昨天的事情只剩下模糊的记忆。他照常出门右转,再直走,进入了城市的地铁站。
来到公司,他先是在储物柜中拿出了笔记本电脑,然后默默地走到他所在的小组,在圆桌旁找了个位置坐下,打开笔记本电脑。和他一组的所有程序员都坐在这里,热烈地讨论着系统设计和开发。他也是其中的一员。不,他并不是其中的一员。
是的,他已经换过很多份工作了。每天他从不同的住处醒来,穿上不一样的服装,经过不一样的路线前往不一样的公司,从事不一样的工作。星期一他是一名编辑,星期二是一名会计,星期三是一位艺术家,今天则是一名程序员。每一份工作都那么枯燥,每一天的经历都那么普通,但他的人生却注定与“普通”二字无缘。
他曾经与曾经的同事倾诉过,但他曾经的同事觉得他疯了。然而下一天,他遇见的却是另一批同事。他曾经试图反抗,删除过曾经的公司数据库,然而下一天,他上班的另一家公司当然丝毫没有受到任何影响。哪怕他提交辞呈,第二天也会有一份新的工作在等待着他。他曾经有一个月一直没有去过上班,期间每一天都接到不同的人事部门打电话询问情况……
他也觉得他疯了。但也许他没有疯,疯的是整个世界。他妥协了。他每天都精神抖擞地带着变得更疲惫的心灵,按部就班地前去工作。工作的内容无一例外,都是坐在电脑前敲打键盘。每一天,都没有任何不同,周而复始地重复着别人的人生。
直到昨天,他遇到了那个红色的女孩。那一抹红是那么刻骨铭心,以至于他的脑海中都只剩下那片红。其他的事情,他已经想不起来了。
熟练地在计算机上敲入提交代码的指令,静静地等待代表测试的黄点变为绿色,他知道今天一天的工作已经结束了。他合上笔记本,将笔记本锁在储物柜,然后离开了公司。
坐在地铁上,他第一次对未来产生了所谓“期待”的情绪。他期待再次见到昨天的那个女孩。他知道,那个女孩一定是有什么特殊之处。不知为何,他明白那个女孩持有一切的钥匙——关于他的存在,他的记忆,以及他的人生。
但除此之外,他却是一片迷茫。他不由自主地胡思乱想。难道那个女孩是神派来的天使?他依稀记得那个女孩有天使般可爱的容颜,但他却怎么也想不起女孩的容貌。难道那个女孩是使他进入这个轮回的罪魁祸首?他依稀记得女孩向他招了招手,但却无法回想起那时她的动作。或者,难道那个女孩只是他的幻觉?他想到这里不由得生出一股恐惧,但他马上强迫自己将这个想法埋藏心底。
地铁到站了。通过剪票机,他走上自动扶梯,眼前仍然是灰蒙蒙的天空下灰蒙蒙的城市。他左顾右盼,却再也看不到那般鲜艳的红色。突然,他看到了!他看到了远处楼顶似乎闪过了一缕亮丽的红色,一如他记忆里最深的铭刻。
意识到的下一个瞬间,他已经在路上奔跑了。此刻,他的心情似乎都与风合为一体。他越跑越快,跨越了任何时间与空间的阻拦。红灯也不能阻止他的跨越——倒不如说此时此刻,整个世界对他来说只剩下唯一的一抹红色。近了,近了,那一缕红色也越来越鲜活,在风中摇曳着。
他跑到了那栋楼下,仰起了头——
一面红旗在迎风飘扬。那象征着烈士鲜血的颜色是那么深沉,似乎永远都不会褪色。远处仿佛传来了嘹亮的胜利歌声。
——他再也没有见过那个女孩。
此调查长期有效,欢迎各位继续 参与或者提交反馈 。
调查截止至 2017 年 4 月底,共收到 10 份调查结果。青色旋律会参考调查结果来决定以后博客准备哪些方面的文章。注意调查结果只反映参与者的兴趣和关注度,不是投票选举之类的。青色旋律会努力回应各位的期待,但未必会优先写票数最多的那一个。毕竟写博客是需要灵感的嘛……
如果您有什么特别想看的内容,也可以继续 参与调查 并进行投票。有什么想法的话也可以在调查的意见反馈和提问处进行提问。
首先,作为技术博客,有很多人支持编程和开发,青色旋律感到很欣慰……不过,其他的类别也有很高的人气呢。互联网和软件目前只推出了系列的第一篇, 《简单易懂的网络求生指南:密码和账户篇》 ,还有很大的坑需要填。之后也可能会记录下其他软件或者网站的使用心得吧。也许会有一篇 Hexo 相关的教程,敬请期待吧。
自由软件和开源方面,暂时还没想好要写什么。如果要写的话,可能会从授权协议开始吧。至于开发或者参与社区的心得……很遗憾青色旋律这边并没有太多经验。
Minecraft 与其他游戏方面,可能会有些有趣的东西。青色旋律对于红石还是有一些研究的,所以大概会做实用的红石教程吧。至于复杂的计算或者红石逻辑电路之类的,大概不会涉及。
其他受到关注的还有哲学与三观、日记、新想法和设计这三个分类。这三个分类大概是想到就写,想不到就不写吧。目前灵感比较枯竭,可能没什么好点子呢……大家有什么好的想法吗?
小说和三题点心,经济与商业是最冷门的两个分类。也许严肃文学这年头已经不畅销了吧?(笑)
反而是同属于虚构类的脑洞文学关注度很不错,这个青色旋律其实已经发了两篇了,分别是 无题 和 的世界 只是……中二度有点爆表,感觉好害羞……还有些其他的脑洞不敢写不敢发……
青色旋律非常喜欢音乐呢,希望能写出一点什么东西来。不过文章的形式感觉好难呢。推荐几首曲子倒是没问题啦,不过文字部分应该写什么呢?请大家多出出主意吧。
在“其他”分类中,唯一的一个建议是 女装照/女装日记。首先,提出这个的人大概是误会了什么。其次,无论怎么想,青色旋律都不可能在博客上发照片的吧……这里毕竟是严肃的技术博客来着……大概……?总之,无视掉吧。
目前的两个系列,《简单易懂的网络求生指南》和《JavaScript: 从入门到放弃》都受到了大家的热切关注,非常感谢! JS 入门已经连载了 总共三篇 了哦,大家快去看看吧~顺便,就算是太难了看不懂或者之类的评论也好,请大家至少在评论中给点反馈吧?不然可能这个系列就直接进入最后一篇——放弃篇了哦?
网络求生指南系列是需要大量查阅资料整理的,所以估计会更新比较慢。下次预定的主题是加密与存储。如果大家有什么想法,或者想要推荐相关的软件,欢迎在评论区告知~
关于技术问答,其实这个一直是属于开放状态的,只是一直没什么人提问。根据上次某位前辈的提问,青色旋律已经撰写了 《JavaScript 之 Promise 简介》 这篇详尽的介绍作为回答了,不知道大家觉得还满意吗?如果有什么其他的问题,欢迎通过评论区、私聊等途径进行提问。青色旋律随时欢迎任何关于技术的疑问和探讨。
你问我答(Ask me anything?)这样的系列稍微冷门一点点,不过关注的人数仍然不少。这个应该很容易实施吧,只要哪天开放一次提问就好了啦。不过,如果人气不够的话,可能会不太热闹,最后变得很无趣。到时候还请各位帮忙多多宣传。
其他系列中,唯一的建议是 OPSEC. 说实话,青色旋律并不是很清楚这边能有什么 critical information. 青色旋律并没有军事背景,也不会泄露什么军事机密啦。如果只是普通的加密话题的话,已经在计划《简单易懂的网络求生指南:加密与存储》和《简单易懂的网络求生指南:通信安全和隐私》了吧?所以,请支持求生指南系列吧。
此外,还有对青色旋律博客的意见或者建议,匿名提问区,以及“有什么话想对青色旋律说吗?”三个版块。
目前,还未收到任何实质性的意见或者建议,也没有什么比较好的匿名提问。但青色旋律仍然会一如既往地欢迎任何评论。如果大家懒得打开调查的话,也可以在评论区告知。
“有什么话想对青色旋律说吗?” 这个问题的答案属于绝对隐私,并不会在这里登载。但非常感谢各位的支持。
以上就是目前收到的结果,以及一些今后的计划。此调查 长期开放,欢迎大家继续提交结果,或者点击页面右上角的进行投票来参与。新提交的调查结果当然也会参考。
由于目前为止只有 10 份数据,很难得出什么结论呢……青色旋律希望有更多人提出意见和建议。无论是参与调查也好,或者简单的在下方评论区评论也好,大家都可以共同参与青色旋律博客的建设。
嘛,今后也请多指教啦~
]]>JS 的类型机制一直以来都被人认为是最坑的几个领域之一。这次的内容是数组、对象和其他类型,并着重介绍 JavaScript 与其他语言不同的地方,讲解语言规范的行为和规避误区。假如各位已经学过了其他语言,可以放心阅读。如果没有学过的话,可能需要自己适当进行补充。
不过在此之前,同学们请把 上次的作业 交上来~这次会首先讲解上次的作业。大家,和青色旋律一起继续愉快的 JavaScript 旅途吧~
(此文经过了彻底重写,和之前学习小组的讨论记录完全不同,看过讨论记录的建议重新读一次本文,相信一定会有收获。主要加入了类型的详细说明和 ES2015 新特性。)
上次的作业 中布置了摄氏度转换为华氏度、交换两个变量的值以及 JS 作为计算器三项内容。不知道大家都完成了吗?如果还没有完成的话,请在 上次文章的评论区 提交作业。
已经提交的作业,均已经在原先的评论下面进行了点评。作业并没有截至时间,各位仍然可以继续提交上次的作业,青色旋律仍然会进行评阅。但是,请不要照抄下面的解答哦。
编写程序计算 39 摄氏度等于多少华氏度。要求将摄氏度的值保存到变量中,然后在计算公式中使用变量。输出的结果范例:
39 摄氏度等于 ? 华氏度。
其中 ? 的地方需要替换为正确答案。可以看到,需要连接字符串和数字以实现这样的文字输出。
解答:首先,需要一个计算的公式。上网随便搜索一下,可以得到类似这样的公式:
°C x 9/5 + 32 = °F
之后,使用 JS 的算术表达式实现这个公式:
c * 9 / 5 + 32
当然,我们需要使用变量来代表摄氏度和华氏度的值,并且最后需要输出,这样就得到:
|
|
由于 JavaScript 本身的运算符具有
优先级
,乘法除法会优先运算,所以 32 + tempC * 9 / 5
也可以得到正确的结果。并不是像老式计算器一样完全是从左到右运算。
输出的场合,也可以使用 模版字符串 来代替字符串的 +
连接操作。比如:
console.log(`39 摄氏度等于 ${tempF} 华氏度。`);
这里, ${
和 }
之间可以是任意表达式, JavaScript 会将表达式的值计算完毕并转换成字符串再插入到对应的位置。这个例子中和 +
操作没有区别,至于实际使用中使用哪种写法可以全凭喜好。但是,要注意模版字符串在只支持 ES5 的旧环境中不可用哦。比如过时的浏览器、浏览器旧版、低版本 Node.js 等。
此外,有一些同学的计算公式就是错的。请参考上面正确的公式订正。
假设 alice
有 8 个苹果,而 bob
只有 5 个。他们互相交换所有的苹果,然后请问
alice
和 bob
分别有几个苹果?
这个作业要求用 alice
和 bob
两个变量存放苹果数量,然后输出初始的各自苹果数量。在程序运行的时候交换两个变量的值,然后再输出一次。
解答:首先,这样是不行的。
|
|
JavaScript 执行顺序语句会有先后。先遇到的语句先执行,所以在第三行 alice = bob;
时就已经把 bob
的值赋值给 alice
了。此时 alice
和 bob
都为 5
, 变量
alice
不再保存有原先的值。此后再执行 bob = alice;
相当于 bob = 5;
, 并不会获取 alice
原先的值。所以这样交换失败了。
对于已经学过编程语言的各位来说,这个应该是常识了。对于刚接触编程不久的各位,可能要花点时间理解下变量、赋值等概念。但有一点是可以确定的, JavaScript 语言会忠实地执行输入的任何指令,哪怕结果不是我们想要的。实在想不到的情况下,可以上网搜索下。
常见的办法之一是使用额外的变量来保存值。
|
|
这里可以理解成让第三位朋友 carol
先拿着 alice
的苹果,然后依次传递。这个传递有点像是按一个圈来传递,回避了 alice
被覆盖,值消失的问题。这个方法,对于所有类型的值都有效,字符串也是相同的道理。由于这里不是讲计算机原理或者编程入门的课,所以更详细的解释就跳过吧,如果仍然不理解的话可以让朋友帮忙解释。
如果 alice
和 bob
都是数,那么也有以下解法:
|
|
这个方法可以类比成 alice
先拿着所有水果,然后再分给 bob
一些, 剩下的留给自己。这是属于相对“聪明”一些的方案,回避了使用额外的变量。然而,这个也有其局限性,例如字符串的场合无法处理(没有合适的 -
操作)等。当然,最大的问题是这个计算并不总是会得出正确的结果,受限于精度等情况,有时交换结果会发生变化。当然最坑的还是其中一方是 NaN
或 Infinity
, 虽然也是数类型,但计算结果……各位自己试试看就知道了。
只是这个题目的话怎么样都好啦,但是正常写代码的时候,太过于“聪明”的这类方法是不推荐的,因为这给别人理解代码造成困难。此外,还要考虑上述的情况……与其相比,多用一个变量也没什么坏处。可能有人会误以为多用一个变量会降低效率或者内存占用会上升,青色旋律在这里只能回答说未必。实际的性能需要在实际的程序里进行测试,并且假如这个代码在关键路径上或者成为瓶颈才需要优化,更何况实际测试的结果还未必哪个好呢。
此外,还有一个常见错误是使用 const alice = 8;
. 使用 const
声明的变量不可再次赋值,因此后续的代码会没办法正常执行。
在支持 ES2015 的环境下,还有一个方法可以使用:
|
|
这个方法的原理是把 bob
和 alice
放到一个数组里,然后
解构赋值
到相反的变量里。那么问题就来了,什么是数组,又怎么用呢?请看这次的教程介绍吧。
在支持 ES2015 的环境下,青色旋律比较推荐最后的方法,因为语句简洁明了,也避免了引入额外的变量。当然,也是要在理解数组和解构赋值的前提下啦。无论如何,实际编码中遇到只需要交换两个变量的情况也比较少,此处仅仅作为范例啦。
这个实际上属于开放问题。之前也说过,选题不一定要是编码相关的问题,也可以是别的。
无论选了什么样的计算,都需要注意变量名的选取是否明确、计算本身的正确性,以及代码整体是否可读。如果表达式过于复杂的情况,建议拆成几条语句分别计算,中间的值用变量来保存。这种类型的计算比较适合使用 const
变量,并且有意识地回避让一个变量表示多个值的情况,可以有效提高代码可读性。
一个做的比较好的例子可以参见 CatLemon 的解答 。在这里也顺便表扬一下,这份解答整体质量都比较高,前两道题目至少正确性也是没什么问题的。
关于使用中文作为变量名,其实在 JS 里确实是没什么大问题的,也很少会遇到不兼容的执行环境。然而,必须要注意的是文件的编码,容易给自己添些奇怪的麻烦,所以一般来说并不推荐。此外,在目前日益国际化的开发者社区,感觉不写英文比较难办,至少在开源项目里是这样的。注释和文档同理也请尽量使用英文。除非有人故意不想让不会中文的人读不懂,或者预计不会中文的人不可能需要读这份代码(可能性太低,忽略)。
以上是作业解答,各位明白了吗?还是更困惑了呢?嘛,还有不懂的再留言追问吧。
上次介绍了 JS 的两大类型, number
(数)和 string
(字符串)。这两种值在
JavaScript 中被称为原始值 (primitive values), 相应的类型称为原始类型。原始类型还有 boolean
, symbol
, undefined
, null
四种。
此处先介绍 boolean
这一类型。这个类型只有两种值, true
和 false
, 对应真和假。比较运算等会返回布尔值。
例如, a > b
, a <= b
, a === b
等运算返回值是 true
或者 false
. 其中
===
运算称为精确相等,具体运算规则请参考 相同与相等 一节的详细描述,此处只要记住 a === b
大致可以认为是类型相同且值相同则为 true
即可,但 NaN
和自己不相等。 ==
称为模糊相等,具体判断规则极为复杂,请参考 本文最后的说明,青色旋律不推荐各位使用。相等运算也有对应的不等于运算符 !==
和模糊不等 !=
,
其结果与对应的运算符相反。
而 >
, >=
, <
, <=
四种运算符称为关系比较。 JavaScript 中关系比较有两种比较模式,分别是数值比较和字符串比较。在比较前,会先把两侧都
转换为原始值, 然后如果两侧都是字符串,则会使用字符串比较模式,会按 字典序 进行比较。如果至少一侧不是字符串,则会将两侧都转换为 number
然后再比较其数值。数值比较中如有 NaN
参与,一律返回 false
, 其他的比较基本上都是符合数学规律的。不考虑 NaN
的情况下, JavaScript 满足 a <= b
与 a > b
的结果相反, a >= b
与 a < b
的结果相反。
根据上述比较模式判定规则, "12" < "2"
(字符串比较), 12 > 2
(数值比较)。此外特别注意转换为原始值后,只要有一侧不是字符串就会强制都转换为数值比较,所以
"12" > 2
会转换为 12 > 2
(数值比较),运算结果为 true
.
有三种逻辑运算常用于布尔值,分别是 !
(逻辑非), &&
(逻辑与)和 ||
(逻辑或)。如果 a
是布尔值 !a
返回相反的值, a
是 true
则返回 false
, a
是 true
则返回 false
. 如果不是布尔值的情况下会隐式转换为布尔值,然后再取非,具体转换规则见本文 转换为布尔值 一节。
a && b
是逻辑与操作。如果两侧都是 boolean
值,那么 a
和 b
都为 true
时返回 true
, 否则返回 false
. a || b
是逻辑或操作,如果两侧都是 boolean
值,那么 a
与 b
都为 false
时,返回 false
, 否则返回 true
.
常见的用法是两个条件的组合,比如 (a > 3) && (a < 7)
表示 a
在 3~7
之间(不含两端),或者是 a === 3 || b === 3
(a
, b
其中至少一个为 3
). 由于运算符优先级规则,比较运算符优先于逻辑运算符,所以此处两侧括号可以省略。
此外, JS 还有一个 a ? b : c
的三元操作符,其行为是计算 a
并转换为布尔值,如果是 true
时计算并返回 b
, false
时计算并返回 c
. 注意没有选择的那个分支是不进行计算的,这个行为称为短路。这里 a
就像是信号灯,决定了接下去走哪条路。学过 if-else
语句的各位会发现,这个语句可以作为 if-else
语句的一种简化。比如
let discount = onSale ? 0.5 : 0.1;
表示甩卖时打 5 折,平时打 9 折。这等同于:
|
|
a && b
类似于 a ? a : b
, 唯一的不同点在于表达式 a
只会计算一次,不会重复计算。 a || b
类似于 a ? b : a
, 选择正好相反,同样地 a
也只会计算一次。虽然这两种操作常用于布尔值,但实际上对于非布尔值也能使用。通过上述的类比,可以很明确地看到返回值一定是 a
或者 b
其中之一,但不一定是 boolean
值。要注意
a && b
和 a || b
并不含有任何强制转换的成分,而是只是在判定选取哪个时,临时计算了一下 a
所对应的布尔值作为依据。
也可以看出,这两个操作左侧和右侧地位不完全相同,左侧表达式一定会计算,然后就像是一扇门一样,假如不满足特定条件,直接返回左侧值,右侧表达式不再计算(短路)。满足条件才会计算并返回右侧表达式。 因此, 0 && 2
结果是 0
(因为 0
转换为 boolean
结果为 false
),而 33 && "hello"
结果是
"hello"
, 返回了右侧值。类似地, 0 || 2
结果是 2
, 33 || "hello"
结果是
33
, 选取的那一侧正好相反。
因此, ||
也可以用于处理默认值。比如
let discount = specialDiscount || defaultDiscount;
这个计算,假如 specialDiscount
是不为 0
和 NaN
的数,则使用
specialDiscount
, 否则使用 defaultDiscount
. 这个结果与
specialDiscount ? specialDiscount : defaultDiscount;
相同,如果各位学过 if
语句,就会明白这个例子相当于以下 if-else
语句:
|
|
不过无论上述的哪种写法, specialDiscount
只要能转换为布尔值 false
, 就会选择另一个分支。这包括了 false
, 0
, NaN
, ""
以及后述的 null
, undefined
几种值,有时候可能会覆盖面太广,导致一些问题。如果要精准判断并取默认值的话,可以用 (specialDiscount !== 0) ? specialDiscount : defaultDiscount
这样的写法,来明确地表示究竟如何选取。
undefined
类型只有一种可能的值,为 undefined
. null
类型只有一种可能的值,其值为 null
. 这两种值是 JavaScript 语言中意义上最接近“空值”的存在。试图在这两个值上获取、赋值、删除属性(后述)会直接导致 TypeError
错误,有点像是其他语言的 NullPointerException
或者空指针。
undefined
通常意义上是表示“没有值”。未赋值的变量比如 let a;
会具有初始值
undefined
. 没有返回值的函数(见后篇)默认返回 undefined
. 访问不存在的属性(后述)会获得值 undefined
. 此外,在代码中也可以使用 undefined
全局变量来获得这个值。请注意这是一个变量,不是一个关键字,所以理论上自己也可以弄一个变量叫这个名字……但是,请不惜一切代价避免。此外,void 0
这个表达式也返回 undefined
,
而且因为 void
是关键字,所以没有上述问题。如果看到其他人写 void xxx
或者编译器编译出来的结果是 void xxx
, 请自动脑补成 undefined
.
null
通常意义上是表示有值,但值为空。因此,经常会用于一些接口的返回值,本来应该返回对象的,但没有找到对象这种感觉。这个情况在 DOM 或者其他浏览器 API 中尤其常见。 JavaScript 语言本身用的比较少,但原型链的顶端是 null
(青色旋律可能会在其他文章中描述原型链,此处暂时不提。)null
没有默认值的概念,而是更接近于其他语言的“空指针”。这从 typeof null
返回 "object"
(后述)也可以看出,其实 null
设计之初是当做对象的空引用的。(实际上不是对象,是原始值,只是概念上接近。) null
是一个关键字。
由于 undefined
和 null
转换为布尔值时 为 false
, 所以经常也会用 ||
来为可能为空的值提供默认值,比如 const myObj = specialObj || defaultObj;
. 当然,正如上面所说的,这个也会误判 specialObj === 0
等情况,所以还是建议使用
specialObj === undefined
之类的方式进行精准判断。而且在设计接口的时候,应该只把
undefined
认为是默认值,而 null
不应该作为默认值处理。比如说, JavaScript 语言内置的默认参数(见后篇)就是这样的设计。
symbol
是 ES2015 引入的新原始类型,
符号类型 。每个符号值是唯一的,不可修改,但是可以像其他值一样传递,比如赋值。每次新创建的符号值都是不同的,获取相同符号的唯一方式是互相传递。符号类型的内部结构在 JS 语言中并不可见,没有数值或者字符串表示之类的。
symbol
值使用 Symbol()
创建,每个新创建的 symbol
都不同。创建时可以提供一个备注字符串,但备注字符串仅供调试使用,并不是 symbol
的身份。例如:
|
|
symbol
类型常用作对象属性的键,见后述对象部分。
以上就是关于原始值的介绍了。但如果要表示一组原始值,则需要数组 Array
和对象
object
. 严格而言数组属于对象的一种,但这里只讨论数组正常使用的情况和用
object
表示简单数据结构的情况。关于对象类型的说明也请参见后文的介绍。
一个数组类似于一个列表,可以存储多个值。比如 const items = ["a", "b", "c", 9];
这里 =
右侧的表达式是一个数组。 items
变量指向这个新创建的数组,数组中含有四个元素。数组表达式从 [
开始到 ]
结束,中间可以含有 0 到多项元素,以逗号分隔。每个元素是一个值。空数组用 []
表示,长度为 0. 变量本身并没有什么特别的,仍然只是一般的一个变量,只是现在指向的值是一个数组值,假如是 let
声明的变量,那么将来还能指向些别的什么东西。
使用 items[0]
可以取得 items
所指向的数组中第一个元素。下标从 0
开始。赋值的语法类似,类似 items[0] = 3;
. 也就是说,下标表达式的用法和变量的用法类似。这样就可以方便地存储一组相关的值。数组元素的下标必须是整数,或者是整数对应的字符串。 items["0"]
和 items[0]
效果相同。
console.log([1, 2, 3]);
可以输出一个数组。大部分环境都是支持的。 Node.js 可能会显示文字 [1,2,3]
, 而开发者工具等可能会支持样式、高亮甚至交互操作。
数组的每个元素都是一个值,值的类型没有任何限制,可以混合各种东西,比如
["a", 1, [2, 3]]
是三个元素的数组,第三个元素本身又是一个数组。
JS 的数组大小可以变化,比如 items.push(10);
会在最后新增一个元素,值为 10
,
这样数组会变成 ["a", "b", "c", 9, 10]
, 长度变为 5. 删除元素的情况例如
items.pop();
会删除最后一个元素,长度变回 4. 数组的长度可以用 items.length
获取。
在学习了循环语句之后,我们就可以实现对于数组的多个元素进行批量操作,那个时候就能发挥出数组真正的威力了。
青色旋律在这里提醒各位, const
声明的变量 items
指向一个数组, 其元素和长度仍然可以改变。 const
声明的变量只是不能再次赋值(这个例子中,不能指向这个数组以外的东西了),并不是“不变”或者“固定”,更不是“只读”. 此外,说成是“常量”严格来说也是不对的, JavaScript 没有常量。在以上的例子中, items
指向的目标没有变,一直是同一个数组,只是数组内部的结构有调整,可以想像成变量指向盒子,而盒子里面的物品仍然可以随意变动。
字符串在某种意义上有点像是数组。比如,假设 const name = "Alice";
, 那么可以用
name.length
来取得字符串长度(这里是 5
)。此外,还可以使用 name[2]
来获取第三个字符,返回的是字符串 "i"
. (JavaScript 没有表示单独字符的类型。)
但是,字符串是不变的值,不能对字符串进行修改。 name[2] = 'o';
并不能修改这个字符串的值,而且 name.length
也是只读的。也不能增加或者删除字符。如果要连接、替换、截取字符串时,必须要生成新的字符串。比如 const li = name.substr(1, 2);
取字符串从第二个位置 (1
) 开始的两个字符 (2
) 来形成一个新字符串。
除了 .length
等少量共同点,字符串与数组支持的操作截然不同。字符串有独有的查找、替换、取子字符串等操作。注意替换和取子字符串等都是生成新的字符串而非修改。
执行 a += "abc";
的时候,实际上执行的是 a = a + "abc";
, 生成新的字符串再赋值给原先的变量,并不改变原先的字符串长度。注意 JS 引擎可能可以优化这个,让性能比复制一遍要高一些,但语义本身不会有任何变化,字符串仍然是不可变。
因为字符串是不可变的,所以当然也就没有所谓的字符串修改,也不存在 a = b;
后修改
a
会不会对 b
有影响这种问题。即使再执行 a = c;
, 也只会让 a
指向一个新的字符串,而不是“修改”。所有的原始值都是不可变的,这也是字符串和数组最大的区别。
如果需要把字符串转换为数组,可以使用 const chars = name.split();
这样的写法。这里 name.split()
把 name
中的每个字符取出来,组成一个新的数组。如果 name
是 Alice
, 返回的 chars
就是 ["A", "l", "i", "c", "e"]
, 每个元素是一个长度为 1
的字符串,只含有一个字符。
JavaScript 的对象是一种类似于字典的数据结构。对象可以有多个属性,每个简单属性包括一个键和一个值。(对象还有很多其他功能和特性,见后述和系列的其他文章。)
一种简单的创建对象的方式是 const obj = {"a": 9};
. 这个例子中 {"a": 9}
创建了一个新的对象,有一个属性 "a"
值为 9
. 这里,这个属性的键是字符串 "a"
,
9
是这个属性的值。要取得这个属性,可以使用语法 obj["a"]
. 这里和数组不同,方括号里是一个字符串,而不是一个整数。通过这个语法,也可以给这个属性重新赋值,更改这个属性对应的值,例如: obj["a"] = 44;
.
如果碰巧属性名是一个合法的标识符,那么也可以使用 obj.a
的简便写法。创建的时候,如果属性名是标识符或者数字,也可以省略引号,例如 const obj = {a: 9};
. 但注意无论是否省略引号,用方括号还是点表达式,效果是等价的。属性名本身仍然还是字符串 "a"
, 而不是变量 a
之类的。一般为了简便,人们常常使用不带引号的创建方式以及点操作符的引用方式,但假如属性名不是标识符的情况,比如
const obj = {"foo-bar": 0};
, 创建时必须使用引号,并且只能用 obj["foo-bar"]
的方括号形式获取了。否则 obj.foo-bar
会解析成 obj.foo - bar
的减法表达式之类的。其他的字符串同理,比如 {"a^@#sdfp": 9}
, 可以是任意字符串。(青色旋律提示:
"__proto__"
这个字符串除外,有特殊含义必须避免。无论是否加引号都要避免。)
对象的创建语法中, {a: b}
左边是键,右边是值,值可以是表达式。在这里例子中,左边是字符串 "a":
的简写,右边是变量 b
作为表达式,表示取变量 b
的值,千万不要混淆了。多个属性之间用逗号隔开,比如 {foo: 1, bar: 2}
. 空对象是 {}
.
使用 {...}
的形式创建对象,叫做对象字面量表达式,每次执行会创建一个新的对象。
如果访问的键需要计算,那么只能用方括号语法,假如 const field = "id";
的话,
obj["student_" + field]
可以用于获取 "student_id"
属性。 ES2015 以来,对象字面量也可以使用方括号来计算键,比如 const obj = {["student_" + field]: 1};
.
这个语法消除了以往只能 const obj = {}; obj["student_" + field] = 1;
的不便。
试图用数作为属性的时候,会强制转换为字符串再操作。例如 obj[1]
等于 obj["1"]
.
console.log(obj);
可以用于输出对象。至于输出的结果仍然是和环境有关,各位自己试试看就知道了。这个可以作为一种调试的手段。
obj.foo = "Bob";
假如 obj
上没有 "foo"
属性的情况下,会在 obj
上创建
"foo"
属性,然后赋值为 "Bob"
. 已经有的情况下会更新(覆盖)属性值。用方括号表达式的效果相同,以下如果不特别说明,两种语法都可以使用。
取 obj.foo
的时候,比如 console.log(obj.foo);
的情况,假如 obj
上没有
"foo"
属性,则会返回 undefined
这一原始值。
但返回 undefined
不代表属性一定不存在。也可能属性存在,但值是 undefined
之类的,例如 const obj = {foo: undefined};
. 要判断属性是否存在,可以使用
obj.hasOwnProperty("foo")
然后检查结果是 true
还是 false
.
要删除一个属性,可以使用 delete obj.foo;
或者 delete obj["foo"]
. 成功删除的属性就变成不存在了,和没有添加过的效果一样。
ES2015 起,对象的属性键除了是字符串,还可以是 symbol
类型的值。
symbol
类型是一种 ES2015 新加入的原始类型,使用 Symbol()
创建,每个新创建的
symbol
都不同,如前文所述。使用 symbol
作为属性键的例子:
|
|
由于 symbol
值不是字符串,所以没有简写法,只能用方括号表达式,不能用点表达式。
obj.foo
的意思是 obj["foo"]
, 不是 obj[foo]
, 请勿混淆。由于 symbol
值不可仿造,所以如果没有这个 symbol
就访问不到这个属性了,可以用于避免名字冲突。但是,一个对象的所有属性的键仍然属于公开信息,可以用各种方式获得,所以用来做访问控制并不是绝对安全的。 Object.getOwnPropertyNames(obj)
返回一个数组,表示对象
obj
自身的所有字符串键,而 Object.getOwnPropertySymbols(obj)
返回对象自身所有
symbol
键,通过这个方式就能获得那些 symbol
的引用了,所以并非外部不能访问。
注意数组也是一种特殊的对象,所以对象的上述所有功能数组也可以用。数组的
items[0]
属性实际上就是字符串键 "0"
. 这里可以理解成数组和普通对象类似,只是对于某些键(例如 "0"
, "1"
, "10000"
等)做了特殊处理,根据赋值和删除自动更新 .length
属性,而普通的对象没有这个功能。此外数组还有继承的 .push(x)
, .pop()
等方法,具体的原理青色旋律会在今后博文中,关于原型链的部分单独讲述。
当然,青色旋律不推荐各位把数组当做对象用,只会让代码更混乱……也不推荐把普通对象当数组用,因为普通对象没有特殊的 .length
属性和数组的方法,会比较难用。此外,虽然对象可以使用字符串来存取值,很像是一个字典或者说哈希表数据结构,但直接把普通对象当字典用会面临继承、 __proto__
带来的很多问题,不推荐。 ES2015 以上有专门的
Map 对象
可以使用,支持任何类型的键,且没有奇怪的特殊行为,只支持正常的存取操作。即使您没有 ES2015 环境,也建议去找一个 字典库
或者至少读一下 为什么这么做有很多坑 。
就像刚说的一样,普通对象是不适合当做哈希表用的。普通对象更适合当做类似其他语言的结构体实例或者类实例那样用,存储确定的几个属性和值就足够了。当然, JS 的一大优势是不用像很多静态语言那样先声明类的结构然后就遵循那个固定的结构,而是可以动态创建和改变结构。但这也是坏处,滥用容易导致代码难以维护之类的。此外,如果记错了属性名,最多只会拿到一个 undefined
, 而类型检查工具之类的很难检测出这种问题,所以记得多写单元测试或者勤测试。对于初学者来说,建议每种业务对象使用一个固定的结构即可。
例如,假如在 JS 中加载博客系统的文章列表,则可以做如下的数据结构设计:
|
|
这里 articles
是一个数组,里面每个元素表示一篇文章。每篇文章是一个对象,拥有相同的结构,都有 "title"
, "url"
, "comments"
三个属性,但属性值各自不同。其中每篇文章的 "comments"
又是一个数组,里面装入评论对象。
像这样的数据结构,就很容易用循环遍历所有的文章,然后找出哪一篇评论最多,或者进行类似的数据处理任务,比起使用单独的变量更加高效。如果博客发表了新文章,也可以使用 articles.push({title: 'foo', url: 'bar', comments: []})
的方式加入到这个数组最后。如果有新的访客评论,也可以使用 articles[2].comments.push({...})
来在某一篇文章里新增评论。这样合理使用 JavaScript 的数组和对象,可以实现维护特定的数据结构。
这个例子的数据结构类似于其他基于类的语言的如下构造:
|
|
区别是 JS 这个例子没有强制类型也没有类型检查。优点是加入新的属性比较便利,比如每篇文章加入 date
可以随时添加,甚至有的文章有这个属性,有的没有。缺点是
articles[2].comments.push
等语句在早期难以进行类型检查,因为类型检查工具一般很难推断这个语句是否合法,因为 articles[2]
可能有 comments
属性也可能没有,运行时可以修改的。所以重要代码各位一定要多测试,不要认为可以运行了就没问题了。
此外,用序号来查找文章是不稳定的,因为增加、删除文章等操作可能会变更原有的序号。如果想要通过唯一标识符 (ID) 来定位文章,就需要 Map 对象 等特殊的构造,例如:
|
|
(每个文章对象的具体结构请参考上一个例子,这里不再重复。)
Map 对象支持 .set
, .get
, .has
等操作,实现了字典或者说映射数据结构。具体用法请参阅上方链接的文档。
有了基本类型、数组、对象和 Map, 大部分的数据结构也就可以在 JavaScript 中表达了。更为复杂的数据结构,例如队列、栈、树、图等都可以基于上述类型来实现。
关于原始值和对象的讲解就暂时到这里了。此外还有函数类型也属于对象范畴,青色旋律在这个系列的下一期会进行介绍。值得注意的是 JavaScript 并没有其他的基本类型了,并不像很多语言一样有表示单个字符的类型、枚举、指针等等。
在 JavaScript 中,使用 typeof exp
可以看到表达式 exp
计算结果的值属于何种类型。对于原始值,这个表达式根据值的类型,返回字符串 "undefined"
, "boolean"
,
"number"
, "string"
, "symbol"
共五种。唯一的例外是 typeof null
(或者任何计算结果为 null
的表达式)会返回 "object"
, 这是一个坑,因为 null
属于原始值而不是对象,和对象没有什么共同点。但考虑到 web 和其他平台的需要向后兼容,所以这个结果估计是不会被修复了。不存在 typeof
返回值为 "null"
的值。
typeof
还有一种特殊行为,假如表达式的右侧只有一个未声明的变量 foo
, 那么
typeof foo
直接返回 "undefined"
, 不会求值. 但这只是 typeof
的一种特殊行为,并不说明 foo
存在并且有值 undefined
。在其他场合取未声明变量的值,比如 foo + 1
还是会报错,因为这个变量确实不存在。(例外:用
let
或者 const
声明的变量,不能在同一作用域下,声明语句 之前 访问,哪怕是
typeof
也不行。青色旋律博客之后会另写文章详细说明。)
用这个特殊行为可以判断变量是否存在,但假如存在但值为 undefined
, 返回结果也完全相同,所以无法区分这种情况。比如说,判断 JS 运行环境是否支持 Symbol
, 如果直接写 Symbol !== undefined
,假如环境不支持的话执行时找不到 Symbol
这个变量。但如果用 typeof Symbol !== 'undefined'
就可以回避这个问题。
从 JS 语言规范的角度,不是原始值的都是对象。
typeof
只区分三种对象:宿主对象, function
和 object
. typeof
的算法类似于下面列出来的这些步骤:
null
, 返回 "object"
.null
以外的原始值, 返回对应的原始类型字符串。"undefined"
, "boolean"
, "number"
, "string"
, "symbol"
."function"
."object"
.可见 function
也只是一种可以调用的 object
而已。关于函数的话题要到下一次才能讨论,但这里只要明白上述的大部分针对对象的内容也适用于函数就好。
相同的对象是相同且相等的,不同的对象是不同且不相等的。 JavaScript 里赋值是传递引用,并不是拷贝或者克隆什么的。所以 const a = {}; const b = a;
只会让 a
和
b
指向同一个对象,结果 a === b
. 但 const c = {}; const d = {};
是两个不同的对象,尽管看起来没什么区别,但每次执行 {}
的时候都会创建一个新对象,如上文所述, 所以最后 c !== d
. 原始类型的相等判断则比较复杂,见后文所述。
JS 里,原始值被认为是数据的最小单元。原始值不可变。原始值没有“内部”可修改的部分。对象可能可变。
null
和 undefined
没有属性。试图获取、添加、删除属性会导致 TypeError
.
对其他原始值使用属性会将产生一个对应的箱对象.
然后属性操作等会在箱对象上进行。原始值本身无法添加属性。
任何两个不同的对象是不相同的,对象和原始值也不相同。不同类型的原始值不相同,但同类型的原始值,根据类型,分别有不同的相同判断规则:
undefined
与自身相同。null
与自身相同。true
与 true
相同, false
与 false
相同, true
与 false
不同。number
相同,不同数值的不同。string
相同,不同序列的不同。 (UTF-16)symbol
总是不同的,同一个 symbol
赋值、传递后仍然相同。(简记:undefined
, null
, true
, false
是单例,只和自己相同。symbol
值和对象一样,每个都是唯一的。number
值和 string
值都是内容相同则相同。)
此外,没有任何办法把相同的原始值区分开来。比如 1 + 2
产生的 3
和 4 - 1
产生的 3
在各种意义上都是相同的,没有任何区别。也没有办法找一个 3
出来,给它做上特殊的标记,让它和其他的 3
有不同的行为。特别地,由于无法使用属性,所以没办法在原始值上增加属性来进行区分。
ES2015 加入了
Object.is(a, b)
这个函数,专门用于判断“相同”这一概念。简单来说,不同类型的值不相同,不同的对象不相同,而同类型的原始值可能相同,见上述判断规则。特别注意的是,NaN
与 NaN
相同, +0
与 -0
不同。请牢记相同类型相同值这一判断依据。
如前述 ES2015 新加入的
Map 对象
支持任何类型作为键。判断两个值是否为同一个键的标准正是“相同”这个概念,除了 +0
和 -0
指向同一个键。原因嘛……大概是怕程序员们被计算结果的正负零坑?
而 a === b
可以用于判断两个值是否相等。相等的算法和相同类似,除了 NaN
和
+0
, -0
需要单独讨论之外,其他是一致的。这个对于原始值和对象都成立。嗯,这里有三个等于号,三个等于号会禁用类型转换直接进行比较,结果也比较好预测,大方向仍然是相同类型相同值,比较推荐使用。
至于两个特例: NaN !== NaN
(相同却不相等)以及 +0 === -0
(相等却不相同)。
至于为什么有 +0
和 -0
, 以及 NaN
为什么这么坑,详情请参阅
IEEE 浮点数 。
最后,还有一个 ==
运算符,大致是试图转换两边为相同类型然后再比较是否相等。本来这个不算是一件坏事,但 转换规则过于复杂
正常人都记不住,并且有一些令人惊讶的边界情况……
青色旋律在此推荐尽量使用 ===
进行比较。如果需要同时转换为字符串后再进行比较,请手动写 String(a) === String(b)
. 如果需要同时转换为数进行比较,也请手动写 Number(a) === Number(b)
或者 Object.is(Number(a), Number(b))
(根据应该如何处理NaN
, +0
和 -0
来选择其中一种), 这样代码的意图清晰一点,也没什么令人惊讶的情况。
青色旋律认为唯一可以接受的 ==
用法是用 a == null
来判断 a
是 undefined
或者是 null
. ESLint 可以 设置成
禁用 ==
表达式,除了这种和其他两种绝对安全的情况。
另外一个很容易混淆的转换是 !!x
或者 if (x) { ... }
的情况下 x
如何隐式转换为 boolean
值。首先,任何对象都转换为 true
, 其中包括空数组和空对象。原始值中,一共只有 7 个值会转换为 false
, 分别是 null
, undefined
, false
,
+0
, -0
, NaN
和 ""
, 其他的值都转换为 true
. 这里基本上只有 0, NaN
和空字符串需要特别去记忆,其他的都比较自然。空字符串是指长度为 0
的字符串,只有一种可能就是""
. 含有空格的字符串例如 " "
长度不为 0, 转换为 true
.
如果 x
转换为 boolean
结果为 true
, x
俗称为 truthy
, 否则称为 falsy
.
这次的讲解就到这里了哦。各位明白了吗?其实 JavaScript 的类型系统基本已经讲完啦。哪怕这次的内容确实有点多,里面有一些特殊情况和坑,但也请暂时忍受一下吧, 以后应该不会再这么坑了……大概吧?
[[Prototype]]: 哈?有人叫我? this: (咳嗽)
……呜,好吧,差点把您两位给忘了。但是,至少类型的坑已经基本解决了。大家看过以后感想怎么样呢?也没想象的那么坑吧?嘛,如果有任何问题请在评论区留言好了。
以下是一些作业,仅供参考。作业并不是必须的,但如果有人完成了,可以发布到 https://gist.github.com 上面,然后把链接贴到下方的评论区,青色旋律会逐一批改并点评的。万幸,这次的作业并不是坑,而是比较正常的数据处理类的东西,可以用于实践数组、对象等的用法。作业没有截至日期,但下次教程的时候会公布分析和参考答案。
下次教程会讲控制语句,并且会着重讲解 JS 特有的几种控制语句以及和其他语言的区别,但不会花太多篇幅讲解控制语句本身的基础。如果各位是第一次接触编程,那么请尽可能自行掌握条件语句、循环语句等概念,然后再阅读下一次的内容。必要的话可以参考流程图之类的解释说明。如果已经学过了其他语言的话,简单复习下即可。
请编写一个抽奖程序,从一组预设的奖项中任意抽取一个结果。当然,玩过抽奖的各位都知道,大部分预设的结果都应该是“谢谢惠顾”,少量其他奖项。
将所有可能的结果作为字符串保存在数组中,然后随机取出一个元素来进行抽取吧。提示:用 const num = Math.floor(Math.random() * 8);
可以获得一个 0 到 7 之间的随机数。
最后,将抽取的结果打印出来。可以多抽取几次作为测试。
改进 1: 实际抽奖的时候,大奖的概率总是低一些的。为了达到这个效果,可以在数组里只写一次”一等奖”,而写十次”谢谢惠顾”。多抽几次,试试看自己的运气吧~
改进 2: 请做成每次抽取后不返还。也就是说,抽取过的结果不能再次被抽取。
现在假设你要来帮助谷歌做 G+ 。你作为一个资深用户,深刻地体会到 G+ 是由土豪、菊苣和你自己组成的,因此在设计社交圈时,你需要充分考虑到这一点。
首先你改革了 G+ 的圈子系统,要求圈子必须包含”土豪”、”菊苣”两个分类。所有的好友,必须至少处在这两个圈子其中之一。而自己不能添加自己为好友。(背景知识:圈子是 G+ 上对于好友的分类。一个人可以创建任意数量的圈子,每位好友可以放在一个或多个圈子中。)
接下去,请设计一个数据结构,用于存放所有的人际关系数据,包括所有用户、每位用户创建的所有圈子和每个圈子中各自有哪些好友。请活用 JS 的各种数据类型来实现这个数据结构。
设计的数据结构必须支持以下查询场景:
并且,还需要支持以下的数据更新操作:
a
新建一个圈子,名字为 "foo"
.a
将用户 b
添加到 "土豪"
圈。b
原先在 a
的 "土豪"
圈中,然后用户 a
将其移除。注意到如果每次统计有多少人认为 a
是土豪可能需要遍历整个数组,效率可能会受到影响。可以考虑对于每位用户进行缓存,保存认为该人是土豪 / 菊苣 的人数。然后,每次有人把此人加入土豪圈,需要把这个数增加 1
, 而移除需要减少 1
.
假如设计了缓存或者多个数据结构的情况下,要保证以上操作时数据一致。
如果已经掌握了循环和控制语句,那么也可以开始进行这些操作的编码实现。如果没有的话,只需要在设计数据结构的时候考虑一下如何进行实现即可。
如果觉得处理了菊苣再处理土豪要写两遍太浪费时间,可以只写其中之一。但记得把代码写得通用一点,使得这个代码今后稍微做一点修改也能处理新的一类生物——学霸。
提交作业时请写一个范例,表示设计的数据结构如何保存以下这样的人际关系:
金珂拉好处都有啥?谁说对了就给他!
米国圣地亚鸽公司研发了一款新产品,正在征集电视广告词。脍炙人口的“金珂拉”顺口溜成为了一项热门的选择。已知顺口溜的原文如下:
|
|
公司请您为他们的新产品填写广告词,但新产品的名称是宇宙核心机密,您无法得知。但您可以编写一段程序,从变量 product
中获取新产品的名称,然后产生新的广告词。
例如,假设 const product = "眼镜蛤";
, 程序需要输出:
|
|
(注:产品名称应该是“眼镜蛇”,但来自东方的神秘力量导致输入法出了一点小小的问题,结果错了一个部首,导致广告报道出了偏差,没有钦定的意思!青色旋律没有水表,域名注册商在米国不同意 clientHold, 博客服务器在米国不接受删帖断电拔网线,不欢迎 DDoS. )
此外,公司此次邀请了全新的配音和演出阵容,因此广告词中涉及到的两个国家名称必须根据演员长相特点来进行调整。假设两个地名分别是变量 place1
, place2
. 例如
const place1 = "法国";
以及 const place2 = "米国";
.
给定 product
, place1
, place2
三个变量,请输出最终广告词。请注意,
product
名字未必是三个汉字,所以请判断 product
长度,并输出不同的结果。如果
product
超过四个字,取前两个字和最后一个字,例如 "圣地亚鸽"
=> "圣地鸽"
.
如果 product
是两个字,进行叠音处理,比如 "切糕"
=> "切糕糕"
. 假如是一个字,请输出错误信息,表明顺口溜无法正常完成。
对于 product
不同长度的处理,各位也可以发挥自己的想象力进行其他的文字处理,甚至根据长度不同,打印不同风格的顺口溜,甚至是其他成句或者语录。复杂的处理可能需要
if
语句,如果还未掌握请自行学习。
改进 1: 为了保证广告词不断推陈出新,请尽量不要手动拼接字符串,而是做成输入一个模版字符串,然后通过字符串替换、截取等操作输出一个新的字符串。替换的位置不能写死。比如,模版可以是 "PLACE1农业不发达,必须要有PRODUCT。..."
, 然后通过字符串替换 PLACE1
为
"非洲"
, PRODUCT
为 "金珂拉"
来生成。
各位,作业请加油~如果完成了别忘记在评论区提交作业哦。
]]>目前,很多库已经加入 Promise 支持,新的 API (如 fetch 等)很多也基于 Promise 设计, Promise 开始在 JS 社区流行起来。而随着 ES2015 (ES6) 的普及, Promise 被标准化 ,也进入了 JavaScript 标准运行环境,相比 ES3, ES5 等环境可以省去加载库的代价和麻烦。
那么 Promise 究竟是什么,又如何在 JavaScript 日常开发中使用呢?请跟随青色旋律一起来了解吧。
Promise 简单而言,是一种表示未来结果的对象。它代表一个正在进行的任务,当任务完成时会返回一个值或者任务失败时返回一个错误。有些语言或者库中, Promise 也称为 Future.
一个简单的类比是餐厅点餐的凭单。当您在快餐店点了一个汉堡的时候,收银员会给您一张小票,并提醒您稍后可以凭小票领餐。此处的凭单不是汉堡本身,但却是一个和汉堡可能有关的对象。您可以将其交给您的朋友,让其代替您领取。您也可以让服务员在汉堡准备完成的时候根据凭单号码通知您。当然,您也可以根据这个凭单来规划下一步行程,比如叫一辆车在吃完饭后来接您。但是任何对于未来的判断都具有不确定性——服务员可能会告诉您,由于餐厅缺少原料,您的汉堡恐怕没办法完成了,从而进行更改订单、投诉、退款等错误处理流程。
Promise 具有三种状态——等待中、已完成、或者已拒绝。 Promise 可以从等待变为已完成状态,或者是变为已拒绝。已完成和已拒绝是最终状态,不能再改变。已完成状态的
Promise 具有一个完成值,表示任务的最终结果,当然值也可以是 null
或者
undefined
来表示无返回值。已拒绝状态的 Promise 有一个拒绝值而没有返回值,拒绝值一般是错误对象。
Promise 本身是一个对象,可以到处传递。通过 Promise 对象可以注册回调函数等,可以做到当完成时调用此回调函数,并传入完成值,或者当拒绝时调用另外的回调函数,并传入拒绝值等等,有点像是事件机制。假如 Promise 已经是完成状态,新注册的回调函数仍然会被调用,而不会错过时机,拒绝同理。
首先, Promise 是对象,可以像其他对象一样存储、传递和赋值。相比之下,单纯的回调模式难以传递,事件机制可以传递目标对象本身但难以组合。
Promise 是一种规范化而可靠的机制。 Promise/A+ 规范(下述)的行为可预测,各种库的 Promise 可以互相兼容,且对于不完全遵守规范的实现有防御机制。 ES2015 规范在此基础上进一步定义了行为和工具,并将 Promise 引入标准运行环境。
Promise 在异步操作方面表达力极强,尤其是针对序列任务和各种执行模式,如并发、竞争等。 Promise 作为一种控制反转 (IoC) 机制,将监听和回调的任务转移给了调用方,允许更灵活的配置比如多回调函数、链式操作等。
越来越多的库支持 Promise, 新的 API 正在使用 Promise 制定。已有的回调函数等机制很容易转换为 Promise, 有库支持几乎自动的封装。 Promise 本身有大量第三方库实现,提供完善的工具集实现常用控制流。
此外, ES2015 (ES6) 的 yield
关键字使得书写基于 Promise 的控制流变得非常简单,语法几乎与同步操作等同。 ES2016 预计引入的 async
/await
机制基于 Promise.
关于 Promise 与其他模式的对比,请参考本文附录。
显而易见,要使用 Promise 模式,必须先获取 Promise 对象实例。在支持 ECMAScript
2016 (ES6) 的环境中, Promise
构造函数必须作为全局对象提供。使用
new Promise(executor)
构造函数可以创建一个可控的 Promise
,或者
Promise.resolve(value)
来创建一个初始完成的 Promise
,再或者使用
Promise.reject(err)
来创建一个初始拒绝的 Promise
.
在不支持 ES2015 Promise 的环境中,也可以使用第三方库来构造 Promise
. 比如
bluebird, Q,
when.js,
RSVP.js,
等等。以上的库都兼容
Promise/A+ 规范
,产生的 Promise 对象都可以根据下述方式使用。但是,产生 Promise 对象的方式则各不相同,有的类似 ES6 使用构造函数,有的提供工厂方法,等等。
由于实现 Promise 对象本身并不需要先进的 JavaScript 语言特性,因此 Promise 的兼容性极好,上述的有些库甚至可以兼容 ES3 运行环境。如果您还在犹豫是否要使用 ES2015, 也无须犹豫使用使用 Promise, 因为 Promise 可以像常见的 jQuery 等库一样加载后开箱即用,无须编译流程或者运行环境的特殊支持。甚至,有一些框架本身也带有 Promise 的实现,比如 AngularJS 以及 YUI.
在 Promise/A+ 规范 中, promise
是指具有 .then
函数的对象,且此函数的行为符合一定的规范。简而言之,只有 .then
函数本身是此规范的一部分,而对象的其他行为一概未定义,可以继承任何对象,拥有任何其他属性等等。符合 Promise/A+ 规范的各种对象可以互相兼容。
.then
接受两个可选参数, onFulfilled
和 onRejected
函数。
onFulfilled
回调函数必须在 promise
已经完成后被调用,传入一个参数,此参数为
promise
的完成值。onRejected
在 promise
已经被拒绝后被调用,传入一个参数表示拒绝值。两个回调函数中最多只会有一个被调用,而且只会被调用一次。实现的角度而言,这两个回调函数一定会异步被调用,不得同步调用。这样的设计使得执行顺序可控。理解 JS 运行机制的读者可以由此推断出,
promise.then(A, B); C();
的场合,一定是 C
函数先执行,然后再是异步执行 A
函数或者 B
函数其中之一,不可能是其他顺序。
一个简单的例子,假如 readFile('a.txt')
返回一个符合规范的 promise
对象,则有:
|
|
(注:本文中所有代码如无特殊说明,均兼容 ES5 及以上。有 ES2015 (ES6) 支持的读者可自行使用 ES2015 语法,如箭头函数等,不影响代码实际效果。 promise
的回调函数本身并不需要依赖 this
, 这里基本上不需要多考虑箭头函数对 this
的影响。)
这里实现了简单的成功流程处理和错误流程处理,无须使用事件机制等。如果只需要处理成功或者错误其中一种情况,则可以省略另外一个函数。注意省略完成回调的场合,必须以 .then(undefined, onRejected)
等形式调用,而不能只传递一个参数。由于这个写法实在是太丑了, ES2015 的 Promise
对象也提供了 .catch(onRejected)
这个额外的函数,其效果与上述写法相同。大部分上述的第三方库也实现了这一函数。要注意的是,
.catch
并非 Promise/A+ 标准之一,所以兼容 Promise/A+ 的库也可能会不实现这个函数或者这个函数做的是其他的事情,具体请查阅库的文档。后文中青色旋律会使用
.catch(onRejected)
这一方法,如需兼容 Promise/A+ 请自行替换为
.then(undefined, onRejected)
.
.then
函数可以在一个 Promise 上执行多次,注册多个完成或者拒绝回调。回调的执行顺序与注册的顺序相同,比如:
|
|
.then
函数本身也会返回一个 promise
对象,而这个新的 promise
行为和两个回调函数有关。(注意 promise
以外也可能触发此行为,见后述。)
首先,两个回调函数如果成功执行,返回的值将决定新 promise
的成功值,例如:
|
|
此处 getConfig
是一个新的 promise
,假设文件读取成功,内容是合法 JSON 的情况下, getConfig
会以 JSON.parse
的返回值完成。假如文件读取失败,则 return null;
会导致 getConfig
以 null
值完成。由于
JavaScript 本身的函数机制,函数不 return
相当于最后 return undefined;
, 会使得新 promise
以 undefined
值完成。
但这里有一个特例,假如返回值是 promise
, 那么新的 promise
结果和返回的
promise
相同。直接举个例子理解起来会比较快:
|
|
这里 getConfig
接上一段代码。可见,无论哪条执行路径,readFile
都会被调用,这里假设 readFile
返回一个 promise
(这里称为 p1
). 那么 getData
的结果会根据 p1
变化。如果 p1
完成则 getData
也完成,完成值相同。 p1
拒绝则
getData
也拒绝,拒绝值相同。这个特性被称为 Promise 链式使用 (chaining
), 是
Promise 重要的特性之一。这一特性大大简化了异步序列的写法,例如:
|
|
这其中涉及多个步骤,每个步骤可以是同步或者异步(返回 promise
),按顺序执行。当同步操作变为异步的时候也无须修改代码结构。通常认为,这种写法与回调函数风格相比,避免了大量嵌套、管理多个回调函数等繁琐的编码,且可以进行灵活的异常处理(后述)。
注意对于返回值的判定实际上非常宽松,并不一定是 promise
才会触发以上特殊行为。事实上,任何具有 .then
属性且此属性为函数的对象(俗称 thenable )都会触发此行为,主要是为了兼容那些并不完全符合 Promise/A+ 规范的对象。这一行为在 Promise/A+ 规范
中有详细描述。
但是这也带来了一个问题,就是有 .then
属性且属性值为函数的对象可能无法正常在
Promise/A+ 机制下运作。本来认为是最终值的其他对象,仅仅是因为碰巧也有一个函数类型的 then
属性就被误判从而导致错误调用。(另一种可能,对象有一个 .then
的
getter, 然后 getter 抛出异常也会导致新 promise
被拒绝,拒绝理由和抛出异常相同,此类对象少见但并非不存在。)此行为不可避免,只能通过把这种对象装在数组里之类的方式绕过,比如 return [obj];
然后处理的时候打开数组。如果您是一个库的作者,需要用到 promise
,对于任何用户提供的对象,请务必小心。
任何正常的兼容 Promise/A+ 的库,基本上都不可能出现 Promise 中嵌套 Promise 的情况,包括 ES2015 规范规定的标准实现。事实上 Promise 里套 thenable 正常情况下都不可能。任何这种尝试基本上都会因为上面那个算法而解套。因为仔细考虑一下,其实
Promise 里套 Promise 是没有用的,正如未来的未来还是未来。当然,如果有一个对象,刚开始没有 .then
函数然后后面又有了的话……但愿正常人写代码没这么坑吧。
假如 onFulfilled
或者 onRejected
执行时抛出异常,则新的 promise
会变为拒绝状态,且拒绝值为异常对象。这个特性可以用于错误处理。
|
|
这个例子中,第一个 .then
调用返回一个 promise
对象,赋值给 getConfig
.
然后,在 getConfig
上注册了一个拒绝回调来处理可能发生的错误。在执行时,假如
contents
不是合法 JSON, JSON.parse
会抛出 SyntaxError
, 而此错误将会导致
getConfig
变为拒绝状态,最终导致最后一个函数运行,打印出 Invalid JSON
开头的信息。注意是 getConfig
这一 promise
变为拒绝,而不是 readConfigFile
变为拒绝状态。后者已经是完成状态了,状态不可能再改变。因此,第一个 .then
上注册的拒绝回调函数也不会运行。
假如拒绝回调函数没有注册的话,那么返回的 promise
会直接变为拒绝状态,且拒绝原因和第一个 promise
相同,这个也称为 promise
的错误传递。利用此原理可以统一处理错误:
|
|
此处中间任何一环出错,都会执行最后的错误处理函数。在编写 promise
风格的代码时,推荐在任何 promise
链的最后增加 .catch
,至少打印错误以便调试。(由于所有执行时抛出的错误都会被捕获,所以不存在全局异常的情况,不会导致进程退出等。但这也导致了执行环境通常不把这些当做错误来反馈。有的环境,如较新版本的 Chromium 和
Firefox ,会在控制台中打印出“可能未处理的拒绝”以方便调试。)
因此,通常来说 Promise 编程中,很少会使用到 .then(onFulfilled, onRejected)
这种写法,而是 .then(onFulfilled).catch(onRejected)
比较多,后一种写法可以顺便把 onFulfilled
代码中抛出的异常也处理了。没有 .catch
的情况下,也建议使用 .then(onFulfilled).then(undefined, onRejected)
.
当然,也可以 .catch(onRejected1).catch(onRejected2)
,假如 onRejected1
也可能会抛出异常的话,类似于嵌套 try-catch
.
此处也要注意抛出异常不是唯一的拒绝方式。如上一节所述,如果返回值是一个 promise
则新的 promise
结果与其相同。这也意味着,如果 return promise1;
之后
promise1
拒绝,那么处理与上述相同。 ES2015 规范中提供函数
Promise.reject(val)
可以直接创建一个已经拒绝的 promise
, 可以代替
throw val;
使用,例如:
|
|
Promise.resolve(val)
假如 val 是普通值,会直接返回一个已经完成的 promise
.
如果 val 是 thenable, 返回一个行为和 val
状态相同的 promise
, 具体算法与上述
thenable 算法相同。这两个特性都在 .then
的回调函数里没什么用(因为行为与 return
相同),但适合作为 Promise 链的起点。
除了以上提到的 Promise.reject
和 Promise.resolve
以外, ES2015 还提供不少好用的 Promise 处理函数。这些函数大部分常用 Promise 库也有实现,但名称和参数可能不同。
Promise.all(iterable)
接受一组 Promise, 返回一个新的 Promise. 如其名所示,假如所有 Promise 都完成,返回的 Promise 也会完成,且完成值是一个数组,每个元素是各自的完成值。假如其中任何一个拒绝,返回的 Promise 也会拒绝且拒绝值相同。这个函数可以用于聚合多个 Promise 的值,可以用于实现并发模式:
|
|
(青色旋律温馨提示:假如说 readFile
本身的实现在一个文件读取完成之前会阻塞第二个,那么代码仍然能运行,只是性能可能没有想像的那么好。同理,假如一个数据库操作函数内部使用的连接池容量有限,那么并发级别也不会是无限的。用了这个模式不代表就提高了并发,更不代表提高了性能。)
Promise.race(iterable)
也是接受一组 Promise, 但返回的 Promise 结果与第一个完成或者拒绝的 Promise 相同。正如其名,这一组 Promise 互相为竞争关系,第一个状态变为已完成或者已拒绝的 Promise 决定最终结果。这个可以用于实现竞争模式:
|
|
以上假设 rejectAfter(3000)
返回一个 Promise, 3000
毫秒后必定拒绝,且拒绝值满足 instanceof TimeoutError
. 这段代码实现了下载文件,无论出错还是超时均认为是失败这个逻辑。青色旋律温馨提示,此逻辑第三方库里有更好用的工具,如 bluebird 有
downloadFile().timeout(3000)
,一般无须自己实现,此处仅为范例。
首先,业务代码中大部分 Promise 都不是用构造函数创建的。一般都会以某个库(例如数据库 JS 封装)或者 API (例如 fetch
)返回的 Promise 作为起点,然后用
.then
来创建更多的 Promise 链。因为异步的来源一般是外部因素而不是内部因素。
如果您在和旧式的 API 打交道,而旧式的 API 还不支持 Promise, 而是使用 Node.js 风格回调等方式,那么也请先考虑使用第三方库等来把那些 API 封装为 Promise API. 例如, bluebird 库存在 Promisification 这个功能可以用于转换。链接也讲述了如何封装常见的几个库。手动转换往往面临考虑不够周全,一些边界条件下会出问题之类的陷阱。
但偶尔,可能也需要实现一些自定义的控制流,此时可能需要 new Promise
等。这个构造函数的常用方式是 new Promise(function (resolve, reject) { ... })
, 返回一个
Promise, 当 reject(x)
被调用时以 x
拒绝,或者函数抛出异常的时候以异常值拒绝,当 resolve(y)
被调用且 y
不是 thenable 时以 y
完成,否则按照之前提到的
thenable 算法决定状态。可以看到, resolve
和 reject
两个函数由 ES 执行环境提供,是用于手动控制 Promise 状态的手段。例如,可以手动实现前述的 rejectAfter
函数(假设 TimeoutError
已经定义):
|
|
(青色旋律温馨提示:此逻辑第三方库里一般已经有实现,如 bluebird 有
Promise.delay(3000).throw(new TimeoutError())
, 此处仅为范例。)
或者手动将一个事件 API 转换为 Promise:
|
|
(青色旋律温馨提示:如果浏览器支持,应直接使用原生支持 Promise 的 Fetch API 代替 XHR, 或在不支持的浏览器中使用 Fetch API Polyfill, 此处仅为范例,不推荐此类写法。)
第三方 Promise 库中也可能有类似的构造函数,或者工厂函数,以及 deferred 等其他 API, 详情请查阅您所使用的库的文档。
第一个例子是需要读取三个文件,但此处要求串行读取,也就是说同时只能读取一个文件。这个例子的正确代码如下:
|
|
这里的麻烦之处是需要等上一步完成后再进行下一步,还要另外收集所有的结果。如果自己写的话,很可能会出问题,比如如下所述是一个错误写法:
|
|
这个看起来好像是对的,但其实在 files.map(readFile)
的时候,所有的 readFile
调用就已经执行了,结果其实是实现了并发模式。同理,永远不可能做出来一个函数,实现
serialExecution(promises)
这种签名然后返回序列化的结果,因为任务指令已经发出了才会返回一个 Promise. Promise 本身是任务结果的代表,而不是任务执行的控制器。
青色旋律在这里向大家推荐 bluebird 库的 .mapSeries 工具 。使用这一工具函数,代码可简化为:
|
|
类似地,还有 .map(elements, mapper, {concurrency: 3})
这样的工具来实现按指定的并发量来运行。
关于其他常见 Promise 任务,青色旋律会陆续在此进行一些介绍。如果大家好奇用 Promise 如何做某件事,也可以在评论区留言来提问。
以上就是 Promise 的介绍和常见用法啦。大家在做新项目的时候,不妨试试看哦~对了,以防有人不看上面的内容直接跳到总结,青色旋律有必要再次提醒大家,使用 Promise 无须 ES2015 (ES6) 支持,只要找个合适的库即可,技术投资并不算太大。
如果您在计划一个新的库,不妨提供 Promise API, 然后使用 各种方式兼容 Node.js 样式的回调,这样两派人士都能开心地使用。内部逻辑可以完全基于 Promise 实现。
如果您有一些陈旧的代码需要重写,其中异步的逻辑非常混乱,也不妨试试看使用 Promise 重新理清思路。
如果您碰巧正在使用支持 ES2015 的环境,那么您也可以使用 yield
来配合 Promise
改善代码流,但需要一个支持 coroutine 的库,比如 bluebird. 或者您也可以走在时代的最前沿,使用 async
/await
关键字来改善流控制。不过后者目前尚未收录进规范中,实现基本上也只有
babel 等。
那么,青色旋律这次的介绍就到这里啦。各位下回再见~
Promise 的一个重要的特性就是如果完成,所有完成回调函数一定会被执行。如果拒绝,则所有拒绝函数一定会被执行,哪怕调用 .then
时状态转移已经结束。例如:
|
|
假如文件读取足够快,可能用不到 10 秒,readFilePromise
就已经是完成状态了,但之后再调用 .then
也仍然能够继续执行代码,而不会像事件一样,晚注册的就不会收到事件了。使用事件机制的 API 受到这个影响,很可能需要额外地检查属性等来判断任务是否已经完成,影响事件处理代码的表现力。
Promise 的缺点在于无法表达可能重复发生的事情,因为 Promise 最多只能经历一次状态转换。另外一个设计模式 Obserable 提供这样的支持。
总而言之,青色旋律认为 Promise 适合表达“任务”,而非发生的“事情”。事件机制则相反,各有不同的用途。当某个任务来自于事件,则应按照上述的方式将其封装为 Promise.
事件机制对于多步操作或者控制流并无支持,一般而言编写代码较为尴尬。
首先, Promise 是回调模式的一种,其 API 是基于回调的。一般而言说 Promise 和回调模式对比,是指比较自由的回调模式,或者 Node.js 风格的回调模式。至少也是指用于监听任务处理完成的那种回调,与 array.forEach
之类的回调没有任何可比性。
Promise 是一种“严格管理”的回调。 onFulfilled
和 onRejected
只能执行其中一个,且最多只能执行其中一次,就是一个很好的限制。回调模式下,如果代码有问题会导致多次调用从而导致难以调试的 bug, 例如:
|
|
这个例子中,虽然粗看并没有什么问题,但假如 ret.err
为真值, callback
被调用后未返回,会导致接下来的代码继续执行,说不定会出错,也说不定 callback
会再被执行一次,导致难以调试的错误。 Promise 模式无此问题:
|
|
首先,通过多步操作,将非 Promise 的代码隔离在比较小的区域内,很难出现拒绝了还可以继续执行的问题,或者忘记之前已经调用过回调的问题。其次,哪怕真的调用了多次
resolve
和 reject
, 甚至都调用了, Promise 库只会处理第一次,调用者不会被这个问题困扰。当然,假如库的维护者不写单元测试, Promise 当然也拯救不了他们……
此外, Promise 机制自动保证 onFulfilled
等回调异步执行,有效解决了
Zalgo 的问题。
总结而言,任何 API 必须同步返回或者异步返回,不能有时这样而有时那样,否则代码会非常难以调试。 Promise 的 .then
方法总是异步返回,在这一点上非常安全。此外,
Promise 链每一环都是异步执行,即使上一次的 return x;
中 x
不是 thenable, 也避免了很多问题。Promise.resolve(x)
可以把任何是或者不是 Promise 的东西转换为
Promise, 也可以用作统一作为异步处理。当然,青色旋律个人认为,最重要的是 Promise
写异步代码不需要花费太多力气,是一种默认行为,所以不会因为忘记
process.nextTick
或者嫌麻烦从而导致 Zalgo 被释放。
最后, Promise 有助于解决过度嵌套问题。和很多人相信的不一样,此问题并非回调风格独享,而是广泛存在的问题, Promise 也不能回避这个问题。真正的解决方案不是换哪一种风格,而是将代码拆分成较小可维护的块,并解除嵌套。 Promise 的优势在于表达链式的操作较为简便,而链式操作在回调模式中常常是过度嵌套的源泉,例如:
|
|
这里最不动脑子的办法就是这样写,所以才会有人这样写。如果适当进行设计和重构,完全可以在不涉及 Promise 的情况下解决:
|
|
假设 A/B
逻辑相关, C/D
逻辑相关,则这个已经拆成不错的分块了,同时嵌套层数显著减少。相比,使用 Promise 的话,最不动脑子的方式也没什么太大的问题:
|
|
至少代码已经天然拆分为 4 个组件了,而且随时可以在任何地方打断成两个函数而无须太大的调整。缩进的级别也还算可以接受,默认为扁平化。此外,假如错误需要默认统一处理,也无须太多重复代码。假如需要重构:
|
|
重构任务也只是进行了一下分组,调用结构大体不变。此外,在其中加入一环等重构也比回调模式更为简便,插入异常处理的灵活性也没有损失。因此, Promise 模式有助于避免“回调地狱”带来的维护难题。
]]>无论您有多努力,总有一天您必定会放弃 JS. 当然,如果您足够努力的话,也许可以坚持到 JS 语言废弃也说不定~
好了,负能量就到这里吧,如果您准备好了,那么就和青色旋律一起开始愉快的 JavaScript 旅途吧?
(此文根据之前JS学习小组的讨论进行整理,类似讨论笔记。)
要学习 JS,必须有一个可以运行 JS 的环境。在阅读以下内容之前,请务必配置好自己的 JavaScript 环境。
F12
,在弹出的窗口中选择 Console (控制台),在那里可以直接运行 JS。打开了以后,请试着运行这样一条语句(请复制粘贴):
|
|
如果出现Hello, JavaScript!
字样,则说明已经配置成功了。那么恭喜,赶快进入接下来的教程吧?
上面执行的那条语句可以算是最简单的 JS 程序之一了。如果您已经学习过了其他类似的语言,那么不妨跳过这一段内容,直接进入下一节吧?或者,如果您对 JS 已经有了最基本的了解,请期待本系列的第二篇。
此处 console.log(...);
的作用是在控制台打印输出...
处的内容。其中...
可以是字符串 "ABC"
也可以是整数 9
. JavaScript 中每句语句以分号 ;
结束。
数据类型是指编程语言能够处理的基本单元。JS 中常见的类型包括文本(字符串,string)和整数(数,number),括号中为正式名称。
字符串顾名思义就是一串字符,以双引号开始和结束,例如 "Hello"
包含5个字符。中文字符也可以出现在字符串中,例如 "你好"
,但必须使用英文半角双引号。
数包括整数和非整数。 JS 不区分整数和小数类型,无论整数或者浮点数都统称为数。例如
3.14
与 9
均为 number
类型。
JS 可以进行算术运算,支持 +
, -
, *
, /
运算符,例如表达式 1+2
. 除法的结果不会自动取整,例如 1/2
结果是 0.5
. 算术运算受浮点数精度限制,结果不一定是精确值。
复杂表达式也可以用括号分组,例如 (1 + 2) * 3
结果为 9
. 算术运算的结果必定为
number
类型。
在 console.log(1+2);
时,会先计算括号内的表达式内容,因此结果打印为 3
, 而不是打印 1+2
. 如果需要打印 1+2
, 应该使用字符串 console.log("1+2");
.
数中有特殊值 Infinity
和 -Infinity
对应正负无穷大。如果算术运算结果超出处理范围,返回值可能会是这两个值。同理由于精度限制,如果算数处理的结果非常接近于 0
,则会返回 0
, 不存在无穷小这个说法。
此外,还有特殊值 NaN
表示 Not a Number
(不是数)。例如 0/0
的结果是 NaN
,
不会报错。NaN
也属于 number
类型,尽管读作不是数。此外,其他类型的值转换为数失败的场合也会变成 NaN
值,见下述。
NaN
与任何数进行算术运算的结果均为 NaN
. 此外 NaN !== NaN
(与自身不相等)。
+
除了进行加法运算以外,还可以进行字符串的连接。比如
console.log("青色" + "旋律");
, 此处运算结果是两个字符串相连接,而不是加法。
如遇到字符串与数相连接的情况, JS 会将数转换为字符串再进行操作,例如
console.log("今天的气温是" + 33 + "摄氏度");
此处 33
会被转换为对应的字符串 "33"
,然后再执行连接。
但减法的情况下,两边必须都是数才能执行。此时 JS 会将字符串转换为数。例如:
console.log("3" - "1")
会输出数 2
(不是字符串 "2"
)。
此处的执行过程可以理解成两边各自转换为数变成 3 - 1
, 然后再得到 2
.
如果转换失败,则那个值用 NaN
代替,例如 "你好" - 3
变成 NaN - 3
, 结果为 NaN
.
-
, *
, /
三种运算都有此特性,会将两侧的值分别转换为数。这个特性称为“隐式类型转换”。
但 +
的处理是不同的。如果两侧均为数,则会按照加法运算来执行,结果为 number
类型。如果其中至少一侧为字符串,则会隐式转换另一侧为字符串,然后执行连接操作,结果为
string
类型。
理解了数、字符串和运算以后,就可以把 JS 作为计算器使用了。例如青色旋律的访客中一共有 60 人,其中 12 人会JS,那么会JS的访客比例是 12 / 60
. 如果需要转换为百分比,需要手动乘以 100
。为了输出美观,可以在后面接上百分比符号的字符串 "%"
.
于是我们得到:
|
|
其中 "%"
是一个字符串,起到装饰作用,并不是数学运算之类的。
大部分的 REPL 不需要 console.log
也能输出表达式的计算值。例如控制台里直接输入
1+2
回车即可输出 3
, 可以方便地计算各种值。但是在完整的 JS 程序里,大部分语句是只会执行不会输出的,需要使用 console.log
等语句来进行输出。
变量可以存放一个值,值的类型可以是数或者字符串等。let a = 42;
声明一个变量 a
,
其值为数 42
.
接下去假如执行表达式 a/3
, 结果是 14
, 相当于把 a 的值取出然后再参与运算。
let b = 1 + 2;
的情况下,b
的值是 3
, 而不是算术式 1 + 2
也不是字符串
"1 + 2"
.
利用这个特性,可以存储一部分运算结果供接下去的运算使用。
变量存储字符串的情况例如 let name = "Tom";
. 此时可以 console.log("Hello, " + name);
.
使用 let
或者 var
(后述)声明的变量可以修改其对应值,称为赋值。赋值语句的语法为
a = 3
, 读作”a赋值为3”,或者 a gets 3
. 这里的等号实际上可以理解成左箭头 ←
,
而不是数学意义上的相等之类的。
b = b + 1;
的执行结果为变量 b
的值增加1. (计算右侧表达式的值,然后赋值给左侧变量).
如果使用 const
代替 let
声明变量,则该变量不能再次赋值。例如 const pi = 3.14;
之后,执行 pi = 3
会报错。也有人将其称为“常量”,但其实 JS 语言中这些仍然属于变量,只是增加了赋值时会报错的检查。
let
和 const
声明的变量具有块作用域,且不可在声明语句之前访问。而另一种 var
声明的变量具有函数作用域,且声明会提升。关于作用域可以参考
这里的说明,这个属于比较高级的话题,系列之后的文章会专门介绍。
青色旋律在此推荐写程序时,变量最好用 const
声明,然后如果之后需要改变值的时候再回头将此处修改为 let
即可。正常的程序中绝大部分变量是不需要修改值的,因此这样能尽早发现一些错误的赋值。
JS 作为弱类型语言,变量本身没有类型,只有值有类型。因此 let a = 0;
之后,仍然可以执行赋值 a = "Hello";
, 而不会报错。 JS 的变量更像是指向一个值的“指针”,而不是一块固定的存储空间之类的。
假如青色旋律博客当前访客人数为 62, 其中 12 人会 JavaScript. 如果用变量保存并计算其中会 JS 的人数比例,可以有以下代码:
|
|
假设在此之后,又有 3 位新访客,其中 1 位会 JS, 则可以在以上代码之后续写:
|
|
其中 totalVisitors = totalVisitors + 3;
可以简写为 totalVisitors += 3;
, 读作
totalVisitors
增加 3. 此外 visitorsWhoKnowJS = visitorsWhoKnowJS + 1;
可以简写成 visitorsWhoKnowJS++;
, 读作递增,作用为变量值增加 1. 也有人不推荐使用 ++
.
上面的例子中,不一定需要修改变量的值,也可以直接使用变量运算:
|
|
事实上,假如这两个变量使用 const
声明,则只能这样书写。实际编码中请自行选择合适的方式吧。通常来说,假如用 totalVisitors
变量记录当前人数,则推荐用 let
并修改值。但假如运算的结果意义改变了,则推荐使用另一个变量,如
const visitorsWhoDontKnowJS = totalVisitors - visitorsWhoKnowJS;
, 提高可读性。
青色旋律在这里提供一些作业题目以供各位自习使用。作业内容全部为可选,不需要提交。但如果您对于以上的概念是第一次接触,建议还是尽量完成作业。
作业完成后,可以上传至 https://gist.github.com/ ,并将链接贴在下方评论区中。青色旋律会对作业进行批改和反馈。如果有任何问题也可以使用下方评论区提问。
作业的参考答案将会在下一次的系列博文中公布。
除了作业外,也请预习 JS 的各种控制语句,例如 if
, for
, while
等。如果已经有其他语言基础的话,那么请简单复习下就好。下次的内容可不会这么简单了哦?教程会假设各位基本上已经了解过控制语句了,会着重介绍 JS 与其他语言不同的地方,即使是有编程基础的各位应该也能愉快地学习~请继续关注青色旋律博客吧~
那么,下次再见了哦~
编写程序计算 39 摄氏度等于多少华氏度。要求将摄氏度的值保存到变量中,然后在计算公式中使用变量。输出的结果范例:
39 摄氏度等于 ? 华氏度。
其中 ?
的地方需要替换为正确答案。可以看到,需要连接字符串和数字以实现这样的文字输出。
关于计算公式,请自行搜索。
假设 alice
有 8 个苹果,而 bob
只有 5 个。他们互相交换所有的苹果,然后请问
alice
和 bob
分别有几个苹果?
这个作业要求用 alice
和 bob
两个变量存放苹果数量,然后输出初始的各自苹果数量。在程序运行的时候交换两个变量的值,然后再输出一次。
这样是不行的:
alice = bob;
bob = alice;
各位可以去运行一下,就知道为什么不行了。请记住 =
用于改变变量的值。一旦经过改变,变量变为存储新的值,而忘记了原有的值。
程序执行有顺序,并不能同时执行两条语句。需要用其他的方法来交换。
实在想不出的情况,请上网搜索交换两个变量的值。
上面已经说过了, JS 可以作为计算器使用。请尝试用 JS 做一些简单的运算,或者解决一些实际问题。
以下问题仅供参考:
(也可以是任意生活中遇到的实际问题或感兴趣的其他问题。)
各位,作业请加油~如果完成了别忘记在评论区提交作业哦。
]]>每个网站都有一定的风险,不同的网站风险不同。要随时记住:在任何网站上的密码,都随时有可能泄露。即使密码泄露,合理的安全措施也可以尽量减少损失。
青色旋律建议每个网站使用不同密码。如此一来,那么即使密码被公布了,那么也只需修改那个网站的密码即可。在最坏的情况下,其他人也只能获取那一个网站的数据和权限。
当然,人的记忆力是有限的,所以不可能每个网站的密码都单独记忆。青色旋律建议记住三个以内的密码即可,其他密码则可以保存在其他媒介中,例如密码管理服务、存储设备或者移动设备中。这样需要一个主密码来管理所有子密码,如果主密码泄露仍然会导致严重后果,但合理规避了网站被黑所带来的风险,如下所述。
对于需要存储的密码,首先青色旋律建议使用尽可能强的随机密码。假如不需要记忆这个密码,当然是越强越好,越随机越好啦。最好是 32 位以上的字母、数字、特殊符号等混合的密码并且强度能够通过某些检验。如果网站本身对密码的长度或者字符集有限制,那么就使用其提供的全部字符集、最大长度即可。一旦密码生成之后,就无须记录这些限制。
使用存储密码可以保证每个网站都使用不同的密码。但哪些密码应该存储,而哪些密码应该记忆呢?首先,如果使用密码管理服务,那么主密码是必须记忆的。同理,如果使用存储设备、移动设备或者专用的加密设备,建议使用加密存储并牢记其密钥。其余的密码,都不是必须记忆的。为了方便起见,可以记住一两个常用服务的密码。
此外,青色旋律强烈建议不要在任何网站上使用主密码,因为主密码丢失的后果非常严重,几乎相当于所有网站的密码都泄露。如果有条件的话,主密码在任何时候都不要通过网络传输,更不要在不可靠的设备上输入。如果使用设备存储密码,那么获取某网站的子密码这一操作可以在本地完成。使用移动设备则更为便捷,哪怕需要在陌生的环境登录,也可以取出手机轻松完成,避免了使用不可信设备解密带来的风险。
假如使用的是在线密码管理工具,则主密码不可避免地需要在网络上传输。但也可以使用多个在线密码工具来分担风险,而且每个在线管理工具本身的密码也可以使用设备存储来减轻记忆负担。
两步验证对于账户安全是非常必要的。当密码被键盘记录器记录、经不安全的传输信道泄露、遭受中间人攻击或钓鱼、数据库泄露时,两步验证能提供最后的一层防范。两步验证使用两种信息,其中一种一般是密码,而另一种是用户本人拥有的东西或者知道的情报。
用户本人拥有的东西包括手机短信(实质上是拥有 SIM 卡这种智能卡片)、数字令牌、验证卡片、证书等。其中数字令牌不一定需要专门的设备,一个简单的应用也可以实现。其本质是生成一个和时间以及某个密钥相关的数字。服务器和客户端持有相同密钥、并且较准了时间的前提下,双方的生成算法就会生成相同的数字,而无须进行通信。因此,无须迷信银行等机构提供的所谓安全设备,一般来说那些并没有任何特殊之处。如果需要的话,自己也可以实现这类算法。
证书的原理与上述基本相同。证书使用公、私钥算法,其中使用公钥加密的信息只有私钥可以解密,而私钥签名的信息只要有公钥即可验证。因此,公钥完全可以公开,而私钥则必须严格保护。一些验证卡片或者智能芯片上也会存储私钥。
总而言之,大部分拥有的东西实质上是拥有一个密钥。因此,在移动设备上安装的数字令牌应用,如 Google Authenticator 风险会大一些,因为手机被攻破的情况下密钥可能会泄露。而其他应用如果提升了权限(利用漏洞或者 ROOT),也可以获取密钥。一个不联网的专用数字令牌设备相对而言安全一些,但也不是绝对的。
手机短信本身并全不是加密的。运营商可以随时查看短信内容,而且手机短信也有一定的可能被窃听。给 SIM 卡设置密码有助于防范手机失窃导致的安全问题,但对其他情况无能为力。
此外,安全问答也可以作为两步验证的一种。有一些问题很少会在网络上上传、即使身边的人也很难提起,例如童年的好友等,有助于增加安全性。但如果密码是存储的话,其实差别并不是很大。
首先,如果您可以记忆一个 32 位随机数字字母字符的密码,不会轻易遗忘,那么可以无视掉这一节。如果不能的话,推荐选择一个好记又安全的密码。
有很多人会认为越复杂难记的密码越安全,但事实不完全如此。密码本身的安全性可以有很多种评估的方式,但其中比较好的一个标准是穷举所需的次数。这个指标在可以无限重试的前提下非常有意义。此外,这还适用于网站数据库被盗、密码的哈希值泄露的情况以及加密存储设备被盗的情况。
然而,攻击者有时并不会从 aaaaaa
开始,逐个尝试到 zzzzzz
,
而是直接尝试常见的密码或者密码构成方式。例如,他们会逐个尝试最受欢迎的1000个密码,或者是常见的 5000 个英文单词。对于每个英文单词,往往他们还会尝试常见的变形。以 piano
为例,常见变形有 Piano
(大写)、P1ano
、Pian0
(数字替换)、
p!ano
(特殊符号替换),以及以上的组合。假如每个字符都可能不替换或者常见替换,那么对于这个词语而言,有 2*3*2*3*3=108
种可能,不到 2**7
. 估算一下,对于大部分英文单词(2**16
)做常见变换,一共也只需 2**23
次尝试而已,这个数字相当危险。哪怕算上不同的常见构筑方式以及少量额外的字符,仍然可能被暴力破解。因此,青色旋律不建议使用此类构成密码方式。虽然这样的密码确实够长、有各类字符、很难记,却未必安全。
相比而言,假如只从2000个常见英文单词中随机选取4个单词,就有 2000**4
,约 2**44
种可能。即使知道构成方式,也不太容易被暴力破解。并且,随机选取的4个单词也可以通过联想记忆法等牢记。这样选取的密码虽然足够安全,但缺点是没有数字和特殊字符,可能无法通过网站的安全性校验。青色旋律推荐在后面加上固定的一串字符即可,例如 Abc1@3
,并不会造成记忆负担。即使每个人都用 Abc1@3
(也就是说,是公开的固定元素)也不会有任何问题,因为仍然需要对四个单词进行 2*44
次穷举。
注意,这种方式选取的单词必须是完全随机的,否则可能性会减少,更容易遭受字典和穷举攻击。例如,I love playing piano
可能就不是一个好的选择,因为这只有主语(大概率
I
)、表示观点的动词(love/hate/like/dislike/..
)和兴趣三种要素,最多也不会超过 2**20
种可能。而且,别认为密码学分析猜不到这种方式哦。许多人会花很多心思去研究泄露的密码中的常见模式,然后把几种常见模式都枚举一下就好啦。真正安全的密码构成方式未必罕见,但具体选取上有很多可能性,即使别人知道构成方式,也会因为可能性太多而无法暴力破解。
因此,假如碰巧随机出的四个单词能构成一句常见的句子或者有显而易见的关联,最好重新随机一次。虽然使用的方式是随机的,但用另外一种方式也能简单枚举出来当然也不行。完全随机字符的存储密码同理,假如随机出的是 00000...000
那和没随机没任何区别啦。
以上内容灵感来源于 xkcd 的漫画,地址是 https://xkcd.com/936/ 严格而言,此类强度分析应该使用信息论的专业术语(如熵),但此处为了简明易懂,使用了通俗语言。
总之,以这种方式生成的密码可以满足好记又安全的特性,适用于主密码等必须安全但又需要记忆的情况。假如是上述的存储密码,还是使用随机密码比较好,因为 32 位的随机字符密码大约有 2**160
种可能,远远超过了规律构筑的情况。在网站上使用时,基本上可以免疫被暴力穷举或者数据库泄露的情况。注意随机密码并不是说随便想的或者随手敲的就可以……最好使用安全的随机数生成器生成在字符集内的随机字符串。如果不清楚,请上网找一个离线且安全的随机密码生成器,或者使用密码管理器自带的随机生成功能。
当然,如果某些网站存储的是明文密码,或者用的加密算法比较弱,那么神都拯救不了他们了……当然也有可能运气不好,随机密码和一个常见密码哈希值相同,不过概率太低,可以不考虑。
访问账户不止有密码一种方式。首先,很多网站都支持社交账户登录,那么假如社交账户被盗,那么多强的密码也没用了。其次,正常的网站都支持找回密码功能,而找回密码一般是使用邮件和短信,那么假如常用邮箱被盗,那么大部分账户就都……
首先讨论用其他账户登录的情况。很多人认为这样无须密码,是最安全的,至少和主账户一样安全。这样的说法其实也没错,但是其实应该更全面的考虑。在这样的情况下,主要风险来源于主账户所在服务器的数据库泄露、主账户所在的服务器被控制和主账户密码泄露。主账户密码泄露的情况之前讨论过,相当于把鸡蛋放在一个篮子里,两个网站等于一个网站,风险没有隔离。如果很多网站都使用同一个主账户登录,后果更为严重。如果主账户所在的数据库泄露,也只是有密码哈希被破解的风险,参考以上的说明即可。此外,主账户的服务器如果被控制(俗称被黑),那么当然也就能获取了从账户的所有资料和权限。
邮箱找回密码的情况则更为简单,邮箱被盗则账户可能被重置密码。因此,邮箱的密码一定不能成为薄弱环节,甚至邮箱的密码必须更强来规避风险。使用多个邮箱也可以合理地降低风险,但假如都只是把邮件转发到某个邮箱的话,那也没有任何价值。使用客户端来收取多个邮箱的邮件倒是不失为一种不错的策略,不过客户端上就需要存储密码了,必须是可信的设备并且加上磁盘加密才足够安全。
如果可能的话,接收通知和找回密码的邮箱最好不要设置成同一个。当然,并不是所有网站都支持这样的配置。也有一种办法是注册多个邮箱,但使用过滤器,只转发不带有
password
或者 recovery
字样的邮件到常用邮箱,也算是一种不错的策略。假如常用邮箱被盗,切记第一时间登录所有其他邮箱取消转发,然后根据转发记录,再次重置掉那些被别人重置的账户密码(假如有的话)。
至于短信找回密码……其实根本不安全,但假如敌人不是政府、运营商或者持有一定通信监听技术的组织或者个人……还算勉强可以接受吧。
顺便一提,正常的找回密码都是发送一个链接,然后点击可以重置密码。如果有的网站找回密码是把密码原文发送到邮箱……那么说明那个网站存储了明文密码,安全性极低,千万不要在那个网站上存储任何重要数据。
总而言之,从安全性而言,统一登录的安全性和使用在线密码管理工具类似、风险也类似。如果使用完全本地的密码存储,不建议使用统一登录。如果使用在线密码管理工具,且社交账户的密码足够强的话,其实统一登录对安全性影响不大。找回密码的邮箱也可以像在线密码管理工具那样,多注册几个,可以有效降低风险。
首先,如果网站没有使用 HTTPS,最好还是别登录了吧。如果因为工作需要之类的,一定要用那个网站……首先一定一定不能使用和其他网站相同的密码,其次尽量在自己信任的网络环境登录。如果证书有错误或者过期,一定不能点忽略错误,更不能登录。输入密码前看清网址,有可能是钓鱼网站。特别注意钓鱼网站也可能有合法的 HTTPS 证书,不要看到绿色就以为安全。
大部分支持 HTTPS 的网站,在使用 HTTP 访问时会重定向到 HTTPS. 这个行为很方便,但有一些隐患。在已经登录的情况下,第一次访问 HTTP 时 cookie 会送出,从而导致监听者可以获取 cookie. 当然,中间人也可以劫持网页,返回点奇怪的东西而不是跳转。网站拥有者可以使用 HSTS 技术来强制 HTTPS 等,但作为用户并没有办法严格避免这种问题。安装 HTTPS Everywhere 浏览器扩展可以防止浏览器在某些网站发出 HTTP 请求。扩展会直接修改请求到对应的 HTTPS 网址,大大增强了安全性,但只适用于已经收录的网址。
不安全的渠道上,不要传输密码或者交换私密的信息。不安全的渠道上也没有办法确认对方的身份,请谨防诈骗。关于如何进行加密通话,敬请期待《简单易懂的网络求生指南:通信安全和隐私》篇。
啊,顺便……如果您的设备上有 Symantec 或者 CNNIC 证书的话,还是赶紧删了吧。毕竟这两个机构喜欢 不经过网站所有者申请就颁发证书 。也就是说,这些机构可以让任何人声称自己是任何其他网站。假如在陌生的网络环境下,DNS 如果被劫持,则可能会出现访问
https://www.google.com
, 证书无错误,是绿色图标,但其实访问的是其他人的服务器的情况。关于如何在各个设备各个浏览器上删除这些证书,请参考网上的教程。此外,在登录重要的账户时,也可以查看证书链的详情,看看证书链上是否有这些机构。并不是说这些机构颁发的所有证书都不可靠,但假如和平时不同就很可疑。例如, Google 的证书并不是以上机构发布的,如果在证书链中看到了奇怪的机构,那应该就是假的。
以上就是一些简单的安全建议和说明了。这其中大部分建议其实不难实施,但带来的安全性收益还是不错的。至少,可以试着去使用个密码管理软件或者服务来自动生成密码,并不麻烦。其他的建议还包括选用好记又安全的密码、启用两步验证等等。此外,也要多留意通信安全,尤其是网站的证书。做到这些事情并不会花太多时间,也不会太麻烦,反而可能可以方便很多。一般而言做到这些就已经足够安全生存啦,但在以上的基础上,如果您不嫌麻烦的话,也可以使用多个邮箱或者多个密码管理软件来分担风险,或者搭建自己的加密存储解决方案以提高安全性。之后的《简单易懂的网络求生指南:加密与存储》会详细介绍。
这次就说到这里啦,下次再见~
]]>青色旋律在这里向大家问好。祝大家有愉快的阅读体验~
这个博客以后会作为分享比较长的文字的地方。青色旋律还是比较希望写一些技术相关的内容,但博客应该也会有很多其他类型的文字,比如音乐相关的话题, Minecraft, 关于自由软件和开源的想法之类的。偶尔大概也会有一些思考和感悟。
嘛,感觉刚开始做博客的话,内容和定位是比较难把握好的。说不定之后就变成了想到什么就写什么了,甚至懒癌犯了,没准就会停更也说不定呢。
之前已经有写好的 JS 相关教程,应该会整理润色一下,然后陆续发表在这里吧,大家敬请期待吧。此外,还有一两个奇怪的系列,比如简单易懂的网络求生指南,青色旋律也会努力地填坑~
此外,如果大家希望看到青色旋律分享什么,也请在留言中告知,或者匿名参与调查。 这边会参考大家的意见来决定新的内容撰写。如果有什么问题,也可以通过评论区或者其他方式告知,说不定青色旋律也会写一篇文章来回答呢~
这边希望给大家分享一些有帮助的内容,所以有什么想法欢迎反馈~
在这里还要感谢几位朋友鼓励青色旋律撰写文章和折腾这个博客。看到这个新的博客,大家觉得还满意吗?青色旋律是否符合大家的期待呢?嘛,今后也请拭目以待吧。
之前不认识的各位,青色旋律也希望能和大家成为朋友。您可以称呼我为“青色旋律”或者“青”。如果需要交换友情链接的话,也请留言告知~
老朋友也好,新朋友也好,总之各位请多关照啦~
]]>