导航
导航

JS高级程序设计读书笔记(下)

DOM

dom是文档对象模型的简称,dom将html语言描述成了一个拥有层次结构的树,允许开发人员对其进行操作,以达到使用js修改页面的行为

NODE类型

每个节点都有一个 nodeType 属性,用于表明节点的类型。节点类型由在 Node 类型中定义的下列 12 个数值常量来表示,任何节点类型必居其一:

  • Node.ELEMENT_NODE(1)
  • Node.ATTRIBUTE_NODE(2)
  • Node.TEXT_NODE(3)
  • Node.CDATA_SECTION_NODE(4)
  • Node.ENTITY_REFERENCE_NODE(5)
  • Node.ENTITY_NODE(6)
  • Node.PROCESSING_INSTRUCTION_NODE(7)
  • Node.COMMENT_NODE(8)
  • Node.DOCUMENT_NODE(9)
  • Node.DOCUMENT_TYPE_NODE(10)
  • Node.DOCUMENT_FRAGMENT_NODE(11)
  • Node.NOTATION_NODE(12)

通过比较上面这些常量,可以很容易地确定节点的类型,例如:

1
2
3
4
//在 IE 中无效
if (someNode.nodeType == Node.ELEMENT_NODE){
alert("Node is an element.");
}

这个例子比较了 someNode.nodeType 与 Node.ELEMENT_NODE 常量。如果二者相等,则意味着 someNode 确实是一个元素。然而,由于 IE 没有公开 Node 类型的构造函数,因此上面的代码在 IE 中 会导致错误。为了确保跨浏览器兼容,最好还是将 nodeType 属性与数字值进行比较,如下所示:

1
2
3
4
//适用于所有浏览器
if (someNode.nodeType == 1){
alert("Node is an element.");
}

节点关系

父子节点

每个节点都有一个 childNodes 属性,其中保存着一个 NodeList 对象。NodeList 是一种类数组 对象,用于保存一组有序的节点,可以通过位置来访问这些节点。请注意,虽然可以通过方括号语法来 访问 NodeList 的值,而且这个对象也有 length 属性,但它并不是 Array 的实例。NodeList 对象的 独特之处在于,它实际上是基于 DOM 结构动态执行查询的结果,因此 DOM 结构的变化能够自动反映 在 NodeList 对象中。

下面的例子展示了如何访问保存在 NodeList 中的节点——可以通过方括号,也可以使用 item() 方法。

1
2
3
var firstChild = someNode.childNodes[0];
var secondChild = someNode.childNodes.item(1);
var count = someNode.childNodes.length;

父节点的 firstChild 和 lastChild 属性分别指向其 childNodes 列表中的第一个和最后一个节点。其中,someNode.firstChild 的值 始 终 等 于 someNode.childNodes[0] , 而 someNode.lastChild 的 值 始 终 等 于 someNode. childNodes [someNode.childNodes.length-1]。在只有一个子节点的情况下,firstChild 和 lastChild 指向同一个节点。如果没有子节点,那么 firstChild 和 lastChild 的值均为 null。

每个节点都有一个 parentNode 属性,该属性指向文档树中的父节点。包含在 childNodes 列表中 的所有节点都具有相同的父节点, 因此它们的 parentNode 属性都指向同一个节点。

兄弟节点

包含在 childNodes 列表中的每个节点相互之间都是同胞节点。通过使用列表中每个节点的 previousSibling 和 nextSibling 属性,可以访问同一列表中的其他节点。列表中第一个节点的 previousSibling 属性 值为 null,而列表中最后一个节点的 nextSibling 属性的值同样也为 null,如下面的例子所示:

1
2
3
4
5
if (someNode.nextSibling === null){
alert("Last node in the parent’s childNodes list.");
} else if (someNode.previousSibling === null){
alert("First node in the parent’s childNodes list.");
}

操作节点

appendChild(),用于向 childNodes 列表的末尾添加一个节点。添加节点后,childNodes 的新增 节点、父节点及以前的最后一个子节点的关系指针都会相应地得到更新。更新完成后, appendChild() 返回新增的节点。

