1. 解析代码:HTML代码解析为DOM,CSS代码解析为CSSOM(CSS Object Model) 1. 对象合成:将DOM和CSSOM合成一棵渲染树(render tree) 1. 布局:计算出渲染树的布局(layout) 1. 绘制:将渲染树绘制到屏幕 以上四步并非严格按顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。所以,会看到这种情况:网页的HTML代码还没下载完,但浏览器已经显示出内容了。 (2)JavaScript引擎 JavaScript引擎的主要作用是,读取网页中的JavaScript代码,对其处理后运行。 本节主要介绍JavaScript引擎的工作方式。 ## JavaScript代码嵌入网页的方法 JavaScript代码只有嵌入网页,才能运行。网页中嵌入JavaScript代码有多种方法。 ### 直接添加代码块 通过` ` 如果脚本文件使用了非英语字符,还应该注明编码。 ```html 加载外部脚本和直接添加代码块,这两种方法不能混用。下面代码的`console.log`语句直接被忽略。 ```html 为了防止攻击者篡改外部脚本,`script`标签允许设置一个`integrity`属性,写入该外部脚本的Hash签名,用来验证脚本的一致性。 ```html 上面代码中,`script`标签有一个`integrity`属性,指定了外部脚本`/assets/application.js`的SHA265签名。一旦有人改了这个脚本,导致SHA265签名不匹配,浏览器就会拒绝加载。 除了JavaScript脚本,外部的CSS样式表也可以设置这个属性。 ### 行内代码 除了上面两种方法,HTML语言允许在某些元素的事件属性和`a`元素的`href`属性中,直接写入JavaScript。 ```html
这种写法将HTML代码与JavaScript代码混写在一起,非常不利于代码管理,不建议使用。 ## script标签的工作原理 正常的网页加载流程是这样的。 1. 浏览器一边下载HTML网页,一边开始解析 1. 解析过程中,发现script标签 1. 暂停解析,网页渲染的控制权转交给JavaScript引擎 1. 如果script标签引用了外部脚本,就下载该脚本,否则就直接执行 1. 执行完毕,控制权交还渲染引擎,恢复往下解析HTML网页 也就是说,加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。原因是JavaScript可以修改DOM(比如使用`document.write`方法),所以必须把控制权让给它,否则会导致复杂的线程竞赛的问题。 如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。 为了避免这种情况,较好的做法是将script标签都放在页面底部,而不是头部。这样即使遇到脚本失去响应,网页主体的渲染也已经完成了,用户至少可以看到内容,而不是面对一张空白的页面。 如果某些脚本代码非常重要,一定要放在页面头部的话,最好直接将代码嵌入页面,而不是连接外部脚本文件,这样能缩短加载时间。 将脚本文件都放在网页尾部加载,还有一个好处。在DOM结构生成之前就调用DOM,JavaScript会报错,如果脚本都在网页尾部加载,就不存在这个问题,因为这时DOM肯定已经生成了。 ```html 上面代码执行时会报错,因为此时`body`元素还未生成。 一种解决方法是设定`DOMContentLoaded`事件的回调函数。 ```html 另一种解决方法是,使用`script`标签的`onload`属性。当script标签指定的外部脚本文件下载和解析完成,会触发一个load事件,可以把所需执行的代码,放在这个事件的回调函数里面。 ```html 但是,如果将脚本放在页面底部,就可以完全按照正常的方式写,上面两种方式都不需要。 ```html 如果有多个script标签,比如下面这样。 ```html 浏览器会同时平行下载`1.js`和`2.js`,但是,执行时会保证先执行`1.js`,然后再执行`2.js`,即使后者先下载完成,也是如此。也就是说,脚本的执行顺序由它们在页面中的出现顺序决定,这是为了保证脚本之间的依赖关系不受到破坏。 当然,加载这两个脚本都会产生“阻塞效应”,必须等到它们都加载完成,浏览器才会继续页面渲染。 Gecko和Webkit引擎在网页被阻塞后,会生成第二个线程解析文档,下载外部资源,但是不会修改DOM,网页还是处于阻塞状态。 解析和执行CSS,也会产生阻塞。Firefox会等到脚本前面的所有样式表,都下载并解析完,再执行脚本;Webkit则是一旦发现脚本引用了样式,就会暂停执行脚本执行,等到样式表下载并解析完,再恢复执行。 此外,对于来自同一个域名的资源,比如脚本文件、样式表文件、图片文件等,浏览器一般最多同时下载六个(IE11允许同时下载13个)。如果是来自不同域名的资源,就没有这个限制。所以,通常把静态文件放在不同的域名之下,以加快下载速度。 ## defer属性 为了解决脚本文件下载阻塞网页渲染的问题,一个方法是加入defer属性。 ```html `defer`属性的作用是,告诉浏览器,等到DOM加载完成后,再执行指定脚本。 1. 浏览器开始解析HTML网页 2. 解析过程中,发现带有`defer`属性的script标签 3. 浏览器继续往下解析HTML网页,同时并行下载script标签中的外部脚本 4. 浏览器完成解析HTML网页,此时再执行下载的脚本 有了`defer`属性,浏览器下载脚本文件的时候,不会阻塞页面渲染。下载的脚本文件在`DOMContentLoaded`事件触发前执行(即刚刚读取完``标签),而且可以保证执行顺序就是它们在页面上出现的顺序。 对于内置而不是连接外部脚本的script标签,以及动态生成的script标签,`defer`属性不起作用。 ## async属性 解决“阻塞效应”的另一个方法是加入`async`属性。 ```html `async`属性的作用是,使用另一个进程下载脚本,下载时不会阻塞渲染。 1. 浏览器开始解析HTML网页 2. 解析过程中,发现带有`async`属性的`script`标签 3. 浏览器继续往下解析HTML网页,同时并行下载`script`标签中的外部脚本 4. 脚本下载完成,浏览器暂停解析HTML网页,开始执行下载的脚本 5. 脚本执行完毕,浏览器恢复解析HTML网页 `async`属性可以保证脚本下载的同时,浏览器继续渲染。需要注意的是,一旦采用这个属性,就无法保证脚本的执行顺序。哪个脚本先下载结束,就先执行那个脚本。另外,使用`async`属性的脚本文件中,不应该使用`document.write`方法。 `defer`属性和`async`属性到底应该使用哪一个? 一般来说,如果脚本之间没有依赖关系,就使用`async`属性,如果脚本之间有依赖关系,就使用`defer`属性。如果同时使用`async`和`defer`属性,后者不起作用,浏览器行为由`async`属性决定。 ## 重流和重绘 渲染树转换为网页布局,称为“布局流”(flow);布局显示到页面的这个过程,称为“绘制”(paint)。它们都具有阻塞效应,并且会耗费很多时间和计算资源。 页面生成以后,脚本操作和样式表操作,都会触发重流(reflow)和重绘(repaint)。用户的互动,也会触发,比如设置了鼠标悬停(`a:hover`)效果、页面滚动、在输入框中输入文本、改变窗口大小等等。 重流和重绘并不一定一起发生,重流必然导致重绘,重绘不一定需要重流。比如改变元素颜色,只会导致重绘,而不会导致重流;改变元素的布局,则会导致重绘和重流。 大多数情况下,浏览器会智能判断,将“重流”和“重绘”只限制到相关的子树上面,最小化所耗费的代价,而不会全局重新生成网页。 作为开发者,应该尽量设法降低重绘的次数和成本。比如,尽量不要变动高层的DOM元素,而以底层DOM元素的变动代替;再比如,重绘table布局和flex布局,开销都会比较大。 ```javascript var foo = document.getElementById(‘foobar’); foo.style.color = ‘blue’; foo.style.marginTop = ‘30px’; 上面的代码只会导致一次重绘,因为浏览器会累积DOM变动,然后一次性执行。 下面的代码则会导致两次重绘。 ```javascript var foo = document.getElementById(‘foobar’); foo.style.color = ‘blue’; var margin = parseInt(foo.style.marginTop); foo.style.marginTop = (margin + 10) + ‘px’; 下面是一些优化技巧。 - 读取DOM或者写入DOM,尽量写在一起,不要混杂 - 缓存DOM信息 - 不要一项一项地改变样式,而是使用CSS class一次性改变样式 - 使用document fragment操作DOM - 动画时使用absolute定位或fixed定位,这样可以减少对其他元素的影响 - 只在必要时才显示元素 - 使用`window.requestAnimationFrame()`,因为它可以把代码推迟到下一次重流时执行,而不是立即要求页面重流 - 使用虚拟DOM(virtual DOM)库 下面是一个`window.requestAnimationFrame()`对比效果的例子。 ```javascript // 重绘代价高 function doubleHeight(element) { var currentHeight = element.clientHeight; element.style.height = (currentHeight * 2) + ‘px’; all_my_elements.forEach(doubleHeight); // 重绘代价低 function doubleHeight(element) { var currentHeight = element.clientHeight; window.requestAnimationFrame(function () { element.style.height = (currentHeight * 2) + ‘px’; all_my_elements.forEach(doubleHeight); ## 脚本的动态嵌入 除了用静态的`script`标签,还可以动态嵌入`script`标签。 ```javascript ['1.js', '2.js'].forEach(function(src) { var script = document.createElement('script'); script.src = src; document.head.appendChild(script); 这种方法的好处是,动态生成的`script`标签不会阻塞页面渲染,也就不会造成浏览器假死。但是问题在于,这种方法无法保证脚本的执行顺序,哪个脚本文件先下载完成,就先执行哪个。 如果想避免这个问题,可以设置async属性为`false`。 ```javascript ['1.js', '2.js'].forEach(function(src) { var script = document.createElement('script'); script.src = src; script.async = false; document.head.appendChild(script); 上面的代码依然不会阻塞页面渲染,而且可以保证`2.js`在`1.js`后面执行。不过需要注意的是,在这段代码后面加载的脚本文件,会因此都等待`2.js`执行完成后再执行。 我们可以把上面的写法,封装成一个函数。 ```javascript (function() { var scripts = document.getElementsByTagName('script')[0]; function load(url) { var script = document.createElement('script'); script.async = true; script.src = url; scripts.parentNode.insertBefore(script, scripts); load('//apis.google.com/js/plusone.js'); load('//platform.twitter.com/widgets.js'); load('//s.thirdpartywidget.com/widget.js'); }()); 上面代码中,`async`属性设为`true`,是因为加载的脚本没有互相依赖关系。而且,这样就不会造成堵塞。 此外,动态嵌入还有一个地方需要注意。动态嵌入必须等待CSS文件加载完成后,才会去下载外部脚本文件。静态加载就不存在这个问题,`script`标签指定的外部脚本文件,都是与CSS文件同时并发下载的。 ## 加载使用的协议 如果不指定协议,浏览器默认采用HTTP协议下载。 ```html 上面的`example.js`默认就是采用HTTP协议下载,如果要采用HTTPs协议下载,必需写明(假定服务器支持)。 ```html 但是有时我们会希望,根据页面本身的协议来决定加载协议,这时可以采用下面的写法。 ```html ## JavaScript虚拟机 JavaScript是一种解释型语言,也就是说,它不需要编译,可以由解释器实时运行。这样的好处是运行和修改都比较方便,刷新页面就可以重新解释;缺点是每次运行都要调用解释器,系统开销较大,运行速度慢于编译型语言。为了提高运行速度,目前的浏览器都将JavaScript进行一定程度的编译,生成类似字节码(bytecode)的中间代码,以提高运行速度。 早期,浏览器内部对JavaScript的处理过程如下: 1. 读取代码,进行词法分析(Lexical analysis),将代码分解成词元(token)。 2. 对词元进行语法分析(parsing),将代码整理成“语法树”(syntax tree)。 3. 使用“翻译器”(translator),将代码转为字节码(bytecode)。 4. 使用“字节码解释器”(bytecode interpreter),将字节码转为机器码。 逐行解释将字节码转为机器码,是很低效的。为了提高运行速度,现代浏览器改为采用“即时编译”(Just In Time compiler,缩写JIT),即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存(inline cache)。通常,一个程序被经常用到的,只是其中一小部分代码,有了缓存的编译结果,整个程序的运行速度就会显著提升。 不同的浏览器有不同的编译策略。有的浏览器只编译最经常用到的部分,比如循环的部分;有的浏览器索性省略了字节码的翻译步骤,直接编译成机器码,比如chrome浏览器的V8引擎。 字节码不能直接运行,而是运行在一个虚拟机(Virtual Machine)之上,一般也把虚拟机称为JavaScript引擎。因为JavaScript运行时未必有字节码,所以JavaScript虚拟机并不完全基于字节码,而是部分基于源码,即只要有可能,就通过JIT(just in time)编译器直接把源码编译成机器码运行,省略字节码步骤。这一点与其他采用虚拟机(比如Java)的语言不尽相同。这样做的目的,是为了尽可能地优化代码、提高性能。下面是目前最常见的一些JavaScript虚拟机: - [Chakra](http://en.wikipedia.org/wiki/Chakra_(JScript_engine\))(Microsoft Internet Explorer) - [Nitro/JavaScript Core](http://en.wikipedia.org/wiki/WebKit#JavaScriptCore) (Safari) - [Carakan](http://dev.opera.com/articles/view/labs-carakan/) (Opera) - [SpiderMonkey](https://developer.mozilla.org/en-US/docs/SpiderMonkey) (Firefox) - [V8](http://en.wikipedia.org/wiki/V8_(JavaScript_engine\)) (Chrome, Chromium) ## 单线程模型 ### 含义 首先,明确一个观念:JavaScript只在一个线程上运行,不代表JavaScript引擎只有一个线程。事实上,JavaScript引擎有多个线程,其中单个脚本只能在一个线程上运行,其他线程都是在后台配合。JavaScript脚本在一个线程里运行。这意味着,一次只能运行一个任务,其他任务都必须在后面排队等待。 JavaScript之所以采用单线程,而不是多线程,跟历史有关系。JavaScript从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。 单线程模型带来了一些问题,主要是新的任务被加在队列的尾部,只有前面的所有任务运行结束,才会轮到它执行。如果有一个任务特别耗时,后面的任务都会停在那里等待,造成浏览器失去响应,又称“假死”。为了避免“假死”,当某个操作在一定时间后仍无法结束,浏览器就会跳出提示框,询问用户是否要强行停止脚本运行。 如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。JavaScript语言的设计者意识到,这时CPU完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是JavaScript内部采用的Event Loop。 ### 消息队列 JavaScript运行时,除了一根运行线程,系统还提供一个消息队列(message queue),里面是各种需要当前程序处理的消息。新的消息进入队列的时候,会自动排在队列的尾端。 运行线程只要发现消息队列不为空,就会取出排在第一位的那个消息,执行它对应的回调函数。等到执行完,再取出排在第二位的消息,不断循环,直到消息队列变空为止。 每条消息与一个回调函数相联系,也就是说,程序只要收到这条消息,就会执行对应的函数。另一方面,进入消息队列的消息,必须有对应的回调函数。否则这个消息就会遗失,不会进入消息队列。举例来说,鼠标点击就会产生一条消息,报告`click`事件发生了。如果没有回调函数,这个消息就遗失了。如果有回调函数,这个消息进入消息队列。等到程序收到这个消息,就会执行click事件的回调函数。 另一种情况是`setTimeout`会在指定时间向消息队列添加一条消息。如果消息队列之中,此时没有其他消息,这条消息会立即得到处理;否则,这条消息会不得不等到其他消息处理完,才会得到处理。因此,`setTimeout`指定的执行时间,只是一个最早可能发生的时间,并不能保证一定会在那个时间发生。 一旦当前执行栈空了,消息队列就会取出排在第一位的那条消息,传入程序。程序开始执行对应的回调函数,等到执行完,再处理下一条消息。 ### Event Loop 所谓Event Loop,指的是一种内部循环,用来一轮又一轮地处理消息队列之中的消息,即执行对应的回调函数。[Wikipedia](http://en.wikipedia.org/wiki/Event_loop)的定义是:“**Event Loop是一个程序结构,用于等待和发送消息和事件**(a programming construct that waits for and dispatches events or messages in a program)”。可以就把Event Loop理解成动态更新的消息队列本身。 下面是一些常见的JavaScript任务。 - 执行JavaScript代码 - 对用户的输入(包含鼠标点击、键盘输入等等)做出反应 - 处理异步的网络请求 所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在JavaScript执行进程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入JavaScript执行进程、而进入“任务队列”(task queue)的任务,只有“任务队列”通知主进程,某个异步任务可以执行了,该任务(采用回调函数的形式)才会进入JavaScript进程执行。 以Ajax操作为例,它可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着Ajax操作返回结果,再往下执行;如果是异步任务,该任务直接进入“任务队列”,JavaScript进程跳过Ajax操作,直接往下执行,等到Ajax操作有了结果,JavaScript进程再执行对应的回调函数。 也就是说,虽然JavaScript只有一根进程用来执行,但是并行的还有其他进程(比如,处理定时器的进程、处理用户输入的进程、处理网络通信的进程等等)。这些进程通过向任务队列添加任务,实现与JavaScript进程通信。 想要理解Event Loop,就要从程序的运行模式讲起。运行以后的程序叫做"进程"(process),一般情况下,一个进程一次只能执行一个任务。如果有很多任务需要执行,不外乎三种解决方法。 1. **排队。**因为一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务。 2. **新建进程。**使用fork命令,为每个任务新建一个进程。 3. **新建线程。**因为进程太耗费资源,所以如今的程序往往允许一个进程包含多个线程,由线程去完成任务。 如果某个任务很耗时,比如涉及很多I/O(输入/输出)操作,那么线程的运行大概是下面的样子。 ![synchronous mode](http://image.beekka.com/blog/201310/2013102002.png) 上图的绿色部分是程序的运行时间,红色部分是等待时间。可以看到,由于I/O操作很慢,所以这个线程的大部分运行时间都在空等I/O操作的返回结果。这种运行方式称为"同步模式"(synchronous I/O)。 如果采用多线程,同时运行多个任务,那很可能就是下面这样。 ![synchronous mode](http://image.beekka.com/blog/201310/2013102003.png) 上图表明,多线程不仅占用多倍的系统资源,也闲置多倍的资源,这显然不合理。 ![asynchronous mode](http://image.beekka.com/blog/201310/2013102004.png) 上图主线程的绿色部分,还是表示运行时间,而橙色部分表示空闲时间。每当遇到I/O的时候,主线程就让Event Loop线程去通知相应的I/O程序,然后接着往后运行,所以不存在红色的等待时间。等到I/O程序完成操作,Event Loop线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。 可以看到,由于多出了橙色的空闲时间,所以主线程得以运行更多的任务,这就提高了效率。这种运行方式称为"[异步模式](http://en.wikipedia.org/wiki/Asynchronous_I/O)"(asynchronous I/O)。 这正是JavaScript语言的运行方式。单线程模型虽然对JavaScript构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果部署得好,JavaScript程序是不会出现堵塞的,这就是为什么node.js平台可以用很少的资源,应付大流量访问的原因。 如果有大量的异步任务(实际情况就是这样),它们会在“消息队列”中产生大量的消息。这些消息排成队,等候进入主线程。本质上,“消息队列”就是一个“先进先出”的数据结构。比如,点击鼠标就产生一系列消息(各种事件),`mousedown`事件排在`mouseup`事件前面,`mouseup`事件又排在`click`事件的前面。

