08月17, 2017

Javascript整洁之道

Javascript整洁之道

简介

变量

  1. 使用有意义的和可读的变量名

    Bad:

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

    Good:

    const currentDate = moment.format('YYYY/MM/DD');
    
  2. 对同样的变量类型使用相同的词汇

    Bad:

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

    Good:

    getUser();
    
  3. 使用可检索的变量名。 我们读的代码多于写的代码。所以代码的课可读性和可检索性非常重要。使用无意义的变量命名会为我们理解程序带来一定的困难。请确保你的变量名可搜索。类似于buddy.jsESLint可以检查这些没有命名的常量

    Bad:

    //864000000到底是什么意思
    setTimeout(blastOff, 86400000);
    

    Good:

    //将它声明为常量
    const MILLSECONDS_IN_A_DAY = 86400000;
    setTimeout(blastOff, MILLSECONDS_IN_A_DAY);
    
  4. 使用解释性的变量

    Bad:

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

    Good:

    const address = 'One Infinite Loop, Cupertino 95014';
    const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
    //利用数组的解构,返回的变量
    const [, city, zipCode] = address.match(cityZipCodeRegex) || [];
    saveCityZipCode(city, zipCode);
    
  5. 详诉好于概述

    Bad:

    const locations = ['Austin', 'New York', 'San Francisco'];
         locations.forEach((l) => {
         doStuff();
         doSomeOtherStuff();
         // ...
         // ...
         // ...
         // Wait, what is `l` for again?
         dispatch(l);
    });
    

    Good:

    const locations = ['Austin', 'New York', 'San Francisco'];
         locations.forEach((location) => {
         doStuff();
         doSomeOtherStuff();
         // ...
         // ...
         // ...
         dispatch(location);
    });
    
  6. 不要增加不必要的上下文

    如果你的类或者对象名有意义,不要再在变量名中重复

    Bad:

    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';
    }
    
  7. 使用默认参数代替短路判读或者条件判断

    默认的参数总是比短路赋值活着条件语句更加简洁。注意,你使用他们的时候,你的函数将会为未定义的参数提供一个默认值。其他的错误的值,类似于‘’、”“、false、null、0、and NAN将不会被默认值取代

    Bad

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

    Good:

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