如果传入到 appendChild()中的节点已经是文档的一部分了,那结果就是将该节点从原来的位置 转移到新位置。即使可以将 DOM 树看成是由一系列指针连接起来的,但任何 DOM 节点也不能同时出 现在文档中的多个位置上。因此,如果在调用 appendChild()时传入了父节点的第一个子节点,那么 该节点就会成为父节点的最后一个子节点,如下面的例子所示。

1
2
3
4
//someNode 有多个子节点 
var returnedNode = someNode.appendChild(someNode.firstChild);
alert(returnedNode == someNode.firstChild); //false
alert(returnedNode == someNode.lastChild); //true

insertBefore()方法。这个方法接受两个参数:要插入的节点和作为参照的节点。插入节点后,被插 入的节点会变成参照节点的前一个同胞节点(previousSibling),同时被方法返回。如果参照节点是 null,则 insertBefore()与 appendChild()执行相同的操作

replaceChild()方法接受的两个参数:要插入的节点和要替换的节点。要替换的节点将由这个方法返回并从文档树中被移除,同时由要插入的节点占据其位置。在使用 replaceChild()插入一个节点时,该节点的所有关系指针都会从被它替换的节点复制过来。从技术上讲,被替换的节点仍然还在文档中,但它在文档中已经没有了自己的位置。

removeChild()方法接受一个参数,即要移除的节点。被移除的节点将成为方法的返回值。与使用 replaceChild()方法一样,通过 removeChild()移除的节点仍然为文档所有,只不过在文档中已经没有了自己的位置。

cloneNode()用于创建调用这个方法的节点的一个完全相同的副本。cloneNode()方法接受一个布尔值参数,表示是否执行深复制。在参数为 true 的情况下,执行深复制,也就是复制节点及其整个子节点树;在参数为 false 的情况下,执行浅复制,即只复制节点本身。复制后返回的节点副本属于文档所有,但并没有为它指定父节点。因此,这个节点副本就成为了一个“孤儿”。

Document类型

JavaScript 通过 Document 类型表示文档。在浏览器中,document 对象是 HTMLDocument(继承自 Document 类型)的一个实例,表示整个 HTML 页面。而且,document 对象是 window 对象的一个 属性,因此可以将其作为全局对象来访问。

文档子节点

虽然 DOM 标准规定 Document 节点的子节点可以是 DocumentType、Element、ProcessingIn-struction 或 Comment,但还有两个内置的访问其子节点的快捷方式。第一个就是 documentElement 属性,该属性始终指向 HTML 页面中的<html>元素。另一个就是通过 childNodes 列表访问文档元素, 但通过 documentElement 属性则能更快捷、更直接地访问该元素。

文档中通常只包含一个子节点, 即 <html> 元素。 可以通过 documentElement 或 childNodes 列表来访问这个元素。作为 HTMLDocument 的实例,document 对象还有一个 body 属性,直接指向<body>元素(document.body)

文档信息

title包含着<title>元素中的文本——显示在浏览器窗口的标题栏或标签页上。通过这个属性可以取得当前页面的 标题,也可以修改当前页面的标题并反映在浏览器的标题栏中。修改 title 属性的值不会改变<title> 元素。

URL属性中包含页面完整的 URL(即地址栏中显示的 URL)。

domain属性中只包含页面的域名。

referrer属性中则保存着链接到当前页面的那个页面的 URL。在没有来源页面的情况下,referrer 属性中可能 会包含空字符串。

所有这些信息都存在于请求的 HTTP 头部, 只不过是通过这些属性让我们能够在 JavaScrip 中访问它们而已

查找元素

getElementById()接收一个参数:要取得的元素的 ID。如果找到相应的元素则返回该元素,如果不存在带有相应 ID 的元素,则返回 null。注意,这里的 ID 必须与页面中元素的 id 特性(attribute)严格匹配,包括大小写。

getElementsByClass()接收一个参数:要取得的元素的class。返回一个类似数组的对象,包含了所有指定 class 名称的子元素。当调用发生在document对象上时, 整个DOM都会被搜索, 包含根节点。

