整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JS合并拼接字符串的5种方法

JS合并拼接字符串的5种方法

在前端开发中,JavaScript是不可或缺的一部分,它为网页带来了动态交互能力。而字符串拼接作为日常开发中的基础操作之一,其效率直接影响到用户体验和页面性能。本文旨在探讨JavaScript中几种常见的字符串拼接方法,并通过实例来比较它们的优缺点。


技术概述

定义与特性

字符串拼接是指将两个或多个字符串连接成一个新的字符串。JavaScript提供了多种方式进行字符串拼接,包括但不限于使用加号 (+), 模板字符串 (${}$), String.prototype.concat(), Array.prototype.join() 以及第三方库如 Lodash 的 _.join() 方法。

核心特性与优势

  • 简单性: 使用加号或模板字符串进行拼接非常直观易懂。
  • 灵活性: 模板字符串支持嵌入变量和表达式,可以更灵活地构建动态文本。
  • 性能: 在处理大量字符串时,join() 和第三方库可能提供更好的性能。

示例代码

// 使用加号
let name="John";
let greeting="Hello, " + name + "!";

// 使用模板字符串
greeting=`Hello, ${name}!`;

// 使用 concat()
greeting="Hello, ".concat(name, "!");

// 使用 join()
let parts=["Hello, ", name, "!"];
greeting=parts.join("");

// 使用 lodash
greeting=_.join(["Hello, ", name, "!"], "");

技术细节

