前言

  • 刷完红宝书,在此总结一下第24章,最佳实践,感觉这一章节可以提升一下写代码的方式和技术含量。

JavaScript性能.xmind

背景

  • 该章节提到,现如今的代码越来来约复杂,从当初的小特效和表单校验,到现在成千上万行js代码,执行各种复杂的过程。我们应该考虑可维护性,因为大部分时间我们都是在维护他人的代码,只有确保自己代码的可维护性,才可以开展更好的工作。

提出问题

什么样的代码才是可维护的代码呢?

  • 可维护代码的特点
    • 可维护的代码应该有以下特点:
      • 可理解性
        • 要让其他开发人员可以接手代码并理解它的意图和一般途径,无需完整解释
      • 直观性
        • 代码中的东西无论操作过程多么复杂,都应该一眼就能看明白
      • 可适应性
        • 如果有数据上的变化,我们不应该完全重写这个代码
      • 可扩展性
        • 在代码架构上已考虑到未来允许对核心功能进行扩展
      • 可调试性
        • 代码可以给出足够的信息让你了解哪个地方出错

代码约定

  • 为了实现以上目标,让代码变得可维护,我们就有一套代码约定
  • 可读性
    • 可读性体现在缩进和注释两个方面:
      • 缩进:一般是4个空格(PS:我用的Tab,空格键坚决不能忍)
      • 注释:需要注释的地方有
        • 函数和方法:注释描述“目的”,用于完成任务可能使用的算法,陈述事先的假设,如参数代表啥,函数是否有返回值
        • 大段代码:用于完成单个任务的多行代码应该在前面放一个描述任务的注释
        • 复杂的算法:当使用了一种独特的方式解决某个问题,应该在注释中解释是如何做的
        • Hack:Hack是个啥我没法解释,因为我也不知道。书上说的是,浏览器存在差异,所以JavaScript代码一般会包含一些hack,不要假设其他人在看代码时能够理解hack索要应付的浏览器问题。如果因为某个浏览器无法使用普通的方法,所以你需要用一些不同的方法,那么要将这些信息放在注释中,这样减少出现这种“别人看到你的hack,然后修正了它,最后却引来了你本来修正了的错误”的这种可能性。
  • 变量和函数命名
    • 变量名:应为名词
    • 函数名:以动词开始如getName(),返回布尔类型值的函数一般以is开头,如isEnable()
    • 变量和函数都应使用合乎逻辑的名字,不要担心长度,因为长度会被浏览器压缩。
  • 变量类型透明
    • 初始化:定义一个变量后要初始化它的值,来暗示它应该如何应用。初始化为一个特定的数据类型可以很好的指明变量的类型,缺点:无法用于函数声明中的参数
    • 匈牙利标记法:如声明一个变量Found,那么如果使用匈牙利标记法,该变量将被写成:布尔型:bFound,整数:iFound,字符串:sFound,对象:oPerson。缺点是难以阅读。
    • 类型注释:类型注释放在变量名右边,但是在初始化前面。如:var found /:Boolean/ = false;优点:维持代码整体可读性,同时注入了类型信息。缺点:不能使用多行注释注释大块代码,因为类型注释也是多行注释,两者会冲突。
  • 松散耦合
    • 解耦HTML/JavaScript
      • 避免在JS中创建大量HTML,HTML呈现应该尽可能与JavaScript保持分离,当JavaScript用于插入数据时,尽量不要直接插入标记。而应该在页面中直接包含并隐藏标记,等到整个页面渲染好以后,用JavaScript显示标记,而非生成它。
      • 用Ajax请求并直接获取更多的显示的HTML,让同样的渲染层(PHP,JSP,Ruby)等来输出标记,而不是直接嵌入在JavaScript中。
    • 解耦CSS/JavaScript
      • 不要直接在JS中控制CSS,而可以为HTML添加class类名。
    • 解耦应用逻辑/事件处理程序
      • 将应用逻辑和事件处理程序相分离,这样两者分别处理各自的东西。一个事件处理程序应该从实践对象中提取相关信息,并将这些信息传怂到处理应用逻辑的某个方法中。
    • 应用和业务逻辑之间松散耦合的几条原则:
      • 勿将event对象传给其他方法,只传来自event对象中所需的数据
      • 任何可以在应用层面的动作都应该可以在不执行任何事件处理程序的情况下进行
      • 任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑
  • 编程实践
    • 尊重对象所有权
      • 不要为实例或原型添加属性
      • 不要为实例或原型添加方法
      • 不要重定义已存在的方法
      • 如果想要为对象创建新的功能,你可以:
        • 创建包含所需功能的新对象,并用它与相关对象进行交互
        • 创建自定义类型,继承需要进行修改的类型,然后可以为自定义类型添加额外功能
    • 避免全局量
    • 避免与null进行比较
      • 与null比较很少适合情况而被使用,必须按照所期望的对值进行检查,而非按照不被期望的那些。
        • eg:values参数应该是一个数组,那么就要检查它是不是一个数组,而不是检查它是否非null。
      • 如果看到了与null比较的代码,尝试使用以下技术替换:
        • 值为引用类型,使用instanceof操作符检查其构造函数
        • 值为基本类型,使用typeof检查其类型
      • 如果希望对象包含某个特定的方法名,则使用typeof操作符确保指定名字的方法存在于对象上。
    • 使用常量
      • 重复值:任何在多处用到的值都应该抽取为一个常量,限制了当一个值变了而另一个值没有变的时候造成的错误,包括CSS类名
      • 用户界面字符串:任何用于显示给用户的字符串,都应该被抽取出来方便国际化
      • URLS:在web应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的URL
      • 任意可能会改变的值:每当用到字面量值的时候,都要考虑这个值在未来是不是会变化,如果是,那么该值应该被提出取来作为一个常量。

        性能