定时器

JavaScript提供定时执行代码的功能,叫做定时器(timer),主要由`setTimeout()`和`setInterval()`这两个函数来完成。它们向任务队列添加定时任务。 ## setTimeout() `setTimeout`函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。 ```javascript var timerId = setTimeout(func|code, delay) 上面代码中,`setTimeout`函数接受两个参数,第一个参数`func|code`是将要推迟执行的函数名或者一段代码,第二个参数`delay`是推迟执行的毫秒数。 ```javascript console.log(1); setTimeout('console.log(2)',1000); console.log(3); 上面代码的输出结果就是1,3,2,因为`setTimeout`指定第二行语句推迟1000毫秒再执行。 需要注意的是,推迟执行的代码必须以字符串的形式,放入setTimeout,因为引擎内部使用eval函数,将字符串转为代码。如果推迟执行的是函数,则可以直接将函数名,放入setTimeout。一方面eval函数有安全顾虑,另一方面为了便于JavaScript引擎优化代码,setTimeout方法一般总是采用函数名的形式,就像下面这样。 ```javascript function f(){ console.log(2); setTimeout(f,1000); // 或者 setTimeout(function (){console.log(2)},1000); 如果省略`setTimeout`的第二个参数,则该参数默认为0。 除了前两个参数,setTimeout还允许添加更多的参数。它们将被传入推迟执行的函数(回调函数)。 ```javascript setTimeout(function(a,b){ console.log(a+b); },1000,1,1); 上面代码中,setTimeout共有4个参数。最后那两个参数,将在1000毫秒之后回调函数执行时,作为回调函数的参数。 IE 9.0及以下版本,只允许setTimeout有两个参数,不支持更多的参数。这时有三种解决方法。第一种是在一个匿名函数里面,让回调函数带参数运行,再把匿名函数输入setTimeout。 ```javascript setTimeout(function() { myFunc("one", "two", "three"); }, 1000); 上面代码中,myFunc是真正要推迟执行的函数,有三个参数。如果直接放入setTimeout,低版本的IE不能带参数,所以可以放在一个匿名函数。 第二种解决方法是使用bind方法,把多余的参数绑定在回调函数上面,生成一个新的函数输入setTimeout。 ```javascript setTimeout(function(arg1){}.bind(undefined, 10), 1000); 上面代码中,bind方法第一个参数是undefined,表示将原函数的this绑定全局作用域,第二个参数是要传入原函数的参数。它运行后会返回一个新函数,该函数不带参数。 第三种解决方法是自定义setTimeout,使用apply方法将参数输入回调函数。 ```html 除了参数问题,setTimeout还有一个需要注意的地方:如果被setTimeout推迟执行的回调函数是某个对象的方法,那么该方法中的this关键字将指向全局环境,而不是定义时所在的那个对象。 ```javascript var x = 1; var o = { x: 2, y: function(){ console.log(this.x); setTimeout(o.y,1000); 上面代码输出的是1,而不是2,这表示`o.y`的this所指向的已经不是o,而是全局环境了。 再看一个不容易发现错误的例子。 ```javascript function User(login) { this.login = login; this.sayHi = function() { console.log(this.login); var user = new User('John'); setTimeout(user.sayHi, 1000); 上面代码只会显示undefined,因为等到user.sayHi执行时,它是在全局对象中执行,所以this.login取不到值。 为了防止出现这个问题,一种解决方法是将user.sayHi放在函数中执行。 ```javascript setTimeout(function() { user.sayHi(); }, 1000); 上面代码中,sayHi是在user作用域内执行,而不是在全局作用域内执行,所以能够显示正确的值。 另一种解决方法是,使用bind方法,将绑定sayHi绑定在user上面。 ```javascript setTimeout(user.sayHi.bind(user), 1000); HTML 5标准规定,setTimeout的最短时间间隔是4毫秒。为了节电,对于那些不处于当前窗口的页面,浏览器会将时间间隔扩大到1000毫秒。另外,如果笔记本电脑处于电池供电状态,Chrome和IE 9以上的版本,会将时间间隔切换到系统定时器,大约是15.6毫秒。 ## setInterval() `setInterval`函数的用法与`setTimeout`完全一致,区别仅仅在于`setInterval`指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。 ```html 上面代码表示每隔1000毫秒就输出一个2,直到用户点击了停止按钮。 与`setTimeout`一样,除了前两个参数,`setInterval`方法还可以接受更多的参数,它们会传入回调函数,下面是一个例子。 ```javascript function f(){ for (var i=0;i= 0) { div.style.opacity = opacity; } else { clearInterval(fader); }, 100); 上面代码每隔100毫秒,设置一次`div`元素的透明度,直至其完全透明为止。 `setInterval`的一个常见用途是实现轮询。下面是一个轮询URL的Hash值是否发生变化的例子。 ```javascript var hash = window.location.hash; var hashWatcher = setInterval(function() { if (window.location.hash != hash) { updatePage(); }, 1000); setInterval指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterval指定每100ms执行一次,每次执行需要5ms,那么第一次执行结束后95毫秒,第二次执行就会开始。如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。 为了确保两次执行之间有固定的间隔,可以不用setInterval,而是每次执行结束后,使用setTimeout指定下一次执行的具体时间。 ```javascript var i = 1; var timer = setTimeout(function() { alert(i++); timer = setTimeout(arguments.callee, 2000); }, 2000); 上面代码可以确保,下一个对话框总是在关闭上一个对话框之后2000毫秒弹出。 根据这种思路,可以自己部署一个函数,实现间隔时间确定的setInterval的效果。 ```javascript function interval(func, wait){ var interv = function(){ func.call(null); setTimeout(interv, wait); setTimeout(interv, wait); interval(function(){ console.log(2); },1000); 上面代码部署了一个interval函数,用循环调用setTimeout模拟了setInterval。 HTML 5标准规定,setInterval的最短间隔时间是10毫秒,也就是说,小于10毫秒的时间间隔会被调整到10毫秒。 ## clearTimeout(),clearInterval() setTimeout和setInterval函数,都返回一个表示计数器编号的整数值,将该整数传入clearTimeout和clearInterval函数,就可以取消对应的定时器。 ```javascript var id1 = setTimeout(f,1000); var id2 = setInterval(f,1000); clearTimeout(id1); clearInterval(id2); setTimeout和setInterval返回的整数值是连续的,也就是说,第二个setTimeout方法返回的整数值,将比第一个的整数值大1。利用这一点,可以写一个函数,取消当前所有的setTimeout。 ```javascript (function() { var gid = setInterval(clearAllTimeouts, 0); function clearAllTimeouts() { var id = setTimeout(function() {}, 0); while (id > 0) { if (id !== gid) { clearTimeout(id); id--; })(); 运行上面代码后,实际上再设置任何setTimeout都无效了。 下面是一个clearTimeout实际应用的例子。有些网站会实时将用户在文本框的输入,通过Ajax方法传回服务器,jQuery的写法如下。 ```javascript $('textarea').on('keydown', ajaxAction); 这样写有一个很大的缺点,就是如果用户连续击键,就会连续触发keydown事件,造成大量的Ajax通信。这是不必要的,而且很可能会发生性能问题。正确的做法应该是,设置一个门槛值,表示两次Ajax通信的最小间隔时间。如果在设定的时间内,发生新的keydown事件,则不触发Ajax通信,并且重新开始计时。如果过了指定时间,没有发生新的keydown事件,将进行Ajax通信将数据发送出去。 这种做法叫做debounce(防抖动)方法,用来返回一个新函数。只有当两次触发之间的时间间隔大于事先设定的值,这个新函数才会运行实际的任务。假定两次Ajax通信的间隔不小于2500毫秒,上面的代码可以改写成下面这样。 ```javascript $('textarea').on('keydown', debounce(ajaxAction, 2500)) 利用setTimeout和clearTimeout,可以实现debounce方法。该方法用于防止某个函数在短时间内被密集调用,具体来说,debounce方法返回一个新版的该函数,这个新版函数调用后,只有在指定时间内没有新的调用,才会执行,否则就重新计时。 ```javascript function debounce(fn, delay){ var timer = null; // 声明计时器 return function(){ var context = this; var args = arguments; clearTimeout(timer); timer = setTimeout(function(){ fn.apply(context, args); }, delay); // 用法示例 var todoChanges = _.debounce(batchLog, 1000); Object.observe(models.todo, todoChanges); 现实中,最好不要设置太多个setTimeout和setInterval,它们耗费CPU。比较理想的做法是,将要推迟执行的代码都放在一个函数里,然后只对这个函数使用setTimeout或setInterval。 ## 运行机制 setTimeout和setInterval的运行机制是,将指定的代码移出本次执行,等到下一轮Event Loop时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮Event Loop时重新判断。这意味着,setTimeout指定的代码,必须等到本次执行的所有代码都执行完,才会执行。 每一轮Event Loop时,都会将“任务队列”中需要执行的任务,一次执行完。setTimeout和setInterval都是把任务添加到“任务队列”的尾部。因此,它们实际上要等到当前脚本的所有同步任务执行完,然后再等到本次Event Loop的“任务队列”的所有任务执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeout和setInterval指定的任务,一定会按照预定时间执行。 ```javascript setTimeout(someTask,100); veryLongTask(); 上面代码的setTimeout,指定100毫秒以后运行一个任务。但是,如果后面立即运行的任务(当前脚本的同步任务))非常耗时,过了100毫秒还无法结束,那么被推迟运行的someTask就只有等着,等到前面的veryLongTask运行结束,才轮到它执行。 这一点对于setInterval影响尤其大。 ```javascript setInterval(function(){ console.log(2); },1000); (function (){ sleeping(3000); })(); 上面的第一行语句要求每隔1000毫秒,就输出一个2。但是,第二行语句需要3000毫秒才能完成,请问会发生什么结果? 结果就是等到第二行语句运行完成以后,立刻连续输出三个2,然后开始每隔1000毫秒,输出一个2。也就是说,setIntervel具有累积效应,如果某个操作特别耗时,超过了setInterval的时间间隔,排在后面的操作会被累积起来,然后在很短的时间内连续触发,这可能或造成性能问题(比如集中发出Ajax请求)。 为了进一步理解JavaScript的单线程模型,请看下面这段伪代码。 ```javascript function init(){ { 耗时5ms的某个操作 } 触发mouseClickEvent事件 { 耗时5ms的某个操作 } setInterval(timerTask,10); { 耗时5ms的某个操作 } function handleMouseClick(){ 耗时8ms的某个操作 function timerTask(){ 耗时2ms的某个操作 请问调用init函数后,这段代码的运行顺序是怎样的? - **0-15ms**:运行init函数。 - **15-23ms**:运行handleMouseClick函数。请注意,这个函数是在5ms时触发的,应该在那个时候就立即运行,但是由于单线程的关系,必须等到init函数完成之后再运行。 - **23-25ms**:运行timerTask函数。这个函数是在10ms时触发的,规定每10ms运行一次,即在20ms、30ms、40ms等时候运行。由于20ms时,JavaScript线程还有任务在运行,因此必须延迟到前面任务完成时再运行。 - **30-32ms**:运行timerTask函数。 - **40-42ms**:运行timerTask函数。 ## setTimeout(f,0) ### 含义 `setTimeout`的作用是将代码推迟到指定时间执行,如果指定时间为`0`,即`setTimeout(f, 0)`,那么会立刻执行吗? 答案是不会。因为上一段说过,必须要等到当前脚本的同步任务和“任务队列”中已有的事件,全部处理完以后,才会执行`setTimeout`指定的任务。也就是说,setTimeout的真正作用是,在“消息队列”的现有消息的后面再添加一个消息,规定在指定时间执行某段代码。`setTimeout`添加的事件,会在下一次`Event Loop`执行。 `setTimeout(f, 0)`将第二个参数设为`0`,作用是让`f`在现有的任务(脚本的同步任务和“消息队列”指定的任务)一结束就立刻执行。也就是说,`setTimeout(f, 0)`的作用是,尽可能早地执行指定的任务。而并不是会立刻就执行这个任务。 ```javascript setTimeout(function () { console.log('你好!'); }, 0); 上面代码的含义是,尽可能早地显示“你好!”。 `setTimeout(f, 0)`指定的任务,最早也要到下一次Event Loop才会执行。请看下面的例子。 ```javascript setTimeout(function() { console.log("Timeout"); }, 0); function a(x) { console.log("a() 开始运行"); b(x); console.log("a() 结束运行"); function b(y) { console.log("b() 开始运行"); console.log("传入的值为" + y); console.log("b() 结束运行"); console.log("当前任务开始"); a(42); console.log("当前任务结束"); // 当前任务开始 // a() 开始运行 // b() 开始运行 // 传入的值为42 // b() 结束运行 // a() 结束运行 // 当前任务结束 // Timeout 上面代码说明,`setTimeout(f, 0)`必须要等到当前脚本的所有同步任务结束后才会执行。 即使消息队列是空的,0毫秒实际上也是达不到的。根据[HTML 5标准](http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#timers),`setTimeOut`推迟执行的时间,最少是4毫秒。如果小于这个值,会被自动增加到4。这是为了防止多个`setTimeout(f, 0)`语句连续执行,造成性能问题。 另一方面,浏览器内部使用32位带符号的整数,来储存推迟执行的时间。这意味着`setTimeout`最多只能推迟执行2147483647毫秒(24.8天),超过这个时间会发生溢出,导致回调函数将在当前任务队列结束后立即执行,即等同于`setTimeout(f, 0)`的效果。 ### 应用 setTimeout(f,0)有几个非常重要的用途。它的一大应用是,可以调整事件的发生顺序。比如,网页开发中,某个事件先发生在子元素,然后冒泡到父元素,即子元素的事件回调函数,会早于父元素的事件回调函数触发。如果,我们先让父元素的事件回调函数先发生,就要用到setTimeout(f, 0)。 ```javascript var input = document.getElementsByTagName('input[type=button]')[0]; input.onclick = function A() { setTimeout(function B() { input.value +=' input'; }, 0) document.body.onclick = function C() { input.value += ' body' 上面代码在点击按钮后,先触发回调函数A,然后触发函数C。在函数A中,setTimeout将函数B推迟到下一轮Loop执行,这样就起到了,先触发父元素的回调函数C的目的了。 用户自定义的回调函数,通常在浏览器的默认动作之前触发。比如,用户在输入框输入文本,keypress事件会在浏览器接收文本之前触发。因此,下面的回调函数是达不到目的的。 ```javascript document.getElementById('input-box').onkeypress = function(event) { this.value = this.value.toUpperCase(); 上面代码想在用户输入文本后,立即将字符转为大写。但是实际上,它只能将上一个字符转为大写,因为浏览器此时还没接收到文本,所以`this.value`取不到最新输入的那个字符。只有用setTimeout改写,上面的代码才能发挥作用。 ```javascript document.getElementById('my-ok').onkeypress = function() { var self = this; setTimeout(function() { self.value = self.value.toUpperCase(); }, 0); 上面代码将代码放入setTimeout之中,就能使得它在浏览器接收到文本之后触发。 由于setTimeout(f,0)实际上意味着,将任务放到浏览器最早可得的空闲时段执行,所以那些计算量大、耗时长的任务,常常会被放到几个小部分,分别放到setTimeout(f,0)里面执行。 ```javascript var 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)`的样子,性能压力就会减轻。 ## 正常任务与微任务 正常情况下,JavaScript的任务是同步执行的,即执行完前一个任务,然后执行后一个任务。只有遇到异步任务的情况下,执行顺序才会改变。 这时,需要区分两种任务:正常任务(task)与微任务(microtask)。它们的区别在于,“正常任务”在下一轮Event Loop执行,“微任务”在本轮Event Loop的所有任务结束后执行。 ```javascript console.log(1); setTimeout(function() { console.log(2); }, 0); Promise.resolve().then(function() { console.log(3); }).then(function() { console.log(4); console.log(5); 上面代码的执行结果说明,`setTimeout(fn, 0)`在`Promise.resolve`之后执行。 这是因为`setTimeout`语句指定的是“正常任务”,即不会在当前的Event Loop执行。而Promise会将它的回调函数,在状态改变后的那一轮Event Loop指定为微任务。所以,3和4输出在5之后、2之前。 除了`setTimeout`,正常任务还包括各种事件(比如鼠标单击事件)的回调函数。微任务目前主要就是Promise。