getElementsByTagName()方法接受一个参数,即要取得元素的标签名,而返回的是包含零或多个元素的 NodeList。在 HTML 文档中,这个方法会返回一 个 HTMLCollection 对象,作为一个“动态”集合,该对象与 NodeList 非常类似。

getElementsByName()方法会返回带有给定 name 特性的所有元素。最常使用 getElementsByName()方法的情况是取得单选按钮;为了确保发送给浏览器的值正确无误,所有单选按钮必须具有相同的 name 特性。

Element类型

Element 类型用 于表现 XML 或 HTML 元素,提供了对元素标签名、子节点及特性的访问。像是我们取到的 div 节点都属于该类型

HTML元素

所有 HTML 元素都由 HTMLElement 类型表示,不是直接通过这个类型,也是通过它的子类型来表 示。HTMLElement 类型直接继承自 Element 并添加了一些属性。添加的这些属性分别对应于每个 HTML 元素中都存在的下列标准特性。

  • id,元素在文档中的唯一标识符
  • title,有关元素的附加说明信息,一般通过工具提示条显示出来l
  • lang,元素内容的语言代码,很少使用
  • dir,语言的方向,值为”ltr”(left-to-right,从左至右)或”rtl”(right-to-left,从右至左),也很少使用。
  • className,与元素的 class 特性对应,即为元素指定的 CSS类。没有将这个属性命名为 class, 是因为 class 是 ECMAScript 的保留字

所有的这些特性都可以通过 . 操作符来访问,就如同访问对象的属性一样

每个元素都有一或多个特性,这些特性的用途是给出相应元素或其内容的附加信息。操作特性的 DOM 方法主要有三个,分别是 getAttribute()、setAttribute()和 removeAttribute(),需要注意的是,特性的名称不区分大小写,同时根据HTML5的规范,自定义特性应该加上 data- 前缀以便验证。

两个特殊点:使用上述方法访问style属性和onclick属性时,会返回相应的代码字符串,直接访问则会返回对应的对象

理解 DOM 的关键,就是理解 DOM 对性能的影响。DOM 操作往往是 JavaScript 程序中开销最大的 部分,而因访问 NodeList 导致的问题为最多。NodeList 对象都是“动态的”,这就意味着每次访问 NodeList 对象,都会运行一次查询。有鉴于此,最好的办法就是尽量减少 DOM 操作

代码最佳实践

可维护的代码

  • 可理解性——其他人可以接手代码并理解它的意图和一般途径,而无需原开发人员的完整解释
  • 直观性——代码中的东西一看就能明白,不管其操作过程多么复杂
  • 可适应性——代码以一种数据上的变化不要求完全重写的方法撰写
  • 可扩展性——在代码架构上已考虑到在未来允许对核心功能进行扩展
  • 可调试性——当有地方出错时,代码可以给予你足够的信息来尽可能直接地确定问题所在

代码约定

由于 JavaScript 的可适应性,代码约定对它也很重要。由于和大多数面向对象语言不同,JavaScript 并不强制开发人员将所有东西都定义为对象。语言可以支持各种编程风格,从传统面向对象式到声明式 到函数式。只要快速浏览一下一些开源 JavaScript 库,就能发现好几种创建对象、定义方法和管理环境的途径。

