写点什么

鲜为人知的 JavaScript 陷阱

2019 年 11 月 05 日

鲜为人知的JavaScript陷阱

自我们告别Harmony的时代以来,JavaScript 推出了许多新的、带语法糖的功能。虽说更多新功能可以让我们编写可读性和质量更高的代码,但我们也很容易被这些新奇、亮眼的特性迷惑,反而陷入一些潜在的陷阱。本文作者回顾他在使用 JS 时经常遇到的困惑,新旧问题都有。希望你可以通过阅读本文,避免这些问题在你的编码中发生。


箭头函数和对象字面量

箭头函数提供了更简短的语法,其中一个特性是你可以将函数编写为具有隐式返回值的 lambda 表达式。编写函数样式的代码时这就很顺手,比如说有时你必须使用一个函数映射一些数组的情况。使用常规函数可能会多出很多空行。例如:


const numbers = [1, 2, 3, 4];numbers.map(function(n) {return n * n;});
复制代码


用 lambda 样式的箭头函数来写的话,就会写成两行优雅、易读的代码:


const numbers = [1, 2, 3, 4];numbers.map(n => n * n);
复制代码


在这种用例中,箭头函数的表现符合预期,它将值本身相乘并返回到包含[1, 4, 9, 16]的新数组。


但如果你尝试映射到对象,那么语法可能就不是你直觉期望的那样了。例如,假设我们试图将数字映射到包含如下值的对象数组中:


const numbers = [1, 2, 3, 4];numbers.map(n => { value: n });
复制代码


这里的结果实际上是一个包含未定义值的数组。虽然看起来我们在这里返回一个对象,但是解释器看到的东西是完全不一样的。花括号被解释为箭头函数的块作用域,而值语句最后实际上成为了标签。如果将上述箭头函数外推到解释器最终实际执行的内容中,它将看起来像这样:


const numbers = [1, 2, 3, 4];numbers.map(function(n) {value:nreturn;});
复制代码


解决方法非常微妙。我们只需要将对象包装在括号中,就可以将它变成一个表达式而不是一个块语句,如下所示:


const numbers = [1, 2, 3, 4];numbers.map(n => ({ value: n }));
复制代码


这会计算出一个包含对象数组的数组,该对象数组具有预期的值。


箭头函数和绑定

箭头函数另一个需要注意的点是,它们没有自己的 this 绑定,意味着它们的 this 值和封闭词法作用域的 this 值是一样的。


因此,尽管箭头函数的语法更时尚一些,但它并不能替代一些很好的旧函数。你可能会很容易遇到 this 绑定与你原本所想不一样的情况。例如:


let calculator = {  value: 0,  add: (values) => {    this.value = values.reduce((a, v) => a + v, this.value);  },};calculator.add([1, 2, 3]);console.log(calculator.value);
复制代码


尽管人们可能希望这里的 this 绑定为此处的 calculator 对象,但实际上 this 绑定最后要么是未定义,要么是全局对象,具体取决于代码是否在严格模式下运行。这是因为这里最接近的词汇作用域是全局作用域。在严格模式下这是未定义的。否则,它会是浏览器中的窗口对象(或 Node.js 兼容环境中的过程对象)。


常规函数确实具有 this 绑定。在对象上调用时,this 将指向该对象,因此常规函数仍然是获得成员函数的正确途径。


let calculator = {  value: 0,  add: (values) => {    this.value = values.reduce((a, v) => a + v, this.value);  },};calculator.add([1, 2, 3]);console.log(calculator.value);
复制代码


另外,由于箭头函数没有 this 绑定,因此 Function.prototype.call、Function.prototype.bind 和 Function.prototype.apply 均无法使用。声明箭头函数后,this 绑定设置为固定,无法更改。


因此,在下面的示例中,我们将遇到与之前相同的问题:当调用 adder 的 add 函数时,this 绑定又成了全局对象,尽管我们尝试使用 Function.prototype.call 覆盖它:


const adder = {  add: (values) => {    this.value = values.reduce((a, v) => a + v, this.value);  },};let calculator = {  value: 0};adder.add.call(calculator, [1, 2, 3]);
复制代码


箭头函数很简洁,但不能替换需要 this 绑定的常规成员函数。


自动分号插入

虽然这不是一项新功能,但自动分号插入(ASI)是 JavaScript 中比较怪异的功能之一,因此值得一提。从理论上讲,你可以在大多数时候省略分号(许多项目都这样做)。如果项目有先例,则应遵循此先例。但你一定需要记得 ASI 是一项功能,否则最后你会写出容易迷惑人的代码。


请看以下示例:


return{  value: 42}
复制代码


有人可能会认为它会返回对象字面量,但实际上它会返回未定义的值,因为发生了分号插入,使其成为空的 return 语句,后跟一个 block 语句和一个 label 语句。换句话说,最终被解释的代码看起来更像是下面这种写法:


return;{  value: 42};
复制代码


根据经验,即使使用分号时也切勿以大括号、方括号或模板字符串字面量开头,因为 ASI 总是会起作用。


浅集合

集合较浅,意味着重复的数组和具有相同值的对象,这将导致集合中有多个条目。例如:


let set = new Set();set.add([1, 2, 3]);set.add([1, 2, 3]);console.log(set.length);
复制代码


该集合的大小将为 2,如果你考虑引用它的话就要注意了,因为它们是不同的对象。但字符串是不可变的。以集合中的多个字符串为例:


let set = new Set();set.add([1, 2, 3].join(','));set.add([1, 2, 3].join(','));console.log(set.size);
复制代码


