编写可维护的 JavaScript 代码 -- 编程风格,编程实践 读书笔记

809 阅读9分钟

编程风格

基本格式化

  • 4空格缩进
  • 不省略分号(在原生及使用工具函数的情况不建议省略,在使用比较完善的框架如vue或者自己配置好 webpack 时可以省略)
  • 1行代码长度不超过80个字符(个人比较推荐,毕竟编辑器的自动换行有时真的很难受)
  • 运算符后换行,换行后增加2个缩进单位(变量赋值换行,变量保持和上一行等号右侧第一个变量对齐)
  • 空行,流控制语句之前,方法之间,变量和第一条语句之间,注释之前,
  • 变量和函数命名方式,小驼峰方式
  • 变量命名名词开头,函数命名动词开头,命名长度尽可能短,尽量体现出值得数据类型
    • can 函数返回布尔值
    • has 函数返回布尔值
    • is 函数返回布尔值
    • get 函数返回非布尔值
    • set 函数用来保存值
  • 常量使用大写字母和下划线来命名,下划线分割单词 (URL MAX_NUMBER)
  • 构造函数(class) 大驼峰命名方式
  • 字符串,统一用单引号,多行字符或者需要携带参数使用反引号
  • 数字,不要省略浮点数小数点前后的部分
  • null 当做对象占位符
  • 避免使用 undefined
  • 创建对象时使用对象直接量: {}
  • 创建数组时使用数组直接量: []

注释

  • 单行注释
    • 独占一行,注释前总是有一个空行,缩进与下一行代码保持一致
    • 代码行尾部,代码结束到注释之间有一个空格,代码加上注释不应超过单行最大字符数限制,超过了就应放到当前代码行的上方
    • 单行注释不应以连续多行注释的形式出现
  • 多行注释
    • 最少三行第一行 /* , 第二行 * 与上一行 * 对齐,最后一行 */ (单行多行注释区分开比较好,多行注释风格很多,这里只是一种,找到适合的就好)
      
      /*
       * 注释
       */
      
    • 注释前总是有一个空行,缩进与下一行代码保持一致
    • 代码结束到注释之间有一个缩进
  • 添加注释的一般原则
    • 需要让代码表达的意思更清晰
    • 难以理解的代码
    • 可能被认为错误的代码
    • 浏览器特性 hack
  • 文档注释
    • 最流行的 javaDoc 文档格式
    • 多行注释以单斜杠加双星号开头 /**
    • 接下来是描述信息
    • 使用 @ 来表示一个或多个属性
    /**
     * 注释
     * @ 变量1 {String} 一个字符串变量
     */    
    
  • 如何添加文档注释
    • 所有的方法: 方法作用,期望的参数,返回值 进行描述
    • 所有的构造函数(类): 自定义类型,期望的参数添加描述
    • 所有包含文档化方法的对象: 如果对象包含一个或多个附带文档注释的方法,那么这个对象也应适当添加文档注释
    • 文档生成工具 (jsdoc:目前还在维护更新,使用量,star数也比较多)
    • 注:使用文档生成工具要使用工具要求的注释风格

语句和表达式

  • 块语句都应使用花括号
    • if...else...
    • switch
    • for
    • while
    • do..while..
    • try...catch...
    • ...
  • 花括号对齐方式,左花括号挡在块语句的第一行末尾
    if () {
        
    } else {
        
    }
    
  • 块语句间隔
    • 语句名,小括号,花括号都没有空格
      if(true){
          
      }
      
    • 小括号左右加空格
      if (true) {
          
      }
      
    • 小括号左右加空格,小括号与小括号内容之间加空格
      if ( true ) {
          
      }
      
  • switch 语句
    • 缩进
      • 每条 case 语句相对于 switch 关键字缩进一个层级,每条 case 语句后都有一个空行
        switch (color) {
          case '#000':
              break
          
          case '#f00':
              break
          
          case '#00F':
              break
          
          default:
              break
        }
        
      • case 和 switch 左对齐,且没有空行
          switch (color) {
          case '#000':
              break
          case '#00F':
              break
          default:
              break
        }
        
    • 连续执行: 有意连续执行需要加注释,其他情况默认每条语句结束必须是 break,return,throw
    • default:如果default什么也没做,加上注释,省略default
  • while 语句 尽量避免使用,因为在严格模式下不能运行,而且容易造成对于变量的认知错误
    let message = '你好!'
    let book = {
      name: '少少',
      age: 18,
      message: '你好吗?'
    }
    
    with (book) {
        message += name
        console.log(message)
    }
    
  • for 循环 遍历数组
    • break 立即退出循环
    • commit 跳过本次循环 // 尽量避免这种方式
  • for-in 循环 遍历对象
    • 不用定义任何控制条件,返回属性名而不是值
    • 不仅遍历对象的实例属性同样遍历从原型继承来的属性,往往因意外的结果而终止
      • 使用 hasOwnProperty() 过滤实例属性,除非想查找原型链