因此一套成熟的代码约定能形成一份高可维护的代码,一般来说,代码约定有如下几个方面:

  1. 可读性:要让代码可维护,首先它必须可读。可读性的大部分内容都是和代码的缩进相关的。当所有人都使用一样的缩进方式时,整个项目中的代码都会更加易于阅读。通常会使用若干空格而非制表符来进行缩进,这是因为制表符在不同的文本编辑器中显示效果不同。可读性的另外一个方面是注释,一般来说,需要在以下地方有清晰的注释:
    • 函数和方法——每个函数或方法都应该包含一个注释,描述其目的和用于完成任务所可能使用的算法
    • 大段代码——用于完成单个任务的多行代码应该在前面放一个描述任务的注释
    • 复杂的算法——如果使用了一种独特的方式解决某个问题,则要在注释中解释你是如何做的
    • Hack—- hack 所要应付的浏览器问题,请将这些信息放在注释中
  2. 变量和函数命名:适当给变量和函数起名字对于增加代码可理解性和可维护性是非常重要的。一般的命名遵循以下原则:
    • 变量名应为名词,如 car 或 person
    • 函数名应该以动词开始, 如 getName() 。返回布尔类型值的函数一般以 is 开头,如 isEnable()
    • 变量和函数都应使用合乎逻辑的名字,不要担心长度。长度问题可以通过后处理和压缩来缓解
  3. 变量类型透明:由于在 JavaScript 中变量是松散类型的,很容易就忘记变量所应包含的数据类型。有三种表示变量数据类型的方式:

    • 初始化

      1
      2
      3
      4
      5
      //通过初始化指定变量类型 
      var found = false;
      var count = -1;
      var name = "";
      var person = null;
    • 匈牙利标记法,JavaScript 中最传统的匈牙利标记法是用单个字符表示基本类型:”o”代表对象,”s”代表字符串,”i” 代表整数,”f”代表浮点数,”b”代表布尔型。如下所示:

      1
      2
      3
      4
      5
      //用于指定数据类型的匈牙利标记法 
      var bFound; //布尔型
      var iCount; //整数
      var sName; //字符串
      var oPerson; //对象
    • 类型注释

      1
      2
      3
      4
      5
      //用于指定类型的类型注释 
      var found /*:Boolean*/ = false;
      var count /*:int*/ = 10;
      var name /*:String*/ = "Nicholas";
      var person /*:Object*/ = null;

尊重对象所有权

简单来说就是你不能修改不属于你的对象。如果你不负责创建或维护某个对象、它的对象或它的方法,那么你就不应该对它进行修改,具体点来说:

  • 不要为实例或原型添加属性
  • 不要为实例或原型添加方法
  • 不要重定义已存在的方法

这些规则不仅仅适用于自定义类型和对象,对于诸如 Object、String、document、window 等原生类型和对象也适用。此处潜在的问题可能更加危险,因为浏览器提供者可能会在不做宣布或者是不可预期的情况下更改这些对象

值得注意的是,所谓拥有对象,就是说这个对象是你创建的,比如你自己创建的自定义类型或对象字面量。而 Array、document 这些显然不是你的,它们在你的代码执行前就存在了

避免全局变量

有如下两段代码:

1
2
3
4
5
6
7
8
9
10
11
var name = "Nicholas";
function sayName(){
alert(name);
}

var MyApplication = {
name: "Nicholas",
sayName: function(){
alert(this.name);
}
};

在第一段代码中,变量 name 覆盖了 window.name 属性,可能会与其他功能产生冲突;而重写之后的代码避免了这个问题,同时,它有助消除功能作用域之间的混淆。调用 MyApplication.sayName() 在逻辑上暗示了代码的任何问题都可以通过检查定义 MyApplication 的代码来确定。

单一的全局量的延伸便是命名空间的概念,由 YUI(Yahoo! User Interface)库普及。命名空间包括创建一个用于放置功能的对象。在 YUI 的 2.x 版本中,有若干用于追加功能的命名空间。比如:

  • YAHOO.util.Dom —— 处理 DOM 的方法
  • YAHOO.util.Event —— 与事件交互的方法
  • YAHOO.lang —— 用于底层语言特性的方法

对于 YUI,单一的全局对象 YAHOO 作为一个容器,其中定义了其他对象。用这种方式将功能组合 在一起的对象,叫做命名空间。整个 YUI 库便是构建在这个概念上的,让它能够在同一个页面上与其他的 JavaScript 库共存

避免与null作比较

在与 null 作比较的时候,往往会发生我们不期望发生的结果。因为JavaScript是弱类型语言,类型比较不充分,如果需要进行类型比较,使用以下原则代替与 null 进行比较:

  • 如果值应为一个引用类型,使用 instanceof 操作符检查其构造函数
  • 如果值应为一个基本类型,使用 typeof 检查其类型
  • 如果是希望对象包含某个特定的方法名,则使用 typeof 操作符确保指定名字的方法存在于对象上

使用常量