window对象

## 概述 JavaScript的所有对象都存在于一个运行环境之中,这个运行环境本身也是对象,称为“顶层对象”。这就是说,JavaScript的所有对象,都是“顶层对象”的下属。不同的运行环境有不同的“顶层对象”,在浏览器环境中,这个顶层对象就是`window`对象(`w`为小写)。 所有浏览器环境的全局变量,都是`window`对象的属性。 ```javascript var a = 1; window.a // 1 上面代码中,变量`a`是一个全局变量,但是实质上它是`window`对象的属性。声明一个全局变量,就是为`window`对象的同名属性赋值。 可以简单理解成,`window`就是指当前的浏览器窗口。 从语言设计的角度看,所有变量都是`window`对象的属性,其实不是很合理。因为`window`对象有自己的实体含义,不适合当作最高一层的顶层对象。这个设计失误与JavaScript语言匆忙的设计过程有关,最早的设想是语言内置的对象越少越好,这样可以提高浏览器的性能。因此,语言设计者Brendan Eich就把`window`对象当作顶层对象,所有未声明就赋值的变量都自动变成`window`对象的属性。这种设计使得编译阶段无法检测未声明变量,但到了今天已经没有办法纠正了。 ## 窗口的大小和位置 浏览器提供一系列属性,用来获取浏览器窗口的大小和位置。 (1)window.screenX,window.screenY `window.screenX`和`window.screenY`属性,返回浏览器窗口左上角相对于当前屏幕左上角(`(0, 0)`)的水平距离和垂直距离,单位为像素。 (2)window.innerHeight,window.innerWidth `window.innerHeight`和`window.innerWidth`属性,返回网页在当前窗口中可见部分的高度和宽度,即“视口”(viewport),单位为像素。 当用户放大网页的时候(比如将网页从100%的大小放大为200%),这两个属性会变小。因为这时网页的像素大小不变,只是每个像素占据的屏幕空间变大了,因为可见部分(视口)就变小了。 注意,这两个属性值包括滚动条的高度和宽度。 (3)window.outerHeight,window.outerWidth `window.outerHeight`和`window.outerWidth`属性返回浏览器窗口的高度和宽度,包括浏览器菜单和边框,单位为像素。 (4)window.pageXOffset属性,window.pageYOffset属性 `window.pageXOffset`属性返回页面的水平滚动距离,`window.pageYOffset`属性返回页面的垂直滚动距离,单位都为像素。 ## window对象的属性 ### window.closed `window.closed`属性返回一个布尔值,表示指定窗口是否关闭,通常用来检查通过脚本新建的窗口。 ```javascript popup.closed // false 上面代码检查跳出窗口是否关闭。 ### window.opener `window.opener`属性返回打开当前窗口的父窗口。如果当前窗口没有父窗口,则返回`null`。 ```javascript var windowA = window.opener; 通过`opener`属性,可以获得父窗口的的全局变量和方法,比如`windowA.window.propertyName`和`windowA.window.functionName()`。 该属性只适用于两个窗口属于同源的情况(参见《同源政策》一节),且其中一个窗口由另一个打开。 ### window.name `window.name`属性用于设置当前浏览器窗口的名字。 ```javascript window.name = 'Hello World!'; console.log(window.name) // "Hello World!" 各个浏览器对这个值的储存容量有所不同,但是一般来说,可以高达几MB。 它有一个重要特点,就是只要是本窗口打开的网页,都能读写该属性,不管这些网页是否属于同一个网站。所以,可以把值存放在该属性内,然后让另一个网页读取,从而实现跨域通信(详见《同源政策》一节)。 该属性只能保存字符串,且当浏览器窗口关闭后,所保存的值就会消失。因此局限性比较大,但是与iframe窗口通信时,非常有用。 ### window.location `window.location`返回一个`location`对象,用于获取窗口当前的URL信息。它等同于`document.location`对象。 ```javascript window.location === document.location // true ## 框架窗口 `window.frames`属性返回一个类似数组的对象,成员为页面内所有框架窗口,包括`frame`元素和`iframe`元素。`window.frames[0]`表示页面中第一个框架窗口,`window.frames['someName']`则是根据框架窗口的`name`属性的值(不是`id`属性),返回该窗口。另外,通过`document.getElementById()`方法也可以引用指定的框架窗口。 ```javascript var frame = document.getElementById('theFrame'); var frameWindow = frame.contentWindow; // 等同于 frame.contentWindow.document var frameDoc = frame.contentDocument; // 获取子窗口的变量和属性 frameWindow.function() `window.length`属性返回当前页面中所有框架窗口总数。 ```javascript window.frames.length === window.length // true `window.frames.length`与`window.length`应该是相等的。 由于传统的`frame`窗口已经不建议使用了,这里主要介绍`iframe`窗口。 需要注意的是,`window.frames`的每个成员对应的是框架内的窗口(即框架的`window`对象)。如果要获取每个框架内部的DOM树,需要使用`window.frames[0].document`的写法。 ```javascript var iframe = window.getElementsByTagName('iframe')[0]; var iframe_title = iframe.contentWindow.title; 上面代码用于获取`iframe`页面的标题。 `iframe`元素遵守同源政策,只有当父页面与框架页面来自同一个域名,两者之间才可以用脚本通信,否则只有使用window.postMessage方法。 `iframe`窗口内部,使用`window.parent`引用父窗口。如果当前页面没有父窗口,则`window.parent`属性返回自身。因此,可以通过`window.parent`是否等于`window.self`,判断当前窗口是否为`iframe`窗口。 ```javascript if (window.parent != window.self) { // 当前窗口是子窗口 ## navigator对象 Window对象的navigator属性,指向一个包含浏览器相关信息的对象。 **(1)navigator.userAgent属性** navigator.userAgent属性返回浏览器的User-Agent字符串,用来标示浏览器的种类。下面是Chrome浏览器的User-Agent。 ```javascript navigator.userAgent // "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36" 通过userAgent属性识别浏览器,不是一个好办法。因为必须考虑所有的情况(不同的浏览器,不同的版本),非常麻烦,而且无法保证未来的适用性,更何况各种上网设备层出不穷,难以穷尽。所以,现在一般不再识别浏览器了,而是使用“功能识别”方法,即逐一测试当前浏览器是否支持要用到的JavaScript功能。 不过,通过userAgent可以大致准确地识别手机浏览器,方法就是测试是否包含“mobi”字符串。 ```javascript var ua = navigator.userAgent.toLowerCase(); if (/mobi/i.test(ua)) { // 手机浏览器 } else { // 非手机浏览器 如果想要识别所有移动设备的浏览器,可以测试更多的特征字符串。 ```javascript /mobi|android|touch|mini/i.test(ua) **(2)navigator.plugins属性** navigator.plugins属性返回一个类似数组的对象,成员是浏览器安装的插件,比如Flash、ActiveX等。 ## window.screen对象 `window.screen`对象包含了显示设备的信息。 `screen.height`和`screen.width`两个属性,一般用来了解设备的分辨率。 ```javascript // 显示设备的高度,单位为像素 screen.height // 1920 // 显示设备的宽度,单位为像素 screen.width // 1080 上面代码显示,某设备的分辨率是1920x1080。 除非调整显示器的分辨率,否则这两个值可以看作常量,不会发生变化。显示器的分辨率与浏览器设置无关,缩放网页并不会改变分辨率。 下面是根据屏幕分辨率,将用户导向不同网页的代码。 ```javascript if ((screen.width <= 800) && (screen.height <= 600)) { window.location.replace('small.html'); } else { window.location.replace('wide.html'); `screen.availHeight`和`screen.availWidth`属性返回屏幕可用的高度和宽度,单位为像素。它们的值为屏幕的实际大小减去操作系统某些功能占据的空间,比如系统的任务栏。 `screen.colorDepth`属性返回屏幕的颜色深度,一般为16(表示16-bit)或24(表示24-bit)。 ## window对象的方法 ### window.moveTo(),window.moveBy() `window.moveTo`方法用于移动浏览器窗口到指定位置。它接受两个参数,分别是窗口左上角距离屏幕左上角的水平距离和垂直距离,单位为像素。 ```javascript window.moveTo(100, 200) 上面代码将窗口移动到屏幕`(100, 200)`的位置。 `window.moveBy`方法将窗口移动到一个相对位置。它接受两个参数,分布是窗口左上角向右移动的水平距离和向下移动的垂直距离,单位为像素。 ```javascript window.moveBy(25, 50) 上面代码将窗口向右移动25像素、向下移动50像素。 ### window.open(), window.close() `window.open`方法用于新建另一个浏览器窗口,并且返回该窗口对象。 ```javascript var popup = window.open('somefile.html'); `open`方法的第一个参数是新窗口打开的网址,此外还可以加上第二个参数,表示新窗口的名字,以及第三个参数用来指定新窗口的参数,形式是一个逗号分隔的`property=value`字符串。 下面是一个例子。 ```javascript var popup = window.open( 'somepage.html', 'DefinitionsWindows', 'height=200,width=200,location=no,resizable=yes,scrollbars=yes' 注意,如果在第三个参数中设置了一部分参数,其他没有被设置的`yes/no`参数都会被设成No,只有`titlebar`和关闭按钮除外(它们的值默认为yes)。 `open`方法返回新窗口的引用。 ```javascript var windowB = window.open('windowB.html', 'WindowB'); windowB.window.name // "WindowB" 由于`open`这个方法很容易被滥用,许多浏览器默认都不允许脚本新建窗口。因此,有必要检查一下打开新窗口是否成功。 ```javascript if (popup === null) { // 新建窗口失败 `window.close`方法用于关闭当前窗口,一般用来关闭`window.open`方法新建的窗口。 ```javascript popup.close() `window.closed`属性用于检查当前窗口是否被关闭了。 ```javascript if ((popup !== null) && !popup.closed) { // 窗口仍然打开着 ### window.print() `print`方法会跳出打印对话框,同用户点击菜单里面的“打印”命令效果相同。 页面上的打印按钮代码如下。 ```javascript document.getElementById('printLink').onclick = function() { window.print(); 非桌面设备(比如手机)可能没有打印功能,这时可以这样判断。 ```javascript if (typeof window.print === 'function') { // 支持打印功能 ### URL的编码/解码方法 JavaScript提供四个URL的编码/解码方法。 - decodeURI() - decodeURIComponent() - encodeURI() - encodeURIComponent() ### window.getComputedStyle方法 getComputedStyle方法接受一个HTML元素作为参数,返回一个包含该HTML元素的最终样式信息的对象。详见《DOM》一章的CSS章节。 ### window.matchMedia方法 window.matchMedia方法用来检查CSS的mediaQuery语句。详见《DOM》一章的CSS章节。 ### window.focus() `focus`方法会激活指定当前窗口,使其获得焦点。 ```javascript if ((popup !== null) && !popup.closed) { popup.focus(); 上面代码先检查`popup`窗口是否依然存在,确认后激活该窗口。 当前窗口获得焦点时,会触发`focus`事件;当前窗口失去焦点时,会触发`blur`事件。 ## window对象的事件 ### window.onerror 浏览器脚本发生错误时,会触发window对象的error事件。我们可以通过`window.onerror`属性对该事件指定回调函数。 ```javascript window.onerror = function (message, filename, lineno, colno, error) { console.log("出错了!--> %s", error.stack); error事件的回调函数,一共可以有五个参数,它们的含义依次如下。 - 出错信息 - 出错脚本的网址 - 错误对象 老式浏览器只支持前三个参数。 需要注意的是,如果脚本网址与网页网址不在同一个域(比如使用了CDN),浏览器根本不会提供详细的出错信息,只会提示出错,错误类型是“Script error.”,行号为0,其他信息都没有。这是浏览器防止向外部脚本泄漏信息。一个解决方法是在脚本所在的服务器,设置Access-Control-Allow-Origin的HTTP头信息。 ```bash Access-Control-Allow-Origin:* 然后,在网页的script标签中设置crossorigin属性。 ```html 上面代码的`crossorigin="anonymous"`表示,读取文件不需要身份信息,即不需要cookie和HTTP认证信息。如果设为`crossorigin="use-credentials"`,就表示浏览器会上传cookie和HTTP认证信息,同时还需要服务器端打开HTTP头信息Access-Control-Allow-Credentials。 并不是所有的错误,都会触发JavaScript的error事件(即让JavaScript报错),只限于以下三类事件。 - JavaScript语言错误 - JavaScript脚本文件不存在 - 图像文件不存在 以下两类事件不会触发JavaScript的error事件。 - CSS文件不存在 - iframe文件不存在 ## alert(),prompt(),confirm() `alert()`、`prompt()`、`confirm()`都是浏览器与用户互动的全局方法。它们会弹出不同的对话框,要求用户做出回应。 需要注意的是,`alert()`、`prompt()`、`confirm()`这三个方法弹出的对话框,都是浏览器统一规定的式样,是无法定制的。 `alert`方法弹出的对话框,只有一个“确定”按钮,往往用来通知用户某些信息。 ```javascript // 格式 alert(message); // 实例 alert('Hello World'); 用户只有点击“确定”按钮,对话框才会消失。在对话框弹出期间,浏览器窗口处于冻结状态,如果不点“确定”按钮,用户什么也干不了。 `prompt`方法弹出的对话框,在提示文字的下方,还有一个输入框,要求用户输入信息,并有“确定”和“取消”两个按钮。它往往用来获取用户输入的数据。 ```javascript // 格式 var result = prompt(text[, default]); // 实例 var result = prompt('您的年龄?', 25) 上面代码会跳出一个对话框,文字提示为“您的年龄?”,要求用户在对话框中输入自己的年龄(默认显示25)。 `alert`方法的参数只能是字符串,没法使用CSS样式,但是可以用`\n`指定换行。 ```javascript alert('本条提示\n分成两行'); `prompt`方法的返回值是一个字符串(有可能为空)或者`null`,具体分成三种情况。 1. 用户输入信息,并点击“确定”,则用户输入的信息就是返回值。 2. 用户没有输入信息,直接点击“确定”,则输入框的默认值就是返回值。 3. 用户点击了“取消”(或者按了Esc按钮),则返回值是`null`。 `prompt`方法的第二个参数是可选的,但是如果不提供的话,IE浏览器会在输入框中显示`undefined`。因此,最好总是提供第二个参数,作为输入框的默认值。 `confirm`方法弹出的对话框,除了提示信息之外,只有“确定”和“取消”两个按钮,往往用来征询用户的意见。 ```javascript // 格式 var result = confirm(message); // 实例 var result = confirm("你最近好吗?"); 上面代码弹出一个对话框,上面只有一行文字“你最近好吗?”,用户选择点击“确定”或“取消”。 `confirm`方法返回一个布尔值,如果用户点击“确定”,则返回`true`;如果用户点击“取消”,则返回`false`。 ```javascript var okay = confirm('Please confirm this message.'); if (okay) { // 用户按下“确定” } else { // 用户按下“取消” `confirm`的一个用途是,当用户离开当前页面时,弹出一个对话框,问用户是否真的要离开。 ```javascript window.onunload = function() { return confirm('你确定要离开当面页面吗?');