变量 函数 运算符

  • 变量声明
    • 集中在函数顶部
    • 在有相关性时,合并变量声明
      let value = 10,
          result = 10,
          i,
          len;
      
  • 函数声明
    • 先声明后使用
    • 函数声明不应该出现在语句块之内
  • 函数调用间隔
    • 函数名和小括号之间没有空格
  • 立即调用的函数
    const myFunction = function () {
        // 函数
    }
    let value = function () {
        return 'string' // 变量
    }()
    
    // 好的写法
    let value = (function () {
        return 'string'  
    }())
    
  • 严格模式 "use strict"
    • 尽可能使用严格模式
    • 不在全局使用严格模式
      function () {
          "use strict"
          
      }
      
      (function () {
          "use strict"
          function () {
              
          }
          function () {
              
          }
          function () {
              
          }
      })()
      
  • 相等 JavaScript 有强制类型转换机制
    • 数字 与 字符串比较,尝试将字符串转换数字
    • 数字 与 布尔值比较,尝试将布尔值转换为数字
    • 对象 与 其他类型,先调用valueOf(),没有就调用toString()
    • 使用 === 和 !==

编程实践

UI 层的松耦合

  • 什么是松耦合:修改一个组件而不需要修改其他的组件;组件知道的越少,越有利于形成整个系统
  • 将 js 代码和 css 代码分离开,需要操作样式时最佳方法是操作 className,除非需要 js 计算 css 的属性(比如常见的轮播图)
  • 将 html 和 js 代码分离开
    • 从服务器加载
    • 客户端模板(Handlebars)

避免使用全局变量

  • 全局变量带来的问题
    • 命名冲突
    • 代码脆弱性,依赖全局变量,任何地方都能改变全局变量的值
    • 难以测试依赖全局变量的函数
  • 意外的全局变量
    • 当使用一个未被声明的变量时,JavaScript会自动创建为全局变量
  • 单全局变量
    • 创建的这个唯一全局对象名是独一无二的(不会和内置API产生冲突),并将所有的功能代码全都挂载到这个全局对象上,因此每个可能的全局变量都会成为你唯一的全局对象的属性,从而不会创建多个全局变量
      
      // 再比如表示三本书
      // 方法一
      function Book (name) {
          return `书名:${name}`
      }
      const Book1 = Book("name1")
      const Book2 = Book("name2")
      const Book3 = Book("name3")
      
      console.log(Book1, Book2, Book3)
      
      // 单全局变量
      class Books {
          Book1 = Book("name1")
          Book2 = Book("name2")
          Book3 = Book("name3")
          
          Book (name) {
              return `书名:${name}`
          }
      }
      
      console.log(Books.Book1, Books.Book2, Books.Book3)
      
      /*
       * 其他例子: jQuery 定义了 $ 和 jQuery
       * vue 定义了 Vue
       */
      
    • 命名空间(在类中定义分组,方便管理)
      class My {
          dom = {
              addDom () {
                  ...
              },
              removeDom () {
                  ...
              }
              ...
          }
          
          event = {
              onClick () {
                  ...
              },
              oninput () {
                ...  
              }
              ...
          }
      }
      

事件处理

  • 典型用法 (流水账式写法)
    
    // 常见但是不推荐的写法 
    handleClick (event) {
        const popup = document.getElementById("popup")
        popup.style.left = event.clientX
        popup.style.top = event.clientY
        popup.className = "show"
    }
    
  • 隔离应用逻辑 应用逻辑和用户行为应该区分开(一个函数只做一件事,便于函数复用)
    clas Myapp {
        handleClick (event) {
          this.showPopup(event)  
        },
        showPopup (event) {
          const popup = document.getElementById("popup")
          popup.style.left = event.clientX
          popup.style.top = event.clientY
          popup.className = "show"
        }
    }
    
  • 不要分发事件对象 应用逻辑不应该依赖 event 对象来完成功能(便于测试,便于函数复用)
     clas Myapp {
        handleClick (event) {
        
          // 事件处理程序是接触 event 对象的唯一函数 (一个函数只做一件事)
          event.preventDefault()
          event.stopPropagation()
          
          // 传入应用逻辑
          this.showPopup(event.clientX, event.clientY)  
        },
        showPopup (x, y) {
          const popup = document.getElementById("popup")
          popup.style.left = x
          popup.style.top = y
          popup.className = "show"
        }
    }
    