函数

  1. 函数参数:(两个以下最理想)

     限制参数个数非常重要,因为这样会使得函数测试变得简单。拥有超过三个的参数会导致你的函数的测试用例爆炸式地增加(这么强=.=).
    

    没有参数最理想,一个或者两个参数也可以,三个就应该避免。超过三个就应该整合一下。通常, 如果你有超过两个以上的参数,你的函数就会尝试着做很多的事。大多数情况下,一个高阶的对象作为一个参数就满足了。

    因为在js中,什么都是对象。没有类的样板。如果你需要一大串参数,你可以使用对象。

    为了使函数期望的类型更加明显,你可以使用ES2015/ES6的解构语法,它有几点好处:

    • 当某人一旦看到这个函数声明,就会明白什么属性会被用到。

    • 解构同样克隆参数对象的一些基本类型的值给函数,这个可以排除一些副作用。注意:对象和数组从函数参数对象中不会被克隆

    • Linter也可以警告你什么属性没有被用到。哪个属性将不会被解构

      Bad:

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

      Good:

      function createMenu({ title, body, buttonText, cancellable }) {
        // ...
      }
      createMenu({
        title: 'Foo',
        body: 'Bar',
        buttonText: 'Baz',
        cancellable: true
      });
      
  2. 函数应该只做一件事

    这是软件工程最重要的事,当函数做的事超过了一件。它们会变得越来越难以维护、测试。当你的函数从只做一件事脱离出来,它们可以轻易地被重构,你的代码可读性也会变得很高。

    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();
    }
    
  3. 函数名应该说明他在做什么

    Bad:

    function addToDate(date, month) {
         // ...
    }
    const date = new Date();
     // 难以从函数名分辨出什么被添加了
    addToDate(date, 1);
    

    Good:

    function addMonthToDate(month, date) {
         // ...
    }
    const date = new Date();
    addMonthToDate(1, date);
    
  4. 函数应该只有一个级别的抽象

    当你的函数有多个级别的抽象时,通常做了太多的事,拆分函数可以提高重用性和测试性。

    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 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;
         }
    }
    function parseBetterJSAlternative(code) {
         const tokens = tokenize(code);
         const ast = lexer(tokens);
         ast.forEach((node) => {// parse...});
    }
    
  5. 去掉重复的代码

    尽量不要重复代码。重复的代码意味着当你修改一个逻辑时,需要更改多个逻辑。假设你经营着一家餐馆,你的库存有的番茄,洋葱,咖喱等等。当你的多张库存表单都有这些东西,当上了一道菜之后,所有的库存表单都需要更新。如果你只有一张表,你就只需要更新一张表。

    很多时候,重复的代码是因为你有两个或多个不同细微的东西。他们共享了大部分,但是它们迫使你分离两个或者更多独立的函数来处理相同的逻辑。删除一些重复的代码意味着创造出抽象的部分:函数/模块/类来处理不同的地方。

    让这个抽象的部分正确是至关重要的。这是为什么要你遵循 Classes 那一章的 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 'manager':
                 data.portfolio = employee.getMBAProjects();
             break;
             case 'developer':
                 data.githubLink = employee.getGithubLink();
             break    ;
         }
         render(data);
       });
    }
    
  6. 使用Object.assign设置默认的对象

    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',
         // 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);
    
  7. 不要将flag设置为函数的参数

    flag告诉了你的用户,这个函数做了不止一件事情,函数应该只做一件事。如果像下面的代码,路径决定于不同的布尔值,就将它们拆分出来。

    Bad:

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

    Good:

    function createFile(name) {
         fs.create(name);
    }
    function createTempFile(name) {
         createFile(`./temp/${name}`);
    }
    
  8. 避免副作用

    一个函数接收了一个参数返回了其他的值或数组会产生副作用。这个副作用有可能写入一个文件,或者改变全局变量,或者奇怪地将你口袋里的钱给一个陌生人。

    现在,你确实需要将这些副作用写进你的代码。像之前的例子,你可能会写一个文件。你需要做的集中化你要做的事情。不要将几个函数或类写一个特定的文件。一个服务就够了,有且只有一个。

    重点在于避免常见的陷阱,比如对象共享没有结构的状态,使用被随意赋值的无类型数据,没有使用集中化产生的副作用。

    Bad:

    // 全局的变量将被下面的函数引用
    // 如果另外一个函数使用这个名字,现在它会变成一个数组
    let name = 'Ryan McDermott';
    function splitIntoFirstAndLastName() {
        name = name.split(' ');
    }
    splitIntoFirstAndLastName();
    console.log(name); // ['Ryan', 'McDermott'];
    

    Good:

    function splitIntoFirstAndLastName(name) {
        return name.split(' ');
    }
    const name = 'Ryan McDermott';
    const newName = splitIntoFirstAndLastName(name);
    console.log(name); // 'Ryan McDermott';
    console.log(newName); // ['Ryan', 'McDermott'];
    
  9. 在JS中,基本类型的通过值传递。对象和数组通过引用传递。在这些对象和数组中,如果你改变购物车中的数组,通过购买一些商品,然后其他的函数可能使用被这些影响了的cat 数组。这可能很棒,但是考虑一下情况:

    • 用户点击了购买的按钮, 按钮需要调用一个购买的函数。这个函数可能会发送一个网络请求,然后将购物车的数组发送给服务器。在网络极差的情况下,这个购买函数会不断地发送请求。现在在网络请求真正发送之前,用户突然点击Add to Cart的按钮。这个购买函数将会发送意外的数据,因为它有对这个购物车数组的引用。

    • 一个好的解决思路是addItemToCart函数总是克隆这个cart,编辑它,然后返回克隆。这个保证了没有其他函数保持对这个购物车保持引用,排除了一些意外的影响。

      注意:

    • 这里存在确实想要改变输入框对象的情况,但是当你适应了这种编程方式你会发现这种方式弥足珍贵。为了没有副作用大多数的代码可以被重构。

    • 介于克隆一个大的对象在性能表现上代价非常昂贵。幸运的是在实践中,这不是一个很大的问题。因为有大量的允许这种编程方式又快又好。不需要你去手动克隆对象和数组。

  10. 不要写入全局的函数

    污染全局是非常不好的实践,污染全局变量在JavaScript中是一个不好的做法,因为可能会与另一个库冲突,并且使用你的API用户在生产环境中遇到异常之前不会更明智。让我们来想一个例子:如果你想扩展JavaScript的原生的Array,让两个数组显示出不同的diff函数。你可以在Array.prototype上进行扩展,但是可能会与尝试执行相同操作的另一个库冲突。如果其他库只是使用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));
       }
    }
    
  11. 函数式编程优于指令式编程

    JavaScript不是像Haskell的函数式编程语言。但是它却有函数式的风格,函数式语言更加简洁和容易测试的。当你可以函数式编程时尽情使用这种方式。

    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
       }
    ];
    const INITIAL_VALUE = 0;
    const totalOutput = programmerOutput
       .map((programmer) => programmer.linesOfCode)
       .reduce((acc, linesOfCode) => acc + linesOfCode, INITIAL_VALUE);
    
  12. 封装条件语句

    Bad:

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

    Good:

    function shouldShowSpinner(fsm, listNode) {
       return fsm.state === 'fetching' && isEmpty(listNode);
    }
    if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
       // ...
    }
    
  13. 避免否定条件语句

    Bad:

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

    Good:

    function isDOMNodePresent(node) {
       // ...
    }
    if (isDOMNodePresent(node)) {
        // ...
    }
    
  14. 避免多条件

    这个看起来像是不可完成的任务。当第一次听到这个的时候,人们都会说:"没有if 我能够做什么? "这个回答是:你可以在不同的情况下使用多态完成相同的任务。第二个问题通常是:" 听起来不错,可是为什么我被希望要求做这个?"答案是我们之前学到过的概念:函数只应该做一件事。当你的classfunction有了条件判断,你在告诉你的用户,你的函数在做不止一件事。记住,函数只做一件事。

    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 {
        // ...
    }
    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();
       }
    }
    
  15. 避免类型检测

    • 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。他是JavaScript非常卓越的替代品。它提供了在JS语法基础上的静态类型检测(FaceBook不是有flow吗...)。手动检查普通类型的JavaScript,需要额外多的代码,这种人造的类型安全使代码丧失了可读性。保持代码整洁,书写好的测试用例,良好的代码审阅。否则,用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;
    }
    
  16. 不要过度优化

    现代浏览器在底层运行时做了很多的优化。很多时候你在优化代码只是在浪费时间。这里有很多好的资源来查看哪些需要的优化的,直到它们被修正。

    Bad:

    //在旧的浏览器上, 每次循环 `list.length` 都没有被缓存, 会导致不必要的开销, 因为要重新计
    // 算 `list.length` 。 在现代化浏览器上, 这个已经被优化了。
    for (let i = 0, len = list.length; i < len; i++) {
        // ...
    }
    

    Good:

    for (let i = 0; i < list.length; i++) {
        // ...
    }
    
  17. 移除僵尸代码

    无用的代码和重复的代码一样不好,你的代码库没有理由保留它。如果没有被调用过,清除它。它会保存在历史版本记录中。

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

    对象和数据结构

  1. 使用getter和setter

    在对象上使用getter和setter来获取数据比单纯地在一个对象上获取属性好。你可能会问为什么,这里有几个不被注意的原因。

    • 当你不仅仅只想获取对象属性的时候,你不得不在代码库查找每一个获取对象属性的代码。
    • 当使用set的时候,添加验证变得容易。
    • 封装内部。
    • 当使用setting和getting的时候更容易增加log和错误处理。
    • 你可以懒加载对象属性,我们是说doing服务器获取它。

    Bad:

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

    Good:

    function makeBankAccount() {
         //这是一个私有的属性
        let balance = 0;
         // a "getter",通过下面的返回对象使之成为公有。
        function getBalance() {
            return balance;
        }
         // a "setter", 通过下面的返回对象使之成为公有。
        function setBalance(amount) {
            // ... validate before updating the balance
            balance = amount;
        }
        return {
            // ...
            getBalance,
            setBalance,
        };
    }
    const account = makeBankAccount();
    account.setBalance(100);
    
  2. 让对象拥有私有成员

    通过闭包完成(针对ES5)

    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

  1. 用ES5/ES6的类优于ES5普通的表达式。

    对于ES5的类来说,继承,构造函数和方法的定义非常困难。如果你需要继承(注意你也可能不需要)ES6的class是更好的选择。无论怎么样,小的构造函数优于class,除非你发现需要较大的或更复杂的对象。

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

    Bad:

    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() { /* ... */ }
    }
    
  2. 使用方法链

    这个方法在JS非常常见,你可以在很多库中看到这种写法,比如JQ和Lodash。它可以让你的代码更具有表达性和更少的冗余。基于这个原因,使用方法链然后去看看你的代码会有多整洁。在你的class函数中,简单地在每个函数的结尾return 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;
             // NOTE: Returning this for chaining
             return this;
         }
         setModel(model) {
             this.model = model;
             // NOTE: Returning this for chaining
             return this;
         }
         setColor(color) {
             this.color = color;
             // NOTE: Returning this for chaining
             return this;
         }
         save() {
             console.log(this.make, this.model, this.color);
             // NOTE: Returning this for chaining
             return this;
         }
    }
    const car = new Car('Ford','F-150','red')
    .setColor('pink')
    .save();
    
  3. 组合优于继承

    如“四人帮”的设计模式所述,组合是优于继承的。有很多很好的理由使用继承也有很多好的理由来使用组合。这个要点是,如果你的思想本能地想使用继承,试着想想组合是否能够更好地建模。

    你可能会想知道,“我什么时候应该继承”?这取决于你手头上的问题,这有个一个恰当的列表说明继承比组合更有意义。

    • 你的继承代表着是一个的关系,而不是有一个的关系。(人类 -> 人 vs. User->UserDetail)
    • 你可以从基类复用你的代码。(人类可以像所有的动物一样行动)
    • 通过改变一个基类,全局继承下来的类都可以得到改变。(所有的动物在行动的时候,他们的热量消耗都会改变)

    Bad:

    class Employee {
         constructor(name, email) {
             this.name = name;
             this.email = email;
         }
         // ...
    }
    //因为你的雇员有一个税收的数据. EmployeeTaxData 并不是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);
         }
         // ...
    }
    

