JavaScript 代码整洁之道

代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础。

本文并不是代码风格指南,而是关于代码的可读性、复用性、扩展性探讨。

我们将从几个方面展开讨论:

  1. 变量

  2. 函数

  3. 对象和数据结构

  4. SOLID

  5. 测试

  6. 异步

  7. 错误处理

  8. 代码风格

  9. 注释

变量

用有意义且常用的单词命名变量

Bad:

const yyyymmdstr = moment().format('YYYY/MM/DD');

Good:

const currentDate = moment().format('YYYY/MM/DD');

保持统一

可能同一个项目对于获取用户信息,会有三个不一样的命名。应该保持统一,如果你不知道该如何取名,可以去 codelf 搜索,看别人是怎么取名的。

Bad:

1      getUserInfo(); 2      getClientData(); 3      getCustomerRecord();

Good:

      getUser()

每个常量都该命名

可以用 buddy.js 或者 ESLint 检测代码中未命名的常量。

Bad:

1    // 三个月之后你还能知道 86400000 是什么吗? 2    setTimeout(blastOff, 86400000);

Good:

1    const MILLISECOND_IN_A_DAY = 86400000; 2    setTimeout(blastOff, MILLISECOND_IN_A_DAY);

可描述

通过一个变量生成了一个新变量,也需要为这个新变量命名,也就是说每个变量当你看到他第一眼你就知道他是干什么的。

Bad:

1    const ADDRESS = 'One Infinite Loop, Cupertino 95014'; 2    const CITY_ZIP_CODE_REGEX = /^[^,\]+[,\s]+(.+?)s*(d{5})?$/; 3    saveCityZipCode(ADDRESS.match(CITY_ZIP_CODE_REGEX)[1], 4                    ADDRESS.match(CITY_ZIP_CODE_REGEX)[2]);

Good:

1    const ADDRESS = 'One Infinite Loop, Cupertino 95014'; 2    const CITY_ZIP_CODE_REGEX = /^[^,\]+[,\s]+(.+?)s*(d{5})?$/; 3    const [, city, zipCode] = ADDRESS.match(CITY_ZIP_CODE_REGEX) || []; 4    saveCityZipCode(city, zipCode);

直接了当

Bad:

1    const locations = ['Austin', 'New York', 'San Francisco']; 2    locations.forEach((l) => { 3      doStuff(); 4      doSomeOtherStuff(); 5      // ... 6      // ... 7      // ... 8      // 需要看其他代码才能确定 'l' 是干什么的。 9      dispatch(l); 10    });

Good: ``js const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach((location) => {   doStuff();   doSomeOtherStuff();   // ...   // ...   // ...   dispatch(location); });

1#### 避免无意义的前缀  2 3如果创建了一个对象 car,就没有必要把它的颜色命名为 carColor。 4 5Bad: 6```js 7      const car = { 8        carMake: 'Honda', 9        carModel: 'Accord', 10        carColor: 'Blue' 11      }; 12     13      function paintCar(car) { 14        car.carColor = 'Red'; 15      }

Good:

1    const car = { 2      make: 'Honda', 3      model: 'Accord', 4      color: 'Blue' 5    }; 6     7    function paintCar(car) { 8      car.color = 'Red'; 9    }

使用默认值

Bad:

1    function createMicrobrewery(name) { 2      const breweryName = name || 'Hipster Brew Co.'; 3      // ... 4    }

Good:

1    function createMicrobrewery(name = 'Hipster Brew Co.') { 2      // ... 3    }

函数

参数越少越好

如果参数超过两个,使用 ES2015/ES6 的解构语法,不用考虑参数的顺序。

Bad:

1    function createMenu(title, body, buttonText, cancellable) { 2      // ... 3    }

Good:

1    function createMenu({ title, body, buttonText, cancellable }) { 2      // ... 3    } 4     5    createMenu({ 6      title: 'Foo', 7      body: 'Bar', 8      buttonText: 'Baz', 9      cancellable: true 10    });

只做一件事情

这是一条在软件工程领域流传久远的规则。严格遵守这条规则会让你的代码可读性更好,也更容易重构。如果违反这个规则,那么代码会很难被测试或者重用。

Bad:

1    function emailClients(clients) { 2      clients.forEach((client) => { 3        const clientRecord = database.lookup(client); 4        if (clientRecord.isActive()) { 5          email(client); 6        } 7      }); 8    }

Good:

1    function emailActiveClients(clients) { 2      clients 3        .filter(isActiveClient) 4        .forEach(email); 5    } 6    function isActiveClient(client) { 7      const clientRecord = database.lookup(client);     8      return clientRecord.isActive(); 9    }

顾名思义

看函数名就应该知道它是干啥的。

Bad:

1    function addToDate(date, month) { 2      // ... 3    } 4     5    const date = new Date(); 6     7    // 很难知道是把什么加到日期中 8    addToDate(date, 1);

Good:

1    function addMonthToDate(month, date) { 2      // ... 3    } 4     5    const date = new Date(); 6    addMonthToDate(1, date);

只需要一层抽象层

如果函数嵌套过多会导致很难复用以及测试。

Bad:

1    function parseBetterJSAlternative(code) { 2      const REGEXES = [ 3        // ... 4      ]; 5     6      const statements = code.split(' '); 7      const tokens = []; 8      REGEXES.forEach((REGEX) => { 9        statements.forEach((statement) => { 10          // ... 11        }); 12      }); 13     14      const ast = []; 15      tokens.forEach((token) => { 16        // lex... 17      }); 18     19      ast.forEach((node) => { 20        // parse... 21      }); 22    }

Good:

1    function parseBetterJSAlternative(code) { 2      const tokens = tokenize(code); 3      const ast = lexer(tokens); 4      ast.forEach((node) => { 5        // parse... 6      }); 7    } 8     9    function tokenize(code) { 10      const REGEXES = [ 11        // ... 12      ]; 13     14      const statements = code.split(' '); 15      const tokens = []; 16      REGEXES.forEach((REGEX) => { 17        statements.forEach((statement) => { 18          tokens.push( /* ... */ ); 19        }); 20      }); 21     22      return tokens; 23    } 24     25    function lexer(tokens) { 26      const ast = []; 27      tokens.forEach((token) => { 28        ast.push( /* ... */ ); 29      }); 30     31      return ast; 32    }

删除重复代码

很多时候虽然是同一个功能,但由于一两个不同点,让你不得不写两个几乎相同的函数。

要想优化重复代码需要有较强的抽象能力,错误的抽象还不如重复代码。所以在抽象过程中必须要遵循 SOLID 原则(SOLID 是什么?稍后会详细介绍)。

Bad:

1    function showDeveloperList(developers) { 2      developers.forEach((developer) => { 3        const expectedSalary = developer.calculateExpectedSalary(); 4        const experience = developer.getExperience(); 5        const githubLink = developer.getGithubLink(); 6        const data = { 7          expectedSalary, 8          experience, 9          githubLink 10        }; 11     12        render(data); 13      }); 14    } 15     16    function showManagerList(managers) { 17      managers.forEach((manager) => { 18        const expectedSalary = manager.calculateExpectedSalary(); 19        const experience = manager.getExperience(); 20        const portfolio = manager.getMBAProjects(); 21        const data = { 22          expectedSalary, 23          experience, 24          portfolio 25        }; 26     27        render(data); 28      }); 29    }

Good:

1    function showEmployeeList(employees) { 2      employees.forEach(employee => { 3        const expectedSalary = employee.calculateExpectedSalary(); 4        const experience = employee.getExperience(); 5        const data = { 6          expectedSalary, 7          experience, 8        }; 9     10        switch(employee.type) { 11          case 'develop': 12            data.githubLink = employee.getGithubLink(); 13            break 14          case 'manager': 15            data.portfolio = employee.getMBAProjects(); 16            break 17        } 18        render(data); 19      }) 20    }

对象设置默认属性

Bad:

1    const menuConfig = { 2      title: null, 3      body: 'Bar', 4      buttonText: null, 5      cancellable: true 6    }; 7     8    function createMenu(config) { 9      config.title = config.title || 'Foo'; 10      config.body = config.body || 'Bar'; 11      config.buttonText = config.buttonText || 'Baz'; 12      config.cancellable = config.cancellable !== undefined ? config.cancellable : true; 13    } 14     15    createMenu(menuConfig);

Good:

1    const menuConfig = { 2      title: 'Order', 3      // 'body' key 缺失 4      buttonText: 'Send', 5      cancellable: true 6    }; 7     8    function createMenu(config) { 9      config = Object.assign({ 10        title: 'Foo', 11        body: 'Bar', 12        buttonText: 'Baz', 13        cancellable: true 14      }, config); 15     16      // config 就变成了: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true} 17      // ... 18    } 19     20    createMenu(menuConfig);

不要传 flag 参数

通过 flag 的 true 或 false,来判断执行逻辑,违反了一个函数干一件事的原则。

Bad:

1    function createFile(name, temp) { 2      if (temp) { 3        fs.create(`./temp/${name}`); 4      } else { 5        fs.create(name); 6      } 7    }

Good:

1    function createFile(name) { 2      fs.create(name); 3    } 4    function createFileTemplate(name) { 5      createFile(`./temp/${name}`) 6    }

避免副作用(第一部分)

函数接收一个值返回一个新值,除此之外的行为我们都称之为副作用,比如修改全局变量、对文件进行 IO 操作等。

当函数确实需要副作用时,比如对文件进行 IO 操作时,请不要用多个函数/类进行文件操作,有且仅用一个函数/类来处理。也就是说副作用需要在唯一的地方处理。

副作用的三大天坑:随意修改可变数据类型、随意分享没有数据结构的状态、没有在统一地方处理副作用。

Bad:

1    // 全局变量被一个函数引用 2    // 现在这个变量从字符串变成了数组,如果有其他的函数引用,会发生无法预见的错误。 3    var name = 'Ryan McDermott'; 4     5    function splitIntoFirstAndLastName() { 6      name = name.split(' '); 7    } 8     9    splitIntoFirstAndLastName(); 10     11    console.log(name); // ['Ryan', 'McDermott'];

Good:

1    var name = 'Ryan McDermott'; 2    var newName = splitIntoFirstAndLastName(name) 3     4    function splitIntoFirstAndLastName(name) { 5      return name.split(' '); 6    } 7     8    console.log(name); // 'Ryan McDermott'; 9    console.log(newName); // ['Ryan', 'McDermott'];

避免副作用(第二部分)

在 JavaScript 中,基本类型通过赋值传递,对象和数组通过引用传递。以引用传递为例:

假如我们写一个购物车,通过 addItemToCart() 方法添加商品到购物车,修改 购物车数组。此时调用 purchase() 方法购买,由于引用传递,获取的 购物车数组 正好是最新的数据。

看起来没问题对不对?

如果当用户点击购买时,网络出现故障, purchase() 方法一直在重复调用,与此同时用户又添加了新的商品,这时网络又恢复了。那么 purchase() 方法获取到 购物车数组 就是错误的。

为了避免这种问题,我们需要在每次新增商品时,克隆 购物车数组 并返回新的数组。

Bad:

1    const addItemToCart = (cart, item) => { 2      cart.push({ item, date: Date.now() }); 3    };

Good:

1    const addItemToCart = (cart, item) => { 2      return [...cart, {item, date: Date.now()}] 3    };

不要写全局方法

在 JavaScript 中,永远不要污染全局,会在生产环境中产生难以预料的 bug。举个例子,比如你在 Array.prototype 上新增一个 diff 方法来判断两个数组的不同。而你同事也打算做类似的事情,不过他的 diff 方法是用来判断两个数组首位元素的不同。很明显你们方法会产生冲突,遇到这类问题我们可以用 ES2015/ES6 的语法来对 Array 进行扩展。

Bad:

1    Array.prototype.diff = function diff(comparisonArray) { 2      const hash = new Set(comparisonArray); 3      return this.filter(elem => !hash.has(elem)); 4    };

Good:

1    class SuperArray extends Array { 2      diff(comparisonArray) { 3        const hash = new Set(comparisonArray); 4        return this.filter(elem => !hash.has(elem));         5      } 6    }

比起命令式我更喜欢函数式编程

函数式变编程可以让代码的逻辑更清晰更优雅,方便测试。

Bad:

1    const programmerOutput = [ 2      { 3        name: 'Uncle Bobby', 4        linesOfCode: 500 5      }, { 6        name: 'Suzie Q', 7        linesOfCode: 1500 8      }, { 9        name: 'Jimmy Gosling', 10        linesOfCode: 150 11      }, { 12        name: 'Gracie Hopper', 13        linesOfCode: 1000 14      } 15    ]; 16     17    let totalOutput = 0; 18     19    for (let i = 0; i < programmerOutput.length; i++) { 20      totalOutput += programmerOutput[i].linesOfCode; 21    }

Good:

1    const programmerOutput = [ 2      { 3        name: 'Uncle Bobby', 4        linesOfCode: 500 5      }, { 6        name: 'Suzie Q', 7        linesOfCode: 1500 8      }, { 9        name: 'Jimmy Gosling', 10        linesOfCode: 150 11      }, { 12        name: 'Gracie Hopper', 13        linesOfCode: 1000 14      } 15    ]; 16    let totalOutput = programmerOutput 17      .map(output => output.linesOfCode) 18      .reduce((totalLines, lines) => totalLines + lines, 0)

封装条件语句

Bad:

1    if (fsm.state === 'fetching' && isEmpty(listNode)) { 2      // ... 3    }

Good:

1    function shouldShowSpinner(fsm, listNode) { 2      return fsm.state === 'fetching' && isEmpty(listNode); 3    } 4     5    if (shouldShowSpinner(fsmInstance, listNodeInstance)) { 6      // ... 7    }

尽量别用“非”条件句

Bad:

1    function isDOMNodeNotPresent(node) { 2      // ... 3    } 4     5    if (!isDOMNodeNotPresent(node)) { 6      // ... 7    }

Good:

1    function isDOMNodePresent(node) { 2      // ... 3    } 4     5    if (isDOMNodePresent(node)) { 6      // ... 7    }

避免使用条件语句

Q:不用条件语句写代码是不可能的。

A:绝大多数场景可以用多态替代。

Q:用多态可行,但为什么就不能用条件语句了呢?

A:为了让代码更简洁易读,如果你的函数中出现了条件判断,那么说明你的函数不止干了一件事情,违反了函数单一原则。

Bad:

1    class Airplane { 2      // ... 3     4      // 获取巡航高度 5      getCruisingAltitude() { 6        switch (this.type) { 7          case '777': 8            return this.getMaxAltitude() - this.getPassengerCount(); 9          case 'Air Force One': 10            return this.getMaxAltitude(); 11          case 'Cessna': 12            return this.getMaxAltitude() - this.getFuelExpenditure(); 13        } 14      } 15    }

Good:

1    class Airplane { 2      // ... 3    } 4    // 波音777 5    class Boeing777 extends Airplane { 6      // ... 7      getCruisingAltitude() { 8        return this.getMaxAltitude() - this.getPassengerCount(); 9      } 10    } 11    // 空军一号 12    class AirForceOne extends Airplane { 13      // ... 14      getCruisingAltitude() { 15        return this.getMaxAltitude(); 16      } 17    } 18    // 赛纳斯飞机 19    class Cessna extends Airplane { 20      // ... 21      getCruisingAltitude() { 22        return this.getMaxAltitude() - this.getFuelExpenditure(); 23      } 24    }

避免类型检查(第一部分)

JavaScript 是无类型的,意味着你可以传任意类型参数,这种自由度很容易让人困扰,不自觉的就会去检查类型。仔细想想是你真的需要检查类型还是你的 API 设计有问题?

Bad:

1    function travelToTexas(vehicle) { 2      if (vehicle instanceof Bicycle) { 3        vehicle.pedal(this.currentLocation, new Location('texas')); 4      } else if (vehicle instanceof Car) { 5        vehicle.drive(this.currentLocation, new Location('texas')); 6      } 7    }

Good:

1    function travelToTexas(vehicle) { 2      vehicle.move(this.currentLocation, new Location('texas')); 3    }

避免类型检查(第二部分)

如果你需要做静态类型检查,比如字符串、整数等,推荐使用 TypeScript,不然你的代码会变得又臭又长。

Bad:

1    function combine(val1, val2) { 2      if (typeof val1 === 'number' && typeof val2 === 'number' || 3          typeof val1 === 'string' && typeof val2 === 'string') { 4        return val1 + val2; 5      } 6     7      throw new Error('Must be of type String or Number'); 8    }

Good:

1    function combine(val1, val2) { 2      return val1 + val2; 3    }

不要过度优化

现代浏览器已经在底层做了很多优化,过去的很多优化方案都是无效的,会浪费你的时间,想知道现代浏览器优化了哪些内容,请点这里。

Bad:

1    // 在老的浏览器中,由于 `list.length` 没有做缓存,每次迭代都会去计算,造成不必要开销。 2    // 现代浏览器已对此做了优化。 3    for (let i = 0, len = list.length; i < len; i++) { 4      // ... 5    }

Good:

1    for (let i = 0; i < list.length; i++) { 2      // ... 3    }

删除弃用代码

很多时候有些代码已经没有用了,但担心以后会用,舍不得删。

如果你忘了这件事,这些代码就永远存在那里了。

放心删吧,你可以在代码库历史版本中找他它。

Bad:

1    function oldRequestModule(url) { 2      // ... 3    } 4     5    function newRequestModule(url) { 6      // ... 7    } 8     9    const req = newRequestModule; 10    inventoryTracker('apples', req, 'www.inventory-awesome.io');

Good:

1    function newRequestModule(url) { 2      // ... 3    } 4     5    const req = newRequestModule; 6    inventoryTracker('apples', req, 'www.inventory-awesome.io');

对象和数据结构

用 get、set 方法操作数据

这样做可以带来很多好处,比如在操作数据时打日志,方便跟踪错误;在 set 的时候很容易对数据进行校验…

Bad:

1    function makeBankAccount() { 2      // ... 3     4      return { 5        balance: 0, 6        // ... 7      }; 8    } 9     10    const account = makeBankAccount(); 11    account.balance = 100;

Good:

1    function makeBankAccount() { 2      // 私有变量 3      let balance = 0; 4     5      function getBalance() { 6        return balance; 7      } 8     9      function setBalance(amount) { 10        // ... 在更新 balance 前,对 amount 进行校验 11        balance = amount; 12      } 13     14      return { 15        // ... 16        getBalance, 17        setBalance, 18      }; 19    } 20     21    const account = makeBankAccount(); 22    account.setBalance(100);

使用私有变量

可以用闭包来创建私有变量

Bad:

1    const Employee = function(name) { 2      this.name = name; 3    }; 4     5    Employee.prototype.getName = function getName() { 6      return this.name; 7    }; 8     9    const employee = new Employee('John Doe'); 10    console.log(`Employee name: ${employee.getName()}`);  11    // Employee name: John Doe 12    delete employee.name; 13    console.log(`Employee name: ${employee.getName()}`); 14     // Employee name: undefined

Good:

1    function makeEmployee(name) { 2      return { 3        getName() { 4          return name; 5        }, 6      }; 7    } 8     9    const employee = makeEmployee('John Doe'); 10    console.log(`Employee name: ${employee.getName()}`);  11    // Employee name: John Doe 12    delete employee.name; 13    console.log(`Employee name: ${employee.getName()}`);  14    // Employee name: John Doe

使用 class

在 ES2015/ES6 之前,没有类的语法,只能用构造函数的方式模拟类,可读性非常差。

Bad:

1    // 动物 2    const Animal = function(age) { 3      if (!(this instanceof Animal)) { 4        throw new Error('Instantiate Animal with `new`'); 5      } 6     7      this.age = age; 8    }; 9     10    Animal.prototype.move = function move() {}; 11     12    // 哺乳动物 13    const Mammal = function(age, furColor) { 14      if (!(this instanceof Mammal)) { 15        throw new Error('Instantiate Mammal with `new`'); 16      } 17     18      Animal.call(this, age); 19      this.furColor = furColor; 20    }; 21     22    Mammal.prototype = Object.create(Animal.prototype); 23    Mammal.prototype.constructor = Mammal; 24    Mammal.prototype.liveBirth = function liveBirth() {}; 25     26    // 人类 27    const Human = function(age, furColor, languageSpoken) { 28      if (!(this instanceof Human)) { 29        throw new Error('Instantiate Human with `new`'); 30      } 31     32      Mammal.call(this, age, furColor); 33      this.languageSpoken = languageSpoken; 34    }; 35     36    Human.prototype = Object.create(Mammal.prototype); 37    Human.prototype.constructor = Human; 38    Human.prototype.speak = function speak() {};

Good:

1    // 动物 2    class Animal { 3      constructor(age) { 4        this.age = age 5      }; 6      move() {}; 7    } 8     9    // 哺乳动物 10    class Mammal extends Animal{ 11      constructor(age, furColor) { 12        super(age); 13        this.furColor = furColor; 14      }; 15      liveBirth() {}; 16    } 17     18    // 人类 19    class Human extends Mammal{ 20      constructor(age, furColor, languageSpoken) { 21        super(age, furColor); 22        this.languageSpoken = languageSpoken; 23      }; 24      speak() {}; 25    }

链式调用

这种模式相当有用,可以在很多库中发现它的身影,比如 jQuery、Lodash 等。它让你的代码简洁优雅。实现起来也非常简单,在类的方法最后返回 this 可以了。

Bad:

1    class Car { 2      constructor(make, model, color) { 3        this.make = make; 4        this.model = model; 5        this.color = color; 6      } 7     8      setMake(make) { 9        this.make = make; 10      } 11     12      setModel(model) { 13        this.model = model; 14      } 15     16      setColor(color) { 17        this.color = color; 18      } 19     20      save() { 21        console.log(this.make, this.model, this.color); 22      } 23    } 24     25    const car = new Car('Ford','F-150','red'); 26    car.setColor('pink'); 27    car.save();

Good:

1    class Car { 2      constructor(make, model, color) { 3        this.make = make; 4        this.model = model; 5        this.color = color; 6      } 7     8      setMake(make) { 9        this.make = make; 10        return this; 11      } 12     13      setModel(model) { 14        this.model = model; 15        return this; 16      } 17     18      setColor(color) { 19        this.color = color; 20        return this; 21      } 22     23      save() { 24        console.log(this.make, this.model, this.color); 25        return this; 26      } 27    } 28     29    const car = new Car('Ford','F-150','red') 30      .setColor('pink'); 31      .save();

不要滥用继承

很多时候继承被滥用,导致可读性很差,要搞清楚两个类之间的关系,继承表达的一个属于关系,而不是包含关系,比如 Human->Animal vs. User->UserDetails

Bad:

1    class Employee { 2      constructor(name, email) { 3        this.name = name; 4        this.email = email; 5      } 6     7      // ... 8    } 9     10    // TaxData(税收信息)并不是属于 Employee(雇员),而是包含关系。 11    class EmployeeTaxData extends Employee { 12      constructor(ssn, salary) { 13        super(); 14        this.ssn = ssn; 15        this.salary = salary; 16      } 17     18      // ... 19    }

Good:

1    class EmployeeTaxData { 2      constructor(ssn, salary) { 3        this.ssn = ssn; 4        this.salary = salary; 5      } 6     7      // ... 8    } 9     10    class Employee { 11      constructor(name, email) { 12        this.name = name; 13        this.email = email; 14      } 15     16      setTaxData(ssn, salary) { 17        this.taxData = new EmployeeTaxData(ssn, salary); 18      } 19      // ... 20    }

SOLID

SOLID 是几个单词首字母组合而来,分别表示 单一功能原则、开闭原则、里氏替换原则、接口隔离原则以及依赖反转原则。

单一功能原则

如果一个类干的事情太多太杂,会导致后期很难维护。我们应该厘清职责,各司其职减少相互之间依赖。

Bad:

1    class UserSettings { 2      constructor(user) { 3        this.user = user; 4      } 5     6      changeSettings(settings) { 7        if (this.verifyCredentials()) { 8          // ... 9        } 10      } 11     12      verifyCredentials() { 13        // ... 14      } 15    }

Good:

1    class UserAuth { 2      constructor(user) { 3        this.user = user; 4      } 5      verifyCredentials() { 6        // ... 7      } 8    } 9     10    class UserSetting { 11      constructor(user) { 12        this.user = user; 13        this.auth = new UserAuth(this.user); 14      } 15      changeSettings(settings) { 16        if (this.auth.verifyCredentials()) { 17          // ... 18        } 19      } 20    } 21    }

开闭原则

“开”指的就是类、模块、函数都应该具有可扩展性,“闭”指的是它们不应该被修改。也就是说你可以新增功能但不能去修改源码。

Bad:

1    class AjaxAdapter extends Adapter { 2      constructor() { 3        super(); 4        this.name = 'ajaxAdapter'; 5      } 6    } 7     8    class NodeAdapter extends Adapter { 9      constructor() { 10        super(); 11        this.name = 'nodeAdapter'; 12      } 13    } 14     15    class HttpRequester { 16      constructor(adapter) { 17        this.adapter = adapter; 18      } 19     20      fetch(url) { 21        if (this.adapter.name === 'ajaxAdapter') { 22          return makeAjaxCall(url).then((response) => { 23            // 传递 response 并 return 24          }); 25        } else if (this.adapter.name === 'httpNodeAdapter') { 26          return makeHttpCall(url).then((response) => { 27            // 传递 response 并 return 28          }); 29        } 30      } 31    } 32     33    function makeAjaxCall(url) { 34      // 处理 request 并 return promise 35    } 36     37    function makeHttpCall(url) { 38      // 处理 request 并 return promise 39    }

Good:

1    class AjaxAdapter extends Adapter { 2      constructor() { 3        super(); 4        this.name = 'ajaxAdapter'; 5      } 6     7      request(url) { 8        // 处理 request 并 return promise 9      } 10    } 11     12    class NodeAdapter extends Adapter { 13      constructor() { 14        super(); 15        this.name = 'nodeAdapter'; 16      } 17     18      request(url) { 19        // 处理 request 并 return promise 20      } 21    } 22     23    class HttpRequester { 24      constructor(adapter) { 25        this.adapter = adapter; 26      } 27     28      fetch(url) { 29        return this.adapter.request(url).then((response) => { 30          // 传递 response 并 return 31        }); 32      } 33    }

里氏替换原则

名字很唬人,其实道理很简单,就是子类不要去重写父类的方法。

Bad:

1    // 长方形 2    class Rectangle { 3      constructor() { 4        this.width = 0; 5        this.height = 0; 6      } 7     8      setColor(color) { 9        // ... 10      } 11     12      render(area) { 13        // ... 14      } 15     16      setWidth(width) { 17        this.width = width; 18      } 19     20      setHeight(height) { 21        this.height = height; 22      } 23     24      getArea() { 25        return this.width * this.height; 26      } 27    } 28     29    // 正方形 30    class Square extends Rectangle { 31      setWidth(width) { 32        this.width = width; 33        this.height = width; 34      } 35     36      setHeight(height) { 37        this.width = height; 38        this.height = height; 39      } 40    } 41     42    function renderLargeRectangles(rectangles) { 43      rectangles.forEach((rectangle) => { 44        rectangle.setWidth(4); 45        rectangle.setHeight(5); 46        const area = rectangle.getArea();  47        rectangle.render(area); 48      }); 49    } 50     51    const rectangles = [new Rectangle(), new Rectangle(), new Square()]; 52    renderLargeRectangles(rectangles);

Good:

1    class Shape { 2      setColor(color) { 3        // ... 4      } 5     6      render(area) { 7        // ... 8      } 9    } 10     11    class Rectangle extends Shape { 12      constructor(width, height) { 13        super(); 14        this.width = width; 15        this.height = height; 16      } 17     18      getArea() { 19        return this.width * this.height; 20      } 21    } 22     23    class Square extends Shape { 24      constructor(length) { 25        super(); 26        this.length = length; 27      } 28     29      getArea() { 30        return this.length * this.length; 31      } 32    } 33     34    function renderLargeShapes(shapes) { 35      shapes.forEach((shape) => { 36        const area = shape.getArea(); 37        shape.render(area); 38      }); 39    } 40     41    const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; 42    renderLargeShapes(shapes);

接口隔离原则

JavaScript 几乎没有接口的概念,所以这条原则很少被使用。官方定义是“客户端不应该依赖它不需要的接口”,也就是接口最小化,把接口解耦。

Bad:

1    class DOMTraverser { 2      constructor(settings) { 3        this.settings = settings; 4        this.setup(); 5      } 6     7      setup() { 8        this.rootNode = this.settings.rootNode; 9        this.animationModule.setup(); 10      } 11     12      traverse() { 13        // ... 14      } 15    } 16     17    const $ = new DOMTraverser({ 18      rootNode: document.getElementsByTagName('body'), 19      animationModule() {} // Most of the time, we won't need to animate when traversing. 20      // ... 21    });

Good:

1    class DOMTraverser { 2      constructor(settings) { 3        this.settings = settings; 4        this.options = settings.options; 5        this.setup(); 6      } 7     8      setup() { 9        this.rootNode = this.settings.rootNode; 10        this.setupOptions(); 11      } 12     13      setupOptions() { 14        if (this.options.animationModule) { 15          // ... 16        } 17      } 18     19      traverse() { 20        // ... 21      } 22    } 23     24    const $ = new DOMTraverser({ 25      rootNode: document.getElementsByTagName('body'), 26      options: { 27        animationModule() {} 28      } 29    });

依赖反转原则

说就两点:

高层次模块不能依赖低层次模块,它们依赖于抽象接口。
抽象接口不能依赖具体实现,具体实现依赖抽象接口。
总结下来就两个字,解耦。

Bad:

1    // 库存查询 2    class InventoryRequester { 3      constructor() { 4        this.REQ_METHODS = ['HTTP']; 5      } 6     7      requestItem(item) { 8        // ... 9      } 10    } 11     12    // 库存跟踪 13    class InventoryTracker { 14      constructor(items) { 15        this.items = items; 16     17        // 这里依赖一个特殊的请求类,其实我们只是需要一个请求方法。 18        this.requester = new InventoryRequester(); 19      } 20     21      requestItems() { 22        this.items.forEach((item) => { 23          this.requester.requestItem(item); 24        }); 25      } 26    } 27     28    const inventoryTracker = new InventoryTracker(['apples', 'bananas']); 29    inventoryTracker.requestItems();

Good:

1    // 库存跟踪 2    class InventoryTracker { 3      constructor(items, requester) { 4        this.items = items; 5        this.requester = requester; 6      } 7     8      requestItems() { 9        this.items.forEach((item) => { 10          this.requester.requestItem(item); 11        }); 12      } 13    } 14     15    // HTTP 请求 16    class InventoryRequesterHTTP { 17      constructor() { 18        this.REQ_METHODS = ['HTTP']; 19      } 20     21      requestItem(item) { 22        // ... 23      } 24    } 25     26    // webSocket 请求 27    class InventoryRequesterWS { 28      constructor() { 29        this.REQ_METHODS = ['WS']; 30      } 31     32      requestItem(item) { 33        // ... 34      } 35    } 36     37    // 通过依赖注入的方式将请求模块解耦,这样我们就可以很轻易的替换成 webSocket 请求。 38    const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterHTTP()); 39    inventoryTracker.requestItems();

测试

随着项目变得越来越庞大,时间线拉长,有的老代码可能半年都没碰过,如果此时上线,你有信心这部分代码能正常工作吗?测试的覆盖率和你的信心是成正比的。

PS: 如果你发现你的代码很难被测试,那么你应该优化你的代码了。

单一化

Bad:

1    import assert from 'assert'; 2     3    describe('MakeMomentJSGreatAgain', () => { 4      it('handles date boundaries', () => { 5        let date; 6     7        date = new MakeMomentJSGreatAgain('1/1/2015'); 8        date.addDays(30); 9        assert.equal('1/31/2015', date); 10     11        date = new MakeMomentJSGreatAgain('2/1/2016'); 12        date.addDays(28); 13        assert.equal('02/29/2016', date); 14     15        date = new MakeMomentJSGreatAgain('2/1/2015'); 16        date.addDays(28); 17        assert.equal('03/01/2015', date); 18      }); 19    });

Good:

1    import assert from 'assert'; 2     3    describe('MakeMomentJSGreatAgain', () => { 4      it('handles 30-day months', () => { 5        const date = new MakeMomentJSGreatAgain('1/1/2015'); 6        date.addDays(30); 7        assert.equal('1/31/2015', date); 8      }); 9     10      it('handles leap year', () => { 11        const date = new MakeMomentJSGreatAgain('2/1/2016'); 12        date.addDays(28); 13        assert.equal('02/29/2016', date); 14      }); 15     16      it('handles non-leap year', () => { 17        const date = new MakeMomentJSGreatAgain('2/1/2015'); 18        date.addDays(28); 19        assert.equal('03/01/2015', date); 20      }); 21    });

异步

不再使用回调

不会有人愿意去看嵌套回调的代码,用 Promises 替代回调吧。

Bad:

1    import { get } from 'request'; 2    import { writeFile } from 'fs'; 3     4    get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response) => { 5      if (requestErr) { 6        console.error(requestErr); 7      } else { 8        writeFile('article.html', response.body, (writeErr) => { 9          if (writeErr) { 10            console.error(writeErr); 11          } else { 12            console.log('File written'); 13          } 14        }); 15      } 16    });

Good:

1    get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') 2      .then((response) => { 3        return writeFile('article.html', response); 4      }) 5      .then(() => { 6        console.log('File written'); 7      }) 8      .catch((err) => { 9        console.error(err); 10      });

Async/Await 比起 Promises 更简洁

Bad:

1    import { get } from 'request-promise'; 2    import { writeFile } from 'fs-promise'; 3     4    get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') 5      .then((response) => { 6        return writeFile('article.html', response); 7      }) 8      .then(() => { 9        console.log('File written'); 10      }) 11      .catch((err) => { 12        console.error(err); 13      });

Good:

1    import { get } from 'request-promise'; 2    import { writeFile } from 'fs-promise'; 3     4    async function getCleanCodeArticle() { 5      try { 6        const response = await get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'); 7        await writeFile('article.html', response); 8        console.log('File written'); 9      } catch(err) { 10        console.error(err); 11      } 12    }

错误处理

不要忽略抛异常

Bad:

1    try { 2      functionThatMightThrow(); 3    } catch (error) { 4      console.log(error); 5    }

Good:

1    try { 2      functionThatMightThrow(); 3    } catch (error) { 4      // 这一种选择,比起 console.log 更直观 5      console.error(error); 6      // 也可以在界面上提醒用户 7      notifyUserOfError(error); 8      // 也可以把异常传回服务器 9      reportErrorToService(error); 10      // 其他的自定义方法 11    }

不要忘了在 Promises 抛异常

Bad:

1    getdata() 2      .then((data) => { 3        functionThatMightThrow(data); 4      }) 5      .catch((error) => { 6        console.log(error); 7      });

Good:

1    getdata() 2      .then((data) => { 3        functionThatMightThrow(data); 4      }) 5      .catch((error) => { 6        // 这一种选择,比起 console.log 更直观 7        console.error(error); 8        // 也可以在界面上提醒用户 9        notifyUserOfError(error); 10        // 也可以把异常传回服务器 11        reportErrorToService(error); 12        // 其他的自定义方法 13      });

代码风格

代码风格是主观的,争论哪种好哪种不好是在浪费生命。市面上有很多自动处理代码风格的工具,选一个喜欢就行了,我们来讨论几个非自动处理的部分。

常量大写

Bad:

1    const DAYS_IN_WEEK = 7; 2    const daysInMonth = 30; 3     4    const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; 5    const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles']; 6     7    function eraseDatabase() {} 8    function restore_database() {} 9     10    class animal {} 11    class Alpaca {}

Good:

1    const DAYS_IN_WEEK = 7; 2    const DAYS_IN_MONTH = 30; 3     4    const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; 5    const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles']; 6     7    function eraseDatabase() {} 8    function restoreDatabase() {} 9     10    class Animal {} 11    class Alpaca {}

先声明后调用

就像我们看报纸文章一样,从上到下看,所以为了方便阅读把函数声明写在函数调用前面。

Bad:

1    class PerformanceReview { 2      constructor(employee) { 3        this.employee = employee; 4      } 5     6      lookupPeers() { 7        return db.lookup(this.employee, 'peers'); 8      } 9     10      lookupManager() { 11        return db.lookup(this.employee, 'manager'); 12      } 13     14      getPeerReviews() { 15        const peers = this.lookupPeers(); 16        // ... 17      } 18     19      perfReview() { 20        this.getPeerReviews(); 21        this.getManagerReview(); 22        this.getSelfReview(); 23      } 24     25      getManagerReview() { 26        const manager = this.lookupManager(); 27      } 28     29      getSelfReview() { 30        // ... 31      } 32    } 33     34    const review = new PerformanceReview(employee); 35    review.perfReview();

Good:

1    class PerformanceReview { 2      constructor(employee) { 3        this.employee = employee; 4      } 5     6      perfReview() { 7        this.getPeerReviews(); 8        this.getManagerReview(); 9        this.getSelfReview(); 10      } 11     12      getPeerReviews() { 13        const peers = this.lookupPeers(); 14        // ... 15      } 16     17      lookupPeers() { 18        return db.lookup(this.employee, 'peers'); 19      } 20     21      getManagerReview() { 22        const manager = this.lookupManager(); 23      } 24     25      lookupManager() { 26        return db.lookup(this.employee, 'manager'); 27      } 28     29      getSelfReview() { 30        // ... 31      } 32    } 33     34    const review = new PerformanceReview(employee); 35    review.perfReview();

注释

只有业务逻辑需要注释

代码注释不是越多越好。

Bad:

1    function hashIt(data) { 2      // 这是初始值 3      let hash = 0; 4     5      // 数组的长度 6      const length = data.length; 7     8      // 循环数组 9      for (let i = 0; i < length; i++) { 10        // 获取字符代码 11        const char = data.charCodeAt(i); 12        // 修改 hash 13        hash = ((hash << 5) - hash) + char; 14        // 转换为32位整数 15        hash &= hash; 16      } 17    }

Good:

1    function hashIt(data) { 2      let hash = 0; 3      const length = data.length; 4     5      for (let i = 0; i < length; i++) { 6        const char = data.charCodeAt(i); 7        hash = ((hash << 5) - hash) + char; 8     9        // 转换为32位整数 10        hash &= hash; 11      } 12    }

删掉注释的代码

git 存在的意义就是保存你的旧代码,所以注释的代码赶紧删掉吧。

Bad:

1    doStuff(); 2    // doOtherStuff(); 3    // doSomeMoreStuff(); 4    // doSoMuchStuff();

Good:

    doStuff();

javascript

不要记日记
记住你有 git!,git log 可以帮你干这事。

Bad:

1    /** 2     * 2016-12-20: 删除了 xxx 3     * 2016-10-01: 改进了 xxx 4     * 2016-02-03: 删除了第12行的类型检查 5     * 2015-03-14: 增加了一个合并的方法 6     */ 7    function combine(a, b) { 8      return a + b; 9    }

Good:

1    function combine(a, b) { 2      return a + b; 3    }

注释不需要高亮

注释高亮,并不能起到提示的作用,反而会干扰你阅读代码。

Bad:

1    //////////////////////////////////////////////////////////////////////////////// 2    // Scope Model Instantiation 3    //////////////////////////////////////////////////////////////////////////////// 4    $scope.model = { 5      menu: 'foo', 6      nav: 'bar' 7    }; 8     9    //////////////////////////////////////////////////////////////////////////////// 10    // Action setup 11    //////////////////////////////////////////////////////////////////////////////// 12    const actions = function() { 13      // ... 14    };

Good:

1    $scope.model = { 2      menu: 'foo', 3      nav: 'bar' 4    }; 5     6    const actions = function() { 7      // ... 8    };
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

Java修道之路,问鼎巅峰,我辈代码修仙法力齐天

<center<fontcolor00FF7Fsize5face"黑体"代码尽头谁为峰,一见秃头道成空。</font<center<fontcolor00FF00size5face"黑体"编程修真路破折,一步一劫渡飞升。</font众所周知,编程修真有八大境界:1.Javase练气筑基2.数据库结丹3.web前端元婴4.Jav

JavaScript 代码整洁之道 - HelloWorld