通过将数据抽取出来变成单独定义的常量的方式,将应用逻辑与数据修改隔离开来,这能够避免很多时候修改复杂逻辑中的数据带来的错误:

  • 重复值——任何在多处用到的值都应抽取为一个常量。这就限制了当一个值变了而另一个没变 的时候会造成的错误。这也包含了 CSS 类名
  • 用户界面字符串 —— 任何用于显示给用户的字符串,都应被抽取出来以方便国际化
  • URLs —— 在 Web 应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的 URL
  • 任意可能会更改的值 —— 每当你在用到字面量值的时候,你都要问一下自己这个值在未来是不是会变化。如果答案是“是”,那么这个值就应该被提取出来作为一个常量

性能

避免全局查找

在规模较大的web应用中,会创建出许多作用域,此时访问当前作用域以外的变量的时间也在增加。因为需要遍历作用域链,所以要尽量避免使用全局作用域下的变量

使用正确的方法

即在相应的问题下,采用正确的算法,尽量做到使用最优的时间复杂度和空间复杂度来解决当前问题,其中有一点需要注意使用变量和数组要比访问对象上的属性更有效率,前者是一个O(1)的操作,而后者是一个 O(n)操作

优化循环

一个循环的基本优化步骤如下所示:

  1. 减值迭代——大多数循环使用一个从 0 开始、增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效
  2. 简化终止条件——由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。也就是说 避免属性查找或其他 O(n)的操作
  3. 简化循环体——循环体是执行最多的,所以要确保其被最大限度地优化。确保没有某些可以被 很容易移出循环的密集计算
  4. 使用后测试循环——最常用 for 循环和 while 循环都是前测试循环。而如 do-while 这种后测试循环,可以避免最初终止条件的计算,因此运行更快

此外,如果循环次数是确定的,那么展开循环,改用重复调用某个函数来解决问题,可能结果会更快

避免双重解释

代码在运行之前需要经过解析,而如果在解析时遇到了 字符串中的代码 就会存在双重解释惩罚,例如:

var sayHi = new Function("alert('Hello world!')");

在以上的例子中,要解析包含了 JavaScript 代码的字符串。这个操作是不能在初始的解析过程中完成的,因为代码是包含在字符串中的,也就是说在 JavaScript 代码运行的同时必须新启动一个解析器来解析新的代码。实例化一个新的解析器有不容忽视的开销,所以这种代码要比直接解析慢得多

最小化语句数

多个变量声明:

1
2
3
4
5
6
7
8
9
var count = 5;
var color = "blue";
var values = [1,2,3];
var now = new Date();

var count = 5,
color = "blue",
values = [1,2,3],
now = new Date();

插入迭代值:

1
2
3
4
var name = values[i]; 
i++;

var name = values[i++];

优化DOM交互

利用文档片段减少现场更新

1
2
3
4
5
6
7
8
9
10
11
12
var list = document.getElementById("myList"),
fragment = document.createDocumentFragment(),
item,
i;

for (i=0; i < 10; i++) {
item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}

list.appendChild(fragment);

上面这段代码插入了一个ul列表,如果每次单个插入,需要更新至少10次DOM结构,使用文档片段整合后,只需要更新一次

使用innerHTML — 有两种在页面上创建 DOM 节点的方法:使用诸如 createElement()和 appendChild()之类的 DOM 方法,以及使用 innerHTML。对于小的 DOM 更改而言,两种方法效率都差不多。然而,对于大的 DOM 更改,使用 innerHTML 要比使用标准 DOM 方法创建同样的 DOM 结构快得多。

当把 innerHTML 设置为某个值时,后台会创建一个 HTML 解析器,然后使用内部的 DOM 调用来 创建 DOM 结构,而非基于 JavaScript 的 DOM 调用。由于内部方法是编译好的而非解释执行的,所以执行快得多。

使用事件代理

注意HTMLCollection — 例如 childNodes 的长度应该存入变量后再访问,而不是在 for 循环中每次都去访问

写在最后

这个系列的文章初衷是为了让自己能够记录下啃红皮书的过程,虽然一遍阅读没有读的很完整,仅仅记录了一些自己觉得重要的东西,而且大篇幅是书中的内容,但是也有了自己以后供自己查阅的资料,算是一次小小的记录,再接再厉,砥砺前行。