健壮

  1. 单一原则

    正如整洁准则所诉,永远不要有超过一个理由来修改一个类。令人兴奋的是,有很多方式来打包你的类。正如你可以在飞机上带上你的行李箱。关键点在于你的类不应该有概念上的耦合,它会给你很多理由去改变。最大化地减少改变类的次数是非常重要的。如果一个类中的方法太多,当你修改一个的时候,这个修改到底影响了多少其他的代码会变的难以理解。

    Bad:

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

    Good:

    class UserAuth {
         constructor(user) {
             this.user = user;
         }
         verifyCredentials() {
             // ...
         }
    }
    class UserSettings {
         constructor(user) {
             this.user = user;
             this.auth = new UserAuth(user);
    }
         changeSettings(settings) {
             if (this.auth.verifyCredentials()) {
             // ...
             }
         }
    }
    
  2. 开放/封闭原则

    Bertrand Meyer所说,软件实体(类、模块、函数等)应该有扩展的开放性,对修改也有封闭性。这意味着什么?这个原则最基本地要求你允许你的用户增加新的功能而是改变现有的代码。

    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) => {
                 // transform response and return
             });} else if (this.adapter.name === 'httpNodeAdapter') {
                 return makeHttpCall(url).then((response) => {
                  // transform response and return
                 });
             }
         }
    }
    function makeAjaxCall(url) {
         // request and return promise
    }
    function makeHttpCall(url) {
         // request and return promise
    }
    

    Bad:

    class AjaxAdapter extends Adapter {
         constructor() {
             super();
             this.name = 'ajaxAdapter';
         }
         request(url) {
             // request and return promise
             }
    }
    class NodeAdapter extends Adapter {
         constructor() {
             super();
             this.name = 'nodeAdapter';
         }
         request(url) {
             // request and return promise
         }
    }
    class HttpRequester {
         constructor(adapter) {
             this.adapter = adapter;
         }
         fetch(url) {
             return this.adapter.request(url).then((response) => {
             // transform response and return});
         }
    }
    
  3. 里氏替换原则

    这是一个小而精的原则。‘如果S是T的子类,那么T的对象可以被S取代(S的对象可能代替T的对象)而不是警告程序任何可取的属性(正确性、表现)’。这是一个难以解释的定义。

    简单点:如果你有一个父类和一个子类,然后基类和子类都可以相互交换而不使用不正确的数据。这可能会困惑,让我们来看看类Square-Rectangle的例子。数学上,一个正方形是一个长方形,但是如果你用是一个的关系来代替继承,你可能陷入麻烦。

    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(); // BAD: Returns 25 for Square. Should be 20.
             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);
    
  4. 接口隔离原则

    Javascript没有接口,所以这个原则并不想其他原则那么严格适用。对于缺少类型的语言来说,这依旧非常重要。 接口隔离原则是说:‘客户端不应该被强制使用他们不需要的接口’,因为鸭子类型接口在JS中是隐式的契约。 一个好的例子:一个类需要一个较大的对象来设置,不用客户端去设置一个较大对象来设置是非常有益的。因为大多数的时候,他们不需要设置全部的选项。让它们成为一个可选项避免成为一个胖接口 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() {}
         }
    });
    
  5. 依赖反转原则
    这个原则陈述了两件非常重要的事:

    • 高阶的模块不应该依赖低阶的模块,两者都应该依赖于抽象
    • 抽象不应该依赖具体,具体应该依赖抽象。

    这个一开始可能难以理解,但是用过AngularJS,你就会看见这个原则是以依赖注入的形式。然而它们不是相同的概念,依赖反转是为了防止高级模块知道低级模块的细节和配置他们。它可以通过依赖注入完成。一个大的好处是减少模块之间的耦合。耦合是一个糟糕的开发模式,它让代码难以重构。

    Bad:

    class InventoryRequester {
         constructor() {
             this.REQ_METHODS = ['HTTP'];
         }
         requestItem(item) {
             // ...
         }
    }
    class InventoryTracker {
         constructor(items) {
             this.items = items;
             // BAD: We have created a dependency on a specific request implementation.
             // We should just have requestItems depend on a request method: `request`
             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);
         });
     }
    }
    class InventoryRequesterV1 {
         constructor() {
             this.REQ_METHODS = ['HTTP'];
         }
         requestItem(item) {
             // ...
         }
    }
    class InventoryRequesterV2 {
         constructor() {
             this.REQ_METHODS = ['WS'];
         }
         requestItem(item) {
             // ...
         }
    }
    //通过构造我们的外部依赖性并注入他们,可以轻松地使用一个新的`WebSocket`请求方式
    const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
    inventoryTracker.requestItems();
    