注意作用域

  • 问题:访问全局变量总是比访问局部变量慢,因为需要遍历作用域链
  • 思路:减少花费在作用域链上的时间
  • 解决:
    • 避免全局查找
      • 应该将在一个函数中会用到多次的全局对象存储为局部变量
    • 避免with语句
      • with语句会创建自己的作用域,导致增加执行代码的作用域链长度,影响性能

选择正确方法

  • 避免不必要的属性查找

    • 圈复杂度
    • 多重属性查找,只要数一数代码中的点的数量,就可以确定属性查找次数

      1. var query = window.location.href.substring(window.location.href.indexOf("?"));
    • 改良版:

      1. var url = window.location.href;
      2. var query = url.substring(url.indexOf("?"))
  • 优化循环

    • 减值迭代:从最大值开始,在循环中不断减值迭代的迭代器更加高效
    • 简化终止条件:每次循环过程都会计算终止条件,所以必须保证它尽可能快。即,避免属性查找或其他O(n)的操作。
    • 简化循环体:循环体是执行最多的,所以要确保其被最大限度的优化,确保没有某些可以被很容易移出循环的密集计算
    • 使用后测试循环:最常用for循环和while循环都是前测试循环,do-while是后测试循环,可以避免最初中止条件的计算,运行更快
  • 展开循环
    • 当循环的次数是确定的,消除循环并使用多次函数调用往往更快。
    • 如果循环中的迭代次数不能事先确定,则可以考虑使用一种叫做Duff装置的技术
      • duff装置的基本概念是通过计算迭代的次数是否为8的倍数将一个循环展开为一系列语句。
      • duff装置的实现是通过将values数组中元素个数除以8来计算出循环需要进行多少次迭代的。
      • 展开循环可以提升大数据集的处理速度。如果不是大数据集,一般来说不值得。
  • 避免双重解释
    • 如果要提高代码性能,尽可能避免出现需要按照JavaScript解释的字符串
  • 性能的其他注意事项
    • 原生方法较快:只要有可能,应该使用原生方法而不是自己用JavaScript重写一个,原生方法是用诸如c/c++之类的编译型语言写出来的,所以要比JavaScript的快很多很多
    • Switch语句较快:如果一系列复杂的if-else语句,可以转换成单个switch语句则可以得到更快的代码,还可以通过将case语句按照最可能的到最不可能的顺序进行组织,来进一步优化switch语句
    • 位运算符较快:当进行数学运算时,位运算操作要比任何布尔运算或算数运算快,选择性地用位运算替换算数运算可以极大提升复杂计算的性能,如取模,逻辑与和逻辑或都可以考虑用位运算来替换。

最小化语句数

  • 多个变量声明
    • var a,b,c 来代替var a;var b;var c…
  • 插入迭代值

    • 当使用迭代值(即在不同的位置进行增加或减少的值)的时候,尽可能合并语句

      1. var name = values[i];i--
    • 可替换成

      1. var name = values[i--];
  • 使用数组和对象字面量

优化DOM交互

  • 最小化现场更新

    • 将列表从页面上移除,最后进行更新,最后再将列表插回同样的位置
      • 缺点:每次页面更新时会不必要的闪烁
    • 使用文档片段来构建DOM结构

      • 只有一次现场更新,发生在所有项目都创建好以后,文档片段用作一个临时的占位符,放置新创建的项目,然后使用appendChild()将所有项目添加到列表中。当给appendChild传入文档片段时,只有片段中的子节点被添加到目标,片段本身不会被添加的。
      • 一旦需要更新DOM,请考虑使用文档片段来构建DOM结构,然后再将其添加到现存的文档中。
        1. fragment = document.createDocumentFragment();
    • 使用innerHTML

      • 在页面上创建DOM节点的方法:
        • createElement()&&appendChild()之类的DOM方法
        • innerHTML
      • 对于小的DOM更改而言两者区别不大,对于大的DOM更改,使用innerHTML要比使用标准DOM方法创建同样的DOM结构快很多。
        • 理由:当把innerHTML设置为某个值时,后台会创建一个HTML解析器,然后使用内部的DOM调用来创建DOM结构,而非基于JavaScript的DOM调用,由于内部方法是编译好的而非解释执行的,所以执行快很多。
        • 使用innerHTML的关键在于最小化调用它的次数
    • 使用事件代理
      • 任何可以冒泡的事件都可以在任何祖先节点上处理,就可以将时间处理程序附加到更高层的地方负责多个目标的时间处理
    • 注意HTMLCollection
      • HTMLCollection被访问时,无论访问的是属性还是方法,都是在文档上进行一个查询,该查询开销很昂贵
      • 要知道何时会返回HTMLCollection对象,最小化对他们的访问
        • 进行对getElementByTagName()的调用
        • 获取元素的childNodes属性
        • 获取元素的attributes属性
        • 访问特殊集合,如document.forms,document.images
      • 合理访问HTMLCollection对象会极大提升代码执行速度