InfoQ 编辑部出品——2021年度技术盘点与展望 了解详情
写点什么

鲜为人知的 JavaScript 陷阱

  • 2019 年 11 月 05 日
  • 本文字数:2786 字

    阅读完需:约 9 分钟

鲜为人知的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:562033
用户头像
王文婧 InfoQ编辑

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

关注

评论 3 条评论

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

Vue进阶(三):Axios 应用详解

No Silver Bullet

Vue axios 7月日更

完善数字人民币发行应用机制 打造可靠金融基础设施

CECBC

仅仅上线一小时,下载量就破10W!阿里内部Java性能优化实战手册

Java 编程 程序员 面试 IT

架构实战营 毕业总结

竹林七贤

使用MLlib进行机器学习(十-下)

数据与智能

spark 决策树 优化

极致性能一睹为快!阿里全新出品性能优化手册 从此拒绝系统瘫痪!

Java 编程 程序员 架构师 计算机

关于体验设计的十大重要定律

石云升

读书笔记 用户体验 商业洞察 7月日更 体验设计

FNDZ,以DeFi为核心的“复制交易”获利平台

区块链小八歌

粉了!京东商城核心亿级流量并发Java系统架构设计方案手册

Java架构追梦

Java 架构 面试 高并发 京东

学生管理系统详细架构设计文档

张文龙

#架构实战营

Java版人脸检测详解上篇:运行环境的Docker镜像(CentOS+JDK+OpenCV)

编程菌

Java 编程 程序员 后端 java技术宅

深入了解Spring之Environment

邱学喆

Profile Environment PropertySource PropertySources

加码物联网安全,熵核科技做终端安全的守护者

熵核科技

CODING 携手 CoDesign:让设计与开发更简单

CODING DevOps

DevOps 设计 开发工具 CoDesign

祝贺中国跳水队夺金!百度智能云挺敢做梦的人

百度大脑

人工智能 跳水队

基于深度学习的实时噪声抑制——深度学习落地移动端的范例

声网Agora

人工智能 算法 移动端

阿里 P8 熬了一个月肝出这份 32W 字 Java 面试手册,在 Github 标星 31K+

Java 编程 架构 面试 IT

CRUD 程序员勿进!JDK源码剖析手册与并发编程图册,完美诠释高并发

Java 编程 程序员 IT 计算机

墙裂推荐!四面阿里拿 offer 后,才发现师哥给的面试笔记有多强大

Java 架构 IT 计算机 知识

应届女生美团Java岗4面,一次性斩offfer,我受到了万点暴击

编程菌

程序员 面试 后端 计算机

都2021年了,还在问网络安全怎么入门,气得我当场脑血栓发作

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 安全漏洞

Tensorflow for Java + Spark-Scala分布式机器学习计算框架的应用实践

Qunar技术沙龙

机器学习 tensorflow spark 后端 分布式计算

FNDZ,基于DeFi打造的交易复制生态

股市老人币圈新

spring,springboot,底层原理解析

java小李

某宝付费的Redis核心原理深度实践PDF,竟在GitHub标星86k+霸榜

白亦杨

Java 编程 程序员 架构师 计算机

PancakeSwap交易所做市机器人|交易所画K线机器人

Geek_23f0c3

交易所机器人 pancakeswap 做市机器人

一种Vue应用程序错误/异常处理机制

devpoint

Vue 异常处理 vue2 7月日更

架构训练营 模块三

小卷儿

偶获阿里大佬纯手码“887”页 Java 面试手册,突击学习一个月,成功跳槽阿里!

Java 编程 架构 面试 IT

"开放数据,蔚然成林"—浪潮助力多地获得数据开放全国标杆

浪潮云

云计算

15年前的3篇论文,变成了万亿大生意

百度大脑

人工智能 论文

鲜为人知的JavaScript陷阱-InfoQ