一、变量
使用有意义和可发音的变量名
- // 不好的写法
- const yyyymmdstr = moment().format("YYYY/MM/DD");
-
- // 好的写法
- const currentDate = moment().format("YYYY/MM/DD");
对同一类型的变量使用相同的词汇
- // 不好的写法
- getUserInfo();
- getClientData();
- getCustomerRecord();
-
- // 好的写法
- getUser();
使用可搜索的名字
我们读的会比我们写的多得多,所以如果命名太过随意不仅会给后续的维护带来困难,也会伤害了读我们代码的开发者。让你的变量名可被读取,像 buddy.js 和 ESLint 这样的工具可以帮助识别未命名的常量。
- // 不好的写法
- // 86400000 的用途是什么?
- setTimeout(blastOff, 86400000);
-
- // 好的写法
- const MILLISECONDS_IN_A_DAY = 86_400_000;
- setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
使用解释性变量
- // 不好的写法
- const address = "One Infinite Loop, Cupertino 95014";
- const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
- saveCityZipCode(
- address.match(cityZipCodeRegex)[1],
- address.match(cityZipCodeRegex)[2]
- );
-
-
- // 好的写法
- const address = "One Infinite Loop, Cupertino 95014";
- const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
- const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
- saveCityZipCode(city, zipCode);
避免费脑的猜测
显式用于隐式
- // 不好的写法
- const locations = ["Austin", "New York", "San Francisco"];
- locations.forEach(l => {
- doStuff();
- doSomeOtherStuff();
- // ...
- // ...
- // ...
- // 等等,“l”又是什么?
- dispatch(l);
-
- // 好的写法
- const locations = ["Austin", "New York", "San Francisco"];
- locations.forEach(location => {
- doStuff();
- doSomeOtherStuff();
- // ...
- // ...
- // ...
- dispatch(location);
- });
无需添加不必要的上下文
如果类名/对象名已经说明了,就无需在变量名中重复。
- // 不好的写法
- const Car = {
- carMake: "Honda",
- carModel: "Accord",
- carColor: "Blue"
- };
-
- function paintCar(car) {
- car.carColor = "Red";
- }
- // 好的写法
- const Car = {
- make: "Honda",
- model: "Accord",
- color: "Blue"
- };
-
- function paintCar(car) {
- car.color = "Red";
- }
使用默认参数代替逻辑或(与)运算
- // 不好的写法
- function createMicrobrewery(name) {
- const breweryName = name || "Hipster Brew Co.";
- // ...
- }
- // 好的写法
- function createMicrobrewery(name = "Hipster Brew Co.") {
- // ...
- }
二、函数
函数参数(理想情况下为2个或更少)
限制函数参数的数量是非常重要的,因为它使测试函数变得更容易。如果有三个以上的参数,就会导致组合爆炸,必须用每个单独的参数测试大量不同的情况。
一个或两个参数是理想的情况,如果可能,应避免三个参数。除此之外,还应该合并。大多数情况下,大于三个参数可以用对象来代替。
- // 不好的写法
- function createMenu(title, body, buttonText, cancellable) {
- // ...
- }
-
- createMenu("Foo", "Bar", "Baz", true);
-
- // 好的写法
- function createMenu({ title, body, buttonText, cancellable }) {
- // ...
- }
-
- createMenu({
- title: "Foo",
- body: "Bar",
- buttonText: "Baz",
- cancellable: true
- });
函数应该只做一件事
这是目前为止软件工程中最重要的规则。当函数做不止一件事时,它们就更难组合、测试和推理。可以将一个函数隔离为一个操作时,就可以很容易地重构它,代码也会读起来更清晰。
- // 不好的写法
- function emailClients(clients) {
- clients.forEach(client => {
- const clientRecord = database.lookup(client);
- if (clientRecord.isActive()) {
- email(client);
- }
- });
- }
-
- // 好的写法
-
- function emailActiveClients(clients) {
- clients.filter(isActiveClient).forEach(email);
- }
-
- function isActiveClient(client) {
- const clientRecord = database.lookup(client);
- return clientRecord.isActive();
- }
函数名称应说明其作用
- // 不好的写法
- function addToDate(date, month) {
- // ...
- }
-
- const date = new Date();
-
- // 从函数名称很难知道添加什么
- addToDate(date, 1);
-
- // 好的写法
- function addMonthToDate(month, date) {
- // ...
- }
-
- const date = new Date();
- addMonthToDate(1, date);
函数应该只有一个抽象层次
当有一个以上的抽象层次函数,意味该函数做得太多了,需要将函数拆分可以实现可重用性和更简单的测试。
- // 不好的写法
- function parseBetterJSAlternative(code) {
- const REGEXES = [
- // ...
- ];
-
- const statements = code.split(" ");
- const tokens = [];
- REGEXES.forEach(REGEX => {
- statements.forEach(statement => {
- // ...
- });
- });
-
- const ast = [];
- tokens.forEach(token => {
- // lex...
- });
-
- ast.forEach(node => {
- // parse...
- });
- }
-
- // 好的写法
- function parseBetterJSAlternative(code) {
- const tokens = tokenize(code);
- const syntaxTree = parse(tokens);
- syntaxTree.forEach(node => {
- // parse...
- });
- }
-
- function tokenize(code) {
- const REGEXES = [
- // ...
- ];
-
- const statements = code.split(" ");
- const tokens = [];
- REGEXES.forEach(REGEX => {
- statements.forEach(statement => {
- tokens.push();
- });
- });
-
- return tokens;
- }
-
- function parse(tokens) {
- const syntaxTree = [];
- tokens.forEach(token => {
- syntaxTree.push();
- });
-
- return syntaxTree;
- }
删除重复的代码
尽量避免重复的代码,重复的代码是不好的,它意味着如果我们需要更改某些逻辑,要改很多地方。
通常,有重复的代码,是因为有两个或多个稍有不同的事物,它们有很多共同点,但是它们之间的差异迫使我们编写两个或多个独立的函数来完成许多相同的事情。 删除重复的代码意味着创建一个仅用一个函数/模块/类就可以处理这组不同事物的抽象。
获得正确的抽象是至关重要的,这就是为什么我们应该遵循类部分中列出的 「SOLID原则」。糟糕的抽象可能比重复的代码更糟糕,所以要小心!说了这么多,如果你能做一个好的抽象,那就去做吧!不要重复你自己,否则你会发现自己在任何时候想要改变一件事的时候都要更新多个地方。
「设计模式的六大原则有」
- Single Responsibility Principle:单一职责原则
- Open Closed Principle:开闭原则
- Liskov Substitution Principle:里氏替换原则
- Law of Demeter:迪米特法则
- Interface Segregation Principle:接口隔离原则
- Dependence Inversion Principle:依赖倒置原则
把这六个原则的首字母联合起来(两个 L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计。下面我们来分别看一下这六大设计原则。
「不好的写法」
- function showDeveloperList(developers) {
- developers.forEach(developer => {
- const expectedSalary = developer.calculateExpectedSalary();
- const experience = developer.getExperience();
- const githubLink = developer.getGithubLink();
- const data = {
- expectedSalary,
- experience,
- githubLink
- };
-
- render(data);
- });
- }
-
- function showManagerList(managers) {
- managers.forEach(manager => {
- const expectedSalary = manager.calculateExpectedSalary();
- const experience = manager.getExperience();
- const portfolio = manager.getMBAProjects();
- const data = {
- expectedSalary,
- experience,
- portfolio
- };
-
- render(data);
- });
- }
「好的写法」
- function showEmployeeList(employees) {
- employees.forEach(employee => {
- const expectedSalary = employee.calculateExpectedSalary();
- const experience = employee.getExperience();
-
- const data = {
- expectedSalary,
- experience
- };
-
- switch (employee.type) {
- case "manager":
- data.portfolio = employee.getMBAProjects();
- break;
- case "developer":
- data.githubLink = employee.getGithubLink();
- break;
- }
-
- render(data);
- });
- }
使用Object.assign设置默认对象
「不好的写法」
- const menuConfig = {
- title: null,
- body: "Bar",
- buttonText: null,
- cancellable: true
- };
-
- function createMenu(config) {
- configconfig.title = config.title || "Foo";
- configconfig.body = config.body || "Bar";
- configconfig.buttonText = config.buttonText || "Baz";
- configconfig.cancellable =
- config.cancellable !== undefined ? config.cancellable : true;
- }
-
- createMenu(menuConfig);
「好的写法」
- const menuConfig = {
- title: "Order",
- // User did not include 'body' key
- buttonText: "Send",
- cancellable: true
- };
-
- function createMenu(config) {
- config = Object.assign(
- {
- title: "Foo",
- body: "Bar",
- buttonText: "Baz",
- cancellable: true
- },
- config
- );
-
- // config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
- // ...
- }
-
- createMenu(menuConfig);
不要使用标志作为函数参数
标志告诉使用者,此函数可以完成多项任务,函数应该做一件事。如果函数遵循基于布尔的不同代码路径,请拆分它们。
- // 不好的写法
- function createFile(name, temp) {
- if (temp) {
- fs.create(`./temp/${name}`);
- } else {
- fs.create(name);
- }
- }
-
- // 好的写法
- function createFile(name) {
- fs.create(name);
- }
-
- function createTempFile(name) {
- createFile(`./temp/${name}`);
- }
避免副作用(第一部分)
如果函数除了接受一个值并返回另一个值或多个值以外,不执行任何其他操作,都会产生副作用。副作用可能是写入文件,修改某些全局变量,或者不小心将你的所有资金都汇给了陌生人。
「不好的写法」
- let name = "Ryan McDermott";
-
- function splitIntoFirstAndLastName() {
- namename = name.split(" ");
- }
-
- splitIntoFirstAndLastName();
-
- console.log(name); // ['Ryan', 'McDermott'];
「好的写法」
- function splitIntoFirstAndLastName(name) {
- return name.split(" ");
- }
-
- const name = "Ryan McDermott";
- const newName = splitIntoFirstAndLastName(name);
-
- console.log(name); // 'Ryan McDermott';
- console.log(newName); // ['Ryan', 'McDermott'];
避免副作用(第二部分)
在JavaScript中,原始类型值是按值传递,而对象/数组按引用传递。对于对象和数组,如果有函数在购物车数组中进行了更改(例如,通过添加要购买的商品),则使用该购物车数组的任何其他函数都将受到此添加的影响。那可能很棒,但是也可能不好。来想象一个糟糕的情况:
用户单击“购买”按钮,该按钮调用一个purchase 函数,接着,该函数发出一个网络请求并将cart数组发送到服务器。由于网络连接不好,purchase函数必须不断重试请求。现在,如果在网络请求开始之前,用户不小心点击了他们实际上不需要的项目上的“添加到购物车”按钮,该怎么办?如果发生这种情况,并且网络请求开始,那么购买函数将发送意外添加的商品,因为它有一个对购物车数组的引用,addItemToCart函数通过添加修改了这个购物车数组。
一个很好的解决方案是addItemToCart总是克隆cart数组,编辑它,然后返回克隆。这可以确保购物车引用的其他函数不会受到任何更改的影响。
关于这种方法有两点需要注意:
- 可能在某些情况下,我们确实需要修改输入对象,但是当我们采用这种编程实践时,会发现这种情况非常少见,大多数东西都可以被改造成没有副作用。
- 就性能而言,克隆大对象可能会非常昂贵。幸运的是,在实践中这并不是一个大问题,因为有很多很棒的库使这种编程方法能够快速进行,并且不像手动克隆对象和数组那样占用大量内存。
- // 不好的写法
- const addItemToCart = (cart, item) => {
- cart.push({ item, date: Date.now() });
- };
-
- // 好的写法
- const addItemToCart = (cart, item) => {
- return [...cart, { item, date: Date.now() }];
- };
不要写全局函数
污染全局变量在 JS 中是一种不好的做法,因为可能会与另一个库发生冲突,并且在他们的生产中遇到异常之前,API 的用户将毫无用处。让我们考虑一个示例:如果想扩展 JS 的原生Array方法以具有可以显示两个数组之间差异的diff方法,该怎么办?可以将新函数写入Array.prototype,但它可能与另一个尝试执行相同操作的库发生冲突。如果其他库仅使用diff来查找数组的第一个元素和最后一个元素之间的区别怎么办?这就是为什么只使用 ES6 类并简单地扩展Array全局会更好的原因。
- // 不好的写法
- Array.prototype.diff = function diff(comparisonArray) {
- const hash = new Set(comparisonArray);
- return this.filter(elem => !hash.has(elem));
- };
-
- // 好的写法
- class SuperArray extends Array {
- diff(comparisonArray) {
- const hash = new Set(comparisonArray);
- return this.filter(elem => !hash.has(elem));
- }
- }
尽量使用函数式编程而非命令式
JavaScript不像Haskell那样是一种函数式语言,但它具有函数式的风格。函数式语言可以更简洁、更容易测试。如果可以的话,尽量喜欢这种编程风格。
「不好的写法」
- const programmerOutput = [
- {
- name: "Uncle Bobby",
- linesOfCode: 500
- },
- {
- name: "Suzie Q",
- linesOfCode: 1500
- },
- {
- name: "Jimmy Gosling",
- linesOfCode: 150
- },
- {
- name: "Gracie Hopper",
- linesOfCode: 1000
- }
- ];
-
- let totalOutput = 0;
-
- for (let i = 0; i < programmerOutput.length; i++) {
- totalOutput += programmerOutput[i].linesOfCode;
- }
「好的写法」
- const programmerOutput = [
- {
- name: "Uncle Bobby",
- linesOfCode: 500
- },
- {
- name: "Suzie Q",
- linesOfCode: 1500
- },
- {
- name: "Jimmy Gosling",
- linesOfCode: 150
- },
- {
- name: "Gracie Hopper",
- linesOfCode: 1000
- }
- ];
-
- const totalOutput = programmerOutput.reduce(
- (totalLines, output) => totalLines + output.linesOfCode,
- 0
- );
封装条件
- // 不好的写法
- if (fsm.state === "fetching" && isEmpty(listNode)) {
- // ...
- }
-
- // 好的写法
- function shouldShowSpinner(fsm, listNode) {
- return fsm.state === "fetching" && isEmpty(listNode);
- }
-
- if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
- // ...
- }
避免使用非条件
- // 不好的写法
- function isDOMNodeNotPresent(node) {
- // ...
- }
-
- if (!isDOMNodeNotPresent(node)) {
- // ...
- }
-
- // 好的写法
- function isDOMNodePresent(node) {
- // ...
- }
-
- if (isDOMNodePresent(node)) {
- // ...
- }
避免使用过多条件
这似乎是一个不可能完成的任务。一听到这个,大多数人会说,“没有if语句,我怎么能做任何事情呢?”答案是,你可以在许多情况下使用多态性来实现相同的任务。
第二个问题通常是,“那很好,但是我为什么要那样做呢?”答案是上面讲过一个概念:一个函数应该只做一件事。当具有if语句的类和函数时,这是在告诉你的使用者该函数执行不止一件事情。
「不好的写法」
- class Airplane {
- // ...
- getCruisingAltitude() {
- switch (this.type) {
- case "777":
- return this.getMaxAltitude() - this.getPassengerCount();
- case "Air Force One":
- return this.getMaxAltitude();
- case "Cessna":
- return this.getMaxAltitude() - this.getFuelExpenditure();
- }
- }
- }
「好的写法」
- class Airplane {
- // ...
- }
-
- class Boeing777 extends Airplane {
- // ...
- getCruisingAltitude() {
- return this.getMaxAltitude() - this.getPassengerCount();
- }
- }
-
- class AirForceOne extends Airplane {
- // ...
- getCruisingAltitude() {
- return this.getMaxAltitude();
- }
- }
-
- class Cessna extends Airplane {
- // ...
- getCruisingAltitude() {
- return this.getMaxAltitude() - this.getFuelExpenditure();
- }
- }
避免类型检查
JavaScript 是无类型的,这意味着函数可以接受任何类型的参数。有时q我们会被这种自由所困扰,并且很想在函数中进行类型检查。有很多方法可以避免这样做。首先要考虑的是一致的API。
- // 不好的写法
- function travelToTexas(vehicle) {
- if (vehicle instanceof Bicycle) {
- vehicle.pedal(this.currentLocation, new Location("texas"));
- } else if (vehicle instanceof Car) {
- vehicle.drive(this.currentLocation, new Location("texas"));
- }
- }
-
- // 好的写法
- function travelToTexas(vehicle) {
- vehicle.move(this.currentLocation, new Location("texas"));
- }
不要过度优化
现代浏览器在运行时做了大量的优化工作。很多时候,如果你在优化,那么你只是在浪费时间。有很好的资源可以查看哪里缺乏优化,我们只需要针对需要优化的地方就行了。
- // 不好的写法
-
- // 在旧的浏览器上,每一次使用无缓存“list.length”的迭代都是很昂贵的
- // 会为“list.length”重新计算。在现代浏览器中,这是经过优化的
- for (let i = 0, len = list.length; i < len; i++) {
- // ...
- }
-
- // 好的写法
- for (let i = 0; i < list.length; i++) {
- // ...
- }