简述 js 的代码整洁之道

lxf2023-12-15 14:30:01

前言

为什么代码要整洁?

代码质量与整洁度成正比。有的团队在赶工期的时候,不注重代码的整洁,代码写的越来越糟糕,项目越来越混乱,生产力也跟着下降,那就必须找更多人来提高生产力,开发成本越来越高。

整洁的代码是怎样的?

清晰表达意图、消除重复、简单抽象、能通过测试。 换句话说:具有可读性、可重用性和可重构性。

命名

  1. 名副其实:不使用缩写、不使用让人误解的名称,不要让我自己推测。

    // bad: 啥?
    const yyyymmdstr = moment().format("YYYY/MM/DD");
    // bad: 缩写
    const cD = moment().format("YYYY/MM/DD");
    
    // good:
    const currentDate = moment().format("YYYY/MM/DD");
    
    const locations = ["Austin", "New York", "San Francisco"];
    
    // bad:推测l是locations的项
    locations.forEach(l => doSomeThing(l));
    
    // good
    locations.forEach(location => doSomeThing(location));
    
  2. 使用方便搜索的名称:避免硬编码,对数据用常量const记录。

    // bad: 86400000指的是?
    setTimeout(goToWork, 86400000);
    
    // good: 86400000是一天的毫秒数
    const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000;
    setTimeout(goToWork, MILLISECONDS_PER_DAY);
    
  3. 类名应该是名词,方法名应该是动词。

    // bad
    function isVisble() {}
    
    // good
    function getIsVisble() {}
    
  4. 多个变量属于同一类型的属性,那就他们整合成一个对象。同时省略多余的上下文。

    // bad:可以整合
    const carMake = "Honda",
    const carModel = "Accord",
    const carColor = "Blue",
    
    // bad: 多余上下文
    const Car = {
      carMake: "Honda",
      carModel: "Accord",
      carColor: "Blue",
    };
    
    // good
    const Car = {
      make: "Honda",
      model: "Accord",
      color: "Blue",
    };
    

其他:

  • 不要写多余的废话,比如theMessagethe可以删除。

  • 统一术语。比如通知一词,不要一会在叫notice,一会叫announce

  • 用读得通顺的词语。比如getElementById就比 useIdToGetElement好读。