测试

测试比发布(shipping)更加重要,如果你没有测试或者测试不充分,那么每次发布代码的时候你就不能保证代码没有破坏任何事,测试的数量取决于你的团队,但是有100%的覆盖率(所有的代码语句和分支)可以提高自信和开发者内心的平静。这意味着,除了测试框架之外你还需要好的[覆盖率测试工具](http://gotwarlost.github.io/istanbul/)    

没有不写测试的理由,你的团队可以从大量的测试框架中选择一个。当你发现一个适用于你的团队,那么就用它为每一新的feature或者模块写测试。入股你偏好测试驱动开发,那非常好,这时主要的点在于上线之前,或者重构代码之前,确保你的测试覆盖率达到目标。

  1. 一个测试一个概念

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

并发

  1. 使用Promise而不是回调

    回调并不干净,他们可能造成大量的回调金字塔。在ES6中,Promise是一个全局的变量,使用他们。

    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:

    import { get } from 'request';
    import { writeFile } from 'fs';
    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);
    });
    
  2. Async/Await比Promise更干净

    Promise是回调非常干净的替代。但是ES7带来了更加干净的Async/Await。

    Bad:

    import { get } from 'request';
    import { writeFile } from 'fs';
    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);
         }
    }
    

错误处理

抛出错误是非常好的事,他们意味值运行时已经非常成功了并且让你知道通过停止在栈上正在运行的函数。杀死进程然后在控制台中用一个堆栈跟踪提示你。

  1. 不要忽略捕获到的错误 对捕获到的异常不进行处理不给你修复错误或响应错误的能力。console.log(err)并不见得好,因为有可能丢失在海量的console中。如果你想用try.catch来包装他们,那意味着这个错误可能出现在这里因此你有备无患,或者当错误出现时有一个代码的路径。

    Bad:

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

    Good:

    try {
         functionThatMightThrow();
    } catch (error) {
         // One option (more noisy than console.log):
         console.error(error);
         // Another option:
         notifyUserOfError(error);
         // Another option:
         reportErrorToService(error);
         // OR do all three!
    }
    
  2. 不要rejected的Promise状态

    Bad:

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

    Good:

    getdata()
    .then((data) => {
         functionThatMightThrow(data);
    })
    .catch((error) => {
         // One option (more noisy than console.log):
         console.error(error);
         // Another option:
         notifyUserOfError(error);
         // Another option:
         reportErrorToService(error);
         // OR do all three!
    });
    

