折800 网站模板,网站设计说明书功能流程图,wordpress怎样搭建网站,wordpress的主题博客本文参考自电子书《ECMAScript 6 入门》#xff1a;https://es6.ruanyifeng.com/ Module 的语法
1. 概述
历史上#xff0c;JavaScript 一直没有模块#xff08;module#xff09;体系#xff0c;无法将一个大程序拆分成互相依赖的小文件#xff0c;再用简单的方法拼装… 本文参考自电子书《ECMAScript 6 入门》https://es6.ruanyifeng.com/ Module 的语法
1. 概述
历史上JavaScript 一直没有模块module体系无法将一个大程序拆分成互相依赖的小文件再用简单的方法拼装起来。其他语言都有这项功能比如 Ruby 的require、Python 的import甚至就连 CSS 都有import但是 JavaScript 任何这方面的支持都没有这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前社区制定了一些模块加载方案最主要的有 CommonJS 和 AMD 两种。前者用于服务器后者用于浏览器。ES6 在语言标准的层面上实现了模块功能而且实现得相当简单完全可以取代 CommonJS 和 AMD 规范成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化使得编译时就能确定模块的依赖关系以及输入和输出的变量。CommonJS 和 AMD 模块都只能在运行时确定这些东西。比如CommonJS 模块就是对象输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readfile } require(fs);// 等同于
let _fs require(fs);
let stat _fs.stat;
let exists _fs.exists;
let readfile _fs.readfile;上面代码的实质是整体加载 fs 模块即加载 fs 的所有方法生成一个对象_fs然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”因为只有运行时才能得到这个对象导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象而是通过 export 命令显式指定输出的代码再通过 import 命令输入。
// ES6模块
import { stat, exists, readFile } from fs;上面代码的实质是从fs模块加载 3 个方法其他方法不加载。这种加载称为“编译时加载”或者静态加载即 ES6 可以在编译时就完成模块加载效率要比 CommonJS 模块的加载方式高。当然这也导致了没法引用 ES6 模块本身因为它不是对象。
由于 ES6 模块是编译时加载使得静态分析成为可能。有了它就能进一步拓宽 JavaScript 的语法比如引入宏macro和类型检验type system这些只能靠静态分析实现的功能。
除了静态加载带来的各种好处ES6 模块还有以下好处。
不再需要UMD模块格式了将来服务器和浏览器都会支持 ES6 模块格式。目前通过各种工具库其实已经做到了这一点。将来浏览器的新 API 就能用模块格式提供不再必须做成全局变量或者navigator对象的属性。不再需要对象作为命名空间比如Math对象未来这些功能可以通过模块提供。
2. 严格模式
ES6 的模块自动采用严格模式不管你有没有在模块头部加上use strict;。
严格模式主要有以下限制。
变量必须声明后再使用函数的参数不能有同名属性否则报错不能使用with语句不能对只读属性赋值否则报错不能使用前缀 0 表示八进制数否则报错不能删除不可删除的属性否则报错不能删除变量 delete prop会报错只能删除属性 delete global[prop]eval 不会在它的外层作用域引入变量eval 和 arguments 不能被重新赋值arguments 不会自动反映函数参数的变化不能使用 arguments.callee不能使用 arguments.caller禁止 this 指向全局对象不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈增加了保留字比如 protected、static 和 interface
上面这些限制模块都必须遵守。由于严格模式是 ES5 引入的不属于 ES6所以请参阅相关 ES5 书籍。
其中尤其需要注意 this 的限制。ES6 模块之中顶层的 this 指向 undefined即不应该在顶层代码使用 this。
3. export 命令
模块功能主要由两个命令构成export 和 import。export 命令用于规定模块的对外接口import 命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量外部无法获取。如果你希望外部能够读取模块内部的某个变量就必须使用 export 关键字输出该变量。下面是一个 JS 文件里面使用 export 命令输出变量。
// profile.js
export var firstName Michael;
export var lastName Jackson;
export var year 1958;上面代码是 profile.js 文件保存了用户信息。ES6 将其视为一个模块里面用 export 命令对外部输出了三个变量。
export 的写法除了像上面这样还有另外一种。
// profile.js
var firstName Michael;
var lastName Jackson;
var year 1958;export { firstName, lastName, year };上面代码在 export 命令后面使用大括号指定所要输出的一组变量。它与前一种写法直接放置在 var 语句前是等价的但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部一眼看清楚输出了哪些变量。
export 命令除了输出变量还可以输出函数或类class。
export function multiply(x, y) {return x * y;
};上面代码对外输出一个函数multiply。
通常情况下export输出的变量就是本来的名字但是可以使用as关键字重命名。
function v1() { ... }
function v2() { ... }export {v1 as streamV1,v2 as streamV2,v2 as streamLatestVersion
};上面代码使用as关键字重命名了函数v1和v2的对外接口。重命名后v2可以用不同的名字输出两次。
需要特别注意的是export命令规定的是对外的接口必须与模块内部的变量建立一一对应关系。
// 报错
export 1;// 报错
var m 1;
export m;上面两种写法都会报错因为没有提供对外的接口。第一种写法直接输出 1第二种写法通过变量m还是直接输出 1。1只是一个值不是接口。正确的写法是下面这样。
// 写法一
export var m 1;// 写法二
var m 1;
export {m};// 写法三
var n 1;
export {n as m};上面三种写法都是正确的规定了对外的接口m。其他脚本可以通过这个接口取到值1。它们的实质是在接口名与模块内部变量之间建立了一一对应的关系。
同样的function和class的输出也必须遵守这样的写法。
// 报错
function f() {}
export f;// 正确
export function f() {};// 正确
function f() {}
export {f};目前export 命令能够对外输出的就是三种接口函数Functions 类Classesvar、let、const 声明的变量Variables。
另外export 语句输出的接口与其对应的值是动态绑定关系即通过该接口可以取到模块内部实时的值。
export var foo bar;
setTimeout(() foo baz, 500);上面代码输出变量 foo值为 bar500 毫秒之后变成 baz。
这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存不存在动态更新。
最后export 命令可以出现在模块的任何位置只要处于模块顶层就可以。如果处于块级作用域内就会报错下一节的 import 命令也是如此。这是因为处于条件代码块之中就没法做静态优化了违背了 ES6 模块的设计初衷。
function foo() {export default bar // SyntaxError
}
foo()上面代码中export语句放在函数之中结果报错。
4. import 命令
使用export命令定义了模块的对外接口以后其他 JS 文件就可以通过import命令加载这个模块。
// main.js
import { firstName, lastName, year } from ./profile.js;function setName(element) {element.textContent firstName lastName;
}上面代码的 import 命令用于加载 profile.js 文件并从中输入变量。import 命令接受一对大括号里面指定要从其他模块导入的变量名。大括号里面的变量名必须与被导入模块profile.js对外接口的名称相同。
如果想为输入的变量重新取一个名字import 命令要使用 as 关键字将输入的变量重命名。
import { lastName as surname } from ./profile.js;import命令输入的变量都是只读的因为它的本质是输入接口。也就是说不允许在加载模块的脚本里面改写接口。
import {a} from ./xxx.jsa {}; // Syntax Error : a is read-only;上面代码中脚本加载了变量 a对其重新赋值就会报错因为 a 是一个只读的接口。但是如果 a 是一个对象改写 a 的属性是允许的。
import {a} from ./xxx.jsa.foo hello; // 合法操作上面代码中a 的属性可以成功改写并且其他模块也可以读到改写后的值。不过这种写法很难查错建议凡是输入的变量都当作完全只读不要轻易改变它的属性。
import 后面的 from 指定模块文件的位置可以是相对路径也可以是绝对路径。如果不带有路径只是一个模块名那么必须有配置文件告诉 JavaScript 引擎该模块的位置。
import { myMethod } from util;上面代码中util是模块文件名由于不带有路径必须通过配置告诉引擎怎么取到这个模块。
注意import命令具有提升效果会提升到整个模块的头部首先执行。
foo();import { foo } from my_module;上面的代码不会报错因为 import 的执行早于 foo 的调用。这种行为的本质是import 命令是编译阶段执行的在代码运行之前。
由于 import 是静态执行所以不能使用表达式和变量这些只有在运行时才能得到结果的语法结构。
// 报错
import { f oo } from my_module;// 报错
let module my_module;
import { foo } from module;// 报错
if (x 1) {import { foo } from module1;
} else {import { foo } from module2;
}上面三种写法都会报错因为它们用到了表达式、变量和if结构。在静态分析阶段这些语法都是没法得到值的。
最后import语句会执行所加载的模块因此可以有下面的写法。
import lodash;上面代码仅仅执行lodash模块但是不输入任何值。
如果多次重复执行同一句import语句那么只会执行一次而不会执行多次。
import lodash;
import lodash;上面代码加载了两次lodash但是只会执行一次。
import { foo } from my_module;
import { bar } from my_module;// 等同于
import { foo, bar } from my_module;上面代码中虽然 foo 和 bar 在两个语句中加载但是它们对应的是同一个 my_module 模块。也就是说import 语句是 Singleton 模式。
目前阶段通过 Babel 转码CommonJS 模块的 require 命令和 ES6 模块的 import 命令可以写在同一个模块里面但是最好不要这样做。因为 import 在静态解析阶段执行所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。
require(core-js/modules/es6.symbol);
require(core-js/modules/es6.promise);
import React from React;5. 模块的整体加载
除了指定加载某个输出值还可以使用整体加载即用星号*指定一个对象所有输出值都加载在这个对象上面。
下面是一个circle.js文件它输出两个方法area和circumference。
// circle.jsexport function area(radius) {return Math.PI * radius * radius;
}export function circumference(radius) {return 2 * Math.PI * radius;
}现在加载这个模块。
// main.jsimport { area, circumference } from ./circle;console.log(圆面积 area(4));
console.log(圆周长 circumference(14));上面写法是逐一指定要加载的方法整体加载的写法如下。
import * as circle from ./circle;console.log(圆面积 circle.area(4));
console.log(圆周长 circle.circumference(14));注意模块整体加载所在的那个对象上例是circle应该是可以静态分析的所以不允许运行时改变。下面的写法都是不允许的。
import * as circle from ./circle;// 下面两行都是不允许的
circle.foo hello;
circle.area function () {};6. export default 命令
从前面的例子可以看出使用import命令的时候用户需要知道所要加载的变量名或函数名否则无法加载。但是用户肯定希望快速上手未必愿意阅读文档去了解模块有哪些属性和方法。
为了给用户提供方便让他们不用阅读文档就能加载模块就要用到export default命令为模块指定默认输出。
// export-default.js
export default function () {console.log(foo);
}上面代码是一个模块文件export-default.js它的默认输出是一个函数。
其他模块加载该模块时import命令可以为该匿名函数指定任意名字。
// import-default.js
import customName from ./export-default;
customName(); // foo上面代码的import命令可以用任意名称指向export-default.js输出的方法这时就不需要知道原模块输出的函数名。需要注意的是这时import命令后面不使用大括号。
export default命令用在非匿名函数前也是可以的。
// export-default.js
export default function foo() {console.log(foo);
}// 或者写成function foo() {console.log(foo);
}export default foo;上面代码中foo函数的函数名foo在模块外部是无效的。加载的时候视同匿名函数加载。
下面比较一下默认输出和正常输出。
// 第一组
export default function crc32() { // 输出// ...
}import crc32 from crc32; // 输入// 第二组
export function crc32() { // 输出// ...
};import {crc32} from crc32; // 输入上面代码的两组写法第一组是使用 export default 时对应的 import 语句不需要使用大括号第二组是不使用 export default 时对应的 import 语句需要使用大括号。
export default 命令用于指定模块的默认输出。显然一个模块只能有一个默认输出因此 export default 命令只能使用一次。所以import 命令后面才不用加大括号因为只可能唯一对应 export default 命令。
本质上export default 就是输出一个叫做 default 的变量或方法然后系统允许你为它取任意名字。所以下面的写法是有效的。
// modules.js
function add(x, y) {return x * y;
}
export {add as default};
// 等同于
// export default add;// app.js
import { default as foo } from modules;
// 等同于
// import foo from modules;正是因为export default命令其实只是输出一个叫做default的变量所以它后面不能跟变量声明语句。
// 正确
export var a 1;// 正确
var a 1;
export default a;// 错误
export default var a 1;上面代码中export default a 的含义是将变量 a 的值赋给变量 default。所以最后一种写法会报错。
同样地因为 export default 命令的本质是将后面的值赋给 default 变量所以可以直接将一个值写在 export default 之后。
// 正确
export default 42;// 报错
export 42;上面代码中后一句报错是因为没有指定对外的接口而前一句指定对外接口为 default。
有了 export default 命令输入模块时就非常直观了以输入 lodash 模块为例。
import _ from lodash;如果想在一条import语句中同时输入默认方法和其他接口可以写成下面这样。
import _, { each, forEach } from lodash;对应上面代码的export语句如下。
export default function (obj) {// ···
}export function each(obj, iterator, context) {// ···
}export { each as forEach };上面代码的最后一行的意思是暴露出forEach接口默认指向each接口即forEach和each指向同一个方法。
export default也可以用来输出类。
// MyClass.js
export default class { ... }// main.js
import MyClass from MyClass;
let o new MyClass();7. export 与 import 的复合写法
如果在一个模块之中先输入后输出同一个模块import语句可以与export语句写在一起。
export { foo, bar } from my_module;// 可以简单理解为
import { foo, bar } from my_module;
export { foo, bar };上面代码中export 和 import 语句可以结合在一起写成一行。但需要注意的是写成一行以后foo 和 bar 实际上并没有被导入当前模块只是相当于对外转发了这两个接口导致当前模块不能直接使用 foo 和 bar。
模块的接口改名和整体输出也可以采用这种写法。
// 接口改名
export { foo as myFoo } from my_module;// 整体输出
export * from my_module;默认接口的写法如下。
export { default } from foo;具名接口改为默认接口的写法如下。
export { es6 as default } from ./someModule;// 等同于
import { es6 } from ./someModule;
export default es6;同样地默认接口也可以改名为具名接口。
export { default as es6 } from ./someModule;ES2020 之前有一种import语句没有对应的复合写法。
import * as someIdentifier from someModule;ES2020补上了这个写法。
export * as ns from mod;// 等同于
import * as ns from mod;
export {ns};8. 模块的继承
模块之间也可以继承。
假设有一个circleplus模块继承了circle模块。
// circleplus.jsexport * from circle;
export var e 2.71828182846;
export default function(x) {return Math.exp(x);
}上面代码中的 export *表示再输出 circle 模块的所有属性和方法。注意export * 命令会忽略 circle 模块的 default 方法。然后上面代码又输出了自定义的 e 变量和默认方法。
这时也可以将 circle 的属性或方法改名后再输出。
// circleplus.jsexport { area as circleArea } from circle;上面代码表示只输出circle模块的area方法且将其改名为circleArea。
加载上面模块的写法如下。
// main.jsimport * as math from circleplus;
import exp from circleplus;
console.log(exp(math.e));上面代码中的import exp表示将circleplus模块的默认方法加载为exp方法。
9. 跨模块常量
const声明的常量只在当前代码块有效。如果想设置跨模块的常量即跨多个文件或者说一个值要被多个模块共享可以采用下面的写法。
// constants.js 模块
export const A 1;
export const B 3;
export const C 4;// test1.js 模块
import * as constants from ./constants;
console.log(constants.A); // 1
console.log(constants.B); // 3// test2.js 模块
import {A, B} from ./constants;
console.log(A); // 1
console.log(B); // 3如果要使用的常量非常多可以建一个专门的constants目录将各种常量写在不同的文件里面保存在该目录下。
// constants/db.js
export const db {url: http://my.couchdbserver.local:5984,admin_username: admin,admin_password: admin password
};// constants/user.js
export const users [root, admin, staff, ceo, chief, moderator];然后将这些文件输出的常量合并在index.js里面。
// constants/index.js
export {db} from ./db;
export {users} from ./users;使用的时候直接加载index.js就可以了。
// script.js
import {db, users} from ./constants/index;10. import() 函数
简介
前面介绍过import命令会被 JavaScript 引擎静态分析先于模块内的其他语句执行import命令叫做“连接” binding 其实更合适。所以下面的代码会报错。
// 报错
if (x 2) {import MyModual from ./myModual;
}上面代码中引擎处理 import 语句是在编译时这时不会去分析或执行 if 语句所以 import 语句放在 if 代码块之中毫无意义因此会报句法错误而不是执行时错误。也就是说import 和 export 命令只能在模块的顶层不能在代码块之中比如在 if 代码块之中或在函数之中。
这样的设计固然有利于编译器提高效率但也导致无法在运行时加载模块。在语法上条件加载就不可能实现。如果 import 命令要取代 Node 的 require 方法这就形成了一个障碍。因为 require 是运行时加载模块import 命令无法取代 require 的动态加载功能。
const path ./ fileName;
const myModual require(path);上面的语句就是动态加载require到底加载哪一个模块只有运行时才知道。import命令做不到这一点。
ES2020提案 引入import()函数支持动态加载模块。
import(specifier)上面代码中import 函数的参数 specifier指定所要加载的模块的位置。import 命令能够接受什么参数import() 函数就能接受什么参数两者区别主要是后者为动态加载。
import() 返回一个 Promise 对象。下面是一个例子。
const main document.querySelector(main);import(./section-modules/${someVariable}.js).then(module {module.loadPageInto(main);}).catch(err {main.textContent err.message;});import() 函数可以用在任何地方不仅仅是模块非模块的脚本也可以使用。它是运行时执行也就是说什么时候运行到这一句就会加载指定的模块。另外import() 函数与所加载的模块没有静态连接关系这点也是与 import 语句不相同。import() 类似于 Node.js 的 require() 方法区别主要是前者是异步加载后者是同步加载。
由于 import() 返回 Promise 对象所以需要使用 then() 方法指定处理函数。考虑到代码的清晰更推荐使用 await 命令。
async function renderWidget() {const container document.getElementById(widget);if (container ! null) {// 等同于// import(./widget).then(widget {// widget.render(container);// });const widget await import(./widget.js);widget.render(container);}
}renderWidget();上面示例中await命令后面就是使用import()对比then()的写法明显更简洁易读。
适用场合
下面是import()的一些适用场合。
1按需加载。
import()可以在需要的时候再加载某个模块。
button.addEventListener(click, event {import(./dialogBox.js).then(dialogBox {dialogBox.open();}).catch(error {/* Error handling */})
});上面代码中import()方法放在click事件的监听函数之中只有用户点击了按钮才会加载这个模块。
2条件加载
import()可以放在if代码块根据不同的情况加载不同的模块。
if (condition) {import(moduleA).then(...);
} else {import(moduleB).then(...);
}上面代码中如果满足条件就加载模块 A否则加载模块 B。
3动态的模块路径
import()允许模块路径动态生成。
import(f())
.then(...);上面代码中根据函数f的返回结果加载不同的模块。
注意点
import()加载模块成功以后这个模块会作为一个对象当作then方法的参数。因此可以使用对象解构赋值的语法获取输出接口。
import(./myModule.js)
.then(({export1, export2}) {// ...·
});上面代码中export1 和 export2 都是 myModule.js 的输出接口可以解构获得。
如果模块有 default 输出接口可以用参数直接获得。
import(./myModule.js)
.then(myModule {console.log(myModule.default);
});上面的代码也可以使用具名输入的形式。
import(./myModule.js)
.then(({default: theDefault}) {console.log(theDefault);
});如果想同时加载多个模块可以采用下面的写法。
Promise.all([import(./module1.js),import(./module2.js),import(./module3.js),
])
.then(([module1, module2, module3]) {···
});import()也可以用在 async 函数之中。
async function main() {const myModule await import(./myModule.js);const {export1, export2} await import(./myModule.js);const [module1, module2, module3] await Promise.all([import(./module1.js),import(./module2.js),import(./module3.js),]);
}
main();11. import.meta
开发者使用一个模块时有时需要知道模板本身的一些信息比如模块的路径。ES2020 为 import 命令添加了一个元属性import.meta返回当前模块的元信息。
import.meta只能在模块内部使用如果在模块外部使用会报错。
这个属性返回一个对象该对象的各种属性就是当前运行的脚本的元信息。具体包含哪些属性标准没有规定由各个运行环境自行决定。一般来说import.meta至少会有下面两个属性。
1import.meta.url
import.meta.url返回当前模块的 URL 路径。举例来说当前模块主文件的路径是https://foo.com/main.jsimport.meta.url就返回这个路径。如果模块里面还有一个数据文件data.txt那么就可以用下面的代码获取这个数据文件的路径。
new URL(data.txt, import.meta.url)注意Node.js 环境中import.meta.url返回的总是本地路径即file:URL协议的字符串比如file:///home/user/foo.js。
2import.meta.scriptElement
import.meta.scriptElement是浏览器特有的元属性返回加载模块的那个script元素相当于document.currentScript属性。
// HTML 代码为
// script typemodule srcmy-module.js data-fooabc/script// my-module.js 内部执行下面的代码
import.meta.scriptElement.dataset.foo
// abcModule 的加载实现
如何在浏览器和 Node.js 之中加载 ES6 模块参考https://es6.ruanyifeng.com/#docs/module-loader
编程风格
1. 块级作用域
1let 取代 var
ES6 提出了两个新的声明变量的命令let 和 const。其中let 完全可以取代 var因为两者语义相同而且 let 没有副作用。
use strict;if (true) {let x hello;
}for (let i 0; i 10; i) {console.log(i);
}上面代码如果用var替代let实际上就声明了两个全局变量这显然不是本意。变量应该只在其声明的代码块内有效var命令做不到这一点。
var命令存在变量提升效用let命令没有这个问题。
use strict;if (true) {console.log(x); // ReferenceErrorlet x hello;
}上面代码如果使用var替代letconsole.log那一行就不会报错而是会输出undefined因为变量声明提升到代码块的头部。这违反了变量先声明后使用的原则。
所以建议不再使用var命令而是使用let命令取代。
2全局常量和线程安全
在let和const之间建议优先使用const尤其是在全局环境不应该设置变量只应设置常量。
const优于let有几个原因。一个是const可以提醒阅读程序的人这个变量不应该改变另一个是const比较符合函数式编程思想运算不改变值只是新建值而且这样也有利于将来的分布式运算最后一个原因是 JavaScript 编译器会对const进行优化所以多使用const有利于提高程序的运行效率也就是说let和const的本质区别其实是编译器内部的处理不同。
// bad
var a 1, b 2, c 3;// good
const a 1;
const b 2;
const c 3;// best
const [a, b, c] [1, 2, 3];const声明常量还有两个好处一是阅读代码的人立刻会意识到不应该修改这个值二是防止了无意间修改变量值所导致的错误。
所有的函数都应该设置为常量。
长远来看JavaScript 可能会有多线程的实现比如 Intel 公司的 River Trail 那一类的项目这时let表示的变量只应出现在单线程运行的代码中不能是多线程共享的这样有利于保证线程安全。
2. 字符串
静态字符串一律使用单引号或反引号不使用双引号。动态字符串使用反引号。
// bad
const a foobar;
const b foo a bar;// acceptable
const c foobar;// good
const a foobar;
const b foo${a}bar;3. 解构赋值
使用数组成员对变量赋值时优先使用解构赋值。
const arr [1, 2, 3, 4];// bad
const first arr[0];
const second arr[1];// good
const [first, second] arr;函数的参数如果是对象的成员优先使用解构赋值。
// bad
function getFullName(user) {const firstName user.firstName;const lastName user.lastName;
}// good
function getFullName(obj) {const { firstName, lastName } obj;
}// best
function getFullName({ firstName, lastName }) {
}如果函数返回多个值优先使用对象的解构赋值而不是数组的解构赋值。这样便于以后添加返回值以及更改返回值的顺序。
// bad
function processInput(input) {return [left, right, top, bottom];
}// good
function processInput(input) {return { left, right, top, bottom };
}const { left, right } processInput(input);4. 对象
单行定义的对象最后一个成员不以逗号结尾。多行定义的对象最后一个成员以逗号结尾。
// bad
const a { k1: v1, k2: v2, };
const b {k1: v1,k2: v2
};// good
const a { k1: v1, k2: v2 };
const b {k1: v1,k2: v2,
};对象尽量静态化一旦定义就不得随意添加新的属性。如果添加属性不可避免要使用Object.assign方法。
// bad
const a {};
a.x 3;// if reshape unavoidable
const a {};
Object.assign(a, { x: 3 });// good
const a { x: null };
a.x 3;如果对象的属性名是动态的可以在创造对象的时候使用属性表达式定义。
// bad
const obj {id: 5,name: San Francisco,
};
obj[getKey(enabled)] true;// good
const obj {id: 5,name: San Francisco,[getKey(enabled)]: true,
};上面代码中对象obj的最后一个属性名需要计算得到。这时最好采用属性表达式在新建obj的时候将该属性与其他属性定义在一起。这样一来所有属性就在一个地方定义了。
另外对象的属性和方法尽量采用简洁表达法这样易于描述和书写。
var ref some value;// bad
const atom {ref: ref,value: 1,addValue: function (value) {return atom.value value;},
};// good
const atom {ref,value: 1,addValue(value) {return atom.value value;},
};5. 数组
使用扩展运算符...拷贝数组。
// bad
const len items.length;
const itemsCopy [];
let i;for (i 0; i len; i) {itemsCopy[i] items[i];
}// good
const itemsCopy [...items];使用 Array.from 方法将类似数组的对象转为数组。
const foo document.querySelectorAll(.foo);
const nodes Array.from(foo);6. 函数
立即执行函数可以写成箭头函数的形式。
(() {console.log(Welcome to the Internet.);
})();那些使用匿名函数当作参数的场合尽量用箭头函数代替。因为这样更简洁而且绑定了 this。
// bad
[1, 2, 3].map(function (x) {return x * x;
});// good
[1, 2, 3].map((x) {return x * x;
});// best
[1, 2, 3].map(x x * x);箭头函数取代Function.prototype.bind不应再用 self/_this/that 绑定 this。
// bad
const self this;
const boundMethod function(...params) {return method.apply(self, params);
}// acceptable
const boundMethod method.bind(this);// best
const boundMethod (...params) method.apply(this, params);简单的、单行的、不会复用的函数建议采用箭头函数。如果函数体较为复杂行数较多还是应该采用传统的函数写法。
所有配置项都应该集中在一个对象放在最后一个参数布尔值最好不要直接作为参数因为代码语义会很差也不利于将来增加其他配置项。
// bad
function divide(a, b, option false ) {
}// good
function divide(a, b, { option false } {}) {
}不要在函数体内使用 arguments 变量使用 rest 运算符...代替。因为 rest 运算符显式表明你想要获取参数而且 arguments 是一个类似数组的对象而 rest 运算符可以提供一个真正的数组。
// bad
function concatenateAll() {const args Array.prototype.slice.call(arguments);return args.join();
}// good
function concatenateAll(...args) {return args.join();
}使用默认值语法设置函数参数的默认值。
// bad
function handleThings(opts) {opts opts || {};
}// good
function handleThings(opts {}) {// ...
}7. Map 结构
注意区分 Object 和 Map只有模拟现实世界的实体对象时才使用 Object。如果只是需要key: value的数据结构使用 Map 结构。因为 Map 有内建的遍历机制。
let map new Map(arr);for (let key of map.keys()) {console.log(key);
}for (let value of map.values()) {console.log(value);
}for (let item of map.entries()) {console.log(item[0], item[1]);
}8. Class
总是用 Class取代需要 prototype 的操作。因为 Class 的写法更简洁更易于理解。
// bad
function Queue(contents []) {this._queue [...contents];
}
Queue.prototype.pop function() {const value this._queue[0];this._queue.splice(0, 1);return value;
}// good
class Queue {constructor(contents []) {this._queue [...contents];}pop() {const value this._queue[0];this._queue.splice(0, 1);return value;}
}使用extends实现继承因为这样更简单不会有破坏instanceof运算的危险。
// bad
const inherits require(inherits);
function PeekableQueue(contents) {Queue.apply(this, contents);
}
inherits(PeekableQueue, Queue);
PeekableQueue.prototype.peek function() {return this._queue[0];
}// good
class PeekableQueue extends Queue {peek() {return this._queue[0];}
}9. 模块
ES6 模块语法是 JavaScript 模块的标准写法坚持使用这种写法取代 Node.js 的 CommonJS 语法。
首先使用import取代require()。
// CommonJS 的写法
const moduleA require(moduleA);
const func1 moduleA.func1;
const func2 moduleA.func2;// ES6 的写法
import { func1, func2 } from moduleA;其次使用export取代module.exports。
// commonJS 的写法
var React require(react);var Breadcrumbs React.createClass({render() {return nav /;}
});module.exports Breadcrumbs;// ES6 的写法
import React from react;class Breadcrumbs extends React.Component {render() {return nav /;}
};export default Breadcrumbs;如果模块只有一个输出值就使用export default如果模块有多个输出值除非其中某个输出值特别重要否则建议不要使用export default即多个输出值如果是平等关系export default与普通的export就不要同时使用。
如果模块默认输出一个函数函数名的首字母应该小写表示这是一个工具方法。
function makeStyleGuide() {
}export default makeStyleGuide;如果模块默认输出一个对象对象名的首字母应该大写表示这是一个配置值对象。
const StyleGuide {es6: {}
};export default StyleGuide;10. ESLint 的使用
ESLint 是一个语法规则和代码风格的检查工具可以用来保证写出语法正确、风格统一的代码。
首先在项目的根目录安装 ESLint。
$ npm install --save-dev eslint然后安装 Airbnb 语法规则以及 import、a11y、react 插件。
$ npm install --save-dev eslint-config-airbnb
$ npm install --save-dev eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react最后在项目的根目录下新建一个.eslintrc文件配置 ESLint。
{extends: eslint-config-airbnb
}现在就可以检查当前项目的代码是否符合预设的规则。
index.js文件的代码如下。
var unused I have no purpose!;function greet() {var message Hello, World!;console.log(message);
}greet();使用 ESLint 检查这个文件就会报出错误。
$ npx eslint index.js
index.js1:1 error Unexpected var, use let or const instead no-var1:5 error unused is defined but never used no-unused-vars4:5 error Expected indentation of 2 characters but found 4 indent4:5 error Unexpected var, use let or const instead no-var5:5 error Expected indentation of 2 characters but found 4 indent✖ 5 problems (5 errors, 0 warnings)上面代码说明原文件有五个错误其中两个是不应该使用var命令而要使用let或const一个是定义了变量却没有使用另外两个是行首缩进为 4 个空格而不是规定的 2 个空格。