函数(方法)

  • 删除重复的代码,don't repeat yourself。很多地方可以注意dry,比如偷懒复制了某段代码、try...catch或条件语句写了重复的逻辑。

     // bad
     try {
         doSomeThing();
         clearStack();
     } catch (e) {
         handleError(e);
         clearStack();
     }
     // good
     try {
         doSomeThing();
     } catch (e) {
         handleError(e);
     } finally {
         clearStack();
     }
    
  • 形参不超过三个,对测试函数也方便。多了就使用对象参数。

    • 同时建议使用对象解构语法,有几个好处:

      1. 能清楚看到函数签名有哪些熟悉,
      2. 可以直接重新命名,
      3. 解构自带克隆,防止副作用,
      4. Linter检查到函数未使用的属性。
     // bad
     function createMenu(title, body, buttonText, cancellable) {}
    
     // good
     function createMenu({ title, body, buttonText, cancellable }) {}
    
  • 函数只做一件事,代码读起来更清晰,函数就能更好地组合、测试、重构。

     // bad: 处理了输入框的change事件,并创建文件的切片,并保存相关信息到localStorage
     function handleInputChange(e) {
         const file = e.target.files[0];
         // --- 切片 ---
         const chunkList = [];
         let cur = 0;
         while (cur < file.size) {
             chunkList.push({
               chunk: file.slice(cur, cur + size)
             });
             cur += size;
         }
         // --- 保存信息到localstorage ---
         localStorage.setItem("file", file.name);
         localStorage.setItem("chunkListLength", chunkList.length);
     }
    
     // good: 将三件事分开写,同时自顶而下读,很舒适
     function handleInputChange(e) {
         const file = e.target.files[0];
         const chunkList = createChunk(file);
         saveFileInfoInLocalStorage(file, chunkList);
     }
     function createChunk(file, size = SLICE_SIZE) {
         const chunkList = [];
         let cur = 0;
         while (cur < file.size) {
             chunkList.push({
               chunk: file.slice(cur, cur + size)
             });
             cur += size;
         }
         return chunkList
     }
     function saveFileInfoInLocalStorage(file, chunkList) {
         localStorage.setItem("file", file.name);
         localStorage.setItem("chunkListLength", chunkList.length);
     }
    
  • 自顶向下地书写函数,人们都是习惯自顶向下读代码,如,为了执行A,需要执行B,为了执行B,需要执行C。如果把A、B、C混在一个函数就很难读了。(看前一个的例子)。

  • 不使用布尔值来作为参数,遇到这种情况时,一定可以拆分函数。

     // 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}`);
     }
    
  • 避免副作用。

    • 副作用的缺点:出现不可预期的异常,比如用户对购物车下单后,网络差而不断重试请求,这时如果添加新商品到购物车,就会导致新增的商品也会到下单的请求中。

    • 集中副作用:遇到不可避免的副作用时候,比如读写文件、上报日志,那就在一个地方集中处理副作用,不要在多个函数和类处理副作用。

    • 其它注意的地方:

      • 常见就是陷阱就是对象之间共享了状态,使用了可变的数据类型,比如对象和数组。对于可变的数据类型,使用immutable等库来高效克隆。
      • 避免用可变的全局变量。
    // bad:注意到cart是引用类型!
    const addItemToCart = (cart, item) => {
      cart.push({ item, date: Date.now() });
    };
    
    // good
    const addItemToCart = (cart, item) => {
      return [...cart, { item, date: Date.now() }];
    };
    
  • 封装复杂的判断条件,提高可读性。

     // bad
     if (!(obj => obj != null && typeof obj[Symbol.iterator] === 'function')) {
         throw new Error('params is not iterable')
     }
    
     // good
     const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
     if (!isIterable(promises)) {
         throw new Error('params is not iterable')
     }
    
  • 在方法中有多条件判断时候,为了提高函数的可扩展性,考虑下是不是可以使用能否使用多态性来解决。

     // 地图接口可能来自百度,也可能来自谷歌
     var googleMap = {
         show: function (size) {
             console.log('开始渲染谷歌地图', size));
         }
     };
     var baiduMap = {
         render: function (size) {
             console.log('开始渲染百度地图', size));
         }
     };
    
     // bad: 出现多个条件分支。如果要加一个腾讯地图,就又要改动renderMap函数。
     function renderMap(type) {
         const size = getSize();
         if (type === 'google') {
             googleMap.show(size);
         } else if (type === 'baidu') {
             baiduMap.render(size);
         }
     };
     renderMap('google')
    
     // good:实现多态处理。如果要加一个腾讯地图,不需要改动renderMap函数。
     // 细节:函数作为一等对象的语言中,作为参数传递也会返回不同的执行结果,也是“多态性”的体现。
     function renderMap (renderMapFromApi) {
         const size = getSize();
         renderMapFromApi(size);
     }
     renderMap((size) => googleMap.show(size));
    

其他

  • 如果用了TS,没必要做多余类型判断。

注释

  1. 一般代码要能清晰的表达意图,只有遇到复杂的逻辑时才注释。

     // good:由于函数名已经解释不清楚函数的用途了,所以注释里说明。
     // 在nums数组中找出 和为目标值 target 的两个整数,并返回它们的数组下标。
     var twoSum = function(nums, target) {
         let map = new Map()
         for (let i = 0; i < nums.length; i++) {
             const item = nums[i];
             const index = map.get(target - item)
             if (index !== undefined){
                 return [index, i]
             }
             map.set(item, i)
         }
         return []
     };
    
     // bad:加了一堆废话
     var twoSum = function(nums, target) {
         // 声明map变量
         let map = new Map()
         // 遍历
         for (let i = 0; i < nums.length; i++) {
             const item = nums[i];
             const index = map.get(target - item)
             // 如果下标为空
             if (index !== undefined){
                 return [index, i]
             }
             map.set(item, i)
         }
         return []
     };
    
  2. 警示作用,解释此处不能修改的原因。

    // hack: 由于XXX历史原因,只能调度一下。
    setTimeout(doSomething, 0)
    
  3. TODO注释,记录下应该做但还没做的工作。另一个好处,提前写好命名,可以帮助后来者统一命名风格。

    class Comment {
        // todo: 删除功能后期实现
        delete() {}
    }
    
  4. 没用的代码直接删除,不要注释,反正git提交历史记录可以找回。

    // bad: 如下,重写了一遍两数之和的实现方式
    
    // var twoSum = function(nums, target) {
    //     for(var i = 0;i<nums.length;i++){
    //         for(var j = i+1;j<nums.length;j++){
    //             if (nums[i] + nums[j] === target) {
    //                 return [i,j]
    //             }
    //         }
    //     }
    // };
    var twoSum = function(nums, target) {
        let map = new Map()
        for (let i = 0; i < nums.length; i++) {
            const item = nums[i];
            const index = map.get(target - item)
            if (index !== undefined){
                return [index, i]
            }
            map.set(item, i)
        }
        return []
    };
    
  5. 避免循规式注释,不要求每个函数都要求javadoc,javadoc一般是用在公共代码上。

    // bad or good?
    /**
     * @param {number[]} nums
     * @param {number} target
     * @return {number[]}
     */
    var twoSum = function(nums, target) {}
    