history对象

## 概述 浏览器窗口有一个`history`对象,用来保存浏览历史。 比如,当前窗口先后访问了三个地址,那么`history`对象就包括三项,`history.length`属性等于3。 ```javascript history.length // 3 `history`对象提供了一系列方法,允许在浏览历史之间移动。 - `back()`:移动到上一个访问页面,等同于浏览器的后退键。 - `forward()`:移动到下一个访问页面,等同于浏览器的前进键。 - `go()`:接受一个整数作为参数,移动到该整数指定的页面,比如`go(1)`相当于`forward()`,`go(-1)`相当于`back()`。 ```javascript history.back(); history.forward(); history.go(-2); 如果移动的位置超出了访问历史的边界,以上三个方法并不报错,而是默默的失败。 以下命令相当于刷新当前页面。 ```javascript history.go(0); 常见的“返回上一页”链接,代码如下。 ```javascript document.getElementById('backLink').onclick = function () { window.history.back(); 注意,返回上一页时,页面通常是从浏览器缓存之中加载,而不是重新要求服务器发送新的网页。 ## history.pushState(),history.replaceState() HTML5为history对象添加了两个新方法,history.pushState() 和 history.replaceState(),用来在浏览历史中添加和修改记录。所有主流浏览器都支持该方法(包括IE10)。 ```javascript if (!!(window.history && history.pushState)){ // 支持History API } else { // 不支持 上面代码可以用来检查,当前浏览器是否支持History API。如果不支持的话,可以考虑使用Polyfill库[History.js]( https://github.com/browserstate/history.js/)。 history.pushState方法接受三个参数,依次为: - **state**:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。 - **title**:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。 - **url**:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。 假定当前网址是`example.com/1.html`,我们使用pushState方法在浏览记录(history对象)中添加一个新记录。 ```javascript var stateObj = { foo: "bar" }; history.pushState(stateObj, "page 2", "2.html"); 添加上面这个新记录后,浏览器地址栏立刻显示`example.com/2.html`,但并不会跳转到2.html,甚至也不会检查2.html是否存在,它只是成为浏览历史中的最新记录。假定这时你访问了google.com,然后点击了倒退按钮,页面的url将显示2.html,但是内容还是原来的1.html。你再点击一次倒退按钮,url将显示1.html,内容不变。 > 注意,pushState方法不会触发页面刷新。 如果 pushState 的url参数,设置了一个当前网页的#号值(即hash),并不会触发hashchange事件。如果设置了一个非同域的网址,则会报错。 ```javascript // 报错 history.pushState(null, null, 'https://twitter.com/hello'); 上面代码中,pushState想要插入一个非同域的网址,导致报错。这样设计的目的是,防止恶意代码让用户以为他们是在另一个网站上。 `history.replaceState`方法的参数与`pushState`方法一模一样,区别是它修改浏览历史中当前页面的值。下面的例子假定当前网页是example.com/example.html。 ```javascript history.pushState({page: 1}, "title 1", "?page=1"); history.pushState({page: 2}, "title 2", "?page=2"); history.replaceState({page: 3}, "title 3", "?page=3"); history.back(); // url显示为http://example.com/example.html?page=1 history.back(); // url显示为http://example.com/example.html history.go(2); // url显示为http://example.com/example.html?page=3 ## history.state属性 history.state属性保存当前页面的state对象。 ```javascript history.pushState({page: 1}, "title 1", "?page=1"); history.state // { page: 1 } ## popstate事件 每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。需要注意的是,仅仅调用pushState方法或replaceState方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用JavaScript调用back、forward、go方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。 使用的时候,可以为popstate事件指定回调函数。这个回调函数的参数是一个event事件对象,它的state属性指向pushState和replaceState方法为当前url所提供的状态对象(即这两个方法的第一个参数)。 ```javascript window.onpopstate = function(event) { console.log("location: " + document.location); console.log("state: " + JSON.stringify(event.state)); // 或者 window.addEventListener('popstate', function(event) { console.log("location: " + document.location); console.log("state: " + JSON.stringify(event.state)); 上面代码中的event.state,就是通过pushState和replaceState方法,为当前url绑定的state对象。 这个state对象也可以直接通过history对象读取。 ```javascript var currentState = history.state; 另外,需要注意的是,当页面第一次加载的时候,在onload事件发生后,Chrome和Safari浏览器(Webkit核心)会触发popstate事件,而Firefox和IE浏览器不会。 ## URLSearchParams API URLSearchParams API用于处理URL之中的查询字符串,即问号之后的部分。没有部署这个API的浏览器,可以用[url-search-params](url-search-params)这个垫片库。 ```javascript var paramsString = 'q=URLUtils.searchParams&topic=api' var searchParams = new URLSearchParams(paramsString); URLSearchParams有以下方法,用来操作某个参数。 - `has()`:返回一个布尔值,表示是否具有某个参数 - `get()`:返回指定参数的第一个值 - `getAll()`:返回一个数组,成员是指定参数的所有值 - `set()`:设置指定参数 - `delete()`:删除指定参数 - `append()`:在查询字符串之中,追加一个键值对 - `toString()`:返回整个查询字符串 ```javascript var paramsString = "q=URLUtils.searchParams&topic=api" var searchParams = new URLSearchParams(paramsString); searchParams.has('topic') // true searchParams.get('topic') // "api" searchParams.getAll('topic') // ["api"] searchParams.get('foo') // null,注意Firefox返回空字符串 searchParams.set('foo', 2); searchParams.get('foo') // 2 searchParams.append('topic', 'webdev'); searchParams.toString() // "q=URLUtils.searchParams&topic=api&foo=2&topic=webdev" searchParams.append('foo', 3); searchParams.getAll('foo') // [2, 3] searchParams.delete('topic'); searchParams.toString() // "q=URLUtils.searchParams&foo=2&foo=3" URLSearchParams还有三个方法,用来遍历所有参数。 - `key()`:遍历所有参数名 - `values()`:遍历所有参数值 - `entries()`:遍历所有参数的键值对 上面三个方法返回的都是Iterator对象。 ```javascript var searchParams = new URLSearchParams('key1=value1&key2=value2'); for(var key of searchParams.keys()) { console.log(key); // key1 // key2 for(var value of searchParams.values()) { console.log(value); // value1 // value2 for(var pair of searchParams.entries()) { console.log(pair[0]+ ', '+ pair[1]); // key1, value1 // key2, value2 在Chrome浏览器之中,`URLSearchParams`实例本身就是Iterator对象,与`entries`方法返回值相同。所以,可以写成下面的样子。 ```javascript for (var p of searchParams) { console.log(p); 下面是一个替换当前URL的例子。 ```javascript // URL: https://example.com?version=1.0 var params = new URLSearchParams(location.search.slice(1)); params.set('version', 2.0); window.history.replaceState({}, '', `${location.pathname}?${params}`); // URL: https://example.com?version=2.0 `URLSearchParams`实例可以当作POST数据发送,所有数据都会URL编码。 ```javascript let params = new URLSearchParams(); params.append('api_key', '1234567890'); fetch('https://example.com/api', { method: 'POST', body: params }).then(...) DOM的`a`元素节点的`searchParams`属性,就是一个`URLSearchParams`实例。 ```javascript var a = document.createElement('a'); a.href = 'https://example.com?filter=api'; a.searchParams.get('filter') // "api" `URLSearchParams`还可以与`URL`接口结合使用。 ```javascript var url = new URL(location); var foo = url.searchParams.get('foo') || 'somedefault';

