JavaScript 代码整洁之道

Stella981
• 阅读 612

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

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

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

  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:

      getUserInfo();
      getClientData();
      getCustomerRecord();

Good:

      getUser()

每个常量都该命名

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

Bad:

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

Good:

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

可描述

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

Bad:

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

Good:

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

直接了当

Bad:

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

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

#### 避免无意义的前缀 

如果创建了一个对象 car,就没有必要把它的颜色命名为 carColor。

Bad:
```js
      const car = {
        carMake: 'Honda',
        carModel: 'Accord',
        carColor: 'Blue'
      };
    
      function paintCar(car) {
        car.carColor = 'Red';
      }

Good:

    const car = {
      make: 'Honda',
      model: 'Accord',
      color: 'Blue'
    };
    
    function paintCar(car) {
      car.color = 'Red';
    }

使用默认值

Bad:

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

Good:

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

函数

参数越少越好

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

Bad:

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

Good:

    function createMenu({ title, body, buttonText, cancellable }) {
      // ...
    }
    
    createMenu({
      title: 'Foo',
      body: 'Bar',
      buttonText: 'Baz',
      cancellable: true
    });

只做一件事情

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

Bad:

    function emailClients(clients) {
      clients.forEach((client) => {
        const clientRecord = database.lookup(client);
        if (clientRecord.isActive()) {
          email(client);
        }
      });
    }

Good:

    function emailActiveClients(clients) {
      clients
        .filter(isActiveClient)
        .forEach(email);
    }
    function isActiveClient(client) {
      const clientRecord = database.lookup(client);    
      return clientRecord.isActive();
    }

顾名思义

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

Bad:

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

Good:

    function addMonthToDate(month, date) {
      // ...
    }
    
    const date = new Date();
    addMonthToDate(1, date);

只需要一层抽象层

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

Bad:

    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...
      });
    }

Good:

    function parseBetterJSAlternative(code) {
      const tokens = tokenize(code);
      const ast = lexer(tokens);
      ast.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 lexer(tokens) {
      const ast = [];
      tokens.forEach((token) => {
        ast.push( /* ... */ );
      });
    
      return ast;
    }

删除重复代码

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

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

Bad:

    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);
      });
    }

Good:

    function showEmployeeList(employees) {
      employees.forEach(employee => {
        const expectedSalary = employee.calculateExpectedSalary();
        const experience = employee.getExperience();
        const data = {
          expectedSalary,
          experience,
        };
    
        switch(employee.type) {
          case 'develop':
            data.githubLink = employee.getGithubLink();
            break
          case 'manager':
            data.portfolio = employee.getMBAProjects();
            break
        }
        render(data);
      })
    }

对象设置默认属性

Bad:

    const menuConfig = {
      title: null,
      body: 'Bar',
      buttonText: null,
      cancellable: true
    };
    
    function createMenu(config) {
      config.title = config.title || 'Foo';
      config.body = config.body || 'Bar';
      config.buttonText = config.buttonText || 'Baz';
      config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
    }
    
    createMenu(menuConfig);

Good:

    const menuConfig = {
      title: 'Order',
      // 'body' key 缺失
      buttonText: 'Send',
      cancellable: true
    };
    
    function createMenu(config) {
      config = Object.assign({
        title: 'Foo',
        body: 'Bar',
        buttonText: 'Baz',
        cancellable: true
      }, config);
    
      // config 就变成了: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
      // ...
    }
    
    createMenu(menuConfig);

不要传 flag 参数

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

Bad:

    function createFile(name, temp) {
      if (temp) {
        fs.create(`./temp/${name}`);
      } else {
        fs.create(name);
      }
    }

Good:

    function createFile(name) {
      fs.create(name);
    }
    function createFileTemplate(name) {
      createFile(`./temp/${name}`)
    }

避免副作用(第一部分)

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

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

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

Bad:

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

Good:

    var name = 'Ryan McDermott';
    var newName = splitIntoFirstAndLastName(name)
    
    function splitIntoFirstAndLastName(name) {
      return name.split(' ');
    }
    
    console.log(name); // 'Ryan McDermott';
    console.log(newName); // ['Ryan', 'McDermott'];

避免副作用(第二部分)

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

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

看起来没问题对不对?

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

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

Bad:

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

Good:

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

不要写全局方法

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

Bad:

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

Good:

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

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

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

Bad:

    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;
    }

Good:

    const programmerOutput = [
      {
        name: 'Uncle Bobby',
        linesOfCode: 500
      }, {
        name: 'Suzie Q',
        linesOfCode: 1500
      }, {
        name: 'Jimmy Gosling',
        linesOfCode: 150
      }, {
        name: 'Gracie Hopper',
        linesOfCode: 1000
      }
    ];
    let totalOutput = programmerOutput
      .map(output => output.linesOfCode)
      .reduce((totalLines, lines) => totalLines + lines, 0)

封装条件语句

Bad:

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

Good:

    function shouldShowSpinner(fsm, listNode) {
      return fsm.state === 'fetching' && isEmpty(listNode);
    }
    
    if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
      // ...
    }

尽量别用“非”条件句

Bad:

    function isDOMNodeNotPresent(node) {
      // ...
    }
    
    if (!isDOMNodeNotPresent(node)) {
      // ...
    }

Good:

    function isDOMNodePresent(node) {
      // ...
    }
    
    if (isDOMNodePresent(node)) {
      // ...
    }

避免使用条件语句

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

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

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

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

Bad:

    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();
        }
      }
    }

Good:

    class Airplane {
      // ...
    }
    // 波音777
    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 是无类型的,意味着你可以传任意类型参数,这种自由度很容易让人困扰,不自觉的就会去检查类型。仔细想想是你真的需要检查类型还是你的 API 设计有问题?

Bad:

    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'));
      }
    }

Good:

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

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

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

Bad:

    function combine(val1, val2) {
      if (typeof val1 === 'number' && typeof val2 === 'number' ||
          typeof val1 === 'string' && typeof val2 === 'string') {
        return val1 + val2;
      }
    
      throw new Error('Must be of type String or Number');
    }

Good:

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

不要过度优化

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

Bad:

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

Good:

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

删除弃用代码

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

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

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

Bad:

    function oldRequestModule(url) {
      // ...
    }
    
    function newRequestModule(url) {
      // ...
    }
    
    const req = newRequestModule;
    inventoryTracker('apples', req, 'www.inventory-awesome.io');

Good:

    function newRequestModule(url) {
      // ...
    }
    
    const req = newRequestModule;
    inventoryTracker('apples', req, 'www.inventory-awesome.io');

对象和数据结构

用 get、set 方法操作数据

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

Bad:

    function makeBankAccount() {
      // ...
    
      return {
        balance: 0,
        // ...
      };
    }
    
    const account = makeBankAccount();
    account.balance = 100;

Good:

    function makeBankAccount() {
      // 私有变量
      let balance = 0;
    
      function getBalance() {
        return balance;
      }
    
      function setBalance(amount) {
        // ... 在更新 balance 前,对 amount 进行校验
        balance = amount;
      }
    
      return {
        // ...
        getBalance,
        setBalance,
      };
    }
    
    const account = makeBankAccount();
    account.setBalance(100);

使用私有变量

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

Bad:

    const Employee = function(name) {
      this.name = name;
    };
    
    Employee.prototype.getName = function getName() {
      return this.name;
    };
    
    const employee = new Employee('John Doe');
    console.log(`Employee name: ${employee.getName()}`); 
    // Employee name: John Doe
    delete employee.name;
    console.log(`Employee name: ${employee.getName()}`);
     // Employee name: undefined

Good:

    function makeEmployee(name) {
      return {
        getName() {
          return name;
        },
      };
    }
    
    const employee = makeEmployee('John Doe');
    console.log(`Employee name: ${employee.getName()}`); 
    // Employee name: John Doe
    delete employee.name;
    console.log(`Employee name: ${employee.getName()}`); 
    // Employee name: John Doe

使用 class

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

Bad:

    // 动物
    const Animal = function(age) {
      if (!(this instanceof Animal)) {
        throw new Error('Instantiate Animal with `new`');
      }
    
      this.age = age;
    };
    
    Animal.prototype.move = function move() {};
    
    // 哺乳动物
    const Mammal = function(age, furColor) {
      if (!(this instanceof Mammal)) {
        throw new Error('Instantiate Mammal with `new`');
      }
    
      Animal.call(this, age);
      this.furColor = furColor;
    };
    
    Mammal.prototype = Object.create(Animal.prototype);
    Mammal.prototype.constructor = Mammal;
    Mammal.prototype.liveBirth = function liveBirth() {};
    
    // 人类
    const Human = function(age, furColor, languageSpoken) {
      if (!(this instanceof Human)) {
        throw new Error('Instantiate Human with `new`');
      }
    
      Mammal.call(this, age, furColor);
      this.languageSpoken = languageSpoken;
    };
    
    Human.prototype = Object.create(Mammal.prototype);
    Human.prototype.constructor = Human;
    Human.prototype.speak = function speak() {};

Good:

    // 动物
    class Animal {
      constructor(age) {
        this.age = age
      };
      move() {};
    }
    
    // 哺乳动物
    class Mammal extends Animal{
      constructor(age, furColor) {
        super(age);
        this.furColor = furColor;
      };
      liveBirth() {};
    }
    
    // 人类
    class Human extends Mammal{
      constructor(age, furColor, languageSpoken) {
        super(age, furColor);
        this.languageSpoken = languageSpoken;
      };
      speak() {};
    }

链式调用

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

Bad:

    class Car {
      constructor(make, model, color) {
        this.make = make;
        this.model = model;
        this.color = color;
      }
    
      setMake(make) {
        this.make = make;
      }
    
      setModel(model) {
        this.model = model;
      }
    
      setColor(color) {
        this.color = color;
      }
    
      save() {
        console.log(this.make, this.model, this.color);
      }
    }
    
    const car = new Car('Ford','F-150','red');
    car.setColor('pink');
    car.save();

Good:

    class Car {
      constructor(make, model, color) {
        this.make = make;
        this.model = model;
        this.color = color;
      }
    
      setMake(make) {
        this.make = make;
        return this;
      }
    
      setModel(model) {
        this.model = model;
        return this;
      }
    
      setColor(color) {
        this.color = color;
        return this;
      }
    
      save() {
        console.log(this.make, this.model, this.color);
        return this;
      }
    }
    
    const car = new Car('Ford','F-150','red')
      .setColor('pink');
      .save();

不要滥用继承

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

Bad:

    class Employee {
      constructor(name, email) {
        this.name = name;
        this.email = email;
      }
    
      // ...
    }
    
    // TaxData(税收信息)并不是属于 Employee(雇员),而是包含关系。
    class EmployeeTaxData extends Employee {
      constructor(ssn, salary) {
        super();
        this.ssn = ssn;
        this.salary = salary;
      }
    
      // ...
    }

Good:

    class EmployeeTaxData {
      constructor(ssn, salary) {
        this.ssn = ssn;
        this.salary = salary;
      }
    
      // ...
    }
    
    class Employee {
      constructor(name, email) {
        this.name = name;
        this.email = email;
      }
    
      setTaxData(ssn, salary) {
        this.taxData = new EmployeeTaxData(ssn, salary);
      }
      // ...
    }

SOLID

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

单一功能原则

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

Bad:

    class UserSettings {
      constructor(user) {
        this.user = user;
      }
    
      changeSettings(settings) {
        if (this.verifyCredentials()) {
          // ...
        }
      }
    
      verifyCredentials() {
        // ...
      }
    }

Good:

    class UserAuth {
      constructor(user) {
        this.user = user;
      }
      verifyCredentials() {
        // ...
      }
    }
    
    class UserSetting {
      constructor(user) {
        this.user = user;
        this.auth = new UserAuth(this.user);
      }
      changeSettings(settings) {
        if (this.auth.verifyCredentials()) {
          // ...
        }
      }
    }
    }

开闭原则

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

Bad:

    class AjaxAdapter extends Adapter {
      constructor() {
        super();
        this.name = 'ajaxAdapter';
      }
    }
    
    class NodeAdapter extends Adapter {
      constructor() {
        super();
        this.name = 'nodeAdapter';
      }
    }
    
    class HttpRequester {
      constructor(adapter) {
        this.adapter = adapter;
      }
    
      fetch(url) {
        if (this.adapter.name === 'ajaxAdapter') {
          return makeAjaxCall(url).then((response) => {
            // 传递 response 并 return
          });
        } else if (this.adapter.name === 'httpNodeAdapter') {
          return makeHttpCall(url).then((response) => {
            // 传递 response 并 return
          });
        }
      }
    }
    
    function makeAjaxCall(url) {
      // 处理 request 并 return promise
    }
    
    function makeHttpCall(url) {
      // 处理 request 并 return promise
    }

Good:

    class AjaxAdapter extends Adapter {
      constructor() {
        super();
        this.name = 'ajaxAdapter';
      }
    
      request(url) {
        // 处理 request 并 return promise
      }
    }
    
    class NodeAdapter extends Adapter {
      constructor() {
        super();
        this.name = 'nodeAdapter';
      }
    
      request(url) {
        // 处理 request 并 return promise
      }
    }
    
    class HttpRequester {
      constructor(adapter) {
        this.adapter = adapter;
      }
    
      fetch(url) {
        return this.adapter.request(url).then((response) => {
          // 传递 response 并 return
        });
      }
    }

里氏替换原则

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

Bad:

    // 长方形
    class Rectangle {
      constructor() {
        this.width = 0;
        this.height = 0;
      }
    
      setColor(color) {
        // ...
      }
    
      render(area) {
        // ...
      }
    
      setWidth(width) {
        this.width = width;
      }
    
      setHeight(height) {
        this.height = height;
      }
    
      getArea() {
        return this.width * this.height;
      }
    }
    
    // 正方形
    class Square extends Rectangle {
      setWidth(width) {
        this.width = width;
        this.height = width;
      }
    
      setHeight(height) {
        this.width = height;
        this.height = height;
      }
    }
    
    function renderLargeRectangles(rectangles) {
      rectangles.forEach((rectangle) => {
        rectangle.setWidth(4);
        rectangle.setHeight(5);
        const area = rectangle.getArea(); 
        rectangle.render(area);
      });
    }
    
    const rectangles = [new Rectangle(), new Rectangle(), new Square()];
    renderLargeRectangles(rectangles);

Good:

    class Shape {
      setColor(color) {
        // ...
      }
    
      render(area) {
        // ...
      }
    }
    
    class Rectangle extends Shape {
      constructor(width, height) {
        super();
        this.width = width;
        this.height = height;
      }
    
      getArea() {
        return this.width * this.height;
      }
    }
    
    class Square extends Shape {
      constructor(length) {
        super();
        this.length = length;
      }
    
      getArea() {
        return this.length * this.length;
      }
    }
    
    function renderLargeShapes(shapes) {
      shapes.forEach((shape) => {
        const area = shape.getArea();
        shape.render(area);
      });
    }
    
    const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
    renderLargeShapes(shapes);

接口隔离原则

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

Bad:

    class DOMTraverser {
      constructor(settings) {
        this.settings = settings;
        this.setup();
      }
    
      setup() {
        this.rootNode = this.settings.rootNode;
        this.animationModule.setup();
      }
    
      traverse() {
        // ...
      }
    }
    
    const $ = new DOMTraverser({
      rootNode: document.getElementsByTagName('body'),
      animationModule() {} // Most of the time, we won't need to animate when traversing.
      // ...
    });

Good:

    class DOMTraverser {
      constructor(settings) {
        this.settings = settings;
        this.options = settings.options;
        this.setup();
      }
    
      setup() {
        this.rootNode = this.settings.rootNode;
        this.setupOptions();
      }
    
      setupOptions() {
        if (this.options.animationModule) {
          // ...
        }
      }
    
      traverse() {
        // ...
      }
    }
    
    const $ = new DOMTraverser({
      rootNode: document.getElementsByTagName('body'),
      options: {
        animationModule() {}
      }
    });

依赖反转原则

说就两点:

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

Bad:

    // 库存查询
    class InventoryRequester {
      constructor() {
        this.REQ_METHODS = ['HTTP'];
      }
    
      requestItem(item) {
        // ...
      }
    }
    
    // 库存跟踪
    class InventoryTracker {
      constructor(items) {
        this.items = items;
    
        // 这里依赖一个特殊的请求类,其实我们只是需要一个请求方法。
        this.requester = new InventoryRequester();
      }
    
      requestItems() {
        this.items.forEach((item) => {
          this.requester.requestItem(item);
        });
      }
    }
    
    const inventoryTracker = new InventoryTracker(['apples', 'bananas']);
    inventoryTracker.requestItems();

Good:

    // 库存跟踪
    class InventoryTracker {
      constructor(items, requester) {
        this.items = items;
        this.requester = requester;
      }
    
      requestItems() {
        this.items.forEach((item) => {
          this.requester.requestItem(item);
        });
      }
    }
    
    // HTTP 请求
    class InventoryRequesterHTTP {
      constructor() {
        this.REQ_METHODS = ['HTTP'];
      }
    
      requestItem(item) {
        // ...
      }
    }
    
    // webSocket 请求
    class InventoryRequesterWS {
      constructor() {
        this.REQ_METHODS = ['WS'];
      }
    
      requestItem(item) {
        // ...
      }
    }
    
    // 通过依赖注入的方式将请求模块解耦,这样我们就可以很轻易的替换成 webSocket 请求。
    const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterHTTP());
    inventoryTracker.requestItems();

测试

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

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

单一化

Bad:

    import assert from 'assert';
    
    describe('MakeMomentJSGreatAgain', () => {
      it('handles date boundaries', () => {
        let date;
    
        date = new MakeMomentJSGreatAgain('1/1/2015');
        date.addDays(30);
        assert.equal('1/31/2015', date);
    
        date = new MakeMomentJSGreatAgain('2/1/2016');
        date.addDays(28);
        assert.equal('02/29/2016', date);
    
        date = new MakeMomentJSGreatAgain('2/1/2015');
        date.addDays(28);
        assert.equal('03/01/2015', date);
      });
    });

Good:

    import assert from 'assert';
    
    describe('MakeMomentJSGreatAgain', () => {
      it('handles 30-day months', () => {
        const date = new MakeMomentJSGreatAgain('1/1/2015');
        date.addDays(30);
        assert.equal('1/31/2015', date);
      });
    
      it('handles leap year', () => {
        const date = new MakeMomentJSGreatAgain('2/1/2016');
        date.addDays(28);
        assert.equal('02/29/2016', date);
      });
    
      it('handles non-leap year', () => {
        const date = new MakeMomentJSGreatAgain('2/1/2015');
        date.addDays(28);
        assert.equal('03/01/2015', date);
      });
    });

异步

不再使用回调

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

Bad:

    import { get } from 'request';
    import { writeFile } from 'fs';
    
    get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response) => {
      if (requestErr) {
        console.error(requestErr);
      } else {
        writeFile('article.html', response.body, (writeErr) => {
          if (writeErr) {
            console.error(writeErr);
          } else {
            console.log('File written');
          }
        });
      }
    });

Good:

    get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
      .then((response) => {
        return writeFile('article.html', response);
      })
      .then(() => {
        console.log('File written');
      })
      .catch((err) => {
        console.error(err);
      });

Async/Await 比起 Promises 更简洁

Bad:

    import { get } from 'request-promise';
    import { writeFile } from 'fs-promise';
    
    get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
      .then((response) => {
        return writeFile('article.html', response);
      })
      .then(() => {
        console.log('File written');
      })
      .catch((err) => {
        console.error(err);
      });

Good:

    import { get } from 'request-promise';
    import { writeFile } from 'fs-promise';
    
    async function getCleanCodeArticle() {
      try {
        const response = await get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');
        await writeFile('article.html', response);
        console.log('File written');
      } catch(err) {
        console.error(err);
      }
    }

错误处理

不要忽略抛异常

Bad:

    try {
      functionThatMightThrow();
    } catch (error) {
      console.log(error);
    }

Good:

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

不要忘了在 Promises 抛异常

Bad:

    getdata()
      .then((data) => {
        functionThatMightThrow(data);
      })
      .catch((error) => {
        console.log(error);
      });

Good:

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

代码风格

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

常量大写

Bad:

    const DAYS_IN_WEEK = 7;
    const daysInMonth = 30;
    
    const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
    const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];
    
    function eraseDatabase() {}
    function restore_database() {}
    
    class animal {}
    class Alpaca {}

Good:

    const DAYS_IN_WEEK = 7;
    const DAYS_IN_MONTH = 30;
    
    const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
    const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles'];
    
    function eraseDatabase() {}
    function restoreDatabase() {}
    
    class Animal {}
    class Alpaca {}

先声明后调用

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

Bad:

    class PerformanceReview {
      constructor(employee) {
        this.employee = employee;
      }
    
      lookupPeers() {
        return db.lookup(this.employee, 'peers');
      }
    
      lookupManager() {
        return db.lookup(this.employee, 'manager');
      }
    
      getPeerReviews() {
        const peers = this.lookupPeers();
        // ...
      }
    
      perfReview() {
        this.getPeerReviews();
        this.getManagerReview();
        this.getSelfReview();
      }
    
      getManagerReview() {
        const manager = this.lookupManager();
      }
    
      getSelfReview() {
        // ...
      }
    }
    
    const review = new PerformanceReview(employee);
    review.perfReview();

Good:

    class PerformanceReview {
      constructor(employee) {
        this.employee = employee;
      }
    
      perfReview() {
        this.getPeerReviews();
        this.getManagerReview();
        this.getSelfReview();
      }
    
      getPeerReviews() {
        const peers = this.lookupPeers();
        // ...
      }
    
      lookupPeers() {
        return db.lookup(this.employee, 'peers');
      }
    
      getManagerReview() {
        const manager = this.lookupManager();
      }
    
      lookupManager() {
        return db.lookup(this.employee, 'manager');
      }
    
      getSelfReview() {
        // ...
      }
    }
    
    const review = new PerformanceReview(employee);
    review.perfReview();

注释

只有业务逻辑需要注释

代码注释不是越多越好。

Bad:

    function hashIt(data) {
      // 这是初始值
      let hash = 0;
    
      // 数组的长度
      const length = data.length;
    
      // 循环数组
      for (let i = 0; i < length; i++) {
        // 获取字符代码
        const char = data.charCodeAt(i);
        // 修改 hash
        hash = ((hash << 5) - hash) + char;
        // 转换为32位整数
        hash &= hash;
      }
    }

Good:

    function hashIt(data) {
      let hash = 0;
      const length = data.length;
    
      for (let i = 0; i < length; i++) {
        const char = data.charCodeAt(i);
        hash = ((hash << 5) - hash) + char;
    
        // 转换为32位整数
        hash &= hash;
      }
    }

删掉注释的代码

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

Bad:

    doStuff();
    // doOtherStuff();
    // doSomeMoreStuff();
    // doSoMuchStuff();

Good:

    doStuff();

javascript

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

Bad:

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

Good:

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

注释不需要高亮

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

Bad:

    ////////////////////////////////////////////////////////////////////////////////
    // Scope Model Instantiation
    ////////////////////////////////////////////////////////////////////////////////
    $scope.model = {
      menu: 'foo',
      nav: 'bar'
    };
    
    ////////////////////////////////////////////////////////////////////////////////
    // Action setup
    ////////////////////////////////////////////////////////////////////////////////
    const actions = function() {
      // ...
    };

Good:

    $scope.model = {
      menu: 'foo',
      nav: 'bar'
    };
    
    const actions = function() {
      // ...
    };
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
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
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
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
Souleigh ✨ Souleigh ✨
3年前
前端性能优化 - 雅虎军规
无论是在工作中,还是在面试中,web前端性能的优化都是很重要的,那么我们进行优化需要从哪些方面入手呢?可以遵循雅虎的前端优化35条军规,这样对于优化有一个比较清晰的方向.35条军规1.尽量减少HTTP请求个数——须权衡2.使用CDN(内容分发网络)3.为文件头指定Expires或CacheControl,使内容具有缓存性。4.避免空的
Wesley13 Wesley13
3年前
5分钟带你领略写出优美代码的核心要素(内含 感恩节彩蛋)
【导语】马丁(RobertC.Martin)撰写的《代码整洁之道》(CleanCode)一书中提出了代码质量与其整洁度成正比,干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础,并由此揭示代码整洁之道。文中讲述了:当你在做CodeReview时,从审查者的言辞平静或是激烈的词语中,可以领略到你写的代码是BadCode还
Stella981 Stella981
3年前
Python之time模块的时间戳、时间字符串格式化与转换
Python处理时间和时间戳的内置模块就有time,和datetime两个,本文先说time模块。关于时间戳的几个概念时间戳,根据1970年1月1日00:00:00开始按秒计算的偏移量。时间元组(struct_time),包含9个元素。 time.struct_time(tm_y
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
3年前
JOptionPane修改图标
1.在Linux平台下.JOptionPane会显示Java默认的图标,在window平台不显示图标,如何替换这个图标了?2JOptionPane.setIcon(Icon)修改的是内容区域的icon,而不是左上角的Icon.所以需要通过修改Jdialog/Frame的图标来达到修改默认图标的问题.3.代码:if(JOptio