JavaScript基础知识笔记
0.前言
本文基于ES5
版本的JavaScript
教程,阅读时整理的基于知识点结构的笔记。仅梳理了本人以往不清晰或者与其他编程语言有差异之处或该语言特点。作为一文流笔记以备之后检索使用。
1.lable
JavaScript
语言允许,语句的前面有标签(label)
,相当于定位符,用于跳转到程序的任意位置,标签的格式如下。
1 | label: |
标签可以是任意的标识符,但不能是保留字,语句部分可以是任意语句。
标签通常与break
语句和continue
语句配合使用,跳出特定的循环。
1 | top: |
上面代码为一个双重循环区块,break
命令后面加上了top
标签(注意,top
不用加引号),满足条件时,直接跳出双层循环。如果break
语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。
标签也可以用于跳出代码块。
1 | foo: { |
上面代码执行到break foo
,就会跳出区块。
continue
语句也可以与标签配合使用。
1 | top: |
上面代码中,continue
命令后面有一个标签名,满足条件时,会跳过当前循环,直接进入下一轮外层循环。如果continue
语句后面不使用标签,则只能进入下一轮的内层循环。
2.布尔值
如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false
,其他值都视为true
。
undefined
null
false
0
NaN
""
或''
(空字符串)
注意:空数组([]
)和空对象({}
)对应的布尔值,都是true
。
3.数值
不要拿在
js
中直接做小数的数值运算js
可以使用科学计数法表示数值,但不要用parseInt()
去转化科学计数法表示的值,用praseFloat()
。其他进制表示:(八进制前缀不要只加
0
)- 八进制:有前缀
0o
或0O
的数值,或者有前导0、且只用到0-7
的八个阿拉伯数字的数值。 - 十六进制:有前缀
0x
或0X
的数值。 - 二进制:有前缀
0b
或0B
的数值。
- 八进制:有前缀
了解几个特殊值:(尤其是涉及特殊值的运算,结果要注意)
NaN
+0 /-0
Infinity
常见方法
isFinite
方法返回一个布尔值,表示某个值是否为正常的数值。1
2
3
4
5
6isFinite(Infinity) // false
isFinite(-Infinity) // false
isFinite(NaN) // false
isFinite(undefined) // false
isFinite(null) // true
isFinite(-1) // true除了
Infinity
、-Infinity
、NaN
和undefined
这几个值会返回false
,isFinite
对于其他的数值都会返回true
。
4.字符串
字符串可以被视为字符数组,但通过这种方式:字符串内部的单个字符无法改变和增删,这些操作会默默地失败
1
2
3
4
5
6
7
8
9
10
11
12
13var s = 'hello';
s[0] // "h"
s[1] // "e"
s[4] // "o"
// 直接对字符串使用方括号运算符
'hello'[1] // "e"
delete s[0];
s // "hello"
s[1] = 'a';
s // "hello"JavaScript
原生提供两个 Base64 相关的方法。注意这两个方法不适合非ASCII
码的字符,会报错btoa()
:任意值转为 Base64 编码atob()
:Base64 编码转为原来的值
要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。
1
2
3
4
5
6
7
8
9
10function b64Encode(str) {
return btoa(encodeURIComponent(str));
}
function b64Decode(str) {
return decodeURIComponent(atob(str));
}
b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"
5.对象
- 对象的引用
如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。
1 | var o1 = {}; |
上面代码中,o1
和o2
指向同一个对象,因此为其中任何一个变量添加属性,另一个变量都可以读写该属性。
此时,如果取消某一个变量对于原对象的引用,不会影响到另一个变量。
1 | var o1 = {}; |
上面代码中,o1
和o2
指向同一个对象,然后o1
的值变为1,这时不会对o2
产生影响,o2
还是指向原来的那个对象。
对象属性的操作
- 查看一个对象本身的所有属性,可以使用
Object.keys
方法。
1 | var obj = { |
delete
命令用于删除对象的属性,删除成功后返回true
。
1 | var obj = { p: 1 }; |
上面代码中,delete
命令删除对象obj
的p
属性。删除后,再读取p
属性就会返回undefined
,而且Object.keys
方法的返回值也不再包括该属性。
注意,删除一个不存在的属性,delete
不报错,而且返回true
。
1 | var obj = {}; |
上面代码中,对象obj
并没有p
属性,但是delete
命令照样返回true
。因此,不能根据delete
命令的结果,认定某个属性是存在的。
只有一种情况,delete
命令会返回false
,那就是该属性存在,且不得删除。
1 | var obj = Object.defineProperty({}, 'p', { |
上面代码之中,对象obj
的p
属性是不能删除的,所以delete
命令返回false
。
另外,需要注意的是,delete
命令只能删除对象本身的属性,无法删除继承的属性。
1 | var obj = {}; |
上面代码中,toString
是对象obj
继承的属性,虽然delete
命令返回true
,但该属性并没有被删除,依然存在。这个例子还说明,即使delete
返回true
,该属性依然可能读取到值。
in
运算符
in
运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回true
,否则返回false
。它的左边是一个字符串,表示属性名,右边是一个对象。
1 | var obj = { p: 1 }; |
in
运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。就像上面代码中,对象obj
本身并没有toString
属性,但是in
运算符会返回true
,因为这个属性是继承的。
这时,可以使用对象的hasOwnProperty
方法判断一下,是否为对象自身的属性。
1 | var obj = {}; |
6.函数
函数A.name
可以获取函数名,.length
可以获取函数参数个数,toString
返回函数源码,有点反射内味儿了。
函数内部也能定义函数…
1 | function foo() { |
上面代码中,函数foo
内部声明了一个函数bar
,bar
的作用域绑定foo
。当我们在foo
外部取出bar
执行时,变量x
指向的是foo
内部的x
,而不是foo
外部的x
。正是这种机制,构成了下文要讲解的“闭包”现象。
函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)
。这意味着,在函数体内修改参数值,不会影响到函数外部。
但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)
。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
javaScript中的闭包的理解
闭包(closure)
是 JavaScript
语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
理解闭包,首先必须理解变量作用域。前面提到,JavaScript
有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。
1 | var n = 999; |
上面代码中,函数f1
可以读取全局变量n
。
但是,正常情况下,函数外部无法读取函数内部声明的变量。
1 | function f1() { |
上面代码中,函数f1
内部声明的变量n
,函数外是无法读取的。
如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。
1 | function f1() { |
上面代码中,函数f2
就在函数f1
内部,这时f1
内部的所有局部变量,对f2
都是可见的。但是反过来就不行,f2
内部的局部变量,对f1
就是不可见的。这就是 JavaScript 语言特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2
可以读取f1
的局部变量,那么只要把f2
作为返回值,我们不就可以在f1
外部读取它的内部变量了吗!
1 | function f1() { |
上面代码中,函数f1
的返回值就是函数f2
,由于f2
可以读取f1
的内部变量,所以就可以在外部获得f1
的内部变量了。
闭包就是函数f2
,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2
记住了它诞生的环境f1
,所以从f2
可以得到f1
的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。
1 | function createIncrementor(start) { |
上面代码中,start
是函数createIncrementor
的内部变量。通过闭包,start
的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc
使得函数createIncrementor
的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。
为什么闭包能够返回外层函数的内部变量?原因是闭包(上例的inc
)用到了外层变量(start
),导致外层函数(createIncrementor
)不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。
闭包的另一个用处,是封装对象的私有属性和私有方法。
1 | function Person(name) { |
上面代码中,函数Person
的内部变量_age
,通过闭包getAge
和setAge
,变成了返回对象p1
的私有变量。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
eval
命令接受一个字符串作为参数,并将这个字符串当作语句执行。
总之,eval
的本质是在当前作用域之中,注入代码。由于安全风险和不利于 JavaScript 引擎优化执行速度,一般不推荐使用。通常情况下,eval
最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的JSON.parse
方法。
7.数组
length
属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员数量会自动减少到length
设置的值。
清空数组的一个有效方法,就是将length
属性设为0。
1 | var arr = [ 'a', 'b', 'c' ]; |
如果人为设置length
大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空位 undefined。
值得注意的是,由于数组本质上是一种对象,所以可以为数组添加属性,但是这不影响length
属性的值。
1 | var a = []; |
检查某个键名是否存在的运算符in
,适用于对象,也适用于数组。
1 | var arr = [ 'a', 'b', 'c' ]; |
上面代码表明,数组存在键名为2
的键。由于键名都是字符串,所以数值2
会自动转成字符串。
for...in
不仅会遍历数组所有的数字键,还会遍历非数字键。
所以,不推荐使用for...in
遍历数组。
数组的遍历可以考虑使用普通for
循环或while
循环。
使用delete
命令删除一个数组成员,会形成空位,并且不会影响length
属性。
1 | var a = [1, 2, 3]; |
上面代码用delete
命令删除了数组的第二个元素,这个位置就形成了空位,但是对length
属性没有影响。也就是说,length
属性不过滤空位。所以,使用length
属性进行数组遍历,一定要非常小心。
数组的某个位置是空位,与某个位置是undefined
,是不一样的。如果是空位,使用数组的forEach
方法、for...in
结构、以及Object.keys
方法进行遍历,空位都会被跳过。如果某个位置是undefined
,遍历的时候就不会被跳过。这就是说,空位就是数组没有这个元素,所以不会被遍历到,而undefined
则表示数组有这个元素,值是undefined
,所以遍历不会跳过。
类似数组的对象
如果一个对象的所有键名都是正整数或零,并且有length
属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object)。
1 | var obj = { |
上面代码中,对象obj
就是一个类似数组的对象。但是,“类似数组的对象”并不是数组,因为它们不具备数组特有的方法。对象obj
没有数组的push
方法,使用该方法就会报错。
“类似数组的对象”的根本特征,就是具有length
属性。只要有length
属性,就可以认为这个对象类似于数组。但是有一个问题,这种length
属性不是动态值,不会随着成员的变化而变化。
1 | var obj = { |
上面代码为对象obj
添加了一个数字键,但是length
属性没变。这就说明了obj
不是数组。
典型的“类似数组的对象”是函数的arguments
对象,以及大多数 DOM 元素集,还有字符串。
1 | // arguments对象 |
上面代码包含三个例子,它们都不是数组(instanceof
运算符返回false
),但是看上去都非常像数组。
数组的slice
方法可以将“类似数组的对象”变成真正的数组。
1 | var arr = Array.prototype.slice.call(arrayLike); |
除了转为真正的数组,“类似数组的对象”还有一个办法可以使用数组的方法,就是通过call()
把数组的方法放到对象上面。
1 | function print(value, index) { |
上面代码中,arrayLike
代表一个类似数组的对象,本来是不可以使用数组的forEach()
方法的,但是通过call()
,可以把forEach()
嫁接到arrayLike
上面调用。
下面的例子就是通过这种方法,在arguments
对象上面调用forEach
方法。
1 | // forEach 方法 |
字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call
遍历。
1 | Array.prototype.forEach.call('abc', function (chr) { |
注意,这种方法比直接使用数组原生的forEach
要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach
方法。
1 | var arr = Array.prototype.slice.call('abc'); |
8.运算符
- 指数运算符:
x ** y
- 余数运算符:
x % y
JavaScript 提供两种相等运算符:==
和===
。
简单说,它们的区别是相等运算符(==
)比较两个值是否相等,严格相等运算符(===
)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(===
)直接返回false
,而相等运算符(==
)会将它们转换成同一个类型,再用严格相等运算符进行比较。
undefined
和null
与自身严格相等。
- 二进制或运算符(or):符号为
|
,表示若两个二进制位都为0
,则结果为0
,否则为1
。 - 二进制与运算符(and):符号为
&
,表示若两个二进制位都为1,则结果为1,否则为0。 - 二进制否运算符(not):符号为
~
,表示对一个二进制位取反。 - 异或运算符(xor):符号为
^
,表示若两个二进制位不相同,则结果为1,否则为0。 - 左移运算符(left shift):符号为
<<
,详见下文解释。 - 右移运算符(right shift):符号为
>>
,详见下文解释。 - 头部补零的右移运算符(zero filled right shift):符号为
>>>
,详见下文解释。
void
运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined
。
这个运算符的主要用途是浏览器的书签工具(Bookmarklet),以及在超级链接中插入代码防止网页跳转。
请看下面的代码。
1 | <script> |
上面代码中,点击链接后,会先执行onclick
的代码,由于onclick
返回false
,所以浏览器不会跳转到 example.com。
void
运算符可以取代上面的写法。
1 | <a href="javascript: void(f())">文字</a> |
下面是一个更实际的例子,用户点击链接提交表单,但是不产生页面跳转。
1 | <a href="javascript: void(document.form.submit())"> |
Boolean()
函数可以将任意类型的值转为布尔值。
它的转换规则相对简单:除了以下五个值的转换结果为false
,其他的值全部为true
。
undefined
null
0
(包含-0
和+0
)NaN
''
(空字符串)
使用Number()
函数,可以将任意类型的值转化成数值。 转不了的会变成NaN,同理也有String()
9.错误机制处理
除了 JavaScript 原生提供的七种错误对象,还可以定义自己的错误对象。
1 | function UserError(message) { |
上面代码自定义一个错误对象UserError
,让它继承Error
对象。然后,就可以生成这种自定义类型的错误了。
1 | new UserError('这是自定义的错误!'); |
try catch
语句内不管报错还是有return
,只要有finally
就一定会执行它。
10. js编程风格建议
JavaScript 最大的语法缺点,可能就是全局变量对于任何一个代码块,都是可读可写。这对代码的模块化和重复使用,非常不利。
因此,建议避免使用全局变量。如果不得不使用,可以考虑用大写字母表示变量名,这样更容易看出这是全局变量,比如UPPER_CASE
。
建议不要使用相等运算符(==
),只使用严格相等运算符(===
)。
建议不要将不同目的的语句,合并成一行。
建议自增(++
)和自减(--
)运算符尽量使用+=
和-=
代替。
1 | function doAction(action) { |
建议switch...case
结构可以用对象结构代替。
11. console
对象
打开开发者工具以后,顶端有多个面板。
- Elements:查看网页的 HTML 源码和 CSS 代码。
- Resources:查看网页加载的各种资源文件(比如代码文件、字体文件 CSS 文件等),以及在硬盘上创建的各种内容(比如本地缓存、Cookie、Local Storage等)。
- Network:查看网页的 HTTP 通信情况。
- Sources:查看网页加载的脚本源码。
- Timeline:查看各种网页行为随时间变化的情况。
- Performance:查看网页的性能情况,比如 CPU 和内存消耗。
- Console:用来运行 JavaScript 命令。
console.log
方法支持以下占位符,不同类型的数据必须使用对应的占位符。
%s
字符串%d
整数%i
整数%f
浮点数%o
对象的链接%c
CSS 格式字符串
使用%c
占位符时,对应的参数必须是 CSS 代码,用来对输出内容进行 CSS 渲染。
1 | console.log( |
对于某些复合类型的数据,console.table
方法可以将其转为表格显示。
1 | var languages = [ |
console.count
方法用于计数,输出它被调用了多少次。
该方法可以接受一个字符串作为参数,作为标签,对执行次数进行分类。
1 | function greet(user) { |
上面代码根据参数的不同,显示bob
执行了两次,alice
执行了一次。
console.assert
方法主要用于程序运行过程中,进行条件判断,如果不满足条件,就显示一个错误,但不会中断程序执行。这样就相当于提示用户,内部状态不正确。
它接受两个参数,第一个参数是表达式,第二个参数是字符串。只有当第一个参数为false
,才会提示有错误,在控制台输出第二个参数,否则不会有任何结果。
1 | console.assert(list.childNodes.length < 500, '节点个数大于等于500') |
上面代码中,如果符合条件的节点小于500个,不会有任何输出;只有大于等于500时,才会在控制台提示错误,并且显示指定文本。
console.time() console.timeEnd()
这两个方法用于计时,可以算出一个操作所花费的准确时间。
1 | console.time('Array initialize'); |
time
方法表示计时开始,timeEnd
方法表示计时结束。它们的参数是计时器的名称。调用timeEnd
方法之后,控制台会显示“计时器名称: 所耗费的时间”。
console.trace
方法显示当前执行的代码在堆栈中的调用路径。
1 | console.trace() |
console.clear
方法用于清除当前控制台的所有输出,将光标回置到第一行。如果用户选中了控制台的“Preserve log”选项,console.clear
方法将不起作用。
控制台常见命令
浏览器控制台中,除了使用console
对象,还可以使用一些控制台自带的命令行方法。
(1)$_
$_
属性返回上一个表达式的值。
1 | 2 + 2 |
(2)$0
- $4
控制台保存了最近5个在 Elements 面板选中的 DOM 元素,$0
代表倒数第一个(最近一个),$1
代表倒数第二个,以此类推直到$4
。
(3)$(selector)
$(selector)
返回第一个匹配的元素,等同于document.querySelector()
。注意,如果页面脚本对$
有定义,则会覆盖原始的定义。比如,页面里面有 jQuery,控制台执行$(selector)
就会采用 jQuery 的实现,返回一个数组。
(4)$$(selector)
$$(selector)
返回选中的 DOM 对象,等同于document.querySelectorAll
。
(5)$x(path)
$x(path)
方法返回一个数组,包含匹配特定 XPath 表达式的所有 DOM 元素。
1 | $x("//p[a]") |
上面代码返回所有包含a
元素的p
元素。
(6)inspect(object)
inspect(object)
方法打开相关面板,并选中相应的元素,显示它的细节。DOM 元素在Elements
面板中显示,比如inspect(document)
会在 Elements 面板显示document
元素。JavaScript 对象在控制台面板Profiles
面板中显示,比如inspect(window)
。
(7)getEventListeners(object)
getEventListeners(object)
方法返回一个对象,该对象的成员为object
登记了回调函数的各种事件(比如click
或keydown
),每个事件对应一个数组,数组的成员为该事件的回调函数。
(8)keys(object)
,values(object)
keys(object)
方法返回一个数组,包含object
的所有键名。
values(object)
方法返回一个数组,包含object
的所有键值。
1 | var o = {'p1': 'a', 'p2': 'b'}; |
(9)monitorEvents(object[, events]) ,unmonitorEvents(object[, events])
monitorEvents(object[, events])
方法监听特定对象上发生的特定事件。事件发生时,会返回一个Event
对象,包含该事件的相关信息。unmonitorEvents
方法用于停止监听。
1 | monitorEvents(window, "resize"); |
上面代码分别表示单个事件和多个事件的监听方法。
1 | monitorEvents($0, 'mouse'); |
上面代码表示如何停止监听。
monitorEvents
允许监听同一大类的事件。所有事件可以分成四个大类。
- mouse:”mousedown”, “mouseup”, “click”, “dblclick”, “mousemove”, “mouseover”, “mouseout”, “mousewheel”
- key:”keydown”, “keyup”, “keypress”, “textInput”
- touch:”touchstart”, “touchmove”, “touchend”, “touchcancel”
- control:”resize”, “scroll”, “zoom”, “focus”, “blur”, “select”, “change”, “submit”, “reset”
1 | monitorEvents($("#msg"), "key"); |
上面代码表示监听所有key
大类的事件。
(10)其他方法
命令行 API 还提供以下方法。
clear()
:清除控制台的历史。copy(object)
:复制特定 DOM 元素到剪贴板。dir(object)
:显示特定对象的所有属性,是console.dir
方法的别名。dirxml(object)
:显示特定对象的 XML 形式,是console.dirxml
方法的别名。
debugger
语句主要用于除错,作用是设置断点。如果有正在运行的除错工具,程序运行到debugger
语句时会自动停下。如果没有除错工具,debugger
语句不会产生任何结果,JavaScript 引擎自动跳过这一句。
Chrome 浏览器中,当代码运行到debugger
语句时,就会暂停运行,自动打开脚本源码界面。
1 | for(var i = 0; i < 5; i++){ |
上面代码打印出0,1,2以后,就会暂停,自动打开源码界面,等待进一步处理。
Js
对象认识
所谓“静态方法”,是指部署在Object
对象自身的方法。
写在对象的原型对象上的方法,就是”实例”方法,
12.Object
对象
Object
本身是一个函数,可以当作工具方法使用,将任意值转为对象。这个方法常用于保证某个值一定是对象。
如果参数是原始类型的值,Object
方法将其转为对应的包装对象的实例
如果Object
方法的参数是一个对象,它总是返回该对象,即不用转换。
利用这一点,可以写一个判断变量是否为对象的函数。
1 | function isObject(value) { |
Object.keys
方法和Object.getOwnPropertyNames
方法都用来遍历对象的属性。区别是前者返回的数组的成员都是该对象自身的(而不是继承的)所有属性名。后者包含了该对象自身的所有属性名。
1 | var a = ['Hello', 'World']; |
一般情况下,几乎总是使用Object.keys
方法,遍历对象的属性。
Object
还有不少其他静态方法,将在后文逐一详细介绍。
(1)对象属性模型的相关方法
Object.getOwnPropertyDescriptor()
:获取某个属性的描述对象。Object.defineProperty()
:通过描述对象,定义某个属性。Object.defineProperties()
:通过描述对象,定义多个属性。
(2)控制对象状态的方法
Object.preventExtensions()
:防止对象扩展。Object.isExtensible()
:判断对象是否可扩展。Object.seal()
:禁止对象配置。Object.isSealed()
:判断一个对象是否可配置。Object.freeze()
:冻结一个对象。Object.isFrozen()
:判断一个对象是否被冻结。
(3)原型链相关方法
Object.create()
:该方法可以指定原型对象和属性,返回一个新的对象。(生成实例对象的常用方法是,使用new
命令让构造函数返回一个实例。但是很多时候,只能拿到一个实例对象,它可能根本不是由构建函数生成的,那么能不能从一个实例对象,生成另一个实例对象呢?该方法用来满足这种需求。)Object.getPrototypeOf()
:获取对象的Prototype
对象。Object.setPrototypeOf
方法为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象。
__proto__
说明Object.prototype.__proto__
实例对象的的这个属性(前后各两个下划线),返回该对象的原型。该属性可读写。
1 | var obj = {}; |
上面代码通过__proto__
属性,将p
对象设为obj
对象的原型。
根据语言标准,__proto__
属性只有浏览器才需要部署,其他环境可以没有这个属性。它前后的两根下划线,表明它本质是一个内部属性,不应该对使用者暴露。因此,应该尽量少用这个属性,而是用Object.getPrototypeOf()
和Object.setPrototypeOf()
,进行原型对象的读写操作。
原型链可以用__proto__
很直观地表示。
Object 的实例方法
除了静态方法,还有不少方法定义在Object.prototype
对象。它们称为实例方法,所有Object
的实例对象都继承了这些方法。
Object
实例对象的方法,主要有以下六个。
Object.prototype.valueOf()
:返回当前对象对应的值。Object.prototype.toString()
:返回当前对象对应的字符串形式。Object.prototype.toLocaleString()
:返回当前对象对应的本地字符串形式。Object.prototype.hasOwnProperty()
:判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性。Object.prototype.isPrototypeOf()
:判断当前对象是否为另一个对象的原型。Object.prototype.propertyIsEnumerable()
:判断某个属性是否可枚举。
JavaScript
对于对象提供了六个默认属性,通过getOwnPropertyDescriptor()
方法可以获取属性描述对象。它的第一个参数是目标对象,第二个参数是一个字符串,对应目标对象的某个属性名。以下是默认属性说明。
1 | var obj = { p: 'a' }; |
(1)value
value
是该属性的属性值,默认为undefined
。
(2)writable
writable
是一个布尔值,表示属性值(value)是否可改变(即是否可写),默认为true
。
(3)enumerable
enumerable
是一个布尔值,表示该属性是否可遍历,默认为true
。如果设为false
,会使得某些操作(比如for...in
循环、Object.keys()
、JSON.stringify
方法)跳过该属性。
- 如果对象的 JSON 格式输出要排除某些属性,就可以把这些属性的
enumerable
设为false
(4)configurable
configurable
是一个布尔值,表示属性的可配置性,默认为true
。如果设为false
,将阻止某些操作改写属性描述对象,比如无法删除该属性,也不得改变各种元属性(value
属性除外)。也就是说,configurable
属性控制了属性描述对象的可写性。
(5)get
get
是一个函数,表示该属性的取值函数(getter),默认为undefined
。
(6)set
set
是一个函数,表示该属性的存值函数(setter),默认为undefined
。
一旦定义了取值函数
get
(或存值函数set
),就不能将writable
属性设为true
,或者同时定义value
属性,否则会报错。这些属性也被称作元属性。通过
defineProperty()
方法进行修改obj.p
定义了get
和set
属性。obj.p
取值时,就会调用get
;赋值时,就会调用set
。以下是一般写法
1 | // 写法二 |
Object.getOwnPropertyNames
方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历。实例对象的
propertyIsEnumerable()
方法返回一个布尔值,用来判断某个属性是否可遍历。注意,这个方法只能用于判断对象自身的属性,对于继承的属性一律返回false
。
对象的拷贝
既需要拷贝自定义属性,也需要拷贝元属性。
1 | var extend = function (to, from) { |
控制对象的状态
有时需要冻结对象的读写状态,防止对象被改变。JavaScript 提供了三种冻结方法,最弱的一种是Object.preventExtensions
,其次是Object.seal
,最强的是Object.freeze
。
Object.preventExtensions
方法可以使得一个对象无法再添加新的属性。1
2var obj = new Object();
Object.preventExtensions(obj);
Object.isExtensible
方法用于检查一个对象是否使用了Object.preventExtensions
方法。也就是说,检查是否可以为一个对象添加属性。
Object.seal
方法使得一个对象既无法添加新属性,也无法删除旧属性。Object.seal
实质是把属性描述对象的configurable
属性设为false
,因此属性描述对象不再能改变了。Object.seal
只是禁止新增或删除属性,并不影响修改某个属性的值。Object.isSealed
方法用于检查一个对象是否使用了Object.seal
方法。Object.freeze
方法可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量。(凯亚:冻结吧!)Object.isFrozen
方法用于检查一个对象是否使用了Object.freeze
方法。Object.isFrozen
的一个用途是,确认某个对象没有被冻结后,再对它的属性赋值。漏洞
- 漏洞一:虽然冻结了对象(Object),但是可以通过改变原型对象,来为对象增加属性。
一种解决方案是,把
obj
的原型也冻结住。1
2var proto = Object.getPrototypeOf(obj);
Object.preventExtensions(proto);- 漏洞二:如果属性值是对象,上面这些方法只能冻结属性指向的对象,而不能冻结对象本身的内容。比如说:
obj.bar
属性指向一个数组,obj
对象被冻结以后,这个指向无法改变,即无法指向其他值,但是所指向的数组是可以改变的。
13.Array
对象
push()
末尾插数pop()
末尾减数,返回被减去的数对象
push
和pop
结合使用,就构成了“后进先出”的栈结构(stack)。
shift()
头部插数unshift()
头部减数,返回被减去的数对象
push()
和shift()
结合使用,就构成了“先进先出”的队列结构(queue)。
join()
相当于Java
的spilt
,concat()
方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变。1
2
3['hello'].concat(['world'], ['!'])
//concat也接受其他类型的值作为参数,添加到目标数组尾部。
[1, 2, 3].concat(4, 5, 6)如果数组成员包括对象,
concat
方法返回当前数组的一个浅拷贝。reverse
方法用于颠倒排列数组元素,返回改变后的数组。注意,该方法将改变原数组。arr.slice(start, end)
方法用于提取目标数组的一部分,返回一个新数组,原数组不变。如果slice()
方法的参数是负数,则表示倒着数计算的位置。slice()
方法的一个重要应用,是将类似数组的对象转为真正的数组。1
2
3
4
5Array.prototype.slice.call({ 0: 'a', 1: 'b', length: 2 })
// ['a', 'b']
Array.prototype.slice.call(document.querySelectorAll("div"));
Array.prototype.slice.call(arguments);上面代码的参数都不是数组,但是通过
call
方法,在它们上面调用slice()
方法,就可以把它们转为真正的数组。splice()
方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。1
arr.splice(start, count, addElement1, addElement2, ...);
splice
的第一个参数是删除的起始位置(从0开始),第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。相同的,起始位置如果是负数,就表示从倒数位置开始删除。- 如果只是单纯地插入元素,
splice
方法的第二个参数可以设为0
。 - 如果只提供第一个参数,等同于将原数组在指定位置拆分成两个数组。
- 如果只是单纯地插入元素,
sort
方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变。如果想让
sort
方法按照自定义方式排序,可以传入一个函数作为参数。1
2
3
4[10111, 1101, 111].sort(function (a, b) {
return a - b;
})
// [111, 1101, 10111]map()
方法将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回。map()
方法接受一个函数作为参数。该函数调用时,这个参数函数可以传入三个参数:当前成员、当前位置和数组本身。map()
方法还可以接受第二个参数,用来绑定回调函数内部的this
变量,就是把结果指向arr
数组。1
2
3
4
5
6[1, 2, 3].map(function(elem, index, arr) {
return elem * index;
});
[1, 2, 3].map(function(elem) {
return elem + 1;
},arr);forEach()
的用法与map()
方法一致,参数是也一致。如果数组遍历的目的是为了得到返回值,那么使用map()
方法,否则使用forEach()
方法。forEach()
方法无法中断执行,总是会将所有成员遍历完。如果希望符合某种条件时,就中断遍历,要使用for
循环forEach()
方法不会跳过undefined
和null
,但会跳过空位。filter()
方法用于过滤数组成员,满足条件的成员组成一个新数组返回,参数同上。1
2
3
4[1, 2, 3, 4, 5].filter(function (elem) {
return (elem > 3);
})
// [4, 5]类似断言的方法
some
方法是只要一个成员的返回值是true
,则整个some
方法的返回值就是true
,否则返回false
。every
方法是所有成员的返回值都是true
,整个every
方法才返回true
,否则返回false
。
reduce()
方法和reduceRight()
方法依次处理数组的每个成员,最终累计为一个值。它们的差别是,reduce()
是从左到右处理(从第一个成员到最后一个成员),reduceRight()
则是从右到左(从最后一个成员到第一个成员),其他完全一样。1
2
3
4
5
6
7
8
9[1, 2, 3, 4, 5].reduce(function (a, b) {
console.log(a, b);
return a + b;
})
// 1 2
// 3 3
// 6 4
// 10 5
//最后结果:15这四个参数之中,只有前两个是必须的,后两个则是可选的。
1
2
3
4
5
6
7[1, 2, 3, 4, 5].reduce(function (
a, // 累积变量,必须
b, // 当前变量,必须
i, // 当前位置,可选
arr // 原数组,可选
) {
// ... ...如果要对累积变量指定初值,可以把它放在
reduce()
方法和reduceRight()
方法的第二个参数。1
2
3
4[1, 2, 3, 4, 5].reduce(function (a, b) {
return a + b;
}, 10);
// 25lastIndexOf
方法返回给定元素在数组中最后一次出现的位置,如果没有出现则返回-1
该和
indexOf()
方法不能用来搜索NaN
的位置,即它们无法确定数组成员是否包含NaN
。这是因为这两个方法内部,使用严格相等运算符(===
)进行比较,而NaN
是唯一一个不等于自身的值。总结一下:这个就是类似
java Stream
的方法,它们也可以链式调用。
14.包装对象
所谓“包装对象”,指的是与数值、字符串、布尔值分别相对应的Number
、String
、Boolean
三个原生对象。
值得注意的是包装对象还可以自定义方法和属性,供原始类型的值直接调用。
比如,我们可以新增一个double
方法,使得字符串和数字翻倍。
1 | String.prototype.double = function () { |
15.Boolean
值得注意的点
Boolean对于特殊值的判断
1 | Boolean(undefined) // false |
使用双重的否运算符(!
)也可以将任意值转为对应的布尔值。
1 | !!undefined // false |
最后,对于一些特殊值,Boolean
对象前面加不加new
,会得到完全相反的结果,必须小心。
1 | if (Boolean(false)) { |
16.Number对象值得注意的点
Number.POSITIVE_INFINITY
:正的无限,指向Infinity
。Number.NEGATIVE_INFINITY
:负的无限,指向-Infinity
。Number.NaN
:表示非数值,指向NaN
。Number.MIN_VALUE
:表示最小的正数(即最接近0的正数,在64位浮点数体系中为5e-324
),相应的,最接近0的负数为-Number.MIN_VALUE
。Number.MAX_SAFE_INTEGER
:表示能够精确表示的最大整数,即9007199254740991
。Number.MIN_SAFE_INTEGER
:表示能够精确表示的最小整数,即-9007199254740991
。
toFixed()
方法先将一个数转为指定位数的小数,然后返回这个小数对应的字符串。toFixed()
方法的参数为小数位数,有效范围为0到100,超出这个范围将抛出 RangeError 错误。由于浮点数的原因,小数
5
的四舍五入是不确定的,使用的时候必须小心。toExponential
方法用于将一个数转为科学计数法形式。toExponential
方法的参数是小数点后有效数字的位数,范围为0到100,超出这个范围,会抛出一个 RangeError 错误。Number.prototype.toPrecision()
方法用于将一个数转为指定位数的有效数字。 参数同上该方法用于四舍五入时不太可靠,跟浮点数不是精确储存有关。
总结:Js做小数的操作都不太靠谱,能不用尽量别用。
17.String
对象值得注意的点
charCodeAt()
方法返回字符串指定位置的 Unicode 码点(十进制表示),相当于String.fromCharCode()
的逆操作。concat
方法用于连接两个字符串,返回一个新字符串,不改变原字符串。slice()
方法用于从原字符串取出子字符串并返回,不改变原字符串。它的第一个参数是子字符串的开始位置,第二个参数是子字符串的结束位置(不含该位置)。substr
方法好像作用一样。trim
方法用于去除字符串两端的空格,返回一个新字符串,不改变原字符串。match
方法用于确定原字符串是否匹配某个子字符串,返回一个数组,成员为匹配的第一个字符串。如果没有找到匹配,则返回null
。search
方法作用一样,但还可以使用正则表达式作为参数。
18.Math
对象值得注意的点
Math
对象的静态属性,提供以下一些数学常数。
Math.E
:常数e
。Math.LN2
:2 的自然对数。Math.LN10
:10 的自然对数。Math.LOG2E
:以 2 为底的e
的对数。Math.LOG10E
:以 10 为底的e
的对数。Math.PI
:常数π
。Math.SQRT1_2
:0.5 的平方根。Math.SQRT2
:2 的平方根。
Math
对象提供以下一些静态方法。
Math.abs()
:绝对值Math.ceil()
:向上取整Math.floor()
:向下取整Math.max()
:最大值Math.min()
:最小值Math.pow()
:幂运算Math.sqrt()
:平方根Math.log()
:自然对数Math.exp()
:e
的指数Math.round()
:四舍五入Math.random()
:随机数
19.Date
对象值得注意的点
定义时间就跟Java8 Time
包的差不多,但更灵活。
1 | new Date(2013, 0, 1, 0, 0, 0, 0) |
日期设为
0
,就代表上个月的最后一天。参数还可以使用负数,表示扣去的时间。
日期运算不能直接加减,先转成对应的毫秒数,运算后在
new Date()
转回来。
静态方法
Date.now
方法返回当前时间距离时间零点(1970年1月1日 00:00:00 UTC)的毫秒数,相当于 Unix 时间戳乘以1000。Date.parse
方法用来解析日期字符串,返回该时间距离时间零点(1970年1月1日 00:00:00)的毫秒数。如果解析失败,返回NaN
。Date.UTC
方法接受年、月、日等变量作为参数,返回该时间距离时间零点(1970年1月1日 00:00:00 UTC)的毫秒数
实例方法
Date
的实例对象,有几十个自己的方法,除了valueOf
和toString
,可以分为以下三类。
to
类:从Date
对象返回一个字符串,表示指定的时间。get
类:获取Date
对象的日期和时间。getFullYear
这种就不加以赘述set
类:设置Date
对象的日期和时间。setFullYear
这种就不加以赘述
valueOf
方法返回实例对象距离时间零点(1970年1月1日00:00:00 UTC)对应的毫秒数,等同于getTime
方法。toDateString
方法返回日期字符串(不含小时、分和秒)。toTimeString
方法返回时间字符串(不含年月日)。
正则用到的时候再看吧,语法都是其次,here.
20.JSON
对象
JSON.stringify()
方法用于将一个值转为 JSON 字符串。该字符串符合 JSON 格式,JSON.stringify()
方法还可以接受一个数组,作为第二个参数,指定参数对象的哪些属性需要转成字符串。(
用处不大)
:JSON.stringify()
还可以接受第三个参数,用于增加返回的 JSON 字符串的可读性。默认返回的是单行字符串,对于大型的 JSON 对象,可读性非常差。第三个参数使得每个属性单独占据一行,并且将每个属性前面添加指定的前缀(不超过10个字符)。第三个属性如果是一个数字,则表示每个属性前面添加的空格(最多不超过10个)。1
2
3
4
5
6
7
8
9
10// 默认输出
JSON.stringify({ p1: 1, p2: 2 })
// JSON.stringify({ p1: 1, p2: 2 })
// 分行输出
JSON.stringify({ p1: 1, p2: 2 }, null, '\t')
// {
// "p1": 1,
// "p2": 2
// }
toJSON()
方法跟JSON.stringify()
作用一样。它的一个应用是,将正则对象自动转为字符串。因为JSON.stringify()
默认不能转换正则对象,但是设置了toJSON()
方法以后,就可以转换正则对象了。
JSON.parse()
方法用于将 JSON 字符串转换成对应的值。
21.面向对象
构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用
Object.create()
方法。this
就是属性或方法“当前”所在的对象。JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……
如果一层层地上溯,所有对象的原型最终都可以上溯到
Object.prototype
,即Object
构造函数的prototype
属性。也就是说,所有对象都继承了Object.prototype
的属性。这就是所有对象都有valueOf
和toString
方法的原因,因为这是从Object.prototype
继承的。那么,
Object.prototype
对象有没有它的原型呢?回答是Object.prototype
的原型是null
。null
没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null
。JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function M1() {
this.hello = 'hello';
}
function M2() {
this.world = 'world';
}
function S() {
M1.call(this);
M2.call(this);
}
// 继承 M1
S.prototype = Object.create(M1.prototype);
// 继承链上加入 M2
Object.assign(S.prototype, M2.prototype);
// 指定构造函数
S.prototype.constructor = S;
var s = new S();
s.hello // 'hello'
s.world // 'world'上面代码中,子类
S
同时继承了父类M1
和M2
。这种模式又称为 Mixin(混入)。
JavaScript 模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。
模块是实现特定功能的一组属性和方法的封装。
简单的做法是把模块写成一个对象,所有的模块成员都放到这个对象里面。
1 | var module1 = (function () { |
上面的函数m1
和m2
,都封装在module1
对象里。使用的时候,就是调用这个对象的属性。
如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用“放大模式”(augmentation)。
1 | var module1 = (function (mod) { |
严格模式
进入严格模式的标志,是一行字符串use strict
。作用域可以是脚本:使用需要放在脚本文件的第一行。作用域也可以是单个函数:需要将其放在函数体的第一行。
1 | 'use strict'; |
在多人开发中,有时需要把不同的脚本合并在一个文件里面。如果一个脚本是严格模式,另一个脚本不是,它们的合并就可能出错。严格模式的脚本在前,则合并后的脚本都是严格模式;如果正常模式的脚本在前,则合并后的脚本都是正常模式。这两种情况下,合并后的结果都是不正确的。这时可以考虑把整个脚本文件放在一个立即执行的匿名函数之中。
1 | (function () { |
22.JavaScript
的异步操作
首先,
JavaScript
是单线程模型的语言,但JavaScript
引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。对于高频大量访问的情况是通过异步解决的。程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。
同步任务就在主线程上排队,异步任务就其他任务队列(队列有多个)的,从而不影响异步任务后的任务。引擎会实时检查异步任务能否是否满足执行条件,满足的话就把它挪到主线程上去(异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数,如果一个异步任务没有回调函数,就不会重新进入主线程,因为没有用回调函数指定下一步的操作。),当任务队列清空,程序结束。
异步的操作模式
- 回调函数
- 事件监听
- 发布/订阅
多个异步的流程控制
- 串行执行:编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。
- 并行执行:即所有异步任务同时执行,等到全部完成以后,才执行
final
函数。 - 线程池模式:并行给他设置个池子大小,即同时最多只能执行
n
个任务….简易的线程池。
23.定时器相关
setTimeout
函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。setTimeout
函数接受两个参数,第一个参数func|code
是将要推迟执行的函数名或者一段代码,第二个参数delay
是推迟执行的毫秒数。setTimeout
的第二个参数如果省略,则默认为0。
setTimeout
还允许更多的参数。下例中的1,1
就作为回调函数的参数a,b
。
1 | setTimeout(function (a,b) { |
值得注意的点:
setTimeout
的作用是将代码推迟到指定时间执行,如果指定时间为
0,即
setTimeout(f, 0)会在下一轮事件循环一开始就执行。setTimeout(f, 0)
有几个非常重要的用途。可以调整事件的发生顺序(举例:父子模块执行顺序)。
另一个应用是,用户自定义的回调函数,通常在浏览器的默认动作之前触发。(举例:用户每次输入文本后,立即将字符转为大写。但是实际上,它只能将本次输入前的字符转为大写,因为浏览器此时还没接收到新的文本,所以
this.value
取不到最新输入的那个字符。只有用setTimeout
改写,上面的代码才能发挥作用。)由于
setTimeout(f, 0)
实际上意味着,将任务放到浏览器最早可得的空闲时段执行,所以那些计算量大、耗时长的任务,常常会被放到几个小部分,分别放到setTimeout(f, 0)
里面执行。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var div = document.getElementsByTagName('div')[0];
// 写法一
for (var i = 0xA00000; i < 0xFFFFFF; i++) {
div.style.backgroundColor = '#' + i.toString(16);
}
// 写法二
var timer;
var i=0x100000;
function func() {
timer = setTimeout(func, 0);
div.style.backgroundColor = '#' + i.toString(16);
if (i++ == 0xFFFFFF) clearTimeout(timer);
}
timer = setTimeout(func, 0);上面代码有两种写法,都是改变一个网页元素的背景色。写法一会造成浏览器“堵塞”,因为 JavaScript 执行速度远高于 DOM,会造成大量 DOM 操作“堆积”,而写法二就不会,这就是
setTimeout(f, 0)
的好处。另一个使用这种技巧的例子是代码高亮的处理。如果代码块很大,一次性处理,可能会对性能造成很大的压力,那么将其分成一个个小块,一次处理一块,比如写成
setTimeout(highlightNext, 50)
的样子,性能压力就会减轻。
如果回调函数是对象的方法,那么
setTimeout
使得方法内部的this
关键字指向全局环境,而不是定义时所在的那个对象。为了防止出现这个问题,一种解决方法是将
obj.y
放入一个函数。1
2
3
4
5
6
7
8
9
10
11
12
13var x = 1;
var obj = {
x: 2,
y: function () {
console.log(this.x);
}
};
setTimeout(function () {
obj.y();
}, 1000);
// 2上面代码中,
obj.y
放在一个匿名函数之中,这使得obj.y
在obj
的作用域执行,而不是在全局作用域内执行,所以能够显示正确的值。另一种解决方法是,使用
bind
方法,将obj.y
这个方法绑定在obj
上面。1
2
3
4
5
6
7
8
9
10
11var x = 1;
var obj = {
x: 2,
y: function () {
console.log(this.x);
}
};
setTimeout(obj.y.bind(obj), 1000)
// 2
setInterval
函数的用法与setTimeout
完全一致,区别仅仅在于setInterval
指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。
setInterval
指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterval
指定每 100ms 执行一次,每次执行需要 5ms,那么第一次执行结束后95毫秒,第二次执行就会开始。如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。
为了确保两次执行之间有固定的间隔,可以不用setInterval
,而是每次执行结束后,使用setTimeout
指定下一次执行的具体时间。即双重setTimeout
。
1 | var i = 1; |
setTimeout
和setInterval
函数,都返回一个整数值,表示计数器编号。将该整数传入clearTimeout
和clearInterval
函数,就可以取消对应的定时器。
setTimeout
和setInterval
返回的整数值是连续的,也就是说,第二个setTimeout
方法返回的整数值,将比第一个的整数值大1。
利用这一点,可以写一个函数,取消当前所有的setTimeout
定时器。先调用setTimeout
,得到一个计算器编号,然后把编号比它小的计数器全部取消。
定时器的实现逻辑
setTimeout
和setInterval
的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。
这意味着,setTimeout
和setInterval
指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeout
和setInterval
指定的任务,一定会按照预定时间执行。
页面防抖处理 debounce
1 | $('textarea').on('keydown', debounce(ajaxAction, 2500)); |
上面代码中,只要在2500毫秒之内,用户再次击键,就会取消上一次的定时器,然后再新建一个定时器。这样就保证了回调函数之间的调用间隔,至少是2500毫秒。
24.Promise
对象(重要)
Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。
Promise 是一个对象,也是一个构造函数。传统的写法可能需要把f2
作为回调函数传入f1
,比如写成f1(f2)
,异步操作完成后,在f1
内部调用f2
。Promise 使得f1
和f2
变成了链式写法。不仅改善了可读性,而且对于多层嵌套的回调函数尤其方便。
1 | // 传统写法 |
Promise
对象的状态
Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。
- 异步操作未完成(pending)
- 异步操作成功(fulfilled)
- 异步操作失败(rejected)
上面三种状态里面,fulfilled
和rejected
合在一起称为resolved
(已定型)。
因此,Promise 的最终结果只有两种。
- 异步操作成功,Promise 实例传回一个值(value),状态变为
fulfilled
。 - 异步操作失败,Promise 实例抛出一个错误(error),状态变为
rejected
。
构造函数
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
(异步操作成功时调用)和reject
(失败时调用)。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。
Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。
而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。
Promise 的缺点是,编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then
,必须自己在then
的回调函数里面理清逻辑。
Promise 的回调函数属于异步任务,会在同步任务之后执行。但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。
DOM 操作 忽略
25.鼠标事件
鼠标事件主要有下面这些,所有事件都继承了MouseEvent
接口
(1)点击事件
鼠标点击相关的有四个事件。
click
:按下鼠标(通常是按下主按钮)时触发。dblclick
:在同一个元素上双击鼠标时触发。mousedown
:按下鼠标键时触发。mouseup
:释放按下的鼠标键时触发。
click
事件可以看成是两个事件组成的:用户在同一个位置先触发mousedown
,再触发mouseup
。因此,触发顺序是,mousedown
首先触发,mouseup
接着触发,click
最后触发。
双击时,dblclick
事件则会在mousedown
、mouseup
、click
之后触发。
(2)移动事件
鼠标移动相关的有五个事件。
mousemove
:当鼠标在一个节点内部移动时触发。当鼠标持续移动时,该事件会连续触发。为了避免性能问题,建议对该事件的监听函数做一些限定,比如限定一段时间内只能运行一次。mouseenter
:鼠标进入一个节点时触发,进入子节点不会触发这个事件(详见后文)。mouseover
:鼠标进入一个节点时触发,进入子节点会再一次触发这个事件(详见后文)。mouseout
:鼠标离开一个节点时触发,离开父节点也会触发这个事件(详见后文)。mouseleave
:鼠标离开一个节点时触发,离开父节点不会触发这个事件(详见后文)。
mouseover
事件和mouseenter
事件,都是鼠标进入一个节点时触发。两者的区别是,mouseenter
事件只触发一次,而只要鼠标在节点内部移动,mouseover
事件会在子节点上触发多次。
(3)其他事件
contextmenu
:按下鼠标右键时(上下文菜单出现前)触发,或者按下“上下文”菜单键时触发。wheel
:滚动鼠标的滚轮时触发,该事件继承的是WheelEvent
接口。
26.键盘事件
键盘事件由用户击打键盘触发,主要有keydown
、keypress
、keyup
三个事件,它们都继承了KeyboardEvent
接口。
keydown
:按下键盘时触发。keypress
:按下有值的键时触发,即按下 Ctrl、Alt、Shift、Meta 这样无值的键,这个事件不会触发。对于有值的键,按下时先触发keydown
事件,再触发这个事件。keyup
:松开键盘时触发该事件。
如果用户一直按键不松开,就会连续触发键盘事件,触发的顺序如下。
- keydown
- keypress
- keydown
- keypress
- …(重复以上过程)
- keyup
KeyboardEvent
接口用来描述用户与键盘的互动。这个接口继承了Event
接口,并且定义了自己的实例属性和实例方法。
浏览器原生提供KeyboardEvent
构造函数,用来新建键盘事件的实例。
1 | new KeyboardEvent(type, options) |
KeyboardEvent
构造函数接受两个参数。第一个参数是字符串,表示事件类型;第二个参数是一个事件配置对象,该参数可选。除了Event
接口提供的属性,还可以配置以下字段,它们都是可选。
key
:字符串,当前按下的键,默认为空字符串。code
:字符串,表示当前按下的键的字符串形式,默认为空字符串。location
:整数,当前按下的键的位置,默认为0
。ctrlKey
:布尔值,是否按下 Ctrl 键,默认为false
。shiftKey
:布尔值,是否按下 Shift 键,默认为false
。altKey
:布尔值,是否按下 Alt 键,默认为false
。metaKey
:布尔值,是否按下 Meta 键,默认为false
。repeat
:布尔值,是否重复按键,默认为false
。
27.进度事件
进度事件用来描述资源加载的进度,主要由 AJAX 请求、<img>
、<audio>
、<video>
、<style>
、<link>
等外部资源的加载触发,继承了ProgressEvent
接口。它主要包含以下几种事件。
abort
:外部资源中止加载时(比如用户取消)触发。如果发生错误导致中止,不会触发该事件。error
:由于错误导致外部资源无法加载时触发。load
:外部资源加载成功时触发。loadstart
:外部资源开始加载时触发。loadend
:外部资源停止加载时触发,发生顺序排在error
、abort
、load
等事件的后面。progress
:外部资源加载过程中不断触发。timeout
:加载超时时触发。
注意,除了资源下载,文件上传也存在这些事件。
28.表单事件
input
事件当<input>
、<select>
、<textarea>
的值发生变化时触发。select
事件当在<input>
、<textarea>
里面选中文本时触发。change
事件当<input>
、<select>
、<textarea>
的值发生变化时触发。它与input
事件的最大不同,就是不会连续触发,只有当全部修改完成时才会触发,另一方面input
事件必然伴随change
事件。具体来说,分成以下几种情况。- 激活单选框(radio)或复选框(checkbox)时触发。
- 用户提交时触发。比如,从下列列表(select)完成选择,在日期或文件输入框完成选择。
- 当文本框或
<textarea>
元素的值发生改变,并且丧失焦点时触发。
invalid
事件当用户提交表单时,如果表单元素的值不满足校验条件时触发。reset
事件当表单重置(所有表单成员变回默认值)时触发。submit
事件当表单数据向服务器提交时触发。注意,submit
事件的发生对象是<form>
元素,而不是<button>
元素,因为提交的是表单,而不是按钮。
29.触摸事件
触摸引发的事件,有以下几种。可以通过TouchEvent.type
属性,查看到底发生的是哪一种事件。
touchstart
:用户开始触摸时触发,它的target
属性返回发生触摸的元素节点。touchend
:用户不再接触触摸屏时(或者移出屏幕边缘时)触发,它的target
属性与touchstart
事件一致的,就是开始触摸时所在的元素节点。它的changedTouches
属性返回一个TouchList
实例,包含所有不再触摸的触摸点(即Touch
实例对象)。touchmove
:用户移动触摸点时触发,它的target
属性与touchstart
事件一致。如果触摸的半径、角度、力度发生变化,也会触发该事件。touchcancel
:触摸点取消时触发,比如在触摸区域跳出一个模态窗口(modal window)、触摸点离开了文档区域(进入浏览器菜单栏)、用户的触摸点太多,超过了支持的上限(自动取消早先的触摸点)。
浏览器的触摸 API 由三个部分组成。
- Touch:一个触摸点
- TouchList:多个触摸点的集合
- TouchEvent:触摸引发的事件实例
Touch
接口的实例对象用来表示触摸点(一根手指或者一根触摸笔),包括位置、大小、形状、压力、目标元素等属性。有时,触摸动作由多个触摸点(多根手指)组成,多个触摸点的集合由TouchList
接口的实例对象表示。TouchEvent
接口的实例对象代表由触摸引发的事件,只有触摸屏才会引发这一类事件。
很多时候,触摸事件和鼠标事件同时触发,即使这个时候并没有用到鼠标。这是为了让那些只定义鼠标事件、没有定义触摸事件的代码,在触摸屏的情况下仍然能用。如果想避免这种情况,可以用event.preventDefault
方法阻止发出鼠标事件。
Touch 接口代表单个触摸点。触摸点可能是一根手指,也可能是一根触摸笔。
浏览器原生提供Touch
构造函数,用来生成Touch
实例。
1 | var touch = new Touch(touchOptions); |
Touch
构造函数接受一个配置对象作为参数,它有以下属性。
identifier
:必需,类型为整数,表示触摸点的唯一 ID。target
:必需,类型为元素节点,表示触摸点开始时所在的网页元素。clientX
:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的水平距离,默认为0。clientY
:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的垂直距离,默认为0。screenX
:可选,类型为数值,表示触摸点相对于屏幕左上角的水平距离,默认为0。screenY
:可选,类型为数值,表示触摸点相对于屏幕左上角的垂直距离,默认为0。pageX
:可选,类型为数值,表示触摸点相对于网页左上角的水平位置(即包括页面的滚动距离),默认为0。pageY
:可选,类型为数值,表示触摸点相对于网页左上角的垂直位置(即包括页面的滚动距离),默认为0。radiusX
:可选,类型为数值,表示触摸点周围受到影响的椭圆范围的 X 轴半径,默认为0。radiusY
:可选:类型为数值,表示触摸点周围受到影响的椭圆范围的 Y 轴半径,默认为0。rotationAngle
:可选,类型为数值,表示触摸区域的椭圆的旋转角度,单位为度数,在0到90度之间,默认值为0。force
:可选,类型为数值,范围在0
到1
之间,表示触摸压力。0
代表没有压力,1
代表硬件所能识别的最大压力,默认为0
。
30.拖拉事件
在网页中,除了元素节点默认不可以拖拉,其他(图片、链接、选中的文字)都可以直接拖拉。为了让元素节点可拖拉,可以将该节点的draggable
属性设为true
。
1 | <div draggable="true"> |
当元素节点或选中的文本被拖拉时,就会持续触发拖拉事件,包括以下一些事件。
drag
:拖拉过程中,在被拖拉的节点上持续触发(相隔几百毫秒)。dragstart
:用户开始拖拉时,在被拖拉的节点上触发,该事件的target
属性是被拖拉的节点。通常应该在这个事件的监听函数中,指定拖拉的数据。dragend
:拖拉结束时(释放鼠标键或按下 ESC 键)在被拖拉的节点上触发,该事件的target
属性是被拖拉的节点。它与dragstart
事件,在同一个节点上触发。不管拖拉是否跨窗口,或者中途被取消,dragend
事件总是会触发的。dragenter
:拖拉进入当前节点时,在当前节点上触发一次,该事件的target
属性是当前节点。通常应该在这个事件的监听函数中,指定是否允许在当前节点放下(drop)拖拉的数据。如果当前节点没有该事件的监听函数,或者监听函数不执行任何操作,就意味着不允许在当前节点放下数据。在视觉上显示拖拉进入当前节点,也是在这个事件的监听函数中设置。dragover
:拖拉到当前节点上方时,在当前节点上持续触发(相隔几百毫秒),该事件的target
属性是当前节点。该事件与dragenter
事件的区别是,dragenter
事件在进入该节点时触发,然后只要没有离开这个节点,dragover
事件会持续触发。dragleave
:拖拉操作离开当前节点范围时,在当前节点上触发,该事件的target
属性是当前节点。如果要在视觉上显示拖拉离开操作当前节点,就在这个事件的监听函数中设置。drop
:被拖拉的节点或选中的文本,释放到目标节点时,在目标节点上触发。注意,如果当前节点不允许drop
,即使在该节点上方松开鼠标键,也不会触发该事件。如果用户按下 ESC 键,取消这个操作,也不会触发该事件。该事件的监听函数负责取出拖拉数据,并进行相关处理。
关于拖拉事件,有以下几个注意点。
- 拖拉过程只触发以上这些拖拉事件,尽管鼠标在移动,但是鼠标事件不会触发。
- 将文件从操作系统拖拉进浏览器,不会触发
dragstart
和dragend
事件。 dragenter
和dragover
事件的监听函数,用来取出拖拉的数据(即允许放下被拖拉的元素)。由于网页的大部分区域不适合作为放下拖拉元素的目标节点,所以这两个事件的默认设置为当前节点不允许接受被拖拉的元素。如果想要在目标节点上放下的数据,首先必须阻止这两个事件的默认行为。
31.资源事件
beforeunload
事件在窗口、文档、各种资源将要卸载前触发。它可以用来防止用户不小心卸载资源。unload
事件在窗口关闭或者document
对象将要卸载时触发。它的触发顺序排在beforeunload
、pagehide
事件后面。unload
事件发生时,文档处于一个特殊状态。所有资源依然存在,但是对用户来说都不可见,UI 互动全部无效。这个事件是无法取消的,即使在监听函数里面抛出错误,也不能停止文档的卸载load
事件在页面或某个资源加载成功时触发。注意,页面或资源从浏览器缓存加载,并不会触发load
事件。页面的load
事件也可以用pageshow
事件代替。error
事件是在页面或资源加载失败时触发。abort
事件在用户取消加载时触发。
32.session
历史事件
pageshow
事件在页面加载时触发,包括第一次加载和从缓存加载两种情况。如果要指定页面每次加载(不管是不是从浏览器缓存)时都运行的代码,可以放在这个事件的监听函数。pagehide
事件与pageshow
事件类似,当用户通过“前进/后退”按钮,离开当前页面时触发。它与 unload 事件的区别在于,如果在 window 对象上定义unload
事件的监听函数之后,页面不会保存在缓存中,而使用pagehide
事件,页面会保存在缓存中。
注意,这两个事件只在浏览器的history
对象发生变化时触发,跟网页是否可见没有关系。
popstate
事件在浏览器的history
对象的当前记录发生显式切换时触发。注意,调用history.pushState()
或history.replaceState()
,并不会触发popstate
事件。该事件只在用户在history
记录之间显式切换时触发,比如鼠标点击“后退/前进”按钮,或者在脚本中调用history.back()
、history.forward()
、history.go()
时触发。该事件对象有一个
state
属性,保存history.pushState
方法和history.replaceState
方法为当前记录添加的state
对象。hashchange
事件在 URL 的 hash 部分(即#
号后面的部分,包括#
号)发生变化时触发。该事件一般在window
对象上监听。hashchange
的事件实例具有两个特有属性:oldURL
属性和newURL
属性,分别表示变化前后的完整 URL。
33.网页状态事件
网页下载并解析完成以后,浏览器就会在
document
对象上触发 DOMContentLoaded 事件。这时,仅仅完成了网页的解析(整张页面的 DOM 生成了),所有外部资源(样式表、脚本、iframe 等等)可能还没有下载结束。也就是说,这个事件比load
事件,发生时间早得多。注意,网页的 JavaScript 脚本是同步执行的,脚本一旦发生堵塞,将推迟触发
DOMContentLoaded
事件。
34.窗口事件
scroll
事件在文档或文档元素滚动时触发,主要出现在用户拖动滚动条。1
window.addEventListener('scroll', callback);
该事件会连续地大量触发,所以它的监听函数之中不应该有非常耗费计算的操作。推荐的做法是使用
requestAnimationFrame
或setTimeout
控制该事件的触发频率,然后可以结合customEvent
抛出一个新事件。resize
事件在改变浏览器窗口大小时触发,主要发生在window
对象上面。1
2
3
4
5
6
7var resizeMethod = function () {
if (document.body.clientWidth < 768) {
console.log('移动设备的视口');
}
};
window.addEventListener('resize', resizeMethod, true);该事件也会连续地大量触发,所以最好像上面的
scroll
事件一样,通过throttle
函数控制事件触发频率。fullscreenchange
事件在进入或退出全屏状态时触发,该事件发生在document
对象上面。1
2
3document.addEventListener('fullscreenchange', function (event) {
console.log(document.fullscreenElement);
});fullscreenerror
事件在浏览器无法切换到全屏状态时触发。
35.剪切板事件
以下三个事件属于剪贴板操作的相关事件。
cut
:将选中的内容从文档中移除,加入剪贴板时触发。copy
:进行复制动作时触发。paste
:剪贴板内容粘贴到文档后触发。
举例来说,如果希望禁止输入框的粘贴事件,可以使用下面的代码。
1 | inputElement.addEventListener('paste', e => e.preventDefault()); |
上面的代码使得用户无法在<input>
输入框里面粘贴内容。
cut
、copy
、paste
这三个事件的事件对象都是ClipboardEvent
接口的实例。ClipboardEvent
有一个实例属性clipboardData
,是一个 DataTransfer 对象,存放剪贴的数据。具体的 API 接口和操作方法,请参见《拖拉事件》的 DataTransfer 对象部分。
1 | document.addEventListener('copy', function (e) { |
上面的代码使得复制进入剪贴板的,都是开发者指定的数据,而不是用户想要拷贝的数据。
36.焦点事件 FocusEvent
焦点事件发生在元素节点和document
对象上面,与获得或失去焦点相关。它主要包括以下四个事件。
focus
:元素节点获得焦点后触发,该事件不会冒泡。blur
:元素节点失去焦点后触发,该事件不会冒泡。focusin
:元素节点将要获得焦点时触发,发生在focus
事件之前。该事件会冒泡。focusout
:元素节点将要失去焦点时触发,发生在blur
事件之前。该事件会冒泡。
37.CustomEvent 接口
CustomEvent 接口用于生成自定义的事件实例。那些浏览器预定义的事件,虽然可以手动生成,但是往往不能在事件上绑定数据。如果需要在触发事件的同时,传入指定的数据,就可以使用 CustomEvent 接口生成的自定义事件对象。
浏览器原生提供CustomEvent()
构造函数,用来生成 CustomEvent 事件实例。
1 | new CustomEvent(type, options) |
CustomEvent()
构造函数接受两个参数。第一个参数是字符串,表示事件的名字,这是必须的。第二个参数是事件的配置对象,这个参数是可选的。CustomEvent
的配置对象除了接受 Event 事件的配置属性,只有一个自己的属性。
detail
:表示事件的附带数据,默认为null
。
38.script
元素
正常的网页加载流程是这样的。
- 浏览器一边下载 HTML 网页,一边开始解析。也就是说,不等到下载完,就开始解析。
- 解析过程中,浏览器发现
<script>
元素,就暂停解析,把网页渲染的控制权转交给 JavaScript 引擎。 - 如果
<script>
元素引用了外部脚本,就下载该脚本再执行,否则就直接执行代码。 - JavaScript 引擎执行完毕,控制权交还渲染引擎,恢复往下解析 HTML 网页。
- 为了解决脚本文件下载阻塞网页渲染的问题,一个方法是对
<script>
元素加入defer
属性。它的作用是延迟脚本的执行,等到 DOM 加载生成后,再执行脚本。
1 | <script src="a.js" defer></script> |
- 解决“阻塞效应”的另一个方法是对
<script>
元素加入async
属性。它会并行下载脚本,先下载完的先执行。
1 | <script src="a.js" async></script> |
defer
属性和async
属性到底应该使用哪一个?
一般来说,如果脚本之间没有依赖关系,就使用async
属性,如果脚本之间有依赖关系,就使用defer
属性。如果同时使用async
和defer
属性,后者不起作用,浏览器行为由async
属性决定。