避免空比较

  • 使用 "typeof 变量" 的方式检测 字符串,数字,布尔值,undefined,函数,
  • 使用 "变量 instanceof 对象" 的方式检测某个值的引用类型(不能用于检测是否属于 Object,因为 js 所有的值原型链最终都是 Object )
  • 检测数组 (es5已有自带检测方法)
    
    // 关注对象能做什么,而不是他是什么
    function isArr(value) {
        return typeof value.sort === 'function'
    }
    
    // 更为优雅有效的解决方案 (兼容 IE6 以下版本)
    function isArray (value) {
        if (typeof Array.isArray === 'function') {
            return Array.isArray(value) // 自带判断数组方法
        } else {
        
            // 某个值内置的 toString 在所有的浏览器中都会返回标准的字符串结果
            return Object.prototype.toString.call(value) === '[object Array]'
        }
    }
    
  • 检测属性是否存在
    • 不好的写法,通过给定的名字检查属性的值
      let myObject = {
          count: 0
      }
      
      if (myObject['count']) {
          // 存在但不执行
      }
      
    • 好的写法,通过 in 运算符或者 hasOwnPrperty()
      let myObject = {
          count: 0
      }
      
      if ('count' in myObject) { // in 只检查属性是否存在于对象实例或者对象原型
          // 执行
      }
      
      if (myObject.hasOwnPrperty('count')) { // 只检查是否存在于对象实例
          // 执行
      }
      

将配置数据从代码中分离出来

  • 什么是配置数据 写死在代码里,且将来可能会被修改
    • 接口地址
    • 重复的值
    • 设置(比如页面配置项)
    • 任何可能发生变更的值
    • ...
  • 抽离配置数据,把配置数据拿到外部(类似 vue 中的 data)
  • 保存配置文件,把配置单独保存到文件(类似 vue-cli 搭建的项目中 vue.config.js 的作用)

抛出自定义错误

  • 什么是错误 帮助我们快速定位代码 bug ,以便于调试维护

  • JavaScript 抛出错误的方法

    throw new Error('错误信息')
    
  • 如何书写抛出的错误文本

    throw new Error('函数名: 可能的原因')
    
  • 什么时候抛出错误

    • 不确定自己的函数在哪些地方调用
    • 工具函数
    • JavaScript 类库 (函数调用栈应该在进入库代码接口的时候就终止;意思是在调用类库时先要校验然后进行相应的提示,而不是等到)
    • 修复了一个很难调试的错误,这时添加一两个自定义错误
    • 不想要某种情况发生,为避免这种情况
  • try-catch 语句 catch 代码块不能省略或留空

    能在错误抛出前解析它;
    抛出错误会中断 js 进程, 使用 try-catch 则不会中断 js 进程
    
    个人理解: try-catch 语句可以用在自己知道可能会出错但是不能明确的知道错误原因,
    或者在报错后仍需继续报错代码块之后语句的情况下
    

不是自己的对象不要动

  • 什么是自己的对象

    • 自己创建了一个对象,那么自己拥有这个对象
    • 自己不是创建对象的人,但是负责维护代码,那么也拥有这个对象
    • 只是使用者,不要修改对象
  • 对象的使用原则

    • 不覆盖对象原型上的属性,方法
    • 不在对象原型新增属性,方法
    • 不删除对象原型上的属性,方法
    • 如何使用?
      • 继承
      • 设计模式之外观模式(门面模式 Facade Pattern)
  • 防止对象被修改

    • Object.preventExtensions(对象名) 该方法不能向对象中新添加属性和方法了,但是可以修改对象中存在的属性
    • Object.seal(对象名) 密封对象不能新添加属性、不能删除属性。
    • Object.freeze(对象名) 冻结对象,即是不可扩展的,也是密封的,而且其属性值也不能修改