Cookie

## 概述 Cookie是服务器保存在浏览器的一小段文本信息,每个Cookie的大小一般不能超过4KB。浏览器每次向服务器发出请求,就会自动附上这段信息。 Cookie保存以下几方面的信息。 - Cookie的名字 - Cookie的值 - 到期时间 - 所属域名(默认是当前域名) - 生效的路径(默认是当前网址) 举例来说,如果当前URL是`www.example.com`,那么Cookie的路径就是根目录`/`。这意味着,这个Cookie对该域名的根路径和它的所有子路径都有效。如果路径设为`/forums`,那么这个Cookie只有在访问`www.example.com/forums`及其子路径时才有效。 浏览器可以设置不接受Cookie,也可以设置不向服务器发送Cookie。`window.navigator.cookieEnabled`属性返回一个布尔值,表示浏览器是否打开Cookie功能。 `document.cookie`属性返回当前网页的Cookie。 ```javascript // 读取当前网页的所有cookie var allCookies = document.cookie; 由于`document.cookie`返回的是分号分隔的所有Cookie,所以必须手动还原,才能取出每一个Cookie的值。 ```javascript var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { // cookies[i] name=value形式的单个Cookie `document.cookie`属性是可写的,可以通过它为当前网站添加Cookie。 ```javascript document.cookie = 'fontSize=14'; Cookie的值必须写成`key=value`的形式。注意,等号两边不能有空格。另外,写入Cookie的时候,必须对分号、逗号和空格进行转义(它们都不允许作为Cookie的值),这可以用`encodeURIComponent`方法达到。 但是,`document.cookie`一次只能写入一个Cookie,而且写入并不是覆盖,而是添加。 ```javascript document.cookie = 'test1=hello'; document.cookie = 'test2=world'; document.cookie // test1=hello;test2=world `document.cookie`属性读写行为的差异(一次可以读出全部Cookie,但是只能写入一个Cookie),与服务器与浏览器之间的Cookie通信格式有关。浏览器向服务器发送Cookie的时候,是一行将所有Cookie全部发送。 ```http GET /sample_page.html HTTP/1.1 Host: www.example.org Cookie: cookie_name1=cookie_value1; cookie_name2=cookie_value2 Accept: */* 上面的头信息中,`Cookie`字段是浏览器向服务器发送的Cookie。 服务器告诉浏览器需要储存Cookie的时候,则是分行指定。 ```http HTTP/1.0 200 OK Content-type: text/html Set-Cookie: cookie_name1=cookie_value1 Set-Cookie: cookie_name2=cookie_value2; expires=Sun, 16 Jul 3567 06:23:41 GMT 上面的头信息中,`Set-Cookie`字段是服务器写入浏览器的Cookie,一行一个。 如果仔细看浏览器向服务器发送的Cookie,就会意识到,Cookie协议存在问题。对于服务器来说,有两点是无法知道的。 - Cookie的各种属性,比如何时过期。 - 哪个域名设置的Cookie,因为Cookie可能是一级域名设的,也可能是任意一个二级域名设的。 ## Cookie的属性 除了Cookie本身的内容,还有一些可选的属性也是可以写入的,它们都必须以分号开头。 ```http Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure] 上面的`Set-Cookie`字段,用分号分隔多个属性。它们的含义如下。 (1)value属性 `value`属性是必需的,它是一个键值对,用于指定Cookie的值。 (2)expires属性 `expires`属性用于指定Cookie过期时间。它的格式采用`Date.toUTCString()`的格式。 如果不设置该属性,或者设为`null`,Cookie只在当前会话(session)有效,浏览器窗口一旦关闭,当前Session结束,该Cookie就会被删除。 浏览器根据本地时间,决定Cookie是否过期,由于本地时间是不精确的,所以没有办法保证Cookie一定会在服务器指定的时间过期。 (3)domain属性 `domain`属性指定Cookie所在的域名,比如`example.com`或`.example.com`(这种写法将对所有子域名生效)、`subdomain.example.com`。 如果未指定,默认为设定该Cookie的域名。所指定的域名必须是当前发送Cookie的域名的一部分,比如当前访问的域名是`example.com`,就不能将其设为`google.com`。只有访问的域名匹配domain属性,Cookie才会发送到服务器。 (4)path属性 `path`属性用来指定路径,必须是绝对路径(比如`/`、`/mydir`),如果未指定,默认为请求该Cookie的网页路径。 只有`path`属性匹配向服务器发送的路径,Cokie才会发送。这里的匹配不是绝对匹配,而是从根路径开始,只要`path`属性匹配发送路径的一部分,就可以发送。比如,`path`属性等于`/blog`,则发送路径是`/blog`或者`/blogroll`,Cookie都会发送。`path`属性生效的前提是`domain`属性匹配。 (5)secure `secure`属性用来指定Cookie只能在加密协议HTTPS下发送到服务器。 该属性只是一个开关,不需要指定值。如果通信是HTTPS协议,该开关自动打开。 (6)max-age `max-age`属性用来指定Cookie有效期,比如`60 * 60 * 24 * 365`(即一年31536e3秒)。 (7)HttpOnly `HttpOnly`属性用于设置该Cookie不能被JavaScript读取,详见下文的说明。 以上属性可以同时设置一个或多个,也没有次序的要求。如果服务器想改变一个早先设置的Cookie,必须同时满足四个条件:Cookie的`key`、`domain`、`path`和`secure`都匹配。也就是说,如果原始的Cookie是用如下的`Set-Cookie`设置的。 ```http Set-Cookie: key1=value1; domain=example.com; path=/blog 改变上面这个cookie的值,就必须使用同样的`Set-Cookie`。 ```http Set-Cookie: key1=value2; domain=example.com; path=/blog 只要有一个属性不同,就会生成一个全新的Cookie,而不是替换掉原来那个Cookie。 ```http Set-Cookie: key1=value2; domain=example.com; path=/ 上面的命令设置了一个全新的同名Cookie,但是`path`属性不一样。下一次访问`example.com/blog`的时候,浏览器将向服务器发送两个同名的Cookie。 ```http Cookie: key1=value1; key1=value2 上面代码的两个Cookie是同名的,匹配越精确的Cookie排在越前面。 浏览器设置这些属性的写法如下。 ```javascript document.cookie = 'fontSize=14; ' + 'expires=' + someDate.toGMTString() + '; ' + 'path=/subdirectory; ' + 'domain=*.example.com'; 另外,这些属性只能用来设置Cookie。一旦设置完成,就没有办法读取这些属性的值。 删除一个Cookie的简便方法,就是设置`expires`属性等于0,或者等于一个过去的日期。 ```javascript document.cookie = 'fontSize=;expires=Thu, 01-Jan-1970 00:00:01 GMT'; 上面代码中,名为`fontSize`的Cookie的值为空,过期时间设为1970年1月1月零点,就等同于删除了这个Cookie。 ## Cookie的限制 浏览器对Cookie数量的限制,规定不一样。目前,Firefox是每个域名最多设置50个Cookie,而Safari和Chrome没有域名数量的限制。 所有Cookie的累加长度限制为4KB。超过这个长度的Cookie,将被忽略,不会被设置。 由于Cookie可能存在数量限制,有时为了规避限制,可以将cookie设置成下面的形式。 ```http name=a=b&c=d&e=f&g=h 上面代码实际上是设置了一个Cookie,但是这个Cookie内部使用`&`符号,设置了多部分的内容。因此,读取这个Cookie的时候,就要自行解析,得到多个键值对。这样就规避了cookie的数量限制。 ## 同源政策 浏览器的同源政策规定,两个网址只要域名相同和端口相同,就可以共享Cookie。 注意,这里不要求协议相同。也就是说,`http://example.com`设置的Cookie,可以被`https://example.com`读取。 ## HTTP-Only Cookie 设置cookie的时候,如果服务器加上了`HTTPOnly`属性,则这个Cookie无法被JavaScript读取(即`document.cookie`不会返回这个Cookie的值),只用于向服务器发送。 ```http Set-Cookie: key=value; HttpOnly 上面的这个Cookie将无法用JavaScript获取。进行AJAX操作时,`XMLHttpRequest`对象也无法包括这个Cookie。这主要是为了防止XSS攻击盗取Cookie。