每种方法在内部实现上有所不同:

  • 加号 (+): 实现简单但可能会创建多个临时字符串对象,当拼接次数较多时性能较差。
  • 模板字符串 (`${}$): 支持嵌入表达式,但在处理大量数据时性能不如 join()
  • concat(): 可以接受多个参数,但在现代JavaScript中使用较少。
  • join(): 接受数组和分隔符,对于处理大量数据非常高效。
  • lodash的 join(): 提供了额外的功能和优化,适用于复杂项目。

实战应用

假设我们需要生成一个包含用户信息的欢迎消息,其中包含了用户名、年龄等信息:

function generateWelcomeMessage(user) {
  return `Welcome, ${user.name} (${user.age} years old)!`;
}

const user={ name: "Alice", age: 25 };
console.log(generateWelcomeMessage(user));

在这个例子中,模板字符串提供了最简洁且易于理解的解决方案。

优化与改进

对于频繁或大量的字符串拼接操作,推荐使用 join() 方法,因为它避免了创建中间字符串对象,从而提高了性能。

function generateLongMessage(items) {
  return items.map(item=> `${item.name}: ${item.value}`).join(", ");
}

常见问题

问题1: 性能问题

在循环中使用加号进行拼接会导致性能下降。解决方法是使用 join() 或者数组的 reduce() 方法。

问题2: 特殊字符处理

直接拼接可能会导致HTML特殊字符未被转义的问题。解决方法是在拼接前对特殊字符进行转义。

总结与展望

字符串拼接是JavaScript中的一项基本操作,选择合适的拼接方式可以显著提高代码的可读性和性能。随着ES6及更高版本标准的引入,模板字符串已经成为一种优雅的选择。未来,我们可以期待更多高效的字符串处理工具和技术的发展。

通过本文的学习,我们不仅掌握了如何有效地拼接字符串,还了解了不同方法背后的原理及其适用场景。这将帮助我们在实际开发中做出更合理的选择。

边形合并特效效果图

由于上传图片大小有限制,所以只能制作这样质量的动图,实际效果比这流畅很多!

对于web前端学习不懂的,或者不知道怎么学习的可以来我的前端群589651705,每天都会分享一些企业级的知识和特效源码分享,源码已上传群文件,小伙伴们可以进群下载。

140行原生javascript代码制作正方形合并特效源码

html代码:

css代码:

javascript代码:

几年涌现了很多分而治之的代码管理方案。长久以来,我们一直使用所谓的模块模式,即将代码包装到自调用的函数表达式中。我们必须自己管理脚本的次序,以确保每个模块都在自己的依赖之后加载。

后来,RequireJS 库出现了。它提供了以编程方式定义每个模块依赖的机制,有了依赖图后,我们就不必再操心脚本的加载次序了。RequireJS 接受一个字符串数组,以明确依赖,同时将模块包装为一个函数调用,而这个函数接受前面的依赖作为参数。其实很多其他库都支持类似的功能,只不过API 不大一样。

还有其他的复杂依赖管理机制,如AngularJS 中的依赖注入机制。该机制需要我们用函数定义命名组件,并相应地指定其他命名组件依赖。AngularJS 会替我们管理依赖注入的加载,我们要做的只是命名组件和指定依赖。

取代RequireJS 的是CommonJS,随着Node.js 的火爆,CommonJS 迅速走红。本文将先介绍CommonJS,毕竟它在今天的应用还相当普遍。接下来还将介绍ES6 为JavaScript 引入的原生模块系统。最后,我们将探讨CommonJS 与原生JavaScript 模块(即人们常说的ECMAScript 模块)的互用性。

CommonJS

CommonJS 与其他模块化方案的不同之处是,它将每个文件都看成一个模块,其他方案则以编程方式声明模块。CommonJS 模块拥有隐含的局部作用域,全局作用域必须通过global 显式地被访问。CommonJS 模块导入依赖及导出供外部调用的接口都是动态的,导入依赖是通过require 函数调用实现的。这个函数调用是同步的,会返回所请求模块暴露的接口。

不看代码很难说清一种模块的定义语法。以下代码展示了一个可重用的CommonJS 模块文件。其中的has 和union 函数都位于模块的局部作用域内。在此之上,我们再将union 赋给module.exports,它就成了这个模块的公共API。

function has(list, item) {
 return list.includes(item)
}
function union(list, item) {
 if (has(list, item)) {
 return list
 }
 return [...list, item]
}
module.exports=union

假设将以上文件保存为union.js,那我们就可以在另一个CommonJS 模块中调用它。比如,另一个文件是app.js,为了调用union.js,需要给require 传入一个相对路径。

const union=require('./union.js')
console.log(union([1, 2], 3))
// <- [1, 2, 3]
console.log(union([1, 2], 2))
// <- [1, 2]

如果扩展名是.js 或.json,则可以省略,但不鼓励这么做。

虽然扩展名对require 语句来说是可选的,在使用node CLI 时最好还是养成添加扩展名的习惯。浏览器对ESM 的实现不允许省略扩展名,否则就会导致多一次到服务器的往返才能找到作为HTTP 资源的JavaScript 模块的正确端点。

在Node.js 环境下,我们可以通过CLI 运行app.js,如下所示。

? node app.js
# [1, 2, 3]
# [1, 2]

安装Node.js 后,就可以在终端命令行中使用node 程序。

正如其他JavaScript 函数那样,CommonJS 中的require 函数也可以被动态调用。我们可以利用这一特性实现通过一个接口动态获取不同的模块。举个例子,假设有一个模板目录,其中包含几个视图模板文件,每个文件都导出一个函数。这些函数都接受一个模型参数,然后返回一个HTML 字符串。

通过读取model 对象的属性,以下代码中展示的模板函数构造并返回购物车中的一个商品。

// views/item.js
module.exports=model=> `<li>
 <span>${ model.amount }</span>
 <span>x </span>
 <span>${ model.name }</span>
</li>`

应用模块可以基于这个item.js 提供的视图模板打印一个<li> 元素。

// app.js
const renderItem=require('./views/item.js')
const html=renderItem({
 name: 'Banana bread',
 amount: 3
})
console.log(html)

图1 展示了这个小应用的执行结果。

图1:将模型渲染为HTML 就是向模板字面量中插值而已

接下来的这个模板用于渲染购物车中的所有商品。它接受一个商品数组,并重用前面的item.js 模板来渲染每件商品。

// views/list.js
const renderItem=require('./item.js')
module.exports=model=> `<ul>
 ${ model.map(renderItem).join('\n') }
</ul>`

我们可以像前面那样使用list.js 模板。但要注意,传给它的模型必须是一个商品数组,而非单个商品对象。

// app.js
const renderList=require('./views/list.js')
const html=renderList([{
 name: 'Banana bread',
 amount: 3
}, {
 name: 'Chocolate chip muffin',
 amount: 2
}])
console.log(html)

图2 展示了小应用的当前状况。

图2:基于模板字面量的复合模板同样是信手拈来

到目前为止,这个示例只写了几个小模块,每个模块只基于传入的模型对象和视图模板生成一种HTML 视图。简单的API 便于重用,因此我们才能轻松地将模型映射到item.js 模板函数,以渲染出多个商品,最后再通过换行符将它们连接起来。

既然两个视图的API 相似,都是接受一个模型并返回一段HTML 字符串,那么就可以抽象一下。如果想要一个render 函数能够渲染任何模板,那么可以借助require 的动态特性来轻松实现。以下示例的核心是构建指向模板模块的路径。与前面代码的重要不同点是,require 调用并没有出现在模块代码的顶部。对require 的调用可以出现在任何地方,甚至可以嵌套在其他函数中。

// render.js
module.exports=function render(template, model) {
 return require(`./views/${ template }`.js)(model)
}

有了这样的API 后,就不用操心调用require 时传入的视图模板路径是否正确了,因为render.js 模块会正确拼接出路径。要想渲染模板,只要传入模板的名字和该模板所需要的模型即可,如以下代码和图3 所示。

// app.js
const render=require('./render.js')
console.log(render('item', {
 name: 'Banana bread',
 amount: 1
}))
console.log(render('list', [{
 name: 'Apple pie',
 amount: 2
}, {
 name: 'Roasted almond',
 amount: 25
}]))

图3:模板字面量使得创建HTML 渲染应用变得易如反掌

接下来,我们会看到ES6 模块从某种程度上受到了CommonJS 的影响。接下来我们会讨论export 和import 语句,以及ESM 与CommonJS 有哪些相通之处。

JavaScript模块

在前面介绍CommonJS 模块时,我们已经知道其API 简单却非常灵活、强大。ES6 模块的API 甚至比它还要简单,虽然灵活性稍微差了点,但几乎是一样强大的。

严格模式

在ES6 模块系统中,严格模式默认是打开的。严格模式是一个特性,用于拒绝JavaScript中那些不好的特性,并让很多静默错误变成异常,从而被抛出。在拒绝这些特性的基础上,编译器可以启用优化策略,让JavaScript 运行时更快、更安全。

? 变量必须被声明。

? 函数参数的名字必须是唯一的。

? 禁止使用with 语句。

? 为只读属性赋值会导致抛出错误。

? 00740 这样的八进制数是语法错误。

? 用delete 删除不可删除的属性会抛出错误。

? delete prop 是语法错误,delete global.prop 才是正确的。

? eval 不会为周围的作用域引入新变量。

? eval 和arguments 不能被绑定或赋值。

? arguments 不会神奇地同步方法参数的变化。

? 不再支持arguments.callee,访问它会抛出TypeError。

? 不再支持arguments.caller,访问它会抛出TypeError。

? 方法调用中作为this 传递的上下文不会被“装箱”为Object。

? 不能再使用fn.caller 和fn.arguments 来访问JavaScript 栈。

? 保留字(如protected、static、interface 等)不能被绑定。

接下来我们将深入探讨一下export 语句。

export语句

在CommonJS 模块中,要导出的值必须赋给module.exports。可以导出的内容包括任意类型的值、对象、数组、函数,如下所示。

module.exports='hello'
module.exports={ hello: 'world' }
module.exports=['hello', 'world']
module.exports=function hello() {}

作为文件,ES6 模块是通过export 语句暴露API 的。ESM 中的声明只在局部作用域中有效,这一点与CommonJS 相同。模块中声明的任何变量都必须作为该模块的API 导入,并且在想要使用它们的模块中导入才能访问。

1. 导出默认绑定

将前面CommonJS 代码中的module.exports=替换成export default 即可在ESM 中实现相同的效果。

export default 'hello'
export default { hello: 'world' }
export default ['hello', 'world']
export default function hello() {}

在CommonJS 中,我们可以为module.exports 动态赋值。

function initialize() {
 module.exports='hello!'
}
initialize()

相较于CommonJS,ESM 中的export 语句只能出现在模块顶级。export 语句“只能出现在顶级”是一个很好的限制,因为根据方法调用来动态定义并暴露API 并不是非常必要。这一限制还有助于编译器及静态分析工具解析ES6 模块。

function initialize() {
 export default 'hello!' // SyntaxError
}
initialize()

除了export default 语句,ESM 还支持其他暴露API 的方式。

2. 命名导出

在CommonJS 中,如果想要暴露多个值,不一定需要导出一个包含这些值的对象。我们可以将这些值赋给隐含的module.exports 对象。这样导出的仍然只是一个绑定,其中包含module.exports 对象最终持有的所有属性。也就是说,虽然以下示例看似导出了两个值,但实际上它们都是最终导出对象的属性。

module.exports.counter=0
module.exports.count=()=> module.exports.counter++

在ESM 中,我们可以通过命名导出语法复现这一行为。相较于CommonJS 为隐含的module.exports 对象添加属性,ES6 是直接声明要导出的绑定,如下所示。

export let counter=0
export const count=()=> counter++

注意,前面的代码不能将变量声明提取为独立的语句,然后再作为命名导出传给export,否则会导致语法错误。

let counter=0
const count=()=> counter++
export counter // SyntaxError
export count

ESM 这样严格限制模块中声明的语法是为了方便静态分析,但代价是损失一些灵活性。要想提高灵活性,就必然会提高复杂性,这也是ESM 不提供灵活接口的正当理由。

3. 导出列表

ES6 模块支持导出顶级命名成员的列表,如下所示。这种导出列表的语法很容易解析,同时也就前面提出的问题给出了一个解决方案。

let counter=0
const count=()=> counter++
export { counter, count }

要想重命名导出的绑定,可以使用别名语法export { count as increment}。这样我们就可以将局部作用域中的绑定count 以别名increment 提供给外部,如下所示。

let counter=0
const count=()=> counter++
export { counter, count as increment }

最后,使用命名成员列表语法时还可以指定默认导出。以下代码使用as default 在导出多个命名成员的同时定义了模块的默认导出。

let counter=0
const count=()=> counter++
export { counter as default, count as increment }

虽然以下代码长了点,但功能与上段代码相同。

let counter=0
const count=()=> counter++
export default counter
export { count as increment }

需要特别注意的是,我们导出的是绑定,而不只是值。

4. 绑定,而不是值

ES6 模块导出绑定,而不是值或引用。这意味着以下示例中的模块导出的fungible 会绑定到这个模块的fungible 变量,其值会随fungible 变量的变化而变化。被其他模块引用后再改变公共接口可能会导致困惑,但这一机制在某些情况下确实也很有用。

在以下代码中,模块导出的fungible 一开始绑定的是一个对象,5 秒后又改成了一个数组。

export let fungible={ name: 'bound' }
setTimeout(()=> fungible=[0, 1, 2], 5000)

使用这个API 的模块在5 秒后也能看到fungible 值的变化。以下示例每隔2 秒会打印一次引入的绑定。

import { fungible } from './fungible.js'
console.log(fungible) // <- { name: 'bound' }
setInterval(()=> console.log(fungible), 2000)
// <- { name: 'bound' }
// <- { name: 'bound' }
// <- [0, 1, 2]
// <- [0, 1, 2]
// <- [0, 1, 2]

这种行为特别适合计数器和标记,但除非用途明确,最好不要使用。毕竟从使用者的角度来看,API 接口不确定很难理解。

JavaScript 的模块系统还提供了一个export..from 语法,用于暴露其他模块的接口。

5. 导出另一个模块

向export 添加一个from 子句就可以导出另一个模块的命名导出。此时绑定不会导入到当前模块的作用域。换句话说,当前模块只是传递另一个模块的绑定,并不能直接访问该绑定。

export { increment } from './counter.js'
increment()
// ReferenceError: increment is not defined

在传递绑定时,我们可以为命名导出起个别名。如果以下示例中的模块被命名为aliased,那么调用者可以通过import { add } from 'aliased.js' 取得counter 模块中的increment的绑定。

export { increment as add } from './counter.js'

ESM 也支持用通配符导出另一个模块中的所有命名导出,如下所示。但要注意,此时不会导出counter 模块中的默认绑定。

export * from './counter.js'

要想导出另一个模块的default 绑定,必须使用导出列表语法来添加别名。

export { default as counter } from './counter.js'

我们将ES6 模块暴露API 的所有语法都过了一遍。接下来我们将探讨一下import 语句,看看如何通过它使用其他模块。

import语句

我们可以用import 语句在一个模块中加载另一个模块。加载模块的语法因实现而不同,也就是说,规范并未就此给出描述。如今,我们可以编写兼容ES6 规范的代码,而一些聪明的人已经找到了在浏览器中处理模块加载的办法。

Babel 这样的编译器可以基于CommonJS 等模块系统拼接模块。这意味着Babel 中的import 语句与CommonJS 中的require 具有相同的语义。

假设模块./counter.js 包含以下代码。

let counter=0
const increment=()=> counter++
const decrement=()=> counter--
export { counter as default, increment, decrement }

以下这行代码可以将counter 模块加载到我们的app 模块中。这行代码不会在app 模块的作用域中创建任何变量。但是这会导致counter 模块中的所有顶级代码执行,包括该模块自己的import 语句。

import './counter.js'

与export 语句类似,import 语句也只允许出现在模块代码的顶级。这一限制有助于编译器简化自己的模块加载逻辑,同时有助于其他静态分析工具解析你的代码。

1. 导入默认导出

CommonJS 模块通过require 语句导入其他模块。如果需要引入某个模块的默认导出,只要将该语句的结果赋给一个变量即可。

const counter=require('./counter.js')

要想导入ES6 模块导出的默认绑定,就必须给它起个名字。但语法和语义与声明变量时有些不同,因为这里是导入绑定,而不只是将值赋给一个变量。这个区别有助于静态分析工具和编译器更轻松地解析我们的代码。

import counter from './counter.js'
console.log(counter)
// <- 0

除了默认导出,我们也可以导入命名导出并给它们起别名。

2. 导入命名导出

以下代码展示了如何从counter 模块导入increment 方法。导入命名导出的语法是一对花括号,这让我们联想到了解构赋值。

import { increment } from './counter.js'

为了导入多个绑定,绑定之间以逗号分隔。

import { increment, decrement } from './counter.js'

这里的语法和语义与解构有些不同。比如,解构通过冒号创建别名,而import 语句则使用as关键字,照搬了export 语句的语法。以下代码在导入increment 方法时将其重命名为add。

import { increment as add } from './counter.js'

以逗号作为分隔符,我们可以同时导入默认导出和命名导出。

import counter, { increment } from './counter.js'

我们也可以给default 绑定命名,此时需要一个别名。

import { default as counter, increment } from './counter.js'

以下的代码示例展示了ESM 与CommonJS 导入在语义上的区别。记住,ESM 中导出和导入的是绑定,而不是引用。为方便理解,你可以将以下代码中的绑定counter 想象为一个属性的获取方法(getter),它可以访问counter 模块内部并返回其局部变量counter 的值。

import counter, { increment } from './counter.js'
console.log(counter) // <- 0
increment()
console.log(counter) // <- 1
increment()
console.log(counter) // <- 2

最后,我们将探讨命名空间导入。

3. 通配符导入语句

我们可以用通配符导入一个模块的命名空间对象。相较于导入命名导出或默认导出,这样可以一次性导入所有导出。注意,星号* 后面必须紧跟别名,导入的所有绑定都在这个别名名下。如果存在default 导出,那么它也会被放到这个命名空间绑定之下。

import * as counter from './counter.js'
counter.increment()
counter.increment()
console.log(counter.default) // <- 2

动态import()

有人提出过有关动态import() 表达式的提案(阶段3)。与import 语句的静态分析和链接不同,import() 在运行时加载模块,并在获取、解析并运行请求的模块及其所有依赖后,返回一个包含该模块命名空间对象的Promise。

与import 语句类似,此时的模块说明符可以是任意字符串。但与import 只允许静态的字符串字面量作为模块说明符不同,import() 的模块说明符可以是模板字面量或任何能生成模块说明符字符串的有效JavaScript 表达式。

假设我们正在国际化某个应用,需要根据用户代理的语言偏好加载相应的语言包。我们可以先导入localizationService,然后再通过import() 及根据插入navigator.language 的模板字面量构造的模块说明符来实现本地化数据的动态加载,如下所示。

import localizationService from './localizationService.js'
import(`./localizations/${ navigator.language }.json`)
 .then(module=> localizationService.use(module))

注意,通常并不建议将代码写成以上那样,原因如下。

? 对静态分析不友好,因为静态分析是在构建时执行的,所以几乎不可能推断出${ navigator.language } 这样插值的结果。

? JavaScript 打包工具也很难对其进行打包,结果很可能是应用加载完成后再异步加载这个模块。

? Rollup 等工具很难对其进行摇树优化,难以消除并未导入(因而永远不会用到)的模块代码,因此也就难以缩小包并提升性能。

? 不利于辅助检查模块导入语句中要导入的文件是否缺失的工具(如eslint-plugin-import)发挥作用。

与import 语句类似,规范也没有说明import() 获取模块的方式,因此就要看宿主环境了。

但提案说明了模块被成功解决后,Promise 应该获取解析后的命名空间对象。同时该提案指出,如果发生错误导致模块加载失败,那么Promise 应该被拒绝。

对于不那么重要的模块,我们可以在不阻塞页面加载的情况下进行异步加载,同时还可以在模块加载失败时恰当地处理,如下所示。

import('./vendor/jquery.js')
 .then($=> {
 // 使用jQuery
})
.catch(()=> {
 // 加载jQuery失败
})

我们可以用Promise.all 异步加载多个模块。以下示例同时导入了3 个模块,然后在.then子句中直接用解构获取了对它们的引用。

const specifiers=[
 './vendor/jquery.js',
 './vendor/backbone.js',
 './lib/util.js'
]
Promise
 .all(specifiers.map(specifier=> import(specifier)))
 .then(([$, backbone, util])=> {
 // 使用模块
 })

同样,我们可以用同步循环或async/await 来加载模块,如下所示。

async function load() {
 const { map }=await import('./vendor/jquery.js')
 const $=await import('./vendor/jquery.js')
 const response=await fetch('/cats')
 const cats=await response.json()
 $('<div>')
 .addClass('container cats')
 .html(map(cats, cat=> cat.htmlSnippet))
 .appendTo(document.body)
}
load()

await import() 让动态导入模块看起来像静态的import 语句。我们自己心里必须明白,这里其实是一个接一个地异步加载多个模块。

注意,虽然import() 有点像函数,但语义与常规函数不同。这里import 并非函数定义,不能进行扩展、不能给它添加属性,也不能对它使用解构语法。从这个意义上说,import()更像是类构造器中的super() 调用。

ES模块的实践考量

无论使用什么模块系统,我们都可以做到公开API 并同时隐藏信息。这种完美的信息隐藏正是以前的开发者梦寐以求的特性。那时候,要想实现同样的功能,必须非常熟悉JavaScript 的作用域规则,否则就得盲目地循环某种模式,如下所示。这个示例使用局部作用域的calc 函数创建了一个random 模块,该函数负责生成一个区间为[0, n) 的随机数,而公共API 中包含range 方法,该方法可以计算一个[min, max] 范围内的随机数。

const random=(function() {
 const calc=n=> Math.floor(Math.random() * n)
 const range=(max=1, min=0)=> calc(max + 1 - min) + min
 return { range }
})()

比较以上代码与以下名为random 的ESM 模块中的代码。你会发现,立即调用函数表达式(IIFE,immediately invoked function expression)的包装不见了,模块的名字也不见了。这里模块的名字已经变成了文件名。我们又回到了以前在HTML 的<script> 标签内编写原始JavaScript 代码的日子。

const calc=n=> Math.floor(Math.random() * n)
const range=(max=1, min=0)=> calc(max + 1 - min) + min
export { range }

虽然没有用IIFE 来包装代码的问题了,但如何定义、测试、说明和使用模块仍然需要认真思考。

决定模块中包含什么内容并不容易。需要考虑的因素非常多,以下列举了其中一部分。

? 过于复杂吗?

? 过大了吗?

? API 有没有明确的含义?

? API 有没有完善的文档?

? 为这个模块编写测试是否简单?

? 为这个模块增加新特性难不难?

? 删除模块中的特性困难吗?

相较于模块长度,复杂性是需要考量的首要指标。一个模块可能有几千行代码但很简单,比如将文件说明符映射为特定语言字符串的字典;模块也可能只有几十行代码却非常难以理解,比如涉及域名验证和其他业务逻辑规则的数据模型。我们可以将代码拆分成更小的模块以降低复杂性,每个模块只专注于解决问题的某一方面。只要不是过于复杂,大模块也不是什么大问题。

明确定义的API 同时配有完善的文档也是优秀模块化设计的关键。模块的API 应该聚焦,遵循信息隐藏原理。换句话说,只对模块使用者暴露必要的东西。通过隐藏模块的内部实现,即使模块代码缺乏注释和文档,或者将来再被修改,我们仍然可以从整体上保持接口简单,避免意外的调用出现。通过给公开的API 编写完善的文档,即使这些文档是写在代码中的注释,抑或代码本身就可以自解释,我们可以降低模块使用者的认知门槛。

测试应该只针对模块的公开接口来编写,模块的内部实现应该看作无关紧要的实现细节。测试要覆盖模块公开接口的不同方面,只要API 的输入和输出不变,对内部实现的修改就不应该影响测试覆盖率。

同样,为模块增加或减少特性的容易性也是需要考量的一个因素。

? 添加一个新特性有多难?

? 为实现某个逻辑是不是必须修改几个不同的模块?

? 这个流程是不是重复了很多次?或许我们可以将那些变化抽象到一个高层模块,以隐藏复杂性,也许这样做很大程度上只是引入了一个中间层,虽然有一些好处或改进,却导致代码更难理解了。

? 从另一方面看,这个API 有多么不容易改动?

? 删除模块的一部分、完全删除,或用其他逻辑代替它是不是很容易?

? 如果模块之间的依赖度很高,那代码年代越久远,改版次数越多,代码量越大,修改就越困难。

浏览器实现的功能只是原生JavaScript 模块的一点皮毛。现在,有的浏览器已经实现了import 和export 语句。有的浏览器已经实现了<script type='module'>,通过指定module脚本类型来使用模块。模块加载器规范还未最终制定完成,其最新进展参见https://github.com/whatwg/loader#implementation-status。

在此期间,Node.js 发布的新版本还没有包含JavaScript 模块系统的实现。考虑到JavaScript生态系统中的工具都依赖Node,到底能实现多大程度的兼容还说不清楚。实现迟迟不能推出的原因是,目前还无法确定一个文件是CommonJS 模块还是ESM 模块。根据文件中至少存在一个import 或export 语句来判断它是否为ESM 模块的提案最终被废弃了。

目前的做法是准备为ESM 模块专门引入一个新文件扩展名。鉴于运行Node.js 的平台及使用场景具有多样性,这里要考虑的细节非常庞杂。得到一个优雅、完美、正确的方案是非常难的。

——本文内容选自《深入理解JavaScript特性》。一本由JavaScript之父作序推荐,360资深前端精心翻译的著作!

它旨在让你轻松学习JavaScript的新进展,包括ES6及后续更新。

书中提供了大量实用示例,以循序渐进的方式讲解了异步函数、对象解构、动态导入及异步生成器等内容,并从实践角度提供了许多建议,既能帮助广大前端开发者建立一个完整的知识体系,也能助其在工作中如虎添翼,开发出更好的Web应用。

书中不仅介绍了箭头函数、解构、模板字面量以及其他语法方面的新元素,还全面展示了ES6引入的流程控制机制,以及如何高效地简化自己的代码。本书的讨论还涉及ES6内置的新集合类型、使用代理控制属性访问、ES6中内置API的改进、CommonJS与ECMAScript模块的互用性等方面。

“尼古拉斯写的东西特别实用……建议你好好读读,从中发现对自己有用的东西,进而真正拥抱JavaScript,致力于为所有人开发更好的Web应用。”——Brendan Eich,JavaScript之父

“本书全面介绍了ES6新特性的语法和语义,有助于你大幅度提升代码的表达能力。作者把这些特性融入简单易懂的示例中,帮你快速上手。”——Kent C. Dodds,PayPal前端工程师,TC39成员

——

【图灵教育】

阅读改变世界,阅读塑造人生

让我们站在巨人的肩膀上,解锁更多IT技能!