格式化

格式化代码是非常主观的。像这里的其他规则一样,这没有硬性和快速的规则你必须去遵循。不要因为格式去争论。这里有很多工具去帮助格式化。使用一个即可。因为格式去争吵对于工程师来说非常地浪费时间和金钱。

对于不属于自动格式化规则(缩进,制表符与空格,双重和单引号等)的可以看下面的指南:

  1. 常量大写 JavaScript是无类型的语言,所以字母大写可以告诉很多关于你的变量、函数等等。这里有很多主观的规则,你的team可以选择任何他们想要的,关键点在于无论你选择什么,保持一致。

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

    Bad:

    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 {}
    
  2. 函数声明和函数调用应该离得近一点

    如果一个函数在调用另外一个函数,让这些函数在文件的位置中保持水平。理想情况下,声明在调用之上。

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

注释

  1. 只对业务代码逻辑复杂的地方添加注释。

    注释只是代码的辩解,不是必须。好的代码本身就是 文档。

    Bad:

    function hashIt(data) {
     // The hash
     let hash = 0;
     // Length of string
     const length = data.length;
     // Loop through every character in data
     for (let i = 0; i < length; i++) {
          // Get character code.
         const char = data.charCodeAt(i);
         // Make the hash
         hash = ((hash << 5) - hash) + char;
         // Convert to 32-bit integer
         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;
         // Convert to 32-bit integer
         hash &= hash;
       }
    }
    
  2. 不要让注释掉的代码遗留在代码库中

    版本控制是有原因的,在你的历史版本中保留他们。

    Bad:

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

    Good:

    doStuff();
    
  3. 不要有日志式的评论

    记住版本控制的存在,这里没有必要遗留毫无作用的代码、释的代码、特别是日志式的注释。使用git log来获取历史记录。

    Bad:

    /**
    * 2016-12-20: Removed monads, didn't understand them (RM)
    * 2016-10-01: Improved using special monads (JP)
    * 2016-02-03: Removed type-checking (LI)
    * 2015-03-14: Added combine with type-checking (JR)
    */
    function combine(a, b) {
    return a + b;
    }
    

    Good:

    function combine(a, b) {
    return a + b;
    }
    
  4. 避免占位符

    它们仅仅添加了干扰。 让函数和变量名称与合适的缩进和格式化为你的代码提供视觉结构。

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

本文链接:http://inkzhou.com/post/js-clean-code.html

-- EOF --

Comments