Web Storage:浏览器端数据储存机制

## 概述 这个API的作用是,使得网页可以在浏览器端储存数据。它分成两类:sessionStorage和localStorage。 sessionStorage保存的数据用于浏览器的一次会话,当会话结束(通常是该窗口关闭),数据被清空;localStorage保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。除了保存期限的长短不同,这两个对象的属性和方法完全一样。 它们很像cookie机制的强化版,能够动用大得多的存储空间。目前,每个域名的存储上限视浏览器而定,Chrome是2.5MB,Firefox和Opera是5MB,IE是10MB。其中,Firefox的存储空间由一级域名决定,而其他浏览器没有这个限制。也就是说,在Firefox中,`a.example.com`和`b.example.com`共享5MB的存储空间。另外,与Cookie一样,它们也受同域限制。某个网页存入的数据,只有同域下的网页才能读取。 通过检查window对象是否包含sessionStorage和localStorage属性,可以确定浏览器是否支持这两个对象。 ```javascript function checkStorageSupport() { // sessionStorage if (window.sessionStorage) { return true; } else { return false; // localStorage if (window.localStorage) { return true; } else { return false; ## 操作方法 ### 存入/读取数据 sessionStorage和localStorage保存的数据,都以“键值对”的形式存在。也就是说,每一项数据都有一个键名和对应的值。所有的数据都是以文本格式保存。 存入数据使用setItem方法。它接受两个参数,第一个是键名,第二个是保存的数据。 ```javascript sessionStorage.setItem("key","value"); localStorage.setItem("key","value"); 读取数据使用getItem方法。它只有一个参数,就是键名。 ```javascript var valueSession = sessionStorage.getItem("key"); var valueLocal = localStorage.getItem("key"); ### 清除数据 removeItem方法用于清除某个键名对应的数据。 ```javascript sessionStorage.removeItem('key'); localStorage.removeItem('key'); clear方法用于清除所有保存的数据。 ```javascript sessionStorage.clear(); localStorage.clear(); ### 遍历操作 利用length属性和key方法,可以遍历所有的键。 ```javascript for(var i = 0; i < localStorage.length; i++){ console.log(localStorage.key(i)); 其中的key方法,根据位置(从0开始)获得键值。 ```javascript localStorage.key(1); ## storage事件 当储存的数据发生变化时,会触发storage事件。我们可以指定这个事件的回调函数。 ```javascript window.addEventListener("storage",onStorageChange); 回调函数接受一个event对象作为参数。这个event对象的key属性,保存发生变化的键名。 ```javascript function onStorageChange(e) { console.log(e.key); 除了key属性,event对象的属性还有三个: - oldValue:更新前的值。如果该键为新增加,则这个属性为null。 - newValue:更新后的值。如果该键被删除,则这个属性为null。 - url:原始触发storage事件的那个网页的网址。 值得特别注意的是,该事件不在导致数据变化的当前页面触发。如果浏览器同时打开一个域名下面的多个页面,当其中的一个页面改变sessionStorage或localStorage的数据时,其他所有页面的storage事件会被触发,而原始页面并不触发storage事件。可以通过这种机制,实现多个窗口之间的通信。所有浏览器之中,只有IE浏览器除外,它会在所有页面触发storage事件。

