js
2011-12-26 11:54 依水间 阅读(2929) 评论(1) 编辑 收藏 举报Baidu javascript 编码手册
Table of Contents
- 1. 结构
- 2. 命名
- 3. 注释
- 4. 条件
- 5. 数据类型
- 6. 类型检测
- 7. 对象(Object)
- 8. 函数(Function)
- 9. 字符串
- 9.1. 字符串拼接
- 9.2. 字符串格式化
- 9.3. 字符串常用实例方法
- 9.3.1. charAt(position)
- 9.3.2. charCodeAt(position)
- 9.3.3. indexOf(searchString, position)
- 9.3.4. lastIndexOf(searchString, position)
- 9.3.5. localeCompare(otherString)
- 9.3.6. match(regexp)
- 9.3.7. replace(search, replaceValue)
- 9.3.8. search(regexp)
- 9.3.9. slice(start, end)
- 9.3.10. split(separator, limit)
- 9.3.11. substr(start, length)
- 9.3.12. toLowerCase()
- 9.3.13. toUpperCase()
- 9.4. 字符串常用静态方法
- 10. 数组
- 11. 面向对象
- 12. DOM
- 13. DOM事件
- 14. 页面加载与代码构建
- 15. 第三方javascript
- A. 参考书目
List of Tables
- 3.1. jsdoc-toolkit支持标签
List of Examples
- 1.1. 代码空白示例
- 1.2. 断行示例:逗号后面断行
- 1.3. 断行示例:逻辑运算符前面断行
- 1.4. 断行示例:+号前面断行
- 1.5. 容易造成理解歧义的块省略
- 2.1. 常量命名示例
- 2.2. 变量命名示例
- 2.3. 函数命名示例:调用函数
- 2.4. 函数命名示例:构造器
- 3.1. 注释示例:文件描述
- 3.2. 注释示例:函数与方法描述
- 3.3. 注释示例:操作描述
- 4.1. “==”的陷阱
- 4.2. 使用case替换if
- 5.1. 简单类型与包装类型的差异
- 5.2. 使用简单类型
- 5.3. 类型转换:parseInt的风险
- 6.1. typeof检测普通类型
- 6.2. typeof检测对象
- 6.3. instanceof的风险
- 7.1. 对象初始化
- 7.2. Array与RegExp初始化
- 7.3. 使用delete删除对象成员
- 7.4. 原生对象进行扩展的bad case
- 8.1. 函数也是对象
- 8.2. 函数调用示例:直接调用
- 8.3. 函数调用示例:方法调用
- 8.4. 函数调用示例:构造器调用
- 8.5. 函数调用示例:apply或call
- 8.6. 变量的函数作用域
- 8.7. 简单的闭包
- 8.8. 作用域泛滥
- 8.9. 闭包函数中使用dom元素
- 9.1. 字符串拼接:不好的拼接方式,+=
- 9.2. 字符串拼接:正确拼接方式,Array的push+join
- 9.3. 字符串拼接:更好的拼接方式,Array,使用临时变量存储数组长度
- 9.4. 字符串格式化
- 9.5. string.charAt
- 9.6. string.charCodeAt
- 9.7. string.indexOf
- 9.8. string.lastIndexOf
- 9.9. string.localeCompare
- 9.10. string.match
- 9.11. string.replace:替换第一项与全部替换
- 9.12. string.replace:使用替换函数
- 9.13. string.search
- 9.14. string.slice
- 9.15. string.split
- 9.16. string.substr
- 9.17. string.toLowerCase
- 9.18. string.toUpperCase
- 9.19. String.formCharCode
- 10.1. 数组的浅复制
- 10.2. 使用splice删除数组项
- 10.3. 使用length清空数组
- 10.4. 使用length删除数组尾部的项
- 10.5. 数组遍历:提前存储数组的lenght
- 10.6. 数组遍历:使用倒序便利
- 10.7. 使用sort方法进行排序
- 10.8. 使用string.localeCompare进行中文排序
- 10.9. array.concat
- 10.10. array.join
- 10.11. array.pop
- 10.12. array.push
- 10.13. array.shift
- 10.14. array.slice
- 10.15. array.sort:默认按字符串排序
- 10.16. array.sort:通过比较函数实现数值排序
- 10.17. array.splice
- 10.18. array.unshift
- 11.1. 声明静态类
- 11.2. 声明静态类:通过闭包
- 11.3. 原型继承
- 11.4. 原型继承的风险
- 12.1. id与name的冲突
- 12.2. dom操作实时反映到结果集合中
- 12.3. 设置元素的class
- 12.4. 设置元素的style
- 12.5. 可变样式定义在标签内内联中
- 12.6. 使用额外的class管理可变样式
- 12.7. 一个频繁触发reflow的例子
- 12.8. 一个频繁触发reflow的例子的改进
- 13.1. 添加事件处理:标签内联
- 13.2. 添加事件处理:expando属性
- 13.3. 添加事件处理:添加监听器
- 14.1. 加载外部js
- 14.2. 开发时外部js装载
- 14.3. 自动构建的shell脚本
- 15.1. 隔离作用域,防止变量冲突
- 15.2. 全局变量暴露
- 15.3. 字符编码转换
- 15.4. 与用户页面dom结构有关时,使用document.write创建元素
- 15.5. 与用户页面dom结构无关时,在用户页面load完成后,使用createElement创建
Table of Contents
适当的空白字符可以让代码更容易被阅读。
var me = this;
上面的代码中,等号两端的空白字符是可以去掉的,但我们需要保留它们,不至于让"me=this"看起来像一个token。
基本代码空白规则:
-
左括号的左边需要有空白。
-
右括号的右边需要有空白。
-
双目或三目运算符的两边需要有空白。
-
逗号或分号的右边需要有空白。
-
如果有多个空白符时合并成一个。
-
function声明中,形参左括号的左边不需要空白,保持和调用的形式一致。
使用4个空格字符作为代码缩进,避免使用tab空白字符。tab空白字符在不同编辑器下可能会有不同的表现,使用空格字符作为缩进能很好的避免这种情况。
主流编辑器都能够设置自动对tab进行替换,以方便代码编写。
每行的字符数建议不超过100个字符。
过长的程序行应在适当的地方断行,并保持断行后的代码整齐。断行不要破坏表达式本身的意义。
Table of Contents
javascript中无法使用const声明常量。如果要声明一个在程序运行阶段不会更改的变量(如配置项),命名规则为:每个字母大写,单词间以下划线分割。
javascript中,函数可以用来实例化对象,也可以被调用。为了增加代码可读性,需要在命名上进行区分。
Table of Contents
注释是一种意识,注释是一种责任。
在javascript文件顶部需要有文件描述的注释,其中包括项目名、作者、修改日期等信息。
Example 3.1. 注释示例:文件描述
/*
* Tangram
* Copyright 2009 Baidu Inc. All rights reserved.
*
* path: baidu.js
* author: allstar, erik
* date: 2009/12/2
*/
Tip
文件描述通常不用于生成api文档。
函数和方法必须用注释描述其功能。
Example 3.2. 注释示例:函数与方法描述
/**
* 发送get请求的简单外观接口
*
* @param {string} url 需要发送请求的地址
* @param {Function} onsuccess optional 请求成功之后调用的函数
* @return {XMLHttpRequest} 发送请求的xhr对象
*/
baidu.ajax.get = function (url, onsuccess) {
}
Tip
我们描述一个函数的时候,只描述做什么,不描述具体怎么做。
常用的注释标签包括param、return、private、extend等。下表是jsdoc-toolkit支持的部分标签:
Table 3.1. jsdoc-toolkit支持标签
标签 | 说明 |
---|---|
@augments | 指明类的基类,同@extends。建议使用@extends |
@author | 作者名 |
@class | 用来给一个类提供描述(不描述构造函数) |
@constant | 指明常量 |
@constructor | 描述一个类的构造函数 |
@constructs | 指定一个函数作为构造函数 |
@default | 描述变量的默认值 |
@deprecated | 表示函数/类已不再提供 |
@description | 说明的描述,与首行不带tag的描述同义 |
@event | 描述事件 |
@example | 提供使用示例,比如常用的调用方式 |
@extends | 指明类的基类 |
@field | 指明变量引用 |
@function | 指明变量引用了一个函数(function类型) |
@ignore | 让jsdoc忽视改描述,不生成文档 |
@link | 类似于@link标签,用于连接许多其它页面 |
@memberOf | 指明成员所属类 |
@name | 强迫生成文档时不适用默认名,指定一个名称 |
@namespace | 描述一个对象为namespace |
@param | 描述一个函数的参数 |
@private | 表示成员或类为私有,默认不生成文档 |
@property | 在构造器上指明类的属性 |
@public | 指明一个内部变量或成员为公有 |
@requires | 描述依赖的资源 |
@return | 描述一个函数的返回值 |
@returns | 描述一个函数的返回值 |
@see | 描述相关资源 |
@since | 指明一个版本,表示该特性从该版本开始被支持 |
@throws | 描述函数可能产生的错误 |
@type | 指定函数的返回类型 |
@version | 描述版本 |
Table of Contents
在条件判断时,这样的一些值表示false:
-
false
-
null
-
undefined
-
字符串: ''
-
数字: 0
-
数字: NaN
而在==时,则会有一些让人难以理解的陷阱:
Example 4.1. “==”的陷阱
(function () {
var undefined;
undefined == null; // true
1 == true; // true
2 == true; // false
0 == false; // true
0 == ''; // true
NaN == NaN; // false
[] == false; // true
[] == ![]; // true
})();
对于不同类型的 “==” 判断,有这样一些规则,顺序自上而下:
-
undefined与null相等
-
一个是number一个是string时,会尝试将string转换为number
-
尝试将boolean转换为number,0或1
-
尝试将Object转换成number或string,取决于另外一个对比量的类型
所以,对于0、空字符串的判断,建议使用 “===” 。“===”会先判断两边的值类型,类型不匹配时为false。
想了解更多,请参考ecma-262第三版的12.5章、9.2章、11.9.3章。
在同时满足下面的条件时,建议使用case代替if:
-
分支条件单一
-
分支可能性 > 2
下面是一个简单的小例子。
Table of Contents
javascript有6种数据类型:undefined、null、string、number、boolean、object。
javascript原生的object包括:Object、Function、Array、String、Boolean、Number、Math、Date、RegExp、Error。
下面的例子展示了简单类型与包装类型的差异:
Example 5.1. 简单类型与包装类型的差异
var str = '1',
str2 = new String('1');
typeof str; // string
typeof str2; // object
Important
对于布尔、数值与字符串来说,应针对简单类型进行编程,通过隐式装箱调用包装类型的方法。
Table of Contents
大多数情况,我们可以用typeof检测数据类型,如number、string、boolean。
Example 6.1. typeof检测普通类型
typeof 1; // number
typeof ''; // string
typeof false; // boolean
typeof void(0); // undefined
但是,typeof对Object的检测有时会给我们带来麻烦。它能区分Function,无法区分null。
Caution
需要使用对象的时候,不要使用typeof来判断。
我们可以用instanceof来检测对象的构造器。
instanceof会沿着原型链遍历。比如b instanceof A,将会判断b的原型链中每一项是否为A的实例,直到null为止。
但是,由于instanceof遍历原型链的特性,可能产生如下问题:
Example 6.3. instanceof的风险
function A(){
this.testA = new Function();
}
function B(){
this.testB = new Function();
}
var a = new A();
A.prototype = new B();
a instanceof A; // false
上面的例子中,instanceof认为a不是A的实例,因为A的prototype已经被更改。
Caution
实例化对象前,应完成类的prototype成员构建,否则可能导致不期望的判断结果。
我们需要判断一个变量的类型的时候,需要考虑的很多可能性。
一个例子:判断变量 a 是否字符串
最简单的:
typeof a == 'string'
可能a是字符串的包装类型,于是:
a.constructor == String
可是a为null或undefined时会报运行时错误,于是:
typeof a == 'string' || a instanceof String
如果js把String重写了,如function String(){},会判断出错,于是:
Object.prototype.toString.call(a) == '[object String]'
如果Object.prototype.toString再被重写了......这个时候,我们就没办法了
上面的例子说明了:类型判断没有完全靠谱的可能。但是我们回过头来想,上面这个例子,我们为什么需要判断包装类型?就算需要判断,为什么需要考虑构造器或prototype被重写的情况?
于是,我们得出这样的结论:
Important
-
回顾一下上一章的结论:对于布尔、数值与字符串来说,我们应针对简单类型进行编程。
-
不要重写原生对象构造器,不要扩展原生对象的prototype。
Table of Contents
使用对象直接量“{}”初始化对象,不使用“new Object”。
Warning
对象的最后一个成员后不应存在分隔的“,”号,否则在ie下会报语法错误。
类似的,初始化Array与RegExp时,也应使用直接量。。
Tip
RegExp直接量声明比new RegExp性能要高。但是如果是页面内使用ctpl,并且带$时,需要使用new RegExp('\x24')。因为ctpl会把$以及后续的字符识别为模板变量。
对象成员访问有两种方式:
-
obj.identifier
-
obj[expression]
成员名称为javascript保留字,或者通过表达式访问时,通过obj[expression]方式访问。其他时候,通过obj.identifier访问。该方式增加可读性,并且减少字符数。
Table of Contents
函数也是对象,所以,函数能够被随意引用,能够作为参数传递,能够被返回。
函数可以被以下4种方式调用:
函数在被调用时,this指针是由调用者所决定的。
-
直接调用时,this指针为Globel Object,浏览器端为window。
-
方法调用时,this指针为方法所属对象。如a.b.c(),this指针为a.b。
-
构造器调用时,解析器会创建一个Object,this指针为这个Object,默认这个Object会被返回
-
apply或call可以通过第一个参数指定调用时的this指针。
与熟知的绝大多数高级语言(c、c++、java...)不同,javascript变量的作用域是function域,不是块域。
Example 8.6. 变量的函数作用域
function myFunc() {
for (var i = 0; i < 10; i++) {
var num = i; // 生命周期是function的生命周期
}
num; // 9
}
myFunc();
函数在被调用的时候,会创建一个作用域。作用域可以看做是一个javascript对象,其中包括arguments属性,以及与形参同名的属性
函数对象会引用父函数作用域对象。所以,我们能够通过闭包,让随后执行的函数使用当前父函数运行时的环境(除了this与arguments)
父函数作用域对象被子函数引用后,如果子函数包含引用,则父函数的作用域不会被回收。下面是一个作用域泛滥的例子,这个例子额外创建了lis.length个scope,并且他们都不会被释放:
-
能不使用闭包的时候,不使用闭包。
-
保持闭包环境的简单可管理,通常闭包环境中不包含超过10个父域变量。
-
不包含对非原生对象的引用,如dom对象。如果包含循环引用,并引用了非原生对象,可能导致无法释放产生内存泄露。
Tip
如果闭包中需要使用dom,闭包环境应引用dom元素id,在需要使用的时候再获取。如下例:
Example 8.9. 闭包函数中使用dom元素
function getHandler() {
var domId = 'dom';
return function () {
var dom = document.getElementById(domId); // 获取dom元素
};
}
Tip
在mouseover事件或其他频繁执行的状态,如果多次获取同一个dom对象会导致效率问题,可以在第一次持有dom对象的引用,在使用完后释放。
Table of Contents
- 9.1. 字符串拼接
- 9.2. 字符串格式化
- 9.3. 字符串常用实例方法
- 9.3.1. charAt(position)
- 9.3.2. charCodeAt(position)
- 9.3.3. indexOf(searchString, position)
- 9.3.4. lastIndexOf(searchString, position)
- 9.3.5. localeCompare(otherString)
- 9.3.6. match(regexp)
- 9.3.7. replace(search, replaceValue)
- 9.3.8. search(regexp)
- 9.3.9. slice(start, end)
- 9.3.10. split(separator, limit)
- 9.3.11. substr(start, length)
- 9.3.12. toLowerCase()
- 9.3.13. toUpperCase()
- 9.4. 字符串常用静态方法
字符串拼接,应使用数组作为StringBuffer保存字符串片段,使用时调用join方法。避免使用+或+=的方式拼接较长的字符串,每个字符串都会使用一个小的内存片段,过多的内存片段会影响性能。
Tip
绝大部分现代浏览器都对+=的字符串拼接进行了优化,但IE6的使用率依然很高。
Example 9.1. 字符串拼接:不好的拼接方式,+=
var str = '';
for (var i = 0, len = list.length; i < len; i++) {
str += '<div>' + list[i] + '</div>';
}
dom.innerHTML = str;
Example 9.2. 字符串拼接:正确拼接方式,Array的push+join
var str = [];
for (var i = 0, len = list.length; i < len; i++) {
str.puzh('<div>' + list[i] + '</div>');
}
dom.innerHTML = str.join('');
复杂的字符串拼接,应使用格式化的方式,提高代码可读性。请参考baidu.string.format。
charAt方法返回字符串处于position位置的字符。如果position为负值或超出字符串长度,则返回空字符串。
charCodeAt方法返回字符串处于position位置字符的unicode码。如果position为负值或超出字符串长度,则返回NaN。
indexOf方法在字符串内查找子字符串searchString的第一个匹配的位置。无匹配时返回-1。可选参数position可设置从指定位置开始查找。
lastIndexOf方法与indexOf方法类似,但是lastIndexOf方法是从尾部向前查找。
localeCompare用于比较两个字符串,该方法与本机环境相关。如果字符串比otherString小,则返回-1,相等则返回0。
Tip
该方法一般与数组的sort方法结合使用。
match方法对字符串匹配一个正则表达式,返回一个匹配数组。
如果regexp不包含g标识,该方法结果与regexp.exec(string)结果相同,包含第一个匹配和其中的分组。如果regexp包含g标识,该方法结果包含所有的匹配。
replace方法对字符串进行替换,并返回一个新字符串。
Tip
search参数可以是一个字符串或一个正则表达式。只有当search是一个带有g标识的正则表达式时,才会替换字符串中所有匹配项,否则只替换第一个匹配项。
Example 9.11. string.replace:替换第一项与全部替换
'border_left_style'.replace('_', '-'); // 'border-left_style',只替换第一项
'border_left_style'.replace(/_/g, '-'); // 'border-left-style',替换所有项
replaceValue参数可以是一个字符串或一个函数。当replaceValue是一个函数时,将对每一个匹配项进行调用,将返回的文本替换匹配文本。
search方法在字符串内查找regexp的第一个匹配的位置。和indexOf不同的是:
-
它只接受正则表达式。
-
它不接受position参数。
slice方法截取字符串的一部分构成新的字符串,从start到end(不包括end)。
start参数为负值时,将与字符串的length相加,如果还是负数则为0。
end参数可选,默认为字符串长度。如果指定一个end,处理规则和start参数一样。
Example 9.14. string.slice
'Hello world!'.slice(0, 5); // Hello
'Hello world!'.slice(-6, -1); // world
Caution
不要使用substring来截取字符串,它的功能和slice完全一样,只是不支持负数作为参数。
split方法将字符串分割成数组,separator可以是一个字符串,或是一个正则表达式。
limit参数可以限定分割的数量,这个参数不常用。
Caution
IE下,如果split结果的第一项或最后一项是空字符串,会被直接省略掉。
substr与slice一样都可以用来截取字符串。不同的是substr第二个参数指定的是要截取的字符串长度。
Tip
在某些环境下,substr方法的start参数不支持负数,负数时会默认成0。
toLowerCase方法将字符串所有字母转换为小写格式。
Table of Contents
数组的复制可以使用slice方法。
使用slice方法是浅复制。如果数组中包含对象,则相应索引的对象引用是同一个对象。如果需要深复制请参考:baidu.object.clone
Example 10.1. 数组的浅复制
[1, 2, 3].slice(0); // [1, 2, 3]
var arr = [{}]
arr.slice(0)[0] == a[0]; // true
Important
避免使用Array.apply的方法复制数组。当只有一个参数时,会被当作初始化的数组长度,导致不期望的结果。
删除数组项使用splice方法。
数组遍历时,应先将数组的length保存到临时变量中,避免在循环的每一步都读取数组的length。
Tip
如果能保证数组所有项的ToBoolean不为false,可以使用for (var i = 0, obj; obj = list[i]; i++)进行遍历。这种遍历方式对性能有提高。
在javascript中,可以使用sort方法对数组进行排序。
在基于比较的排序情况下,不建议自己写排序。因为使用sort排序性能并不比自己写快速排序差,甚至更好。
sort方法默认会将数组每一项转换为字符串,如果要按数值排序,需要传递一个比较函数。
concat方法返回一个新数组,并将参数附加在数组后面。如果参数是一个数组,则数组中的每一项会被分别添加。
join方法把数组构造成一个字符串,并以分隔符分隔。默认的分隔符为“,”。
pop方法会移除数组最后一项,并返回该项。
push方法向数组尾部添加一个或多个项,并返回操作后的数组长度。
Tip
push与pop配合可使数组像stack一样工作。
push与concat有一些不同:
-
push会修改当前数组。
-
如果参数是一个数组,push会把参数作为一项添加到当前数组中。
shift方法会移除数组第一项,并返回该项。
slice返回数组的浅复制。索引从start开始到end(不包括end)。
如果start或end是负数,解析器会试图把他们与数组的length相加,使其成为非负数。
sort方法可以对数组进行排序。默认会按照字符串的方式进行排序。
我们可以传递一个比较函数。该函数对数组中的两项进行比较,相等时返回0,如果想要第二个参数排在前面,则返回一个正数。
splice方法最大的作用是移除数组的项。其还能在删除项的位置插入新的item。
splice方法会以一个新的数组返回删除的项。
Table of Contents
通常我们使用静态类封装同一类型或同一业务相关的功能。静态类在javascript里使用频率远高于可实例化的类。静态类在javascript里表现为Object。
Example 11.1. 声明静态类
var Util = {
formatDate: function (date) {
},
encodeHTML: function (source) {
}
};
使用function执行并返回,可以达到变量私有的效果。
Tip
该方法可以提高Javascript代码压缩的效果。因为局部变量是可以被压缩的,而全局变量以及对象成员是无法被压缩的。
Example 11.2. 声明静态类:通过闭包
var Util = function () {
var format = "yyyy-MM-dd"; // 私有变量,不对外暴露
return {
formatDate: function (date) {
},
encodeHTML: function (source) {
}
};
}();
Caution
如果返回对象是function或包含对function的引用,则当前执行的scope不会被释放。使用该方法请遵循闭包原则。
在继承的实现上,通常我们利用javascript的语言特性,使用原型继承。
原型继承的优点是:
-
做类型判断的时候,dog is a Animal。这点如果使用属性复制法,则无法实现。
-
多实例使用一个原型对象,减少内存开销。
原型继承的缺点是:
-
由于子类原型是父类的实例,所以实例化的时候需要关注构造函数的参数。
-
子类构造函数和方法需要指定父类的名称来进行super的调用,在子类编写的过程中都需要关心父类的名称。如SuperClass.call或SuperClass.prototype.method.call。
Example 11.3. 原型继承
function Animal(name) {
this.name = name;
}
Animal.prototype = {
jump: function () {
alert('animal ' + this.name + ' jump');
}
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = new Animal();
Dog.prototype.jump = function () {
alert('dog ' + this.name + ' jump');
};
由于原型对象的成员被所有实例共享,所以编码是我们应该遵守这样的原则:原型对象只包含程序不会修改的成员,如方法函数或配置项。
下面是一个简单的风险例子:
Example 11.4. 原型继承的风险
function ListBase() {
this.container = [];
}
ListBase.prototype = {
push: function (item) {
this.container.push(item);
},
alert: function () {
alert(this.container);
}
};
function List() {
}
List.prototype = new ListBase();
var list1 = new List(),
list2 = new List();
list1.push(1);
list2.push(2);
list2.alert(); // 1,2
Table of Contents
由于dom对象不是javascript原生对象,所以我们在使用dom对象的时候,可能会面临这样的麻烦:性能、浏览器兼容性、内存泄露等。
通常,我们使用document.getElementById来获取dom元素,避免使用document.all。document.getElementById是标准方法,兼容所有浏览器。
Caution
ie浏览器会混淆元素的id和name属性,document.getElementById可能获得不期望的元素。在对元素的id与name属性的命名需要非常小心,应使用不同的命名法。下面是一个name与id冲突的例子:
-
getElementsByTagName
getElementsByTagName(tagName)方法可以根据标签名获得元素下的子元素,包含非直接附属的子元素。我们可以指定tagName参数为'*'来获得元素的所有子元素。
Caution
该方法的结果并不直接引用dom元素,而是对索引进行读取,所以dom结构的改变会实时反映到结果中。使用了getElementsByTagName方法需要小心dom操作的时序性。
Example 12.2. dom操作实时反映到结果集合中
<body>
<div></div>
<span></span>
<script>
var elements = document.body.getElementsByTagName('*');
alert(elements[0].tagName); // div
var div = elements[0],
u = document.createElement('u');
document.body.insertBefore(u, div);
alert(elements[0].tagName); // u,实时反应到结果中
</script>
</body>
-
childNodes
childNodes属性可以获取控件的直接子节点集合。集合中包括文本、注释、属性等类型的节点。
-
children
children属性可以获取控件的直接子元素集合。集合中只包含html元素。
Caution
dom结构的改变也会实时反映到结果中。
样式读取的陷阱很多。比如通过class具有的样式无法被直接读取,还有对颜色的读取在不同浏览器下的表示方式存在差异,等等。所以我们应该遵循如下建议:
-
不以样式判断状态,也不建议以className判断状态。状态应被单独管理。这样可以减少读取样式的可能性,避免差异发生。
-
如果非要读取元素实时样式,使用tangram的baidu.dom.getStyle方法。
通过设置元素的className属性可以为元素设置class。该方式兼容所有浏览器。
Caution
避免使用setAttribute方法设置元素的class。
通过style可以给元素设置样式。通常css样式对应的设置名称为:去掉单词连接的中划线,并使用驼峰命名法。如:margin-left -> marginLeft。
通常我们认为style.display = ''可以设置一个元素为显示。但是当元素具有的class中包含display:none的定义时,必须设置为block才能显示该元素。这对我们带来了困扰。
我们可以通过下面的方法解决:
-
不将display:none定义在class中,而定义在元素的style属性中。display作为可变的样式定义,不应该被定义在元素的显示样式规则中。
-
使用一个额外的class(如myclass-hide)来控制显示隐藏,在这个class中定义display:none,需要隐藏时让元素多具有一个class。
Important
避免通过获取元素的当前display样式来进行显示隐藏。获取当前样式会导致性能问题。我们应该规划管理我们的样式。
提高dom操作的性能有一个主要原则:减少dom操作,减少浏览器的reflow。
举一个简单的例子:构建一个列表。我们可以用两种方式:
-
在循环体中createElement并append到父元素中。
-
在循环体中拼接html字符串,循环结束后为父元素赋innerHTML。
第一种方法看起来比较标准,但是每次循环都会对dom进行操作,性能极低。在这里推荐使用第二种方法。
下面一些场景会触发浏览器的reflow:
-
DOM元素的添加、修改(内容)、删除。
-
应用新的样式或者修改任何影响元素布局的属性。
-
Resize浏览器窗口、滚动页面。
-
读取元素的某些属性(offsetLeft、offsetTop、offsetHeight、offsetWidth、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()、currentStyle(in IE)) 。
下面的例子是一个频繁触发reflow的例子:
Example 12.7. 一个频繁触发reflow的例子
var parent = document.getElementById('parent'),
elements = parent.getElementsByTagName('li'),
len = elements.length,
i;
for (i = 0, ; i < len; i++) {
elements[i].style.width = parent.offsetWidth + 'px';
}
在这个例子中,循环的每一步都读取了parent元素的offsetWidth,所以触发reflow进行重新渲染了len次。这个例子中,更好的做法是读取一次,较少reflow。
Example 12.8. 一个频繁触发reflow的例子的改进
var parent = document.getElementById('parent'),
elements = parent.getElementsByTagName('li'),
width = parent.offsetWidth,
len = elements.length,
i;
for (i = 0, ; i < len; i++) {
elements[i].style.width = width + 'px';
}
Table of Contents
IE与标准浏览器在事件处理上有很明显的差异,这些差异为浏览器端开发带来了很大的麻烦:
-
EventArgument
IE通过window.event获取EventArgument;标准浏览器通过监听器的第一个参数获取EventArgument。
EventArgument对象本身也存在差异性,最典型的是IE通过srcElement属性获得触发事件的元素,标准浏览器则是target属性。
-
事件触发
IE的事件触发为冒泡模型;标准浏览器的触发为捕获-冒泡模型。
-
监听方法
ie使用attachEvent方法添加监听器;标准浏览器使用addEventListener方法添加监听器。
通常,为一个dom元素添加事件处理,可以使用3种方法:
-
标签内联
这种方法最简单,但是在html中书写行为,违背了结构-表现-行为分离的原则。
-
expando属性
这种方法比较通用,但是如果为一个元素挂载多个event handler的话,后面的会覆盖前面。
-
添加监听器
这种方法比较流行,但是浏览器存在差异,并可能带来性能问题。
Important
标准的addEventListener提供了两种时间触发模型:冒泡和捕获。可以通过第三个参数指定。IE的attachEvent支持冒泡的事件触发。所以为了保持一致性,通常addEventListener的第三个参数都为false。
事件类型中,标准浏览器不需要添加“on”,如“click”;IE需要添加为“onclick”。
IE下使用attachEvent挂载监听器,当事件被触发时,函数的指针指向window,而不是触发事件的元素本身。
Example 13.3. 添加事件处理:添加监听器
function listener(e) {}
if (baidu.ie) {
dom.attachEvent('onclick', listener);
} else {
dom.addEventListener('click', listener, false);
}
Tip
为了屏蔽浏览器兼容性,我们可以使用baidu.event.on
-
拼接html时,使用标签内联的方法添加事件。
有时候我们需要用javascript拼接html字符串。这个时候我们面对的不是dom对象,可以使用标签内联的方式添加事件。标签内联的方法可以不用关心事件的释放。
Tip
标签内联的方式使得我们丧失了对事件控制的权利。我们没有办法获得触发事件的target,没有办法停止事件冒泡,没有办法停止默认行为。我们唯一能做的就是使用this把当前dom元素传递给处理函数。
-
运行环境可控时,使用expando属性的方法添加事件。
这个时候,javascript运行的页面环境全部是由我们控制的,我们不担心会有其他的javascript脚本覆盖我们挂载的event handler。
对于大多数项目,使用这种方法代替流行的addEventListener,因为该方式会获得更高的性能(如document.body.onmousemove的拖拽处理)
-
对于页面级别的事件管理,使用添加监听器的方法。
有时我们需要在点击页面时关闭所有的浮动层。类似的情况向body添加监听器更容易实现,相互之间无影响。
Caution
有的时候一个项目由多人开发,大家为了不覆盖相互对同一个元素的事件处理而添加监听器。但这种情况大多是因为对交互的分析、事件的管理没有很好的规划。
-
针对第三方环境时,使用添加监听器的方法。
添加监听器的方法能够不覆盖之前对响应元素添加的监听器。不过监听器之间的处理顺序无法保证。
开发人员需要持有监听器函数的引用,在适当时候(元素释放、页面卸载等)卸载添加的监听器。
Table of Contents
通常我们在body标签结束之前加载外部js。在这个位置加载js的好处是:
-
让用户预先下载可见的html视图,避免下载js的阻断导致用户面对空白页面等待。
-
保证页面的dom结构已经加载完成,操作dom可以不用顾忌或判断是否加载完毕。
Example 14.1. 加载外部js
<body>
<div class="wrap">...</div>
<script type="text/javascript" src="my.js"></script>
</body>
Important
script标签中必选的属性为type与src。
下面的一些属性虽然常用,但已不推荐:
-
defer属性为ie only的属性,能延后脚本的执行时间,不推荐。
-
language属性已被html5标准删除,不推荐。
-
charset属性可以通过脚本文件本身编码来解决,不推荐。
通常,一个项目中我们可能设计公共模块与业务模块划分,开发时我们会划分模块,并分成多个javascript文件。
关于模块划分,有如下建议:
-
公共模块以一个namespace暴露。可参考tangram。
-
业务模块是独立的业务单元,以一个namespace来暴露。模块中包括模块公共部分(语言、公共函数等),以及各个业务功能。
-
charset属性可以通过脚本文件本身编码来解决,不推荐。
关于文件划分:开发时,我们可以使用一个js文件来装载要用到的javascript文件(如build.js)。这样做的好处是,在提测前构建的时候,无需修改相应的html或模板。
Example 14.2. 开发时外部js装载
document.write('<script type="text/javascript" src="/src/UIBase.js"></script>');
document.write('<script type="text/javascript" src="/src/UIManager.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/MonthView.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/Calendar.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/Link.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/Button.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/TextInput.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/BaseBox.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/CheckBox.js"></script>');
document.write('<script type="text/javascript" src="/src/ui/RadioBox.js"></script>');
// ......
Important
切勿以document.write输出静态js引用的方式上线!在IE下,document.write无法保证脚本加载的时序性。经过测试,我们认为对于同域的静态资源,时序型可以保证,所以开发时可以采用这种方式。
提测时,我们需要一些脚本来自动将这些文件的具体内容打包到这个文件中,并且进行压缩。这个过程避免手工。想想,如果手工合并文件,是一件多麻烦的事情,在每个提测前可能都要干一遍......
我们可以使用shell、ant、wscript、python等脚本进行合并。下面是一个简单的shell构建脚本的例子,这个例子中build.js会被打包压缩成build-c.js,为了避免文件被覆盖,需要手工备份build.js,mv build-c.js。当然我们也可以不备份而从svn中恢复,但那样我们需要解决svn冲突。
下面是一个简单的自动构建的shell脚本示例:
Table of Contents
有时候,我们开发的javascript是为了给第三方页面提供服务的(广告、开放api...),我们的js会被运行在各种各样的页面环境中。相比环境完全可控的js开发,我们需要关注一些额外的东西:
由于环境的未知性,所有暴露的全局变量都可能产生危险。我们应该使用function隔离作用域。
对于提供api而必须暴露的全局变量,首先减少暴露的个数,以1个为宜。通过挂载到window的property的方式暴露。
第三方页面使用的字符集编码是无法确定的,可能是gbk、utf-8等等。外联的js如果编码与页面编码不一致,可能导致问题。
解决办法:将ascii大于127的字符(如中文字符),使用unicode进行转码,保证代码中不包含ascii大于127的字符。
Caution
unicode转码能带来字符编码的安全性。但是对于脚本执行环境编码可控的页面,不建议进行unicode转码。因为会给js文件增加额外的字节大小。
在第三方页面中创建dom元素,有两种可能:与用户页面本身dom结构有关或无关。
-
与用户页面dom结构有关的情况,比如需要在引入脚本的位置创建元素时,使用document.write:
-
与用户页面dom结构无关的情况,比如绘制一个提供服务的浮动层时,应在用户页面load完成后,使用createElement创建:
Caution
由于window.onload事件的触发时机是页面完全加载(包括图片等资源),所以建议使用DOMContentLoaded事件,在dom树在页面中构建完成后即可创建元素。该事件非所有浏览器都支持,详细方案见baidu.dom.ready。
Caution
绝对定位的元素应创建于body标签下。切忌创建在未知的当前区域,有可能因为处于表格内部或其他绝对定位元素内,导致定位错误。