对象

  • 多使用getter和setter(getXXX和setXXX)。好处:

    • 在set时方便验证。
    • 可以添加埋点,和错误处理。
    • 可以延时加载对象的属性。
    // good
    function makeBankAccount() {
      let balance = 0;
    
      function getBalance() {
        return balance;
      }
    
      function setBalance(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");
    delete employee.name;
    console.log(employee.getName()); // undefined
    
    
    // good
    function makeEmployee(name) {
      return {
        getName() {
          return name;
        }
      };
    }
    

solid

  • 单一职责原则 (SRP) - 保证“每次改动只有一个修改理由”。因为如果一个类中有太多功能并且您修改了其中的一部分,则很难预期改动对其他功能的影响。

    // 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()) {
          // ...
        }
      }
    }
    
  • 开闭原则 (OCP) - 对扩展放开,但是对修改关闭。在不更改现有代码的情况下添加新功能。比如一个方法因为有switch的语句,每次出现新增条件时就要修改原来的方法。这时候不如换成多态的特性。

    // bad: 注意到fetch用条件语句了,不利于扩展
    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 === "nodeAdapter") {
          return makeHttpCall(url).then(response => {
            // transform response and return
          });
        }
      }
    }
    
    function makeAjaxCall(url) {
      // request and return promise
    }
    
    function makeHttpCall(url) {
      // request and return promise
    }
    
    // good
    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
        });
      }
    }
    
  • 里氏替换原则 (LSP)

    • 两个定义

      • 如果S是T的子类,则T的对象可以替换为S的对象,而不会破坏程序。
      • 所有引用其父类对象方法的地方,都可以透明的替换为其子类对象。
      •     也就是,保证任何父类对象出现的地方,用其子类的对象来替换,不会出错。下面的例子是经典的正方形、长方形例子。
    // 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: 返回了25,其实应该是20
        rectangle.render(area);
      });
    }
    
    const rectangles = [new Rectangle(), new Rectangle(), new Square()];// 这里替换了
    renderLargeRectangles(rectangles);
    
    // good: 取消正方形和长方形继承关系,都继承Shape
    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);
    
  • 接口隔离原则 (ISP) - 定义是"客户不应被迫使用对其而言无用的方法或功能"。常见的就是让一些参数变成可选的。

     // bad
     class Dog {
         constructor(options) {
             this.options = options;
         }
    
         run() {
             this.options.run(); // 必须传入 run 方法,不然报错
         }
     }
    
     const dog = new Dog({}); // Uncaught TypeError: this.options.run is not a function
      
     dog.run()
    
     // good
     class Dog {
         constructor(options) {
             this.options = options;
         }
    
         run() {
             if (this.options.run) {
                 this.options.run();
                 return;
             }
             console.log('跑步');
         }
     }
    
  • 依赖倒置原则(DIP) - 程序要依赖于抽象接口(可以理解为入参),不要依赖于具体实现。这样可以减少耦合度。

     // bad
     class OldReporter {
       report(info) {
         // ...
       }
     }
    
     class Message {
       constructor(options) {
         // ...
         // BAD: 这里依赖了一个实例,那你以后要换一个,就麻烦了
         this.reporter = new OldReporter();
       }
    
       share() {
         this.reporter.report('start share');
         // ...
       }
     }
    
     // good
     class Message {
       constructor(options) {
         // reporter 作为选项,可以随意换了
         this.reporter = this.options.reporter;
       }
    
       share() {
         this.reporter.report('start share');
         // ...
       }
     }
     class NewReporter {
       report(info) {
         // ...
       }
     }
     new Message({ reporter: new NewReporter });
    

其他

  • 优先使用 ES2015/ES6 类而不是 ES5 普通函数。

  • 多使用方法链。

  • 多使用组合而不是继承。

错误处理

  • 不要忽略捕获的错误。而要充分对错误做出反应,比如console.error()到控制台,提交错误日志,提醒用户等操作。

  • 不要漏了catch promise中的reject。

格式

可以使用eslint工具,这里就不展开说了。

最后

接受第一次愚弄

让程序一开始就做到整洁,并不是一件很容易的事情。不要强迫症一样地反复更改代码,因为工期有限,没那么多时间。等到下次需求更迭,你发现到代码存在的问题时,再改也不迟。

参考:

  1. 《代码整洁之道》
  2. github.com/ryanmcdermo… (里面有很多例子。有汉化但没更新)
本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!