由于字符串是不可变的,且驻留在 JavaScript 中,因此最终的集合大小为 1。如果你需要存储一组对象,那么这就可以是一种解决方法,也可以将它们序列化和反序列化。


类与暂时死区

在 JavaScript 中,常规函数被提升到词法作用域的顶部,这意味着下面的示例将按预期工作:


let segment = new Segment();function Segment() {  this.x = 0;  this.y = 0;}
复制代码


但是对于类来说并非如此。类实际上没有被提升,并且在尝试使用它们之前需要在词法作用域内对其进行完全定义。例如:


let segment = new Segment();class Segment {  constructor() {    this.x = 0;    this.y = 0;  }}
复制代码


尝试构造类的新实例时会导致 ReferenceError,因为它们没有像函数那样被提升。


Finally

Finally 是一个特殊情况。看一下以下代码片段:


try {  return true;} finally {  return false;}
复制代码


你认为它会返回什么值?答案既符合直觉,同时又可以是反直觉的。有人可能会认为第一个 return 语句使函数实际返回并弹出调用栈。但这里是该规则的例外,因为 Finally 语句始终都会运行,因此会返回 Finally 块中的 return 语句。


小结

JavaScript 很容易入门,但很难精通。换句话说,开发人员需要搞清楚自己正在做什么,明白为什么要这样做,否则就很容易出错。


ECMAScript 6 及其含糖功能尤其如此。特别是箭头函数哪里都会冒出来。让我来猜的话,那是因为开发人员认为它们比常规函数更漂亮。但它们不是常规函数,因此无法替代后者。


时不时地浏览一下规范并没有什么坏处。它不是世界上最激动人心的文档,但就规范本身而言写得还算不错。


AST Explorer 之类的工具还可以帮助你了解某些极端场景下的状况。人类和计算机往往会以不同的方式来解析事物。


文章最后,我把最后一个示例留作练习供大家思考。



原文链接:


https://medium.com/better-programming/lesser-known-javascript-hazards-8d688a463b1f


2019 年 11 月 05 日 17:561926
用户头像
王文婧 InfoQ编辑

发布了 126 篇内容, 共 61.6 次阅读, 收获喜欢 251 次。

关注

评论 3 条评论

发布
用户头像
取其精华,去其糟粕,get到了。
2019 年 11 月 06 日 13:20
回复
用户头像
因果关系都搞反了。是箭头函数解决了JS的this混乱的问题。
2019 年 11 月 06 日 08:08
回复
用户头像
这文章太低级了,更适合出现在掘金...
2019 年 11 月 05 日 19:05
回复
没有更多了
发现更多内容

架构师训练营第二周作业

尹斌

java安全编码指南之:可见性和原子性

程序那些事

Java java安全编码 java编码指南 java安全编码指南

高难度对话读书笔记—认知篇2

wo是一棵草

架构师训练营第 2 周作业

netspecial

极客大学架构师训练营

架构师训练营第 1 期第 2周作业

du tiezheng

极客大学架构师训练营

项目实战,动态增删form表单

麦洛

jquery 克隆

架构师训练营第 1 期第 2 周学习总结

du tiezheng

极客大学架构师训练营

监控应用,应该监控什么?

小清新同学

云计算 运维 监控

难得干货,揭秘支付宝的2维码扫码技术优化实践之路

JackJiang

支付宝

从大数据的角度来谈谈运维监控这件事儿

小清新同学

运维 监控

三步带你开发一个短链接生成平台

Geek_Willie

Java SpreadJS Node

自己动手写SQL执行引擎

无毁的湖光

Java MySQL 数据库 Linux 算法

缓存解决方案-技术专题-Caffeine Cache

李浩宇/Alex

MySQL varchar类型最大值,原来一直都理解错了

架构精进之路

MySQL varchar

Python 自动化测试全攻略:五种自动化测试模型实战详解

Geek_Willie

自动化测试

2B还是2C,这真是个问题

MavenTalker

SaaS

收藏+下载!Flink 社区最全学习渠道汇总

Apache Flink

flink

关于Java 编译Servlet或者自定义Tag,引入包的问题

谷鱼

Java

Go中的HTTP请求之——HTTP1.1请求流程分析

Gopher指北

go golang HTTP Go web

什么才是“应用拓扑”?

小清新同学

运维 监控

TensorFlow 篇 | TensorFlow 2.x 基于 Keras 模型的本地训练与评估

Alex

tensorflow 模型训练 keras

巡展2020第十三届亚洲国际物联网展览会-南京站

InfoQ_caf7dbb9aa8a

不一样的面向对象(二)

书旅

php 面向对象

Dolphinscheduler系统架构设计

dll

Apache DolphinScheduler

让世界为之赞叹的开源项目,除了Linux,你知道Git吗?

小Q

Java git 学习 程序员 面试

上班路上也是一道美景

xcbeyond

生活 摄影 摄影征文

永续合约系统开发源码,区块链合约交易所搭建

WX13823153201

如何设计Go语言中的channel

soolaugust

go channel goroutines

程序执行太慢?快来学习SIMD加速技术,这个案例下的加速效果我也没想到(附带动手实验)

Optimize-Lab

go 优化代码 优化技巧 开源社区 simd

如何快速制造OOM

Since

JVM OOM

保留时序数据波动细节的一种采样算法

小清新同学

监控 时序数据库

编译系统设计赛(华为毕昇杯)技术报告会|5月1日

编译系统设计赛(华为毕昇杯)技术报告会|5月1日

鲜为人知的JavaScript陷阱-InfoQ