本文是关于JavaScript的一些常用Tips。
上面的代码中,等号两端的空白字符是可以去掉的,但我们需要保留它们,不至于让that=this看起来像一个token。
在特定的位置加上空格有助于代码的可读性,以下位置必须加上空格:
+
, -
等等{
前,包括if
、else
、try
、finally
这些关键字之后,以及函数定义的参数列表之后for
、switch
、while
{ ... }
)的每个属性名的冒号:
后,
后{ ... }
)左大括号{
后和右大括号}
前更具体点来说,基本代码空白规则如下:
使用2个空格字符作为代码缩进,避免使用tab空白字符,因为tab空白字符在不同编辑器下可能会有不同的表现,使用空格字符作为缩进能很好的避免这种情况。主流编辑器都能够设置自动对tab进行替换,以方便代码编写。
每行的字符数建议不超过80个字符。 过长的程序行应在适当的地方断行,并保持断行后的代码整齐,同时断行不能破坏表达式本身的意义。
不得出现以下情况:
这种方法会导致语义上的分裂,即条件和分支在一行,另一分支在一行,没有按逻辑进行组织。
对于if
、while
、for
、do
,应用{}
将执行体包围,避免产生理解障碍。
主流的编辑器,以回车新起一行时,输入光标通常保留在上一行的缩进位置,所以编写时容易造成这样的情况:
thisIsAnApple
ThisIsAnApple
this_is_an_apple
this-is-an-apple
根据不同类型的内容,采用不同的命名法:
_
开头IS_DEBUG_ENABLED
命名同时还需要关注语义,如:
目前主流浏览器中,JavaScript中无法使用const
(ES6已经支持const)声明常量。如果要声明一个在程序运行阶段不会更改的变量(如配置项),命名规则为:每个字母大写,单词间以下划线分割。
局部变量与全局变量的命名均使用Camel命名法。
JavaScript中,函数可以作为构造器用来实例化对象,也可以被调用。为了增加代码可读性,需要在命名上进行区分。
new
来实例化对象的函数构造器,使用Pascal命名法JavaScript已经提供了许多内置的函数构造器,如Object
, RegExp
等等。
对象的命名规则与对象本身的职责有关。
你必须为你的代码写注释,尽管除了你其他人不太可能去看你的代码。对于工程师来说,注释是一种编程意识,同时也是一种责任。 当你写代码时,可能你会觉得这段代码的意思是很明显的,但是当你过一段时间回过头来看这段代码时,你得花可能比写这段代码更多的时间去回想起它是做什么的。
当然也这也不是让你去为每个变量,每一个表达式都加上注释,但是,至少你也得为每个函数添加注释,让看代码的人知道这个函数是做什么的,它需要哪些类型的函数,而它的返回值类型又是什么,它里面又用到了哪些很Trick的技巧或是技术。
对于注释,最重要的也是最难坚持的习惯是保持注释最新,因为过时的注释会误导你,有时比没有注释还更糟糕。
在JavaScript文件顶部需要有文件描述的注释,其中包括项目名、作者、修改日期等信息。
但是有一点需要注意,文件描述通常不用于生成API文档的。
函数和方法必须用注释描述其功能。
我们描述一个函数的时候,只描述做什么(What),不描述具体如何做(How)。
常用的注释标签包括param
、return
、private
、extend
等。下表是JSDoc支持的部分标签.
标签 | 说明 |
---|---|
@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 | 描述版本 |
当算法或业务逻辑代码不容易被阅读时,我们可以用单行或多行注释进行内部描述。
按执行频率排列分支的顺序。这样做有两个好处:
在JavaScript中, false
, null
, 0
, ''
, undefined
和NaN
都被当做false
值。
而在执行==
时,则会有一些让人难以理解的陷阱。
==
的Tricks对于不同类型的==
判断,有这样一些规则,顺序自上而下:
x
和y
的类型相同,则使用===
算法比对x
为null
且y
为undefined
,则返回true
,反之亦然Number
类型,另外一个是String
类型,那么需要先把String
类型的转换为Number
,然后再执行==
Boolean
类型,另外一个是非Boolean
类型,那么需要先把Boolean
类型转换为Number
类型,然后再执行==
String
或是Number
类型,另外一个是Object
类型,那么需要先把Object
类型转换为primitive的类型,然后再执行==
false
想了解更多,请参考JavaScript tricks 2: Use === instead of ==[3]。
case
代替if
在同时满足下面的条件时,建议使用case
代替if
:
下面是一个简单的小例子。
case
代替if
例子switch
模式在写switch
语句时,你可以遵循该模式从而改进代码的可读性和鲁棒性。如下面的代码:
case
与switch
对齐case
内的代码case
语句以break
结尾,避免fall-through,如果你确实不需要某个break
,那么必须添加注释说明,否则别人会认为这是一个错误default
结尾,从而保证始终会有一个值返回,即使所有的条件都不满足在JavaScript中,分为primitive和non-primitive值。一共有5种primitive类型,分别表示不同的primitive值。
NaN
和Infinity
所有其它的值都是non-primitive的,也就是Object
类型,包括:Object、Function、Array、String、Boolean、Number、Math、Date、RegExp、Error。
typeof
我们可以通过typeof
操作符检测某个值的类型:
typeof null
是不应该返回'object'
的,因为它并不是一个对象。 这是自从JavaScript第一版中就已经存在的一个错误,但是已经为时已晚,现在已经不能去修复它了,因为它会造成breaking change。更理想情况下是返回'null'
。
instanceof
我们可以用instanceof来检测对象的构造器。
如果它返回true
,那么它就是由这个Constructor
所创建的一个对象实例。
对于value instanceof Constructor
这样的表达式,如果Constructor.prototype
在value
的原型(proto)链里,那么就返回true
。
但是,由于instanceof遍历原型链的特性,可能产生如下问题:
上面的例子中,instanceof
认为a
不是A
的实例,因为A
的prototype已经被更改。
实例化对象前,应完成类的prototype成员构建,否则可能导致不期望的判断结果。
我们需要判断一个变量的类型的时候,需要考虑很多的可能性。
一个例子:判断变量a
是否字符串。
最简单的:
可能a是字符串的包装类型,于是:
可是a
为null
或undefined
时会报runtime error,于是:
如果我们把String
构造函数覆盖了,如function String(){}
,会判断出错,于是:
如果Object.prototype.toString
再被覆盖了……这个时候,我们就没办法了。
JavaScript被许多其它编程语言影响过,其中之一就是Java,是Java让JavaScript也有了简单类型和包装类型。
function
关键词onclick
内联事件模型那么,问题来了:
如果一个原生String类型没有属性,那么为什么`‘foo’.length可以正确执行返回字符串的长度呢?
那是因为JavaScript会自动地进行隐式装箱操作,也就是把原生类型转换为与它对应的包装类型。
在上面这个例子中,字符串值foo
会转换为String对象,从而让你可以访问length
属性。
对于布尔、数值与字符串来说,应针对简单类型进行编程,包装类型的方法会自动通过隐式装箱调用。
将其它值转换为字符串,最常见的方法就是使用+运算符,因为+运算符中如果其中一个操作数是字符串,那么它就会执行字符串拼接。 所以,想要得到类型转换后的字符串值,最简单的方法就是将该值与一个空字符串拼接。如:
另外一个可选的方法就是将String构造函数作为函数调用,即将待转换值作为参数传入它。
'' + colValue
做类型转换:数值# | -3.14 | -0 | +0 | 1 | 3.14 | 314e-2 | -Infinity | +Infinity | NaN |
---|---|---|---|---|---|---|---|---|---|
’’ + colValue | ‘-3.14’ | ‘0’ | ‘0’ | ‘1’ | ‘3.14’ | ‘3.14’ | ‘-Infinity’ | ‘+Infinity’ | ‘NaN’ |
'' + colValue
做类型转换:其它值# | undefined | null | true | false | {} | [] | function foo() {} |
---|---|---|---|---|---|---|---|
’’ + colValue | ‘undefined’ | ‘null’ | ‘true’ | ‘false’ | ‘[object Object]’ | ’’ | ‘function foo(){}’ |
对象或是函数需要转换为字符串时,会默认调用它们的toString方法。注意,Object.prototype.toString
和Function.prototype.toString
方法我们是可以任意覆盖的。
将函数转换为字符串的过程我们称之为函数反编译,一般我们可以通过fn.toString()
或String(fn)
或fn + ''
实现,但是最后都是通过Function.prototype.toString
[4]实现的。但是它并不一定会反射函数的源代码,因为它的实现与平台相关,结果并不总是一样的[5],特别是ES6中新出现的arrow function。
字符串到数值的转换,大家首先会想到的最好的方式一般是parseInt和parseFloat,但是他们真的是最好的方式吗?
我们得到的结论:
parseInt
是你最正确的选择parseInt
并不是常常能正确的工作: #todo我们可以使用Math.floor
,Math.round
,Math.ceil
,又或是ToInteger
方法。
使用对象直接量{}
初始化对象,不使用new Object
。
对象的最后一个成员后不要添加,号,否则在IE下会报语法错误。
类似的,初始化Array
或是RegExp
时,也应使用直接量。
Array
与RegExp
初始化我们都知道在JavaScript中提供了二种方式定义正则表达式,一个是通过RegExp
构造函数,另一个通过直接量。
RegExp
构造函数产生的对象是在runtime时编译的,但是直接量却是在脚本加载时就已经编译了,所以它的性能相对会更好[6]。从另一方面来看,字面量更适合已经知道表达式构成时,而通过构造函数更适合动态构建正则表达式,如通过用户的输入。
对象成员访问有两种方式:
如果成员名称为JavaScript保留字,则可以通过obj[expression]
方式访问。其他时候,通过obj.identifier
访问,该方式增加可读性,并且减少字符数。
delete
[7]关键字可以删除对象的成员。
禁止扩展原生对象,如
Object
、String
、Function
等。
污染原生对象可能会带来冲突,比如两个开发者或者使用的两个库都扩展了String
的format
函数,则代码字面上后面的会覆盖前面的,导致不可预料的错误。
污染原生对象可能导致不期望的后果,如扩展了Object
,则对任何一个对象的for...in
都会遍历出这个扩展。
我们通过Object.getOwnPropertyDescriptor
可以看到,foo
的enumerable
属性是为true
的,所以在for in
遍历时我们会看到它。
JavaScript是支持First-class functions的,也就是说函数是一等公民。
所谓第一等公民(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
函数可以以以下4种方式调用:
函数在被调用时,this指针是由调用者所决定的。
this
指针为Globel
,浏览器端为window
,但是如果是在strict mode下访问this
会抛出错误this
指针为方法所属对象。如obj.foo()
,this
指针为obj
。Object
,this
指针为这个Object
,默认这个Object
会被返回apply
或call
时,可以通过第一个参数指定调用时的this
指针与熟知的绝大多数高级语言(c、c++、java…)不同,JavaScript变量的作用域是function域,不是block域。
函数在被调用的时候,会创建一个作用域。作用域可以看做是一个JavaScript对象,其中包括arguments
属性,以及变量,函数声明和与形参同名的属性。
函数对象会引用父函数作用域对象。所以,我们能够通过闭包,让随后执行的函数使用当前父函数运行时的环境(除了this
与arguments
)
父函数作用域对象被子函数引用后,如果子函数包含引用,则父函数的作用域不会被回收。下面是一个作用域泛滥的例子,这个例子额外创建了lis.length个scope,并且他们都不会被释放:
DOM
对象。如果包含循环引用,并引用了非原生对象,可能导致无法释放产生内存泄露如果闭包中需要使用DOM,闭包环境应引用DOM元素id,在需要使用的时候再获取。
在mouseover事件或其他频繁执行的状态,如果多次获取同一个DOM对象会导致效率问题,可以在第一次持有DOM对象的引用,在使用完后释放。
字符串处理是前端最常见的操作,所以它很有可能成为性能瓶颈。
字符串可以使用正则也可以使用String.prototype.indexOf
、String.prototype.search
,从这个测试可以看出,这里的性能问题有很多种情况,对于我们关注的IE6而言,indexOf字符串和search正则性能是相同的,不够search字符串要比正则快,有可能是构建正则的开销导致,除了IE6、IE7,其它JS引擎的正则都不会慢于字符串,甚至快很多。
在v8中的indexOf会根据待匹配的字符串特点来选择不同的算法,对于小于7个字符会使用线性查找,而大于等于7个字符则使用Boyer-Moore-Horspool。
字符串拼接是一个相当消耗资源的操作,但是最好的做法是什么呢?过去常见的性能优化方法中都会提到使用数组push+join的方式来取代字符串的拼接,但是实际上是这样的么? 如果我们对拼接后的字符串不做任何操作,那么最新版的Chrome(40.0.2214.111 (64-bit))和Firefox(35.0.1)结果是一样的:+=操作符比+, String.prototype.concat和Array.prototype.join都快,并且Array.prototype.join是最慢的[[8]](#[8]]。
但是,如果我们对拼接后的字符串进行操作,结果是不一样的[9]。
v8中的实现类似Java的StringBuilder,内部也是用数组来实现的,但做了特殊优化,所以性能比JS数组快。 Firefox的方法是对于短字符串使用memcpy,长字符串使用Rope数据结构。
总的来说,字符串拼接,应使用数组作为StringBuffer保存字符串片段,使用时调用join方法,尽量避免使用+或+=的方式拼接较长的字符串,每个字符串都会使用一个小的内存片段,过多的内存片段会影响性能。
绝大部分现代浏览器都对+=的字符串拼接进行了优化,但在IE6和IE7中字符串拼接远远慢于push+join,同时IE6的使用率依然很高。
复杂的字符串拼接,应使用格式化的方式,提高代码可读性。
更多的可以参考这里。
charAt
方法返回字符串处于position位置的字符。如果position为负值或超出字符串长度,则返回空字符串。
charCodeAt
方法返回字符串处于position位置字符的Unicode编码。如果position为负值或超出字符串长度,则返回NaN
。
indexOf
方法在字符串内查找子字符串searchString的第一个匹配的位置,无匹配时返回-1,可选参数position可设置从指定位置开始查找。
一般情况下,如果我们不需要searchString的索引值,那么我们也可以使用RegExp.prototype.test
来检索字符串中指定的值,它返回true
或false
。
JSPerf: RegExp.prototype.test & String.prototype.indexOf: indexOf
更快,可能是因为省去了构建正则表达式的开销。
lastIndexOf()
方法与indexOf()
方法类似,但是lastIndexOf()
方法是从尾部向前查找。
localeCompare
用本地特定的顺序来比较两个字符串,该方法与本机环境相关。如果字符串比otherString小,则返回小于0的数,则该方法返回大于 0 的数。如果两个字符串相等,或根据本地排序规则没有区别,该方法返回0。
该方法一般与数组的
sort()
方法结合使用,localeCompare()
方法提供的比较字符串的方法,考虑了默认的本地排序规则。**ECMAscript **标准并没有规定如何进行本地特定的比较操作,它只规定该函数采用底层操作系统提供的排序规则。
match()
方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。
该方法类似indexOf()
和lastIndexOf()
,但是它返回指定的值,而不是字符串的位置。
在全局检索模式下,
match()
既不提供与子表达式匹配的文本的信息,也不声明每个匹配子串的位置。如果您需要这些全局检索的信息,可以使用RegExp.exec()
。
如果regexp没有标志g,那么match()
方法就只能在stringObject中执行一次匹配。如果没有找到任何匹配的文本,match()
将返回null
。否则,它将返回一个数组,其中存放了与它找到的匹配文本有关的信息。该数组的第0个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的子表达式匹配的文本。除了这些常规的数组元素之外,返回的数组还含有两个对象属性。index属性声明的是匹配文本的起始字符在stringObject中的位置,input属性声明的是对stringObject的引用。
让我们来比较与RegExp相关的几个字符串方法(match
, search
, exec
, test
)的性能:JSPerf: RegExp方法。
RegExp.prototype.exec
比String.prototype.match
快很多,但是那是因为他们的用途根本就不一样。在搜索子串时,RegExp.prototype.test
相对更快,也许是因为它并不需要返回匹配子串的索引吧。
replace()
方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串,并返回一个新字符串。
replaceValue可以是字符串,也可以是函数。如果它是字符串,那么每个匹配都将由字符串替换。但是replaceValue中的$字符具有特定的含义[[10]],它说明从模式匹配得到的字符串将用于替换。
search()
方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,并返回stringObject中第一个与*regexp *相匹配的子串的起始位置。和stringObject.indexOf
不同的是:
性能测试:String.prototype.search vs String.prototype.indexOf
很显示,indexOf
肯定是最快,其次是search(string)
,最后是search(regexp)
slice()
方法提取字符串的片断(从start到end,但不包括end),并在新的字符串中返回被提取的部分。
start参数为负值时,将与字符串的length相加,如果还是负数则为0。end参数可选,默认为字符串长度。如果指定一个end,处理规则和start参数一样。
不要使用substring来截取字符串,它的功能和slice完全一样,只是不支持负数作为参数。
split()
方法将字符串分割成数组,separator可以是一个字符串,或是一个正则表达式。limit参数可以限定分割的数量,这个参数不常用。
IE下,如果split结果的第一项或最后一项是空字符串,会被直接省略掉。
substr
与slice
一样都可以用来截取字符串。不同的是substr
第二个参数指定的是要截取的字符串长度。
####### string.substr
toLowerCase()
方法将字符串转换为小写。
toUpperCase()
方法将字符串转换为大写。
fromCharCode()
方法可以从一串参数中返回一个字符串,每个参数对应的是相应位置的Unicode编码。
####### String.formCharCode
数组的复制可以使用slice()
方法,但是使用slice()
方法是浅复制。如果数组中包含对象,则相应索引的对象引用是同一个对象[[11]]。
我们可以利用Array.apply
消除数组中的hole。虽然hole在遍历数组或是判断使用in
判断某个元素是否在数组中时都起作用,但是当你想输出hole的值时,却是undefined
。
避免使用Array.apply的方法复制数组。当只有一个参数时,会被当作初始化的数组长度,导致不期望的结果。
删除数组项可以使用splice()
方法。
我们可以设置数组的length为0来清除数组的所有项。
当length被设置为小于当前长度时,下标大于或等于设置长度的项会被删除。
数组遍历时,应先将数组的length保存到临时变量中,避免在循环的每一步都读取数组的length。
如果能保证数组所有项的ToBoolean不为false,可以使用
for (var i = 0, obj; obj = list[i]; i++)
进行遍历,这种遍历方式对性能有提高。
当遍历的顺序无关时,建议使用倒序遍历。
在JavaScript中,可以使用sort()
方法对数组进行排序。
在基于比较的排序情况下,不建议自己写排序。因为使用sort排序性能并不比自己写快速排序差,甚至更好。
sort()
方法默认会将数组每一项转换为字符串,如果要按数值排序,需要传递一个比较函数。
使用字符串的localeCompare()
方法,可以对中文进行排序。localeCompare()
方法的返回值类似于sort方法比较函数的约定,并且它是用本地特定的顺序来比较两个字符串,所以它依赖于本地环境。
更多的可以参考这里。
concat()
方法返回一个新数组,并将参数附加在数组后面。如果参数是一个数组,则数组中的每一项会被分别添加。
join()
方法把数组构造成一个字符串,并以分隔符分隔。默认的分隔符为’,’。
pop()
方法会移除数组最后一项,并返回该项。
push()
方法向数组尾部添加一个或多个项,并返回操作后的数组长度。
push与pop配合可使数组像stack一样工作。
push与concat有一些不同:
shift()
方法会移除数组第一项,并返回该项。
slice()
方法会返回数组的浅复制,索引从start开始到end(不包括end)。
如果start或end是负数,解析器会试图把他们与数组的length相加,使其成为非负数。
sort()
方法用于对数组的元素进行排序,默认将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。
请注意,数组在原数组上进行排序,不生成副本。
####### array.sort:默认按字符串排序
另外,我们也可以传递一个比较函数。该函数对数组中的两项进行比较,相等时返回0,如果想要第二个参数排在前面,则返回一个正数。
splice()
方法最大的作用是移除数组的项,同时还能在删除项的位置插入新的项,然后返回被删除的项。
unshift()
方法向数组开始部分添加一个或多个项。
unshift与pop配合可使数组像Queue一样工作。
通常我们使用静态类封装同一类型或同一业务相关的功能。静态类在JavaScript里使用频率远高于可实例化的类。静态类在JavaScript里表现为Object。
使用IIFE(immediately invoked function expression)可以达到变量私有的效果,该方法可以提高JavaScript代码压缩的效果。因为局部变量是可以被压缩的,而全局变量以及对象成员是无法被压缩的。
在继承的实现上,通常我们利用JavaScript的语言特性,使用原型继承。
原型继承的优点是:
原型继承的缺点是:
由于原型对象的成员被所有实例共享,所以编码是我们应该遵守这样的原则:原型对象只包含程序不会修改的成员,如方法函数或配置项。
下面是一个简单的风险例子:
由于DOM对象不是JavaScript原生对象,所以我们在使用DOM对象的时候,可能会面临各种各样的麻烦:性能、浏览器兼容性、内存泄露等。
通常,我们使用document.getElementById()
方法来获取DOM元素,避免使用document.all
。
document.getElementById()方法
是标准方法,兼容所有浏览器。但是,IE浏览器会混淆元素的id和name属性,document.getElementById()
可能获得不期望的元素。在对元素的id与name属性的命名需要非常小心,应使用不同的命名法。下面是一个name与id冲突的例子:
getElementsByTagName(tagName)
方法可以根据标签名获得元素下的子元素,包含非直接附属的子元素。我们可以指定tagName参数为_*_来获得元素的所有子元素。
childNodes
属性可以获取控件的直接子节点集合,但是集合中包括文本、注释、属性等类型的节点。
children
属性可以获取控件的直接子元素集合,但是集合中只包含元素节点,并不包括文本节点等等。
以上方法或属性的结果并不直接引用DOM元素,而是对索引进行读取,所以DOM结构的改变会实时反映到结果中。使用了getElementsByTagName方法需要小心DOM操作的时序性。
浏览器不兼容性的重灾区。
通过style
属性,我们不能读取embeded, linked或者是imported的样式,但是我们又偶尔需要去获取这些非inline样式,Microsoft和W3C提供了不同的方法去访问这些非inline样式 。
通过style
属性可以给元素设置inline样式。
The general rule is that all dashes are removed from the CSS property names, and that the character after a dash becomes uppercase. Thus margin-left becomes marginLeft, text-decoration becomes textDecoration, and border-left-style becomes borderLeftStyle.
许多CSS样式名称里面都会包含-,比如font-size
。在JavaScript
中,-是不用于属性名称中的,所以,一般的原则是去掉所有的-,然后-后面的第一个字符变为大写,如margin-left变为 marginLeft。另外,别忘记添加CSS单位,如px
,因为如果没有它,浏览器会不知道如何解析它,所以它什么都不会做。
HTML
元素的style
属性仅仅提供访问元素的内联样式。
Microsoft是使用currentStyle
属性,它与style
属性几乎一样,除了它支持访问元素的所有样式,并不限于inline样式,因此它是真正反应了作用于元素上的实时样式,另外,它是只读的。
W3C的解决方案是window.getComputedStyle()
方法, 它与currentStyle
差不多,但是它的语法更复杂:
getComputedStyle()
方法始终返回的是计算后的pixel值,尽管它的原始值可能是使用的em或是百分比%。
JavaScript允许你动态地更改元素的class
和id
属性,之后浏览器会自动更新元素的样式。幸运的是该方式兼容所有浏览器。
提高DOM操作的性能有一个主要原则:减少DOM操作,减少浏览器的Reflow。
举一个简单的例子:构建一个列表。我们可以用两种方式:
createElement()
并append到父元素中第一种方法看起来比较标准,但是每次循环都会对DOM进行操作,性能极低。在这里推荐使用第二种方法。
下面一些场景会触发浏览器的reflow:
display:none
),移动元素或是修改元素出现顺序:hover
, :checked
, :first-child
等等在这个例子中,循环的每一步都读取了parent元素的offsetWidth,所以触发Reflow进行重新渲染了len次。这个例子中,更好的做法是读取一次,较少Reflow。
IE与标准浏览器在事件处理上有很明显的差异,这些差异为浏览器端开发带来了很大的麻烦。
IE通过window.event
获取EventArgument;标准浏览器通过监听器的第一个参数获取EventArgument。
EventArgument对象本身也存在差异性,最典型的是IE通过srcElement属性获得触发事件的元素,标准浏览器则是target属性。
IE的事件触发为冒泡模型,而标准浏览器的触发为捕获-冒泡模型。
IE使用attachEvent()
方法添加监听器;标准浏览器使用addEventListener()
方法添加事件监听器(IE9开始支持它)。
通常,为一个DOM元素添加事件处理,可以使用3种方法:
在内联事件模型中,事件处理器是作为HTML元素的属性添加的。如下所示:
如果想了解更多,请参考这里。
在传统事件模型中,事件处理器是通过JavaScript脚本添加或删除的。与内联模型相同的是,每一个事件一次只能绑定一个事件处理器。
如果想了解更多,请参考这里。
标准的addEventListener()
方法提供了两种时间触发模型:冒泡和捕获。可以通过第三个参数指定。IE的attachEvent()方法
只支持冒泡的事件触发。所以为了保持一致性,通常addEventListener()
方法的第三个参数都为false。
事件类型中,标准浏览器不需要添加on,如click;IE需要添加为onclick。
IE通过下使用attachEvent()
挂载事件监听器,当事件被触发时,函数的this指针始终指向window
,而不是触发事件的元素本身。
有时候我们需要用JavaScript拼接html字符串。这个时候我们面对的不是DOM对象,可以使用标签内联的方式添加事件。标签内联的方法可以不用关心事件的释放。
标签内联的方式使得我们丧失了对事件控制的权利。我们没有办法获得触发事件的target,没有办法停止事件冒泡,没有办法停止默认行为。我们唯一能做的就是使用this把当前DOM元素传递给处理函数。
这个时候,JavaScript运行的页面环境全部是由我们控制的,我们不担心会有其他的JavaScript脚本覆盖我们挂载的event handler。
对于大多数项目,使用这种方法代替流行的addEventListener
,因为该方式会获得更高的性能(如document.body.onmousemove的拖拽处理)
有时我们需要在点击页面时关闭所有的浮动层。类似的情况向body添加监听器更容易实现,相互之间无影响。
对于元素级别事件挂载:有的时候一个项目由多人开发,大家为了不覆盖相互对同一个元素的事件处理而添加监听器。但这种情况大多是因为对交互的分析、事件的管理没有很好的规划。
添加监听器的方法能够不覆盖之前对响应元素添加的监听器。不过监听器之间的处理顺序无法保证。
开发人员需要持有监听器函数的引用,在适当时候(元素释放、页面卸载等)卸载添加的监听器。
通常我们在body标签结束之前加载外部JavaScript。在这个位置加载JavaScript的好处是:
script标签中必选的属性为type和src*。
下面的一些属性虽然常用,但部分已不推荐:
HTML的<script>标签允许你定义JavaScript代码何时可以开始执行。
这是>script<标签默认的行为,脚本执行的时候HTML的解析会暂停。对于延迟性高的服务器和大型的脚本来说,这也就意味着页面的显示会被推迟。
与正常执行相对,延迟执行也就是会延迟脚本的执行直到HTML的解析完成。这样做的好处是对于你的脚本来说,此时DOM是可以访问了的。然而,并不是所有浏览器都支持它,所以不要单纯地依赖于它。
如果你并不在乎脚本何时会执行,那么异步模式是最好的选择。因为对于异步执行来说,HTML的解析会继续,而脚本一旦下载成功,就会开始解析并执行脚本。Google Analytics就是使用的这种方式。
通常,一个项目中我们可能设计公共模块与业务模块划分,开发时我们会划分模块,并分成多个JavaScript文件。
关于模块划分,有如下建议:
关于文件划分:开发时,我们可以使用一个JavaScript文件来装载要用到的JavaScript文件(如build.js)。这样做的好处是,在提测前构建的时候,无需修改相应的html或模板。
切勿以document.write输出静态js引用的方式上线!在IE下,document.write无法保证脚本加载的时序性。经过测试,我们认为对于同域的静态资源,时序型可以保证,所以开发时可以采用这种方式。
提测时,我们需要一些脚本来自动将这些文件的具体内容打包到这个文件中,并且进行压缩。这个过程避免手工。想想,如果手工合并文件,是一件多麻烦的事情,在每个提测前可能都要干一遍……
我们可以使用shell、ant、wscript、python等脚本进行合并。下面是一个简单的shell构建脚本的例子,这个例子中build.js会被打包压缩成build-c.js,为了避免文件被覆盖,需要手工备份build.js。当然我们也可以不备份而从svn中恢复,但那样我们需要解决svn冲突。
下面是一个简单的自动构建的shell脚本示例:
当然,我们可以会考虑使用现在比较流行的一些build工具,如Grunt,Gulp又或是Webpack。
有时候,我们开发的JavaScript是为了给第三方页面提供服务的(广告、开放api…),我们的js会被运行在各种各样的页面环境中。相比环境完全可控的js开发,我们需要关注一些额外的东西:
由于环境的未知性,所有暴露的全局变量都可能产生危险,我们应该使用function隔离作用域。
对于提供API而必须暴露的全局变量,首先减少暴露的个数,以1个为宜。通过挂载到window的方式暴露。
第三方页面使用的字符集编码是无法确定的,可能是GBK、UTF-8等等。外联的js如果编码与页面编码不一致,可能导致问题。
解决办法:将ASCII大于127的字符(如中文字符),使用Unicode进行转码,保证代码中不包含ASCII大于127的字符。
Unicode转码能带来字符编码的安全性。但是对于脚本执行环境编码可控的页面,不建议进行Unicode转码。因为会给js文件增加额外的字节大小。
在第三方页面中创建DOM元素,有两种可能:与用户页面本身DOM结构有关或无关。
document.write()
:由于window.onload事件的触发时机是页面完全加载(包括图片等资源),所以建议使用DOMContentLoaded事件,在DOM树在页面中构建完成后即可创建元素。该事件非所有浏览器都支持,详细方案见$.ready。
绝对定位的元素应创建于body标签下。切忌创建在未知的当前区域,有可能因为处于表格内部或其他绝对定位元素内,导致定位错误。
===
instead of ==