同源政策

浏览器安全的基石是”同源政策“([same-origin policy](https://en.wikipedia.org/wiki/Same-origin_policy))。很多开发者都知道这一点,但了解得不全面。 本节详细介绍”同源政策“的各个方面,以及如何规避它。 ![](http://www.ruanyifeng.com/blogimg/asset/2016/bg2016040801.jpg) ## 概述 ### 含义 1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。 最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页“同源”。所谓“同源”指的是”三个相同“。 > - 协议相同 > - 域名相同 > - 端口相同 举例来说,`http://www.example.com/dir/page.html`这个网址,协议是`http://`,域名是`www.example.com`,端口是`80`(默认端口可以省略)。它的同源情况如下。 - `http://www.example.com/dir2/other.html`:同源 - `http://example.com/dir/other.html`:不同源(域名不同) - `http://v2.www.example.com/dir/other.html`:不同源(域名不同) - `http://www.example.com:81/dir/other.html`:不同源(端口不同) - `https://www.example.com/dir/page.html`:不同源(协议不同) ### 目的 同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。 设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么? 很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。 由此可见,”同源政策“是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。 ### 限制范围 随着互联网的发展,“同源政策”越来越严格。目前,如果非同源,共有三种行为受到限制。 > (1) Cookie、LocalStorage 和 IndexDB 无法读取。 > (2) DOM 无法获得。 > (3) AJAX 请求不能发送。 虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面,我将详细介绍,如何规避上面三种限制。 ## Cookie Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置`document.domain`共享 Cookie。 举例来说,A网页是`http://w1.example.com/a.html`,B网页是`http://w2.example.com/b.html`,那么只要设置相同的`document.domain`,两个网页就可以共享Cookie。 ```javascript document.domain = 'example.com'; 现在,A网页通过脚本设置一个 Cookie。 ```javascript document.cookie = "test1=hello"; B网页就可以读到这个 Cookie。 ```javascript var allCookie = document.cookie; 注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。 另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如`.example.com`。 ```http Set-Cookie: key=value; domain=.example.com; path=/ 这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。 ## iframe 如果两个网页不同源,就无法拿到对方的DOM。典型的例子是`iframe`窗口和`window.open`方法打开的窗口,它们与父窗口无法通信。 比如,父窗口运行下面的命令,如果`iframe`窗口不是同源,就会报错。 ```javascript document.getElementById("myIFrame").contentWindow.document // Uncaught DOMException: Blocked a frame from accessing a cross-origin frame. 上面命令中,父窗口想获取子窗口的DOM,因为跨源导致报错。 反之亦然,子窗口获取主窗口的DOM也会报错。 ```javascript window.parent.document.body // 报错 如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的`document.domain`属性,就可以规避同源政策,拿到DOM。 对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题。 > - 片段识别符(fragment identifier) > - window.name > - 跨文档通信API(Cross-document messaging) ### 片段识别符 片段标识符(fragment identifier)指的是,URL的`#`号后面的部分,比如`http://example.com/x.html#fragment`的`#fragment`。如果只是改变片段标识符,页面不会重新刷新。 父窗口可以把信息,写入子窗口的片段标识符。 ```javascript var src = originURL + '#' + data; document.getElementById('myIFrame').src = src; 子窗口通过监听`hashchange`事件得到通知。 ```javascript window.onhashchange = checkMessage; function checkMessage() { var message = window.location.hash; // ... 同样的,子窗口也可以改变父窗口的片段标识符。 ```javascript parent.location.href= target + “#” + hash; ### window.name 浏览器窗口有`window.name`属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。 父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入`window.name`属性。 ```javascript window.name = data; 接着,子窗口跳回一个与主窗口同域的网址。 ```javascript location = 'http://parent.url.com/xxx.html'; 然后,主窗口就可以读取子窗口的`window.name`了。 ```javascript var data = document.getElementById('myFrame').contentWindow.name; 这种方法的优点是,`window.name`容量很大,可以放置非常长的字符串;缺点是必须监听子窗口`window.name`属性的变化,影响网页性能。 ### window.postMessage 上面两种方法都属于破解,HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。 这个API为`window`对象新增了一个`window.postMessage`方法,允许跨窗口通信,不论这两个窗口是否同源。 举例来说,父窗口`aaa.com`向子窗口`bbb.com`发消息,调用`postMessage`方法就可以了。 ```javascript var popup = window.open('http://bbb.com', 'title'); popup.postMessage('Hello World!', 'http://bbb.com'); `postMessage`方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即“协议 + 域名 + 端口”。也可以设为`*`,表示不限制域名,向所有窗口发送。 子窗口向父窗口发送消息的写法类似。 ```javascript window.opener.postMessage('Nice to see you', 'http://aaa.com'); 父窗口和子窗口都可以通过`message`事件,监听对方的消息。 ```javascript window.addEventListener('message', function(e) { console.log(e.data); },false); `message`事件的事件对象`event`,提供以下三个属性。 > - `event.source`:发送消息的窗口 > - `event.origin`: 消息发向的网址 > - `event.data`: 消息内容 下面的例子是,子窗口通过`event.source`属性引用父窗口,然后发送消息。 ```javascript window.addEventListener('message', receiveMessage); function receiveMessage(event) { event.source.postMessage('Nice to see you!', '*'); 上面代码有几个地方需要注意。首先,`receiveMessage`函数里面没有过滤信息的来源,任意网址发来的信息都会被处理。其次,`postMessage`方法中指定的目标窗口的网址是一个星号,表示该信息可以向任意网址发送。通常来说,这两种做法是不推荐的,因为不够安全,可能会被恶意利用。 `event.origin`属性可以过滤不是发给本窗口的消息。 ```javascript window.addEventListener('message', receiveMessage); function receiveMessage(event) { if (event.origin !== 'http://aaa.com') return; if (event.data === 'Hello World') { event.source.postMessage('Hello', event.origin); } else { console.log(event.data); ### LocalStorage 通过`window.postMessage`,读写其他窗口的 LocalStorage 也成为了可能。 下面是一个例子,主窗口写入iframe子窗口的`localStorage`。 ```javascript window.onmessage = function(e) { if (e.origin !== 'http://bbb.com') { return; var payload = JSON.parse(e.data); localStorage.setItem(payload.key, JSON.stringify(payload.data)); 上面代码中,子窗口将父窗口发来的消息,写入自己的LocalStorage。 父窗口发送消息的代码如下。 ```javascript var win = document.getElementsByTagName('iframe')[0].contentWindow; var obj = { name: 'Jack' }; win.postMessage(JSON.stringify({key: 'storage', data: obj}), 'http://bbb.com'); 加强版的子窗口接收消息的代码如下。 ```javascript window.onmessage = function(e) { if (e.origin !== 'http://bbb.com') return; var payload = JSON.parse(e.data); switch (payload.method) { case 'set': localStorage.setItem(payload.key, JSON.stringify(payload.data)); break; case 'get': var parent = window.parent; var data = localStorage.getItem(payload.key); parent.postMessage(data, 'http://aaa.com'); break; case 'remove': localStorage.removeItem(payload.key); break; 加强版的父窗口发送消息代码如下。 ```javascript var win = document.getElementsByTagName('iframe')[0].contentWindow; var obj = { name: 'Jack' }; // 存入对象 win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com'); // 读取对象 win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*"); window.onmessage = function(e) { if (e.origin != 'http://aaa.com') return; // "Jack" console.log(JSON.parse(e.data).name); ## AJAX 同源政策规定,AJAX请求只能发给同源的网址,否则就报错。 除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制。 > - JSONP > - WebSocket > - CORS ### JSONP JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。 它的基本思想是,网页通过添加一个`