-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 885 KB
/
content.json
1
[{"title":"《JavaScript高级程序设计》 第二十四章 最佳实践","date":"2017-12-15T14:13:57.000Z","path":"2017/12/15/《JavaScript高级程序设计》-第二十四章-最佳实践/","text":"24.1可维护性24.1.1什么是可维护的代码可维护的代码遵循以下特点:·可理解性·直观性·可适应性——代码以一种数据上的变化不要求完全重写的方法撰写。·可扩展性——在代码架构上已考虑到在未来允许对核心功能进行扩展。·可调试性——当代码出错时,代码可以给予你足够的信息来尽可能直接地确定问题所在。对于专业人士而言,能写出可维护的JavaScript代码是非常重要的技能。 这正是周末改改网站的爱好者和真正理解自己作品的开发人员之间的区别。24.1.2代码约定以下小节将讨论代码约定的概论。对这些主题的解说非常重要,虽然可能解说方式会有区别,这取决于个人需求。1.可读性要让代码可维护,首先它必须可读。可读性与代码作为文本文件的格式化方式有关。可读性的大部分内容都是和代码的缩进有关。通常会使用若干空格而非制表符来进行缩进。这是因为制表符在不同的文本编辑器中显示效果不同。一种不错的、很常见的缩进大小为4个空格。可读性的另一方面是注释。在大多数编译语言中,对每个方法的注释都视为一个可行的实践。因为JavaScript可以在代码的任何地方创建函数,所以这点常常被忽略。正因如此,在JavaScript中为每个函数编写文档就更加重要了。一般而言,有如下一些地方需要进行注释:·函数和方法——每个函数和方法都应该包含一个注释。描述其目的、用于完成任务可能使用的算法、陈述事先的假设:如参数代表什么,函数是否有返回值(因为这不能从函数定义中推断出来)。·大段代码——用于完成单个任务的多行代码应该在前面放一个描述任务的注释。·复杂的算法——让人看得懂,自己下次看也容易理解·Hack——因为存在浏览器差异,JavaScript代码一般会包含一些hack。不要假设其他人在看代码的时候能够理解hack所要应付的浏览器问题。防止有人偶然看到你的hack,然后“修正”了它,最后重新引入了你本来修正了的错误。2.变量和函数命名命名的一般规则如下:·变量名应为名称如car或person。·函数名应该以动词开始,如getName(),返回布尔类型值的函数一般以is开头,如isEnable()。·变量和函数都应使用合乎逻辑的名字,不要担心长度,长度问题可以通过后处理和压缩缓解。3.变量类型透明由于在JavaScript中变量是松散类型的,很容易忘记变量所应包含的数据类型。合适的命名方式可以一定程度上缓解这个问题,但放到所有的情况下看,还不够。表示变量数据类型的方式有三种:·第一种方式是初始化。定义了一个变量后,把变量初始化为一个值,来暗示它应该应该如何应用。例如://通过初始化指定变量类型var found = false; //布尔型var count = -1; //数字var name = ""; //字符串var person = null; //对象这种方式的缺点是它无法用语函数声明中的函数参数。第二种方式是使用匈牙利标记法来指定变量类型。在变量名之前加上一个或多个字符串来表示数据类型。JavaScript中最传统的匈牙利标记法是用单个字符串基本类型:“o”表示对象,“s”表示字符串,“i”表示整数,“f”表示浮点数,“b”表示布尔型。例子://用于指定数据类型的匈牙利标记法var bFound; //布尔型var iCount; //整数var sName; //字符串var oPerson; //对象JavaScript中用匈牙利标记法的好处是函数参数一样可以使用。缺点是让代码某种程度上难以阅读。阻碍了没有用它时,代码的直观性和句子式的特质(什么特质?)。最后一种指定变量类型的方式是使用类型注释。类型注释放在变量名右边,但是在初始化前面。这种方式是在变量旁边放一段指定类型的注释。例子://用于指定类型的类型注释var found /Boolean/ = false;var count /int/ = 10;var name /String/ = "Nicholas";var person /Object/ = null;类型注释维持了代码的整体可读性,同时注入了类型信息。类型注释的缺点是你不能用多行注释一次注释大块的代码,因为类型注释也是多行注释,两者会冲突。例子://以下代码不能正确运行/var found /:Boolean/ = false;var count /:int/ = 10;var name /:String/ = "Nicholas";var person /:Object/ = null;/如果你想要注释掉这些类型注释的代码行,最好在每一行上使用单行注释。24.1.3松散耦合只要应用的某个部分过分依赖于另一个部分,代码就是耦合过紧,难于维护。典型的问题如:对象直接引用另一个对象,并且当修改其中的一个的同时需要修改另一个。紧密耦合的软件难于维护并且需要经常重写。1.解耦HTML/JavaScript在Web上,HTML和JavaScript各自代表了解决方案中的不同层次:HTML是数据,JavaScript是行为。因为它们天生就需要交互,所以有多种不同的方式将两个技术关联起来。但是,有一些方法会将HTML和JavaScript过于紧密地耦合在一起。直接写在HTML中的JavaScript,使用包含内联代码的<script>元素或者是使用HTML属性来分配事件处理程序,都是过于紧密的耦合。例子:<!– 使用了<script>的紧密耦合的HTML/JavaScript –><script> document.write("Hello world");</script><!– 使用了事件处理程序属性值的紧密耦合的HTML/JavaScript –><input type="button" value="Click Me" onclick="doSomething()"/>虽然这些从技术上来说都是正确的,但是实践中,它们将表示数据的HTML和定义行为的JavaScript紧密耦合在一起。理想情况是,HTML和JavaScript应该完全分离,并通过外部文件和使用DOM附加行为来包含JavaScript。当HTML和JavaScript过于紧密的耦合在一起时,出现JavaScript错误时就要先判断错误是出现在HTML部分还是在JavaScript中。它还会引入和代码是否可用的相关新问题。在这个例子中,可能在doSomething()函数可用之前,就已经按下按钮,引发了一个JavaScript错误。因为任何对按钮行为的更改要同时触及HTML和JavaScript,因此影响了可维护性。而这些更改本该只在JavaScript中进行。HTML和JavaScript的紧密耦合也可以在相反的关系上成立:JavaScript包含了HTML。这通常会出现在使用innerHTML来插入一段HTML文本到页面上这种情况中,例子://将HTML紧密耦合到JavaScriptfunction insertMessage(msg){ var container = document.getElementById("container"); container.innerHTML = "<div class=\\"msg\\"><p class=\\"post\\">" + msg + "</p>" + "<p><em>Latest message above.</em></p></div>";}一般来说,你应该避免在JavaScript中创建大量HTML。再一次重申要保存层次的分离,这样可以很容易的确定错误来源。当使用上面这个例子的时候,有一个页面布局的问题,可能和动态创建的HTML没有被正确格式化有关。不过,要定位这个错误非常困难,因为你可能一般先看页面的源代码来查找那段烦人的HTML,但是却没能找到,因为它是动态生成的。对数据或者布局的更改也会要求更改JavaScript,这也表面了这两个层次过于紧密地耦合了。HTML呈现应该尽可能与JavaScript保持分离。当JavaScript用于插入数据时,尽量不要直接插入标记。一般可以在页面中直接包含并隐藏标记,然后等到整个页面渲染好之后,就可以用JavaScript显示该编辑,而非生成它。另一种方法是进行Ajax请求并获取更多要显示的HTML,这个方法可以让同样的渲染层(PHP、JSP、Ruby等等)来输出标记,而不是直接嵌在JavaScript中。将HTML和JavaScript解耦可以在调试过程中节省事件,更加容易确定错误的来源,也减轻维护的难度:更改行为只需要在JavaScript文件中进行,而更改标记则只要在渲染文件中。2.解耦CSS/JavaScript另一个Web层次则是CSS,它主要复杂页面的显示。JavaScript和CSS也是非常紧密相关的:它们都是HTML之上的层次,因此常常一起使用。但是,和HTML与JavaScript情况一样,CSS和JavaScript也可能会过于紧密地耦合在一起。最常见的紧密耦合的例子是使用JavaScript来更改某些样式,例子://CSS对JavaScript的紧密耦合element.style.color = "red";element.style.backgroundColor = "blue";由于CSS负责页面的显示,当显示出现任何问题时都应该只是查看CSS文件来解决。然而,当使用了JavaScript来更改某些样式的时候,比如颜色,就出现了第二个可能已更改和必须检查的地方。结果是JavaScript也在某种程度上负责了页面的显示,并与CSS紧密耦合了。如果未来需要更改样式表,CSS和JavaScript文件可能都需要修改,这就给开发人员造成了维护上的噩梦。所以这两个层次之间必须有清晰的划分。现代Web应用常常要使用JavaScript来更改样式,所以虽然不可能完全将CSS和JavaScript解耦,但还是能让耦合更松散的。这是通过动态更改样式类而非特定样式来实现的,例子://CSS对JavaScript的松散耦合element.className = "edit";通过只修改过某个元素的CSS类,就可以让大部分样式信息严格保留在CSS中。JavaScript可以更改样式,但并不会直接影响到元素的样式。只要应用了正确的类,那么任何显示问题都可以直接追溯到CSS而非JavaScript。再次提醒,好的层次划分是非常重要的。显示问题的唯一来源应该是CSS,行为问题的唯一来源应该是JavaScript。在这些层次之间保存松散耦合可以让你的整个应用更加易于维护。3.解耦应用逻辑/事件处理程序每个Web应用一般都有相当多的事件处理程序,监听着无数不同的事件。然而,很少有能仔细得将应用逻辑从事件处理程序中分离的。例子:function handlerKeyPress(event){ event = EventUtil.getEvent(event); if (event.keyCode == 13) { var target = EventUtil.getTarget(event); var value = 5 parseInt(target.value); if (value > 10) { document.getElementById("error-msg").style.display = "block"; } }}这个事件处理程序除了包含了应用逻辑,还进行了事件的处理。这种方式的问题有其双重性。首先,除了通过事件之外就再也没有办法执行应用逻辑,这让调试变得困难。如果没有发生预想的结果怎么办?是不是表示事件处理程序没有被调用还是值应用逻辑失败?其次,如果一个后续的事件引发同样的应用逻辑,那就必须复制功能代码或者将代码抽取到一个单独的函数中。无论哪种方式,都要作比实际所需更多的改动。较好的方法是将应用逻辑和事件处理程序相分离,这样两者分别处理各自的东西。一个事件处理程序应该从事件对象(event)中提取相关信息,并将这些信息传送到处理应用逻辑的某个方法中。例如,前面的代码可以被重写为:function validateValue(value){ value = 5 parseInt(value); if (value > 10) { document.getElementById("error-msg").style.display = "block"; }}function handleKeyPress(event){ event = EventUtil.getEvent(event); if (event.keyCode == 13) { var target = EventUtil.getTarget(event); validateValue(target.value); }}改动过的代码合理将应用逻辑从事件处理程序中分离了出来。handleKeyPress()函数确认是按下了Enter键(event.keyCode为13),取得了事件的目标并将value属性传递给validateValue()函数,这个函数包含了应用逻辑。注意validateValue()中没有任何东西会依赖域任何事件处理程序逻辑,它只是接收一个值,并根据该值进行其他处理。从事件处理程序中分离应用逻辑有几个好处。首先,可以让你更容易更改触发特定过程的事件。如果最开始由鼠标点击事件触发过程,但现在按键也要进行同样处理,这种更改就很容易。其次,可以在不附加到事件的情况下测试代码,使其更易创建单元测试或者是自动化应用流程(这两个词都不懂)。以下是要牢记的应用和业务逻辑之间松散耦合的几条原则:·勿将event对象传给其他方法;只传来自event对象中所需的数据;·任何可以在应用层的动作都应该可以在不执行任何事件处理程序的情况下进行;·任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑。牢记这几条可以在任何代码中都获得极大的可维护性的改进,并且为进一步的测试和开发制造了很多可能。24.1.4编程实践1.尊重对象所有权JavaScript的动态性质使得几乎任何东西在任何时候都可以修改。有人说在Javascript没有什么神圣的东西,因为无法将某些东西标记为最终或恒定状态。这种情况在ECMAScript 5中通过引入防篡改对象(seal(),freeze()还有那个防扩展方法)得以改变;不过,默认情况下所有对象都是可以修改的。在其他语言中,当没有实际的源代码的时候,对象和类是不可变的。JavaScript可以在任何时候修改过任意对象,这样就可以以不可预计的方式覆写默认的行为。因为这门语言没有强行的限制(Javascript太自由 了!),所以对于开发者来说,这是很重要的,也是必要的。也许在企业环境中最重要的编程实践就是尊重对象所有权,它的意思是不能修改不属于你的对象。简单地说,如果你不负责创建或维护某个对象、它的对象或者它的方法,那么你就不能对它们进行修改。更具体地说:·不要为实例或原型添加属性;·不要为实例或原型添加方法;·不要重定义已存在的方法。问题在于开发人员会假设浏览器环境按照某个特定方式运行,而对多个人都用到的对象进行改动就会产生错误。如果某人期望叫做stopEvent()的函数能取消某个事件的默认行为,但是你对其进行了更改,然后它完成了本来的任务,后来还追加了另外的事件处理程序,那肯定会出现问题了。其他开发人员会认为函数还是按照原来的方式执行,所以他们的用法会出错并有可能会造成危害,因为他们并不知道有副作用。这些规则不仅仅适用于自定义类型和对象,对于诸如Object、String、document、window等原生类型对象也适用,此外潜在的问题可能更加危险,因为浏览器提供者可能会在不做宣布或者是不可预期的情况下更改这些对象。最佳的方法便是永远不修改不是由你所有的对象。所谓拥有对象,就是说这个对象是你创建的,比如你自己创建的自定义类型或对象字面量。像Array,document这些显然不是你的,所以不要在这些对象上添加新方法或属性,万一以后浏览器实现了一个跟你的方法一样名字的方法,就很有可能因为冲突而出错。可以通过以下方式为对象创建新的功能:·创建包含所需功能的对象,并用它与相关对象进行交互;·创建自定义类型,继承需要进行修改的类型。然后可以为自定义类型添加额外功能。现在很多JavaScript库都赞同并遵守这条开发原理,这样即使浏览器频繁更改,库本身也能继续成长和适应。2.避免全局量与尊重对象所有权密切相关的是尽可能避免全局变量和函数。这也关系到创建一个脚本执行的一致的和可维护的环境。最多创建一个全局变量,让其他对象和函数存在其中。例子://两个变量——避免!var name = "Nicholas";function sayName(){ console.log(name);}这段代码包含了两个全局量:变量name和函数sayName()。其实可以创建一个包含两者的对象,如下所示://一个全局量——推荐var MyApplication = { name:"Nicholas", sayName:function(){ console.log(this.name); }}这段重写的代码引入了一个单一的全局对象MyApplication,name和sayName()都附加到其上。这样做消除了一些存在域前一段代码中的一些问题。首先,变量name覆盖了window.name属性,可能会与其他功能产生冲突;其次,它有助消除功能作用域之间的混淆。调用MyApplication.sayName()在逻辑上暗示了代码的任何问题都可以通过检查定义MyApplication的代码来确定。单一的全局量的延伸便是命名空间的概念,由于YUI库普及。命名空间包括创建一个用于放置功能的对象。在YUI的2.x版本中,有若干用于追加功能的命名空间。比如:·YAHOO.util.Dom —— 处理DOM的方法;·YAHOO.utl.Event —— 与事件交互的方法;·YAHOO.lang —— 用于底层语言特性的方法。对于YUI,单一的全局对象YAHOO作为一个容器,其中定义了其他对象。用这种方式将功能组合在一起的对象,叫做命名空间。整个YUI库便是构建在这个概念上的,让它能够在同一个页面上与其他的JavaScript库共存。命名空间很重要的一部分是确定每个人都同意使用全局对象的名字,并且尽可能唯一,让其他人不太可能也使用这个名字。在大多数情况下,可以是开发代码的公司的名字,例如YAHOO或者Wrox。你可以如下例所示开始创建命名空间来组合功能://创建全局变量var Wrox = {};//为Professional JavaScript创建命名空间Wrox.ProJS = {};//将书中用到的对象附加上去Wrox.ProJS.EventUtil = { … };Wrox.ProJS.CookieUtil = { … };在这个例子中,Wrox是全局变量,其他命名空间在此之上创建。如果本书所有代码都放在Wrox.ProJS命名空间,那么其他作者也应把自己的代码添加到Wrox对象中。只要所有人都遵循这个规则,那么就不用担心其他人也创建叫做EventUtil或者CookieUtil的对象,因为它会存在于不同的命名空间中。例子://为Profession Ajax创建命名空间Wrox.ProAjax = {};//附加该书中所使用的其他对象Wrox.ProAjax.EventUtil = { … };Wrox.ProAjax.CookieUtil = { … };//ProJS还可以继续分别访问Wrox.ProJS.EventUtil.addHandler( … );以及ProAjaxWrox.ProAjax.EventUtil.addHandler( … );虽然命名空间会需要多写一些代码,但是对于可维护的目的而言是值得的。命名空间有助于确保代码可以在同一个页面上与其他代码以无害的方式一起工作。3.避免与 null 进行比较由于JavaScript不做任何自动的类型检查,所以它就成了开发人员的责任。因此,在JavaScript代码中其实很少进行类型检测。最常见的类型检测就是查看某个值是否为null。但是,直接将值与null比较是使用过度的。并且常常由于不充分的类型检查导致错误。例子:function sortArray(values){ if(values != null){ values.sort(caomparator); //避免 }}想想就知道这个判断逻辑有问题,sort()是数组才有的方法,他觉得只要判断values是不是null,不是null的话values就是数组,这种逻辑明显是有问题的。现实中,与null比较很少适合情况而被使用。必须按照所期望的对值进行检查,而非按照不被期望的那些。例如,前面的示例中,values参数应该是一个数组,那么就要检查它是不是一个数组,而不是检查它是否非null。函数按照下面的方式修改会更合适:function sortArray(values){ if(values instanceof Array){ values.sort(comparator); }}当然前面有说过instanceof如果检测跨框架的变量会出问题,什么问题来着?反正前面有介绍一种更好的方法,利用Object的toString()方法。Objece.prototype.toString.call()。✎:啊啊这里说了:这样验证数组的技术在多框架的网页中不一定正确工作,因为每个框架都有其自己的全局对象,因此,也有自己的Array构造函数。如果你是从一个框架将数组传送到另一个框架,那么就要另外检查是否存在sort()方法。如果看到了与null比较的代码,尝试使用以下技术替换:·如果值应为一个引用类型,使用Instanceof操作符检查其他构造函数;·如果值应为一个基本类型,使用typeof检查其类型;·如果是希望对象包含某个特定的方法名,则使用typeof操作符确保指定名字的方法存在于对象上。代码中的null比较越少,就越容易确定代码的目的,并消除不必要的错误。4.使用常量尽管JavaScript没有常量的正式概念(ES6 有啦const),但它还是很有用的。这种将数据从应用逻辑分离出来的思想,可以在不冒引入错误的风险的同时,就改变数据。例子:function validate(value){ if(!value){ console.log("Invalid value!"); location.href = "/errors/invalid.php" }}在这个函数中有两段数据:要显示给用户的信息以及URL。显示在用户界面上的字符串应该与允许进行语言国际化的方式抽取出来。URL也应被抽取出来。因为它们有随着应用成长而改变的倾向。基本上,有着可能由于这样那样原因会变化的这些数据,那么都会需要找到函数并在其中修改代码。而每次修改应用逻辑的代码,都可能会引入错误。可以通过将数据抽取出来变成单独定义的常量的方式,将应用逻辑与数据修改隔离开来。例子:var Constants = { INVALID_VALUE_MSG:"Invalid value!", INVALID_VALUE_URL:"/error/invalid.php"};function validate(value){ if (!value) { console.log(Constants.INVALID_VALUE_MSG); location.href = Constants.INVALID_VALUE_URL; }}在这段重写过的代码中,消息和URL都被定义于Constants对象中,然后函数引用这些值。这些设置允许数据在无需接触使用它的函数的情况下进行变更。Constants对象甚至可以完全在单独的文件中进行定义(单独一个js文件),同时该文件可以由包含正确值的其他过程根据国际化设置来生成。关键在于将数据和使用它的逻辑进行分离。要注意的值的类型如下所示:·重复值——任何在多次用到的值都应抽取为一个常量,这就限制了当一个值变了而另一个没变的时候会造成的错误。这也包含了CSS类型。·用户界面字符串——任何用于显示给用户的字符串,都应被抽取出来以方便国际化。·URLs——在Web应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的URL。·任意可能会更改的值——每当你在用到字面量值的时候,你都要问以下自己这个值在未来是不是会变化。如果答案是“是”,那么这个值就应该被提取出来作为一个常量。对于企业级的JavaScript开发而言,使用常量是非常重要的技巧,因为它能让代码更容易维护,并且在数据更改的同时保护代码。24.2性能24.2.1注意作用域随着作用域链中的作用域数量的增加,访问当前作用域以外的变量的时间也在增加。访问全局变量总是比访问局部变量要慢,因为需要遍历作用域链。只要能减少花费在作用域链上的时间,就能增加脚本的整体性能。1.避免全局查找可能优化脚本性能最重要的就是注意全局查找。使用全局变量和函数肯定要比局部的开销更大,因为要设计作用域链上的查找。例子:function updateUI(){ var imgs = document.getElementsByTagName("img"); for(var i=0,len=imgs.length; i < len; i++){ imgs[i].title = document.title + " image " + i; } var msg = document.getElementById("msg"); msg.innerHTML = "Update complete";}该函数可能看上去完全正常,但是它包含了三个对于全局document对象的引用。如果在页面上有多个图片,那么for循环中的document引用就会被执行多次甚至上百次,每次都会要进行作用域链查找。通过创建一个指向document对象的局部变量,就可以通过限制一次全局查找来改进函数的性能:function updateUI(){ var doc = document; var imgs = doc.getElementsByTagName("img"); for(var i=0,len=imgs.length; i < len; i++){ imgs[i].title = doc.title + " image " + i; } var msg = doc.getElementById("msg"); msg.innerHTML = "Update complete"; }这个方法《高性能JavaScript》说过,也是Nicholas写的,这章估计很多那本书的内容。2.避免with语句with语句会创建自己的作用域,因此会增加其中执行的代码的作用域链的长度。由于额外的作用域链查找,在with语句中执行的代码肯定会比外面执行的代码更慢。with语句主要用于消除额外的字符,在大多数情况下,可以用局部变量完成相同的事情而不引入新的作用域。例子:function updateBody(){ with(document.body){ //把document.body当参数传入,如果下面的属性没有对象.属性这样使用,就默认是document.body的属性或方法 console.log(tagName); innerHTML = "Hello world!"; }}这段代码中的with语句让document.body变得更容易使用。其实可以使用局部变量达到相同的效果。例子:function updateBody(){ var body = document.body; console.log(body.tagName); body.innerHTML = "Hello world!";}24.2.2选择正确的方法1.避免不必要的属性查找在计算机科学中,算法的复杂度是使用O符号来表示的。最简单、最快捷的算法是常数,即O(1)。之后,算法变得越来越负责并花更长时间执行。下面的表格列出了JavaScript中最常见的算法类型:常数值,即O(1),指代字面量和存储在变量中的值。符号O(1)表示无论有多少个值,需要获取常量值的时间都一样。获取常量值是非常高效的过程。例子:var value = 5;var sum = 10 + value;console.log(sum);该代码进行了四次常量值查找,数字5,变量value,数字10和变量sum。这段代码的整体复杂度被认为是O(1)。在JavaScript中访问数组元素也是一个O(1)操作,和简单的变量查找效率一样。所以以下代码和前面的例子效率一样:var value = [5,10];var sum = values[0] + values[1];console.log(sum);使用变量和数组要比访问对象上的属性更有效率,后者是一个O(n)的操作。对象上的任何属性查找都要比访问变量或者数组花费更长时间。对象上的任何属性查找都要比访问变量或者数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索。简而言之,属性查找越多,执行的时间就越长。例子:var values = { first:5,second:10 };var sum = values.first + values.second;console.log(sum);这段代码使用两次属性查找来计算sum的值。进行一两次属性查找并不会导致显著的性能问题,但是进行成百上千次则肯定会减慢执行速度。然而亲测,尴尬的是把两种方式放在一个文件中测试,发现执行速度竟然与执行的先后顺序有关,把查找方法属性型放在变量之后,查找方法型会比较慢,但如果变量型放在查找方法属性型后面,就查找方法属性型比较快了,难道是方法不正确吗://变量型var first=5,second=10;var start1 = Date.now();for(var i=0; i<=1000000000; i++){ var sum1 = first + second}var end1 = Date.now();console.log(end1-start1); //两种方法谁放在前面,基本谁的就比较快//方法属性型var values = { first:5,second:10 };var start = Date.now();for(var i=0; i<=1000000000; i++){ var sum = values.first + values.scecond}var end = Date.now();console.log(end-start);注意获取单个值的多重属性查找。例子:var query = window.location.href.substring(window.location.href.indexOf("?"));在这段代码中,有6次属性查找:window.location.href.substring()有3次,window.location.href.indexOf()又有3次。只要数一数代码中的点的数量,就可以确定属性查找的次数了。这段嗲吗由于两次用到了window.location.href,同样的查找进行了两次,因此效率特别不好。一旦多次用到对象属性,应该将其存储在局部变量中。第一次访问该值会是O(n),然而后续的访问都会是O(1),就会节省很多。例如,之前的代码可以重写如下:var url = window.location.href;var query = url.substring.indexOf("?");这个版本的代码只有4次属性查找,相对于原始版本节省了33%。在更大的脚本中进行这种优化,倾向于获得更多的改进。一般来讲,只要能减少算法的复杂度,就要尽可能减少。尽可能多地使用局部变量将属性查找替换为值查找。进一步讲,如果既可以用数字化的数组位置进行访问,也可以使用命名属性(诸如NodeList、HTMLCollection对象),那么使用数字位置。2.优化循环一个循环的基本优化步骤如下:(1)减值迭代——大多数循环使用一个从0开始,增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效。(2)简化终止条件——由于每次循环过程都会计算终止条件,所以必须保证它尽可能快,也就是说避免属性查找或其他O(n)操作。(3)简化循环体——循环体是执行最多的,所以要确保其被最大限度地优化,确保没有某些可以被很容易移除循环的密集计算。(4)使用后测试循环——最常用for循环和while循环都是前测试循环。而如do-while这种后测试循环,可以避免最初终止条件的计算,因此运行更快。用一个例子来描述这种改动。以下是一个基本的for循环:for(var i=0; i < values.length; i++){ process(values[i]);}这段代码中变量 i 从 0 递增到values数组中的元素总数。假设值的处理顺序无关紧要,那么循环可以改为 i 减值。例子:for(var i = values.length - 1; i >= 0; i–){ process(values[i]);}(怪不得很多for循环都要用一个变量把对属性的查找存储起来,是为了提高性能)这里,变量 i 每次循环之后都会减 1。在这个过程中,将终止条件从values.length的O(n)调用简化成了 0 的O(1)调用。由于循环体只有一个语句,无法进一步优化。不过循环还能改成后测试循环,如下:var i = values.length-1;if(i > -1){ do{ process(values[i]); }while(–i >= 0);}(不过很少人这么用吧)此处蛀牙殴打优化是将终止条件和自减操作符组合成了单个语句。这是,任何进一步的优化只能在process()函数中进行了,因为循环的部分已经优化完全了。记住使用“后测试”循环时必须确保要处理的值至少有一个。空数组会导致多余的一次循环而“前循环”循环则可以避免。3.展开循环当循环的次数是确定的,消除循环并使用多次函数调用往往更快。请看一下前面的例子。如果数组的长度总是一样的,对每个元素都调用process()可能更优。例子://消除循环process(values[0]);process(values[1]);process(values[2]);这个例子假设values数组只有3个元素,直接对每个元素调用process()。这样展开循环可以消除建立循环和处理终止条件的额外开销,使代码运行更快。如果循环中的迭代次数不能事先确定,那可以考虑使用一种叫做 Duff 装置的技术。这个技术是以其创建者Tom Duff命名的,他最早在C语言中使用这项技术。正是Jeff Greenberg用JavaScript实现了Duff装置。Duff装置的基本概念是通过计算迭代的次数是否为8的倍数将一个循环展开为一系列语句。请看以下代码://credit:Jeff Greenberg for JS implementation of Duff’s Device//假设 values.length > 0var iterations = Math.ceil(Values.length / 8);var startAt = values.length % 8; //取得余数2var i = 0;do{ switch(startAt){ case 0: process(values[i++]); case 7: process(values[i++]); case 6: process(values[i++]); case 5: process(values[i++]); case 4: process(values[i++]); case 3: process(values[i++]); case 2: process(values[i++]); case 1: process(values[i++]); } startAt = 0;} while (–iterations > 0);Duff 装置的实现是通过将values数组中元素个数除以 8 来计算出循环需要进行多少次迭代的。然后使用取整的上限函数确保结果是整数。如果完全根据除 8 来进行迭代,可能会有一些不能被处理到的元素,这个数量保存在startAt变量中。首次执行该循环时,会检查StartAt变量看有需要多少额外调用。例如,如果数组中有10个值,startAt则等于2,那么最开始的时候process()则只会被调用2次。在接下来的循环中,startAt被重置为0,这样之后的每次循环都会被调用 8 次process()。展开循环可以提升大数据集的处理速度。(很聪明啊!)Andrew B. King提出了更快的 Duff 装置技术,将do-while循环分成2个单独的循环。例子://credit:Speed Up Your Site (New Riders,2003)var iterations = Math.floor(values.length / 8);var leftover = values.length % 8;var i = 0;if (leftover > 0) { do { process(values[i++]); } while (–leftover > 0);}do{ process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]);} while (–iterations > 0);(用两个do-while比用一个do-while+switch快吗?)在这个实现中,剩余的计算部分不会在实际循环中处理,而是在一个初始化循环中进行除以 8 的操作。当处理掉了额外的元素,继续执行每次调用 8 次process()的主循环。这个方法几乎比原始的 Duff 装置实现快上 40%(感觉也更好理解,利用取余后把余数那部分单独运算,剩下的8次8次执行)。针对大数据集使用展开循环可以节省很多时间,但对于小数据集,额外的开销则可能得不偿失。它是要花更多的代码来完成同样的任务,如果处理的不是大数据集,一般来说并不值得。4.避免双重解释当JavaScript代码想解析JavaScript的时候就会存在双重解释惩罚。当使用eval()函数或者是Functoin构造函数以及使用setTimeout()传一个字符串参数时都会发生这种情况。下面有一些例子://某些代码求值——避免!eval("console.log(‘Hello world’)");//创建新函数——避免!var sayHi = new Function("console.log(‘Hello world!’)");//设置超时——避免!setTimeout("console.log(‘Hello world’)",500);在以上例子中,都要解析包含了JavaScript代码的字符串。这个操作是不能在初始的解析过程中完成的,因为代码是包含在字符串中的,也就是说在Javascript代码运行的同时必须新启动一个解析器来解析新的代码。实例化一个新的解析器有不容忽视的开销,所以这种代码要比直接解析慢得多。(没人会这么做吧!)对于这几个例子都有另外的办法。只有极少的情况下eval()是绝对必须的,所以尽可能避免使用。在这个例子中,代码其实可以直接内嵌在原代码中。对于Function构造函数,完全可以直接写成一般的函数,调用setTimeout()可以传入函数作为第一个参数。例子://某些代码求值——已修正!console.log(‘Hello world’);//创建新函数——已修正!var sayHi = function(){ console.log(‘Hello world!’)};//设置超时——已修正!setTimeout(function(){ console.log(‘Hello world’)},500);如果要提高代码的性能,尽可能避免出现需要按照JavaScript解释的字符串。下面并非主要的问题,不过如果使用得到也有相当大的提升。·原生方法较快——只要有可能,使用原生方法而不是自己用JavaScript重写一个。原生方法是用诸如C/C++之类的编译型语言写出来的,所以要比JavaScript快很多很多。JavaScript中最容易被忘记的就是可以在Math对象中找到复杂的数学运算;这些方法要比任何用JavaScript写的同样方法如正弦,余弦快的多。·Switch语句较快——如果有一系列复杂的if-else语句,可以转换成单个switch语句则可以得到更快的代码。还可以通过将case语句按照最可能的到最不可能的顺序进行组织,来进一步优化switch语句。·位运算符较快——当进行数学运算的时候,位运算操作要比任何布尔运算或者算法运算块。选择性地用位运算替换算数运算可以极大地提升复杂计算的性能。诸如取模,逻辑与和逻辑或都可以考虑用位运算来替换。24.2.3最小化语句数JavaScript代码中的语句数量也影响所执行的操作的速度。完成多个操作的单个语句要比完成单个操作的多个语句快。所以就要找出可以组合在一起的语句,以减少脚本整体的执行事件。这里有几个可以参考的模式。1.多个变量声明有个地方很多开发人员都容易创建很多语句,那就是多个变量声明。很容易看到代码中由多个var语句来声明多个变量。例子://4个语句——很浪费var count = 5;var color = "blue",var values = [1,2,3],var now = new Date();在强类型语言中,不同的数据类型的变量必须在不同的语句中声明。然后,在JavaScript中所有的变量都可以使用单个var语句来声明。前面的代码可以如下重写://一个语句var count = 5, color = "blue", values = [1,2,3], now = new Date();(不过又记得在哪本书里见过说这样用不好?)此处,变量生只用了一个var语句,之间用逗号隔开,在大多数情况下这种优化都非常容易做,并且要比单个变量分别声明块很多。2.插入迭代值当使用迭代值(也就是在不同的位置进行增加或减少的值)的时候,尽可能合并语句。请看以下代码:var name = values[i];i++;前面这2句语句各只有一个目的:第一个从values数组中获值,然后存储在name中;第二个给变量 i 增加 1 。这两句可以通过迭代值插入第一个语句组合成一个语句,如下所示:var name = values[i++];这一个语句可以完成和前面两个语句一样的事情。因为自增操作符是后缀操作符,i 的值只有在语句其他部分结束之后才会增加。一旦出现类似情况,都要尝试将迭代值插入到最后使用它的语句中去。3.使用数组和对象字面量本书中,你可能看过两种创建数组和对象的方法:使用构造函数或者是使用字面量。使用构造函数总是要用到更多的语句来插入元素或者定义属性。而字面量可以将这些操作在一个语句中完成。请看以下例子://用4个语句创建和初始化数组————浪费var values = new Array();values[0] = 123;values[0] = 456;values[0] = 789;//用4个语句创建和初始化对象————浪费var person = new Object();person.name = "Nicholas";person.age = 29;person.sayName = function(){ console.log(this.name);};这段代码中,只创建和初始化了一个数组和一个对象。各用了4个语句:一个调用构造函数,其他3个分配数据。其实可以很容易地转换成使用字面量的形式。例子://只用一条语句创建和初始化数组var values = [123,456,789];//只用了一条语句创建和初始化对象var person = { name:"Nicholas", age:29, sayName:function(){ console.log(this.name); }}重写后的代码只包含两条语句,一条创建和初始化数组,另一条创建和初始化对象。之前用了八条语句的东西现在只用了两条,减少了75%的于巨量。在包含成千上万行JavaScript的代码库中,这些优化的价值更大。只要有可能,尽量使用数组和对象的字面量表示式来消除不必要的语句。24.2.4优化DOM交互在JavaScript各个方面中,DOM毫无疑问是最慢的一部分。DOM操作与交互要消耗大量时间,因为它们往往vyao重新渲染整个页面或者某一部分。进一步收,看似细微的操作也可能要花很久来执行,因为ODM要处理非常多的信息。理解如何优化与DOM的交互可以极大得提高脚本完成的速度。1.最小化现场更新一旦你需要访问的DOM部分是已经显示的页面的一部分,那么你就是在进行一个现场更新。之所以叫现场更新,是因为需要立即(现场)对页面对用户的显示进行更新。每一个更改,不管是插入单个字符,还是移除整个片段,都要有一个性能惩罚,因为浏览器要重新计算无数尺寸以进行更新。现场更新进行得越多,代码完成执行所花的时间就越长;完成一个操作所需的现场更新越少,代码就越快。例子:var list = document.getElementById("myList"), item, i;for(i=0; i < 10; i++){ item = document.createElement("li"); list.appendChild(item); item.appendChild(document.createTextNode("Item" + i));}这段代码为列表添加了10个项目。添加每个项目时,都有2个现场更新:一个添加<li>元素,另一个给她添加文本节点。这样添加10个项目,这个操作总共要完成20个现场更新。要修正这个性能瓶颈,需要减少现场更新的数量。一般有2种方法。第一种是将列表从页面上移除,最后进行更新,最后再将列表插回到同样的位置。这个方法不是非常理想,因为在每次页面更新的时候它会不必要的闪烁(出现,消失)。第二种方法是使用文档片段(fragment)来构建DOM结构,接着将其添加到List元素中。这个方式避免了现场更新和页面闪烁的问题。例子:var list = document.getElementById("myList"), fragment = document.createDocumentFragment(), item, i;for(i=0; i < 10; i++){ item = document.createElement("li"); fragment.appendChild(item); item.appendChild(document.createTextNode("Item" + i));}list.appendChild(fragment);(亲测用文档碎片真的明显比每循环一次插入一次快很多,循环10000次的时候快了20%)在这个例子中只有一次现场更新,它发生在所有项目都创建好之后。文档片段用作一个临时的占位符,放置新创建的项目。然后使用appendChild()将所有项目添加到列表中。记住,当给appendChild()传入文档片段时,只有片段中的子节点被添加到目标,片段本身不会被添加。一旦需要更新DOM,请考虑使用文档片段来创建DOM结构,然后再将其添加到现存的文档中。2.使用innerHTML有两种在页面上创建DOM节点的方法:使用诸如createElement()和appendChild()之类的DOM方法,以及使用InnerHTML。对于小的DOM更改而言,两种方法效率都差不多。然而,对于大的DOM更改,使用innerHTML要比使用标准DOM方法创建同样的DOM结构快得多。当把innerHTML设置为某个值时,后台会创建一个HTML解析器,然后使用内部的DOM调用来创建DOM结构,而非基于JavaScript的DOM调用。由于内部方法是编译好的而非解释执行的,所以执行快得多。前面的例子还可以用innerHTML改写如下:var list = document.getElementById("myList"), html = "", i;for(i=0, i < 10; i++){ html += "<li>Item " + i + "</li>";}list.innerHTML = html;这段代码构建了一个HTML字符串,然后将其指定到list.innerHTML,便创建了需要的DOM结构。虽然字符串连接上总是有点性能损失,但这种方式还是要比进行多个DOM操作更快。(前面的部分说过这样HTML与JavaScript又太耦合)。使用innerHTML的关键在于(和其他DOM操作一样)最小化调用它的次数。例如,下面的代码在这个操作中用到innerHTML的次数太多了:var list = document.getElementById("myList"), i;for(i=0; i < 10; i++){ list.innerHTML += "<li>Item " + "</li>";}这段代码的问题在于每次循环都要调用innerHTML,这是极其低效的。调用innerHTML实际上就是一次现场更新,所以也要如此对待。构建好一个字符串然后一次性调用innerHTML要比调用innerHTML多次快得多。3.使用事件代理4.注意HTMLCollectionHTMLCollection对象的陷阱已经在本书中讨论过了,因为它们对于Web应用的性能而言是巨大的损害。记住,任何时候要访问HTMLCollection,不管它是一个属性还是一个方法,都是在文档上进行一个查询,这个查询开销很昂贵。最小化访问HTMLCollection的次数可以极大地改进脚本的性能。也许优化HTMLCollection访问最重要的地方就是循环了。前面提到过将长度计算移入for循环的初始化部分。例子:var imamges = document.getElementsByTagName("img"), i,len;for(i=0, len=images.length; i < len; i++){ //处理}这里的关键在于长度length存入了len变量,而不是每次都去访问HTMLCollection的length属性。当在循环中使用HTMLCollection的时候,下一步应该是获取要使用的项目的引用,如下所示,以便避免在循环体内多次调用HTMLCollection。var images = document.getElementsByTagName("img"), image, i,len;for(i=0, len=images.lengtj; i < len; i++){ image = images[i]; //处理}这段代码添加了image变量,保存了当前的图像。这之后,在循环内就没有理由再访问images的HTMLCollection了。编写JavaScript的时候,一定要知道何时返回HTMLCollection对象,这样你就可以最小化对他们的访问。发生以下情况时会返回HTMLCollection对象:·进行了对getElementsByTagName()的调用;·获取元素的childNodes属性;·获取了元素的attributes属性;·访问了特殊的集合,如document.forms、document.images等。要了解当使用HTMLCollection对象时,合理使用会极大提升代码执行速度。24.3部署也许所有JavaScript解决方案最重要的部分,便是最后部署到运营中的网站或者是Web应用的过程。在这之前可能你已经做了相当多的工作,为普通的使用进行架构并优化一个解决方案。现在是时候从开发环境中走出来并进入Web阶段了,在此将会和真正的用户交互。然而,在这之前还有一系列解决的问题。24.3.1构建过程完备avaScript代码可以用于部署的一件很重要的事情,就是给它开发某些类型的构建过程。软件开发的典型模式是写代码—编译—测试,即首先书写好代码,将其编译通过,然后运行并确保其正常工作。由于JavaScript并非一个编译型语言,模式编程了写代码—测试,这里你写的代码就是你要在浏览器中测试的代码。这个方法的问题在于它不是最优的,你写的代码不应该原封不动地放入浏览器中,理由如下所示:·知识产权问题——如果把带有完整注释的代码放到线上,那别人就更容易知道你的意图,对它在利用,并且可能找到安全漏洞。·文件大小——书写代码要保证容易阅读,才能更好地维护,但这对于性能是不利的。浏览器并不能从额外的空白字符或者是冗长的函数名和变量名中获得什么好处。·代码组织——组织代码要考虑到可维护性并不一定是传送给浏览器的最好方式。基于这些原因,最好给JavaScript定义一个构建过程。构建过程始于在源控制中定义用于存储文件的逻辑结构。最好避免使用一个文件存放所有的JavaScript,遵循以下面向对象语言中的典型模式:将每个对象或自定义类型分别放入其单独的文件中。这样可以确保每个文件包含最少量的代码,使其在不引入错误的情况下更容易修改。另外,在使用CVS或Subversion这类并发源控制系统的时候,这样做也减少了在合并操作中产生冲突的风险。记住将代码分离成多个文件只是为了提高可维护性,并非为了部署。要进行部署的时候,需要将这些源代码并未一个或几个归并文件。推荐Web应用中尽可能使用最少的JavaScript文件,是因为HTPP请求是Web的主要性能瓶颈之一。记住通过<script>标记引用JavaScript文件是一个阻塞操作,当代码下载并运行的时候会停止其他所有的下载。因此,尽量从逻辑上将JavaScript代码分组成部署文件。一旦组织好文件和目录结构,并确定哪些要出现在部署文件中,就可以创建构建系统了。书中推荐了一个构建工具Ant,但是并没有听过,估计现在是不用的了。也看不懂。在开发周期中引入构建这一步能让你在部署之前对JavaScript文件进行更多的处理。24.3.2验证介绍JSLint。JSLint可以查找JavaScript代码中的语法错误以及常见的编码错误。它可以发觉的一些潜在问题如下:为了方便访问,它有一个在线版本,不过它也可以使用基于Java的Rhino JavaScript引擎运行于命令行模式下。要再命令行中运行JSLint,首先要下载Rhino,并从www.jslint.com下载Rhino版本的JSLint。一旦安装完成,便可以使用下面的语法从命令行运行JSLint了:java - jar rhino-1.3R7.jar jslint.js [input files]如这个例子:java -jar rhino-1.6R7.jar jslint.js a.js b.js c.js如果给定文件中有任何语法问题或者是潜在错误,则会输出有关错误和警告的报告。如果没有问题,代码会直接结束而不显示任何信息。24.3.3压缩当谈及JavaScript文件压缩,其实在讨论两个东西:代码长度和配重(Wire weight)。代码长度指是浏览器所需解析的字节数,配重指的是实际从服务器传送到浏览器的字节数。在Web开发的早期这两个数字几乎是一样的,因为从服务器端到客户端原封不动地传递了源文件。而在今天的Web上,这两者很少相等,实际上也不应相等。1.文件压缩因为JavaScript并非编译为字节码,而是按照源代码传送的,代码文件通常包含浏览器执行所不需要的额外的信息和格式。注释,额外的空白,以及长长的变量名和函数名虽然提高了可读性,但确实传送给浏览器时不必要的字节。不过,我们可以使用压缩工具减少文件的大小。压缩器一般进行如下一些步骤:·删除额外的空白(包括换行);·删除所有注释;·缩短变量名所有JavaScript文件在部署到生产环境之前,都应该使用压缩器进行压缩。给构建过程添加一个压缩JavaScript文件的环节以确保每次都进行这个操作。2.HTTP压缩配重指的是实际从服务器传送到服务器的字节数。因为现在的服务器和浏览器都有压缩功能,这个字节数不一定和代码长度一样。所有的五大Web浏览器都支持对所接收的资源进行客户端解压缩。这样服务器端就可以使用服务器端相关功能来压缩JavaScript文件。一个指定了文件使用了给定格式进行了压缩的HTTP头包含在了服务器响应中。接着浏览器会查看该HTTP头确定文件是否已被压缩,然后使用合适的格式进行解压缩。结果是和原来的代码量相比在网络中传递的字节数量大大减少了。注意这一行代码用到了响应的MIME类型来确定是否对其进行压缩。记住虽然<script>的type属性用的是text/javascript,但是JavaScript文件一般还是用application/x-javascript作为其服务的MIME类型。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"Javascript","slug":"Javascript","permalink":"https://millionqw.github.io/tags/Javascript/"}]},{"title":"《JavaScript高级程序设计》 第二十三章 离线应用与客户端存储","date":"2017-12-15T14:13:21.000Z","path":"2017/12/15/《JavaScript高级程序设计》-第二十三章-离线应用与客户端存储/","text":"拖了这么久,第二十二章用了9天!赶紧第二十三章开始。支持离线Web应用开发是HTML5的另一个重点。所谓离线Web应用,就是在设备不能上网的情况下仍然可以运行的应用(Web APP?)HTML5把离线应用作为重点,主要是基于开发人员的心愿。前端开发人员一直希望Web应用能够与传统的客户端应用同场竞技,起码做到只要设备有电就能使用。 开发离线Web应用需要几个步骤。首先要确保应用知道设备是否能上网,以便下一步执行正确的操作。然后,应用还必须能访问一定的资源(图像、JavaScript、CSS等),只有这样才能正常工作。最后,必须有一块本地空间用于保存数据,无论能否上网都不妨碍读写(客户端存储?)。HTML5及其相关的API让开发离线应用成为现实。23.1离线检测开发离线应用的第一步是要知道设备是在线还是离线,HTML5为此定义了一个navigator.onLine属性,这个属性值为true表示设备能上网,值为false表示设备离线(这么方便!?)。这个属性的关键是浏览器必须知道设备能否访问网络,从而返回正确的值。实际应用中,navigator.onLine在不同浏览器间还有些小的差异:·IE 6+和Safari 5+能够正确检测到网络已断开,并将navigator.onLine的值转换为false。·Firefox 3+和Opera 10.6+支持navigator.onLine属性,但你必须手工选中菜单项“文件→Web开发人员(设备)→脱机工作”才能让浏览器正常工作。由于存在上述兼容性问题,单独使用navigator.onLine属性不能确定网络是否连通。即便如此,在请求发生错误的情况下,检测这个属性仍然是管用的,以下是检测该属性状态的示例:if(navigator.onLine){ //正常工作} else { //执行离线状态时的任务}除navigator.onLine属性之外,为了更好地确定网络是否可用,HTML5还定义了两个事件:online和offline。当网络从离线变为在线或者从在线变为离线时,分别触发这两个事件。这两个事件在window对象上触发(牛逼啊)。EventUtil.addHandler(window,"online",function(){ console.log("Online");});EventUtil.addHandler(window,"offline",function(){ console.log("Offline");})为了检测应用是否离线,在页面加载后,最好先通过navigator.onLine取得初始的状态。然后,就是通过上述两个事件来确定网络连接状态是否变化。当上述事件触发时,navigator.onLine属性的值也会改变,不过必须要手工轮询(轮询就是不断查询的意思)这个属性才能检测到网络状态的变化。支持离线检测的浏览器有IE 6+(只支持navigator.onLine属性)、Firefox 3+、Safari 4+、Opera 10.6、Chrome、iOS 3.2版Safari 和 Android版WebKit。23.2应用缓存HTML5的应用缓存(application cache),或者简称为appcache,是专门为开发离线Web应用而设计的。Appcache就是从浏览器的缓存(Cache)中分出来的一块缓存区。要想在这个缓存中保存数据。可以使用一个描述文件(manifest file),列出要下载和缓存的资源。下面是一个简单的描述文件示例:CACHE MANIFEST#Commentfile.jsfile.css在最简单的情况下,描述文件中列出的都是需要下载的资源,以备离线时使用。✎:描述描述文件的选项非常多,不过这本书没有细讲。要将描述文件与页面关联起来,可以在<html>中的manifest属性中指定这个文件的路径,例如:<html manifest="/offline.manifest">以上代码告诉页面,/offline.manifest中包含着描述文件。这个文件的MIME类型必须是text/cache-manifest。(描述文件的扩展名以前推荐用manifest,但现在推荐的是appcache)。虽然应用缓存的意图是确保离线时资源可用,但也有相应的JavaScript API让你知道它都在做什么。这个API的核心是applicationCache对象,这个对象有一个status属性,属性的值是常量,表示应用缓存的如下当前状态:·0:无缓存,即没有与页面相关的应用缓存。·1:闲置,即应用缓存未得到更新。·2:检查中,即正在下载描述文件并更新。·3:下载中,即应用缓存正在下载描述文件中指定的资源。·4:更新完成,即应用缓存已经更新了资源,而且所有资源都已下载完毕,可以通过swapCache()来使用了。·5:废弃,即应用缓存的描述文件已经不存在了,因此页面无法再访问应用缓存。应用缓存还有很多相关的事件,表示其状态的改变,以下是这些事件:·checking:在浏览器为应用缓存查找更新时触发。·error:在检查更新或下载资源期间发生错误时触发。·noupdate:在检查描述文件发现文件无变化时触发。·downloading:在开始下载应用缓存资源时触发。·progress:在文件下载应用缓存的过程中持续不断触发。·updateready:在页面新的应用缓存下载完毕且可以通过swapCache()使用时触发。·cached:在应用缓存完整可用时触发。一般来讲,这些事件会随着页面加载按上述顺序依次触发。不过,通过调用update()方法也可以手工干预,让应用缓存为检查更新而触发上述事件。applicationCache.update();update()一经调用,应用缓存就会去检查描述文件是否更新(触发checking事件),然后就像页面刚刚加载一样,继续执行后续操作。如果触发了cached事件,就说明应用缓存已经准备就绪,不会再发生其他操作了。如果触发了updateready事件,则说明新版本的应用缓存已经可用,而此时你需要调用swapCache()来启用新应用缓存。EventUtil.addHandler(applicationCache,"updateready",function(){ applicationCache.swapCache();});支持HTML5应用缓存的浏览器有Firefox 3+、Safari 4+、Opera 10.6、Chrome、iOS 3.2+版Safari及Android版WebKit。在Firefox 4及之前版本中调用swapCache()会抛出错误(那编程中用到就要用try-catch语句块了,报错哦)。23.3数据存储随着Web应用程序的出现,也产生了对于能够直接在客户端上存储用户信息能力的要求。想法很合乎逻辑,属于某个特定用户的信息应该存在该用户的机器上。无论是登录信息、偏好设定或其他数据,Web应用提供者发现他们在找各种方式将数据存在客户端上。这个问题的第一个方案是以cookie的形式出现的,cookie是原来的网景公司创造的。一份题为“Persistent Client State:HTTP Cookes”(持久客户端状态:HTTP Cookies)的标准中对cookie机制进行了阐述。今天,cookie只是在客户端存储数据的其中一种选项。23.3.1CookieHTTP Cookie,通常直接叫做cookie,最初是在客户端用于存储会话信息的,该标准要求服务器对任意HTTP请求发送Set-Cookie HTTP头作为响应的一部分,其中包含会话信息。例如,这种服务器响应的头可能如下:HTTP/1.1 200 OKContent-type:text/htmlSet-Cookie:name=valueOther-header:other-header-value这个HTTP响应设置以name为名称,以value为值的一个cookie,名称和值在传送时都必须是URL编码的。浏览器会存储这样的会话信息,并在这之后,通过为每个请求添加Cookie HTTP头将信息发送回服务器,如下所示:Get/index.html HTTP/1.1Cookie:name=valueOther-header:other-header-value发送回服务器的额外信息可以用于唯一验证客户来自于发送的哪个请求。1.限制cookie在性质上是绑定在特定的域名下的。当设定了一个cookie后,再给创建它的域名发送请求时,都会包含这个cookie。这个限制确保了储存在cookie中的信息只能让批准的接受者访问,而无法被其他域访问。由于cookie是存在客户端计算机上的,还加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间。每个域的cookie总数是有限的,不过浏览器之间各有不同。如下:当超过单个域名限制之后还要再设置cookie,浏览器就会清除以前设置的cookie。IE和Opera会删除最近最少使用过的(LRU,Least Recently Used)cookie,腾出空间设置新的cookie。Firefox看上去好像是随机决定要清除哪个cookie,所以考虑cookie限制非常重要,以免出现不可预期的后果。浏览器中对于cookie的尺寸也有限制。大多数浏览器都有dayue4096B(加减1)的长度限制。为了最佳的浏览器兼容性,最好将整个cookie长度限制在4095B以内。尺寸限制影响到一个域下的所有cookie,而并非每个cookie单独限制。如果你尝试创建超过最大尺寸限制的cookie,那么该cookie会被悄无声息地丢掉。注意,虽然一个字符通常占用一字节,但是多字节情况则有不同。2.cookie的构成cookie由浏览器保存的以下几块信息构成:·名称:一个唯一确定cookie的名称。cookie名称是不区分大小写的,所以myCookie和MYCookie被认为是同一个cookie。然而,实践中最好将cookie名称看作是区分大小写的,因为某些服务器会这样处理cookie。cookie的名称必须是经过URL编码的。·值:储存在cookie中的字符串值。值必须被URL编码。·域:cookie对于哪个域是有效的,所有向该域发送的请求中都会包含这个cookie信息。这个值可以包含子域(subdomain,如www.wrox.com),也可以不包含它(如.wrox.com,则对于wrox.com的所有子域都有效)。如果没有明确规定,那么这个域会被认作来自设置cookie的那个域。·路径:对于指定域中的那个路径,应该向服务器发送cookie。例如,你可以指定cookie只有从http://www.wrox.com/books/中才能访问,那么http://www.wrox.com的页面就不会发送cookie信息,即使请求都是来自同一个域的。·失效时间:表示cookie何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie)。默认情况下,浏览器会话结束时即将所有cookie删除;不过也可以自己设置删除时间。这个值是个GMT格式的日期。(Wdy,DD-Mon-YYYY HH:MM:SS GMT),用于指定应该删除cookie的准确时间。因此,cookie可在浏览器关闭后依然保存在用户的机器上。如果你设置的失效日期是个以前的时间,则cookie会被立刻删除。·安全标志:指定后,cookie只有在使用SSL连接的时候才发送到服务器。例如,cookie信息只能发送给http://www.wrox.com。而http://www.wrox.com的请求则不能发送cookie。每一段信息都作为Set-Cookie头的一部分。使用分号加空格分隔每一段,如下例所示:HTTP/1.1 200 OKContent-type:text/htmlSet-Cookie:name=value; expires=Mon,22-Jan-07 07:10:24 GMT; domain=.wrox.comOther-header:other-header-value该头信息指定了一个叫做name的cookie,它会在格林威治时间2007年1月22日7:10:24失效,同时对于www.wrox.com和worx.com的任何子域(如p2p.wrox.com)都有效。secure标志是cookie中唯一一个非名值对儿的部分,直接包含一个secure单词。如下:HTTP/1.1 200 OKContent-type:text/htmlSet-Cookie:name=value; domain=.wrox.com; path=/; secureOther-header:other-header-value这里,创建了一个对于所有wrox.com的子域和域名下(由path参数指定的)所有页面都有效的cookie。因为设置了secure标志,这个cookie只能通过SSL连接才能传输。尤其要注意,域、路径、失效时间和secure标志都是服务器给浏览器的指示,以指定何时应该发送cookie。这些参数并不会作为发送到服务器的cookie信息的一部分。只有名值对儿才会被发送。3.JavaScript中的cookie在JavaScript中处理cookie有些复杂,因为其众所周知的蹩脚的接口(23333),即BOM的document.cookie属性。这个属性的独特之处在于它会因为使用它的方式不同而表现出不同的行为。当用来获取属性值时,document.cookie返回当前页面可用的(根据cookie的域、路径、失效时间和安全设置)所有cookie的字符串,一系列由分号隔开的名值对儿,例子:name1=value1; name2=value2; name3=value3所有名字和值都是经过URL编码的,所以必须使用decodeURIComponent()来解码。当用于设置值的时候,document.cookie属性可以设置为一个新的cookie字符串。这个cookie字符串会被解释并添加到现有的cookie集合中。设置document.cookie并不会覆盖cookie,除非设置的cookie的名称已经存在。设置cookie的格式如下,和Set-Cookie头中使用的格式一样。nane=value; expires=expiration_time; path=domain_path; domain=domain_name; secure这些参数中,只有cookie的名字和值是必须的。下面是一个简单的例子:document.cookie = "name=Nicholas";这段代码创建了一个叫name的cookie,值为Nicholas。当客户端每次向服务器端发送请求的时候,都会发送这个cookie;当浏览器关闭的时候,它就会被删除。虽然这段代码没问题,但因为这里正好名称和值都无需编码,所以最好每次设置cookie时就像下面这个例子中一样使用encodeURIComponent():document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("Nicholas");要给被创建的cookie指定额外的信息,只要将参数追加到该字符串,和Set-Cookie头中的格式一样,如下所示:document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("Nicholas") + "; domain=.wrox.com; path=/";由于JavaScript中读写cookie不是非常直观,常常需要写一些函数来简化cookie的功能。基本的cookie操作有三种:读取、写入和删除。它们在CookieUtil对象中如下表示:var CookieUtil = { get:function(name){ var cookieName = encodeURIComponent(name) + "=", cookieStart = document.cookie.indexOf(cookieName), cookieValue = null; if (cookieStart > -1) { var cookieEnd = document.cookie.indexOf(";",cookieStart); if (cookieEnd == -1) { cookieEnd = document.cookie.length; } cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length,cookieEnd)); } return cookieValue; }, set:function(name,value,expires,path,domain,secure){ var cookieText = encodeURIComponent(name) + "=" + encodeURIComponent(value); if (expires instanceof Date) { cookieText += "; expires=" + expires.toGMTString(); } if (path) { cookieText += "; path=" + path; } if (domain) { cookieText += "; domain=" + domain; } if (secure) { cookieText += ";secure"; } document.cookie = cookieText; }, unset:function(name,path,domain,secure){ this.set(name,"",new Date(0),path,domain,secure); }};CookieUtil.get()方法根据cookie的名字获取相应的值。它会在document.cookie字符串中查找cookie名加上等号的位置。如果找到了,那么使用indexOf()查找该位置之后的第一个分号(表示了该cookie的结束位置)。如果没有找到分号,则表示该cookie是字符串中的最后一个,则余下的字符串都是cookie的值。该值使用decodeURIComponent()进行解码并最后返回。如果没有发现cookie,则返回null。CookieUtil.set()方法在页面上设置了一个cookie,接收如下几个参数:cookie的名称,cookie的值,可选的用于指定cookie何时应被删除的Date对象。cookie的可选的URL路径,可选的域,以及可选的表示是否要添加secure标志的布尔值。参数是按照它们的使用频率排列的,只有头两个是必需的。在这个方法中,名称和值都使用encodeURIComponent()进行了URL编码,并检查其他选项。如果expires参数是Date对象,那么会使用Date对象的toGMTString()方法正确格式化Date对象,并添加到expires选项上。方法的其他部分就是构造cookie字符串并将其设置到document.cookie中。没有删除已有cookie的直接方法。所以,需要使用相同的路径、域和安全选项在此设置cookie,并将失效时间设置为过去的时间。CookieUtil()方法可以处理这种事情。它接收4个参数:要删除的cookie的名称、可选的路径参数、可选的域参数和可选的安全参数。这些参数加上空字符串并设置失效时间为1970年1月1日(初始化为0ms的Date对象的值),传给CookieUtil。set()。这样就能确保删除cookie。可以像下面这样使用上述方法://设置cookieCookieUtil.set("name","Nicholas");CookieUtil.set("book","Professional javascript");//读取cookie的值console.log(CookieUtil.get("name"));console.log(CookieUtil.get("book"));//删除cookieCookieUtil.unset("name");CookieUtil.unset("book");//设置Cookie,包括它的路径、域、失效日期CookieUtil.set("name","Nicholas","/books/projs/","www.wrox.com",new Date("January 1,2010"));//删除刚刚设置的cookieCookieUtil.unset("name","/books/projs/","www.wrox.com");//设置安全的cookieCookieUtil.set("name","Nicholas",null,null,null,true);这些方法通过处理解析、构造cookie字符串的任务令在客户端利用cookie存储数据更加简单。 4.子cookie为了绕开浏览器的单域名下的cookie数限制,一些开发人员使用了一种称为子cookie(subcookie)的概念。子cookie是存放在单个cookie中的更小段的数据。也就是使用cookie值来存储多个名称值对儿。子cookie最常见的格式如下所示:name=value1=value1&name2=value2&name3=value3&name4=value4&name5=value5子cookie一般也以查询字符串的格式进行格式化。然后这些值可以使用单个cookie进行存储和访问,而非对每个名称-值对儿使用不同的cookie储存。最后网站或者Web应用程序可以无需达到单域名cookie上限也可以储存更加结构化的数据。为了更好地操作子cookie,必须建立一系列新方法。子cookie的解析和系列化会因子cookie的期望用途而略有不同并更加复杂些。例如,要获得一个子cookie,首先要遵循与获得cookie一样的基本步骤,但是在解码cookie值之前,需要按如下方法找出子cookie的信息:var SubCookieUtil = { get:function(name,subName){ var subCookies = this.getAll(name); if (subCookies) { return subCookies(subName); } else { return null; } }, getAll:function(name){ var cookieName = encodeURIComponent(name) + "=", cookieStart = document.cookie.indexOf(cookieName), cookieValue = null, cookieEnd, subCookies, i, parts, result = (); if (cookieStart > -1) { cookieEnd = document.cookie.indexOf(";",cookieStart); if (cookieEnd == -1) { cookieEnd = document.cookie.length; } cookieValue = document.cookie.substring(cookieStart + cookieName.length,cookieEnd); if (cookieValue.length > 0) { subCookies = cookieValue.split("&"); for(i=0,len=subCookies.length; i < len; i++){ parts = subCookies[i].split("="); result[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]); } return result; } } return null; }, //省略更多代码};获取子cookie的方法有两个:get()和getAll()。其中get()获取单个子cookie的值,getAll()获取所有子cookie并将它们放入一个对象中返回,对象的属性为子cookie的名称,对应值为子cookie对应的值。get()方法接收两个参数:cookie的名字和子cookie的名字。它其实就是调用getAll()获取所有的子cookie,然后只返回所需的那一个(如果cookie不存在则返回null)。SubCookieUtil.getAll()方法和CookieUtil.get()在解析cookie值的方式上非常相似。区别在于cookie的值并非立即解码,而是先根据&字符将子cookie分割出来放在一个数组中,每一个子cookie再根据等于号分割,这样在parts数组中的前一部分便是子cookie名,后一部分则是子cookie的值。这两个项目都要使用decodeURIComponent()来解码,然后放入result对象中,最后作为发方法的返回值。如果cookie不存在,则返回null。可以像下面这样使用上述方法://假设document.cookie=data=name=Nicholas&book=Professional%20Javascript//取得全部于cookievar data = SubCookieUtil.getAll("data");console.log(data.name); //"Nicholas"console.log(data.book); //"Profession Javascript"//这个获取子cookieconsole.log(SubCookieUtil.get("data","name")); //"Nicholas"console.log(SubCookieUtil.get("data","book")); //"Professional Javascript"要设置子cookie,也有两种方法:set()和setAll()。以下代码展示了它们的构造:var SubCookieUtil = { set:function(name,subName,value,expires,path,domain,secure){ var subcookies = this.getAll(name) || {}; subcookies[subName] = value; this.setAll(name,subcookies,expires,path,domain,secure); }, setAll:function(name,subcookies,expires,path,domain,secure){ var cookieText = encodeURIComponent(name) + "=", subcookieParts = new Array(); subName; for(subName in subcookies){ if (subName.length > 0 && subcookies.hsaOwnProperty(subName)) { subcookies.push(encodeURIComponent(subName) + "=" + encodeURIComponent(subcookies[subName])); } } if (cookieParts.length > 0) { cookieText += subcookieParts.join("&"); if (expires instanceof Date) { cookieText += "; expires=" + expires.toGMTString(); } if (path) { cookieText += ";path=" + path; } if (domain) { cookieText += "; domain=" + domain; } if (secure) { cookieText += ";secure"; } } else { cookieText += "; expires=" + (new Date(0)).toGMTString(); } document.cookie = cookieText; }, //省略更多代码};这里的set()方法接收7个参数:cookie名称、子cookie名称、子cookie值、可选的cookie失效日期或时间的Date对象、可选的cookie路径、可选的cookie域和可选的布尔secure标志。所有的可选参数都是作用于cookie本身而非子cookie。为了在同一个cookie中存储多个子cookie路径、域和secure标志必须一致;针对整个cookie的失效日期可以在任何一个单独的子cookie写入的时候同时设置。在这个方法中,第一步是获取指定cookies名称对应的所有子cookie。逻辑或操作符“||”用于当getAll()返回null时将subcookies设置为一个新对象。然后,在subcookies对象上设置好了cookie值并传给setAll()。而setAll()方法接收6个参数:cookie名称、包含所有子cookie的对象以及和set()中一样的4个可选参数。这个方法使用 for-in 循环遍历第二个参数中的属性。为了确保确实是要保存的数据,使用了hasOwnProperty()方法,来确保只有实例属性被序列化到子cookie中。由于可能会存在属性名为空字符串的情况,所以在把属性名加入结果对象之前要检查一下属性名的长度。将每个子cookie的名值对儿都存入subcookieParts数组中,以便稍后可以使用join()方法以 & 号组合起来。剩下的方法则和CookieUtil.set()一样。心态崩了,打了又看不懂又用不上。打了也没用。下面的IE的就更不想看了。不过360浏览器还有很多浏览器好像都是基于IE的内核。23.3.3Web存储机制Web Storage最早是在Web超文本应用技术工作组(WHAT-WG)的Web应用1.0规范中描述的。这个规范的最初工作最终成为了HTML5的一部分。Web Storage的目的是克服由cookie带来的一些限制,当数据需要被严格控制在客户端上时,无需持续地将数据发回服务器。Web Storage的两个主要目标是:·提供一种在cookie之外储存会话数据的途径;·提供一种存储大量可以跨会话存在的数据机制。最初的Web Storage规范包含了两种对象的定义:sessionStorage和globalStorage。这两个对象在支持的浏览器中都是以windows对象属性的形式存在的,支持这两个属性的浏览器包括IE 8+、Firefox 3.5+、Chrome 4+和Opera 10.5+。1.Storage类型Storage类型提供最大的存储空间(因浏览器而异)来存储名值对儿。Storage的实例与其他对象类似,有如下方法:·clear():删除所有值;Firefox没有实现(不知道现在有没有)。·getItem(name):根据指定的名字name获取对应的值。·key(index):获得index位置处的值的名字。·removeItem(name):删除由name指定的名值对儿。·setItem(name,value):为指定的name设置一个对应的值。其中,getIem()、removeItem()和setItem()方法可以直接调用,也可通过Storage对象间接调用。因为每个项目都是作为属性存储在该对象上的,所以可以通过点语法或者方括号语法访问属性来读取值,设置也一样,或者通过delete操作符进行删除。不过,我们还建议读者使用方法而不是属性来访问数据,以免某个键会意外重写该对象上已经存在的成员。还可以使用length属性来判断有多少名值对儿存放在Storage对象中。但无法判断对象中所有数据的大小,不过IE 8提供了一个remainingSpace属性,用于获取还可以使用的存储空间的字节数。✎:Storage类型只能存储字符串。非字符串的数据在存储之前会被转换为字符串。2.sessionStorage对象sessionStorage对象存储特定于某个会话的数据,也就是该数据只保持到浏览器关闭。这个对象就像会话cookie,也会在浏览器关闭后消失。存储在sessionStorage中的数据可以跨越页面刷新而存在,同时如果浏览器支持,浏览器崩溃并重启之后依然可用(Firefox和WebKie都支持,IE则不行)。因为sessionStorage对象绑定于某个服务器会话,所以当文件在本地运行的时候是不可用的。存储在sessionStorage中的数据只能由最初给对象存储数据的页面访问到,所以对多页面应用有限制。由于sessionStorage对象其实是Storage的一个实例,所以可以使用setItem()或者直接设置新的属性来存储数据。下面是这两个方法的例子://使用方法存储数据sessionStorage.setItem("name","Nicholas");//使用属性存储数据sessionStorage.book = "Professional Javascript";不同浏览器写入数据方面略有不同。Firefox和WebKit实现了同步写入,所以添加到存储空间中的数据是立刻被提交的。而IE的实现则是异步写入数据,所以在设置数据和将数据实际写入磁盘之间可能有一些延迟。对于少量数据而言,这个差异是可以忽略的。对于大量数据,你会发现IE要比其他浏览器更快地恢复执行,因为它会跳过实际的磁盘写入过程(IE更快?这语气不对啊)。在IE 8中可以强制把数据写入磁盘:在设置新数据之前使用begin()方法,并且在所有设置完成之后调用commit()方法。例子://只适用于IE8sessionStorage.begin();sessionStorage.name = "Nicholas";sessionStorage.book = "Professional Javascript";sessionStorage.commit();这段代码确保了name和book的值在调用commit()之后立刻被写入磁盘。调用begin()是为了确保在这段代码执行的时候不会发生其他磁盘写入操作。对于少量数据而言,这个过程不是必需的;不过,对于大量数据(如文档之类的)可能就要考虑这种事务形式的方法了。sessionStorage中有数据时,可以使用getItem()或者通过直接访问属性名来获取数据。两种方法的例子如下://使用方法读取数据var name = sessionStorage.getItem("name");//使用属性读取数据var book = sessionStorage.book;还可以通过结合length属性和key()方法来迭代sessionStorage中的值,例子:for(var i=0, len=sessionStorage.length; i<len ; i++){ var key = sessionStorage.key(i); var value = sessionStorage.getItem(key); console.log(key + "=" + value);}每次经过循环的时候,key被设置为sessionStorage中下一个名字,此时不会返回任何内置方法或length属性。要从sessionStorage中删除数据,可以使用delete操作符删除对象属性,也可调用removeItem()方法。以下是这些方法的例子://使用delete删除一个值————在WebKit无效delete sessionStorage.name;//使用方法删除一个值sessionStorage.removeItem("book");在撰写这本书时,delete操作符在WebKit中无法删除数据,removeItem()则可以在各种支持的浏览器中正确运行。sessionStorage对象应该主要用于仅针对会话的小段数据的存储。如果需要跨越会话存储数据,那么globalStorage或者localStorage更合适。3.globalStorage对象Firefox 2中实现了globalStorage对象。作为最初的Web Storage规范的一部分,这个对象的目的是跨越会话存储数据,但有特定的访问限制。要使用globalStorage,首先要指定哪些域可以访问该数据。可以通过方括号标记使用属性来实现,例子://保存数据globalStorage["wrox.com"].name = "Nicholas";//获取数据var name = globalStorage["wrox.com"].name;在这里,访问的是针对域名wrox.com的存储空间。globalStorage对象不是Storage的实例,而具体的globalStorage["wrox.com"]才是,这个存储空间对于wrox.com及其所有子域都是可以访问的。可以像下面这样指定子域名://保存数据globalStorage["www.wrox.com"].name = "Nicholas";//获取数据var name = globalStorage["www.wrox.com"].name;这里所指定的存储空间只能由来自www.wrox.com的页面可以访问,其他子域名都不行。某些浏览器允许更加宽泛的访问限制,比如只根据顶级域名进行限制或者允许全局访问,如下面例子所示://存储数据,任何人都可以访问——不要这样做!globalStorage[""].name = "Nicholas";//存储数据,可以让任何以.net 结尾的域名访问——不要这样做!globalStorage["net"].name = "Nicholas";虽然这些也支持,但是还是要避免使用这种可宽泛访问的数据存储,以防止出现潜在的安全问题。考虑到安全问题,这些功能在未来可能会被删除或者是被更严格地限制,所以不应依赖于这类功能。当使用globalStorage的时候一定要指定一个域名。对globalStorage空间的访问,是依据发起请求页面的域名、协议和端口来限制的。例如,如果使用HTTPS协议在wrox.com中存储了数据,那么通过HTTP访问的wrox.com的页面就不能访问该数据。同样,通过80端口访问到页面则无法与同一个域同样协议但通过8080端口访问的页面共享数据。这类似于Ajax请求的同源策略。globalStorage的每个属性都是Storage的实例,因此,可以像如下代码这样使用:globalStorage["www.wrox.com"].name = "Nicholas";globalStorage["www.wrox.com"].book = "Professional Javascript";globalStorage["www.wrox.com"].removeItem("name");var book = globalStorage["www.wrox.com"].getItem("book");也就是说globalStorage的属性可以使用Storage的方法。如果你事先不能确定域名,那么使用location.host作为属性名比较安全。例如:globalStorage[location.host].name = "Nicholas";var book = globalStorage[location.host].getItem("book");如果不使用removeItem()或者delete删除,或者用户未清除浏览器缓存,存储在globalStorage属性中的数据会一直保留在磁盘上。这让globalStorage非常适合在客户端存储文档或者长期保存偏好设置。4.localStorage对象localStorage对象在修订过的HTML 5 规范中作为持久保存客户端数据的方案取代了globalStorage。与globalStorage不同,不能给localStorage指定任何访问规则;规则事先就设定好了。要访问同一个localStorage对象,页面必须来自同一个域名(子域名无效),使用同一种协议,在同一个端口上。这相当于globalStorage[location.host]。由于localStorage是Storage的实例,所以可以像使用sessionStorage一样来使用它。下面是一些例子://使用方法存储数据localStorage.setItem("name", "Nicholas");//使用属性存储数据localStorage.book = "Professional JavaScript";//使用方法读取数据var name = localStorage.getItem("name");//使用属性读取数据var book = localStorage.book;存储在localStorage中的数据和存储在globalStorage中的数据一样,都遵循相同的规则:数据保留到通过JavaScript删除或者是用户清除浏览器缓存。为了兼容只支持globalStorage的浏览器,可以使用以下函数:function getLocalStorage(){ if (typeof getLocalStorage == "object") { return localStorage; } else if (typeof globalStorage == "object") { return globalStorage(location.host); } else { throw new Error("Local storage not available"); }}然后,像下面这样调用一次这个函数,就可以正常地读写数据了:var storage = getLocalStorage();在确定了使用哪个Storage对象之后,就能在所有支持Web Storage的浏览器中使用相同的采取规则操作数据了。5.storage事件对Storage对象进行任何修改,都会在文档上触发storage事件。当通过属性或setItem()方法保存数据,使用delete操作符或removeItem()删除数据,或者调用clear()方法时,都会发生该事件。这个事件的event对象有以下属性:·domain:发生变化的存储空间的域名。·key:设置或者删除的键名。·newValue:如果是设置值,则是新值;如果是删除键,则是null。·oldValue:键被更改之前的值。在这四个属性中,IE8和Firefox只实现了domain属性(噗)。在撰写本书的时候,WebKit尚不支持storage事件。以下代码展示了如何侦听storage事件:EventUtil.addHandler(document,"storage",function(event){ console.log("Storage changed for" + event.domain);});无论对sessionStorage、globalStorage还是localStorage进行操作,都会触发storage事件,但不作区分。6.限制与其他客户端数据存储方案类似,Web Storage同样也有限制。这些限制因浏览器而异。一般来说,对存储空间大小的限制都是以每个来源(协议、域和端口)为单位的。换句话说,每个来源都有固定大小的空间用于保存自己的数据。考虑到这个限制,就要注意分析和控制每个来源中有多少页面需要保存数据。对于localStorage而言,大多数桌面浏览器会设置每个来源5MB的限制。Chrome和Safari对每个来源的限制是2.5MB。而iOS版Safari和Android版WebKit的限制也是2.5MB。对sessionStorage的限制也是因浏览器而异。有的浏览器对sessionStorage的大小没有限制,但Chrome、Safari、iOS版Safari和Android版WebKit都有限制,也都是2.5MB。IE8+和Opera对sessionStorage的限制是5MB。23.3.4IndexedDBIndexed Database API,或者简称为IndexedDB,是在浏览器中保存结构化数据的一种数据库。IndexedDB是为了替代目前已被废弃的Web SQL Database API(因为已废弃所以没讲)而出现的。IndexedDB的思想是创建一套API,方便保存和读取JavaScript对象,同时还支持查询及搜索。IndexedDB设计的操作完全是异步进行的。因此,大多数操作会以请求方式进行,但这些操作会在后期执行,然后如果成功则返回结果,如果失败则返回错误。差不多每次IndexedDB操作,都需要你注册onerror或onsuccess事件处理程序,以确保适当地处理结果。在得到完整支持的情况下,IndexedDB讲师一个作为API宿主的全局对象。由于API仍然可能有变化,浏览器也都使用提供商前缀,因此这个对象在IE10中叫msIndexDB,在Firefox 4中叫mozIndexDB,在Chrome中叫webkitIndexDB。为了清除期间,本节示例中将使用IndexedDB,而实际上每个示例前面都应该加上下面这行代码:var indexedDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB;1.数据库IndexedDB就是一个数据库,与MySQL或Web SQL Database等这些你以前可能用过的数据库类似。IndexedDB最大的特色是使用对象保存数据,而不是使用表来保存数据。一个IndexedDB数据库,就是一组位于相同命名空间下的对象集合。使用IndexedDB的第一步是打开它,即把要打开的数据库名传给indexedDB.open()。如果传入的数据库已经存在,就会发送一个打开它的请求;如果传入的数据库还不存在,就会发送一个创建并打开它的请求。总之,调用indexedDB.open()会返回一个IDBRequest对象,在这个对象上还可以添加onerror和onsuccess事件处理程序。例子:var request , database;request = indexedDB.open("admin");request.onerror = function(event){ console.log("something bad happened while trying to open:" + event.target.errorCode);};request.onsuccess = function(event){ database = event.target.result;};真正的数据库是IDBRequest对象的result属性。在这两个事件处理程序中,event.target都指向request对象,因此它们可以互换使用。如果响应的是onsuccess事件处理程序,那么event.target.result中将有一个数据库实例对象(IDBatabase),这个对象会保存在database变量中,如果发生了错误,那event.target.errorCode中将保存一个错误码,表示问题的性质。以下就是可能的错误码(这个错误码适合所有操作)。默认情况下,IndexedDB数据库是没有版本号的,最好一开始就为数据库指定一个版本号。为此,可以调用setVersion()方法,传入以字符串形式表示的版本号。同样,调用这个方法也会返回一个请求对象(request对象),需要你再指定事件处理程序。if (database.version != "1.0") { request = database.setVersion("1.0"); request.onerror = function(event){ console.log("something bad happened while trying to set version:" + event.target.errorCode); }; request.onsuccess = function(event){ console.log("Database initialization complete. Database name:" + database.name + ", Version:" + database.version); };} else { console.log("Database already initialized. Database name:" + database.name + ", Version:" + database.version);}这个例子尝试把数据库的版本号设置为1.0。第一行先检测version属性,看是否已经为数据库设置了相应的版本号。如果没有,就调用setVersion()创建修改版本的请求。如果请求成功,就显示一条消息,表示版本修改成功。(在真实的项目开发中,你应该在这里建立对象存储空间。详细内容请看下一节。)如果数据库的版本号已经被设置为1.0,则显示一条消息,说明数据库已经初始化过了。总之,通过这种模式,就能知道你想使用的数据库是否已经设置了适当的对象存储空间。在整个Web应用中,随着对数据库结果的更新和修改,可能会产生很多个不同版本的数据库。2.对象存储空间在建立了与数据库的连接之后,下一步就是使用对象存储空间。如果数据库的版本与你传入的版本不匹配,那可能就需要创建一个新的对象存储空间。在创建对象存储空间之前,必须要想清楚你想要保存什么数据类型。假设你要保存的用户记录由用户名、密码等组成,那么保存一条记录的对象应该类似如下所示:var user = { username:"007", firstName:"James", lastName:"Bond", password:"foo"};有了这个对象,很容易想到username属性可以作为这个对象存储空间的键。这个username必须全局唯一,而且大多数时候都要通过这个键来访问数据。这一点非常重要,因为在创建对象存储空间时,必须指定这么一个键。以下就是为保存上述用户记录而创建对象存储空间的示例: var store = db.createObjectStore("users",{keyPath:"username"});其中第二个参数中的keyPath属性,就是空间中将要保存的对象的一个属性,而这个属性将作为存储空间的键来使用。好,现在有了一个对存储空间的引用。接下来可以使用add()或put()方法来向其中添加数据。这两个方法都接收一个参数,即要保存的对象,然后这个对象会被保存到存储空间中。这两个方法的区别在空间中已经键值相同的对象时会体现出来。在这种情况下,add()会返回错误,而put()则会重写原有对象。简单来说,可以把add()想象成插入新值,把put()想象成更新原有的值。在初始化对象存储空间时,可以使用类似下面这样的代码://users中保存着一批用户对象var i=0, len = users.length;while(i < len){ store.add(users[i++]);}每次调用add()或put()都会创建一个新的针对这个对象存储空间的更新请求。如果像验证请求是否成功完成,可以把返回的请求对象保存在一个变量中,然后再指定onerror或onsuccess事件处理程序。//users中保存着一批用户对象var i=0, request, requests = [], len = users.length;while(i < len){ request = store.add(users[i++]); request.onerror = function(){ //处理错误 }; request.onsuccess = function(){ //处理成功 }; requests.push(request); //requests是一个数组}创建了对象存储空间并向其中添加了数据之后,就该查询数据了。3.事务跨过创建对象存储空间这一步之后,接下来的所有操作都是通过事务来完成的。在数据库对象上调用transaction()方法可以创建事务。任何时候,只要想读取或修改数据,都要通过事务来组织所有操作。在最简单的情况下,可以像下面这样创建事务:var transaction = db.transaction();如果没有参数,就只能通过事务来读取数据库中保存的(所有?)对象。最常见的方式是传入要访问的一或多个对象存储空间。(代码里的db都是指示例代码中的database,正文中的“数据库对象”也是指它。感觉讲得不好,代码是用不了的,不如百度看别人的文章)var transaction = db.transaction("users");这样就能保证只加载users存储空间中的数据,以便通过事务进行访问。如果要访问多个对象存储空间,也可以在第一个参数的位置上传入字符串数组(这种坑感觉很容易踏,以后直接输入两个字符串就会报错)。var transaction = db.transaction(["users","anotherStore"]);如前所述,这些事务都是以只读方式访问数据。要修改访问方式,必须在创建事务时传入第二个参数,这个参数表示访问模式,用IDBTransaction接口定义的如下常量表示:READ_ONLY(0)表示只读,READ_WRITE(1)表示读写,VERSION_CHANGE(2)表示改变。IE10+和Firefox 4+实现的是IDBTransaction,但在Chrome中则叫webkitIDBTransaction,所以使用下面的代码可以统一接口:var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;有了这行代码,就可以更方便地为transaction()指定第二个参数了:var transaction = db.transaction("users",IDBTransaction.READ_WRITE);不用写常量这么麻烦,在别的文章中看到,第二个参数写‘readwrite’及表示可读写,带上单引号。这个事务能够写users存储空间。取得了事务的索引后,使用objectStore()方法并传入存储空间的名称,就可以访问特定的存储空间。然后,可以像以前一样使用add()和put()方法,使用get()可以取得值,使用delete()可以删除对象,而使用clear()则可以删除所有对象。get()和delete()方法都接收一个对象键作为参数,而所有这5个方法都会返回一个新的请求对象。例如:var request = db.transaction("users").objectStore("users").get("007");request.onerror = function(event){ console.log("Did not get the object");};request.onsuccess = function(event){ var result = event.target.result; console.log(result.firstName); //"James"};因为一个事务可以完成任何多个请求,所以事务对象本身也有事件处理程序:onerror和oncomplete。这两个事件可以提供事务级的状态信息。transaction.onerror = function(event){ //整个事务都取消了};transaction.oncomplete = function(event){ //整个事务都成功完成了};注意,通过oncomplete事件的事件对象(event)访问不到get()请求返回的任何数据。必须在相应请求的onsuccess事件处理程序中才能访问到数据。4.使用游标查询使用事务可以直接通过已知的键检索单个对象。而在需要检索多个对象的情况下,则需要在事务内部创建游标。游标就是一个指向结果集的指针。与传统数据库查询不同,游标并不提前收集结果。游标指针会先指向结果中的第一项,在接到查找下一项的指令时,才会指向下一项。在对象存储空间上调用openCursor()方法可以创建游标。与IndexedDB中的其他操作一样,openCursor()方法返回的是一个请求对象,因此必须为该对象指定onsuccess和onerror事件处理程序。例如:var store = db.transaction("users").objectStore("users"), //使用transaction()取得“user”存储空间的索引,用objectStore()传入存储空间的名称才可以访问"users"存储空间 request = store.openCursor(); //取得游标对象,下面的onsuccess和onerror事件处理程序都是游标对象的事件处理程序request.onsuccess = function(event){ //处理成功};request.onerror = function(event){ //处理失败};在onsuccess事件处理程序执行时,可以通过event.target.result取得存储空间中的下一个对象(?)。在结果集中有下一项时,这个属性中保存一个IDBCursor的实例,在没有下一项时,这个属性的值为null。IDBCursor的实例有以下几个属性:要检索某一个结果的信息,可以像下面这样:request.onsuccess = function(event){ var cursor = event.target.result; if (cursor) { //必须要检查 console.log("Key:" + cursor.key + ", Value:" + JSON.stringify(cursor.value)); }};请记住,这个例子中的cursor.value是一个对象,这也是为什么在显示它之前先将它转换成JSON字符串的原因。使用游标可以更新个别的记录,调用update()可以用指定的对象更新当前游标的value。与其他操作一样,调用update()方法也会创建一个新请求,因此如果你想知道结果,就要为它指定onsuccess和onerror事件处理程序。request.onsuccess = function(event){ var cursor = event.target.result, value, updateRequest; if (cursor) {//必须要检查 if (cursor.key == "foo") { value = cursor.value; //取得当前的值 value.password = "margic!"; //更新密码 updateRequest = cursor.update(value); //请求保存更新 updateRequest.onsuccess = function(){ //处理成功 }; updateRequest.onerror = function(){ //处理失败 }; } }};此时,如果调用delete()方法,就会删除相应的记录。与update()一样,调用delete()也返回一个请求。request.onsuccess = function(event){ var cursor = event.target.result, value, deleteRequest; if (cursor) { //必须要检查 if (cursor.key == "foo") { deleteRequest = cursor.delete(); deleteRequest.onsuccess = function(){ //处理成功 }; deleteRequest.onerror = function(){ //处理失败 }; } }};如果当前事务没有修改对象存储空间的权限,update()和delete()会抛出错误。默认情况下,每个游标只发起一次请求。要想发起另一次请求,必须调用下面的一个方法:·continue(key):移动到结果集中的下一项。参数key是可选的,不指定这个参数,游标移动到下一项;指定这个参数,游标会移动到指定键的位置。·advance(count):向前移动count指定的项数。这两个方法都会导致游标使用相同的请求,因此相同的onsuccess和onerror事件处理程序也会得到重用。例如,下面的例子遍历了对象存储空间中的所有项:request.onsuccess = function(event){ var cursor = event.target.result; if (cursor) { //必须要检查 console.log("Key" + cursor.key + ", Value:" + JSON.stringify(cursor.value)); cursor.continue(); //移动到下一项 } else { console.log("Done!"); }};调用continue()会触发另一次请求,进而再次调用onsuccess事件处理程序。在没有更多项可以迭代时,将最后一次调用onsuccess事件处理程序,此时event.target.result的值为null。5.键范围使用游标总让人觉得不那么理想,因为通过游标查找数据的方式太有限了。键范围(key range)为使用游标增添了一些灵活性。键范围由IDBKeyRange的实例表示。支持标准IDBKeyRange类型的浏览器有IE10+和Firefox 4+,Chrome中的名字叫webkitIDBKeyRange。与使用IndexedDB中的其他类型一样,你最好先声明一个本地的类型,同时要考虑到不同浏览器中的差异。var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;有四种定义键范围的方式。第一种是使用only()方法,传入你想要取得的对象的键。var onlyRange = IDBKeyRange.only("007");这个范围可以保证只取得键为“007”的对象。使用这个范围创建的游标与直接访问存储空间并调用get("007")差不多。第二种定义键范围的方式是指定结果集的下界。下界表示游标开始的位置。例如,以下键范围可以保证游标从键为“007”的对象开始,然后继续向前移动,直至最后一个对象。//从键为“007”的对象开始,然后可以移动到最后var lowerRange = IDBKeyRange.lowerBound("007");如果你想忽略键为“007”的对象,从它的下一个对象开始,那么可以传入第二个参数true:var lowerRange = IDBKeyRange.lowerBound("007",true);第三种定义键范围的方式是指定结果集的上界,也就是指定游标不能超越哪个键。指定上界使用upperRange()方法。下面这个键范围可以保障游标从头开始,到取得键为“ace”的对象终止。//从头开始,到键为“ace”的对象为止var upperRange = IDBKeyRange.upperBound("ace");如果你不想包含键为指定值的对象,同样,传入第二个参数true://从头开始,到键为“ace”的上一个对象为止var upperRange = IDBKeyRange.upperBound("ace",true);第四种定义键范围的方式——没错(what!?),就是同时指定上、下界,使用bound()方法。这个方法可以接收4个参数:表示下界的键,表示上界的键,可选的表示是否跳过下界的布尔值和可选的表示是否跳过上界的布尔值。例子://从键为“007”的对象开始,到键为“ace”的对象为止var boundRange = IDBKeyRange.bound("007","ace");//从键为“007”的下一个对象开始,到键为“ace”的对象为止var boundRange = IDBKeyRange.bound("007","ace",true);//从键为“007”的下一个对象开始,到键为“ace”的上一个对象为止var boundRange = IDBKeyRange.bound("007","ace",true,true);//从键为“007”的对象开始,到键为“ace”的上一个对象为止var boundRange = IDBKeyRange.bound("007","ace",false,true);无论如何,在定义范围之后,把它传给openCursor()方法,就能得到一个符合相应约束条件的游标。var store = db.transaction("users").objectStore("users"), range = IDBKeyRange.bound("007","ace"); request = store.openCursor(range);request.onsuccess = function(event){ var cursor = event.target.result; if (cursor) { //必须要检查 console.log("Key:" + cursor.key + ", Value:" + JSON.stringify(cursor.value)); cursor.continue(); //移动到下一项 } else { console.log("Done!"); }};这个例子输出的对象的键“007”到“ace”,比上一节最后那个例子输出的值少一些。6.设定游标方向实际上,openCursor()可以接收两个参数。第一个参数就是刚刚看到的IDBKeyRange的实例,第二个是表示方向的数值常量。作为第二个参数的常量是前面讲查询时介绍的IDBCursor中的常量。Firefox 4+和Chrome的实现又有不同,因此第一步还是在本地消除差异:var IDBCursor = window.IDBCursor || window.webkitIDBCursor;正常情况下, 游标都是从存储空间的第一项开始,调用continue()或advance()前进到最后一项。游标的默认方向指是IDBCursor.NEXT。如果对象存储空间中有重复的项,而你想让游标跳过那些重复的项,可以为openCursor传入IDBCursor.NEXT_NO_DUPLICATE作为第二个参数:var store = db.transaction("users").objectStore("users"), request = store.openCursor(null,IDBCursor.NEXT_NO_DUPLICATE);注意,openCursor()的第一个参数是null,表示使用默认的键范围,即包含所有对象。这个游标可以从存储空间中的第一个对象开始,逐个迭代到最后一个对象——但会跳过重复的对象。当然,也可以创建一个游标,让它在对象存储空间中向后移动,即从最后一个对象开始,逐个迭代,直至第一个对象。此时, 要传入的变量是IDBCursor.PREV和IDBCursor.PREV_NO_DUPLICATE。例子:var store = db.transaction("users").objectStore("users"), request = store.openCursor(null, IDBCursor.PREV);使用IDBCursor.PREV或IDBCursor.PREV_NO_DUPLICATE打开游标时,每次调用continue()或advance(),都会在存储空间中向后而不是向前移动游标。7.索引对于某些数据,可能需要为一个对象存储空间指定多个键。比如,若要通过用户ID和用户名两种方式来保存用户资料,就需要通过这两个键来存取记录。为此,可以考虑将用户ID作为主键,然后为用户名创建索引。要创建索引,首先引用对象存储空间,然后调用createIndex()方法,例子:var store = db.transaction("users").objectStore("users"); index = store.createIndex("username","username",{unique:false});createIndex()的第一个参数是索引的名字,第二个参数是索引的属性名字,第三个参数是一个包含unique属性的选项(options)对象。这个选项通常都必须指定,因为它表示键在所有记录中是否唯一。因为username有可能重复,所以这个索引不是唯一的。createIndex()的返回值是IDBIndex的实例。在对象存储空间上调用index()方法也能返回同一个实例。例如,要使用一个已经存在的名为“username”的索引,可以像下面这样取得索引:var store = db.transaction("users").objectStore("users"); index = store.index("username");索引其实与对象存储空间很相似。在索引上调用openCursor()方法也可以创建新的游标,除了将来会把索引键而非主键保存在event.result.key属性中之外,这个游标与在对象存储空间上调用openCursor()返回的游标完全一样。例子:var store = db.transaction("users").objectStore("users"), index = store.index("username"), request = index.openCursor();request.onsuccess = function(event){ //处理成功};在索引上也能创建一个特殊的只返回每条记录主键的游标,那就要调用openKeyCursor()方法。这个方法接收的参数与openCursor()相同。而最大的不同在于,这种情况下event.result.key中仍然保存着索引键,而event.result,value中保存的则是主键,而不再是整个对象。var store = db.transaction("users").objectStore("users"), index = store.index("username"), request = index.openKeyCursor();request.onsuccess = function(event){ //处理成功 //event.result.key中保存索引键,而event.result.value中保存主键};同样,使用get()方法能够从索引中取得一个对象,只要传入相应的索引键即可;当然,这个方法也将返回一个请求。var store = db.transaction("users").objectStore("users"), index = store.index("username"), request = index.get("007");request.onsuccess = function(event){ //处理成功};request.onerror = function(event){ //处理失败};要根据给定的索引键取得主键,可以使用getKey()方法。这个方法也会创建一个新的请求,但event.result.value等于主键的值,而不是包含整个对象。var store = db.transaction("users").objectStore("users"), index = store.index("username"), request = index.get("007");request.onsuccess = function(event){ //处理成功 //event.result.key中保存索引键,而event.result.value中保存主键};在这个例子的onsuccess事件处理程序中,event.result.alue中保存的是用户ID。任何时候,通过IDBIndex对象的下列属性都可以取得有关索引的相关信息:·name:索引的名字。·keyPath:传入createIndex()中的属性路径。·objectStore:索引的对象存储空间。·unique:表示索引键是否唯一的布尔值。另外,通过对象存储对象的indexName属性可以访问到为该空间建立的所有索引。通过以下代码就可以知道根据存储的对象建立了哪些索引:var store = db.transaction("users").objectStore("users"), indexNames = store.indexNames, index, i = 0, len = indexNames.length;while(i < len){ index = store.index(indexNames[i++]); console.log("Index name: " + index.name + ", KeyPath: " + index.keyPath + ", Unique: " + index.unique );}以上代码遍历了每个索引,在控制台中输出了它们的信息。在对象存储空间上调用deleteIndex()方法并传入索引的名字可以删除索引:var store = db.transaction("users").objectStore("users");store.deleteIndex("username");因此删除索引不会影响存储空间中的数据,所以这个操作没有任何回调函数。8.并发问题虽然网页中的IndexedDB提供的是异步API,但仍然存在并发操作的问题。如果浏览器的两个不同的标签页打开了同一个页面,那么一个页面试图更新另一个页面尚未准备就绪的数据库的问题就有可能发生。把数据库设置为新版本有可能导致这个问题。因此,只有当浏览器中仅有一个标签页使用数据库的情况下,调用setVersion()才能完成操作。刚打开数据库时,要记着指定onversionchange事件处理程序。当同一个来源的另一个标签页调用setVersion()时,就会执行这个回调函数。处理这个事件的最佳方式是立即关闭数据库,从而保证版本更新顺利完成。例如:var request,database;request = indexedDB.open("admin");request.onsuccess = function(event){ database = event.target.result; database.onversionchange = function(){ database.close(); };};每次成功打开数据库,都应该指定onversionchange事件处理程序。调用setVersion()时,指定请求的onblocked事件处理程序也很重要。当你想要更新数据库的版本但另一个标签页已经打开数据库的情况下,就会触发这个事件处理程序。此时,最好先通过用户关闭其他标签页,然后再重新调用setVersion()。例如:var request,database;request.onblocked = function(){ console.log("Please close all otherr tabs and try again.");};请记住,其他标签页中的onversionchange事件处理程序也会执行。通过指定这些事件处理程序,就能确保你的 Web应用妥善地处理好IndexedDB的并发问题。9.限制对IndexedDB的限制很多都与对Web Storage的类似。首先,IndexedDB数据库只能由同源(相同协议、域名和端口)页面操作,因此不能跨域共享信息。换句话说,www.wrox.com与p2p.wrox.com的数据库是完全独立的。其次,每个来源的数据库占用的磁盘空间也有限制。Firefox 4+目前的上限是每个源50MB,而Chrome的限制是5MB。移动设备上的Firefox最多允许保存5MB,如果超过了这个配额,将会请求用户的许可。Firefox还有另外一个限制,即不允许本地文件访问IndexedDB。Chrome没有这个限制。如果你在本地运行本书的示例,请使用Chrome。(在Chrome的resources中)","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"Javascript","slug":"Javascript","permalink":"https://millionqw.github.io/tags/Javascript/"}]},{"title":"《JavaScript高级程序设计》 第二十二章 高级技巧","date":"2017-12-15T14:13:03.000Z","path":"2017/12/15/《JavaScript高级程序设计》-第二十二章-高级技巧/","text":"JavaScript是一种极其灵活的语言,具有多种使用风格。一般来说,编写JavaScript要么使用过程方式,要么使用面向对象方式。然而,由于它天生的动态属性,这种语言还能使用更为复杂和有趣的模式。这些技巧要利用ECMAScript的语言特点、BOM扩展和DOM功能来获得强大的效果。22.1高级函数函数是JavaScript中最有趣的部分之一。它们本质上是十分简单和过程化的,但也可以是非常复杂和动态的。一些额外的功能可以通过使用闭包来实现。此外,由于所有函数都是对象,所以使用函数指针非常简单。这些令JavaScript函数不仅有趣而且强大。以下几节描绘了几种在JavaScript中使用函数的高级方法。22.1.1安全的类型检测JavaScript内置的类型检测机制并非安全可靠。事实上,发生错误否定及错误肯定的情况也不在少数。比如说typeof操作符吧,由于它有一些无法预知的行为,经常会导致检测数据类型时得到不靠谱的结果。Safari(直至第4版)在对正则表达式应用typeof操作符时会返回“function”,因此很难确定某个值到底是不是函数。再比如,instanceof操作符在存在多个全局作用域(像一个页面包含多个frame)的情况下,也是问题多多。一个经典的例子(第五章提到过)就是像下面这样将对象标识为数组:var isArray = value instanceof Array;以上代码要返回true,value必须是一个数组,而且还必须与Array构造函数在同个全局作用域中。(别忘了Array是window的属性。)如果value是在另个frame中定义的数组,那么以上代码会返回false。在检测某个对象到底是原生对象还是开发人员自定义的对象的时候,也会有问题。出现这个问题的原因是浏览器开始原生支持JSON对象了(就想原生支持window对象那样,浏览器原生支持了JSON对象)。因为很多人一直在使用Douglas Crockford的JSON库,而该库定义了一个全局JSON对象。于是开发人员很难确定页面中的JSON对象到底是不是原生的。解决上述问题的办法都一样,大家知道,在任何值上调用Object原生的toString()方法,都会返回一个[object NativeConstructorName]格式的字符串。每个类在内部都有一个[[Class]]属性,这个属性中就指定了上述字符串中的构造函数名(比如数组的构造函数名是Array,函数的构造函数名是Function)。例子:console.log(Object.prototype.toString.call(value)); //"[object Array]"为什么要加个call?亲测加个call返回的是“[object Array]”,不加call返回的是“[object Object]”。新创建一个函数vvar,带入括号中检验,加个call返回的是“[object Function]”,不加call返回的是“[object Object]”。所以这个方法有两个点:·使用的方法是Object上原生的toString()方法。·用了一个call()。第一点,只使用Object原生的toString()方法,是因为其他类型的toString()方法都是被重写了的toString()方法,比如数组类型的toString()方法返回的是字符串形式的值,只有原生的Object上的toString()方法才能返回一个[object NativeConstructorName]格式的字符串,才能取到每个类内部的[[Class]]属性,才能得到那个NativeConstrutorName的值。第二点,首先要知道call()的作用:call()函数用于调用当前函数functionObject,并可同时使用指定对象thisObj作为本次执行时functionObject函数内部的this指针引用。不加call()调用toString()方法时this指针应该是指向window对象,传了个数组进去之后,this的引用就改成了这个数组对象,但是确切的使用原因还搞不清楚。由于原生数组的构造函数名与全局作用域无关,因此使用toString()就能保证返回一致的值。利用这一点,可以创建如下函数:function isArray(value){ return Object.prototype.toString.call(value) == "[object Array]";}同样,也可以基于这一思路来测试某个值是不是原生函数或正则表达式:function isFunction(value){ return Object.prototype.toString.call(value) == "[object Function]";}function isRegExp(value){ return Object.prototype.toString.call(value) == "[object RegExp]";}网上有更好的办法,因为前面的“[object”和后面的“]”是固定的,所以可以截取这个字符串中间的那一段不同的就能返回被检测的这个值是什么类型的。要注意,对于在IE中以COM对象形式实现的任何函数,isFunction()都将返回false(因为她们并非原生的JavaScript函数,请参考第十章中更详细的介绍)。这一技巧也广泛应用于检测原生JSON对象。Object的toString()方法不能检测非原生构造函数的构造函数名。因此,开发人员定义的任何构造函数都将返回[object Object](比如自定义了一个Person构造函数,使用这个技巧只会返回[object Object] ,但是可以使用person1 instanceof Person返回true)。有些JavaScript库会包含与下面类似的代码:var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON) == "[object JSON]";在Web开发中能够区分原生与非原生JavaScript对象非常重要。只有这样才能确切知道某个对象到底有哪些功能,这个技巧可以对任何对象给出正确的结论。22.1.2作用域安全的构造函数第六章讲述了用于自定义对象的构造函数的定义和用法。我应该还记得,构造函数其实就是一个使用new操作符调用的函数。当使用new调用时,构造函数内用到的this对象会指向新创建的对象实例,如下面的例子所示:function Person(name,age,job){ this.name = name; this.age = age; this.job = job;}var person = new Person("Nicholas",29,"Software Enigneer");上面这个例子中,Person构造函数使用this对象给三个属性赋值:name、age和job。当和new操作符连用时,则会创建一个新的Person对象,同时会给它分配这些属性。问题出在当没有使用new操作符来调用该构造函数的情况上。由于该this对象是在运行时绑定的,所以直接调用Person(),this会映射到全局对象window上,导致错误对象属性的意外增加。例如:var person = Person("Nicholas",29,"Software Engineer");console.log(window.name); //"Nicholas"console.log(window.age); //29console.log(window.job); //"Software Engineer"这里,原本针对Person实例的三个属性被加到window对象上,因为构造函数是作为普通函数调用的,忽略了new操作符。这个问题是由this对象的晚绑定造成的,在这里this被解析成了window对象。由于window的name属性是用于识别链接目标和iframe的,所以这里对该属性的偶然覆盖可能会造成该页面上出现其他错误。这个问题的解决方法就是创建一个作用域安全的构造函数。作用域安全的构造函数在进行任何更改前,首先确认this对象是正确类型的实例。如果不是,那么会创建新的实例并返回,例子:function Person(name,age,job){ if (this instanceof Person) { this.name = name; this.age = age; this.job = job; } else { return new Person(name,age,job) //相当于重新实例化一次 }}var person1 = Person("Nicholas",29,"Software Engineer");console.log(window.name); //""console.log(person1.name); //"Nicholas"var person2 = new Person("Shelby",34,"Ergonomist");console.log(person2.name) //"Shelby"这段代码中的Person构造函数添加了一个检查并确保this对象是Person实例的 if 语句,它表示要么使用new操作符,要么在现有的Person实例环境中调用构造函数。任何一种情况下,对象初始化都能正常进行。如果this并非Person的实例,那么会再次使用new操作符调用构造函数并返回结果。最后的结果是,调用Person构造函数时无论是否使用new操作符,都会返回一个Person的新实例,这就避免了在全局对象上意外设置属性关于作用域安全的构造函数的贴心提示。实现这个模式后,你就锁定了可以调用构造函数的环境。如果你想使用构造函数窃取模式的继承且不使用原型链,那么这个继承很可能被破坏。例子:function Polygon(sides){ if (this instanceof Polygon) { this.sides = sides; this.getArea = function(){ return 0; } } else { return new Polygon(sides); }}function Rectangle(width,height){ Polygon.call(this,2); this.width = width; this.height = height; this.getArea = function(){ return this.width this.height; };}var rect = new Rectangle(5,10);console.log(rect.sides); //undefined这段代码中,Polygon构造函数是作用域安全的,然而Rectange构造函数则不是。新创建一个Rectangle实例之后,这个实例应该通过Polygon.call()来继承Polygon的sides属性。但是,由于Polygon构造函数是作用域安全的,this对象并非Polygon的实例,所以会创建并返回一个新的Polygon对象。Rectangle构造函数中的this对象并没有得到增长,同时Polygan.call()返回的值也没有用到,所以Rectangle实例中就不会有sides属性。(想过直接把判断语句改成 if (!(this instanceof window)),既然没加new操作符this会指向window对象,那就检测this是不是指向window不就行了吗,试了下发现报错了,就是instanceof 右边那个值不能是window对象)如果构造函数窃取结合使用原型链或者寄生组合则可以解决这个问题。考虑以下例子:function Polygon(sides){ if (this instanceof Polygon) { this.sides = sides; this.getArea = function(){ return 0; }; } else { return new Polygon(sides); }}function Rectangle(width,height){ Polygon.call(this,2); this.width = width; this.height = height; this.getArea = function(){ return this.width this.height; };}Rectangle.prototype = new Polygon;var rect = new Rectangle(5,10);console.log(rect.sides); //2上面这段重写的代码中,一个Rectangle实例也同时是一个Polygon实例,所以Polygon.call()会照愿意执行,最终为Rectangle实例添加了sides属性。(Rectiangle的原型继承自Polygon,Rectiangle中没有sides,就会在原型中找)多个程序员在同一个页面上写JavaScript代码的环境中,作用域安全构造函数就很有用了。届时,对全局对象意外的更改可能会导致一些常常难以追踪的错误。除非你单纯基于构造函数窃取来实现继承,推荐作用域安全的构造函数作为最佳实践。(其实作用域安全的构造函数就是为了反之别人实例化的时候忘记加new?)22.1.3惰性载入函数因为浏览器之间行为的差异,多数JavaScript代码包含了大量的 if 语句,将执行引导到正确的代码中。看看下面来自上一章的createXHR()函数:function createXHR(){ if (typeof XMLHttpRequest != "undefined") { return new XMLHttpRequest(); } else if(typeof ActiveXObject != "undefined") { if (typeof arguments.callee.activeXString != "string") { var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"], len,i; for(i=0,len=versions.length; i<len; i++){ try { new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (ex) { //跳过 } } } return new ActiveXObject(arguments.callee.activeXString); } else { throw new Error("No XHR object available"); }}每次调用createXHR()的时候,它都要对浏览器所支持的能力仔细检查。首先检查内置的XHR,然后测试有没有基于ActiveX的XHR,最后如果都没有发现的话就抛出一个错误。每次调用该函数都是这样,即使每次调用时分支的结果都不变;如果浏览器支持内置XHR,那么它就一直支持了,那么这种测试就变得没必要了。即使只有一个 if 语句的代码,也肯定要比没有 if 语句的漫,所以如果 if 语句不必每次执行,那么代码可以运行得更快一些。解决方案就是称之为惰性载入的技巧。惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式,第一种就是在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一个按合适方式执行的函数,这样任何对原函数的调用就不用再经过执行的分支了。例如,可以用下面的方式使用惰性载入重写createXHR():function createXHR(){ if (typeof XMLHttpRequest != "undefined") { createXHR = function(){ //覆盖原来那个函数 return new XMLHttpRequest; }; } else if (typeof ActiveXObject != "undefined") { createXHR = function(){ if (typeof arguments.callee.activeXString != "string") { var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"], i,len; for(i=0,len=versions.length; i<len; i++){ try{ new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (ex){ //skip } } } return new ActiveXObject(arguments.callee.activeXString); }; } else { createXHR = function(){ throw new Error("No XHR object available."); } } return createXHR(); //就我来说,我觉得这句return才是重点,很可能自己写的会忘记加,如果不把函数return出来在外部就使用不了}在这个惰性载入的createXHR()中,if 语句的每一个分支都会为createXHR变量赋值,有效覆盖了原有的函数。最后一步便是调用新赋的函数。下一次调用createXHR()的时候,就会直接调用被分配的函数,这样就不用在此执行 if 语句了。第二种实现惰性载入的方式是在声明函数时就指定适当的函数。这样,第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能。以下就是按照这一思路重写前面例子的结果:var createXHR = (function createXHR(){ if (typeof XMLHttpRequest != "undefined") { return function(){ return new XMLHttpRequest; }; } else if (typeof ActiveXObject != "undefined") { return function(){ if (typeof arguments.callee.activeXString != "string") { var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"], i,len; for(i=0,len=versions.length; i<len; i++){ try{ new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (ex){ //skip } } } return new ActiveXObject(arguments.callee.activeXString); }; } else { return function(){ throw new Error("No XHR object available."); }; }})();(函数还没使用时就开始执行一遍,得到那个在当前浏览器适用的对象或报错)。这个例子中使用的技巧是创建一个匿名、自执行的函数, 用以确定应该使用哪一个函数实现。实际的逻辑都一样。不一样的地方就是第一行代码(使用var定义函数)、新增了自执行的匿名函数,另外每个分支都返回正确的函数定义,以便立即将其赋值给createXHR()。惰性载入函数的优点是只在执行分支代码时牺牲一点儿性能。至于哪种方式更合适,就要看你的具体需求而定了。不过两种方式都能避免执行不必要的代码。22.1.4函数绑定另一个日益流行的高级技巧叫做函数绑定。函数绑定要创建一个函数,可以在特定的this环境中以指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境。例子:var handler = { message:"Event handled", handleClick:function(event){ console.log(this.message); }};var btn = document.getElementById("my-btn");EventUtil.addHandler(btn,"click",handler.handleClick);在这个例子中,创建了一个叫做handler的对象。handler.handlerClick()方法被分配为一个DOM按钮的事件处理程序。当按下该按钮时,就调用该函数,显示一个警告框。虽然貌似警告框应该显示Event handled,然而实际上显示的是undefined。这个问题在于没有保存handler.handlerClick()的环境,所以this对象最后是指向了DOM按钮而非handler(在IE8中,this指向window)。可以如下面例子所示,使用一个闭包来修正这个问题:var handler = { message:"Event handled", handleClick:function(event){ console.log(this.message); }};var btn = document.getElementById("my-btn");EventUtil.addHandler(btn,"click",function(event){ handler.handleClick(event);});(不懂上面的代码,为什么给handler.handleClick()套一层外部函数就能让this继续指向handler对象,虽然知道用的函数参数event是来自外部函数参数的event,但又怎么样?)(本来想为什么不用call()就行,试了一下果然不行,用call()会还没点击按钮函数就先执行然后输出“Event handled”了。)这个解决方案在onclick事件处理程序内使用了一个闭包直接调用handler.handleClick()。当然,这是特定于这段代码的解决方案。创建多个闭包可能会令你的代码变得难于理解和调试。因此,很多JavaScript库实现了一个可以将函数绑定到指定环境的函数。这个函数一般都叫bind()。一个简单的bind()函数接收一个函数和一个环境,并返回一个给定环境中调用给定函数的函数,并且将所有参数原封不动地传递过去。语法如下:function bind(fn,context){ return function(){ return fn.apply(context,arguments); }}这个函数似乎简单,但其功能是非常强大的。在bind()中创建了一个闭包,闭包使用apply()调用传入的函数,并给apply()传递context对象和参数。注意这里使用的arguments对象是内部函数的,而非bind()的。当调用返回的函数时,它会在给定环境中执行被传入的函数并给出所有参数。bind()函数按如下方式使用:var handler = { message:"Event handled", handleClick:function(event){ console.log(this.message); }};var btn = document.getElementById("my-btn");EventUtil.addHandler(btn,"click",bind(handler.handleClick,handler)); 原生的bind()方法与前面介绍的自定义bind()方法类似,都是要传入作为this值的对象。支持原生bind()方法的浏览器有IE 9+、Firefox 4+ 和 Chrome。只要是将某个函数指针以值的形式进行传递,同时该函数必须在特定环境中执行,被绑定函数的效用就突显出来了。它们主要用于事件处理程序以及setTimeout()和setInterval()。然而,被绑定函数与普通函数相比有更多的开销,她们需要更多内存,同时也因为多重函数调用稍微慢一点,所以最好只在必要时使用。(bind()方法是在调用函数时为了保留函数的执行环境调用的)22.1.5函数柯里化与函数绑定紧密相关的主题是函数柯里化(function currying),它用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的;使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数。例子:function add(num1,num2){ return num1+num2;}function curriedAdd(num2){ return add(5,num2);}console.log(add(2,3)); //5console.log(curriedAdd(3)); //8这段代码定义了两个函数:add()和curriedAdd()。后者本质上是在任何清情况下第一个参数为5的add()版本。尽管从技术上来说curriedAdd()并非柯里化函数,但它很好地展示了其概念。柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传入要柯里化的函数和必要参数。下面是创建柯里化函数的通用方式:function curry(fn){ //这里看起来只有一个函数作为参数,但可以在后面无限制地添加参数 var args = Array.prototype.slice.call(arguments,1); return function(){ var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(null,finalArgs); };}curry()函数的主要工作就是将被返回函数的参数进行排序。curry()的第一个参数是要进行柯里化的函数,其他参数是要传入的值。为了获取第一个参数之后的所有参数,在arguments对象上调用了slice()方法(原来如此!是为了得到curry()里的除第一个参数后面的所有参数,比如curry(fn,1,2,3),args里面就是[1,2,3]),并传入参数1表示被返回的数组包含从第二个参数开始的所有参数。然后args数组包含了来自外部函数的参数。在内部函数中,创建了innerArgs数组用来存放所有传入的参数(又一次用到了slice())。有了存放来自外部函数和内部函数的参数数组后,就可以使用concat()方法将它们组合为finalArgs,然后使用apply()将结果传递给该函数。注意这个函数并没有考虑到执行环境,所以调用apply()时第一个参数是null。curry()函数可以按以下方式应用:function add(num1,nunm2){ return num1 + num2;}var curriedAdd = curry(add,5);console.log(curriedAdd(3)); //8在这个例子中,创建了第一个参数绑定为5的add()的柯里化版本。当调用curriedAdd()并传入3时,3会成为add()的第二个参数,同时第一个参数依然是5,最后结果便是和 8 。你也可以像下面例子这样给出所有的函数参数:function add(num1,num2){ return num1 + num2;}var curriedAdd = curry(add,5,12);console.log(curriedAdd()); //17(哈哈哈哈哈哈,这个不是我刚刚做的实验吗,传多一个参数。哦不是,我做的实验是传多一个参数,同时curried()里也添加一个参数(比如curried(10),最后的结果也是17。因为看curry()的代码,curry(fn,x,x,x)后面的参数被放进args数组里后,在return的函数里,也就是curriedAdd的参数被放进innerArgs数组里,最后的finalArgs=args.concat(innerArgs)把数组innerArgs放在了数组尾端,所以finalArgs = [5,12,10],而add()(也就是fn.apply(null,finalArgs)中的fn)只接收两个参数,所以后面的10被忽略了。))在这里,柯里化的add()函数两个参数都提供了,所以以后就无需再传递它们了。函数柯里化还常常作为函数绑定的一部分包含在其中,构造出更为复杂的bind()函数。例如:function bind(fn,context){ var args = Array.prototype.slice.call(arguments,2); return function(){ var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(context,finalArgs); };}对curry()函数的主要更改在于传入的参数个数,以及它如何影响代码的结果。curry()仅仅接收一个要包裹的函数作为参数,而bind()同时接收函数和一个object对象。这表示给被绑定的函数的参数是从第三个开始而不是第二个,这就要更改slice()的第一处调用。另一处更改是在倒数第3行将object对象传给apply()。当使用bind()时,它会返回绑定到给定环境的函数,并且可能它其中某些函数参数已经被设好。当你想除了event对象再额外给事件处理程序传递参数(就是不止fn(event),还要加参数)时,这非常有用,例如:var handler = { message:"Event handler", handleClick:function(name,event){ console.log(this.message + ":" + name + ":" + event.type); }};var btn = document.getElementById("my-btn");EventUtil.addHandler(btn,"click",bind(handle.handleClick,handler,"my-btn"));在这个更新过的例子中,handler.handlerClick()方法接受了两个参数:要处理的元素的名字和event对象。作为第三个参数参数传递给bind()函数的名字,又被传递给了handler.handleClick(),而handler.handleClick()也会同时接收到event对象。ECMAScript 5的bind()方法也实现函数柯里化,只要在this的值之后再传入另一个参数即可。var handler = { message:"Event handler", handleClick:function(name,event){ console.log(this.message + ":" + name + ":" + event.type); }};var btn = document.getElementById("my-btn");EventUtil.addHandler(btn,"click",handler.handleClick.bind(handler,"my-btn"));JavaScript中的柯里化函数和绑定函数提供了强大的动态函数创建功能。使用bind()还是curry()要根据是否需要object对象响应来决定。她们都能用于创建复杂的算法和功能,当然两者都不应滥用,因为每个函数都会带来额外的开销。22.2防篡改对象JavaScript共享的本质一直是开发人员心头的痛。因为任何对象都可以被在同一环境中运行的代码修改。开发人员很可能会意外地修改别人的代码,甚至更糟糕地,用不兼容的功能重写原生对象。ECMAScript致力于解决这个问题,可以让开发人员定义防篡改对象(tamper-proof object)。第六章讨论了对象属性的问题,也讨论了如何手工设置每个属性的[[Configurable]]、[[Writable]]、[[Enumerable]]、[[Value]]、[[Get]]以及[[Set]]特性,以改变属性的行为。类似地,ECMAScript 5也增加了几个方法,通过它们可以指定对象的行为。不过请注意:一旦把对象定义为防篡改,就无法撤销了。22.2.1不可扩展对象默认情况下,所有对象都是可以扩展的。也就是说,任何时候都可以向对象中添加属性和方法。例如,可以像下面这样先定义一个对象,后来再给它添加一个属性:var person = {name:"Nicholas"};person.age = 29;即使第一行代码已经完整定义person对象,但第二行代码仍然能给它添加属性。现在,使用Object.preventExtensions()方法可以改变这个行为,让你不能再给对象添加属性和方法。例如:var person = {name:"Nicholas"};Object.preventExtensions(person);person.age = 29;console.log(person.age); //undefined在调用了Object.preventExtension()方法后,就不能给person对象添加新属性和方法了。在非严格模式下,给对象添加新成员会导致静默失败,因此person.age将是undefined。而在严格模式下,尝试给不可扩展的对象添加新成员会抛出错误。虽然不能给对象添加新成员,但已有的成员则丝毫不受影响。你仍然可以修改和删除已有的成员。另外,使用Object.isExtensible()方法还可以确定对象是否可以扩展。var person = {name:"Nicholas"};console.log(Object.isExtensible(person)); //true;Object.preventExtensions(person);console.log(Object.isExtensible(person)); //false 也不知道这两个ES 5制定的方法支持程度怎么样。22.2.2密封的对象ECMAScript 5为对象定义的第二个保护级别是密封对象(sealed object)。密封对象是不可扩展,而且已有成员的[[Configurable]]特性也被设置为false。这就意味着不能删除属性和方法,因为不能使用Object.defineProperty()把数据属性修改为访问器属性,或者相反。属性值是可以修改的。要密封对象,可以使用Object.seal()方法:var person = {name:"Nicholas"};Object.seal(person);person.age = 29;console.log(person.age); //undefineddelete person.name;console.log(person.name) //"Nicholas" 在这个例子中,添加age属性的行为被忽略了。而尝试删除name属性的操作也被忽略了,因此这个属性没有受任何影响。这是在非严格模式下的行为。在严格模式下,尝试添加或删除对象成员都会导致抛出错误。使用Object.isSealed()方法可以确定对象是否被密封了。因为被密封的对象不可扩展,所以用Object.isExtensible()检测密封的对象也会返回false。22.2.3冻结的对象最严格的反篡改级别是冻结对象(frozen object)。冻结的对象既不可扩展,又是密封的,而且对象数据属性的[[Writable]]特性会被设置为false。如果定义[[Set]]函数,访问器属性仍然是可写的。ECMAScript 5定义的Object.freeze()方法也可以用来冻结对象。person.age = 29;console.log(person.age); //undefineddelete person.name;console.log(person.name); //"Nicholas"person.name = "Greg";console.log(person.name); //"Nicholas"密封对象虽然不可扩展不可删除,但是已有的属性的[[Writable]]特性是true,是可以改写的。而被冻结的对象,属性不可修改。与密封和不允许扩展一样,对冻结的对象执行非法操作会“非忽严错”。当然,还有一个Object.isFrozen()方法用于检测冻结对象。因为冻结对象既是密封的又是不可扩展的,所以使用Object.isExtensible()和Object.isSealed()检测冻结对象将分别返回false和true。(一个是检测“是否可扩展”,一个是问“是否已经被密封”,所以返回的布尔值不一样)对JavaScript库的作者而言,冻结对象是很有用的。因为JavaScript库最怕有人意外地修改了库中的核心对象。冻结(或密封)主要的库对象能够防止这些问题发生。(哦知道了,seal()、preventExtensions() 、freeze()方法里面其实就是把对象中的几个特性从false修改为true从而实现了放置对象被篡改,给了 API 这样方便不用我们自己写)22.3高级定时器使用setTimeout()和setInterval()创建的定时器可以用于实现有趣且有用的功能。虽然人们对JavaScript的定时器存在普遍的误解,认为它们是线程,其实JavaScript是运行于单线程的环境中的,而定时器仅仅只是计划代码在未来的某个时间执行。执行时机是不能保证的,因为在页面的生命周期中,不同时间可能有其他代码在控制JavaScript进程。在页面下载完后的代码运行、事件处理程序、Ajax回调函数都必须使用同样的线程来执行。实际上,浏览器负责进行排序,指派某段代码在某个时间点运行的优先级。可以把JavaScript想象成在时间线上运行的。当页面载入时,首先执行是任何包含在<script>元素中的代码,通常是页面生命周期后面要用到的一些简单的函数和变量的声明,不过有时候也包含一些初始数据的处理。在这之后,JavaScript进程将等待更多代码执行。当进程空闲的时候,下一个代码会被触发并立刻执行。例如,当点击某个按钮时,onclick事件处理程序会立刻执行,只要JavaScript进程处于空闲状态。这样一个页面的时间线类似于图22-1。除了主JavaScript执行进程外,还有一个需要在进程下一次空闲时执行的代码队列。随着页面在其生命周期中的推移,代码会按照执行顺序添加入队列。例如,当某个按钮被按下时,它的事件处理程序代码就会被添加到队列中,并在下一个可能的时间里执行。当接收到某个Ajax响应时,回调函数的代码会被添加到队列。在JavaScript中没有任何代码是立刻执行的,但一旦进程空闲则尽快执行。定时器对队列的工作方式是,当特定时间过去后将代码插入。注意,给队列添加代码并不意味着对它理科执行,而只能表示它会尽快执行。设定一个i额150ms后执行的定时器不代表到了150ms代码就立刻执行,它代表代码会在150ms后被加入到队列中。如果在这个时间点上,队列中没有其他东西,那么这段代码就会被执行,表面上看上去好像代码就在精确指定的时间点上执行了。其他情况下,代码可能明显地等待更长时间才执行。请看代码:var btn = document.getElementById("my-btn");btn.onclick = function(){ setTimeout(function(){ document.getElementById("message").style.visibility = "visible"; },250); //其他代码}在这里给一个按钮设置了一个事件处理程序。事件处理程序设置了一个250ms后调用的定时器。点击该按钮后,首先将onclick事件处理程序加入队列。该程序执行后才设置定时器,再有250ms后,指定的代码才被添加到队列中等待执行。实际上,对setTimeout()的调用表示要晚点执行某些代码。关于定时器要记住的最重要的事情是,指定的时间间隔表示何时将定时器的代码添加到队列,而不是合适实际执行代码。如果前面的例子中的onclick事件处理程序执行了300ms,那么定时器的代码至少要在定时器设置之后的300ms之后才被执行。队列中的所有代码都要等到JavaScript进程空闲之后才能执行,而不管它们是如何添加到队列中的。见图:如图,尽管在255ms处添加了定时器代码,但这时候还不能执行,因为onclick事件处理程序仍在运行。定时器代码最早能执行的时机是在300ms处,即onclick事件处理程序结束之后。实际上Firefox中定时器的实现还能让你确定定时器过了多久才执行,这需传递一个实际执行的时间与指定的间隔的差值。例子://仅Firefox中setTimeout(function(diff){ if (diff > 0) { //晚调用 } else if (diff < 0) { //早调用 } else { //及时调用 }},250);(diff是Firefox中setTimeout()函数特有的对象?上面说是一个实际执行的时间与指定的间隔的差值)执行完一套代码后,JavaScript进程返回一段很短的时间,这样页面上的其他处理就可以进行了。由于JavaScript进程会阻塞其他页面处理,所以必须有这些小间隔来防止用户界面被锁定(代码长时间运行中还有可能出现)。这样设置一个定时器,可以确保在定时器代码执行前至少有一个进程间隔。22.3.1重复的定时器使用setInterval()创建的定时器确保了定时器代码规则地插入队列中。这个方式的问题在于,定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次,而之间没有任何停顿。幸好,JavaScript引擎够聪明,能避免这个问题。当使用setInterval()时,仅当没有该定时器的任何代码实例时,才将定时器代码添加到队列中。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。这种重复定时器的规则有两个问题:(1)某些间隔被跳过;(2)多个定时器的代码执行之间的间隔可能比预期的小。假设,某个onclick事件处理程序使用setInterval()设置了一个200ms间隔的重复定时器。如果事件处理程序花了300ms多一点的时间完成,同时定时器代码也花了差不多的时间,就会同时出现跳过间隔且连续运行定时器代码的情况。见图:这个例子中的第1个定时器是在205ms处添加到队列中的,但是直到过了300ms处才能执行。当执行这个定时器代码时,在405ms处又给队列添加了另外一个副本。在下一个间隔,即605ms处,第一个定时器代码仍在执行,同时在队列中已经有了一个定时器代码的实例。结果是,在这个时间点上的定时器代码不会被添加到队列中。结果在5ms处添加的定时器代码结束之后,405ms处添加的定时器代码就会立刻执行。为了避免setInterval()的重复定时器这2个缺点,你可以用如下模式使用链式setTimeout()调用。setTime(function(){ //处理中 setTimeout(arguments.callee,interval); //这里会一直递归调用,不止调用两次},intetval)这个模式链式调用了setTimeout(),每次函数执行的时候都会创建一个新的定时器。第二个setTimeout()调用使用了arguments.callee来获取对当前执行的函数的引用,并为其设置另外一个定时器。这样做的好处是,在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。而且,它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。这个模式只要用于重复定时器。例子:setTimeout(function(){ var div = document.getElementById("myDiv"); left = parseInt(div.style.left) + 5; div.style.left = left + "px"; if (left < 200) { setTimeout(arguments.callee,50) }},50);这段定时器代码每次执行的时候将一个<div>元素向右移动,当左坐标在200像素的时候停止。JavaScript动画中使用这个模式很常见。✎:每个浏览器窗口、标签页、或者frame都有其各自的代码执行队列。这意味着,进行跨frame或者跨窗口的定时调用,当代码同时执行的时候可能会导致竞争条件。无论何时需要使用这种通信类型,最好是在接收frame或者窗口中创建一个定时器来执行代码。22.3.2Yielding Processes运行在浏览器中的JavaScript都被分配了一个确定数量的资源。不同于桌面应用往往能够随意控制他们要的内存大小和处理器事件,JavaScript被严格限制了,以防止恶意的Web程序把用户的计算机搞挂了。其中一个限制是长时间运行脚本的制约,如果代码运行超过特定的时间或者特定语句数量就不让它继续执行。如果代码达到了这个限制,会弹出一个浏览器错误的对话框,告诉用户某个脚本会用过长的时间执行,询问是允许其继续执行还是停止它。所有JavaScript开发人员的目标就是,确保用户永远不会在浏览器中看到这个令人费解的对话框。定时器是绕开此限制的方法之一。脚本长时间运行的问题通常是由两个原因造成的:过长的、过深嵌套的函数调用或者是进行大量处理的循环。这两者中,后者是较为容易解决的问题。长时间运行的循环通常遵循以下模式:for(var i=0,len=data.length; i<len; i++){ process(data[i]);}这个模式的问题在于要处理的项目的数量在运行前是不可知的。如果完成process()要花100ms,只有2个项目的数组可能不会造成影响,但是10个的数组可能会导致脚本要运行一秒钟才能完成。数组中的项目数量直接关系到执行完该循环的时间长度。同时由于JavaScript的执行是一个阻塞操作,脚本运行所花时间越久,用户无法与页面交互的时间也越久。在展开该循环之前,你需要回答以下两个重要的问题:·该处理是否必须同步完成?如果这个数据的处理与造成其他运行的阻塞,那么最好不要改动它。不过,如果你对这个问题的回答确定为“否”,那么将某些处理推迟到以后是个不错的备选项。·数据是否按顺序完成?通常,数组只是对项目的组合和迭代的一种简便的方法而无所谓顺序。如果项目的顺序不是非常重要,那么可能可以将某些处理推迟到以后。当你发现某个循环占用了大量时间,同时对于上述两个问题,你的回答都是“否”,那么你就可以使用定时器分割这个循环。这是一种叫做数组分块(array chunking)的技术,小块小块地处理数组,通过每次一小块。基本的思路是为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。基本的模式如下:setTimeout(function(){ //取出下一个条目并处理 var item = array.shift(); process(item); //若还有条目,再设置另一个定时器 if (array.length) { setTimeout(arguments.callee,100); }},100);在数组分块模式中,array变量本质上是一个“待办事宜”列表,它包含了要处理的项目。使用shift()方法可以获取队列中下一个要处理的项目,然后将其传递给某个函数。如果在队列中还有其他项目,则设置另一个定时器,并通过arguments.callee调用同一个匿名函数。要实现数组分块非常简单,可以使用以下函数:function chunk(array,process,context){ setTimeout(function(){ var item = array.shift(); process.call(context,item); if (array.length > 0) { setTimeout(arguments.callee,100); } },100);}chunk()方法接受三个参数:要处理的项目的数组,用于处理项目的函数,以及可选的运行该函数的环境。函数内部用了之前描述过的基本模式,通过call()调用的process()函数,这样可以设置一个合适的执行环境(如果必须)。定时器的时间间隔设置为100ms,使得JavaScript进程有时间在处理项目的事件之间转入空闲。你可以根据你的需要更改这个时间间隔大小,不过100ms在大多数情况下效果不错,可以按如下所示使用该函数:var data = [12,123,1234,453,436,23,23,5,4123,45,346,5634,2234,345,342];function printValue(item){ var div = document.getElementById("myDiv"); div.innerHTML += item + "<br>";}chunk(data,printValue);这个例子使用printValue()函数将data数组中的每个值输出到一个<div>元素。由于函数处在全局作用域内,因此无需给chunk()传递一个context对象。必须当心的地方时,传递给chunk()的数组是用作一个队列的,因此当处理数据的同时,数组中的条目也在改变。如果你想保持原数组不变,则应该将该数组的克隆传递给chunk(),如下列所示:chunk(data.concat(),printValue);当不传递任何参数调用某个数组的concat()方法时,将返回和原来数组中项目一样的数组。这样,就可以保证原数组不会被该函数更改。数组分块的重要性在于它可以将多个项目的处理在执行队列上分开,在每个项目处理之后,给予其他的浏览器处理机会运行,这样就可能避免长时间运行脚本的错误。✎:一旦某个函数需要花50ms以上的时间完成,那么最好看看能否将任务分割为一系列可以使用定时器的小任务。22.3.3函数节流(当事件处理程序会连续触发时通过定时器拉长触发间隔)浏览器中某些计算和处理要比其他的昂贵很多。例如,DOM操作比起非DOM交互需要更多的内存和CPU时间。连续尝试进行过多的DOM相关操作可能会导致浏览器挂起,有时候甚至会崩溃。尤其在IE中使用onresize事件处理程序的时候容易发生,当调整浏览器大小的时候,该事件会连续触发。在onresize事件处理程序内部如果尝试进行DOM操作,其高频率的更高可能会让浏览器崩溃。为了绕开这个问题,可以使用定时器对该函数进行节流。函数节流背后的基本思想是指,某些代码不可以在没有间断的情况连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。以下是该模式的基本形式:var processor = { timeoutId:null, //实际进行处理的方法 performProcessing:function(){ clearTimeout(this.timeoutId);u }, //初始处理调用的方法 process:function(){ clearTimeout(this.timeoutId); var that = this; this.timeoutId = setTimeout(function(){ that.performProcessing(); },100); }};在这段代码中,创建了一个叫做processor对象。这个对象还有2个方法:process()和performProcessing()。前者是初始化任何处理所必须调用的,后者则实际进行应完成的处理。当调用了process(),第一步是清除存好的timeoutId,来阻止之前的调用被执行。然后,创建一个新的定时器调用performProcessing()。由于setTimeout()中用到的函数的环境总是window,所以有必要保存this的引用以方便以后使用。时间间隔设定为了100ms,这表示最后一次调用process()之后至少100ms后才会调用performProcessing()。所以如果100ms之内调用了process()工20次,performanceProcessing()仍只会被调用一次。这个模式可以使用throttle()函数来简化,这个函数可以自动进行定时器的设置和清除,如下所示:function throttle(method,context){ clearTimeout(method.tId); method.tId = setTimeout(function(){ method.call(context); },100);}throttle()函数接受两个参数:要执行的函数以及在哪个作用域中执行。上面这个函数首先清除之前设置的任何定时器。定时器ID是存储在函数的tId属性中的,第一次把方法传递给throttle()的时候,这个属性可能并不存在。接下来,创建一个新的定时器,并将其ID储存在方法的tId属性中。如果这是第一次对这个方法调用throttle()的话,这段代码会创建该属性。定时器代码使用call()来确保方法在适当的环境中执行。如果没有给出第二个参数,那么就在全局作用域内执行该方法。前面提到过,节流在resize事件中是最常用的。如果你基于该事件来改变页面布局的话,最好控制处理的频率,以确保浏览器不会在极短的时间内进行过多的计算。例如,假设有一个<div/>元素需要保持它的高度始终等同于宽度。那么实现这一功能的JavaScript可以如下编写:window.onresize = function(){ var div = document.getElementById("myDiv"); div.style.height = div.offsetWidth + "px";}这段非常简单的例子有两个问题可能会造成浏览器运行缓慢。首先,要计算offsetWidth属性,如果该元素或者页面上其他元素有非常复杂的CSS样式,那么这个过程将会很复杂。其次,设置某个元素的高度需要对页面进行回流来令改动生效。如果页面有很多元素同时应用了相当数量的CSS的话,这又需要很多计算,这就可以用到throttle()函数,例子:function resizeDiv(){ var div = document.getElementById("myDiv"); div.style.height = div.offsetWidth + "px";}window.onresize = function(){ throttle(resizeDiv);}这里调整大小的功能被放入一个叫做resizeDiv()的单独函数中。然后onresize事件处理程序调用throttle()并传入resizeDiv函数,而不是直接调用resizeDiv()。多数情况下,用户是感受不到变化的,虽然给浏览器节省的计算可能会非常大。只要代码是周期性执行的,都应该使用节流,但是你不能控制请求执行的速率。这里展示的throttle()函数用了100ms作为间隔,你当然可以根据你的需求来修改它。22.4自定义事件在本书前面,你已经学到事件是JavaScript与浏览器交互的主要途径。事件一种叫做观察者的设计模式,这是一种创建松散耦合代码的技术。对象可以发布事件,用来表示在该对象生命周期中某个有趣的时间到了(???)。然后其他对象可以观察该对象,等待这些有趣的时刻到来并通过运行代码响应。观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运行即使观察者不存在。从另一个方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。涉及DOM上时,DOM元素便是主体,你的事件处理代码便是观察者。事件是与DOM交互的最常见的方式,但它们也可以用于非DOM代码中——通过实现自定义事件。自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听那些事件。实现此功能的基本模式可以如下定义:function EventTarget(){ this.handlers = {};}EventTarget.prototype = { constructor:EventTarget, addHandler:function(type,handler){ if (typeof this.handlers[type] == "undefined") { this.handlers[type] = []; } this.handlers[type].push(handler); }, fire:function(event){ if (!event.target) { event.target = this; } if (this.handlers[event.type] instanceof Array) { var handlers = this.handlers(event.type); for(var i=0,len=handlers.length; i < len; i++){ handlers(event); } } }, removeHandler:function(type,handler){ if (this.handlers[type] instanceof Array) { var handlers = this.hanlers[type]; for(Var i=0,len=hanlers.length; i < len; i++){ if (handlers[i] === handler) { brreak; } } hanlers.splice(i,1); } }};EventTarget类型有一个单独的属性handlers,用于存储事件处理程序。还有三个方法:addHadnler(),用于注册给定类型事件的事件处理程序;fire(),用于触发一个事件;removeHandler(),用于注销某个事件类型的事件处理程序。addHandler()方法接受两个参数:事件类型和用于处理该事件的函数。当调用该方法时,会进行一次检查,看看handlers属性中是否已经存在一个针对该事件类型的数组;如果没有,则创建一个新的。然后使用push()将该处理程序添加到数组的末尾。如果要触发一个事件,要调用fire()函数。该方法接收一个单独的参数,是一个至少包含type属性的对象。fire()方法先给event对象设置一个target属性,如果它尚未被指定的话。然后它就查找对该事件类型的一组处理程序,调用各个函数,并给出event对象。因为这些都是自定义事件,所以event对象上还需要额外的信息由你自己决定。removeHandler()方法是addHandler()的辅助,它们接受的参数一样:事件的类型和事件处理程序。这个方法搜索事件处理程序的数组找到要删的处理程序的位置。如果找到了,则使用break操作符退出for循环。然后使用splice()方法将项目从数组中删除。然后,使用EventTarget类型的自定义事件可以如下使用:function handleMessage(event){ console.log("Message received:" + event.message);}//创建一个新对象var target = new EventTarget();//添加一个事件处理程序target.addHandler("message",handleMessage);//触发事件target.fire({type:"message",message:"Hello world!"});//删除事件处理程序target.removeHandler("message",handleMessage);//再次,应没有处理程序target.fire({type:"message",message:"Hello world!"});在这段代码中,定义了handlerMessage()函数用于处理message事件。它接受event对象并输出message属性。调用target对象的addHandler()方法并传给“message”以及handleMessage()函数。在接下来的一行上,调用了fire()函数,并传递了包含2个属性,即type和message的对象直接量。它会调用message事件的事件处理程序,这样就会显示一个警告框(来自handlerMessage())。然后删除了事件处理程序,这样即使事件再次触发,也不会显示任何警告框。因为这种功能是封装在一种自定义类型中的,其他对象可以继承EventTarget并获得这个行为,例子:function Person(name,age){ EventTarget.call(this); this.name = name; this.age = age;}inheritPrototype(Person,EventTarget);Person.prototype.say = function(message){ this.fire({type:"message",message:message});};Person类型使用了寄生组合继承(参见第六章)方法来继承EventTarget。一旦调用了say()方法,便触发了事件,它包含了消息的细节。在某种类型的另外的方法中调用fire()方法是很常见的,同时它通常不是公开调用的。这段代码可以照如下方式使用:function handlerMessage(event){ console.log(event.target.name + "says:" + event.message);}//创建新Personvar person = new Person("Nicholas",29);//添加一个事件处理程序person.addHandler("message",handleMessage);//在该对象上调用1个方法,它触发消息事件person.say("Hi there.");这个例子中的handlerMessage()函数显示了某人名字(通过event.target.name获得)的一个警告框和消息正文。当调用say()方法并传递一个消息时,就会触发message事件。接下来,它又会调用handlerMessage()函数并显示警告框。当代码中存在多个部分在特定时刻相互交互的情况下,自定义事件就会非常有用了。这时,如果每个对象都有对其他所有对象的引用,那么整个代码就会紧密耦合,同时维护也变得很困难,因为对某个对象的修改也会影响到其他对象。使用自定义事件有助于解耦相关对象,保持功能的隔绝。在很多情况中,触发事件的代码和监听事件的代码是完全分离的。 实在是不懂,就是模拟了DOM上的那些事件,还有一个是JavaScript中的一个设计模式——观察者模式。22.5拖放拖放是一种非常流行的用户界面模式。它的概念很简单:点击某个对象,并按住鼠标按钮不放,将鼠标移动到另一个区域,然后释放鼠标按钮将对象“放”在这里。拖放功能也流行到了Web上,成为了一些更传统的配置界面的一种候选方案。拖放的基本概念很简单:创建一个绝对定位的元素,使其可以用鼠标移动。这个技术源自一种叫做“鼠标拖尾”的经典网页技巧。鼠标拖尾是一个或者多个图片在页面上跟着鼠标指针移动。氮元素鼠标拖尾的基本代码需要为文档设置一个onmousemove事件处理程序,它总是将指定元素移动到鼠标指针的位置,如下面的例子所示:EventUtil.addHandler(document,"mousemove",function(event){ var myDiv = document.getElementById("myDiv"); myDiv.style.left = event.clientX + "px"; myDiv.style.top = event.clientY + "px";});(要让div可以移动还有一个前提是position:absolute。而且有BUG:滚动页面后div并没有跟随。)在这个例子中,元素的left和top坐标设置为了event对象的clientX和clientY属性,这就将元素放到了视口中指针的位置上。它的效果是一个元素始终跟随指针在页面上的移动。只要正确的时刻(当鼠标按钮按下的时候)实现该功能,并在之后删除它(当释放鼠标按钮时),就可以实现拖放了。最简单的拖放界面可用以下代码实现:var DragDrop = function(){ var dragging = null; function handleEvent(event){ //获取事件和目标 event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); //确定事件类型 switch(event.type){ case "mousedown": if (target.className.indexOf("draggable") > -1) { dragging = target; } break; case "mousemove": console.log("var") if (dragging !== null) { //指定位置 dragging.style.left = event.clientX + "px"; dragging.style.top = event.clientY + "px"; } console.log("var") break; case "mouseup": dragging = null; break; } }; //公共接口 return { enable:function(){ EventUtil.addHandler(document,"mousedown",handleEvent); EventUtil.addHandler(document,"mouseover",handleEvent); EventUtil.addHandler(document,"mouseup",handleEvent); }, disable:function(){ EventUtil.addHandler(document,"mousedown",handleEvent); EventUtil.addHandler(document,"mouseover",handleEvent); EventUtil.addHandler(document,"mouseup",handleEvent); } }}();(代码执行不了啊啊啊啊)DragDrop对象封装了拖放的所有基本功能。这是一个单例对象,并使用了模块模式来隐藏某些实现细节。dragging变量起初是null,将会存放被拖动的元素,所以当该变量不为null时,就知道正在拖动某个东西。handleEvent()函数处理拖放功能中的所有三个鼠标事件。它首先获取event对象和事件目标的引用。之后,用一个switch语句确定要触发哪些事件样式。当mousedown事件发生时,会检查target的class是否包含“draggable”类,如果是,那么将target存放到dragging中。这个技巧可以很方便地通过标记语言而非JavaScript脚本来确定可拖动的元素(这也是一种思想)。handleEvent()的mousemove情况和前面的代码一样,不过要检查dragging是否为null。当它不是null,就知道dragging就是要拖动的元素,这样就会把它放到恰当的位置上。mouseup情况就仅仅是将dragging重置为null,让mousemove事件中的判断失效。DragDrop还有两个公共方法:enable()和disable(),它们只是相应添加和删除所有的事件处理程序,这两个函数提供了额外的对拖放功能的控制手段。22.5.1修缮拖动功能当你试了上面的例子之后,你会发现元素的左上角总是和指针在一起。这个结果对用户来说有一点不爽,因为当鼠标开始移动的时候,元素好像是突然跳了以下。理想情况是,这个活动应该看上去好像这个元素是被指针“抬起来”的,也就是说当在拖动元素的时候,用户点击的那一点就是指针应该保持的位置,见图:要达到需要的结果,必须做一些额外的计算。你需要计算元素左上角和指针位置之间的插值。这个差值应该在mousedown事件发生的时候确定,并且一直保持,直到mouseup事件发生。通过将event的clientX和clientY属性与该元素的offsetLeft和offsetTop属性进行比较,就可以算出水平方向和垂直方向上需要多少空间。见图:为了保存X和Y坐标上的差值,还需要几个变量。diffX和diffY这些变量需要在onmousemove事件处理程序中用到,来对元素进行适当的定位,例子:var DragDrop = function(){ var dragging = null, diffX = 0, diffY = 0; function handleEvent(event){ //获取事件和目标 event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); //确定事件类型 switch(event.type){ case "mousedown": if (target.className.indexOf("draggable") > -1) { dragging = target; diffX = event.clientX - target.offsetLeft; diffY = event.clientY - target.offsetTop; } break; case "mousemove": console.log("var") if (dragging !== null) { //指定位置 dragging.style.left = (event.clientX - diffX) + "px"; dragging.style.top = (event.clientY - diffY) + "px"; } break; case "mouseup": dragging = null; break; } }; //公共接口 return { enable:function(){ console.log("zhixing") EventUtil.addHandler(document,"mousedown",handleEvent); EventUtil.addHandler(document,"mouseover",handleEvent); EventUtil.addHandler(document,"mouseup",handleEvent); }, disable:function(){ EventUtil.addHandler(document,"mousedown",handleEvent); EventUtil.addHandler(document,"mouseover",handleEvent); EventUtil.addHandler(document,"mouseup",handleEvent); } }}();diffX和diffY变量是私有的,因为只有handleEvent()函数需要用到它们。当mousedown事件发生时,通过clientX减去目标的offsetLeft,clientY减去目标的offsetTop,可以计算到这两个变量的值。当触发了mousemove事件后,就可以使用这些变量从指针坐标中减去,得到最终的坐标。最后得到一个更加平滑的拖动体验,更加符合用户所期望的方式。22.5.2添加自定义事件拖放功能还不能真正应用起来,除非能知道什么时候开始了。从这点上看,前面的代码没有提供任何方式表示拖动开始、正在拖动或者已经结束。这时,可以使用自定义事件来指示这几个事件的发生,让应用的其他部分与拖动功能进行交互。由于DragDrop对象是一个使用了模块模式的单例,所以需要进行一些更改来使用EventTarget类型。首先,创建一个新的EventTarget对象,然后添加enable()和disable()方法,最后返回这个对象。看以下内容:var DragDrop = function(){ var dragdrop = new EventTarget(), dragging = null, diffX = 0, diffY = 0; function handleEvent(event){ //获取事件和目标 event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); //确定事件类型 switch(event.type){ case "mousedown": if (target.className.indexOf("draggable") > -1) { dragging = target; diffX = event.clientX - target.offsetLeft; diffY = event.clientY - target.offsetTop; dragdrop.fire({type:"dragstart",target:dragging.x:event.clientX,y:event.clientY}); } break; case "mousemove": console.log("var") if (dragging !== null) { //指定位置 dragging.style.left = (event.clientX - diffX) + "px"; dragging.style.top = (event.clientY - diffY) + "px"; //触发自定义事件 dragdrop.fire({type:"drag",target:dragging,x:event.clientX,y:event.clientY}); } break; case "mouseup": dragdrop.fire({type:"dragend",target:dragging,x:event.clientX,y:event.clientY}); dragging = null; break; } }; //公共接口 dragdrop.enable:function(){ console.log("zhixing") EventUtil.addHandler(document,"mousedown",handleEvent); EventUtil.addHandler(document,"mouseover",handleEvent); EventUtil.addHandler(document,"mouseup",handleEvent); }; dragdrop.disable:function(){ EventUtil.addHandler(document,"mousedown",handleEvent); EventUtil.addHandler(document,"mouseover",handleEvent); EventUtil.addHandler(document,"mouseup",handleEvent); } return dragdrop;}();这段代码定义了三个事件:dragstart、drag和dragend。它们都将被拖动的元素设置为了target,并给出了 x 和 y 属性来表示当前的位置。它们触发于dragdrop对象上,之后在返回对象前给对象增加enable()和disable()方法。这些模块模式中的细小更改令DragDrop对象支持了事件,如下:DragDrop.addHandler("dragstart",function(event){ var status = document.getElementById("status"); status.innerHTML = "Started dragging" + event.target.id;});DragDrop.addHandler("drag",function(event){ var status = document.getElementById("status"); status.innerHTML += "<br/>Dragged " + event.target.id + "to(" + event.x + "," + event.y + ")";});DragDrop.addHandler("dragend",function(event){ var status = document.getElementById("status"); status.innerHTML += "<br/>Dragged " + event.target.id + "at(" + event.x + "," + event.y + ")"});这里,为DragDrop对象的每个事件添加事件处理程序。还使用了一个元素来实现被拖动的元素当前的状态和位置。一旦元素被放下了,就可以看到从它一开始被拖动之后经过的所有的中间步骤。为DragDrop添加自定义事件可以使这个对象更健壮,她将可以在网络应用中处理复杂的拖放功能。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"Javascript","slug":"Javascript","permalink":"https://millionqw.github.io/tags/Javascript/"}]},{"title":"《JavaScript高级程序设计》 第二十一章 Ajax与Comet","date":"2017-12-15T14:12:47.000Z","path":"2017/12/15/《JavaScript高级程序设计》-第二十一章-Ajax与Comet/","text":"目录21.1XMLHttpRequest对象21.1.1XHR的用法21.1.2HTTP头部信息21.1.3GET请求21.1.4POST请求21.2XMLHttpRequest2级21.2.1FormData21.2.2超时设定21.3进度事件21.3.1load事件21.3.2progress事件21.4跨域资源共享21.4.1IE对CORS的实现21.4.2其他浏览器对CORS的实现21.4.3Prefligted Requests21.4.4带凭证的请求21.4.5跨浏览器的CORS21.5其他跨域技术21.5.1图像Ping21.5.2JSONP21.5.3Comet21.5.4服务器发送事件1.SSE API2.事件流21.5.5Web Sockets1.Web Sockets API2.发送和接收数据3.其他事件21.5.6SSE与Web Sockets21.6安全 2005年,Jesse James Garrett 发表了一篇在线文章,题为“Ajax:A new Approach to Web Applications”。他在这篇文章里介绍了一种技术,用他的话说,就叫Ajax,是对Asynchronous Javascript + XML的简写。这一技术能够向服务器请求额外的数据而无需卸载页面,会带来更好的用户体验。Garret还解释了怎样使用这一技术改变自从Web诞生以来就一直沿用的“单击,等待”的交互模式。Ajax技术的核心是XMLHttpRequest对象(简称XHR),这是由微软首先引入的一个特性,其他浏览器提供商后来都提供了相同的实现。在XHR出现之前,Ajax式的通信必须借助一些hack手段来实现,大多数是使用隐藏的框架或内嵌框架。XHR为向服务器发送请求和解析服务器响应提供了流畅的接口。能够以异步方式从服务器获得更多信息,意味着用户单击后,可以不必刷新页面也能取得新数据。也就是说,可以使用XHR对象取得数据,然后通过DOM将数据插入到页面中。另外,虽然名字中包含XML的成分,但Ajax通信与数据格式无关;这种技术就是无须刷新页面即可从服务器取得数据,但不一定是XML数据。实际上,Garrett提到的这种技术已经存在很长事件了。在Garrett撰写那篇文章之前,人们通常将这种技术叫做远程脚本(remote scripting),而且早在1998年就有人采用不同的手段实现了这种浏览器与服务器的通信。再往前推,JavaScript需要通过Java applet或Flash电影等中间层向服务器发送请求。而XHR则将浏览器原生的通信能力提供给了开发人员,简化了实现同样操作的任务。在重命名为Ajax之后,大约是2005年底2006年初,这种浏览器与服务器的通信技术可谓红极一时。人们对JavaScript和Web的全新认识,催生了很多使用原有特性的新技术和新模式。就目前来说,熟练使用XHR对象已经成为所有Web开发人员必须掌握的一项技能。21.1XMLHttpRequest对象IE5是第一款引入XHR对象的浏览器。在IE5中,XHR对象是通过MSXML库中的一个ActiveX对象实现的。因此,在IE中可能会遇到三种不同版本的XHR对象,即MSXML2.XMLHttp、MSXML2.XMLHttp.3.0 和 MXSML2.XMLHttp.6.0。要使用MSXML库中的XHR对象,需要像第十八章讨论创建XML文档时一样,编写一个函数,例如://适用于IE7之前的版本function createXHR(){ if (typeof arguments.callee.activeXString != "string") { var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"], i,len; for(i=0, len=versions.length; i<len; i++){ try{ new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (ex){ //跳过 } } } return new ActiveXObject(arguments.callee.activeXString);}这个函数会尽力根据IE中可用的MSXML库的情况创建最新版本的XHR对象。IE7+、Firefox、Opera、Chrome 和 Safari 都支持原生的XHR对象,在这些浏览器中创建XHR对象要像下面这样使用XMLHttpRequest构造函数:var xhr = new XMLHttpRequest();假如你只想支持IE7及更高版本,那么大可丢掉前面定义的那个函数,而只用原生的XHR实现。但是如果你必须支持IE的早期版本,那么则可以在这个createXHR()函数中加入对原生XHR对象的支持:function createXHR(){ if (typeof XMLHttpRequest != "undefined") { return new XMLHttpRequest(); } else if(typeof ActiveXObject != "undefined") { if (typeof arguments.callee.activeXString != "string") { var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"], len,i; for(i=0,len=versions.length; i<len; i++){ try { new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (ex) { //跳过 } } } return new ActiveXObject(arguments.callee.activeXString); } else { throw new Error("No XHR object available"); }}这个函数中新增的代码首先检测原生XHR对象是否存在,如果存在则返回它的新实例。如果原生对象不存在,则检测ActiveX对象。如果这两种对象都不存在,就抛出一个错误。然后,就可以使用下面的代码在所有浏览器中创建XHR对象了:var xhr = createXHR();由于其他浏览器中对XHR的实现与IE最早的实现是兼容的,因此就可以在所有浏览器中都以相同方式使用上面创建的xhr对象。21.1.1XHR的用法在使用XHR对象时,要调用的第一个方法是open(),它接受3个参数:要发送的请求的类型("get"、"post"等)、请求的URL和表示是否异步发送请求的布尔值。下面就是调用这个方法的例子。xhr.open("get","example.php",false);这行代码会启动一个针对example.php的GET请求。有关这行代码,需要说明两点:一是URL相对于执行代码的当前页面(也可以是绝对路径);二是调用open()方法并不会真正地发送请求,而只是启动一个请求以备发送。✎:只能向同一个域中使用相同端口和协议的URL发送请求。如果URL与启动请求的页面有任何差别,都会引发安全错误。要发送特定的请求,必须像下面这样调用send()方法:xhr.open("open","example.txt",false);xhr.send(null);这里的send()方法接收一个参数,既要作为请求主题发送的数据。如果不需要通过请求主体发送数据,则必须传入null,因为这个参数对有些浏览器来说是必须的。调用send()之后,请求就会被分派到服务器。由于这次请求是同步的,JavaScript代码会等到服务器响应之后再继续执行。在收到响应后,响应的数据会自动填充XHR对象的属性,相关的属性简介如下:·responseText:作为响应主体被返回的文本。·responseXML:如果响应的内容类型是“text/xml”或“application/xml”,这个属性中将保存包含着响应数据的XML DOM文档。·status:响应的HTTP状态。·statusText:HTTP状态的说明在接收到响应后,第一步是检查status属性,以确定响应已经成功返回。一般来说,可以将HTTP状态代码为200作为成功的标志。此时,responseText属性的内容已经就绪,而且在内容类型正确的情况下,responseXML也应该能够访问了。此外,状态代码为304表示请求的资源并没有被修改,可以直接使用浏览器中缓存的版本;当然,也意味着响应是有效的。为确保收到适当的响应,应该像下面这样检查上述这两种状态代码:xhr.open("get","example.php",false);xhr.send(null);if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { console.log(xhr.responseText);} else { console.log("Request was unsuccessful:" + xhr.status);}根据返回的状态代码,这个例子可能会显示由服务器返回的内容,也可能会显示一条错误消息。我们建议读者要通过检测status来决定下一步的操作,不要依赖statusText,因为后者在跨浏览器使用时不太可靠。另外,无论内容类型是什么,响应主题的内容都会保存到responseText属性中;而对于非XML数据而言,responseXML属性的值将为null。✎:有的浏览器会错误地报告204状态代码。IE中XHR的ActiveX版本会将204设置为1223,而IE中原生的XHR则会将204规范为200。Opera会在取得204时报告status的值为0。像前面这样发送同步请求当然没有问题,但多数情况下,我们还是要发送异步请求,才能让JavaScript继续执行而不必等待响应。此时,可以检测XHR对象的readyState属性,该属性表示请求/响应过程的当前活动阶段。这个属性可取的值如下:·0:未初始化。尚未调用open()方法。·1:启动。已经调用open()方法,但尚未调用send()方法。·2:发送。已经调用send()方法,但尚未接收到响应。·3:接收。已经接收到部分响应数据。·4:完成。已经接收到全部响应数据,而且已经可以在客户端使用了。只要readyState属性的值由一个值变成另外一个值,就会触发一次readyStatechange事件。可以利用这个事件来检测每次状态变化后readyState的值。通常,我们只对readyState值为 4 的阶段感兴趣,因为这时所有数据都已经就绪。不过,必须在调用open()之前指定onreadystatechange事件处理程序才能确保跨浏览器兼容性。例子:var xhr = createXHR();xhr.onreadystatechange = function(){ if (xhr.readyState == 4) { if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { console.log(xhr.responseText); } else { console.log("Request was unsuccessful:" + xhr.status); } }};xhr.open("get","example.txt",true);xhr.send(null);以上代码利用DOM 0级方法为XHR对象添加了事件处理程序,原因是并非所有浏览器都支持DOM 2级方法。与其他事件处理程序不同,这里没有向onreadystatechange事件处理程序中传递event对象;必须通过XHR对象本身来确定下一步该怎么做。✎:这个例子在onreadystatechange事件处理程序中使用了xhr对象,没有使用this对象,原因是onreadystatechange事件处理程序的作用域问题。如果使用this对象,在有的浏览器中会导致函数执行失败,或者导致错误发生。因此,使用实际的XHR对象实例变量是较为可靠的一种方式。另外,在接收到响应之前还可以调用abort()方法来取消异步请求,例子:xhr.abort();调用这个方法后,XHR对象会停止触发事件,而且也不再允许访问任何与响应有关的对象属性。在终止请求之后,还应该对XHR对象进行解引用操作(xhr = null 这样吗?)。由于内存原因,不建议重用XHR对象。21.1.2HTTP头部信息每个HTTP请求和响应都会带有相应的头部信息,其中有的对开发人员有用,有的也没有什么用。XHR对象也提供了操作这两种头部(即请求头部和响应头部)信息的方法。默认情况下,在发送XHR请求的同时,还会发送下列头部信息:·Accept:浏览器能够处理的内容类型。·Accept-Charset:浏览器能够显示的字符集。·Accept-Encoding:浏览器能够处理的压缩编码。·Accept-Language:浏览器当前设置的语言。·Connection:浏览器与服务器之间连接的类型。·Cookie:当前页面设置的任何Cookie。·Host:发出请求的页面所在的域。·Referer:发出请求的页面的URI。注意,HTTP规范将这个头部字段拼写错了,而为保证与规范一致,也只能将错就错了。(。。。)(英文单词的正确拼法是referrer)。·User-Agent:浏览器的用户代理字符串。虽然不同浏览器实际发送的头部信息有所不同,但以上列出的基本上是所有浏览器都会发送的。使用setRequestHeader()方法可以设置自定义的请求头部信息。这个方法接受两个参数:头部字段的名称和头部字段的值。要成功发送请求头部信息,必须在调用open()方法之后且调用send()方法之前调用setRequestHeader()。例子:var xhr = createXHR();xhr.onreadystatechange = function(){ if (xhr.readyState == 4) { if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { console.log(xhr.responseText); } else { console.log("Request was unsuccessful:" + xhr.status); } }};xhr.open("get","example.txt",true);xhr.setRequestHeader("MyHeader","Myvalue");xhr.send(null);服务器在接收到这种自定义的头部信息之后,可以执行响应的后续操作。我们建议读者使用自定义的头部字段名称,不要使用浏览器正常发送的字段名陈,否则有可能会影响服务器的响应。有的浏览器允许开发人员重写默认的头部信息,但有的浏览器则不允许这样做。调用XHR对象的getResponseHeader()方法并传入头部字段名称,可以取得相应的响应头部信息。而调用getAllResponseHeaders()方法则可以取得一个包含所有头部信息的长字符串。来看下面的例子:var myHeader = xhr.getResponseHeader("MyHeader");var allHeaders = xhr.getAllResponseHeaders();在服务器端,也可以利用头部信息向浏览器发送额外的、结构化的数据。在没有自定义信息的情况下,getAllResponseHeader()方法通常会返回如下所示的文本内容:这种格式化的输出可以方便我们检查响应中所有头部字段的名称,而不必一个个地检查摸个字段是否存在。21.1.3GET请求GET是最常见的请求类型,最常用于向服务器查询某些信息。必要时,可以将字符串参数追加到URL的末尾,以便将信息发送给服务器。对XHR而言,位于传入open()方法的URL末尾的查询字符串必须经过正确的编码才行。使用GET请求经常会发生的一个错误,就是查询字符串的格式有问题。查询字符串中每个参数的名称和值都必须使用encodeURIComponent()进行编码,然后才能放到URL的末尾;而且所有名-值对儿都必须由和号(&)分隔,如下面的例子所示:xhr.open("get","example.php?name1=value&name2=value2",true);下面这个函数可以辅助向现有URL的末尾添加查询字符串参数:function addURLParam(url,name,value){ url += (url.indexOf("?") == -1 ? "?" : "&"); url += encodeURIComponent(name) + "=" + encodeURIComponent(Value); return url;}这个addURLParam()函数接受三个参数:要添加参数的URL、参数的名称和参数的值。这个函数首先检查URL是否包含问号(以确定是否已经有参数存在)。如果没有,就添加一个问号;否则,就添加一个和号。然后,将参数名称和值进行编码,再添加到URL的末尾。最后返回添加参数之后的URL。下面是使用这个函数来构建请求的URL的示例:var url = "example.php";//添加参数url = addURLParam(url,"name","Nicholas");url = addURLParam(url,"book","Professional javascript");//初始化请求xhr.open("get",url,false);在这里使用addURLParam()函数可以确保查询字符串的格式良好,并可靠地用于XHR对象。21.1.4POST请求使用频率仅次于GET的是POST请求,通常用于向服务器发送应该被保存的数据。POST请求应该把数据作为请求的主题提交,而GET请求传统上不是这样。POST请求的主题可以包含非常多的数据,而且格式不限。在open()方法第一个参数的位置传入“post”,就可以初始化一个POST请求,如下面的例子所示:xhr.open("post","example.php",true);发送POST请求的第二步就是向send()方法中传入某些数据。由于XHR最初的设计主要是为了处理XML,因此可以在此传入XML DOM文档,传入的文档经序列化之后将作为请求主体被提交到服务器。当然,也可以在此传入任何想发送到服务器的字符串。默认情况下,服务器对POST请求和提交Web表单的请求并不会一视同仁。因此,服务器端必须有程序来读取发送过来的原始数据,并从中解析出有用的部分。不过,我们可以使用XHR来模仿表单提交:首先将Content-Type头部信息设置为application/x-www-form-urlencoded,也就是表单提交的内容类型,其次是以适当的格式创建一个字符串。第十四章曾经讨论过,POST数据的格式与查询字符串格式相同。如果需要将页面中表单的数据进行序列化,然后再通过XHR发送到服务器那么就可以使用第十四章介绍的serialize()函数来创建这个字符串:function submitData(){ var xhr = createXHR(); xhr.onreadystatechange = function(){ if (xhr.readyState == 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status ==304) { console.log(xhr.responseText); } else { console.log("Request was unsuccessful:" + xhr.status); } } }; xhr.open("post","postexample.php",true); xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded"); var form = document.getElementById("user-info"); xhr.send(serialize(form));}(serialize()函数不是JavaScript有的,是在十四章作者自己写的一个函数,最后返回的是以查询字符串的格式输出序列化之后的字符串)这个函数可以将ID为“user-info”的表单中的数据序列化之后发送给服务器。而下面的示例PHP文件postexample.php就可以通过$_POST取得提交的数据了:<?php header("Content-Type:text/plain"); echo <<<EOFName:{$_POST[‘user-name’]}Email:{$_POST[‘user-email’]}EOF;?>(看不懂啊上面的代码)如果不设置Content-Type头部信息,那么发送给服务器的数据就不会出现在$_POST超级全局变量中。这时候,要访问同样的数据,就必须借助$HTTP_RAW_POST_DATA。✎:与GET请求相比,POST请求消耗的资源会更多一些。从性能角度来看,以发送相同的数据统计,GET请求的速度最多可达到POST请求的两倍。21.2XMLHttpRequest 2级鉴于XHR已经得到广泛接受,成为了事实标准,W3C也着手指定相应的标准以规范其行为。XMLHttpRequest 1级只是把已有的XHR对象的实现细节描述了出来。而XMLHttpRequest 2级则进一步发展了XHR。并非所有浏览器都完整地实现了XMLHttpRequest 2级规范,但所有浏览器都实现了它规定的部分内容。21.2.1FormData现代Web应用中频繁使用的一项功能就是表单数据的序列化,XMLHttpRequest2级为此定义了FormData类型。FormData为序列化表单以及创建与表单格式相同的数据(用于通过XHR传输)提供了便利。下面的代码创建了一个FormData对象。并向其中添加了一些数据:var data = new FormData();data.append("name","Nicholas");这个append()方法接收两个参数:键和值,分别对应表单字段的名字和字段中包含的值。可以像这样添加任意多个键值对儿。通过向FormData构造函数中传入表单元素,也可以用表单元素的数据预先向其中填入键值对儿:var data = bew FormData(document.forms[0]);创建了FormData的实例后,可以将它直接传给XHR的send()方法。例子:var xhr = createXHR();xhr.onreadystatechange = function(){ if (xhr.readyState == 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status ==304) { console.log(xhr.responseText); } else { console.log("Request was unsuccessful:" + xhr.status); } }};xhr.open("post","postexample.php",true);var form = document.getElementById("user-info");xhr.send(new FormData(form));使用FormData的方便之处体现在不必明确地在XHR对象上设置请求头部。XHR对象能够识别传入的数据类型是FormData的实例,并配置适当的头部信息。支持FormData的浏览器有Firefox 4+、Safari 5+、Chrome和Android 3+版WebKit。21.2.2超时设定IE8为XHR对象添加了一个timeout属性,表示请求在等待响应多少毫秒之后就终止。在给timeout设置一个数值后,如果在规定的事件内浏览器还没有接收到响应,那么就会触发timeout事件,进而会调用ontimeout事件处理程序。这项功能后来也被收入了XMLHttpRequest 2级规范中。例子:var xhr = createXHR();xhr.onreadystatechange = function(){ if (xhr.readyState == 4) { try{ if ((xhr.status >= 200 && xhr.status < 300) || xhr.status ==304) { console.log(xhr.responseText); } else { console.log("Request was unsuccessful:" + xhr.status); } } } catch (ex) { //假设由ontimeout事件处理程序处理 }};xhr.open("get","timeout.php",true);xhr.timeout = 1000; //将超时设置为1秒钟(仅适用于 IE8+)xhr.ontimeout = function(){ console.log("Request did not return in a second");};xhr.send(null);这个例子示范了如何使用timeout属性。将这个属性设置为1000毫秒,意味着如果请求在1秒钟内还没有返回,就会自动终止。请求终止时,会调用ontimeout事件处理程序。但此时readyState可能已经改变为 4 了,这意味着会调用onreadystatechange事件处理程序。可是,如果在超时终止请求之后再访问status属性,就会导致错误。为避免浏览器报告错误,可以将检查status属性的语句封装在一个try-catch语句当中。在写着本树时,只有IE 8+支持超时设定。21.2.3overrideMimeType()方法Firefox最早引入了overrideMimeType()方法,用于重写XHR响应的MIME类型。这个方法后来也被纳入了XMLHttpRequest 2级规范。因为返回响应的MIME类型决定了XHR对象如何处理它,所以提供了一种方法能够重写服务器返回的MIME类型是很有用的。比如,服务器返回的MIME类型是text/plain,但数据中实际包含的是XML。根据MIME类型,即使数据是XML,responseXML属性中仍然是null。通过调用overrideMimeType()方法,可以保证把响应当作XML而非纯文本来处理。var xhr = createXHR();xhr.open("get","text.php",true);xhr.overrideMimeType("text/xml");xhr.send(null);这个例子强迫XHR对象将响应当作XML而非纯文本来处理。调用overrideMimeType()必须在send()方法之前,才能保证重写响应的MIME类型。支持overrideMimeType()方法的浏览器有Firefox、Safari 4+、Opera 10.5和Chrome。21.3进度事件Progress Events 规范是W3C的一个工作草案,定义了与客户端服务器通信有关的事件。这些事件最早其实只针对XHR操作,但目前也被其他API借鉴。有以下6个进度事件:·loadstart:在接收到相应数据的第一个字节时触发。·progress:在接收响应期间持续不断地触发。·error:在请求发生错误时触发。·abort:在因为调用abort()方法而终止连接时触发。·load:在接收到完整的响应数据时触发。·loadend:在通信完成或者触发error、abort或load事件后触发。每个请求都从触发loadstart事件开始,接下来是一或多个progress事件,然后触发error、abort或load事件中的一个,最后以触发loadend事件结束。支持前5个事件的浏览器有Firefox 3.5+、Safari 4+、Chrome、iOS版Safari和Android版WebKit。Opera(从第11版开始)、IE 8+只支持load事件。目前还没有浏览器支持loadend事件。这些事件大都很直观,但其中两个事件有一些细节需要注意。21.3.1load事件Firefox在实现XHR对象的某个版本时,曾致力于简化异步交互模型。最终,Firefox实现中引入了load事件,用以替代readystetechange事件。响应接收完毕后将触发load事件,因此也就没有必要去检查readyState属性了。而onload事件处理程序会接收到一个event对象,其target属性就指向XHR对象实例。因而可以访问到XHR对象的所有方法和属性。然而,并非所有浏览器都为这个事件实现了适当的事件对象。结果,开发人员还是要像下面这样被迫使用XHR对象变量:var xhr = createXHR();xhr.onload = function(){ if ((xhr.status >= 200 && xhr.status < 300) || xhr.status ==304) { //少了前面那些示例中if (xhr.readyState == 4) {} 这一步 console.log(xhr.responseText); } else { console.log("Request was unsuccessful:" + xhr.status); }};xhr.open("get","altevents.php",true);xhr.send(null);上面代码中少了if (xhr.readyState == 4) {} 这一步。只要浏览器接收到服务器的响应,不管其状态如何,都会触发load事件。而这意味着你必须要检查status属性,才能确定数据是否真的已经可用了。Firefox、Opera、Chrome和Safari都支持load事件。21.3.2progress事件Mozilla对XHR的另一个革新是添加了progress事件,这个事件会在浏览器接收新数据期间周期性地触发。而onprogress事件处理程序会接收到一个event对象,其target属性是XHR对象。但包含着三个额外的属性:lengthComputable、position和totalSize。其中,lengthComputable是一个表示进度信息是否可用的布尔值,position表示已经接收的字节数,totalSize表示根据Content-Length响应头部确定的预期字节数。有了这些信息,我们就可以为用户创建一个进度指示器了。下面展示了为用户创建进度指示器的一个示例:var xhr = createXHR();xhr.onload = function(event){ if ((xhr.status >= 200 && xhr.status < 300) || xhr.status ==304) { console.log(xhr.responseText); } else { console.log("Request was unsuccessful:" + xhr.status); }};xhr.onprogress = function(event){ var divStatus = document.getElementById("status"); if (event.lengthComputable) { divStatus.innerHTML = "Received" + event.position + "of" + event.totalSize + "bytes"; }};xhr.open("get","altevents.php",true);xhr.send(null);为确保正常执行,必须在调用open()方法之前添加onprogress事件处理程序。在前面的例子中,每次触发progress事件,都会以新的状态信息更新HTML元素的内容。如果响应头部中包含Content-length字段,那么也可以利用此信息来计算从响应中已经接收到的数据的百分比。21.4跨源资源共享通过XHR实现Ajax通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR对象只能访问与包含它的页面位于同一个域中的资源。这种安全策略可以预防某些恶意行为。但是,实现合理的跨域请求对开发某些浏览器应用程序也至关重要的。CORS(Cross-Origin Resource Sharing,跨源资源共享)是W3C的一个工作草案,定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。CORS背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。比如一个简单的使用GET或POST发送的请求,它没有自定义的头部,而主体内容是text/plain。在发送该请求时,需要给它附加一个额外的Origin头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。下面是Origin头部的一个示例:Origin:http://www.nczonline.net;如果服务器认为这个请求可以接受,就在Access-Control-Allow-Origin头部中回发相同的源信息(如果是公共资源,可以回发“*”)。例如:Access-Control-Allow-Origin:http://www.nczonline.net如果没有这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。注意,请求和响应都不包含cookie信息。21.4.1IE对CORS的实现微软在IE8中引入了XDR(XDomainRequest)类型。这个对象与XHR类似,但能实现安全可靠的跨域通信。XDR对象的安全机制部分实现了W3C的CORS规范。以下是XDR与XHR的一些不同之处:·cookie不会随请求发送,也不会随响应返回。·只能设置请求头部信息中的Content-Type字段。·不能访问响应头部信息。·只支持GET和POST请求。这些变化使CSRF(Cross-Site Request Forgery,跨站点请求伪造)和XSS(Cross-Site Scripting,跨站点脚本)的问题得到了缓解。被请求的资源可以根据它认为合适的任意数据(用户代理、来源页面等)来决定是否设置Access-Control-Allow-Origin头部。作为请求的一部分,Origin头部的值表示请求的来源域,以便远程资源明确地识别XDR请求。XDR对象的使用方法与XHR对象非常相似。也是创建一个XDomainRequest的实例,调用open()方法,再调用send()方法。但与XHR对象的open()方法不同,XDR对象的open()方法只接收两个参数:请求的类型和URL。所有XDR请求都是异步执行的,不能用它来创建同步请求。请求返回之后,会触发load事件,响应的数据也会保存在responseText属性中,如下所示:var xdr = new XDomainRequest();xdr.onload = function(){ console.log(xdr.responseText);};xdr.open("get","http://www.somewhere-else.com/page/&quot;);xdr.send(null);在接收到响应后,你只能访问响应的原始文本;没有办法确定响应的状态代码。而且,只要响应有效就会触发load事件,如果失败(包括响应中缺少Access-Control-Allow-Origin头部)就会触发error事件。遗憾的是,除了错误本身之外,没有其他信息可用,因为唯一能够确定的就只有请求未成功了。要检测错误,可以像下面这样指定一个onerror事件处理程序:var xdr = new XDomainRequest();xdr.onload = function(){ console.log(xdr.responseText);};xdr.onerror = function(){ console.log("An error occurred");};xdr.open("get","http://www.somewhere-else.com/page/&quot;);xdr.send(null);✎:鉴于导致XDR请求失败的因素很多,因此建议你不要忘记通过onerror事件处理程序来捕获该事件,否则,即使请求失败也不会有任何提示。在请求返回前调用abort()方法可以终止请求:xdr.abort(); //终止请求与XHR一样,XDR对象也支持timeout属性以及ontimeout事件处理程序。下面是一个例子:var xdr = new XDomainRequest();xdr.onload = function(){ console.log(xhr.responseText);};xdr.onerror = function(){ console.log("An error occurred");};xdr.timeout = 1000;xdr.ontimeout = function(){ console.log("Request took too long");};xdr.open("get","http://www.somewhere-else.com/page/&quot;);xdr.send(null);这个例子会在运行1秒钟后超时,并随机调用ontimeout事件处理程序。为支持POST请求,XDR对象提供了contentType属性,用来表示发送数据的格式,如下面的例子所示:var xdr = new XDomainRequest();xdr.onload = function(){ console.log(xhr.responseText);};xdr.onerror = function(){ console.log("An error occurred");};xdr.open("post","http://www.somewhere-else.com/page/&quot;);xdr.contentType = "application/x-www-form-urlencoded";xdr.send("name1=value1&name2=value2");这个属性是通过XDR对象影响头部信息的唯一方式。21.4.2其他浏览器对CORS的实现Firefox 3.5+、Safari 4+、Chrome、iOS版Safari 和 Android平台中的WebKit都通过XMLHttpRequest对象实现了对CORS的原生支持。在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。要请求位于另一个域中的资源,使用标准的XHR对象并在open()方法中传入绝对URL即可,例如:var xhr = createXHR();xhr.onreadystatechange = function(){ if ((xhr.status >= 200 && xhr.status < 300) || xhr.status ==304) { console.log(xhr.responseText); } else { console.log("Request was unsuccessful:" + xhr.status); }};xhr.open("get","http://www.somewhere-else.com/page/",true);xhr.send(null);与IE中的XDR对象不同,通过跨域XHR对象可以访问status和statusText属性,而且还支持同步请求。跨域XHR对象也有一些限制,但为了安全这些限制是必须的。以下就是这些限制:·不能使用setRequestHeader()设置自定义头部。·不能发送和接收code。·调用getAllResponseHeaders()方法总会返回空字符串。由于无论同源请求还是跨源请求都使用相同的接口,因此对于本地资源,最好使用相对URL,在访问远程资源时再使用绝对URL。这样做能消除歧义,避免出现限制访问头部或本地cookie信息等问题。21.4.3Preflighted RequestsCORS通过一种叫做Preflighted Requests的透明服务器验证机制支持开发人员使用自定义的头部、GET或POST之外的方法,以及不同类型的主体内容。在使用下列高级选项来发送请求时,就会向服务器发送一个Preflight请求。这种请求使用OPTIONS方法,发送下列头部:·Origin:与简单的请求相同。·Access-Control-Request-Method:请求自身使用的方法。·Access-Control-Request-Headers:(可选)自定义的头部信息,多个头部以逗号分隔。以下是一个带有自定义头部NCZ的使用POST方法发送的请求。Origin:http://www.nczonline.netAccess-Control-Request-Method:POSTAccess-Control-Request-Headers:NCZ发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通。·Access-Control-Allow-Origin:与简单的请求相同。·Access-Control-Allow-Methods:允许的方法,多个方法以逗号分隔。·Access-Control-Allow-Headers:允许的头部,多个头部以逗号分隔。·Access-Control-Max-Age:应该将这个Preflight请求缓存多长时间(以秒表示)。例如:Prelight请求结束后,结果将按照响应中指定的时间缓存起来。而为此付出的代价只是第一次发送这种请求时会多一次HTTP请求。支持Preflight请求的浏览器包括Firefox 3.5+、Safari 4+和Chrome。IE 10及更早版本不支持。21.4.4带凭据的请求默认情况下,跨源请求不提供凭据(cookie、HTTP认证及客户端SSL证明等)。通过将withCredentials属性设置为true,可以指定某个请求应该发送凭据。如果服务器接收带凭据的请求,会用下面的HTTP头部来响应:Access-Control-Allow-Credentials:true如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给JavaScript(于是,responseText中将是空字符串,status的值为0,而且会调用onerror()事件处理程序)。另外,服务器还可以在Prefilight响应中发送这个HTTP头部,表示允许源发送带凭据的请求。支持withCredentials属性的浏览器有Firefox 3.5+、Safari 4+和Chrome。IE 10及更早版本都不支持。21.4.5跨浏览器的CORS即使浏览器对CORS的支持程度并不一样,但所有浏览器都支持简单的(非Preflight和不带凭据的)请求,因此有必要实现一个跨浏览器的方案。检测XHR是否支持CORS的最简单方式,就是检查是否存在withCredentials属性。再结合检测XDomainRequest对象是否存在,就可以兼顾所有浏览器了。function createCORSRequest(method,url){ var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { xhr.open(method,url,true); } else if (typeof XDomainRequest != "undefined"){ vxhr = new XDomainRequest(); xhr.open(method,url); } else { xhr = null; } return xhr;}var request = createCORSRequest("get","http://www.somewhere-else.com/page/&quot;);if (request) { request.onload = function(){ //对request.responseText进行处理 }; request.send();}Firefox、Safari 和 Chrome 中的XMLHttpRequest对象与IE中的XDomainRequest 对象类似,都提供了够用的接口,因此以上模式还是相当有用的。这两个对象共同的属性/方法如下:21.5其他跨域技术在CORS出现以前,要实现Ajax通信颇费一些周折。开发人员想出了一些办法,利用DOM中能够执行跨域请求功能,在不依赖XHR对象的情况下,也能发送某种请求,虽然CORS技术已经无处不再,但开发人员自己发明的这些技术仍然被广泛使用,毕竟这样不需要修改服务器端代码。21.5.1图像Ping上述第一种跨域请求是使用<img>标签。我们知道,一个网页可以从任何网页中加载图像,不用担心跨域不跨域。这也是在线广告跟踪浏览量的主要方式。正如第十三章讨论过的,也可以动态地创建图像,使用它们的onload和onerror事件处理程序来确定是否接收到了响应。动态创建图像经常用于图像Ping。图像Ping是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或204响应。通过图像Ping,浏览器得不到任何具体的数据,但通过侦听load和error事件,它能知道响应是什么时候接收到的。例子:var img = new Image();img.onload = img.onerror = function(){ console.log("Done!");};img.src = "http://www.example.com/test?name=Nicholas&quot;;这里创建了一个Image的实例,然后将onload和onerror事件处理程序指定为同一个函数。这样无论是什么响应,只要请求完成,就能得到通知。请求从设置src属性那一刻开始,而这个例子在请求中发送了一个name参数。图像Ping最常用于跟踪用户点击页面或动态广告曝光次数。图像Ping有两个主要的缺点,一是只能发送Get请求,二是无法访问服务器的响应文本。因此,图像Ping只能用于浏览器与服务器间的单向通信。21.5.2JSONPJSONP是JSON with padding(填充式JSON或参数式JSON)的简写,是应用JSON的一种新方法,在后来的Web服务中非常流行。JSONP看起来与JSON差不多,只不过是被包含在函数调用中的JSON,就像下面这样:callback({"name":"Nicholas"});JSONP由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数。回调函数的名字一般是在请求中指定的。而数据就是传入回调函数中的JSON数据。下面是一个典型的JSONP请求:http://freegeoip.net/json/?callback=handleResponse这个URL是在请求一个JSONP地理定位服务。通过查询字符串来指定JSONP服务的回调函数是很常见的,就像上面的URL所示,这里指定的回调函数的名字叫handleRespon()。JSONP是通过动态<script>元素(第十三章内容)来使用的,使用时可以为src属性指定一个跨域URL。这里的<script>元素与<img>元素类似,都有能力不受限制地从其他域加载资源。因为JSONP是有效的JavaScript代码,所以在请求完成后,即在JSONP响应加载到页面中以后,就会立即执行。例子:function handleResponse(response){ console.log("You’re at IP address" + response.ip + ",which is in" + response.city + "," + response.region_name);}var script = document.createElement("script");script.src = "http://freegeoip.net/json/?callback=handleResponse&quot;;document.body.innerBefore(script,document.body.firstChild)这个例子通过查询地理定位服务来显示你的IP地址和位置信息。JSONP之所以在开发人员中极为流行,主要原型是它非常简单易用。与图像Ping相比,它的有点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。不过JSONP也有两点不足。首先,JSONP是从其他域中加载代码执行,如果其他域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃JSONP调用之外,没有办法追究。因此在使用不是你自己运维的Web服务时,一定得保证它安全可靠。其次,要确定JSONP请求是否失败并不容易,虽然HTML 5给<script>元素新增了一个onerror事件处理程序,但目前还没有得到任何浏览器支持。为此,开发人员不得不使用计时器检测指定时间内是否接收到了响应。但就算这样也不能尽如人意,毕竟不是每个用户上网的速度和带宽都一样。21.5.3CometComet是Alex Russell发明的一个词儿,指的是更高级的Ajax技术(也有人称为“服务器推送”)。Ajax是一种从页面向服务器请求数据的技术,而Comet则是一种服务器向页面推送数据的技术。Comet能够让信息近乎实时地推送到页面上,非常适合处理体育比分的分数和股票报价。有两种实现Comet的方式:长轮询和流。长轮询是传统轮询(也称为短轮询)的一个翻版,即浏览器定时向服务器发送请求,看有没有更新的数据。图展示的是短轮询的时间线:长轮询把短轮询颠倒了以下。页面发起一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。这一过程在页面打开期间一直持续不断。图展示了长轮询的时间线:页面把请求发到服务器之后,服务器连接一直打开,直到有数据了马上发送给浏览器,浏览器马上关闭连接后又马上把请求发给服务器,保持有信息马上发回给浏览器的状态。无论是短轮询还是长轮询,浏览器都需要在接收数据之前,先发起对服务器的连接。两者最大的区别在于服务器如何发送数据。短轮询是服务器立即发送响应,无论数据是否有效,而长轮询是等待发送响应。轮询的优势是所有浏览器都支持,因为使用XHR对象和setTimeout()就能实现。而你要做的就是决定什么时候发送请求。第二种流行的Comet实现是HTTP流。流不同于上述两种轮询,因为它在页面的整个生命周期内只使用一个HTTP连接。具体来说,就是浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性地向浏览器发送数据。比如,下面这段PHP脚本就是采用流实现的服务器中常见的形式。<?php $i = 0; while(true){ //输出一些数据,然后立即刷新输出缓存 echo"Number is $i"; flush(); //等几秒钟 sleep(10); $i++; }所有服务器端语言都支持打印到输出缓存然后刷新(将输出缓存中的内容一次性全部发送到客户端)的功能。这是实现HTTP流的关键所在。在Firefox、Safari、Opera和Chrome中,通过侦听readystatechange事件及检测readyState的值是否为3,就可以利用XHR对象实现HTTP流。在上述这些浏览器中,随着不断从服务器接收数据,readyState的值会周期性地变为3。当readyState值变为3时,responseText属性中就会保存接收到的所有数据。此时,就需要比较此前接收到的数据,决定从什么位置开始取得最新的数据。使用XHR对象实现HTTP流的典型代码如下所示:function createStreamingClient(url,progress,finished){ var xhr = new XMLHttpRequest(), received= 0; xhr.open("get",url,true), xhr.onreadystatechange = function(){ var result; if (xhr.readyState == 3) { //只取得最新数据并调整计数器 result = xhr.responseText.substring(received); received += result.length; //调用progress回调函数 progress(result); } else if (xhr.readyState = 4){ finished(xhr.responseText); } }; xhr.send(null); return xhr;}var client = createStreamingClient("streaming.php",function(data){ console.log("Received:" + data); },function(data){ console.log("Done!"); });这个createStreamingClient()函数接收三个参数:要接收的URL、在接收到数据时调用的函数以及关闭连接时调用的函数。有时候,当连接关闭时,很可能还需要重新建立,所以关注连接什么时候关闭还是有必要的。只要readystatechange事件发生,而且readyState值为3,就对responseText进行分割以取得最新数据。这里的received变量用于记录已经处理了多少个字符,每次readyState值为3时都递增。然后,通过progress回调函数来处理传入的新数据。而当readyState值为4时,则执行finished回调函数,传入响应返回的全部内容。虽然这个例子比较简单,而且能在大多数浏览器中正常运行(IE除外,呸)但管理Comet的连接是很容易出错的,需要时间不断改进才能达到完美。浏览器社区认为Comet是未来Web的一个重要组成部分,为了简化这一技术,又为Comet创建了两个新的接口。21.5.4服务器发送事件SSE(Server-Sent Events,服务器发送事件)是围绕只读Comet交互推出的API或者模式。SSE API用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的MIME类型必须是text/event-stream,而且是浏览器中的JavaScript API能解析格式输出。SSE支持短轮询、长轮询和HTTP流,而且能在断开连接时自动确定何时重新连接。有了这么简单实用的API,再实现Comet就容易多了。支持SSE的浏览器有Firefox 6+、Safari 5+、Opera 11+、Chrome 和iOS 4+版Safari。1.SSE APISSE的JavaScript API与其他传递消息的JavaScript API很相似。要预订新的事件流,首先要创建一个新的Event Source对象,并传进一个入口点:var source = new EventSource("myevent.php");注意,传入的URL必须与创建对象的页面同源(相同的URL模式、域及端口)。EventSource的实例有一个readyState属性,值为0表示正连接到服务器,值为1表示打开了连接,值为2表示关闭了连接。另外还有三个事件:·open:在建立连接时触发。·message:在从服务器接收到新事件时触发。·error:在无法建立连接时触发。就一般的用法而言,onmessage事件处理程序没有什么特别的。source.onmessage = function(event){ var data = event.data;}服务器发回的数据以字符串形式保存在event.data中。默认情况下,EventSource对象会保持与服务器的活动连接。如果连接断开,还会重新连接。这就意味着SSE适合长轮询和HTTP流。如果想强制立即断开连接并且不再重新连接,可以调用close()方法。source.close();2.事件流所谓的服务器事件会通过一个持久的HTTP响应发送,这个响应的MIME类型为text/eventstream。响应的格式是纯文本,最简单的情况是每个数据项都带有前缀data:,例如:data:foodata:bardata:foodata:bar对以上响应而言,事件流中的第一个message事件返回的event.data值为“foo”,第二个message事件返回的event.data值为“bar”,第三个message事件返回的event.data值为“foo\\nbar”(注意中间的换行符)。对于多个连续的以data:开头的数据行,将作为多段数据解析,每个值之间以一个换行符分隔。只有在包含data:的数据行后面有空行时,才会触发message事件,因此在服务器上生成事件流时不能忘了多添加这一行。通过id:前缀可以给特定的事件指定一个关联的ID,这个ID行位于data:行前面或后面皆可:data:fooid:1设置了ID后,EventSource对象会跟踪上一次触发的事件。如果连接断开,会向服务器发送一个包含名为Last-Event-ID的特殊HTTP头部的请求,以便服务器知道下一次该触发哪个事件。在多次连接的事件流中,这种机制可以确保浏览器以正确的顺序收到连接的数据段。21.5.5Web Sockets要说最令人津津乐道的新浏览器API,就得数Web Sockets了。Web Sockets的目标是在一个单独的持久连接上提供全双工(是什么?),双向通信。在JavaScript中创建了Web Socket之后,会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议交换为Web Socket协议。也就是说,使用标准的HTTP服务器无法实现Web Sockets,只有支持这种协议的专门服务器才能正常工作。由于Web Sockets使用了自定义的协议,所以URL模式也略有不同。未加密的连接不再是http://,而是ws://;加密的连接也不是https://,而是wss://。在使用Web Socket URL时,必须带着这个模式,因为将来还有可能支持其他模式。使用自定义协议而非HTTP协议的好处是,能够在客户端和服务器之间发送非常少量的数据,而不必担心HTTP那样字节级的开销。由于传递的数据包很小,因此Web Sockets非常适合移动应用。毕竟对移动应用开发而言,带宽和网络延迟都是关键问题。使用自定义协议的缺点在于,制定协议的时间比制定JavaScript API的时间还要长。Web Sockets曾几度搁浅,就因为不断有人发泄这个新协议存在一致性和安全性的问题。Firefox 4和Opera 11都曾默认启动Web Sockets,但在发布前夕又禁用了,因为又发现了安全隐患。目前支持Web Sockets的浏览器有Firefox 6+、Safari 5+ 和 iOS 4+版 Safari。1.Web Sockets API要创建Web Sockets,先实例一个Web Socket对象并传入要连接的URL;var socket = new WebSocket("ws://www.example.com/server.php");注意,必须给WebSocket构造函数传入绝对URL。同源策略对Web Sockets不适用。因此可以通过它打开到任何站点的连接。至于是否会与某个域中的页面通信,则完全取决于服务器。(通过握手信息就可以知道请求来自何方。)实例化了WebSocket对象后,浏览器就会马上尝试创建连接。与XHR类似,WebSocket也有一个表示当前状态的readyState属性。不过,这个属性的值与XHR并不相同,而是如下所示:·WebSocket.OPENING(0):正在建立连接。·WebSocket.OPEN(1):已经建立连接。·WebSocket.CLOSING(2):正在关闭连接。·WebSocket.CLOSE(3):已经关闭连接。WebSocket没有readystatechange事件;不过,它有其他事件,对应着不同的状态。readyState的值永远从0开始。要关闭Web Socket连接,可以在任何时候调用close()方法。socket.close();调用了close()之后,readyState的值立即变为2(正在关闭),而在关闭连接后就会变成3。2.发送和接收数据Web Socket打开之后,就可以通过连接发送和接收数据。要向服务器发送数据,使用send()方法并传入任意字符串,例如:var socket = new WebSocket("ws://www.example.com/server.php");socket.send("Hello world");因为Web Sockets只能通过连接发送纯文本数据,所以对于复杂的数据结构,在通过连接发送之前,必须进行序列化。下面的例子展示了先将数据序列化为一个JSON字符串,然后再发送到服务器:var message = { time:new Date(), text:"Hello world", clientId:"asdfp8734rew"};socket.send(JSON.stringify(message));接下来,服务器要读取其中的数据,就要解析接收到的JSON字符串。当服务器向客户端发来消息时,WebSocket对象就会触发message事件。这个message事件与其他传递消息的协议类似,也是把返回的数据保存在event.data属性中。socket.onmessage = function(event){ var data = event.data; //处理数据};与通过send()发送到服务器的数据一样,event.data中返回的数据也是字符串。如果你想得到其他格式的数据,必须手工解析这些数据。3.其他事件WebSocket对象还有其他三个事件,在连接生命周期的不同阶段触发。·open:在成功建立连接时触发。·error:在发生错误时触发,连接不能持续。 ·close:在连接关闭时触发。WebSocket对象不支持DOM 2级事件侦听器(当然不知道2017年能不能支持),因此必须使用DOM 0级语法分别定义每个事件处理程序:socket.onopen = function(){ console.log("Connection established.");};socket.onerror = function(){ console.log("Connection error.");};socket.onclose = function(){ console.log("Connection closed.");}在这三个事件中,只有close事件的event对象有额外的信息。这个事件的事件对象有三个额外的属性:wasClean、code和reason。其中,wasClean是一个布尔值,表示连接是否已经明确地关闭;code是服务器返回的数值状态码;而reason是一个空字符串,包含服务器发回的消息。可以把这些信息显示给用户,也可以记录到日记中以便将来分析。socket.onclose = function(event){ console.log("Was clean?" + event.wasClean + "Code=" + event.code + " Reason=" + event.reason );};21.5.6SSE与Web Sockets面对某个具体的用例,在考虑是使用SSE还是使用Web Sockets时,可以考虑如下几个因素。首先,你是否有自由度建立和维护Web Sockets服务器?因为Web Socket协议不同与HTTP,所以现有服务器不能用于Web Socket通信。SSE倒是通过常规HTTP通信,因此现有服务器可以满足需求。第二个要考虑的问题是到底需不需要双向通信。如果用例只需读取服务器数据(如比赛成绩),那么SSE比较容易实现。如果用例必须双向通信(如聊天室),那么Web Sockets显然更好。别忘了,在不能选择Web Sockets的情况下,组合XHR和SSE也是能实现双向通信的。21.6安全讨论Ajax和Comet安全的文章可谓连篇累牍,而相关主题的书已经出了很多本了。大型Ajax应用程序的安全问题涉及面非常之广,但我们可以从普遍意义上探讨一些基本的问题。首先,可以通过XHR访问的任何URL也可以通过浏览器或服务器访问。下面的URL就是一个例子:/getuserinfo.php?id=23如果是向这个URL发送请求,可以想象结果会返回ID为23的用户的某些数据。谁也无法保证别人不会将这个URL的用户ID修改为24、56或其他值。因此,getuserinfo.php文件必须知道请求者是否真的有权限访问要请求的数据;否则,你的服务器就会门户大开,任何人的数据都可能被泄漏出去。对于未被授权系统有权访问某个资源的情况,我们称之为CSRF(Cross-Site Request Forgery,跨站点请求伪造)。未被授权系统会伪装自己,让处理请求的服务器认为它是合法的。受到CSRF共计的Ajax程序有大有小,攻击行为既有旨在揭示系统漏洞的恶作剧,也有恶意的数据窃取或者数据销毁。为确保通过XHR访问的URL安全,通行的做法就是验证发送请求者是否有权限访问相应的资源。有下列几种方式可供选择:·要求以SSL连接来访问可以通过XHR请求的资源。·要求每一次请求都要附带经过相应算法计算得到的验证码。请注意,下列措施对防范CSRF共计不起作用:·要求发送POST而不是GET请求——很容易改变。·检查来源URL以确定是否可信——来源记录很容易伪造。·基于cookie信息进行验证——同样很容易伪造。XHR对象也提供了一些安全机制,虽然表面上看可以保证安全,但实际上却相当不可靠。实际上,前面介绍的open()方法还能再接收两个参数:要随请求一起发送的用户名和密码。带有这两个参数的请求可以通过SSL发送给服务器上的页面,如下面的例子所示:xhr.open("get","example.php",true,"username","password"); //不要这样做!花了五天把第二十一章搞定,但其实很多不知所云,还是要看看实例才能知道怎么用。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"Javascript","slug":"Javascript","permalink":"https://millionqw.github.io/tags/Javascript/"}]},{"title":"《JavaScript高级程序设计》 第二十章 JSON","date":"2017-12-15T14:12:29.000Z","path":"2017/12/15/《JavaScript高级程序设计》-第二十章-JSON/","text":"目录20.1语法20.1.1简单值20.1.2对象20.1.3数组20.2解析与序列化20.2.1JSON对象20.2.2序列化选项1.过滤结果2.字符串缩进3.toJSON()方法 20.2.3解析选项 曾经有一段事件,XML是互联网上传输结构化数据的事实标准。Web服务的第一次浪潮很大程度上都是建立在XML上的,突出的特点是服务器与服务器间通信。然而,业界一直不乏质疑XML的声音。不少人认为XML过于烦琐、冗长。为解决这个问题,也涌现了一些方案。不过,Web的发展方向已经改变了。2006年,Douglas Crockford把JSON(JavaScript Object Notation,JavaScript对象表示法)作为IETFRFC4627提交给IETF,而JSON的应用早在2001年就开始了。JSON是JavaScript的一个严格的子集,利用了JavaScript中的一些模式来表示结构化数据。Crockford认为与XML相比,JSON是在JavaScript中读写结构化数据的更好方式。因为可以把JSON直接传给eval(),而且不必创建DOM对象。关于JSON,最重要的是要理解它是一种数据格式,不是一种编程语言。虽然具有相同的语法形式,但JSON并不从属于JavaScript。而且,并不是只有JavaScript才使用JSON,毕竟JSON只是一种数据格式。很多编程语言都有针对JSON的解析器和序列化器。20.1语法JSON的语法可以表示以下三种类型的值:·简单值:使用与JavaScript相同的语法,可以在JSON中表示字符串、数值、布尔值和null。但JSON不支持JavaScript中的特殊值undefined。·对象:对象作为一种复杂数据类型,表示的是一组无序的键值对儿。而每个键值对儿中的值可以是简单值,也可以是复杂数据类型的值。·数组:数组也是一种复杂数据类型,表示一组有序的值的列表,可以通过数值索引来访问其中的值。数组的值也可以是任意类型——简单值、对象或数组。20.1.1简单值最简单的JSON数据形式就是简单值。例如,下面这个值是有效的JSON数据:5这是JSON表示数值5的方式。类似地,下面是JSON表示字符串的方式:“Hello world”JavaScript字符串与JSON字符串的最大区别在于,JSON字符串必须使用双引号(单引号会导致语法错误)。布尔值和null也是有效的JSON形式。但是,在实际应用中,JSON更多地用来表示更复杂的数据结构,而简单值只是整个数据结构中的一部分。20.1.2对象JSON中的对象与JavaScript字面量稍微有一些不同。下面是一个JavaScript中的对象字面量:var person = { name:"Nicholas", age:29}JSON表示上述对象的方式如下:{ "name":"Nicholas", "age":29}与JavaScript的对象字面量相比,JSON对象有两个地方不一样。首先,没有声明变量(JSON中没有变量的概念)。其次,没有末尾的分号(因为这不是JavaScript语句,不需要分号)。再说一遍,对象的属性必须加双引号,这在JSON中是必需的。属性的值可以是简单值,也可以是复杂类型值,因此可以像下面这样在对象中嵌入对象:{ "name":"Nicholas", "age":29, "school":{ "name":"Merrimack College", "location":"North Andover,MA" }}这个例子在顶级对象中嵌入了学校(“school”)信息。虽然有两个“name”属性,但由于它们分别属于不同的对象,因此这样完全没有问题。不过,在同一个对象中绝对不应该出现两个同名属性。20.1.3数组JSON中的第二种复杂数据类型是数组。JSON数组采用的就是JavaScript中的数组字面量形式。例如,下面是JavaScript中的数组字面量:var values = [25,"hi",true];在JSON中,可以采用同样的语法表示同一个数组:[25,"hi",true]同样要注意,JSON数组也没有变量和分号。把数组和对象结合起来,可以构成更复杂的数据集合,例如:[ { "title":"Professional javascript", "authors":[ "Nicholas C.Zakas" ], edition: 3, year:2011 }, { "title":"Professional javascript", "authors":[ "Nicholas C.Zakas" ], edition: 3, year:2011 }, { "title":"Professional Ajax", "authors":[ "Nicholas C.Zakas", "Jeremy McPeak", "Joe Fawcett" ], edition: 3, year:2011 }, { "title":"Professional Ajax", "authors":[ "Nicholas C.Zakas", "Jeremy McPeak", "Joe Fawcett" ], edition: 3, year:2011 }, { "title":"Professional javascript", "authors":[ "Nicholas C.Zakas" ], edition: 1, year:2006 }]上面说对象里的每个属性都要加双引号,为什么这里面的edition和year不加?这个数组中包含一些表示图书的对象。每个对象都有几个属性,其中一个属性是“authors”,这个属性的值又是一个数组。对象和数组通常是JSON数据结构的最外层形式(当然,并不是强制规定的),利用它们能够创造出各种各样的数据结构。20.2解析与序列化JSON之所以流行,拥有与JavaScript类似的语法并不是全部原因。更重要的一个原因是,可以把JSON数据结构解析为有用的JavaScript对象。与XML数据结构要解析成DOM文档而且从中提取数据极为麻烦相比,JSON可以解析为JavaScript对象的优势及其明显。就以上一节中包含一组图书的JSON数据结构为例,在解析为JavaScript对象后,只需要下面一行简单的代码就可以取得第三本书的书名:books[2].title当然,这里是假设把解析JSON数据结构后得到的对象保存到了变量books中。再看看下面在DOM结构中查找数据的代码:doc.getElementsByTagName("book")[2].getAttribute(title)看看这些多余的方法调用,就不难理解为什么JSON能得到JavaScript开发人员的热烈欢迎了。从此以后,JSON就成了Web服务开发中交换数据的事实标准。20.2.1JSON对象早期的JSON解析器基本上都是使用JavaScript的eval()函数。由于JSON是JavaScript语法的子集,因此eval()函数可以解析、解释并返回JavaScript对象和数组。ECMAScript5对解析JSON的行为进行规范,定义了全局对象JSON。支持这个对象的浏览器有IE8+、Firefox 3.5+、Safari 4+、Chrome和Opera 10.5+。对于较早版本的浏览器,可以使用一个 shim :http://github.com/douglascrockford/JSON-js。在旧版本中,使用eval()对JSON数据结构求值存在风险,因为可能会执行一些恶意代码。对于不能原生支持JSON解析的浏览器,使用这个shim是最佳选择。JSON对象有两个方法:stringify()和parse()。在最简单的情况下,这两个方法分别用于把JavaScript对象序列化为JSON字符串和JSON字符串解析为原生JavaScript值。(JavaScript对象->JSON字符串:序列化;JSON字符串->JavaScript对象:解析)例子:var book = { title:"Professional javascript", authors:[ "Nicholas C.Zakas" ], edition:3, year:2011};var jsonText = JSON.stringify(book);//输出:{"title":"Professional javascript","authors":["Nicholas C.Zakas"],"edition":3,"year":2011}这个例子使用JSON。stringify()把一个JavaScript对象序列化为一个JSON字符串,然后将它保存在变量jsonText中。默认情况下,JSON.stringify()输出的JSON字符串不包含任何空格字符或缩进,因此保存在jsonText中的字符串如下所示:{"title":"Professional javascript","authors":["Nicholas C.Zakas"],"edition":3,"year":2011}在序列化JavaScript对象时,所有函数及原型成员都会被友谊忽略,不体现在结果中。此外,值为undeifned的任何属性也都会被跳过,结果中最终都是值为有效JSON数据类型的实例属性。将JSON字符串直接传递给JSON.parse()就可以得到相应的JavaScript值。例如,使用下列代码就可以创建与book类似的对象:var bookCopy = JSON.parse(jsonText);注意,虽然book与bookCopy具有相同的属性,但它们是两个独立的,没有任何关系的对象。如果传给JSON.parse()的字符串不是有效的JSON,该方法会抛出错误。20.2.2序列化选项实际上,JSON.stringify()除了要序列化的JavaScript对象外,还可以接收另外两个参数,这两个参数用于指定以不同的方式序列化JavaScript对象。第一个参数是个过滤器,可以是一个数组,也可以是一个函数;第二个参数是一个选项,表示是否在JSON字符串中保留缩进。单独或组合使用这两个参数,可以更全面深入地控制 JSON的序列化。1.过滤结果如果过滤器参数是数组,那么JSON.stringify()的结果中将只包含数组中列出的属性,来看下面的例子:var book = { title:"Professional javascript", authors:[ "Nicholas C.Zakas" ], edition:3, year:2011};var jsonText = JSON.stringify(book,["title","edition"]);JSON.stringify()的第二个参数是一个数组,其中包含两个字符串:“title”和“edition”。这两个属性将要序列化的对象中的属性是对应的,因此在返回的结果字符串中,就只包含这两个属性:{"title":"Professional javascript","edition":3}如果第二个参数是函数,行为会稍有不同。传入的函数接收两个参数,属性(键)名和属性值。根据属性(键)名可以知道应该如何处理要序列化的对象中的属性。属性名只能是字符串,而在值并非键值对儿结构的值时,键名可以是字符串。为了改变序列化对象的结果,函数返回的值就是相应键的值。不过要注意,如果函数返回了undefined,那么相应的属性会被忽略。例子:var book = { title:"Professional javascript", authors:[ "Nicholas C.Zakas" ], edition:3, year:2011}var jsonText = JSON.stringify(book,function(key,value){ switch(key){ case "authors": return value.join(",") case "year": return 5000; case "edition": return undefined; default: return value; }});这里,函数过滤器根据传入的键来决定结果。如果键为“authors”,就将数组连接为一个字符串;如果键为“year”,则将其值设置为5000;如果键为“edition”,通过返回undefined删除该属性。最后,一定要提供default项,此时返回传入的值,以便其他值都能正常出现在结果中。实际上,第一次调用这个函数过滤器,传入的键是一个空字符串,而值就是book对象。序列化后的JSON字符串如下所示:{"title":"Professional javascript","authors":"Nicholas C.Zakas,Jermy","year":5000}要序列化的对象中的每一个对象都要经过过滤器,因此数组中的每个带有这些属性的对象经过过滤之后,每个对象斗志包含“title”,“authors”和“year”属性。Firefox 3.5和3.6对JSON.stringify()的实现有一个bug,在将函数作为该方法的第二个参数时,这个bug就会出现,即这个函数只能作为过滤器:返回undefined意味着要跳过某个属性,而返回其他任何值都会在结果中包含相应的属性。Firefox 4修复了这个bug。2.字符串缩进JSON.stringify()方法的第三个参数用于控制结果中的缩进和空白符。如果这个参数是一个数值,那它表示的是每个级别缩进的空格数,例如,要在每个级别缩进4个空格,可以这样写代码:var book = { title:"Professional javascript", authors:[ "Nicholas C.Zakas", ], edition:3, year:2011};var jsonText = JSON.stringify(book,null,4);保存在jsonText中的字符串如下所示:{ "title": "Professional javascript", "authors": [ "Nicholas C.Zakas" ], "edition": 3, "year": 2011}读者已经注意到了。JSON.stringify()也在结果字符串中插入了换行符以提高可读性。只要传入有效的控制缩进的参数值(大于0,亲测小于0不行),结果字符串就会包含换行符。(只缩进而不换行意义不大。)最大缩进空格数为10,所有大于10的值都会自动转换为10。3.toJSON()方法有时候,JSON.stringify()还是不能满足对某些对象进行自定义序列化的需求。在这些情况下,可以给对象定义toJSON()方法,返回其自身的JSON数据格式。原生Data对象有一个toJSON()方法,能够将JavaScript的Data对象自动转换成ISO 8601日期字符串(与在JSON对象上调用toISOString()的结果完全一样)。可以为任何对象添加toJSON()方法,例子:var book = { title:"Professional javascript", authors:[ "Nicholas C.Zakas", ], edition:3, year:2011, toJSON:function(){ return this.title; }};var jsonText = JSON.stringify(book); //输出:"Professional javascript" ————只有这个东西,只有title的值以上代码在book对象上定义了一个toJSON()方法,该方法返回图书的书名,与Date对象类似,这个方法也将被序列化为一个简单的字符串而非对象。可以让toJSON()方法返回任何值,它都能正常工作。比如,可以让这个方法返回undefined,此时如果包含它的对象嵌入在另一个对象中,会导致它的值变成null,而如果它是顶级对象,结果就是undefined。toJSON()可以作为函数过滤器的补充,因此理解序列化的内部顺序十分重要。假设把一个对象传入JSON.stringify(),序列化该对象的顺序如下:(1)如果存在toJSON()方法而且能通过它取得有效的值,则调用该方法。否则,返回对象本身。(2)如果提供了第二个参数,应用这个函数过滤器。传入函数过滤器的值是第(1)步返回的值。(3)对第(2)步返回的每个值进行相应的序列化。(4)如果提供了第三个参数,执行相应的格式化。无论是考虑定义toJSON()方法,还是考虑使用函数过滤器,亦或需要同时使用两者,理解这个顺序都是至关重要的。使用了toJSON()后,序列化的结果就是toJSON()方法里return什么就序列化什么哦,toJSON()未免太霸道了。20.2.3解析选项JSON.parse()方法也可以接收另一个参数,该参数是一个函数,将在每个键值对儿上调用。为了区别JSON.stringify()接收的替换(过滤)函数(replacer),这个函数被成为还原函数(reviver),但实际上这两个函数的签名是相同的——它们都接收两个参数,一个键和一个值,而且都需要返回一个值。如果还原函数返回undefined,则表示要从结果中删除相应的键;如果返回其他值,则将该值插入到结果中。在将日期字符串转换为Date对象时,经常要用到还原函数。例子:var book = { title:"Professional javascript", authors:[ "Nicholas C.Zakas", ], edition:3, year:2011, releasDate: new Date(2011,11,1)};var jsonText = JSON.stringify(book);var bookCopy = JSON.parse(jsonText,function(key,value){ if (key == "releaseDate") { return new Date(value); } else { return value; }});console.log(bookCopy.releaseDate.getFullYear()); //2011以上代码先是为book对象新增了一个releaseDate属性,该属性保存着一个Date对象。这个对象在经过序列化之后变成有效的JSON字符串,然后经过解析又在bookCopy中还原为一个Date对象。还原函数遇到"realeaseDate"键时,会基于相应的值创建一个新的 Date 对象。结果就是bookCopy.releaseDate属性中保存一个Date对象。正因为如此,才能基于这个对象调用getFullYear()方法。(然后意义何在?)","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"Javascript","slug":"Javascript","permalink":"https://millionqw.github.io/tags/Javascript/"}]},{"title":"Javascript中常见的内存泄漏","date":"2017-12-10T03:03:47.000Z","path":"2017/12/10/Javascript中常见的内存泄漏/","text":"内存泄漏:内存泄漏指由于错误或疏忽导致程序未能释放已经不再使用的内存,内存泄漏并非内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该内存之前就失去了对这段内存的控制,从而造成了内存的浪费。–维基百科 下面整理出几种常见的引起内存泄漏的情况 #意外的全局变量当在函数中声明变量时忘记使用var(或let、const)声明变量,就会意外地声明一个全局变量,这个全局变量实际上相当于变成了全局对象window的一个属性:1234function foo() { bar = "Million";}foo() 正常来讲,一个函数执行完成后,函数中不再被使用到的变量就会被全部销毁,由于bar是一个全局变量,所以bar并不会被销毁,会一直保留在内存中,只有页面被关闭或关闭浏览器才会被释放。 还有通过this意外创建的全局变量:1234function foo() { this.bar = "Million";}foo() 函数在全局环境执行时,this指向的就是window对象,相当于另一种方式给window对象增加了属性。 如果想避免这两种情况,可以使用‘use strict’严格模式,严格模式下的this不会自动指向window,而是指向undefined. 同时严格模式下禁止在未声明变量的情况下就使用变量。 ( var 就是变量声明操作符 ) #console.logconsole.log: 向Web控制台打印一条消息,常用来在开发时调试分析,有时会在开发过程中打印出对象信息,但是在网站发布时忘记删除,这个时候,为了维持对象可以再控制台打印,该对象就会被一直留在对象中。这个对象将无法被垃圾回收。 #闭包在同一个外部函数下,如果有多个内部函数,这些内部函数是共享同一个外部函数对象的,如果创建的内部函数没有被外部变量引用,无论内部函数是否有使用外部函数的变量,在函数执行完后,外部函数和和内部函数的变量都将被销毁,反之,如果有一个或多个内部函数被外部变量引用,且内部函数有使用到外部函数的变量,就形成了闭包,外部函数的变量就会存在于内部函数的作用域链中,哪怕这个内部函数没有使用到这个外部函数内变量,但只要同一级的其他内部函数使用到了这个外部函数内变量,这个变量就会存在所有会被外部变量引用的内部函数中。 #DOM泄漏我们知道由于浏览器的原因,当使用JS操作DOM时都会耗费很高的性能,因为DOM是放在浏览器的WebCore中,而JS的操作是放在V8引擎中,JS要取到DOM就相当于要跨过一条长长的桥,需要大量性能。但这不是引起内存泄漏的原因。原因是,我们为了减少性能损失,会在JS中,对DOM的引用放在一个变量中,避免每次都重复引用相同的变量多次,但很多时候,我们引用完DOM后都不会把该变量清除(null 或者 undefined),导致该变量一直保持对DOM的引用,造成内存泄漏。 有一种特殊情况是即使将变量设null也无法让比变量被GC的:12345678910111213141516171819//html<div id="refA"> <ul> <li><a href="#"></a></li> <li><a href="#"></a></li> <li><a href="#" id="refB"></a></li> </ul> </div><div></div><div></div>//jsvar refA = document.getElementById('refA');var refB = document.getElementById('refB');document.body.removeChild(refA);refA = null; //此时由于refB还留有对refA中孙元素refB的引用,所以refA还无法被GCrefB = null; //refB置空后没有变量保持对refeA的引用,refA被GC #timers使用setInterval定时器时如果没有clearInterval,定时器就会一直调用。即使你把调用定时器的那个对象置null,这实际上也是一种闭包。 #EventListener做移动端适配时,需要对不同尺寸设备做适配,当需要考虑横竖屏适配时,一般是在横屏发生变化时,需要将组件销毁再重新生成,而在组件中会对其相关事件绑定,如果在销毁组件时,没有将组件的事件解绑,在横竖屏发生变化时,就会不断地对组件进行事件绑定,这样会导致一些异常。甚至页面崩溃。 这篇文章其实是 常见的 JavaScript 内存泄露的阅读笔记,里面用Chrome控制台的Performance和Memory演示了查看内存泄漏,我觉得是本文最有趣的地方。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"Javascript","slug":"Javascript","permalink":"https://millionqw.github.io/tags/Javascript/"}]},{"title":"《JavaScript高级程序设计》 错误处理与调试","date":"2017-12-10T02:55:03.000Z","path":"2017/12/10/《JavaScript高级程序设计》-第十七章 错误处理与调试/","text":"目录17.1浏览器报告的错误17.1.1IE17.1.2Firefox17.1.3Safari17.1.4Opera17.1.5Chrome17.2错误处理17.2.1try-catch语句1.finally子句2.错误类型(RangeError、SyntaxError等错误类型)3.合理使用try-catch语句17.2.2抛出错误1.抛出错误的时机2.抛出错误与使用try-catch语句17.2.3错误(error)事件17.2.4处理错误的策略17.2.5常见的错误类型1.类型转换错误2.数据类型错误 3.通信错误17.2.6区分致命错误和非致命错误17.2.7把错误记录到服务器17.3调试技术17.3.1将信息记录到控制台17.3.2将消息记录到当前页面17.3.3抛出错误17.4常见的IE错误 由于JavaScript本身是动态语言,而且多年来一直没有固定的开发工具,因此人们普遍认为它是一种最难于调试的编程语言。脚本出错时,浏览器通常会给出类似于“object expected(缺少对象)”这样的消息,没有上下文消息,让人摸不着头脑。ECMAScript第3版致力于解决这个问题,专门引入了try-catch和throw语句以及一些错误类型(引入一些错误类型,有意思哈哈哈),意在让开发人员能够适当地处理错误。几年之后,Web浏览器中也出现了一些JavaScript调试程序和工具。2008年以来,大多数Web浏览器都已经具备了一些调试JavaScript代码的能力。在有了语言特性和工具支持之后,现在的开发人员已经能够适当地实现错误处理,并且能够找到错误的根源。17.1浏览器报告的错误17.1.1IE就是说IE是唯一一个会在浏览器的界面窗体(chrome,名词窗体的英文,不是Chrome浏览器),然后你想让IE弹出多少内容,要怎么设置,开启了脚本调试功能会有什么效果。说的不知道是多少代以前的IE了。直接贴:17.1.2Firefox介绍Firefox不会把错误通过浏览器界面显示出来,会把错误记录到控制台中。然后介绍了Firebug。但是现在Firebug Firefox都已经停止维护了,昨晚刚下了个Firefox开发者版本。17.1.3Safari介绍JavaScript错误要在哪里查看。17.1.4Opera内容跟上面的同。怎么设置让Opera显示错误信息,怎么设置让Opera在JavaScript错误时弹出错误控制台。17.1.5Chrome经常在用的Web Inspector控制台。17.2错误处理阐述了错误处理的重要性。作为开发人员,我们必须理解在处理JavaScript错误的时候,都有哪些手段和工具可以利用。17.2.1try-catch语句ECMA-262第3版引入了try-catch语句。作为JavaScript中处理异常的一种标准方式。基本的语法如下所示,与Java中的try-catch语句是完全相同的:try{ //可能会导致错误的代码} catch(error){ //在错误发生时怎么处理}也就是说,我们应该把所有可能会抛出错误的代码放在try语句块中,把那些用于错误处理的代码放在catch块中。例如:try{ window.someNonexistentFunction();} catch(error){ console.log("An error happened");}如果try块中的任何代码发生了错误,就会立即退出代码执行过程,然后接着执行catch块。此时,catch块会接收到一个包含错误信息的对象。与在其他语言中不同的是,即使你不想使用这个错误对象,也要给它起个名字(就是例子中catch块的参数erroe)。这个对象中包含的实际信息会因浏览器而异,但共同的是有一个保存着错误消息的message属性。ECMA-262还规定了一个保存错误类型的name属性;当前所有浏览器都支持这个属性(Opera 9之前的版本不支持这个属性)。因此,在发生错误时,就可以像下面这样实事求是地显示浏览器给出的消息。例子:try{ window.someNonexistentFunction();} catch(error){ console.log(error.message);}这个例子在向用户显示错误消息时,使用了错误对象的message属性。这个message属性是唯一一个能够保证所有浏览器都支持的属性,除此之外,IE、Firefox、Safari、Chrome以及Opera都为事件对象添加了其他相关信息。IE添加了与message属性完全相同的description属性(喵喵喵?),还添加了保存着内部错误数量的number属性。Firefox添加了fileName、lineNumber和stack(包含栈跟踪信息)属性,Safari添加了line(表示分号)、sourceId(表示内部错误代码)和sourceURL属性。当然,在跨浏览器编程时,最好还是只使用message属性。(那有何用,jQuery可能有兼容的方法吧)1.finally子句(设置了必执行)虽然在try-catch语句中是可选的,但finally子句一经使用,其代码无论如何都会执行。换句话说,try语句块中的代码全部正常执行,finally子句会执行;如果因为出错而执行了catch语句块,finally子句也会执行。甚至try或catch语句块中包含return语句,都不会执行finally子句的执行。例子:function testFinally(){ try{ return 2; } catch(error){ return 1; } finally{ return 0; }}这个函数在try-catch语句的每一部分都放了一条return语句,表面上看,调用这个函数会返回2,因为try语句并没有出错,可是,由于最后还有一个finally子句,结果就会导致该return语句被忽略;也就是说,调用这个函数只能返回0。除非把finally语句拿掉,这个函数才会返回2。如果提供finally子句,则catch子句就成了可选的(两个中选一个就好)。IE7及更早的版本有一个bug:除非有catch子句,否则finally中的代码永远不会执行。既然这么说了,如果要兼容到IE7,那catch语句即使里面什么都不写,那也要提供一个catch语句的。✎:请读者务必记住,只要代码中包含finally子句,那么无论try还是catch语句块中的return语句都将忽略。因此,在使用finally子句之前,一定要非常清楚你想让代码怎么样。(很喜欢最后一句话,所以抄下来)2.错误类型执行代码期间可能会发生的错误有多种类型。每种错误都有对应的错误类型,而当错误发生时,就会抛出相应类型的错误对象。ECMA-262定义了下列7中错误类型:·Error·EvalError·RangeError·ReferenceError·SyntaxError·TypeError·URIError(写JavaScript弹出的那几种错误都在这里了)其中,Error是基类型,其他默认错误类型都继承自该类型。因此,所有错误类型共享了一组相同的属性(错误对象中的方法全是默认的对象方法)。Error类型的错误很少见,如果有也是浏览器抛出的;这个基类型的主要目的是共开发人员抛出自定义错误(是通过实例化这个对象来自定义错误吗?)。EvalError类型的错误会在使用eval()函数而发生异常时抛出。ECMA-262中对这个错误有如下描述:“如果以非直接调用的方式使用eval属性的值(换句话说,没有明确地将其名称作为一个Identifier(是什么?标识符?百度了下是的),即用作CallExpression中的MemberExpression),或者为eval属性赋值。”简单地说,如果没有把eval()当成函数调用,就会抛出错误。(简单粗暴,专门为这个函数设置一种错误类型,这个函数是有多屌)例子:new eval(); //抛出EvalErroreval = foo; //抛出EvalError然而尴尬的是Firefox,Chrome等浏览器抛出的都不是EvalError类型的错误,没看书后面写的就去试了,后面书里也这么说了,更多的是抛出TypeError。而第二种情况也不会报错,而是被成功执行,书也说了,不会发生错误。有鉴于此,加上在实际开发中极少会这样使用eval(),所以遇到这种错误类型的可能性极小(怕是根本遇不到哟)。RangeError类型的错误会在数值超出相应范围时触发。例如,在定义数组时,如果指定了数组不支持的项数(如-20或Nuber.MAX_VALUE),就会触发这种错误。例子:var items1 = new Array(-20); //抛出RangeErrorvar items2 = new Array(Number.MAX_VALUE) //抛出RangeError不是说数组里不能存-20这个数,而是用Array构造函数定义一个数组,里面如果只有一个数字型参数的话,这个数字是表示数组长度的,所以显然,-20会报错。ReferenceError类型的错误,在找不到对象的情况下会发生(这种情况下,会直接导致人所共知的“object expected”浏览器错误(然而我是第一次听说这个错误))。通常,在访问不存在的变量时,就会发生这种错误。例如:var obj = x; //在x并未声明的情况下抛出ReferenceError其实浏览器在抛出错误类型的时候,并不止告诉我们是什么类型的错误,后面往往还有一段话详细地告诉你是什么错。想来错误类型可以用于JavaScript代码中,用于当分别出现什么类型的错误时采取什么措施吧。至于SyntaxError,当我们把语法错误的JavaScript字符串传入eval()函数时,就会导致此类错误。例如:eval("a ++ b"); //抛出SyntaxError如果语法错误的代码出现在eval()函数之外,则不太可能使用SyntaxError,因为此时的语法错误会导致JavaScript立即停止运行。亲测是抛出了SyntaxError,不过不是每一个都是,比如Edge报的错竟然是缺少分号。如果语法错误出现在eval()函数之外,也有浏览器会报语法错误,有Firefox和Safari。TypeError类型在JavaScript中会经常用到,在变量中保存着意外的类型时,或者在访问不存在的方法时,都会导致这种错误。错误的原因虽然多种多样,但归根结底还是由于在执行特定于类型的操作时,变量的类型并不符合要求所致。例子:var o = new 10; //抛出TypeErrorconsole.log("name" in true); //抛出TypeErrorFunction.prototype.toString.call("name"); //抛出TypeError最常发生类型错误的情况,就是传递给函数的参数事先未经检查,结果传入类型与预期类型不相符。URIError错误在使用encodeURI()或decodeURI(),而URI格式不正确时就会发生。这种错误很少见,因为前面说的这两个函数的容错性非常高。利用不同的错误类型,可以获悉更多有关异常的信息,从而有助于对错误做出恰当的处理。要想知道错误的类型,可以像下面这样在try-catch语句的catch语句中使用instanceof操作符。try{ someFunction();} catch(error) { if(error instanceof TypeError){ //处理类型错误 } else if (error instanceof ReferenceError){ //处理引用错误 } else { //处理其他类型的错误 }}在跨浏览器编程中,检查错误类型是确定处理方式的最简便途径;包含在message属性中的错误消息会因浏览器而异。3.合理使用try-catch当try-catch语句中发生错误时,浏览器会认为错误已经被处理了,因为不会通过本章前面讨论的机制记录或报告错误。对于那些不要求用户懂技术,也不需要用户理解错误的Web应用程序,这应该说是个理想的结果。不过,try-catch能够让我们实现自己的错误处理机制。使用try-catch最适合处理那些我们无法控制的错误。假设你在使用一个大型JavaScript库中的函数,该函数可能会有意无意地抛出一些错误。由于我们不能修改这个库的源代码,所以大可将对该函数的调用放在try-catch语句中,万一有什么错误发生,也好恰当地处理它们。在明明白白地知道自己的代码会发生错误时,再使用try-catch语句就不太合适了。例如,如果传递给函数的参数是字符串而非数值,就会造成函数出错,那么就应该先检查参数的类型,然后再决定如何去做。在这种情况下,不应用try-catch语句。17.2.2抛出错误与try-catch语句相配的还有一个throw操作符,用于随时抛出自定义错误。抛出错误时,必须要给throw操作符一个值,这个值是什么类型,没有要求。下列代码都是有效的:throw 12345;throw "Hello world";throw true;throw {name:"Javascript"};在遇到throw操作符时,代码会立即停止执行。仅当有try-catch语句捕获到被抛出的值时(怎么捕获?),代码才会继续执行。通过使用某种内置错误类型,可以更真实地模拟浏览器错误。每种错误类型的构造函数接收一个参数,即实际的错误消息。例子:throw new Error("Something bad happened");这行代码抛出了一个通用错误,带有一条自定义错误消息。浏览器会像处理自己生成的错误一样,来处理这行代码抛出的错误。换句话说,浏览器会以常规方式报告这一错误,并且会显示这里的自定义错误消息。(人为制造错误)像下面使用其他错误类型,也可以模拟出类似的浏览器错误:throw new SyntaxError("I don’t like your syntax.");throw new TypeError("What type of variavle do you take me for?");throw new RangeError("You just don’t have the range.");throw new EvalError("That doesn’t evaluate.");throw new URIError("Uri,is that you?");throw new ReferenceError("You didn’t cite your reference proper.");在创建自定义错误消息时最常用的错误类型是Error、RangeError、ReferenceError和TypeError。另外,利用原型链还可以通过继承Error来创建自定义错误类型。此时,需要为新创建的错误类型指定name和message属性。例子:function CustomError(message){ this.name = "CustomError"; this.message = message;}CustomError.prototype = new Error();throw new CustomError("My message");浏览器对待继承自Error的自定义错误类型,就像对待其他错误类型一样,如果要捕获自己抛出的错误并且把它与浏览器错误区别对待的话,创建自定义错误是很有用的。✎:IE只有在抛出Error对象的时候会显示自定义的错误消息。对于其它类型,它都无一例外地显示“exception thrown and not caught”(抛出了异常,且未被捕获)。1.抛出错误的时机要针对函数为什么会执行失败给出更多信息,抛出自定义错误是一种很方便的方式。应该在出现某种特定的已知错误条件,导致函数无法正常执行时抛出错误。换句话说,浏览器会在某种特定的条件下执行函数时抛出错误。例如,下面的函数会在参数不是数组的情况下失败:function process(values){ values.sort(); //sort()是数组方法,给非数组使用就会报错 for(var i=0,len=values.length; i<len; i++){ if (values[i] > 100) { return values[i]; } } return -1;}如果执行这个函数时传给它一个字符串参数,那么对sort()的调用就会失败。为此,不同浏览器会给出不同的错误消息,但都不是特别明确,如下所示:尽管Firefox、Chrome和Safari都明确地提出了代码中导致错误的部分,但错误消息并没有清楚地告诉我们到底出了什么问题,该怎么修复问题。在处理类似前面例子中的那个函数时,通过调试处理这些错误消息没有什么困难。但是,在面对包含数千行JavaScript代码的复杂的Web应用程序时,要想查找错误来源就没有那么容易了。在这种情况下,带有适当消息的自定义错误能够显著提示代码的可维护性。例子:function process(values){ if (!(value instanceof Array)) { throw new Error("process():Argument must be array."); } values.sort(); for(var i=0,len=value.length; i<len; i++){ if (values[i] > 100) { return values[i]; } } return -1;}这种应该是帮助开发人员调试代码的吧。重写这个函数后,如果values参数不是数组,就会抛出一个错误。错误消息中包含了函数的名称,以及为什么会发生错误的明确描述。如果一个复杂的Web应用程序发生了这个错误,那么查找问题的根源就容易多了。建议读者在开发JavaScript代码的过程中,重点关注函数和可能导致函数执行失败的因素。良好的错误处理机制应该可以确保代码只发生你自己抛出的错误。✎:在多框架环境下使用instanceof来检测数组有一些问题,详细内容参考22.11节。2.抛出错误与使用try-catch说道抛出错误与捕获错误,我们认为只应该捕获那些你确切地知道该如何处理的错误。捕获错误的目的在于避免浏览器以默认方式处理他们;而抛出错误(throw)的目的在于提供错误发生具体原因的消息。(抛出错误的作用只有一个:提供消息,告诉开发人员错在哪)17.2.3错误(error)事件任何没有通过try-catch处理的错误都会触发window对象的error事件。这个事件是Web浏览器最早支持的事件之一,IE、Firefox和Chrome为保持向后兼容,并没有对这个事件作任何修改(Opera和Safari不支持error事件)。在任何Web浏览器中,onerror事件处理程序都不会创建event对象。但它可以接收三个参数:错误消息、错误所在的URL和行号。在多数情况下, 只有错误消息有用,因为URL只是给出了文档的位置,而行号所指的代码行既可能出自嵌入的JavaScript代码,也可能出自外部的文件。要指定onerror事件处理程序,必须使用如下所示的DOM0级技术,它没有遵循“DOM2级事件的标准格式”(前面写EventUtil的addHandler还说几乎没有事件会到达DOM0级技术那个语句块,error事件就是一个了)。window.onerror = function(message,url,line){ console.log(message); return false;}只要发生错误,无论是不是浏览器生成的,都会触发error事件,并执行这个事件处理程序。然后,浏览器默认的机制发生作用,像往常一样显示出错误消息。像下面这样在事件处理程序中返回false,可以阻止浏览器报告错误的默认行为:window.onerror = function(message,url,line){ console.log(message); return false;}通过返回false,这个函数实际上就充当了整个文档中的try-catch语句,可以捕获所有无代码处理的运行错误。这个事件处理程序是避免浏览器报告错误的最后一道防线,理想情况下,只要可能就不应该使用它。只要能够适当地使用try-catch语句,就不会有错误交给浏览器,也就不会触发error事件。自己的理解:可以理解成浏览器报错也是onerror事件的一部分,本来应该在onerror执行的,但被直接return false了就没有执行。只能说这么理解。图像也支持error事件。只要图像的src特性中的URL不能返回可以被识别的图像格式,就会触发error事件。此时的error事件遵循DOM格式,会返回一个以图像为目标的event对象。下面是一个例子:var image = new Image();EventUtil.addHandler(image,"load",function(event){ console.log("Image loaded");})EventUtil.addHandler(image,"error",function(event){ console.log("Image not loaded");})image.src = "smilex.gif"; //指定不存在的文件需要注意的是,发生error事件时,图像下载过程已经结束,也就是说不能重新下载了。(那可以换一个src地址载入另一张图像吗)17.2.4处理错误的策略17.2.5常见的错误类型错误处理的核心,是首先要知道代码里会发生什么错误,由于JavaScript是松散类型的,而且也不会验证函数的参数,因此错误只会在代码运行期间出现(不像Java写一半就能报错)。一般来说,需要关注三种错误:·类型转换错误·数据类型错误·通信错误以上错误分别在特定的模式下或者没有对值进行足够的检查的情况下发生。1.类型转换错误类型转换错误发生在使用某个操作符,或者使用其他可能会自动转换值的数据类型的语言结构时。在使用相等(==)和不相等(!=)操作符,或者在if、for及while等流控制语句中使用非布尔值时,最常发生类型转换错误。第三章讨论相等和不相等操作符在执行比较之前会先转换不同类型的值。由于在非动态语言(JavaScript是动态语言)中,开发人员都使用相同的符号执行直观的比较,因此在JavaScript中往往也会以相同方式错误地使用它们。多数情况下,我们建议使用全等(===)和不全等(!==)操作符,以避免类型转换。例子:console.log(5 == "5"); //trueconsole.log(5 === "5"); //falseconsole.log(1 == true); //trueconsole.log(1 === true); //false使用相等操作符首先会将数值5转成字符串“5”,然后与另一个字符串“5”比较,结果是true。全等操作符知道要比较的是两种不同的数据类型,因为直接返回false。对于1和true也是如此。使用全等和非醛等操作符可以避免因为使用相等和不相等操作符引发的类型转换错误,因此作者强烈推荐使用。容易发生类型转换错误的另一个地方,就是流程控制语句。像if之类的语句在确定下一步操作之前,会自动把任何值转换成布尔值。尤其是if语句,如果使用不当,最容易出错。例子:function concat(str1,str2,str3){ var result = str1 + str2; if (str3) { //绝对不要这样! result += str3; } return result;}这个函数的用意是拼接两或三个字符串,然后返回结果。其中,第三个字符串是可选的,因此必须要检查。第三章介绍过,未使用过的命名变量会自动被赋予undefined值。而undefined指可以被转换成布尔值false,因此这个函数中的if语句实际上只适用于提供了第三个参数的情况。问题在于,不止undefined会被转换成false,如果第三个参数是数值0,那么if语句也会返回分false。在流程控制语句中使用非布尔值,是极为常见的一个错误来源。为避免此错误,就要做到在条件比较时切实传入布尔值。实际上,执行某种形式的比较就可以达到这个目的。例如:我们可以将前面的函数重写如下:function concat(str1,str2,str3){ var result = str1 + str2; if (typeof str3 == "string") { //恰当的比较 result += str3; } return result;}在这个重写后的函数中,if语句的条件会基于比较返回一个布尔值。这个函数相对可靠得多,不容易受非正常值的影响。2.数据类型错误JavaScript是松散类型的,也就是说,在使用变量和函数参数之前,不会对它们进行比较以确保它们的数据类型正确。为了保证不会发生数据类型错误,只能依靠开发人员编写适当的数据类型检测代码。在将预料之外的值传递给函数的情况下,最容易发生数据类型错误。在前面的例子中,通过检测俄第三个参数可以确保它是一个字符串,但并没有检测另外两个参数。如果该函数必须要返回一个字符串,那么只要给它传入两个数值,忽略第三个参数,就可以轻易地导致它的执行结果错误。类似的情况也存在于下面这个函数中://不安全的函数,任何非字符串值都会导致错误function getQueryString(url){ var pos = url.indexOf("?"); if (pos > -1) { return url.substring(pos+1); } return "";}这个函数的用意是返回给定URL中的查询字符串。为此,它首先使用indexOf()寻找字符串中的问号。如果找到了,利用substring()方法返回问号后面的所有字符串。这个例子中的两个函数(indexof()和substring())只能操作字符串,因此只要传入其他数据类型的值就会导致错误。而添加一条简单的类型检测语句,就可以确保函数不那么容易出错。function getQueryString(url){ if (typeof url == "string") { //通过检查类型确保安全 var pos = url.indexOf("?"); if (pos > -1) { return url.substring(pos+1); } } return "";}重写后的这个函数首先检查了传入的值是不是字符串。这样,就确保了不会因为接收到非字符串值而导致错误。前一节提到过,在溜控制语句中使用非布尔值作为条件很容易导致类型转换错误。同样,这样做也经常会导致数据类型错误。来看下面的例子://不安全的函数,任何非数组值都会导致错误function reverseSort(values){ if (values) { //绝对不要这样! values.sort(); values.reverse(); }}这个reverseSort()函数可以将数组反向排序,其中用到了sort()和reverse()方法。对于 if 语句中的控制条件而言,任何会转换为true的非数组都会导致错误。另一个常见的错误就是将参数与null值进行比较,如下所示://不安全的函数,任何非数组值都会导致错误function reverseSort(values){ if (values != null) { //绝对不要这样! values.sort(); values.reverse(); }}与null进行比较只能确保相应的值不是null和undefined(这就相当于使用相等和不相等操作)。要确保传入的值有效,仅检测null是不够的,因此,不应该使用这种技术。同样,我们也不推荐将某个值与undefined作比较。另一种错误的做法,就是只针对要使用的某一特性执行特性检测。例子://不安全的函数,任何非数组值都会导致错误function reverseSort(values){ if (typeof values.sort == "function") { //绝对不要这样! values.sort(); values.reverse(); }}这个例子中,代码先检测了参数中是否存在sort()方法。这样,如果传入一个包含sort()方法的对象(而不是数组)当然也会通过检测,但在调用reverse()函数时可能就会出错了。在确切知道应该传入什么类型的情况下,最好是使用instanceof操作符来检测其数据类型。例子://安全,非数值将被忽略function reverseSort(values){ if (values instanceof Array) { //问题解决了 values.sort(); values.reverse(); }}其实就是直接判断传入的参数的数据类型是不是你要的那个数据类型,不要整花里胡哨的。大体上说,基本类型的值应该使用typeof来检测,而对象的值应该使用instanceof来检测。根据使用函数的方式,有时候并不需要逐个检测所有参数的数据类型。但是,面向公众的API则必须无条件地执行类型检查,以确保函数始终能够正常地执行。3.通信错误随着Ajax编程的兴起(第二十一章讨论Ajax),Web应用程序在其生命周期内动态加载信息或功能,已经成为一件司空见惯的事。不过,JavaScript与服务器之间的任何一次通信,都有可能会产生错误。第一种通信错误与格式不正确的URL或发送的数据有关。最常见的问题是在将数据发送给服务器之前,没有使用encodeURIComponent()对数据进行编码。例如,下面这个URL的格式就是不正确的:针对“redir=”后面的所有字符串调用encodeURIComponent()就可以解决这个问题,结果将产生如下字符串:对于查询字符串,应该记住必须要使用encodeURIComponent()方法。为了确保这一点,有时候可以定义一个处理查询字符串的函数,例如:function addQueryStringArg(url,name,value){ if (url.indexOf("?") == -1) { url += "?"; } else { url += "&"; } url += encodeURIComponent(name) + "=" + encodeURIComponent(value); return url;}这个函数接收三个参数:要追加查询字符串的URL,参数名和参数值。如果传入的URL不包含问号,还要给它添加问号;否则,就要添加一个和号,因为有问号就意味着有其他差un字符串。然后,再将经过编码的查询字符串的名和值添加到URL后面,可以像下面这样使用这个函数:var url = "http://www.somedomain.com&quot;;var newUrl = addQueryStringArg(url,"redir","http://www.someotherdomain.com?a=b&c=d&quot;);console.log(newUrl);//http://www.somedomain.com?redir=http%3A%2F%2Fwww.someotherdomain.com%3Fa%3Db%26c%3Dd原来查询字符串后面可以有一个网址,以为像PHP书上一样只有?age=18?page=1这种。使用这个函数而不是手工构建URL,可以确保编码正确并避免相关错误。另外,第二种通信错误:在服务器相应的数据不正确时,也会发生通信错误。第十章曾经讨论过动态加载脚本和动态加载样式,运用这两种技术都有可能遇到资源都不可用的情况。在没有返回相应资源的情况下,Firefox、Chrome和Safari会默默地失败,IE和Opera则都会报错。然而,对于使用这两种技术产生的错误,很难判断和处理。在某种情况下,使用Ajax通信可以提供有关错误状态的更多信息。17.2.6区分致命错误和非致命错误任何错误处理策略中最重要的一个部分,就是确定错误是否致命。对于非致命错误,可以根据下列一或多个条件来确定:·不影响用户的主要任务;·只影响页面的一部分;·可以恢复;·重复相同操作可以消除错误。致命错误,可以通过以下一或多个条件来确定:·应用程序根本无法继续运行;·错误明显影响到了用户的主要操作;·会导致其他连带错误。要想采取适当的措施,必须要知道JavaScript在什么情况下会发生致命错误。在发生致命错误时,应该立即给用户发送一条消息,告诉他们无法再继续手头的事情了。加入必须刷新页面才能让应用程序正常运行,就必须通知用户,同时给用户提供一个点击即可刷新页面的按钮。区分非致命错误和致命错误的主要依据,就是看它们对用户的影响。设计良好的代码,可以做到应用程序某一部分发生错误不会不必要地影响另一个实例上毫不相干的部分。例如,My Yahoo!的个性化主页上包含了很多互不依赖的模块。如果每个模块都需要通过JavaScript调用来初始话,那么你可能会看到类似下面这样的代码:for(var i=0, len=mods.length; i<len; i++){ mods[i].init(); //可能导致致命错误}表面上看,这些代码没什么问题,一次对每个模块调用init()方法。问题在于,任何模块的init()方法如果出错,都会导致数组中后续的所有模块无法再进行初始化。从逻辑上说,这样编写代码没有什么意义。毕竟,每个模块相互之间没有依赖关系,实现各自不同功能。可能会导致致命错误的原因是代码的结构。不过,经过下面这样修改,就可以把所有模块的错误变成非致命的:for(var i=0, len=mods.length; i<len; i++){ try{ mods[i].init(); //其中一个初始化失败,进入catch块,不影响继续执行 } catch (ex) { //在这里处理错误 } }17.2.7把错误记录到服务器开发Web应用程序过程中的一种常见的做法,就是集中保存错误日志,以便查找重要错误的原因。例如数据库和服务器错误都会定期写入日记,而且会按照常用API进行分类。在复杂的Web应用程序中,我们同样推荐你把JavaScript错误也回写到服务器。换句话说,也要将这些错误写入到保存服务器端错误的地方,只不过要标明它们来自前端。把前后端的错误集中起来,能够极大地方便对数据的分析。要建立这样一种JavaScript错误记录系统,首先需要在服务器上创建一个页面(或者一个服务器入口点),用于处理错误数据。这个页面的作用无非就是从查询字符串中取得数据,然后再将数据写入错误日志中。这个页面可能会使用如下所示的函数:function logError(sev,msg){ var img = new Image(); img.src = "log.php?sev=" + encodeURIComponent(sev) + "&msg=" + encodeURIComponent(msg);}这个logError()函数接收两个参数:表示严重程度的数值或字符串(视所用系统而异)及错误消息。其中,使用了Image对象来发送请求,这样做非常灵活,主要表现如下几方面:·所有浏览器都支持Image对象,包括那些不支持XMLHttpRequest对象的浏览器。·可以避免跨域限制。通常都是一台服务i期要负责处理多台服务器的错误,而这种情况下使用XMLHttpRequest是不行的。·在记录错误的过程中出问题的概率比较低。大多数Ajax通信都是由JavaScript库提供的包装函数来处理的,如果库代码本身有问题,而你还在依赖该库记录错误,可想而知,错误消息是不可能得到记录的。只要是使用try-catch语句,就应该把错误记录到日志中。例子:for(var i=0, len=mods.length; i<len; i++){ try{ mods[i].init(); } catch (ex) { logError("nonfatal","Module init failed:" + ex.message); } }在这里,一旦模块初始化失败,就会调用logError()。第一个参数是“nonfatal”(非致命),表示错误的严重程度。第二个参数是上下文信息加上真正的JavaScript错误消息。记录到服务器中的错误消息应该尽可能多地带有上下文信息,以便鉴别导致错误的真正原因。17.3调试技术17.3.1将消息记录到控制台教我们在控制台使用concole对象(console是个对象!)这个对象具有下列方法:·error(message):将错误消息记录到控制台·info(message):将信息性消息记录到控制台·log(message):将一般消息记录到控制台·warn(message):将警告消息记录到控制台后面就是教你怎么向控制台写入消息。17.3.2将消息记录到当前页面另一种输出调试消息的方式,就是在页面中开辟一小块区域,用以显示消息。这个区域通常是一个元素,而该元素可以总是出现在页面中,但仅用于调试目的;也可以是一个需要动态创建的元素。例子:代码先检测是否已经存在调试元素,如果没有则会新创建一个<div>元素, 并为该元素应用一些样式,以便与页面中的其他元素区别开。然后,又使用innerHTML将消息写入到这个<div>元素中。结果就是页面中会有一小块区域显示错误消息。这种技术在不止JavaScript控制台的IE7及更早版本或其他浏览器十分有用。17.3.3抛出错误在此,如果有一个参数不是数值,就会抛出错误。错误消息中包含了函数的名字,以及导致错误的真正原因。浏览器只要报告了这个错误的消息,我们就可以立即知道错误来源及问题的性质。相对来说,这种具体的错误消息要比那些泛泛的浏览器错误消息更有用。对于大型应用程序来说,自定义的错误通常都使用assert()函数抛出。这个函数接收两个参数,一个是求值结果应该改为true的条件,另一个是条件为false时要抛出的错误。以下就是一个非常基本的assert()函数:function assert(condition,message){ if(!condition){ throw new Error(message); }}可以用这个assert()函数代替某些函数中需要调试的if语句,以便输出错误消息。下面是使用这个函数的例子:function divide(num1,num2){ assert(typeof num1 == "number" && typeof num2 == "number","divide():Both arguments must be numbers"); return num1/num2;}可见,使用assert()函数可以减少抛出错误所需的代码量,而且也比前面的代码更容易看懂。17.4常见的IE错误居然为了IE开了一小节还贼长。真的不想看啊。需要再来。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"《JavaScript高级程序设计》 第十六章 HTML5脚本编程","date":"2017-12-10T02:54:35.000Z","path":"2017/12/10/《JavaScript高级程序设计》-第十六章-HTML5脚本编程/","text":"目录16.1跨文档消息传递(postMessage()方法。Message事件)16.2原生拖放16.2.1拖放事件16.2.2自定义放置目标16.2.3dataTransfer对象16.2.4dropEffect与effectAllowed16.2.5可拖动(draggable属性)16.2.6其他成员16.3媒体元素16.3.1属性(<video>和<audio>元素的各种属性)16.3.2事件(各种齐备的属性)16.3.3自定义媒体播放器16.3.4检测编解码器的支持情况16.3.5Audio类型16.4历史状态管理(hashchange事件) 书里前面讨论过很多HTML5规范定义的新HTML标记,为了配合这些标记的变化,HTML5规范也用显著篇幅定义了很多JavaScript API。定义这些API的用意就是简化此前实现起来困难重重的任务,最终简化创建动态Web界面的工作。16.1跨文档消息传递(有没有意识到“跨文档消息传递”是HTML5规范的其中一个功能)跨文档消息传送(cross-document messaging)有时候简称为XDM,指的是来自不同域的页面传递信息。例如,www.wrox.com域中的页面与位于内嵌框架中的p2p.wrox.com域中的页面通信。在XDM机制出现之前,要稳妥地实现这种通信需要花很多功夫。XDM把这种机制规范化,让我们能既稳妥又简单地跨文档通信。XDM的核心是postMessage()方法。在HTML5规范中,除了XDM部分之外的其他部分也会提到这个方法名,但都是为了同一个目的:向另一个地方传递数据。对于XDM而言,“另一个地方”指的是包含在当前页面中的<iframe>元素,或者由当前页面弹出的窗口。postMessage()方法接收两个参数:一条信息和一个表示消息接收方来自哪个域的字符串。第二个参数对保障安全通信非常重要。可以防止浏览器把消息发送到不安全的地方。例子://注意:所有支持XDM的浏览器也支持iframe的contentWindow属性var iframeWindow = document.getElementById("myframe").contentWindow;iframeWindow.postMessage("A secret","http://www.wrox.com&quot;);最后一行代码尝试发送一条消息,并指定框架中的文档必须来源于“http://www.wrox.com”域。如果来源匹配,消息会传递到内嵌框架中;否则,postMessage()什么也不做(什么也不返回不抱错的意思吗)。这一限制可以避免窗口中的位置在你不知情的情况下发生改变。如果传给postMessage()的第二个参数是“*”,则表示可以把消息发送给来自任何域的文档,但作者不推荐这样做。接收到XDM消息时,会触发window对象的message事件。这个事件是以异步形式触发的,因此从发送消息到接收信息(触发接收窗口的(这个事件是在接收窗口触发)message事件)可能要经过一段时间的延迟。触发message事件后,传递给onmessage处理程序的事件对象包含以下三方面的重要信息:·data:作为postMessage()第一个参数传入数据。·origin:发送消息的文档所在的域,例如“http://www.wrox.com”。·source:发送消息的文档的window对象的代理。这个代理对象主要用于在发送上一条消息的窗口中调用postMessage()方法。如果发送消息的窗口来自同一个域,那这个对象就是window。接收到消息后验证发送窗口的来源是至关重要的。就像给postMessage()方法指定第二个参数,以确保浏览器不会把消息发送给位置页面一样,在onmessage处理程序中检测消息来源可以确保传入的消息来自自己已知的页面。基本的检测模式如下:EventUtil.addHandler(window,"message",function(event){ //确保发送消息的域是已知的域 if (event.origin == "http://www.wrox.com&quot;) { //处理接收到的数据 processMessage(event.data); //可选:向来源窗口发送回执 event.source.postMessage("Received","http://p2p.wrox.com&quot;); }});作者提醒大家,event.source大多数情况下只是window的代理,并非实际的window对象。换句话说,不能通过这个代理对象访问window对象的其他任何信息。记住。只通过这个代理调用postMessage()就好,这个方法永远存在,永远可以调用。XDM还有一些怪异之处。首先,postMessage()的第一个参数最早是作为“永远都是字符串”来实现的,但后来这个参数的定义改了,改成允许传入任何数据结构。可是,并非所有浏览器都实现了这一变化。为保险起见,使用postMessage()时,最好还是只传入字符串。如果你想传入结构化的数据,最佳选择是先在要传入的数据上调用JSON.stringify(),通过postMessage()传入得到的字符串,然后再在onmessage事件处理程序中调用JSON.parse()。在通过内嵌框架加载其他域的内容时,使用XDM是非常方便的。因此,在混搭(mashup)和社交网络应用中,这种传递消息的方法极为常用。有了XDM,包含<iframe>的页面可以确保自身不受恶意内容的侵扰,因为它只通过XDM与嵌入的框架通信。而XDM也可以在来自相同域的页面间使用。支持XDM(跨文档消息传递)的浏览器有IE 8+、Firefox 3.5+、Safari 4+、Opera、Chrome、IOS版Safari及Android版WebKit。XDM已经作为一个规范独立出来,现在它的名字叫Web Messaging。16.2原生拖放最早实现元素拖放的是IE,HTML5以IE的实例为基础指定了拖放规范。Firefox 3.5、Safari 3+和Chrome也根据HTML5规范实现了原生拖放功能。16.2.1拖放事件通过拖放事件,可以控制施放相关的各个方面。其中最关键的地方在于确定哪里发生了拖放事件,有些事件是在被拖动的元素上触发的,而有些事件是在放置目标上触发的。拖动某元素时,将依次触发下列事件:(1)dragstart -> (2)drag -> (3)dragend按下鼠标键并开始移动鼠标时,会在被拖动的元素上触发dragstart事件。此时光标变成“不能放”符号,表示不能把元素放到自己上面。拖动开始时,可以通过ondragstart事件处理程序来运行JavaScript代码。触发dragstart事件后,随即会触发drag事件,而且在元素被拖动期间会持续触发该事件。这个事件与mousemove事件相似,在鼠标移动过程中,mousemove事件也会持续发生。当拖动停止时(无论是把元素放到了有效的放置目标,还是放到了无效的放置目标上),也会触发dragend事件。上述三个事件的目标都是被拖动的元素。默认情况下,浏览器不会在拖动期间改变被拖动元素的外观,但你可以自己修改。不过,大多数浏览器都会为正在被拖动的元素创建一个半透明的副本(像豆瓣拖动主页里的东西一样)。这个副本始终跟随着光标移动。当某个元素被拖动到一个有效的放置目标上时,下列事件会依次发生:(1)dragenter -> (2)dragover -> (3)dragleave或drop只要有元素被拖动到放置目标上,就会触发dragenter事件(类似于mouseover事件)。紧跟随其后的是dragover事件,而且被拖动的元素还在放置目标的方位内移动时,就会持续触发该事件。如果元素被拖出了放置目标,dragover事件不再发生,但会触发dragleave事件(类似于mouseout事件)。如果元素被放到了防止目标中,则会触发drop事件而不是dragleave事件。上述三个事件的目标都是作为放置目标的元素。所以一个元素从开始被拖动,到拖进了一个有效的元素上后到放下,会有6个事件可以被触发。16.2.2自定义放置目标在拖动元素经过某些无效放置目标时,可以看到一种特殊的光标(圆环中有一条反斜线),表示不能放置。虽然所有元素都支持放置目标事件,但这些元素默认是不允许放置的。如果拖动元素经过不允许放置的元素,无论用户如何操作,都不会发生drop事件。不过,你可以把任何元素变成有效的放置目标,方法是重写dragenter和dragover事件的默认行为。例如,假设有一个ID为“droptarget”的<div>元素,可以用如下代码将它变成一个放置目标。var droptarget = document.getElementById("droptarget");EventUtil.addHandler(droptarget."dragover",function(event){ EventUtil.preventDefault(event);});EventUtil.addHandler(droptarget,"dragenter",function(event){ EventUtil.preventDefault(event);})简直就是阻止了默认事件,什么“重写”。以上代码执行后,就会妨碍西安当拖动着元素移动到放置目标上时,光标变成了允许放置的符号。当然,释放鼠标也会触发drop事件、在Firefox 3.5+中,放置事件的默认行为是打开被放到放置目标上的URL。换句话说,如果是把图像拖放到放置目标上,页面就会转向图像文件;而如果是把文本拖放到放置目标上,则会导致无效URL错误。因此,为了让Firefox支持正常的拖放,还要取消drop事件的默认行为,阻止它打开URL:EventUtil.addHandler(droptarget,"drop",function(event){ EventUtil.preventDefault(event);})16.2.3dataTransfer对象为了在施放操作时实现数据交换,IE5引入了dataTransfer对象,它是事件对象的一个属性,用于从被拖动元素向放置目标传递字符串格式的数据。因为它是事件对象的属性,所以只能在拖放事件的事件处理程序中访问dataTransfer对象。在事件处理程序中,可以使用这个对象的属性和方法来完善拖放功能。目前,HTML5规范草案也收入了dataTransfer对象。dataTransfer对象有两个主要方法:getData()和setData()。不难想象,getData()可以取得由setData()保存的值。setData()方法的第一个参数,也是getData()方法唯一的一个参数,是一个字符串,表示保存的是据类型,取值为“text”或“URL”。例子://设置和接收文本数据event.dataTransfer.setData("text","some text");var text = event.dataTransfer.getData("text");//设置和接收URLevent.dataTransfer.setData("URL","http://www.wrox.com&quot;);var url = event.dataTransfer("URL");IE只定义了“text”和“URL”两种有效的数据类型,而HTML5则对此加以扩展,允许指定各种MIME类型。考虑到向后兼容,HTML5也支持“text”和“URL”,但这两种类型会被映射为“text/plain”和“text/uri-list”。实际上,dataTranster对象可以为每种MIME类型都保存一个值。换句话说,同时在这个对象中保存一段文本和一个URL不会有任何问题。不过,保存在dataTransfer对象中的数据只能在drop事件处理程序中读取。如果在ondrop处理程序中没有读到数据,那就是dataTransfer对象已经被销毁,数据也丢失了。在拖动文本框中的文本时,浏览器会调用setData()方法,将拖动的文本以“text”格式保存在dataTransfer对象中。类似地,在拖动连接或图像时,会调用setData()方法并保存URL。然后,在这些元素被拖放到放置目标时,就可以通过getData()读到这些数据。当然,作为开发人员,也可以在dragstart事件处理程序中调用setData(),手工保存自己要传输的数据,以便将来使用。将数据保存为文本和保存为URL是有区别的。如果将数据保存为文本格式,那么数据不会得到任何特殊处理。而如果将数据保存为URL,浏览器会将其当成网页中的链接。换句话说,如果你把它放置到另一个浏览器中,浏览器就会打开该URL。Firefox在其第5个版本之前不能正确地将“url”和“text”映射为“text/uri-list”和“text/plain”。但是却能把“Text(T大写)”映射为“text/plain”。为了更好地在跨浏览器情况下从dataTransfer对象取得数据,最好在取得URL数据时检测两个值,而在取得文本数据时使用“Text”。var dataTransfer = event.dataTransfer;//读取URLvar url = dataTransfer.getData("url") || dataTransfer.getData("text/uri-list");//读取文本var text = dataTransfer.getData("text");注意,一定要把短数据类型放在前面,因为IE10及之前的版本仍然不支持扩展的MIME类型名,而他们在遇到无法识别的数据类型时,会抛出错误。16.2.4dropEffect与effectAllowed利用dataTransfer对象,可不光是能够传输数据,还能通过它来确定被拖动的元素以及作为放置目标的元素能够接收什么操作。为此,需要访问dataTransfer对象的两个属性:dropEffect和effectAllowed。其中,通过dropEffect属性可以知道被拖动的元素能够执行哪种放置行为。这个属性有下列4个可能的值:在把元素拖动到放置目标上时,以上每一个值都会导致光标显示为不同的符号。然而,要怎样实现光标所只是的动作完全取决于你。换句话说,如果你不介入,没有什么会自动地移动、复制,也不会打开链接。总之,浏览器只能帮你改变光标的样式,而其他的都要靠你自己来实现。要使用dropEffect属性,必须在ondragenter事件处理程序中针对放置目标来设置它。dropEffect属性只有搭配effectAllowed属性才有用。effectAllowed属性表示允许拖动元素的哪种dropEffect,effectAllowed属性可能的值如下:必须在ondragstart事件处理程序中设置effectAllowed属性。假设你想允许用户把文本框中的文本拖放到一个<div>元素中。首先,必须将dropEffect和effectAllowed设置为“move”。但是,由于<div>元素的放置事件的默认行为是什么也不做,所以文本不可能自动移动,重写这个默认行为,就能从文本框中移走文本。然后你就可以自己编写代码将文本插入到<div>中,这样整个拖放操作就完成了。如果你将dropEffect和effectAllowed的值设置为“copy”,那就不会自动移走文本框中的文本。✎:Firefox 5及之前的版本在处理effectAllowed属性时会有一个问题,即如果你在代码中设置了这个属性的值,那不一定会触发drop事件。16.2.5可拖动默认情况下,图像、链接和文本是可以拖动的,也就是说,不用额外编写代码,用户就可以拖动它们。文本只有在被选中的情况下才能拖动,而图像和链接在任何时候都可以拖动。让其他元素都可以拖动也是可能的。HTML5为所有HTML元素规定了一个draggable属性,表示元素是否可以拖动,图像和链接的draggable属性自动被设置成了true,而其他元素这个属性的默认值都是false。要想让其他元素都可拖动,或者让图像或链接不能拖动,都可以设置这个属性。例子:<!–让图像不可以拖动–><img src="smile.gif" draggable="false" alt="Smiley face"><!–让这个元素可以拖动–><div draggable="true"></div>支持draggable属性的浏览器有IE 10+、Firefox 4+、Safari 5+和Chrome。Opera 11.5及之前的版本都不支持HTML5的拖放功能。另外,为了让Firefox支持可拖动属性,还必须添加一个ondragstart事件处理程序,并在dataTransfer对象中保存一些信息。✎:在IE9及更早版本中,通过mousedown事件处理程序调用dragDrop()能够让任何元素可拖动。而在Safari 4及之前版本中,必须额外给相应元素设置CSS样式-khtml-user-drag:element。16.2.6其他成员16.3媒体元素随着音频和视频在Web上迅速流行,大多数提供富媒体内容的站点为了保障跨浏览器兼容性。不得不选择Flash。HTML5新增了两个与媒体相关的标签,让开发人员不必依赖任何插件就能在网页中嵌入跨浏览器的音频和视频内容。这两个标签就是<audio>和<video>。这两个标签除了能让开发人员方便地嵌入媒体文件之外,都提供了用于实现常用功能的JavaScript API,允许为媒体创建自定义的空间。这两个元素的用法:<!– 嵌入视频 –><video src="conference.mpg" id="myVideo">Video player not available</video><!– 嵌入音频 –><audio src="song.mp3" id="myAudio">Audio player not available</audio>可以设置width和height属性以指定视频播放器的大小(没说音频播放器可以设置大小,亲测设了也没反应),而为poster属性指定图像的URI可以在加载视频内容期间显示一副图像。另外,如果标签中有controls属性,则意味着浏览器应该显示UI控件,以便用户直接操作媒体。位于开始和结束标签之间的任何内容都将作为后备内容,在浏览器不支持这两个媒体元素的情况下显示。因为并非所有浏览器都支持所有媒体格式,所以可以指定多个不同的媒体来源。为此,不能在标签中指定src属性,而是像下面这样使用一或多个<source>元素。<!– 嵌入视频 –><video id="myVideo"> <source src="conference.webm" type="video/webm; codecs=’vp8 vorbis’"> <source src="conference.ogv" type="video/ogg; codecs=’theora,borbis’"> <source src="conference.mpg" > Video player not available</video><!– 嵌入音频 –><audio id="myAudio"> <source src="song.ogg" type="audio/ogg"> <source src="song.mp3" type="audio/mpeg"> Audio player not available</audio>关于视频和音频编解码器的内容超出了本书讨论的范围(看来里面大有学问)。作者只想告诉大家,不同的浏览器支持不同的编解码器,因此一般来说指定多种格式的媒体来源是必需的。支持这两个媒体元素的浏览器有IE9+、Firefox 3.5+、Safari 4+、Opera 10.5+、Chrome、IOS版Safari和Android版WebKit。16.3.1属性<video>和<audio>元素都提供了完善的JavaScript接口。下表列出了这两个元素共有的属性,通过这些属性可以知道媒体的当前状态。其中很多属性可以直接在<audio>和<video>元素中设置。(那些能记住播放位置的视频可能是利用了played属性,不过这个属性返回的值是这样的:TimeRanges { length: 2 },煞是古怪,获得了播放位置后的值应该是保存在了cookie或者是服务器中吧)16.3.2事件除了大量属性之外,这两个媒体元素还可以触发很多事件。这些事件监控着不同的属性的变化,这些变化可能是媒体播放的结果,也可能是用户操作播放器的结果。下表列出了媒体元素相关的事件:这些事件之所以如此具体,就是为了让开发人员只是用少量HTML和JavaScript(与创建Flash影片相比)即可编写出自定义的音频/视频播放器(Nice!!)。很无聊地实现了一个按暂停弹出广告的代码23333333,就是监听播放暂停然后让一张图片出现消失。发现<video>元素里不能插入<img>元素好像,显示不出来。16.3.3自定义媒体播放器使用<audio>和<video>元素的play()和pause方法,可以手工控制媒体文件的播放。组合使用属性,事件和这两个方法,很容易创建一个自定义的媒体播放器,如下面的例子所示:<div class="mediaplayer"> <div class="video"> <video src="movie.mov" id="player" poster="mymovie.jpg" width="300" height="200"></video> Video player not available </div> <div class="controls"> <input type="button" value="play" id="video-btn"> <span id="curtime">0</span>/<span id="duration">0</span> </div></div>再加上一些JavaScript就可以变成一个简单的视频播放器。以下就是JavaScript代码://取得元素的引用var player = document.getElementById("player"), btn = document.getElementById("video-btn"), curtime = document.getElementById("curtime"), duration = document.getElementById("duration");//更新播放时间duration.innerHTML = player.duration;//为按钮添加事件处理程序EventUtil.addHandler(btn,"click",function(event){ if (player.paused) { player.play(); btn.value = "Pause"; //通过btn的值的切换判断视频是播放还是暂停 } else { player.pause(); btn.value = "play"; }});//定时更新当前时间setInterval(function(){ curtime.innerHTML = player.currentTime;},250);代码有BUG,视频的durantion属性(视频时长)只有在视频开始播放之后才获取得到,刚打开页面视频没播放获取不到duration属性,值是NaN。而且这个属性返回的是总秒数,还要进一步换算成时分秒。其实有的浏览器自带的播放暂停UI控件已经很好看。书里所有通过<video>元素的load事件处理程序,设置了加载完视频后显示播放时间,然而代码中并没有load事件啊,试试。可以了!不过不是用的load事件,是用的canplay事件(视频已经可以播放时触发),因为作者说load事件可能会被废弃,改为canplaythrougn,而且canplay事件与load事件应该是几乎等价的,最后成功显示了播放时长。16.3.4检测编解码器的支持情况如前所述,并非所有浏览器都支持<video>和<audio>的所有编解码器,而这基本上就意味着你必须提供多个媒体来源。不过,也有一个JavaScript API能够检测浏览器是否支持某种格式和编解码器。这两个媒体元素都有一个canPlayType()方法,该方法接收一种格式/编解码器字符串,返回“probably”、“maybe”或“”(空字符串)。空字符串是假值,而“probably”和“maybe”都是真值,因此在if语句的条件测试中可以转换成true。例子:if(audio.canPlayType("audio/mpeg")){ //进一步处理}如果给canPlayType()传入了一种MIME类型,则返回值很可能是“maybe”或空字符串。这是因为媒体文件本身只不过是音频或视频的一个容器,而真正决定文件能否播放的还是编码的格式。在同时传入MIME类型和编解码器的情况下,可能性就会增加,返回的字符串会变成“probably”。下面来看几个例子:var audio = document.getElementById("audio-player");//很可能"maybe"if(audio.canPlayType("audio/mpeg")){ //进一步处理}//可能是"probably"if(audio.canPlayType("audio/ogg; codecs=\\"vorbis\\"")){ //进一步处理}注意,编解码器必须用引号引起来(23333)才行。下表列出了已知的已得到支持的音频格式和编解码器:当然,也可以使用canPlayType()来检测视频格式。下表列出了已知的已得到支持的音频格式和编码器:16.3.5Audio类型<audio>元素还有一个原生的JavaScript构造函数Audio,可以在任何时候播放音频。从同为DOM元素的角度看,Audio与Image相似,但Audio不用像Image那样必须插入到文档中。只要创建一个新实例,并传入音频源文件即可。var audio = new Audio("sound.mp3"); //不用指定路径??EventUtil.addHandler(audio,"canplaythrough",function(event){ audio.play();})创建新的Audio实例即可开始下载指定的文件。下载完成后,调用play()就可以播放音频。在IOS中,调用play()时会弹出一个对话框,得到用户的许可后才能播放声音。如果想在一段音频播放后再播放另一段音频,必须在onfinish事件处理程序中调用play()方法。16.4历史状态管理历史状态管理是现代Web应用开发中的一个难点。在现代Web应用中,用户的每次操作不一定会打开一个全新的页面(比如使用了Ajax),因此“后退”和“前进”按钮也就失去了作用,导致用户很难在不同状态间切换(什么不同状态?)。要解决这个问题,首选使用hashchange事件(第十三章讨论过,URL参数列表一变动就会调用它 。hash不是haschang哦)。HTML5通过更新history对象为管理历史状态提供了方便。通过hashchange事件,可以知道URL的参数什么时候发生了变化,即什么时候该有所反应。而通过状态管理API,能够在不加载新页面的情况下改变浏览器的URL。为此,需要使用history.pushState()方法,该方法可以接收三个参数:状态对象、新状态的标题和可选的相对URL。例子:history.pushState({name:"Nicholas"},"Nicholas’page","nicholas.html");执行pushState()方法后,新的状态信息就会被加入历史状态栈,而浏览器地址栏也会变成新的相对URL。但是,浏览器并不会真的向服务器发送请求,即使状态改变之后查询location.href也会返回与地址栏中相同的地址。另外,第二个参数目前还没有浏览器实现,因此完全可以只传入一个空字符串,或者一个短标题也可以。而第一个参数则应该尽可能提供初始化页面状态所需的各种信息。因为pushState()会创建新的历史状态,所以你会发现“后退”按钮也能使用了。按下“后退”按钮,会触发window对象的popstate事件。popstate事件的事件对象有一个state属性,这个属性就包含着当初以第一个参数传递给pushState()的状态对象。所以我们能在用户点击了“后退”按钮时通过popstate事件添加事件处理程序。EventUtil.addHandler(window,"popstate",function(event){ var state = event.state; if (state) { //第一个页面加载时state为空 processState(state); //processState()方法又是什么鬼 }})得到这个状态对象后,必须把页面重置为状态对象中的数据表示的状态(因为浏览器不会自动为你做这些,不懂这句啊,听起来好绕)。记住,浏览器加载的第一个页面没有状态,因此单击“后退”按钮返回浏览器加载的第一个页面时,event.state值为null。要更新当前状态,可以调用replaceState(),传入的参数与pushState()的前两个参数相同。调用这个方法不会在历史状态栈中创建新状态,只会重写当前状态。例子:history.replaceState({name:"Greg"},"Greg’s page");支持HTML5历史状态管理的浏览器有Firefox 4+、Safari 5+、Opera 11.5+和Chrome。在Safari和Chrome中,传递给pushState()或replaceState()的状态对象中不能包含DOM元素。而Firefox支持在状态对象中包含DOM元素。Opera还支持一个history.state属性,它返回当前状态的状态对象。其实不懂这一节,传入一个对象给当前状态是的有什么用。✎:在使用HTML5的状态管理机制时,请确保使用pushState()创造的每一个“假”URL,在Web服务器上都有一个真的、实际存在的URL与之对应。否则,单击“刷新”按钮会导致404错误。这一节介绍完了,但是感觉HTML5定义的JavaScriptAPI应该不止这么几个,还有很多新增的API能让我们更方便地进行工作,但是兼容性又是一个问题了。但是我觉得啊,window XP退出了历史舞台,移动设备越来越多,IE7,8,9一定用的人越来越少,而且还有那么多JavaScript的库,兼容问题会越来越好做。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"《JavaScript高级程序设计》 第十五章 使用Canvas绘图","date":"2017-12-10T02:53:57.000Z","path":"2017/12/10/《JavaScript高级程序设计》-第十五章-使用Canvas绘图/","text":"目录15.1基本用法15.2 2D上下文15.2.1填充和描边15.2.2绘制矩形15.2.3绘制路径15.2.4绘制文本15.2.5变换15.2.6绘制图像15.2.7阴影15.2.8渐变15.2.9模式15.2.10使用图像数据15.2.11合成15.3WebGL 15.1基本用法<canvas>元素的例子:<canvas id="drawing" width="200" height="200">A drawing of something</canvas>如果不添加任何样式或者不绘制任何图形,在页面中是看不到该元素的。要在这块画布(canvas)上绘图,需要取得绘图上下文。而取得绘图上下文对象的引用,需要调用getContext()方法并传入上下文的名字。传入“2d”,就可以取得2D上下文对象。var drawing = document.getElementById("drawing");//确定浏览器支持<canvas>元素if (drawing.getContext) { var context = drawing.getContext("2d");}在使用<canvas>元素之前,首先要检测getContext()方法是否存在,这一步非常重要,有些浏览器回味HTML规范之外的元素创建默认的HTML元素对象(比如Firefox 3,虽然会为<Canvas>元素创建一个DOM元素,但这个元素里没有getContext()方法)。在这种情况下,即使drawing变量中保存着一个有效的元素引用,也检测不到getContext()方法。使用toDataURL()方法,可以导出在<canvas>元素上绘制的图像。这个方法接受一个参数,即图像的MIME类型格式,而且适合用于创建图像的任何上下文。比如,要取得画布中的一篇PNG格式的图像,可以使用以下代码:var drawing = document.getElementById("drawing");//确定浏览器支持<canvas>元素if (drawing.getContext) { //取得图像的数据URI var imgURI = drawing.toDataURL("image/png"); //显示图像 var image = document.createElement("img"); image.src = imgURI; document.body.appendChild(image);}默认情况下,浏览器会将图像编码为PNG格式(除非另行指定)。Firefox和Opera也支持基于“image/jpeg”参数的JPEG编码格式。由于这个方法是后来才知道的,所以支持<canvas>的浏览器也是在较新的版本中才加入了对它的支持,比如IE9、Firefox 3.5和Opera 10.15.2 2D上下文2D上下文的坐标开始于<canvas>元素的左上角,原点坐标是(0,0)。所有坐标值都基于这个原点计算。15.2.1填充和描边2D上下文的两种基本绘图操作是填充和描边。填充,就是用指定的格式(颜色,渐变或图像)填充图形;描边,就是只在图形的边缘画线。大多数2D上下文操作都会细分为填充和描边两个操作,而操作的结果取决于两个属性:fillStyle和strokeStyle。这两个属性的值可以是字符串,渐变对象或模式对象,而且他们的默认值都是“#000000”。如果为他们指定表示颜色的字符串值,可以使用CSS中指定颜色值的任何格式,包括颜色名、十六进制码、rgb、rgba、hsl或hsla。举个例子:var drawing = document.getElementById("drawing");//确定浏览器支持<canvas>元素if (drawing.getContext) { var context = drawing.getContext("2d"); context.strokeStyle = "red"; context.fillStyle = "#0000ff";}15.2.2绘制矩形矩形是唯一一种可以直接再D上下文中绘制的形状。与矩形有关的方法包括fillRect()、strokeRect()和clearRect()。这三个方法都能接收4个参数:矩形的x坐标、矩形的y坐标、矩形宽度和矩形高度。这些参数的单位都是像素。首先,fillRect()方法在画布上绘制的矩形会填充指定的颜色。填充的颜色通过fillStyle属性指定,比如:var drawing = document.getElementById("drawing");//确定浏览器支持<canvas>元素if (drawing.getContext) { var context = drawing.getContext("2d"); //绘制红色矩形 context.fillStyle = "#ff0000"; //context就是绘图上下文对象的引用,fillStyle等属性和方法都是它的 context.fillRect(10,10,50,50); //绘制半透明的蓝色矩形 context.fillStyle = "rgba(0,0,255,0.5)" context.fillRect(30,30,50,50);}效果还挺好看的。strokeRect()方法在画布上绘制的矩形会使用指定的颜色描边。描边颜色通过strokeStyle属性指定。比如:var drawing = document.getElementById("drawing");//确定浏览器支持<canvas>元素if (drawing.getContext) { var context = drawing.getContext("2d"); //绘制红色描边矩形 context.strokeStyle = "#ff0000"; context.strokeRect(10,10,50,50); //绘制半透膜的蓝色描边矩形 context.strokeStyle = "rgba(0,0,255,0.5)"; context.strokeRect(30,30,50,50);}✎:描边线条的宽度由lineWidth属性控制,该属性可以是任意整数。另外,通过lineCap属性可以通知线条末端的形状是平头、圆头还是方头(“butt”、“round”或“square”),通过lineJoin属性可以控制线条相交的方式是圆交、斜交还是斜接(“round”、“bevel”或“miter”)。最后,clearRect()方法用于清除画布上的矩形区域。本质上,这个方法可以把绘制上下文中的某一矩形区域变透明。通过绘制形状然后再清除指定区域,就可以生成有意思的效果,例如把某个形状切掉一块。例子:var drawing = document.getElementById("drawing");//确定浏览器支持<canvas>元素if (drawing.getContext) { var context = drawing.getContext("2d"); //绘制红色描边矩形 context.fillStyle = "#ff0000"; context.fillRect(10,10,50,50); //绘制半透膜的蓝色描边矩形 context.fillStyle = "rgba(0,0,255,0.5)"; context.fillRect(30,30,50,50); //在两个矩形重叠的地方清除一个矩形 context.clearRect(40,40,10,10);}15.2.3绘制路径2D绘制上下文支持很多在画布上绘制路径的方法。通过路径可以创造出复杂的形状和线条。要绘制路径,首先必须调用beginPath()方法,表示要开始绘制新路径。然后,再通过调用下列方法来实际第绘制路径:·arc(x,y,radius,startAngle,endAngle,counterclockwise):以(x,y)为圆心绘制一条画线,弧线半径为radius,起始和结束角度(用弧度表示)分别为startAngle和endAngle。最后一个参数表示startAngle和endAngle是否按逆时针方向计算,值为false表示按顺时针方向计算。·arcTo(x1,y1,x2,y2,radius):从上一点开始绘制一条弧线,到(x2,y2)为止,并且以给定的半径radius穿过(x1,y1)。·bezierCurveTo(clx,cly,c2x,c2y,x,y):从上一点开始绘制一条曲线,到(x,y)为止,并且以(clx,cly)和(c2x,c2y)为控制点。·lineTo(x,y):从上一点开始绘制一条直线,到(x,y)为止。·moveTo(x,y):将绘图游标移动到(x,y),不画线。·quadraticCurveTo(cx,cy,x,y):从上一点开始绘制一条二次曲线,到(x,y)为止。并且以(cx,cy)作为控制点。·rect(x,y,width,height):从点(x,y)开始绘制一个矩形,宽度和高度分别由width和height指定。这个方法绘制的是矩形路径,而不是strokeRect()和fillRect()所绘制的独立的形状。创建了路径后,接下来有几种可能的选择。如果项绘制一条连接到路径起点的线条,可以调用closePath()。如果路径已经完成,你项用fillStyle填充它,可以调用fill()方法。另外,还可以调用stroke()方法对路径描边,描边使用的是strokeStyle。最后还可以调用clip(),这个方法可以在路径上创建一个剪切区域。下面的例子,是绘制一个不带数字的时钟表盘。var drawing = document.getElementById("drawing");//确定浏览器支持<canvas>元素if (drawing.getContext) { var context = drawing.getContext("2d"); //开始路径 context.beginPath(); //绘制外圆 context.arc(100,100,99,0,2Math.PI,false); //绘制内圆 context.moveTo(194,100); context.arc(100,100,94,0,2Math.PI,false); //绘制分针 context.moveTo(100,100); context.lineTo(100,15); //绘制时针 context.moveTo(100,100); context.lineTo(35,100); //描边路径 context.stroke();}最后一步调用stroke()方法,这样才能把图形绘制到画布上。在2D绘图上下文中,路径是一种主要的绘图方式,由于路径的使用很频繁,所以就有了一个名为isPointPath()的方法。这个方法接收x和y坐标作为参数,用于在路径被关闭之前确定画布上的某一点是否位于路径上。例子:if(context.isPointInPath(100,100)){ console.log("Point (100,100) is in the path");}2D上下文中的路径API已经非常稳定,可以利用它们结合不同的填充和描边样式,绘制出非常复杂的图形来。15.2.4绘制文本绘制文本主要有两个方法:fillText()和strokeText()。这两个方法都可以接收4个参数:要绘制的文本字符串、x坐标、y坐标和可选的最大像素宽度。而且,这两个方法都以下列3个属性为基础:·font:表示文本样式、大小及字体,用CSS中指定字体的格式来指定,例如“10px Arial”。·textAligin:表示文本对齐方式。可能的值有“start”、“end”、“left”、“right”和“center”。作者建议使用“start”和“end”,不要使用“left”和“right”,因为前两者的意思更稳妥。·textBaseline:表示文本的基线,可能的值有“top”、“hanging”、“middle”、“alphabetic”、“ideographic”和“bottom”。 因为这几个属性都有默认值,所以不用每次使用都重新设置一遍值。fillText()方法使用fillStyle属性绘制文本,而strokeText()方法使用strokeStyle属性为文本描边。相对来说,还是使用fillText()的时候更多,因为该方法模仿了在网页中正常显示文本。例如,下面的代码在前一节创建的表盘上方绘制了数字12: context.font = "bold 14px Arial"; context.textAlign = "center"; context.textBaseline = "middle"; context.fillText("12",100,20);由于绘制文本比较复杂,特别是需要把文本控制在某一区域中的时候,2D上下文提供了辅助确定文本大小的方法measureText()。这个方法接收一个参数,即要绘制的文本;返回一个TextMetrics对象。返回的对象目前只有一个width属性,将来会增加更多度量属性。measureText()方法利用font、textAlign、和textBaseline的当前值计算指定文本的大小。比如,你想在一个140像素宽度的矩形区域中绘制文本Hello world!,下面的代码从100像素的字体大小开始递减,最终会找到合适的字体大小。var fontSize = 100; context.font = fontSize + "px Arial"; while(context.measureText("Hello world!").width>140){ fontSize–; context.font = fontSize + "px Arial"; } context.fillText("Hello world!",10,10); context.fillText("Font size is" + fontSize + "px",10,50);不知道意义何在,还是用法不对,所有字并没有完全显示出来。前面提到过,fillText和strokeText()方法都可以接收第四个参数,也就是文本的最大像素宽度,不过,这个可选的参数尚未得到所有浏览器支持。提供这个参数后,调用fillText()或strokeText()时如果传入的字符串大于最大宽度,则绘制的文本字符的高度正确,但宽度会收缩以适应最大宽度。绘制文本还是相对复杂的操作,因此支持<canvas>元素的浏览器也并未完全实现所有与绘制文本相关的API。15.2.5变换通过上下文的变换,可以把处理后的图像绘制到画布上。2D绘制上下文支持各种基本的绘制变换。创建绘制上下文时,会以默认值初始化变换矩阵,在默认的变换矩阵下,所有处理都按描述直接绘制。为绘制上下文应用变换,会导致使用不同的变换矩阵应用处理,从而产生不同的结果。·rotate(angle):围绕原点旋转图像angle弧度。·scale(scaleX,scaleY):缩放图像,在x方向乘以scaleX,在y方向乘以scaleY。scaleX和scaleY的默认值都是1.0。·translate(x,y):将坐标原点移动到(x,y)。执行这个变换后,坐标(0,0)会变成之前由(x,y)表示的点。·transform(m1_1,m1_2,m2_1,m2_2,dx,dy):直接修改变换矩阵,方式是乘以如下矩阵:·setTransform(m1_1,m1_2,m2_1,m2_2,dx,dy):将变换矩阵重置为默认状态,然后再调用transform()。无论是刚才执行的变换,还是fillStyle、strokeStyle等属性,都会在当前上下文中一直有效,除非再对上下文进行什么修改。虽然没有什么办法把上下文中的一切都重置回默认值,但有两个方法可以跟踪上下文的状态变化。如果你知道将来还要返回某组属性与变换的组合,可以调用save()方法。调用这个方法后,当时的所有设置都会进入一个栈结构,得以妥善保管。然后可以对上下文进行其他修改。等想要回到之前保存的设置时,可以调用restore()方法,在保存设置的栈结构中向前返回一级,恢复之前的状态。连续调用save()可以把更多设置保存到栈结构中,之后再连续调用restore()则可以一级一级地返回。例子: context.fillStyle = "#ff0000"; context.save(); context.fillStyle = "#00ff00"; context.save(); context.fillStyle = "0000ff"; context.fillRect(0,0,100,200); //从点(100,100)开始绘制蓝色矩形 context.restore(); context.fillRect(10,10,100,200); //从点(110,110)开始绘制绿色矩形 context.restore(); context.fillRect(0,0,100,200); //从点(0,0)开始绘制红色矩形首先,将fillStyle设置为红色,并调用save()保存上下文状态。接下来,把fillStyle修改为绿色,把坐标原点变换到(100,100),再调用save()保存上下文状态。然后,把fillStyle修改为蓝色并绘制蓝色的矩形。因为此时的坐标原点已经变了,所以矩形的左上角坐标实际上是(100,100)。然后调用restore(),之后fillStyle变回了绿色,因为第二个矩形就是绿色。之所以第二个矩形的起点坐标是(110,110),是因为坐标位置的变换仍然起作用。再调用一次restore(),变换就取消了,而fillStyle也返回了红色。所以最后一个矩形是红色的,而且绘制的起点是(0,0)。(不懂)需要注意的是,save()方法保存的只是对绘图上下文的设置和变换,不会保存绘图上下文的内容。15.2.6绘制图像2D绘图上下文内置了对图像的支持。如果你想把一副图像绘制到画布上,可以使用drawImage()方法。根据期望的最终结果不同,调用这个方法时,可以使用三种不同的参数组合。最简单的调用方式是传入一个HTML<img>元素,以及绘制该图像的起点的x和y坐标。例如:var image = document.images[0];cntext.drawImage(image,10,10); //后面两个参数是坐标这两汉代码取得了文档中的第一幅图像,然后将它绘制到上下文中,起点为(10,10)。绘制到画布上的图像大小与原始大小一样。如果你想改变绘制后图像的大小,可以再多传入两个参数,分别表示目标宽度和目标高度。通过这种方式来缩放图像并不影响上下文文的变换矩阵。例如:context.drawImage(image,50,10,20,30);执行代码后,绘制出来的图像大小会变成2030像素。除了上述两种方式,还可以选择把图像中的某个区域绘制到上下文中。drawImage()方法的这种调用方式总共需要传入9个参数:要绘制的图像、源图像的x坐标、源图像的y坐标、源图像的宽度,源图像的高度、目标图像的x坐标、目标图像的y坐标、目标图像的宽度、目标图像的高度。这样调用drawImage()方法可以获得最多的控制。例如:context.drawImage(image,0,10,50,50,0,100,40,60);这行代码只会把原始图像的一部分绘制到画布上、原始图像的这一部分的起点为(0,10),宽和高都是50像素。最终绘制到上下文中的图像的起点是(0,100),而大小变成了4060像素。很完美地用画布实现了一个图片局部放大功能:if (drawing.getContext) { var context = drawing.getContext("2d"); var image = document.images[0]; EventUtil.addHandler(image,"mousemove",function(event){ context.clearRect(0,100,500,500) event = EventUtil.getEvent(event); var x = event.pageX-img.offsetLeft; var y = event.pageY-img.offsetTop+100; context.drawImage(image,x,y,img.width,img.height,50,100,200,200); context.stroke(); }) EventUtil.addHandler(img,"mouseout",function(event){ context.clearRect(50,100,200,200); })}鼠标在图片扫到的地方都会以这个地方为起点,在旁边将图片局部以画布的形式重绘放大,然后在鼠标滑出图片后清除画布。虽然有BUG。因为画布是以鼠标所在的点为左上角开始画的,所以当把鼠标放到太右边的时候,右边的图片区域没办法绘制出来,所以只有图片的左上四分之一是可以准确绘制的。这些问题只会在限制了<img>元素宽高的情况(比如限制宽高200200)下会出现,如果是给原图放大不会有这种情况。结合使用drawImage()和其他方法,可以对图像进行各种基本操作。而操作的结果可以通过toDataURL()方法(toDataURL()方法是Canvas对象的方法,不是上下文对象的方法)获得。不过,有一个例外,图像不能来自其他域。如果图像来自其他域,调用toDataURL()会抛出一个错误。打个比方,假如位于www.example.com上的页面绘制的图像来自于www.wrox.com,那当前上下文就会被认为“不干净”,因而抛出错误。15.2.7阴影2D上下文会根据以下几个属性的值,自动为形状或路径绘制出阴影:·shadowColor:用CSS颜色格式表示阴影颜色,默认为黑色。·shadowOffsetX:形状或路径x轴方向的阴影偏移量,默认为0。·shadowOffsetY:形状或路径y轴方向的阴影偏移量,默认为0。·shadowBluw:模糊的像素数,默认为0,即不模糊。这些属性都可以通过context对象(上下文对象)来修改。只要在绘制前为她们设置适当的值,就能自动产生阴影。例如:var context = drawing.getContext("2d"); context.shadowOffsetX = 5; context.shadowOffsetY = 5; context.shadowBlur = 4; context.shadowColor = "rbga(0,0,0,0.5)"; //绘制红色矩形 context.fillStyle = "#ff0000"; context.fillRect(10,10,50,50); //绘制蓝色矩形 context.fillStyle = "rgba(0,0,255,1)"; context.fillRect(30,30,50,50);然而360和Firefox都没有阴影效果啊,是我写错了吗兄弟。15.2.8渐变渐变由CanvasGradient实例表示,很容易通过2D上下文来创建和修改。要创建一个新的线性渐变,可以调用createLinearGradient()方法。这个方法接收4个参数:起点的x坐标,起点的y坐标,终点的x坐标,终点的y坐标。调用这个方法后,它就会创建一个指定大小的渐变,并返回CanvasGradient对象的实例。创建了渐变对象后,下一步就是使用addColorStop()方法来指定色标。这个方法接收两个参数:色标位置和CSS颜色值。色标位置是一个0(开始的颜色)到1(结束的颜色)之间的数字。例如:var gradient = context.createLinearGradient(30,30,70,70);gradient.addColorStop(0,"white");gradient.addColorStop(1,"black");可以把fillStyle或strokeStyle设置为这个对象,从而使用渐变来绘制形状或描边://绘制红色矩形context.fillStyle = "#ff0000";context.fillRect(10,10,50,50);//绘制渐变矩形context.fillStyle = gradient;context.fillRect(30,30,50,50);确保渐变与形状对齐非常重要,有时候可以考虑使用函数来确保坐标合适。例如:function createRectLinearGradient(context,x,y,width,height){ return context.createLinearGradient(x,y,x+width,y+height);}这个函数基于起点的x和y坐标以及宽度和高度值来创建渐变对象,从而让我们可以在fillRect()中使用相同的值。var gradient = createLinearGradient(context,30,30,50,50);gradient.addColorStop(0,"white");gradient.addColorStop(1,"black");//绘制渐变矩形context.fillStyle = gradient;context.fillRect(30,30,50,50);要创建径向渐变(或放射渐变),可以使用createRadialGradient()方法。这个方法接收6个参数,对应着两个圆的圆心和半径。前三个参数指定的是起点和圆心(x和y)及半径,后三个参数指定的是终点圆的原心(x和y)及半径。可以把径向渐变想象成一个长圆桶,而这6个参数定义的正式这个桶的两个圆形开口的位置。如果把一个圆形开口定义得比另一个小一些,那这个圆通就变成了圆锥体,而通过移动每个圆形开口的位置,就可达到像旋转这个圆锥体一样的效果。如果想从某个形状的中心点开始创建一个向外扩散的径向渐变效果,就要将两个圆定义为同心圆。比如,就拿前面创建的矩形来说,径向渐变的两个圆的圆心都应该在(55,55),因为矩形的区域是从(30,30)到(80,80)。请看代码:var gradient = context.createRadialGradient(55,55,10,55,55,30);gradient.addColorStop(0,"white");gradient,addColorStop(1,"black");//绘制红色矩形context.fillStyle = "#ff0000";context.fillRect(10,10,50,50);//绘制渐变矩形context.fillStyle = gradient;context.fillRect(30,30,50,50);15.2.9模式模式其实就是重复的图像。可以用来填充或描边图形。要创建一个新模式,可以调用createPattern()方法并传入两个参数:一个HTML<img>元素和一个表示如何重复图像的字符串。其中,第二个参数的值与CSS的background-repeat属性相同,包括“repeat”、“repeat-x”、“repeat-y”和“no-repeat”。例子:var image = document.images[0], pattern = context.createPattern(image,"repeat");//绘制矩形context.fillStyle = pattern;context.fillRect(10,10,150,150);需要注意的是,模式与渐变一样,都是从画布的原点(0,0)开始的。将填充样式(fillStyle)设置为模式对象,只表示在某个特定的区域内显示重复的图像,而不是要从某个位置开始绘制重复的图像。createPattern()方法的第一个参数也可以是一个<video>元素,或者另一个<canvas>元素。15.2.10使用图像数据2D上下文的一个明显的长处就是,可以通过getImageData()取得原始图像数据。这个方法接收4个参数:要取得其数据的画面区域的x和y坐标以及该区域的像素宽度和高度。例如,要取得左上角坐标为(10,5)、大小为5050像素的区域的图像数据,可以使用以下代码:var imageData = context.getImageData(10,5,50,50);这里返回的对象是ImageData的实例。每个ImageData对象都有三个属性:width、height和data。其中data属性是一个数组,保存着图像中每一个像素的数据。在data数组中,每个像素用4个元素来保存,分别表示红、绿、蓝和透明度值。因此,第一个像素的数据就保存在数组的第0到第3个元素中,例如:var data = imageData.data, red = data[0], green = data[1], blue = data[2], alpha = data[3];数组中每个元素的值都介于0到255之间。能够访问到原始图像数据,就能够以各种方式来操作数据。例如,通过修改图像数据,可以像下面这样创建以恶搞简单的灰阶过滤器:var drawing = document.getElementById("drawing");//确定浏览器支持<canvas>元素if (drawing.getContext) { var context = drawing.getContext("2d"), image = document.images[0], imageData,data, i,len,average, red,green,blue,alpha; //绘制原始图像 context.drawImage(image,0,0); //取得图像数据 imageData = context.getImageData(0,0,image.width,image.height); data = imageData.data; for (i=0, len=data.length; i<len; i+=4) { red = data[i]; green = data[i+1]; blue = data[i+2]; alpha = data[i+3]; //求的rgb平均值 average = Math.floor((red+green+blue)/3); //设置颜色值,透明度不变 data[i] = average; data[i+1] = average; data[i+2] = average; } //返回图像数据并显示结果 imageData.data = data; context.putImageData(imageData,0,0); //putImageData()把图像数据绘制到画布上}很好玩,原来黑白照片就是把图片的RGB三个值求平均再把平均值给RGB。还有什么“复古风”,偏绿滤镜都只是调RGB。然后我以为RBG和透明度会是以二维数组的方式储存,没想到是放在一维数组4个数排在一起。这样有更好的性能吗?操作原始像素值不仅能实现灰阶过滤,还能实现其他功能。要了解通过操作原始图像数据实现过滤器的更多信息,作者给了个网址。15.2.11合成还有两个会应用到2D上下文中所有绘制操作的属性:globalAlpha和globalCompositionOperation。其中,globalAlpha是一个介于0和1之间的值(包括0和1),用于指定所有绘制的透明度。默认值为0。如果所有后续操作都要基于相同的透明度,就可以先把globalAlpha设置为适当值,然后绘制,最后再把它设置回默认值0。例子: //绘制红色矩形 context.fillStyle = "#ff0000"; context.fillRect(10,10,50,50); //修改全局透明度 context.globalAlpha = 0.5; //绘制蓝色矩形 context.fillStyle = "rgba(0,0,255,1)"; context.fillRect(30,30,50,50); //重置全局透明度 context.globalAlpha = 0; //透明度为0图像就看不到了为什么要重置为0?第二个属性globalCompositionOperation表示后绘制的图形怎么与先绘制的图形结合。这个属性的值是字符串,可能的值如下:15.3WebGL这节作者说很少浏览器兼容(在写这本书的时候),而且Window XP等老机器会禁用WebGL,应用面怕是更窄。就不写了。直接第十六章走起。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"《JavaScript高级程序设计》第十四章 表单脚本","date":"2017-12-05T13:07:21.000Z","path":"2017/12/05/《JavaScript高级程序设计》第十四章-表单脚本/","text":"14.1表单的基础知识在HTML中,表单由<form>元素表示。而在JavaScript中,表单对应的则是HTMLFormElement类型。HTMLFormElement继承了HTMLElement(意思是说不止继承了HTMLElement类型吗?),所以与其他HTML元素一起具有相同的默认属性。不过HTMLFormElement也有它自己下列独有的属性和方法:·acceptCharset:服务器能够处理的字符集;等价于HTML的accept-charset特性。·action:接收请求的URL;等价于HTML中的action特性。·elements:表单中所有控件的集合(HTMLCollection)。·enctype:请求的编码类型,等价于HTML中的enctype特性。·length:表单中控件的数量。·method:要发送的HTTP请求类型,通常是“get”或“post”;等价于HTML的method特性。·name:表单的名称,等价于HTML中的name特性。·reset():表单重置为默认值。·submit():提交表单。target:用于发送请求和接收响应的窗口名称;等价于HTML中的target特性。除了利用id获得表单元素,还可以通过document.forms取得页面中的所有表单。在这个集合中,可以通过数值索引或name值来取得特定的表单。也没说哪些浏览器支持啊。亲测,连IE5都支持。且document.forms是个HTMLCollection类型的属性,也就是说反复访问会有性能问题。 14.1.1提交表单使用<input>或<button>都可以定义提交按钮。只要将其type特性的值设置为"submit"即可。而图像按钮则是通过将<input>的type特性设置为“image”来定义的。因此,下面代码生成的按钮,都可以提交表单:<!– 通用提交按钮 –><input type="submit" value="Submit"><!– 自定义提交按钮 –><button type="submit">Submit</button><!– 图像按钮 –><input type="image" src="graphic.gif">提交表单时,浏览器会在将请求发送给服务器之前触发submit事件。这样,我们就有机会验证表单、阻止这个事件的默认行为都可以取消表单提交。例如,下面代码会阻止表单提交:var form = document.getElementById("myForm");EventUtil.addHandler(form,"submit",function(event){ //取得事件对象 event = EventUtil.getEvent(event); //阻止默认事件 EventUtil.preventDefault(event);});在JavaScript中,以编程方式调用submit()方法也可以提交表单。而且这种方式无需表单包含提交按钮,任何时候都可以正常提交表单。例子:var form = document.getElementById("myForm");//提交表单form.submit();注意:用submit()方法提交表单不会触发submit事件,所以要记得在调用此方法前先验证数据。为了防止用户反复点击按钮造成服务器处理重复的请求,有两个解决方案:在第一次提交表单后禁用提交按钮,或者利用onsubmit()事件处理程序取消后续的表单提交操作。14.1.2重置表单就是介绍reset特性可以在<input>和<button>元素中使用,且可以通过方法reset()调用。作者说其实这个按钮是很少用的,更厂家难道做法是提供一个取消按钮,让用户能够回到前一个页面(那不就没有表单了吗?)。14.1.3表单字段可以像访问页面中的其他元素一样,使用原生DOM方法访问表单元素。此外,每个表单都有elements属性,该属性是表单中所有表单元素(字段)的集合。elements集合是一个有序列表,其中包含着表单中的所有字段。每个表单字段在elements集合中的顺序,与它们出现在标记中的顺序相同,可以按照位置和name特性来访问它们。(表单字段就是表单元素的意思吧)例子:var form = document.getElementById("form1");//取得表单中的第一个字段var field1 = form.elements[0];//取得名为“textbox1”的字段var field2 = form.elements["textbox1"];//取得表单中包含的字段的数量var fieldCount = form.elements.length;如果有多个表单空间都在使用一个name(如单选按钮),那么就会返回该name命名的一个NodeList。例子:<form id="myForm"> <ul> <li><input type="redio" name="color" value="red"></li> <li><input type="redio" name="color" value="green"></li> <li><input type="redio" name="color" value="blue"></li> </ul></form>在这个HTML表单中,有3个单选按钮,name都是“color”意味着这3个字段是一起的,在访问elements["color"]时,会返回一个NodeList,其中包含这3个元素;不过,如果访问elements[0],则只会返回第一个元素。例子:var form = document.getElementById("myForm");var colorFields = form.elements["color"]; //colorFields是这个NodeList的指针了console.log(colorFields.length) //3var firstColorField = colorFields[0];var firstFormField = form.elements[0];console.log(firstColorField === firstFormField) //true一开始觉得奇怪,“为什么表单的elements[0]不是<ul>元素”,后来大彻大悟,elements属性只包含表单中的表单元素(字段),而<ul>,<li>都不是表单元素。以上代码显示,通过form.elements[0]访问到的第一个表单字段,与包含在form.elements["color"]中的第一个元素相同。✎:也可以通过访问表单的属性来访问元素,例如form[0]可以取得第一个表单字段,而form["color"]则可以取得第一个命名字段。这些属性与通过elements集合访问到的元素是相同的。但是,我们要尽量使用elements,通过表单属性访问元素只是为了与旧浏览器向后兼容而保留的一种过渡方式。1.共有的表单字段属性除了<fieldset>元素之外,所有表单字段都拥有相同的一组属性。由于<input>类型可以表示多种表单字段,因此有些属性只适用于某些字段,但还有一些属性是所有字段共有的。表单字段共有的属性如下:·disabled:布尔值,表示当前字段是否被禁用。·form:指向当前字段所属表单的指针;只读。·name:当前字段的名称。·readOnly:布尔值,表示这个字段是否只读。·tabIndex:表示当前字段的切换(tab)字号。·type:当前字段的类型,如“checkbox”、“radio”,等等。·value:当前字段将被提交给服务器的值,对文件字段(是什么?)来说,这个属性是只读的,包含着文件在计算机中的路径。除了form属性,还有文件字段的value属性,其他属性都可以用JavaScript修改。例子:var form = document.getElementById("myForm");var field = form.elements[0];//修改value属性field.value = "Another value";//检查form属性的值console.log(field.form === form); //true//把焦点设置到当前字段field.focus();//禁用当前字段field.disabled = true;//修改type属性(不推荐,但对<input>确实可以这样做)filed.type = "checkbox";前面提到过用户的重复点击提交按钮问题,最常见的解决方案,就是在第一次单击后就禁用提交按钮。只要侦听submit事件,并在该事件发生时禁用提交按钮即可。例子:EventUtil.addHandler(form,"submit",function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); //取得提交按钮 var btn = target.elements["submit-btn"]; //禁用它 btn.disabled = true})以上代码为表单的submit事件添加了一个事件处理程序。注意,不能通过onclick事件处理程序来实现这个功能,原因是不同浏览器之间存在“时差”:有的浏览器会在触发表单的submit事件之前触发click事件,而有的浏览器则相反。对于先触发click事件的浏览器,以为着会在提交发生之前禁用按钮,结果永运都不会提交表单。因此,最好通过submit事件来禁用提交按钮。不过,submit事件不适合不包含提交按钮的表单,只有在包含提交按钮的情况下,才有可能触发表单的submit事件。除了<fieldset>之外,所有表单字段都有type属性。对于<input>元素,这个值等于HTML特性type的值,对于其他元素,这个type属性的值如下表所列:注意:<input>和<button>元素的type属性是可以动态修改的,而<select>元素的type属性是只读的。2.共有的表单字段方法每个表单字段都有的两个方法:focus()和blur()。focus()方法是干什么的就不细讲了,讲注意点:如果对一个元素用了focus()方法,但这个元素的type特性是“hidden”,或者CSS的display和visibility属性隐藏了该字段,则focus()会导致错误。HTML5为表单字段新增了一个autofocus属性,在支持这个属性的浏览器中,只要设置这个属性,不用JavaScript就能自动地把焦点移动到相应字段。例子:<input type="text" autofocus>支持这个属性的浏览器有Firefox 4+、Safari 5+、Chrome和Opera 9.6。✎:默认情况下,只有表单字段可以获得焦点,对于其他元素而言,如果先将其tabIndex属性设置为-1,然后再调用focus()方法,也可以让这些元素获得焦点。只有Opera不支持这种技术。(亲测可以,刚开始先检测了div的tabIndex是多少,发现已经是-1,但是直接调用focus()是无效的,要手动再设置一次tabIndex等于-1,再调用focus()就可以了)。blur()也不讲了,作者说表单字段还没有readonly特性的时候,可以调用blur()来创建只读字段。3.共有的表单字段事件除了支持鼠标、键盘、更改和HTML事件之外,所有表单字段都支持下列3个事件:·blur:当前字段失去焦点时触发·change:对于<input>和<textarea>元素,在它们失去焦点且value值改变时触发;对于<select>元素,在其选项改变时触发。·focus:当前元素获得焦点时触发。可以利用这三个事件,假设有一个文本框,我们只允许用户输入数值。此时,可以利用focus事件修改文本框的背景颜色,以便更清楚地表明这个字段获得了焦点。可以利用blur事件恢复文本框的背景颜色,利用change事件在用户输入了非数值字符时在此修改背景颜色。下面就给出了实现上述功能的代码:var textbox = document.forms[0].elements[0];EventUtil.addHandler(textbox,"focus",function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); if (target.style.backgroundColor) { target.style.backgroundColor = "yellow"; }});EventUtil.addHandler(textbox,"blur",function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); if (/[^\\d]/.test(target.value)) { target.style.backgroundColor = "red"; } else { target.style.backgroundColor = ""; }});EventUtil.addHandler(textbox,"change",function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); if (/[^\\d]/.test(target.value)) { target.style.backgroundColor = "red"; } else { target.style.backgroundColor = ""; }});只有在文本框中输入值之后框的颜色才会变,value为空时怎么失焦聚焦都不变色。正则表达式验证用户输入的是不是非数值。✎:blur和change事件的触发顺序因为浏览器不同而不同。14.2文本框脚本在HTML中,有两种方式表现文本框,一种是使用<input>元素的单行文本框,另一种是使用<textarea>的多行文本框。这两个控件非常相似,不过,它们之间仍然有一些重要的区别。将<input>元素的type特性设置为“text”就能表现文本框。通过设置size特性,可以指定文本框中能够显示的字符数(还以为是可以输入的字符数。亲测只是让文本框变短,输入的长度仍然不限)。value特性可以设置文本框的初始值。maxlength特性则用于指定文本框可以接受的最大字符数(亲测中英文都是6个)。相对而言<textarea>元素则始终会呈现一个i额多行文本框。rows特性指定的是文本框的字符行数,cols特性指定的是文本框的字符列数(类似<input>元素的size特性)。与<input>元素不同,<textarea>的初始值是放在<textarea></textarea>之间,例子:<textarea rows="25" cols="5">initial value</textarea>另一个与<input>的区别是,<textarea>不能在HTML中指定最大字符数(那就在JavaScript实现)。虽然<textarea>元素的初始值是写在两个标签之间,但用户输入的内容仍然是保存在value属性(体会以下,是属性,不是特性)中。作者建议我们用xx.value="XXX"这样使用value属性读取或者设置文本框的值,不建议使用标准的DOM方法。换句话说,不要用setAttribute()设置<input>元素的value特性,也不要去修改<textarea>元素的第一个子节点(里面的字是文本节点)。原因很简单:对value属性所做的修改,不一定会反映在DOM中。因此,处理文本框的值时,最好不要使用DOM方法。(不会反映在DOM中叫DOM方法?写出这一段的目的是这一段有助于理解什么是DOM方法)14.2.1选择文本上述两种文本框都支持select()方法,这个方法用于选择文本框中的所有文本(还以为是被鼠标按住不放选中的那些字文本)。在调用select()时,大多数浏览器(Opera除外)都会将焦点设置到文本框中。这个方法不接受参数,可以在任何时候被调用。例子:var textbox = document.form[0].elements["textbox1"];textbox.select();在文本框获得焦点时选择其所有文本,是一种非常常见的做法,特别是在文本框包含默认值的时候,可以不让用户一个一个地删除默认值,下面的代码实现了这一操作:var textbox = document.forms[0].elements[0];EventUtil.addHandler(textbox,"focus",function(){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); target.select();})原来这个方法的“自动选择全部文本”是说让所有文字被选,这样就可以直接一个删除键删除掉默认值了。(亲测现在的Opera版本对文本框调用select()方法也能制动获得焦点了)1.选择(select)事件与select()方法对应的,select事件,在选择了文本框中的文本时,就会触发select事件,不过,到底什么时候触发select事件,会因浏览器而异。在IE9+、Opera、Firefox、Chrome和Safari中,只有用户选了文本(且要释放鼠标),才会触发select事件。而在IE8及更早版本中,只要用户选择了一个字母(不必释放鼠标),就会触发select事件。另外,在调用select()方法时会触发select事件。例子:var textbox = document.forms[0].elements[0];EventUtil.addHandler(textbox,"select",function(){ console.log("文本被选择:"+textbox.value)})2.取得选择的文本要知道被选择的文本是什么,我以为很早就会有这个属性,结果是HTML5才通过一些扩展解决了获取被选择的文本这个问题。该规范采取的办法是添加两个属性:selectionStart和selectionEnd。这两个属性中保存的是基于0的数值,表示所选择文本的访问(即文本选区开头和结尾的偏移量)。因此,要获得用户在文本框中选择的文本没那么舒服,要使用下面代码:function getSelecterText(textbox){ return textbox.value.substring(textbox.selectionStart,textbox.selectionEnd);}利用substring()方法基于字符串的偏移量执行操作。IE9+、Firefox、Safari、Chrome和Opera都支持这两个属性,IE8及之前版本不支持,提供了另外一种方案。IE8及更早版本中有一个document.selection对象,其中保存着用户在整个文档范围内选择的文本信息。不过,在与select事件一起使用的时候,可以假定是用户选择栏文本框中的文本,因而触发了该事件。要想取得选择的文本,首先必须创建一个范围(第十二章讨论的那个范围)然后再将文本从其中国提取出来。例子:function getSelecterText(textbox){ if (typeof textbox.selectionStart == "number") { return textbox.value.substring(textbox.selectionStart,textbox.selectionEnd); }else if(document.selection){ return document.selection.createRange().text; }}这是跨浏览器版,创建范围有什么用没看不了解。3.选择部分文本HTML5也为选择文本框中的部分文本提供了解决方案,即最早由Firefox引入的setSelectionRange()方法,现在除了select方法之外,所有文本框都有一个setSelectionRange()方法,这个方法接收两个参数:要选择的第一个字符的索引和要选择的最后一个字符之后的字符的索引(类似substring()方法的两个参数)。例子:textbox.value = "Hello World";//选择所有文本textbox.setSelectionRange(0,textbox.value.length); //"Hello World"//选择前3个字符textbox.setSelectionRange(0,3); //"Hel"//选择第4到第6个字符 他这里的“第4”也是从“第0”开始算的ZZ 第0,第1,第2,第3,第4textbox.setSelectionRange(4,7) //"o w"IE8及更早版本支持使用范围选择部分文本。要选择文本框中的部分文本,必须首先使用IE在所有文本框提供的createTextRange()方法创建一个范围,并将其放在恰当的位置上。然后,使用moveStart()和moveEnd()这两个范围方法将范围移动到位。不过,在调用这两个方法以前,还必须使用collapse()将范围折叠到文本框的开始位置。此时moveStart()将范围的起点和终点移动到了相同的位置,只要再给moveEnd()传入要选择的字符总数即可,就是使用范围的select()方法选择文本。例子:与其他浏览器一样,要项在文本框中看到文本被选择的结果,必须让文本获得焦点。明天上跨浏览器版本:function selectText(textbox,startIndex,stopIndex){ if (text.setSelectionRange) { textbox.setSelectionRange(startIndex,stopIndex) } else if (textbox.createRange) { var range = textbox.createRange(); reange.collapse(true); range.moveStart("character",startIndex); range.moveEnd("character",stopIndex - startIndex); reange.select(); } textbox.focus();}textbox是一个文本框的引用哦,可不是一段字符串。14.2.2过滤输入因为文本框默认情况下没有提供多少验证文本数据的手段,所以我们用JavaScript来完成此类过滤输入操作。而综合运用事件和DOM手段,就可以将普通的文本框转换成能理解用户输入数据的功能型控件。1.屏蔽字符在类似电话号码之类的文本框我们会设置不允许用户输入非数值字符,我们可以通过keypress事件,通过阻止这个事件的默认行为来屏蔽此类字符。极端情况下,通过下列代码可以屏蔽所有按键操作:EventUtil.addHandler(textbox,"keypress",function(event){ event = EventUtil.getEvent(event); EventUtil.preventDefault(event);})这样所有按键操作都会被屏蔽,结果导致文本框变成只读的(让文本框变只读的另一种手段)。如果只想屏蔽特定的字符,则需要检测keypress事件对应的字符编码,然后再决定如何响应。下面的代码就只允许用户输入数值:EventUtil.addHandler(textbox,"keypress",function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); var charCode = EventUtil.getCharCode(event); if (!/\\d/.test(String.fromCharCode(charCode))) { EventUtil.preventDefault(event); }});使用EventUtil.getCharCode()实现了跨浏览器取得字符编码。然后,使用String.fromCharCode()将字符编码转换成字符串,再使用正则表达式/\\d/来测试该字符串,以此确定用户是否输入的是数值。代码还有一个问题,因为屏蔽了所有非数值键,会导致用户无法使用复制、粘贴的快捷键Ctrl+C、Ctrl+V。只有IE浏览器不会有这个问题,所以最后还要加一个检测条件,确保用户没有按下Ctrl键。例子:EventUtil.addHandler(textbox,"keypress",function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); var charCode = EventUtil.getCharCode(event); if (!/\\d/.test(String.fromCharCode(charCode)) && !event.ctrlKey) { EventUtil.preventDefault(event); }});按下时ctrlKey的值就是真的,没按就是假,取反。这样就可以对文本框使用复制、粘贴的快捷键了。2.操作剪贴板IE很棒地第一个支持剪贴板相关事件,并通过JavaScript访问剪贴板数据,所以IE的实现成为了事实上的标准。HTML5后来也罢剪贴板事件纳入了规范。下面6个就是剪贴板事件:·beforecopy:在发生复制操作前触发。·copy:在发生复制操作时触发。·beforecut:在发生剪贴操作前触发。·cut:在发生剪切操作时触发。·beforepaste:在发生粘帖操作前触发。·paste:在发生粘帖操作时触发。前面说了是HTML5指定的标准,后面作者又说“没有针对剪贴板操作的标准”,所以这些事件及相关对象会因浏览器而异。在Safari、Chrome和Firefox中,beforecopy、beforecut和beforepaste事件只会在显示针对文本的上下文菜单(预期将发生剪贴)的情况下触发。但是IE则会在触发copy、cut和paste事件之前先触发这些事件。在实际的事件发生之前,通过beforecopy、beforecut和beforepaste事件可以向剪贴板发送数据,或者从剪贴板取得数据之前,把数据修改。不过,取消这些事件并不会取消对剪贴板的操作——只有取消copy、cut和paste事件,才能取消相应操作发生。要访问剪贴板中的数据,可以使用clipboardData对象:在IE中,这个对象是window对象的属性,而在Firefox 4+、Safari和Chrome中,这个对象是相应event对象的属性。但是,在Firefox、Safari和Chrome中,只有在处理剪贴板对象期间clipboardData对象才有效,这是为了阻止防止对剪贴板的未授权访问;在IE中,则可以随时访问clipboardData对象。为了确保跨浏览器兼容性,最好只在发生剪贴板事件期间使用这个对象(亲测在剪切板事件中使用这个对象会报错“不存在这个属性”)。这个clipboardData对象有三个方法:getData()、setData()、clearData()。其中,getData()用于从剪贴板中取得数据,它接受一个参数,既要取得的数据的格式。在IE中,有两种数据格式:“text”、“URL”。在Firefox、Safari和Chrome中,这个参数是一种MIME类型;不过,可以用“text”代表“text/plain”。类似地,setData()方法的第一个参数也是数据类型,第二个参数是要放在剪贴板中的文本(终于知道知乎每次复制都会加一句东西怎么实现的了)。对于第一个参数,IE照样支持“text”和“URL”,而Safari和Chrome仍然只支持MIME类型。但是,与getData()方法不同的是,Safari和Chrome的setData()方法不能识别“text”类型。这两个浏览器在成功将文本放到剪贴板中后,都会返回true;否则,返回false。为了弥合这些差异,我们可以向EventUtil中再添加下列方法:var EventUtil = { //省略的代码 getClipboardText:function(event){ var clipboardData = (event.clipboardData || window.clipboardData); return clipboardData.getData("text"); }, setClipboardText:function(event,value){ if (event.clipboardData) { return event.clipboardData.setData("text/plain",value); } else if (window.clipboardData){ return window.clipboardData.setData("text",value); } } //省略的代码}getClipboardText()方法相对简单,只要根据浏览器不同,确定clipboardData对象是属于哪个对象,再以“text”类型调用getData()方法即可。setClipboardText()方法要稍微复杂一些,在取得clipboardData对象之后,根据不同的浏览器实现为setData()传入不同的类型(对Safari和Chrome是“text/plain”;对IE是“text”,其他两个浏览器大概是也可以“text/plain”吧)。在需要确保粘帖到文本框中的文本中包含某些字符,或者符合某种格式要求时,能够访问剪贴板是非常有用的。例如,如果一个文本框只接受数值,那么就必须检测粘帖过来的值,以确保有效。在paste事件中,可以确保剪切板中的值是否有效,如果无效,可以像下面示例中那样,取消默认行为:EventUtil.addHandler(textbox,"paste",function(event){ event = EventUtil.getEvent(event); var text = EventUtil.getClipboardText(event); if (!/^\\d$/.test(text)) { EventUtil.preventDefault(event); }});以上代码确保只有数值才会被粘帖到文本框中。如果剪贴板的值与正则表达式不匹配,则会取消粘帖操作。Firefox、Safari和Chrome只允许在onpaste事件处理程序中访问getData()方法。14.2.3自动切换焦点就是当用户填写完当前字段时,自动将焦点切换到下一个字段。通常,在自动切换焦点之前,必须知道用户已经输入了既定长度的数据(例如电话号码)。例如,美国的好吗通常会分为三部分:区号、局号和另外4个数字,为了取得完整的电话号码,很多网页都会提供下列3个文本框:<input type="text" name="tell1" id="textTell1" maxlength="3"><input type="text" name="tell2" id="textTell2" maxlength="3"><input type="text" name="tell3" id="textTell3" maxlength="4">为增强表单字段的易用性,同时加快数据输入,可以在前一个文本框中的字符达到最大数量后,自动将焦点切换到下一个文本框。换句话说,用户在第一个文本框中输入3个数字之后,焦点就会切换到第二个文本框,再输入3个数字,焦点又会切换到第三个文本框。这种“自动切换焦点”的功能,可以通过下列代码实现:(function(){ function tabForward(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); console.log(target.value.length) console.log(target.maxlength) if (target.value.length == target.maxLength) { var form = target.form; //每个表单元素都有的form属性,指向表单<form> for (var i = 0,len=form.elements.length; i < len; i++) { if (form.elements[i] == target) { if (form.elements[i+1]) { form.elements[i+1].focus(); } return; } } } } var textbox1 = document.getElementById("textTell1"); var textbox2 = document.getElementById("textTell2"); var textbox3 = document.getElementById("textTell3"); EventUtil.addHandler(textbox1,"keyup",tabForward); EventUtil.addHandler(textbox2,"keyup",tabForward); EventUtil.addHandler(textbox3,"keyup",tabForward);})();代码哈市有局限性的比如id名必须叫那三个,而且没有考虑隐藏字段(什么鬼?)14.2.4HTML5约束验证API(required属性)HTML5新增了一些功能,在将表单提交到服务器之前验证数据。用了这些功能,即使JavaScript被禁用了或者未能成功加载,也可以确保基本的验证。当然,只有支持这个功能的这部分浏览器有效,这些浏览器有:Firefox 4+、Safari 5+、Chrome 和 Opera 10+(edge应该也是支持的)。只有在某些情况下表单字段才能进行自动验证。具体来说,就是在HTML标记中为特定的字段(就是表单元素)指定一些约束,然后浏览器才会自动执行表单验证。例如:<input type="email" name="email"><input type="url" name="homepage">要检测浏览器是否支持这些新类型,可以在JavaScript创建一个<input>元素,然后将type属性设置为“email”或“url”,然后再检测这个属性的值。不支持它们的旧版本浏览器会自动将未知的值设置为“text”,而支持的浏览器则会返回正确的值。例如:var input = document.createElement("input");input.type = "email";var isEmailsupported = (input.type == "email");要注意的是,如果不给<input>元素设置required属性,那么空文本也会通过验证。另一方面,设置特定的输入类型并不能阻止用户输入无效的值,只是应用某些默认的验证而已。(百度了下required属性,如果设置了这个属性的值是“required”,这个表单字段就是必填的,也是HTML5的属性,应该默认是必填的吧。但是又亲测了一下默认是false。)3.数值范围HTML5还定义了另外几个输入元素。这几个元素都要求填写某种基于数字的值:“number”、“range”、“datetime”、“datetime-local”、“date”、“month”、“week”还有“time”。浏览器对这几个类型的支持情况并不好(截止出书前),所以使用时要小心。对所有这些数值类型的输入元素,可以指定min属性、max属性和step属性(从min到max的两个刻度间的差值)。例如,想让用户只能输入0到100的值,而且这个值必须是5 倍数,可以这样写代码:<input type="number" min="0" max="100" step="5" name="count">step属性的这个用法跟它的解释对应得上吗。有的浏览器可以看到能够自动递增和递减的数值调节按钮。以上这些属性在JavaScript中都能通过对应的元素访问(或修改)。此外,还有两个方法:stepUp()和stepDown(),都接收一个可选的参数:要在当前值基础上加上或减去的数值(默认是加或减1),这两个方法还没有得到任何浏览器支持(切),但下面是使用例子:input.stepUp(); //加1input.stepUp(5); //加5input.stepUp(); //减1input.stepUp(10); //减104.输入模式HTML5为文本字段新增了pattern属性。这个属性的值是一个正则表达式,用于匹配文本框中的值。例如,如果只允许在文本字段中输入数值,可以像下面的代码一样应用约束:<input type="text" pattern="\\d+" name="count">注意,模式属性的开头和末尾不用加^和$符号(假定已经有了)。这两个符号表示输入的值必须从头到尾都与模式匹配。与其他输入类型相似,指定pattern也不能阻止用户输入无效的文本。这个模式应用给值,浏览器来判断值是有效,还是无效。在JavaScript中可以通过pattern属性访问模式。 使用以下代码可以检测浏览器是否支持pattern属性:var isPatternSupported = "pattern" in document.createElement("input");5.检测有效性使用checkValidity()方法可以检测表单中的某个字段是否有效。所有表单字段都有个方法,如果字段的值有效,这个方法返回true,否则返回false。字段的值是否有效的判断一句是本节前面介绍过的那些约束。换句话说,必填字段中如果没有值就是无效的,而字段中的值与pattern属性不匹配也是无效的。例如:if(document.forms[0].elements[0].checkValidity()){ //字段有效,继续} else { //字段无效}要检测整个表单是否有效,可以在表单自身调用checkValidity()方法。如果所有表单字段都有效,这个方法返回true;即使有一个字段无效,也会返回false。if(document.forms[0].checkValidity()){ //表单有效,继续} else { //表单无效}与checkValidity()方法简单地告诉你字段是否有效相比,validity属性则会告诉你为什么字段有效或无效(一天就是个很难支持的属性啊)。这个对象包含一系列属性,每个属性会返回一个布尔值:这种兼容性不好的就直接截了。6.禁用验证通过设置novalidate属性,可以告诉表单不进行验证。<form method="post" action="signup.php" novalidate> <!–这里输入表单元素–></form>这是个<form>元素的特性。在JavaScript中使用novalidate属性可以取得或设置这个值,如果这个属性存在,值为true。如果不存在,值为false。document.forms[0].novalidate = true; //禁用验证如果一个表单中有多个提交按钮,为了指定点击某个提交按钮不必验证表单,可以在相应按钮上添加formnovalidate属性。<form action=""> <input type="submit" value="Regular Submit"> <input type="submit" value="Non-validating Submit" name="btnNoValidate" formovalidate></form>formnovalidate属性是表单内提交按钮的属性。在这个例子中,第二个按钮会不经过验证而提交表单。使用JavaScript可以设置这个属性。//禁用验证document.forms[0].elements["btnNoValidate"].formNoValidate = true;14.3选择框脚本一开始先写了14.3.1,怪不得怎么好像很多没讲过。选择框是通过<select>和<option>元素创建的。为了方便与这个控件交互,除了所有表单字段公有的属性和方法外,HTMLSelectElement类型还提供了下列属性和方法:·add(newOption,relOption):向控件中插入新<option>元素,其位置在相关项(relOption)之前。·multiple:布尔值,表示是否允许多项选择;等价于HTML中的multiple特性。·options:控件中所有<option>元素的HTMLCollection。·remove(index):移除给定位置的选项。·selectedIndex:基于0的选中项的索引,如果没有选中项,则值为-1.对于支持多选的空间,只保存选中项中第一项的索引。·size:选择框中可见的行数;等价于HTML中的size特性。选择框的type属性不是“select-one”,就是“select-multiple”,取决于HTML代码中有没有multiple特性。选择框的value项由当前选中项决定,相应规则如下:·如果没有选中的项,则选择框的value属性保存空字符串。·如果有一个选中项,而且该项的value特性已经在HTML中指定,则选择框的value属性等于选中项的value特性,即使value特性的值是空字符串,也同样遵循此条规则。·如果有一个选中项,但该项的value特性在HTML中未指定,则选择框的value属性等于该项的文本。·如果有多个选中项,则选择框的value属性将依据前两条规则取得第一个选中项的值。以下面的选择框为例: <select name="location" id="selLocation"> <option value="Sunnyvale,CA">Sunnyvale</option> <option value="Los Angeles CA">Los Angeles</option> <option value="Mountain View,CA">Mountain View</option> <option value="">China</option> <option>Australia</option> </select>如果用户选择了其中第一项,则选择框的值就是“Sunnyvale,CA”。如果文本为“China”的选项被选中,则选择框的值就是一个空字符串,因为其value的特性是空的。如果选择了最后一项,那么由于<option>中没有指定value特性,则选择框的值就是“Australia”。(既然这样如果value和中间那个文本的值是一样的,可以不用设置value值)在DOM中,每个<option>元素都有一个HTMLOptionElement对象表示。为便于访问数据,HTMLOptionElement对象添加了下列属性:·index:当前选项在options集合中的索引。·label:当前选项的标签;等价于HTML中的label特性。·selected:布尔值,表示当前选项是否被选中。将这个属性设置为true可以选中当前选项。·text:选项的文本。·value:选项的值(等价于HTML中的value特性)。其中大部分属性的目的,都是为了方便对选项数据的访问。虽然也可以使用常规的DOM功能来访问这些信息,但效率比较低,如下面例子所示: var selectbox = document.forms[0].elements[‘location’];//不推荐var text = selectbox.options[0].firstChild.nodeValue; //选项的文本var value = selectbox.options[0].getAttribute("value"); //选项的值以上代码使用标准DOM方法,取得了选择框中第一项的文本和值,太皮。可以与下面使用选项属性的代码作一比较: var selectbox = document.forms[0].elements[‘location’];//推荐var text = selectbox.options[0].text; //选项的文本var value = selectbox.options[0].value; //选项的值有个options属性很关键啊,HTMLCollection类型,后面跟的属性其实大家都有的。诶。在操作选项时,我们建议最好是使用特定于选项的属性(选项独有的属性的意思?),因为所有浏览器都支持这些属性。在将表单控件作为DOM节点的情况下,实际的交互方式则会因浏览器而异。我们不推荐使用标准DOM技术修改<optioin>元素的文本或者值。最后读者要说的一点:选择框的change事件与其他表单字段的change事件触发的条件不一样。其他表单字段的change事件是在值被修改且焦点离开当前字段时触发,而选择框的change事件只要选中了选项就会触发。✎:不同浏览器,选项的value属性返回什么值也存在差别。但是,在所有浏览器中,value属性始终等于value特性。在未指定value特性的情况下,IE8会返回空字符串,而其他浏览器会返回与text特性相同的值。(看来value特性还是要设置,即使和<option>元素中间的文本相同)14.3.1选择选项对于只允许选择一项的选择框,访问选中项的最简单方式,就是使用选择框的selectedIndex属性。例子:var selectedOption = selectbox.options[selectbox.selectedIndex];取得选中项之后,可以像下面这样显示该选项的信息:var selectedIndex = selectbox.selectedIndex;var selectedOption = selectbox.options[selectedIndex];console.log("Selected index:"+selectedIndex+"\\nSelected text:" + selectedOption.text + "\\nSelected value:" + selectedOption.value)不懂啊这一段,也没给个表单元素的例子,是说的radio单选框吗。这里通过打印显示了选中项的索引、文本和值。(懂了懂了,选择框的selectedIndex属性返回的就是那个被选的项的索引(0,1,2…),然后第二行就是取到那个索引,等价于selectbox.options[0] ,然后取这个项的text属性,value属性。第二行的selcetedIndex已经不是那个属性,而是第一行定义的变量)对于可以选择多项的选择框,selectedfIndex属性就好像只允许(书里话,“好像”?)选择一项一样。设置selectedIndex会导致取消以前的所有选项并选择指定的一项,而读取selectedIndex则只会返回选中项中第一项的索引值。另一种选择选项的方式,就是取得对某一项的引用,然后将其selected属性设置为true。例如,下面的代码会选中选择框中的第一项:selectbox.options[0].selected = true;与selectedIndex不同,在允许多选的选择框中设置选项的selected属性,不会取消对其他选中项的选择,因而可以动态选中任意多个项。但是,如果是在单击选中框中,修改某个选项的selected属性则会取消对其他项的选择。需要注意的是,将selected属性设置为false对单选选择框没有影响。实际上,selected属性的作用主要是确定用户选择了选择框中的哪一项。要取得所有选中的项,可以循环遍历选项集合,然后测试每个选项的selected属性。例子:function getSelectedOptions(selectbox){ var result = new Array(); var option = null; //思路很简单,但是设置一个变量为null的意识可以有 for (var i=0,len= selectbox.options.length; i < len; i+) { option = selectbox.options[i]; if (option.selcted) { result.push(option); } } return result;}下面是一个使用getSelectedOptions()函数取得选中项的实例:var selectbox = document.getElementById("selLocation");var selcetedOptions = getSelectedOptions(selectbox);var message = ""; //这里是设置为空字符串,因为下面是要加字符串的原因吧for(var i=0,len=selcetedOptions.length; i<len; i++){ message += "Selected index:" + selectedOptions[i].index + "\\nSelected text:" + selectedOptions[i].text + "\\nSelected value:" + selectedOptions[i].value + "\\n\\n";}有想过这个函数怎么用,但没想过数组里的值是可以有index、value等属性的!这种技术单选框和多选框都适用。14.3.2添加选项可以使用JavaScript动态创建选项,并将它们添加到选择框中。添加选项的方式有很多,第一种方式就是使用如下的DOM方法:var newOption = document.createElement(‘option’);newOption.appendChild(document.createTextNode("Option text"));newOption.setAttribute("value","Option value");selectbox.appendChild(newOption);这种即使DOM方法,消耗性能,作者既然开头就介绍这个方法想必有更好的。第二种方式是使用Option构造函数来创建新选项,这个构造函数是DOM出现之前就有的,一直遗留到现在。Option构造函数接收两个参数:文本(text)值和值(value);第二个参数可选。虽然这个构造函数会创建一个Object的实例,但兼容DOM的浏览器会返回一个<option>元素。换句话说,在这种情况下,我们仍然可以使用appendChild()将新选项添加到选择框中。例子:var newOption = new Option("Option text","Option value");selectbox.appendChild(newOption); //在IE8及之前的版本有问题这种方式在除IE之外的浏览器中都可以使用。由于存在bug,IE在这种方式下不能正确设置新选项的文本。最佳方案:第三种添加新选项的方式是使用选择框的add()方法。DOM规定这个方法接受两个参数:要添加的选项和将位于新选项之后的选项。如果想在列表的最后添加一个选项,应该将第二个参数设置为null。在IE对add()方法的实现中,第二个参数是可选的,而且如果指定,该参数必须是新选项之后选项的索引。兼容DOM的浏览器要求必须指定第二个参数,因此要想编写跨浏览器的代码,就不能只传入一个参数。这时候,为第二个参数传入undefined,就可以在所有浏览器中都将新选项插入到列表最后了。例子:var newOption = new Option("Option text","Option value");selectbox.add(newOption,undefined); //最佳方案在IE和兼容DOM的浏览器中,上面的代码都可以正常使用。如果你想将新选项添加到其他位置,应该使用标准的DOM技术和insertBefore()方法。14.3.3移除选项与添加选项类似,移除选项的方式也有很多种。首先,可以使用DOM的removeChild()方法,为其传入要移除的选项,如下面的例子所示:selectbox.removeChild(selectedbox.optionns[0]); //移除第一个选项其次,可以使用选择框的remove()方法。这个方法接受一个参数,即要移除选项的索引,如下面的例子所示:selctbox.remove[0] //移除第一个选项要移除选择框中所有的项,需要迭代所有选项并逐个移除它们,如下面的例子所示:function clearSelectbox(selectbox){ for(var i=0,len=selectbox.options.length; i<len; i++){ selectbox.remove[i]; }}这个函数每次只移除选择框中的第一个选项。由于移除第一个选项后,所有后续选项都会自动向上移动一个位置,因此重复移除第一个选项就可以移除所有选项了(但是 i 不是一直在 i++吗)。14.3.4移动和重排选项移动选项:在DOM标准出现之前,前一个选择框中的选项移动到另一个选择框中是非常麻烦的。整个过程要涉及从第一个选择框中移除选项,然后以相同的文本和值创建新选项,最后再将新选项添加到第二个选择框中。而使用DOM的appendChild()方法,就可以将第一个选择框中的选项直接移动到第二个选择框中。我们知道,如果为appendChild()方法传入一个文档中已有的元素,那么就会先从该元素的父节点中移除它,再把它添加到指定的位置。下面的代码展示了将第一个选择框中的第一个选项移动到第二个选择框中的过程:var selectbox1 = document.getElementById("selLocation1");var selectbox2 = document.getElementById("selLocation2");selectbox2.appendChild(selectbox1.options[0]);移动选项与移除选项有一个共同支出,即会重置每一个选项的index属性。重排选项次序的过程也十分相似,最好的方式仍然是使用DOM方法,要将选择框中的某一项移动到特定的位置,最合适的DOM方法就是InsertBefore();appendChild()方法只适用于将选项添加到选择框的最后。要在选择框中向前移动一个选项的位置,可以使用以下代码:var optionToMove = selectbox.options[1];selectbox.insertBefore(optionToMove,selectbox.options[optionToMove.index-1]);以上代码首先选择了要移动的选项,然后将其插入到了排在它前面的选项之前。实际上,第二行代码对除第一个选项之外的其他选项是通用的。类似地,可以使用下列代码将选择框中的选项向后移动一个位置:var optionToMove = selectbox.options[1];selectbox.insertBefor(optionToMove,selectbox.options[optionToMove.index+2])✎:IE7存在一个页面重绘问题,有时候会导致使用DOM方法重排的选项不能马上正确显示。14.4表单序列化随着Ajax的出现,表单序列化已经成为一种常见需求(第二十一章讨论Ajax)。在JavaScript中,可以利用表单字段的type属性,连同name和value属性一起实现对表单的序列化。在编写代码之前,有必须先搞清除在表单提交期间,浏览器是怎么样将数据发送给服务器的。·对表单字段的名称和值进行URL编码,使用和号(&)分隔。·不发送禁用的表单字段。·只发送勾选的复选框和单选按钮。·不发送type为"reset"和“button”的按钮。·多选选择框中的每个选中的值单独一个条目。·在单击提交按钮提交表单的情况下,也会发送提交按钮;否则,不发送提交按钮。也包括type为“image”的<input>元素。·<select>元素的值,就是选中的<option>元素的value特性的值。如果<option>元素没有value特性,则是<option>元素的文本值。在表单序列化过程中,一般不包含任何按钮字段,因为结果字符串很可能是通过其他方式提交的。除此之外的其他上述规则都应该遵循。以下就是实现表单序列化的代码:function serialize(form){ var parts = [],i,len,j,optLen,option,optValue; for(i=0,len=form.elements.length; i<len; i++){ field = form.elements[i]; switch(field.type){ case"select-one"; case"select-multiple": if(field.name.length){ for(j=0,optLen = field.options.length; j<optLen; j++){ option = field.options[j]; if (option.selected) { optValue = ""; if (option.hasAttribute) { optValue = (option.hasAttribute("value")?option.value : option.text); } else { optValue = (option.attributes["value"].specified ? option.value : option.text); } //specified属性:如果已规定某个属性,则返回true(判断value特性有没有规定,没有就是空或者 没设置,为IE安排的这个判断,因为IE不支持上面的hasAttribute这种DOM方法) parts.push(encodeURIComponent(field.name) + "=" + encodeURIComponent(optValue); } } } break; case undefined: //字段集 case "file": //文件输入 case "submit": //提交按钮 case "reset": //重置按钮 case "button": //自定义按钮 break; case "radio": //单选按钮 case "checkbox": //复选框 if(!field.checked){ break; } /执行下面的efault默认操作*/ default: //不包含没有名字的表单字段 if (field.name.length) { parts.push(encodeURIComponent(field.name) + "=" + encodeURIComponent(field.value)); } } } return parts.join("&");}上面这个serialize()函数首先定义了一个名为parts的数组,用于保存将要创建的字符串的各个部分。然后,通过for循环迭代每个表单字段,并将其保存在field变量中。在获得了一个字段的引用之后,使用switch语句检测其type属性。序列化过程中最麻烦的就是<select>元素,它可能是单选框也可能是多选框。为此,需要遍历控件中的每个选项,并在相应选项被选中的情况下向数组中添加一个值。对于单选框,只可能有一个选中项,而多选框则可能有零或多个选中项。这里的代码适用于这两种选择框,至于可选项的数量则是由浏览器控制的。在找到一个选中项之后,需要确定使用什么值。如果不存在value特性,或者虽然存在该特性,但值为空字符串,都要使用选项的文本来代替。为检查这个特性,在DOM兼容的浏览器中需要使用hasAttribute()方法,而在IE中需要使用特性的specified属性。如果表单中包含<fieldset>元素,则该元素会出现在元素集合中,但没有type属性。因此,如果type属性未定义,则不需要对其进行序列化。同样,对于各种按钮以及文件输入字段也是如此(文件输入字段在表单提交过程中包含文件的内容;但是,这个字段是无法模仿的,序列化时一般都要忽略)。对于单选按钮和复选框,要检查其checked属性是否被设置为false,如果是则退出switch语句。如果checked属性为true,则继续执行default语句,即将当前字段的名称和值进行编码,然后添加到parts数组中。函数的最后一部,就是使用join()格式化整个字符串,也就是用和号来分隔每一个表单字段。最后,serialize()函数会以查询字符串的格式输出序列化之后的字符串。当然,要序列化成其他格式,也不是什么困难的事(看到这句话突然觉得很爽)。14.5富文本编辑富文本编辑,又称为WYSIWYG(What You See Is What You Get,所见即所得)。在网页中编辑富文本内容,是人们对Web应用程序最大的期待之一。虽然也没有规范,但在IE最早引入的这一功能基础上,已经出现了事实标准。而且,Opera、Safari、Chrome和Firefox都已经支持这一功能。这一技术的本质,就是在页面嵌入一个包含空HTML页面的iframe。通过设置designMode属性,这个空白的HTML页面可以被编辑,而编辑对象则是该页面<body>元素的HTML代码。designMode属性有两个可能的值:“off”(默认值)和“on”。在设置为“on”时,整个文档都会变得可以编辑(显示插入符号),然后就可以像使用字处理软件一样,通过键盘将文本内容加粗、变成斜体,等等。可以给iframe指定一个非常简单的HTML页面作为其内容来源。例如:<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>Document</title></head><body></body></html>这个页面在iframe中可以像其他页面一样被加载。要让它可以编辑,必须要将designMode设置为“on”,但只有在页面完全加载之后才能设置这个属性。因此,在包含页面中,需要使用onload事件处理程序来在恰当的时刻设置designMode,如下面的例子所示:<iframe src="blank.htm" name="richedit" style="height:100px;width:100px" ></iframe><script>EventUtil.addHandler(window,"load",function(){ frames["richedit"].document.designMode = "on";})</script>等到以上代码执行之后,你就会在页面中看到一个类似文本框的可编辑区字段。这个区字段具有与其他网页相同的默认样式;不过,通过为空白页面应用CSS样式,可以修改可编辑区字段的外观。14.5.1使用contenteditable属性另一种编辑富文本内容的方式是使用名为contenteditable的特殊属性,可以把contenteditable属性应用给页面中的任何元素,然后用户立即就可以编辑该元素。这种方法之所以收到欢迎,是因为它不需要iframe、空白页和JavaScript,只要为元素设置contenteditable属性即可。<div class="editable" id="richedit" contenteditable></div>这样,元素中包含的任何文本内容就可以编辑了,就好像这个元素变成了<textarea>元素一样。通过这个元素上设置contenteditable属性,也能打开或关闭编辑模式。var divided= document.getElementById("richedit");div.contentEditable = "true";contenteditable属性有三个可能的值:“true”表示打开、“false”表示关闭、“inherit”表示从父元素那里继承(因为可以在contenteditable元素中创建或删除元素)。支持contenteditable属性的元素有IE、Firefox、Chrome、Safari和Opera。在移动设备上,支持contenteditable属性的浏览器有iOS 5+中的Safari和Android 3+中的WebKit。14.5.2操作富文本与富文本编辑器交互的主要方式,就是使用document.execCommand()。这个方法可以对文档执行预定义的命令,而且可以应用大多数格式。可以为document.execCommand()方法传递3个参数:要执行的命令名称、表示浏览器是否应该为当前命令提供用户界面的一个布尔值和执行命令必须的一个值(如果不需要值,则传递null)。为了确保跨浏览器的兼容性,第二个参数应该始终设置为false,因为Firefox会在该参数为true时抛出错误。不同浏览器支持的预定义命令也不一样,下表列出了那些被支持最多的命令:其中,与剪贴板有关的命令在不同浏览器中的差异极大。Opera根本没有实现任何剪贴板命令,而Firefox在默认情况下会禁用它们(必须修改用户的首选项来启用它们)。Safari和Chrome实现了cut和copy,但没有实现paste。不过,即使不能通过document.execCommand()来执行这些命令,但却可以通过相应的快捷键来实现同样的操作,可以在任何时候使用这些命令来修改富文本区域的外观,如下面的例子所示://转换粗体文本frames["richedit"].document.execCommand("bold",false,null);//转换斜体文本frames["richedit"].document.execCommand("italic",false,null);//创建指向www.wrox.com的链接frames["richedit"].document.execCommand("createlink",fale,"htttp://www.wrox.com");//格式化为1级标题frames["richedit"].document.execCommand("formatblock",false,"<h1>");美滋滋,一开始在360运行老是错误,一开始一个安全错误,百度后说要再apache上运行才可以,在服务器上运行后又报别的错误Illeagel statement,一个根本没有写进网页的js错误,网页也多了很多别的引入js,还以为是apache搞的鬼,后来在Sources看仔细了原来是浏览器自带的JS加载错误好像,因为那个文件夹里都是各种插件的名称,就换了Firefox试,成功了。而且发现Firefox对每个报错都有一个“详细了解”的链接,点进去有解释这个错误是什么意思,woc那不就是学校JS的神器吗。上面的代码要先选中一段文字,然后在调试台里输入才可以,而且不能用在<div>元素上,contenteditable属性才是能在任何元素上用。同样的方法也适用于页面中contenteditable属性为“true”的区块,只要把对框架的引用替换成当前窗口的document对象即可。//转换粗体文本document.execCommand("bold",false,null);//转换斜体文本document.execCommand("italic",false,null);//创建指向www.wrox.com的链接document.execCommand("createlink",false,"htttp://www.wrox.com");//格式化为1级标题document.execCommand("formatblock",false,"<h1>");美滋滋,因为只对contenteditable属性设置为“true”和选中的文本有效,所以不用指定哪个div之类的,直接用document对象发动这个方法就好。需要注意的是,虽然所有浏览器都支持这些命令,但这些命令所产生的HTML仍然有很大不同。例如,执行bold命令时,IE和Opera会使用<strong>标签包围文本,Safari和Chrome使用<b>标签,而Firefox则使用<span>标签。由于各个浏览器实现命令的方式不同,加上它们通过innerHTML实现转换的方式不一样,因此不能指望富文本编辑器会产生一致的HTML。除了命令之外,还有一些与命令相关的方法。第一个方法就是queryCommandEnabled(),可以用它来检测是否可以针对当前选择的文本,或者当前插入字符所在位置执行某个命令。这个方法接收一个参数,即要检测的命令。如果当前编辑区域允许执行传入的命令,这个方法可以返回true,否则返回false。例如:var result = frames["richedit"].document.queryCommandEnabled("bold");这个方法只返回布尔值,不执行。需要注意的是,queryCommandEnabled()方法返回true,并不以为着实际上就可以执行相应命令,而只能说明对当前选择的文本执行相应命令是否合适。例如,Firefox在默认情况下会禁用剪切操作,但执行queryCommandEnabled("cut")也可能会返回true。另外,queryCommandState()方法用于确定是否已将指定命令应用到了选择的文本。例如,要确定当前选择的文本是否已经转换成了粗体,可以使用如下代码:var isBold = frames["richedit"].document.queryCommandState("bold");如果当前已经对选择的文本执行了"bold"命令,那么上面的代码会返回true。一些功能全面的富文本编辑器,正是利用这个方法来更新粗体、斜体等按钮的状态的(就是那种选了粗体了按钮就有“被按下去”了那种效果)。最后一个方法是queryCommandValue(),用于取得执行命令时传入的值(即前面例子中传给document.execCommand()的第三个参数)。例如,在对一段文本应用“fontsize”命令时如果传入了7,那么下面的代码就会返回“7”;var fontsize = frames["richedit"]document.queryCommandValue("fontsize");通过这个方法可以确定某个命令是怎么应用到选择的文本的,可以据以确定再对其应用后续命令是否合适。归结起来,操作富文本的方法就4个:·document.execCommand():就是给文本加各种格式,对框架使用要在最前面加对框架的引用;·queryCommandEnabled():检测是否可以针对当前文本执行某个命令,但是即使有些不能执行也会返回true,所以用处应该不大;·queryCommandState():检测某个方法是否已经被用在了这段文本上;·queryCommandValue():返回执行命令时传入的值。14.5.3富文本选区在富文本编辑器中,使用框架(iframe)的getSelection()方法,可以确定实际选择的文本。这个方法是window对象和document对象的属性,调用它会返回一个表示当前选择文本的Selection对象。每个Selection对象都有下列属性:·anchorNode:选区起点所在的节点。·anchorOffset:在到达选区起点位置之前跳过的anchorNode中的字符数量。·focusNode:选区终点所在节点。·focusOffset:focusNode中包含在选区之内的字符数量。·isCollapsed:布尔值表示选区的起点和终点是否重合。·rangeCount:选区中包含的DOM范围的数量。Selection对象的这些属性没有包含多少有用的信息(美滋滋,反正看不懂)。好在,该对象的下列方法提供了更多信息,并且支持对选区的操作:·addRange(range):将指定的DOM范围(DOM范围是什么?)添加到选区中。·collapse(node,offset):将选区折叠到指定节点中的相应的文本偏移位置。·collapseToEnd():将选区折叠到终点位置。·collapseToStart():将选区折叠到起点位置。·containsNode(node):确定指定的节点是否包含在选区中。·deleteFromDocument():从文档中删除选区中的文本,与document.execCommand("delete",false,null)命令的结果相同。·extend(node,offset):通过将focusNode和focusOffset移动到指定的值来扩展选区。·getRangeAt(index):返回索引对应的选区中的DOM范围。·removeAllRange():从选区中移除所有DOM范围。实际上,这样会移除选区,因为选区中至少要有一个范围。·selectAllChildren(node):清除选区并选择指定节点的所有子节点。·toString():返回选区所包含的文本内容。Selection对象的这些方法都极为实用,它们利用了(第12章讨论的)DOM范围来管理选区。由于可以直接操作选择文本的DOM表现,因此访问DOM范围与使用execCommand()相比,能够对富文本编辑器进行更加细化的控制。例子:var selection = frames["richedit"].getSelection();//取得选择的文本var selectedText = selection.toString();//取得代表选区的范围var range = selection.getRangeAt(0);//突出显示选择的文本var span = frames["richedit"].document.createElement("span");span.style.backgroundColor = "yellow"range.surroundContents(span);以上代码会为富文本编辑器中被选择的文本添加黄色背景。这里使用了默认选区中的DOM范围,通过surroundContents()方法将选区添加到了带有黄色背景的<span>元素中。HTML5将getSelection()方法纳入了标准,而且IE9、Firefox、Safari、Chrome和Opera 8都实现了它。由于历史原因,在Firefox 3.6+中调用document.getSelection()会返回一个字符串。为此,可以在Firefox 3.6中改作调用window.getSelection(),从而返回selection对象。Firefox 8修复了这个Bug。可以用document对象调用这个方法。IE8及更早的版本不支持DOM范围,但我们可以通过它支持的selection对象操作选择的文本。IE中的selection对象是document属性,本章前面曾经讨论过。要取得富文本编辑器中选择的文本,首先必须创建一个文本范围(第十二章),然后再像下面这样访问其text属性:var range = frames["richedit"].document.selection.createRange();var selectedText = range.text;虽然使用IE的文本范围来执行HTML操作并不像使用DOM范围那么可靠,但也不失为一种有效的途径。要像前面使用DOM范围那样实现相同的文本高亮效果,可以组合使用htmlText属性和pasteHTML()方法。var range = frames["richedit"].document.selection.createRange();range.pasteHTML("<span style=\\"background-color:yellow\\">" + range.htmlText + "</span>");以上代码通过htmlText取得当前选区中的HTML,然后将其放在了一对<span>标签中,最后又使用pasteHTML()将结果重新插入到了选区中。14.5.4表单与富文本由于富文本编辑是使用iframe而非表单空间实现的,因此,从技术上说,富文本编辑器并不属于表单。换句话说,富文本编辑器中的HTML不会被自动提交给服务器,而需要我们手工来提取并提交HTML。为此,通常可以添加一个隐藏的表单字段,让它的值等于从iframe中提取出的HTML。具体来说,就是在提交表单之前,从iframe中提取出HTML,并将其插入到隐藏的字段中。下面就是通过·","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"《JavaScript高级程序设计》第十三章 事件","date":"2017-12-05T13:03:44.000Z","path":"2017/12/05/《JavaScript高级程序设计》第十三章-事件/","text":"JavaScript与HTML之间的交互是通过事件实现的。可以使用侦听器(或处理程序)来预订事件,以便事件发生时执行相应代码。这种在传统软件工程中被称为观察员模式的模型,支持页面行为(JavaScript代码)与页面的外观(HTML和CSS代码)之间的松散耦合。IE9、Firefox、Opera、Safari和Chrome全部已经实现了“DOM2级事件”模块的核心部分。IE8是最后一个仍然使用其专有事件系统的主要浏览器。尽管所有浏览器已经实现了“DOM2级事件”,但这个规范本身并没有涵盖任何事件类型。浏览器对象模型(BOM)也支持一些事件,但这些事件与DOM事件之间的关系并不十分清晰。因为BOM事件长期没有规范可以遵循(HTML5后来给出了详细的说明)。13.1事件流事件流描述的 是从页面接收事件的顺序。IE的事件流是事件冒泡流,NetScape Communicator的事件流是事件捕获流。但是书里又说“老版本的浏览器不支持,因此很少有人使用事件捕获,建议读者放心使用事件冒泡,在有特殊需要时再使用事件捕获。”难道事件流是可以选的吗?不是每个浏览器都有特定的事件流? 13.1.3DOM事件流“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段(事件捕获和事件冒泡是并存的?)。多数支持DOM事件流的浏览器都实现了一种特定的行为;即使“DOM2级事件”规范明确要求捕获阶段不会设计事件目标,但IE9、Safari9、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件,ieguo,就是有两个机会在目标对象上操作事件。亲测所有浏览器都是冒泡流。✎:IE8及更早版本不支持DOM事件流。13.2事件处理程序诸如click、load和mouseover,都是事件的名字。而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字以“on”开头,因此click事件的事件处理程序都是onclick,load的事件处理程序就是onload。为事件指定处理程序的方式有好几种。13.2.1HTML事件处理程序就是行内样式的事件处理程序。说说要注意的问题:由于与相应事件处理程序同名的HTML特性(onclick,onmouseover等)的值是JavaScript,因此不能在其中使用未经转义的HTML语法字符,例如和号(&)、双引号(“”)、小于号(<)、大于号(>)。如果想在代码里使用双引号,那么就要将代码写成这样:<input type="button" value = "Click Me" onclick = "console.log(&quot;Clicked&quot;)"/>也可以调用在页面其他地方定义的脚本:<input type="button" value = "Click Me" onclick = "showMessage()"/>这样指定事件处理程序具有一些独到之处。首先,这样会创建一个封装着元素属性值的函数,这个函数中有一个局部变量event,也就是事件对象。(本章稍后讨论):<!– 输出"click"–><input type="button" value="Click Me" onclick="console.log(event.type)">通过event变量,可以直接访问事件对象,你不用自己定义它,也不用从函数的参数列表中读取。在这个函数内部,this值等于事件的目标元素,例如:<!– 输出"Click Me"–><input type="button" value="Click Me" onclick="console.log(this.value)">像上面这样的动态创建的函数,另一个有意思的地方是它扩展作用域的方式,在这个函数内部,可以像访问局部变量一样访问document及该元素本身的成员。这个函数使用with像下面这样扩展作用域:function(){ with(document){ with(this){ //元素属性 } }}如此一来,事件处理程序要访问自己的属性就简单多了。下面这行代码与前面的例子效果相同:<!– 输出"Click Me" –><input type="button" value="Click Me" onclick="console.log(value)">不知所云啊,在一个匿名函数里写两个with语句块,然后就可以用onclick不加this输出"Click Me"了,什么跟什么。不懂。在HTML中指定事件处理程序有三个缺点:第一,存在时差问题,因为用户可能在页面还没加载完成的时候就在页面上触发相应事件,但当时的事件处理程序可能尚不具备执行条件。用前面的例子来说,假设showMessage()函数是在页面最下面的<script>元素定义的,如果用户在页面解析showMessage()之间就单击了按钮,就会引发错误。第二,这样扩展事件处理程序的作用域在不同浏览器中会导致不同结果。不同JavaScript引擎遵循的标识符解析规则略有差异,很可能在访问非限定对象成员时出错。第三,HTML与JavaScript代码紧密耦合。如果要更换事件处理程序,就要改动两个地方:HTML代码和JavaScript代码,这也就是开发人员不喜欢HTML事件处理程序(专业名称),转而使用JavaScript指定事件处理程序的原因所在。13.2.2DOM0级事件处理程序通过JavaScript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。每个元素(包括window和document)都有自己的事件处理程序属性,这些属性通常全部小写,例如onclick,将这种属性设置为一个函数,就可以指定事件处理程序。例子:var btn = document.getElementById("myBtn");btn.onclick = function(){console.log("Clicked");};要注意:这些代码运行以前不会指定事件处理程序,因此如果这些代码在页面中位于按钮后面,就有可能在一段事件内怎么单击都没有反应(直到这个函数被加载到)。使用DOM0级事件指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的this引用当前元素(注意前面HTML事件处理程序的this是等于事件的目标元素)。例子:var btn = document.getElementById("myBtn");btn.onclick = function(){ console.log(this.id); //"myBtn"}一句话:DOM0级事件处理程序中的this引用当前元素。不仅仅是ID。实际上可以在事件处理程序中通过this访问元素的任何属性和方法。以这种方式添加的事件处理程序会在事件流的冒泡阶段(所以在事件捕获阶段不会被触发?什么事件处理程序会在事件捕获阶段触发?)被处理。通过DOM0级方法指定的事件处理程序也可以被删除。例子:btn.onclick = null;✎:如果你使用HTML指定事件处理程序,那么onclick属性的值就是一个包含着在同名HTML特性中指定的代码函数,将onclick属性设置为Null,也可以删除以HTML事件处理程序指定的事件处理程序。13.2.3DOM2级事件处理程序“DOM2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener()和removeEventListener()。所有DOM节点中都包含这两个方法,并且它们都接受3个参数:要处理的事件名,作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。例子:var btn = document.getElementById("btn");btn.document.addEventListener("click",function(){ console.log(this.id);},false);btn.document.addEventListener("click",function(){ console.log("Hello World");},false)上面代码为同一个按钮添加了两个事件处理程序,第一个事件处理程序不会被覆盖,会按照添加它们的顺序触发。注意:通过addEventListener()添加的事件处理程序只能使用removeEventListener()来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过addEventListner()添加的匿名函数将无法移除。例子:btn = document.addEventListener("click",function(){ console.log(this.id);},false);btn.removeEventListener("click",function(){ //没有用! console.log(this.id;)},false)看似第二个参数使用了相同的参数,但实际上,第二个参数与传入addEventListener()中的那一个是完全不同的函数。除非用函数声明或函数表达式指定函数名。例子:var btn = document.getElementById("btn");var handler = function(){ console.log(this.id);};btn.addEventListener("click",handler,false);btn.removeEventListener("click",handler,false) //有效!大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。作者不建议在事件捕获阶段捕获阶段注册事件处理程序。✎:IE9、Edge、Firefox、Safari、Chrome和Opera支持DOM2级事件处理程序。13.2.4IE事件处理程序IE实现了与DOM中类似的两个方法:attachEvent()和detachEvent()。这两个方法接受相同的两个参数:事件处理程序名称和事件处理程序函数。由于IE8及更早版本只支持事件冒泡(前面又说IE8及之前不支持事件流?),所以通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段。attachEvent()使用例子:var btn = document.getElementById("btn");btn.attachEvent("onclick",function(){ console.log("Clicked");})注意,attachEvent()的第一个参数是“onclick”,而非DOM的addEventListener()方法中的“click”;注意:在IE中使用attacuhEvent()与使用DOM0级方法的主要区别在于事件处理程序的作用域。在使用DOM0级方法的情况下,事件处理程序会在其所谓元素的作用域内运行;在使用attachEvent()方法的情况下,事件处理程序会在全局作用域中运行,因此this等于window。证明:var btn = document.getElementById("btn");btn.attachEvent("onclick",function(){ console.log(this === window); //true})奇怪IE11竟然不支持attachEvent()。牢底addEventListner()和attachEvent()的作用域不同很重要。好蛋疼啊啊啊啊,attachEvent()也支持给一个元素的一个事件添加多个事件处理程序,但是触发的顺序与addEventListener()相反wocccccc。但是同样的,attachEvent()无法移除匿名函数,只有添加相同的函数引用的视口可以删除。13.2.5跨浏览器的事件处理程序作者要创建一个叫addHandler()的函数来解决跨浏览器问题,但是他说这个函数是属于EventUtil对象的就有点懵了,EventUtil对象是作者定义的,还是JavaScript有的?百度了一下,只是习惯上我们把跨浏览器事件处理程序的函数放在这个EventUtil对象里而已。例子:var EventUtil = { addHandler:function(element,type,handler){ if(element.addEventListener){ element.addEventListener(type,handler,false); }else if(element.attachEvent){ element.attachEvent("on"+type,handler); }else{ element["on"+type] = handler; //事件是元素的一个属性,type是个变量,用变量引用属性要用方括号,第五章的知识点方括号的用法 } }, removeHandler:function(element,type,handler){ if(element.removeEventListener){ element.removeEventListener(type,handler,false); }else if(element.detachEvent){ element.detachEvent("on"+type,handler); }else{ element["on"+type] = null; } }};如果DOM2级方法可以用就用DOM2级方法,如果不行就用IE的方法,此时的事件类型要加“on”前缀。最后一种可能是DOM0级方法。但是作者说在现代浏览器几乎不可能进入到第三种方法的,突然觉得很爽。不使用DOM0级而使用DOM2级的原因是DOM0级对每个事件只支持一个事件处理程序。13.3事件对象在触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。例如,鼠标操作导致的事件对象中,会包含鼠标位置的信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。所有浏览器都支持event对象,但支持方式不同。13.3.1DOM中的事件对象兼容DOM的浏览器会将一个event对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法(DOM0级或DOM2级),都会传入event对象。例子:var btn = document.getElementById("btn");btn.onclick = function(event){ console.log(event.type); //"click"};btn.addEventListener("click",function(event){ console.log(event.type); //"click"},false);DOM0级,2级中都有event对象。通过HTML特性指定事件处理程序时,变量event中保存着event对象(体会一下这句话啊,太专业了,event只是个变量,是个指针,指向event对象)。例子:<input type="button" value="Click Me" onclick="console.log(event.type)"/>event对象包含与创建它的特定事件有关的属性和方法。触发的类型不一样,event对象的属性和方法不一样。不过,下面的属性和方法都是所有事件都有的:注意:event对象的这些属性和方法都是只读的。在事件处理程序内部,currentTarget的值始终等于对象this,而target则只包含事件的实际目标(鼠标实际点的元素)。如果直接将事件处理程序指定给了目标元素,则this、currentTarget和target包含相同的值。例子:var btn = document.getElementById("btn");btn.onclick = function(event){ console.log(event.currentTarget === this); //true console.log(event.target === this); //true}这个例子检测了currentTarget、target和this的值。由于click事件的目标是按钮,因此这三个值是相等的。现在假设事件处理程序存在于按钮的父节点中(例如document.body),那么这些值是不相同的,例子://假设点击了btn按钮document.body.onclick = function(event){ console.log(event.currentTarget === document.body); //true console.log(this === document.body); //true console.log(event.target === document.getElementById("btn")); //ture}单击按钮时,this和current.target都等于document.body,因为事件处理程序都注册在这个元素上,然而target属性却等于按钮元素,因为target只包含事件的实际目标。只是由于按钮上没有注册事件处理程序,结果click事件冒泡到了document.body,在那里鼠标点击事件才被得到处理(体会这句话,我们发出了一个click事件,在document.body做出了响应,“处理”了这个click事件)。当一个函数需要在多种事件下使用时,可以使用event对象的type属性(被触发的事件类型)。例如:var btn = document.getElementById("btn");var handler = function(event){ switch(event.type){ case "click": console.log("Clicked"); break; case "mouseover": event.target.style.backgroundColor = "red"; break; case "mouseout": event.target.style.backgroundColor = ""; break; }};btn.onclick = handler;btn.onmouseover = handler;btn.onmouseout = handler;刚刚用随机+间歇调用+访问样式表规则随机改变<body>背景颜色,很嗨。要阻止特定事件的默认行为,可以使用preventDefault()方法。例如,链接的默认行为就是在被单击时会导航到其href特性指定的URL。如果你想取消这个默认行为,可以通过链接的onclick事件处理程序取消它,例子:var link = document.getElementById("myLink");link.onclick = function(event){ event.preventDefault();};只有event对象的cancelable属性(表明是否可以取消事件的默认行为)设置为true事件,才可以使用preventDefault()来取消默认行为(亲测这个属性的默认值是true);另外,stopPropagation()方法用于立即停止事件在DOM层次中的传播,即取消进一步的事件捕获或冒泡(冒泡捕获都取消!)。记住这两个都是event对象的方法,所以调用是用event.preventDefault()/event.stopPropagation()。事件对象的eventPhase属性可以用来确定事件当前正处于事件流的哪个阶段。如果处理程序是在捕获阶段调用的,eventPhase返回1,如果是在目标对象上被调用,eventPhase属性返回2,如果是在冒泡阶段调用的,eventPhase属性返回3。当eventPhase等于2时,this、target、currentTarget的值始终都相等。✎:只有在事件处理程序执行期间,event对象才会存在;一旦事件处理程序执行完成,event对象就被销毁。13.3.2IE中的事件对象IE好烦啊啊啊啊。要访问IE中的event对象有几种不同的方式,取决于指定事件处理程序的方法(DOM0级,2级,HTML特性指定)。用DOM0级方法添加事件处理程序时,event对象是作为window对象的一个属性存在。例子:var btn = document.getElementById("btn");btn.onclick = function(){ var event = window.event; console.log(event.type); //"Click"};如果事件处理程序是通过DOM2级方法添加的,那么就会有一个event对象作为参数被传入事件处理程序函数中。例子:var btn = document.getElementById("btn");btn.attachEvent("onclick",function(event){ console.log(event.type);})其他浏览器也是有个event对象作为参数传入啊,可能区别于DOM0级,DOM2级的event对象也可以通过window对象访问,不过为方便起见,同一个对象也会作为参数传递。如果是通过HTML特性指定的事件处理程序,那于其他浏览器也是无区别:<input type="button" value="Click Me" onclick="console.log(event.type)"/>IE的event对象同样也包含与创建它的事件相关的属性和方法。其中很多属性和方法都有对应的或者相关的DOM属性和方法(前面的event对象属性和方法是规范的DOM规定的,IE搞了自己的一套,神烦)。属性和方法因为事件类型的不同而不同,但与DOM的event对象一样都包含下列的属性和方法:因为事件处理程序的作用域是根据指定它的方式来确定的,所以this不可靠,最好使用event.srcElement比较保险。例子:var btn = document.getElementById("btn");btn.onclick = function(){ console.log(window.event.srcElement === this); //true}btn.attachEvent("onclick",function(event){ console.log(event.srcElement === this); //false})回顾一下,DOM0级(IE和其他浏览器的DOM0级通用)的this指向当前元素,DOM2级的this也指向当前元素,但是IE的attachEvent()(不能说这个方法是DOM2级方法),如果是在全局环境调用,则this是指向window。如果第二个判断是window === event.srcElement,则返回true。如前所述,returnValue属性相当于DOM中的preventDefalut()方法。只要将returnValue属性设置为false,就可以阻止默认行为。相应地,cancelBubble属性与DOM中的stopPropagation()方法作用相同,都是用来停止事件冒泡的。因为IE不支持事件捕获,因而只能取消事件冒泡,但是stopPropagation()可以同时取消事件捕获和冒泡。13.3.3跨浏览器的事件对象简而言之,根据DOM和IE中event对象的相似性,拿出跨浏览器的方案,仍然写在前面的EventUtil对象中,增强EventUtil对象。例子:var EventUtil = { addHandler:function(element,type,handler){ //忽略的代码 }, removeHandler:function(element,type,handler){ //忽略的代码 }, getEvent:function(){ return event ? event : window.event; }, getTarget:function(){ return event.target || event.srcElement; } preventDefault:function(){ if(event.preventDefault){ event.preventDefault(); }else{ event.returnValue = false; } }, stopPropagation:function(){ if(event.stopPropagation){ event.stopPropagation(); }else{ event.cancelable = true; } }};直接下一节走起。13.4事件类型这节开始讲解各种不同的事件类型。下面的表是“DOM3级事件”规定的几类事件:除了“DOM3级事件”定义了事件,HTML5也定义了一些事件,而有些浏览器还会在BOM和DOM中实现其他专有事件。这些专有事件没什么规范,因此不同浏览器之间的实现可能不一致。13.4.1UI事件UI事件(User Interface)指的是那些不一定与用户操作有关的事件。现有的UI事件如下:·load:当页面完全加载后在window上触发的,当所有框架都加载完毕时在框架上面触发,当图像加载完在<img>元素上触发,当嵌入的内容加载完毕在<object>元素上面触发。(只有页面,框架,图像,嵌入的内容有这个事件)·unload:当页面完全卸载后在window上触发,当所有框架都卸载后在框架上触发,当嵌入的内容卸载完毕后在<object>上触发。(图像没unload事件)·abort:在用户停止下载进程,如果嵌入的内容没加载完,则在<object>元素上触发。·error:当发生JavaScript错误时在window上触发,当无法加载图像时在<img>上触发,当无法加载嵌入内容时在<object>元素上触发,或者当有一或多个框架无法加载时在框架上触发。·select:当用户选择文本框(<input>或<textarea>)中的一个或多个字符时触发。·resize:当窗口或框架的大小变化时在window上触发。·scroll:当用户滚动带滚动条的元素中的内容时,在该元素上触发。<body>元素中包含加载页面的滚动条。这些事件在DOM2级事件中都归为HTML事件(所以这些到底是DOM3级事件还是DOM2级事件?)要确定浏览器是否支持DOM2级事件规定的HTML事件,可以使用如下代码:var isSupported = document.implementation.hasFeature("HTMLEvents","2.0");注意,只有根据“DOM2级事件”实现这些事件的浏览器才会返回true。要确定浏览器是否支持“DOM3级事件”,可以使用如下代码:var isSupported = document.implementation.hasFeature("UIEvent","3.0");1.load事件window上面的load事件,会在页面完全加载后(包括所有图像、JavaScript文件、CSS文件等外部资源),就会触发window上面的load事件。有两种定义load事件处理程序的方法,一种是通过JavaScript指定事件处理程序:EventUtil.addHandler(window,"load",function(event){ console.log("Loaded");});load事件的event对象不包含有关这个事件的任何附加信息,除了在兼容DOM的浏览器中,event,target属性的值会被设置为document,IE不会为这个事件设置srcElement属性。第二种指定onload事件处理程序的方式是为<body>元素添加一个onload特性,例子:<body onload="concole.log(‘Hello World’)">作者建议读者尽可能使用JavaScript方式。✎:根据“DOM2级事件”规范,应该在document而非window上触发load事件,但是,所有浏览器都在window上面实现了该事件,以确保向后兼容。图像上定义load事件处理程序的方式跟在window上一样。要讲的一点是,在用JavaScript创建<img>元素时,可以为其指定一个事件处理程序,此时,最重要的是在指定src属性之前先指定事件。例子:EventUtil.addHandler(window,"load",function(){ var image = document.createElement("img"); EventUtil.addHandler(image,"load",function(event){ event = EventUtil.getEvent(event); console.log(EventUtil.getTarget(event).src); //注意这里面的东西都是在加载完后执行的,所以可以成功返回图像src而不是空的 }); document.body.appendChild(image); image.src = "3.jpg"; //先定义事件再指定src属性})创建<img>元素要在window的load事件里完成,原因在于,我们是想向DOM中添加一个新元素,所以必须确定页面加载完毕——如果在页面加载前操作document.bod会导致错误。还有一定要注意的:新图像元素不一定要从添加到文档才开始下载,只要设置了src属性就会开始下载(虽然这个例子是先添加到文档再设置src)。创建<img>还有第二种方法:使用DOM0级的image对象。在DOM出现之前,开发人员经常使用Image对象在客户端预先加载图像(历史)。可以像使用<img>元素一样使用Image对象,只不过无法将其添加到DOM树中。例子:EventUtil.addHandler(window,"load",function(){ var image = new Image(); EventUtil.addHandler(image,"load",function(event){ console.log("Image loaded"); console.log(image.src) }); image.src = "1.jpg";})有显示图像加载完成,也给出正确的src值,然而图像并没有在网页中显示,那有什么用?✎:在不属于DOM文档的图像(包括未添加到文档的<img>元素和Image对象)上触发load事件时,IE8及之前的版本不会生成event对象,IE9修复了这个问题。还有其他的元素也以非标准的方式支持load事件,比如<script>元素在五大浏览器也会触发load事件,与图像不同,只有在设置了<script>元素的src属性并将该文档添加到文档后,才会开始下载JavaScript文件,也就是说,对<script>元素而言,指定src属性和指定事件处理程序的先后顺序并不重要。例子:EventUtil.addHandler(window,"load",function(){ var script = document.createElement("script"); EventUtil.addHandler(script,"load",function(event){ console.log("loaded"); }); script.src = "example.js"; document.body.appendChild(script);})IE8及之前的版本不支持<script>元素上的load事件。用JavaScript创建的<script>元素中的load事件中的event对象的target属性在大多数浏览器中都是引用<script>节点,而在Firefox 3之前的版本中,引用的则是document。IE和Opera还支持<link>元素上的load事件,与<script>元素类似,未指定href属性并将<link>元素添加到文档之前也不会开始下载样式表。(亲测所有的浏览器除了Safari都支持在<link>上调用load事件处理程序)搞清楚这节在讲什么兄弟,这节讲的是能不能在<script>和<link>元素上调用load事件处理程序,不是能不能创建<link>、<script>元素。搞清楚,当然可以创建元素了。2.unload事件只要用户从一个页面切换到另一个页面,就会发生unload事件(惊了!我以为是关掉网页才会发生unload事件!)。而利用unload事件最多的情况就是解除引用,以避免内存泄漏。使用方式与使用load事件方式相同。✎:根据“DOM2级事件”规范,应该在<body>元素而非window上触发unload事件,但是,所有浏览器都在window上面实现了该事件,以确保向后兼容。亲测切换页面并不会触发unload事件啊,但是在刷新的瞬间会触发,所以每次重新加载网页的瞬间之前的网页都会被卸载吧。3.resize事件当浏览器的高宽度被重新调整的时候就会触发resize事件,这个事件在window上面触发,因此可以通过JavaScript或者<body>元素中的onresize特性来指定事件处理程序。如前所述,作者还是推荐我们使用JavaScript方式。在兼容DOM的浏览器中,传入resize事件处理程序中的event对象有一个target属性,值为document,而IE8及之前版本不会提供任何属性。各浏览器触发resize事件的机制还有不同:IE、Safari、Chrome和Opera会在浏览器窗口变化了1px时就触发resize事件,Firefox则会在用户停止调整窗口大小时才触发resize事件。由于这个差别,应该注意不要在这个事件的处理程序中加入大计算量的代码,因为这些代码可能被反复执行,导致浏览器反应明显变慢。✎:浏览器窗口最小化和最大化时也会触发resize事件。4.scroll事件虽然scroll事件是在window对象上发生的,但它实际表示的则是页面中相应元素的变化(不懂啊这句)。在混杂模式下,可以通过<body>元素的scrollLeft和scrollTop来监控这一变化;而在标准模式下,除Safari之外的所有浏览器都会通过<html>元素来反映这一变化(Safari仍然基于<body>元素跟踪滚动位置)。例子:EventUtil.addHandler(window,"srcoll",function(){ if(document.compatMode == "CSS1Compat"){ console.log(document.documentElement.scrollTop); }else{ console.log(document.body.scrollTop); }})然而运行这段代码滚动网页什么事都没发生是什么情况。与resize事件类似,scroll事件也会在文档被滚动期间重复被触发,所有有必要保持事件处理程序代码的简单。13.4.2焦点事件焦点事件hi在页面元素获得或失去焦点时触发。利用下面那些属性与hasFocus()事件及document.activeElement属性(回顾下什么是activeElement属性,见第十一章DOM扩展)配合,可以知晓用户在页面上的行踪,有以下6个焦点事件:·blur:元素失去焦点时触发,这个事件不会冒泡;所有浏览器都支持。·DOMFocusIn:在元素获得焦点时触发,这个事件与HTML事件focus等价,但它冒泡。只有Opera支持这个属性。DOM3级废弃了DOMFocusIn,选择了focusin。·DOMFocusOut:在元素失去焦点时触发,失去焦点的事件没有冒不冒泡这个问题。这个事件是HTML事件blur的通用版本。只有Opera支持。DOM3级也废弃了它,选择了focusout。·focus:元素获得焦点时触发,不冒泡,所有浏览器都支持。·focusIn:元素获得焦点时触发,与HTML事件focus等价,冒泡。支持这个事件的浏览器有IE5.5+、Safari5.1+、Opera 11.5+和Chrome。·focusout:元素失去焦点时触发,是HTML事件blur的通用版本,支持这个事件的浏览器有IE5.5+、Safari5.1+、Opera 11.5+和Chrome。这一类事件中最主要的两个是blur和focus。他们都是JavaScript早期就得到浏览器支持的事件。最大的问题是不冒泡(我怎么觉得不冒泡挺好的)。因此IE才定义了focusin和focusout,并被DOM3级事件采纳为标准方式。当焦点从页面中的一个元素移动到另一个元素,会依次触发下列事件:要确定浏览器是否支持那些事件,可以使用如下代码:var isSupported = document.implementation.hasFeature("FocusEvent","3.0");✎:即使focus和blur不冒泡,也可以在捕获阶段侦听到它们。13.4.3鼠标与滚轮事件DOM3级定义了9个鼠标事件:·click:在用户单击鼠标左键,或按下回车键时触发。所以onclick事件既可以在鼠标上也可以在键盘上触发。·dbclick:双击。DOM3级将其纳入了标准。·mousedown:用户按下任意鼠标按钮时触发。·mouseenter:鼠标光标从元素外部首次移动到元素范围之内时触发。不冒泡。而且光标移动到后代元素上不会触发。DOM3级事件将它纳入标准。IE、Opera和Firefox支持这个事件。·mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发。不冒泡。光标移动到后代元素也不会触发。DOM3级事件将它纳入标准。IE、Opera和Firefox支持这个事件。·mousemove:鼠标指针在元素内部移动时重复地触发。·mouseout:在鼠标位于一个元素上方,然后用户将其移入另一个元素时触发。另一个元素可以是前一个元素的外部,也可以是这个元素的子元素。·mouseover:在鼠标指针位于元素外部,然后用户将光标首次移入另一个元素边界之内时触发。·mouseup:在用户释放鼠标按钮时触发。页面上的所有元素都支持鼠标事件,除了mouseenter和mouseleave,所有鼠标事件都会冒泡。只有在同一个元素上相继触发了mousedown和mouseup事件,才会触发click事件;如果mousedown和mouseup中的一个被取消(这里的取消并不是说把mouseop/down设置为null,因为亲测设置为null后click事件还是会触发,那么什么是取消就不清楚了。),就不会触发click事件。要检测浏览器是否支持以上DOM2级事件(除dbclick、mouseenter和mouseleave之外):var isSupported = document.implementation.hasFeature("MouseEvents","2.0");要检测浏览器是否支持以上所有事件,可以使用:var isSupported = document.implementation.hasFeature("MouseEvent","2.0"); //DOM3级事件的feature名是“MouseEvent”,而非“MouseEvents”鼠标滚轮也有一个事件,mousewheel事件。这个事件跟踪鼠标滚轮,类似于Mac的触控板。1.客户区坐标位置鼠标事件就是在浏览器视口中的特定位置发生的。鼠标事件的位置信息保存在事件对象的clientX和clientY属性中。所有浏览器都支持这两个属性。他们的只表示事件发生时鼠标指针在视口中的水平和垂直坐标。图展示了视口中客户区坐标位置的含义:可以使用类似下面的代码取得鼠标事件的客户端坐标信息:var div = document.getElementById("myDiv");EventUtil.addHandler(div,"click",function(event){ event = EventUtil.getEvent(event); console.log(event.clientX+","+event.clientY);})当用户点击一次元素,就会看到事件的客户端坐标信息。注意:这些值不包括页面滚动的距离,因此这个位置并不表示鼠标在页面上的位置。鼠标在页面上的位置要用下面这个属性获得。2.页面坐标位置通过事件对象的pageX和pageY属性,能告诉你事件在页面中的什么位置发生。这两个属性表示鼠标光标在页面中的位置,因此坐标是从页面本身而非视口的左边和顶边计算的。以下代码可以取得鼠标事件在页面的坐标:var div = document.getElementById("myDiv");EventUtil.addHandler(div,"click",function(event){ event = EventUtil.getEvent(event); console.log(event.pageX+","+event.pageY); //其实就是上面的代码改一下属性})页面没有滚动的情况下,pageX和pageY的值与clientX和clientY的值相等。IE及更早版本不支持事件对象上的页面坐标,不过使用客户区坐标和滚动信息可以计算出来这两个值。这就需要用到scrollLeft和scrollTop属性了。计算过程如下:var div = document.getElementById("myDiv");EventUtil.addHandler(div,"click",function(event){ event = EventUtil.getEvent(event); var pageX = event.pageX, pageY = event.pageY; if(pageX === undefined){ pageX = event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft); } if (pageY === undefined) { pageY = event.clientY + (document.body.scrollTop || document.documentElement.scrollTop); } console.log(pageX+","+pageY);})混杂模式下的scrollLeft和scrollTop用document.body访问,标准模式用document.documentElement。3.屏幕坐标位置顾名思义。通过screenX和screenY属性可以确定鼠标事件发生时鼠标指针相对于整个屏幕的坐标信息。如图:怎么获取这两个坐标的代码就不写了,好傻啊。4.修改键修改键就是Shift、Ctrl、Alt和Meta(Windows中是windows键,苹果中是Cmd键)。通过修改键组合鼠标事件会影响到所要采取的行为。DOM为此规定了4个属性,表示这些修改键的状态:shiftKey、ctrlKey、altKey和metaKey。这些属性中包含的都是布尔值,如果相应的键被按下,则值为true。否则为false。当某个鼠标事件发生时,通过检测这几个属性就可以确定用户是否同时按下了其中的键。例子:var div = document.getElementById("myDiv");EventUtil.addHandler(div,"click",function(event){ event = EventUtil.getEvent(event); var leys = new Array(); if (event.shiftKey) { keys.push("Shift"); } if (event.ctrlKey) { keys.push("Ctrl"); } if (event.altKey) { keys.push("Alt"); } if (event.metaKey) { keys.push("Meta"); } console.log("Keys"+keys.join(","));});其实这个就是修改键的用法,通过判断4个属性的值是不是真的,真的就触发if里的事件处理程序(不能说触发事件,不严谨,是里面的事件处理程序)。✎:IE9、Firefox、Safari、Chrome和Opera支持这4个键,IE8及之前的版本不支持metaKey属性。5.相关属性在发生mouseover和mouseout事件时,还会涉及更多元素。这两个元素都会涉及把鼠标指针从一个元素的边界移动到另一个元素的边界之内。对mouseover事件而言,事件的主目标是获得光标的元素,而相关元素就是失去坐标的那个元素。类似地,对mousout而言,事件的主目标是失去光标的元素,而相关元素则是获得光标的元素。DOM通过event对象的relatedTarget属性提供了相关元素的信息。这个属性只对mouseover和mouseout才包含值,其他事件这个属性的值是null。IE8之前不支持这个属性但是提供了保存着同样信息的不同属性。在mouseover事件触发时,IE的fromElement属性中保存相关元素;在mouseout事件触发时,IE的toElement保存着相关元素。IE9支持所有这些属性。可以把下面这个跨浏览器取得相关元素的方法添加到EventUtil事件中:var EventUtil = { //忽略其他代码 getRelatedTarget:function(event){ if (event.relatedTarget) { return event.getRelatedTarget; } else if(event.toElement){ return event.toElement; } else if(event.fromElement){ return event.fromElement } else{ return null; } } //忽略其他代码}可以像下面这样使用EventUtil.getRelatedTarget()方法:var div = document.getElementById("myDiv");EventUtil.addHandler(div,"mouseout",function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); var relatedTarget = EventUtil.getRelatedTarget(event); console.log("Mouse out of" + target.tagName + "to" + relatedTarget.name);});有BUG啊这个代码,relatedTarget是undefined。6.鼠标按钮就是在mousedown和mouseup事件的event对象中有一个button属性,根据按的值不同有不同的属性值:在DOM中,0表示主鼠标按钮(左键),1表示中间鼠标按钮,2表示次鼠标按钮。IE8及之前版本的button属性,但与DOM的buttond的取值有很大差异:好奇设置这个属性意义何在,用定义的9个鼠标事件已经够多了吧。作者还是做了跨浏览器处理,因为DOM和IE的鼠标按钮的属性是同名的,所以使用能力检测无法确定差异,作者另辟蹊径,利用支持DOM版鼠标事件的浏览器可以通过hasFeature()方法来检测,所以可以再为EventUtil对象添加如下getButton()方法。var EventUtil = { //忽略其他代码 getButton:function(event){ if (document.implementation.hasFeature("MouseEvents","2.0")) { return event.button; } else { switch(event.button){ case 0: case 1: case 3: case 5: case 7: return 0; case 2: case 6: return 2; case 4: return 1; } } } //忽略其他代码}使用方法:var div = document.getElementById("myDiv");EventUtil.addHandler(div,"mousedown",function(event){ event = EventUtil.getEvent(event); console.log(EventUtil.getButton(event));});亲测可用。记住button属性几乎算是mousedown和mouseup才有的属性,其他事件的button属性值都是null。7.更多的事件信息作者一句这些属性用处不大,一方面只有IE支持,另一方面提供的信息没有什么价值,可以通过其他方式计算得来,就拉闸。8.鼠标滚轮事件mousewheel事件,一些网站的年终总结什么的滚一次轮一个画面可能用到这个事件。与mousewheel事件对应的event对象除包含鼠标事件的所有标准信息外,还包含一个特殊的wheelDelta属性。当用户向前滚动鼠标滚轮时,wheelDelta是120的倍数;当用户向后滚动鼠标滚轮时,wheelDelta是-120的倍数。将mousewheel事件处理程序指定给页面中的任何元素或document对象,即可处理鼠标滚轮的交互操作。例子:EventUtil.addHandler(document,"mousewheel",function(event){ event = EventUtil.getEvent(event); console.log(event.wheelDelta);});支持mousewheel事件的只有Opera、IE、Chrome和Safari,这次特立独行的是Firefox,在Firefox中与mousewheel事件类似的是DOMMouseScroll事件。有关鼠标滚轮的信息保存在detail属性中,再有不同的是,当向前滚动鼠标滚轮时,这个属性的值是-3的倍数,而向后滚动鼠标滚轮时,属性的值是3的倍数(与mousewheel事件方向相反)。所以还是要给出跨浏览器的方案,第一步就是创建一个能取得鼠标滚轮增量值(delta)的方法。下面是添加到EventUtil对象中的方法:var EventUtil = { //忽略其他代码getWheelDelta:function(event){ if (event.wheelDelta) { return event.wheelDelta; } else{ return -event.detail40; } } //忽略其他代码}书中有一步是为了兼容Opera9.5之前的版本的,没写在里面。调用这个方法的方式和之前给的例子不一样,作者用立即调用函数,搞不懂。例子:(function(){ function handleMouseWheel(event){ event = EventUtil.getEvent(event); var delta = EventUtil.getWheelDelta(event); console.log(delta); } EventUtil.addHandler(document,"mousewheel",handleMouseWheel); EventUtil.addHandler(document,"DOMMouseScroll",handleMouseWheel);})();作者说把代码放在一个私有作用域中,是不让新定义的函数干扰全局作用域。定义的handleMouseWheel()函数可以用作两个事件的处理程序(如果指定的事件不存在,则为该事件指定处理程序的代码就会静默地失败)。9.触摸设备10.无障碍性问题要考虑屏幕阅读器设备,尽量不使用onmousedown和onmouseover在操作功能上。牛逼,无缝截图。13.4.4键盘与文本事件“DOM2级事件”最初规定了键盘事件,但在最终定稿之前又删除了相应的内容。结果,对键盘事件的支持主要遵循的是DOM0级。“DOM3级事件”为键盘事件指定了规范,IE9率先实现了该规范。其他浏览器也在着手实现这一标准,但仍然有很多遗留问题(F*k)。键盘事件有三个,如下:·keydown:当用户按下任意键时触发,如果按住不放,就重复触发。·keypress:当用户按下字符键时触发,按住不放重复触发。按下ESC也会触发这个事件。·keyup:用户释放键盘上的键时触发。虽然所有元素都支持以上3个事件,但只有在用户通过文本框输入文本时最常用到。只有一个文本事件:textInput。这个事件是对keyPress的补充,用意是将文本显示给用户之前更容易拦截文本。在文本插入文本框之前会触发textInput事件。触发顺序:当用户按下一个键盘上的字符键,先触发keydown,再触发keypress事件,最后触发keyup事件。keydown和keypress都是在文本框发生变化之前被触发的;而keyup事件则是在文本框已经发生变化后触发的。如果用户按下一个键不放,就会重复触发keydown和keypress事件。✎:键盘事件与鼠标事件一样,都支持相同的修改键,而且,键盘事件的事件对象中也有shiftKey、ctrlKey、altKey、和metaKey属性。IE不支持metaKey。1.键码(keyCode)在发生keydown和keyup事件时,event对象的keyCode属性会包含一个代码,与键盘上的一个特定的键对应。对数字字母字符键,keyCode属性的值与ASCII码中对应小写字母或数字的编码相同。因此,数字键7的keyCode值为55(what!?),而字母A键的keyCode的keyCode值为65——与Shift键的状态无关。例子:var textbox = document.getElementById("myText");EventUtil.addHandler(textbox,"keyup",function(event){ event = EventUtil.getEvent(event); console.log(event.keyCode);})keyCode属性DOM和IE都支持,终于有个大家都开心的属性了。然而陈某持续按着一个键不放,却只打印出一个keyCode。还有发现删除键也是有keyCode码的,是8.下面是所有非字符键的键码:无论keydown或keyup事件都会存在一些特殊情况。在Firefox和Opera中,按分号键时keyCode值为59,也就是ASCII中分号的编码;但IE和Safari返回186,即键盘中按键的键码。2.字符编码(charCode)发生keypress事件意味着按下的键会影响到屏幕中文本的显示。在所有浏览器中,按下能够插入或删除字符的键都会触发keypress事件(删除键也可以触发keypress?亲测不可以,是理解有问题吗);按下其他键能否触发此事件因浏览器而异。IE9、Firefox、Chrome和Safari的event对象都支持一个charCode属性,这个属性只有在发生keypress事件时才包含值,而且这个值是按下的那个键所代表的ASCII编码。此时的keyCode通常等于0或者也可能等于所按键的键码。IE8及之前版本和Opera则是在keyCode中保存字符的ASCII编码,要想以跨浏览器的方式取得字符编码,必须首先检测charCode属性是否可用,如果不可用则使用keyCode(意思就是这两个浏览器里event对象没有charCode属性咯,所以又要有跨浏览器方案),如下面的例子所示:var EventUtil = { //忽略其他代码getCharCode:function(event){ if (typeof event.charCode == "number") { return event.charCode; } else { return event.keyCode; } } //忽略其他代码}下面是使用示例,利用String.fromCharCode()方法将字符编码转换为实际字符,喜欢看大写字母所以又用toUpperCase()转换成大写。var textbox = document.getElementById("myText");EventUtil.addHandler(textbox,"keypress",function(event){ event = EventUtil.getEvent(event); console.log(String.fromCharCode(event.charCode).toUpperCase());})3.DOM3级变化在DOM3级事件的键盘事件中,不再包含charCode属性,而是包含两个新属性:key和char。其中,key属性是为了取代keyCode属性新增的,它的值是一个字符串。在按下某个字符键时,key的值就是相应的文本字符(如“k”或“M”);在按下非字符键时,key的值是相应键名(如“Shift”或“Down”)。而char属性在按下字符键时的行为与Key相同,但在按下非字符键时值为null。IE9支持key属性,不支持char属性。Safari 5和Chrome支持名为keyIdentifier的属性,在按下非字符键(例如Shift)的情况下与key的值相同。对于字符键,keyIdentifier返回一个格式类似“U+0000”的字符串,表示Unicode值。因为存在跨浏览器问题,所以作者不推荐使用key、keyIdentifier或char。(DOM0,2,3级只是一种规范,对各大浏览器的建议,有几个浏览器实现又是一回事。)DOM3级还有一个location属性,也是兼容性问题作者不推荐。根据属性的值让人知道用的是键盘左侧的修改键还是右侧的修改键或者手柄,移动设备键盘。最后是一个getModifierState()方法。用来检测修改键有没有被按下,有返回true,没有返回false。IE9是唯一支持这个方法的浏览器。4.textInput事件“DOM3级事件”规范中引入了一个新事件,名叫textInput。根据规范,当用户在可编辑区域中输入字符时,就会触发这个事件。textInput事件跟keypress事件有两个:·keypress事件可以在任何元素上触发,textInput事件只可以在可编辑区域触发。·textInput事件只会在用户按下实际字符键时才会被触发,而keypress事件则在按下那些能够影响文本显示的键时也会触发(例如退格键)。textInput事件主要考虑的是字符,因此它的event对象中还包含一个data属性,这个属性的值就是用户输入的字符(字符,不是字符编码)。也就是说,当用户按下“s”键时,data的值就是"s",如果用户开了大写输入“S”,data的值就是“S”。另外还有一个属性叫inoutMethod,表示把文本输入到文本框中的方式:使用这个属性就能知道文本是怎么输入到文本框的。支持textInput属性的浏览器有IE 9+、Safari和Chrome。只有IE支持inoutMethod属性。5.设备中的键盘事件P38413.4.5复合事件用于处理ME的输入序列,例如,使用拉丁文键盘的用户通过IME照样能输入日文字符。IME通常需要按住多个键。IE9+是到2011年唯一支持复合事件的浏览器,那就玩蛇。13.4.6变动事件DOM2级的变动(mutation)事件能在DOM中的某一个部分发生变化时给出提示。变动事件是为XML或HTML DOM设计的,并不特定于某种语言(不限于JavaScript)。DOM2级定义了如下变动事件:没怎么接触过的事件真是不想写啊。使用下列代码检测浏览器是否支持变动事件:var isSupported = document.implementatio.hasFeature("MutationEvents","2.0");竟然所有浏览器都支持。IE在IE9开始支持。下表列出不同浏览器对不同变事件的支持情况:因为DOM3级作废了很多变动事件(就剩这三个?)所以只介绍将来会支持的事件。1.删除节点在使用removeChild()或replaceChild()从DOM中删除节点时,首先会触发DOMNodeRemoved事件,这个事件的目标是(evnent.target)被删除的节点,而event.relatedNode属性中包含着对目标节点父节点的引用。在这个事件触发时,节点尚未从其父节点删除,因此其parentNode属性仍然指向父节点(与event.relatedNode相同)。这个事件会冒泡,因而可以在DOM的任何层次上面处理它。如果被删除的节点包括子节点,那么在其所有子节点及这个被移除的节点上会相继触发DOMNodeRemoevedFromDocument事件。但这个事件不会冒泡。所以只有直接指定给其中一个子节点的事件处理程序才会被调用。这个事件的目标是相应的子节点或者那个被移除的节点,除此之外event对象不包含其他信息。紧随其后触发的是DOMSubtreeModified事件这个事件的目标是被移除节点的父节点;此时event对象也不会特工与事件相关的信息。2.插入节点在使用appendChild()、replaceChild()或insertBefore()向DOM中插入节点时,首先会触发DOMNodeInserted事件。这个事件的目标是被插入的节点,而event.relatedNode属性中包含一个对父节点的引用。在这个事件触发时,节点已经被插入到了新的父节点中。这个事件是冒泡的,因此可以在DOM的各个层次上处理它。紧接着,会在新插入的节点上触发DOMNodeInsertedIntoDocument事件。这个事件不会冒泡,因此必须在插入节点前为它添加这个事件处理程序。这个事件的目标是被插入的节点,除此之外event对象中不包含其他信息。最后一个触发的事件是DOMSubtreeModified,触发于新插入节点的父节点。13.4.7HTML5事件DOM规范并没有涵盖所有浏览器支持的所有事件。很多浏览器实现了一些DOM规范没有的自定义事件,HTML5详尽(好奇多详尽)列出了浏览器应该支持的所有事件。本节只讨论得到浏览器完善的事件。但并非全部。(其他事件在本书其他章节讨论)1.contextmenu事件上下文菜单,就是鼠标右键单机产生的那个菜单。在Mac中,是Ctrl+单击调出。开发人员为了解决如何屏蔽与该操作关联的默认上下文菜单,定义了contextmenu这个事件,用以表示何时应该显示上下文菜单,以便开发人员取消默认的上下文拿菜刀而提供自定义菜单。由于contextmenu事件是冒泡的,因此可以为document指定一个事件处理程序,来处理页面中发生的所有类似事件。这个事件的目标是发生用户操作的元素。在所有浏览器中都可以取消这个事件:在DOM浏览器中,使用event.preventDefalut();在IE中,将event.returnValue的值设置为false。因为contextmenu事件属于鼠标事件,所以其事件对象中包含与光标位置有关的所有属性。通常使用contextmenu事件来显示自定义的上下文菜单,使用onclick事件处理程序来隐藏该菜单。例子:<body> <div id="myDiv"></div> <ul id="myMenu" style="position:absolute;visibility:hidden;background-color:silver"> <li><a href="http://www.nczonline.net">Nicholas's site</a></li> <li><a href="http://www.wrox.com">Wrox site</a></li> <li><a href="http://www.yahoo.com">Yahoo!</a></li&gt; </ul></body>书中说“<div>元素包含一个自定义的上下文菜单”,没有包含住吧。<ul>元素作为上下文菜单,在初始时是隐藏的。实现这个例子的JavaScript代码如下:EventUtil.addHandler(window,"load",function(event){ //网页加载完再执行 var div = document.getElementById("myDiv"); EventUtil.addHandler(div,"contextmenu",function(event){ event = EventUtil.getEvent(event); EventUtil.preventDefault(event); var menu = document.getElementById("myMenu"); console.log(event.clientX+","+event.clientY) menu.style.left = event.clientX + "px"; menu.style.top = event.clientY + "px"; menu.style.visibility = "visible"; }); EventUtil.addHandler(document,"click",function(){ //左键点击关闭上下文菜单的操作是在全文档上,即点击任意地方就关闭上下文菜单 document.getElementById("myMenu").style.visibility = "hidden"; });})一开始函数参数中没有传入event对象,clientX和clientY一直是undefined。正确出现clientX和clientY后菜单出现的位置也不对啊,X轴是对的,Y轴差了几千个px。然后又试了下把div从网页最下面提到最上面,上下文菜单就在鼠标旁边显示了,是因为上下文菜单“下不来这么深”吗?未解BUG。支持contextmenu事件的浏览器有IE、Firefox、Safari、Chrome和Opera 11+。2.beforeunload事件之所以定义window对象上的boforeunload事件,是为了让开发人员有可能在页面卸载前阻止这一操作。这个事件会在浏览器卸载页面之前触发。可以通过它来取消卸载并继续使用原有页面(比如提示是否离开网页)。为了显示这个对话框,必须将event.returnValue的值设置为要现实给用户看的字符串(对IE及Firefox而言),同时作为函数的值返回(对Safari和Chrome而言),如下面例子:EventUtil.addHandler(window,"beforeunload",function(event){ event = EventUtil.getEvent(event); var message = "确定离开网页?" event.returnValue = message; //returnValue不是阻止默认事件的? return message;})IE和Firefox、Safari和Chrome都支持beforeunload事件,Opera 11及之前的版本不支持beforeunload事件。3.DOMContentLoaded事件众所周知,window的load事件会在页面完全加载完再触发,竟然有人觉得这样太久了,又搞出了一个DOMContentLoaded事件,在DOM树加载完后就触发,不等图像,JavaScript文件,CSS文件或其他资源下载完毕。要处理DOMContentLoaded事件,可以为document或window添加相应的事件处理程序(尽管事件会冒泡到window,但它目标实际上是document)。例子:EventUtil.addHandler(document,"DOMContentLoaded",function(event){ console.log("Content loaded");});DOMConetentLoaded事件对象不会提供任何额外的信息,除了target属性是document。IE9+、Firefox、Chrome、Safari 3.1和Opera 9+都支持DOMContentLoaded事件,这个事件即可以添加事件处理程序,也可以执行其他DOM操作。对不支持这个事件的浏览器,可以在页面加载期间设置一个时间为0毫秒的超时调用,这样就可以尽快调用程序。但这样也不能保证在所有环境中该超时调用一定会早于load事件触发。4.readystatechange事件一个能提供与文档或元素的加载状态有关的信息的事件。支持的浏览器有IE、Firefox 4+和Opera,不想写啊,有必要这么抢先机吗,还写了个函数判断是readystatechange事件块还是DOMContentLoaded事件快。在IE和Opera中<script>元素,在IE中<link>元素也可以触发这个事件。5.pageshow和pagehide事件6.haschange事件HTML5新值的haschange事件,以便在URL参数列表发生变化时通知开发人员。新增这个事件的原因,是因为在Ajax应用中,开发人员经常要利用URL参数列表来保存状态或导航信息。haschange事件必须添加给window对象(说haschange事件是window对象的不就好了吗),然后URL参数列表一变动就会调用它。此时的event对象包含两个额外属性:newURL和oldURL。这两个属性分别保存着参数列表变化前后完整的URL。支持haschange事件的浏览器有IE8+、Firefox 3.6+、Safari 5+、Chrome和Opera 10.6+。这些浏览器中,只有Firefox 6+、Chrome和Opera支持oldURL和newURL属性。所以最好使用location对象来确定当前的参数列表。EventUtil.addHandler(window,"haschange",function(){ console.log("当前网址的哈希值是:" + location.hash)});使用下面代码检测浏览器是否支持haschange事件:var isSupported = ("onhashchange" in window); //此处有bug如果IE8是在IE7文档模式下运行,即使功能无效也会返回true。为解决这个问题,可以使用下面这种更稳妥的方式:var isSupported = ("onhashchange" in window) && (document.documentMode === undefined || document.documentMode > 7);13.4.8设备事件W3C从2011年开始着手指定一份关于设备事件的新草案,以涵盖不断增长的设备类型并为他们定义事件。本节会同时讨论这份草案中设计的API和特定于浏览器开发商的事件。1.orientationchange事件当用户将设备由横向模式切换为纵向查看模式,就会触发orientationchange事件,这个事件是苹果为移动Safari添加的事件。window.orientation属性中包含3个值:0表示肖像模式,90表示向左旋转的横向模式,-90表示向右旋转的横向模式。这个事件的event对戏不包含任何有价值的信息。所有IOS设备都支持这个事件和window.irientationchange属性,安卓没说,不知道有没有实现。2.MozOrientationFirefox 3.6为检测设备的方向引入了这个新事件,前缀Moz已经表示这是个特定于浏览器开发商的事件,不是标准事件。MozOrientation事件也是在window对象上触发的。只有带加速计的设备可以支持这个事件,而且作者说这是个实验性API,未来可能会变(gg)。3.deviceorientation事件到2011年,支持这个事件的浏览器有IOS 4.2中的Safari、Chrome和Android版的Webkit,兼容得很多,很想写,但是好蛋疼啊。不如以后在写吧。4.devicemotion事件DeviceOrientation Event规范还定义了一个devicemotion事件(前面也没说这个规范是什么,上一个事件也是这个规范定义的,大概是W3C那个吧)这个事件是要告诉开发人员设备什么时候移动,而不仅仅是方向的改变。例如检测设备是不是正在往下掉,或者试试被走着的人拿在手里。13.4.9触摸与手势事件1.触摸事件2.手势事件13.5内存和性能在JavaScript中,添加到页面上的事件处理程序数量将关系到页面的整体运行性能。导致这个问题的原因是多方面的,首先,每个函数都是对象,都会占用内存,内存中的对象越多,性能越差。其实,必须事先指定所有事件处理程序而导致的DOM访问次数,会延迟整个页面的交互就绪时间。所以,从如何利用好事件处理程序的角度出发,还是有一些方法能提升性能的。13.5.1事件委托对“事件处理程序过多”问题的解决方案,就是事件委托。事件委托利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如click事件hi一直冒泡到document层次,也就是说,我们可以为整个页面指定一个onclick事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。书里有个给每个<li>的onclick事件都定义事件处理程序的坏例子,直接上优化的例子吧:<ul id="myLinks"> <li id="goSomewhere">Go somewhere</li> <li id="doSomething">Do something</li> <li id="sayHi">Say hi</li></ul>可以直接把onclick事件定义在<ul>元素上,利用target属性(这个属性指向真正的目标元素)和冒泡,switch语句块,就可以只定义一个事件完成3件事:var list = document.getElementById("myLinks");EventUtil.addHandler = (list,"click",function(event){ event = EventUtil.getEvent(); var target = EventUtil.getTarget(); switch(target.id){ case "doSomething": document.title = "I change the document’s title"; break; case "goSomewhere": location.href = "http://www.wrox.com&quot;; break; case "sayHi": console.log("Hi"); break; }})测了以下没反应是什么鬼。13.5.2移除事件处理程序每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的JavaScript代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。在不需要的时候移除事件处理程序,是解决这个问题的一个方案。内存中留有那些过时不用的“空事件处理程序(dangling event handler)”,也是造成Web应用程序内存与性能问题的主要原因。在两种情况下,可能会造成空事件处理程序问题,第一种情况是从文档中移除带有事件处理程序的元素时。例如使用removeChild()和replaceChild()方法,但更多地是发生在使用innerHTML替换页面中某一部分时,如果带有事件处理程序的元素被innerHTML删除了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。例子:<div id="myDIv"> <input type="button" value="Click Me" id="myBtn"/></div><script>var btn = document.getElementById("myBtn");btn.onclick = function(){ //其中的操作 document.getElementById("myDiv").innerHTML = "操作中…" //gg!}</script>这里是为了避免按钮被双击(发生重复提交什么的),在点击了一次后按钮就被移除并替换成了一条信息,但是当按钮被删除时,它还带着一个事件处理程序呢。按钮确实被innerHTML移走了,但事件处理程序仍然保持着与按钮的引用关系。有的浏览器(“尤其IE”23333)很有可能将对元素和对事件处理程序的引用都保存在内存中。如果知道某个元素会被移除,最好手工移除事件处理程序。例子:<div id="myDIv"> <input type="button" value="Click Me" id="myBtn"/></div><script>var btn = document.getElementById("myBtn");btn.onclick = function(){ //其中的操作 btn.onclick = null; document.getElementById("myDiv").innerHTML = "操作中…" //gg!}</script>在按钮的单击事件中删除这个单击事件是可以的。DOM0级事件也可以删除DOM2级事件。注意,在事件处理程序中删除按钮也能阻止事件冒泡。目标元素在文档中是事件冒泡的前提。✎:采用事件委托也能解决这个问题,把按钮单击事件放在较高层次的元素上,同样能处理该区域中的事件(不过这样事件处理程序还是在内存中,只是这样的事件处理程序是可以被回收的)。导致“空事件处理程序”的另一种情况,就是卸载页面的时候。IE8及更早版本在这种情况下依然是问题最多的浏览器。其他浏览器或多或少也有。如果在页面被卸载之前没有清理干净事件处理程序,那它们就会滞留在内存中。每次加载完页面再卸载页面时(可以是在两个页面间来回切换, 也可以是单击了“刷新”按钮),内存中滞留的对象数目就会增加,因为事件处理程序占用的内存并没有被释放。一般来说,最好的做法就是在页面卸载之前,先通过onunload事件处理程序移除所有事件处理程序。对这种类似撤销的操作,我们可以把它想象成:只要通过onload事件处理程序添加的东西,最后都要通过onunload事件处理将它们移除。13.6模拟事件在测试Web应用程序,模拟触发事件是极其有用的技术。DOM2级规范为此规定了模拟特定事件的方式。Opera、Firefox、Safari、Chrome和IE都支持这种方式。IE有它自己模拟事件的方式(艹艹艹艹)。13.6.1DOM中的事件模拟可以在document对象上使用createEvent()方法来创建event对象。这个方法接收一个参数,即表示要创建的事件类型的字符串。在DOM2级中,所有这些字符串都使用英文复数形式,而在DOM3级中都变成了单数。这个字符串可以是下列字符串之一:“DOM2级事件”没有规定键盘事件,后来的“DOM3级事件”中才正式将其作为一种事件给出规定。截止出书只有IE9支持DOM3级键盘事件。不过,在其他浏览器中可以通过几种方式来模拟键盘事件。在创建了event对象之后,还需要使用与事件有关的信息对其进行初始化。每种类型的event对象都有一个特殊的方法,为它传入适当的数据就可以初始化该event对象。不同类型的这个方法的名称不同,具体取决于createEvent()中使用的参数。模拟事件的最后一步是触发事件,这一步需要使用dispatchEvent()方法,所有支持事件的DOM节点都支持这个方法。调用dispatchEvent()方法时,需要传入一个参数,即表示要触发事件的event对象,触发事件之后,该事件就跻身“官方事件”之列了,因为能够照样冒泡并引发相应事件处理程序的执行。1.模拟鼠标事件创建鼠标事件对象的方法是为createEvent()字符串“MouseEvents”。返回的对象有一个名为initMouseEvent()方法,用于指定与该鼠标事件有关的信息。initMouseEvent()方法接收15个参数,分别与鼠标事件中每个典型的属性一一对应,这些参数的含义如下:这些参数与鼠标事件的event对象所包含的属性一一对应。其中,前4个参数对正确地激发事件至关重要,因为浏览器需要这些参数;而剩下的所以参数只有在事件处理程序中才会用到。当把event对象传给dispatchEvent()方法时,这个对象的target属性会自动设置。下面,我们就通过一个例子来了解如何模拟对按钮的单击事件。例子:var btn = document.getElementById("myBtn");//创建事件对象var event = document.createEvent("MouseEvents");//初始化事件对象event.initMouseEvent("click",true,true,document.defaultView,0,0,0,0,0,false,false,false,false,0,null)//触发事件btn.dispatchEvent(event);模拟事件只是模拟一个点击事件出来,就是模拟“点一下”这个动作,但是事件是什么不是模拟事件的工作,应该是会触发你在EventUtil.addHandler()中定义的事件处理程序。2.模拟键盘事件DOM3级规定,调用createEvent()并传入“KeyboardEvent”就可以创建一个键盘事件。返回的事件对象会包含一个initKeyEvent()方法。这个方法接收下列参数:由于DOM3级不提倡使用keypress事件,因此只能利用这种技术来模拟keydown和keyup事件。(前后两句话有关系吗??)模拟键盘事件例子:var textbox = document.getElementById("myTextbox"),event;//以DOM3级方式创建事件对象if (document.implementation.hasFeature("KeyboardEvents","3.0")) { event = document.creatEvent("KeyboardEvent"); //初始化事件对象 event.initKeyboardEvent("keydown",true,true,document.defaultView,"a",0,"Shift",0); //触发事件 textbox.dispatchEvent(event);这个例子模拟的是按住Shift的同时按下A键。使用document.createEvent("KeyboardEvent")之前,应该先检测浏览器是否支持DOM3级事件;其他浏览器返回一个非标准的KeyboardEvent对象(其他浏览器是什么浏览器?)。在Firefox中,调用createEvent()并传入“KeyEvents”就可以创建一个键盘事件。返回的事件对象会包含一个initKeyEvent()方法,这个方法接受下列10个参数:将创建的event对象传入到dispatchEvent()方法就可以触发键盘事件,例子://只适用于Firefoxvar textbox = document.getElementById("myTextbox");//创建事件对象var event = document.createEvent("KeyEvents");//初始化事件对象event.initKeyEvent("keypress",true,true,document.defaultView,false,false,false,false,65,65);//触发事件text.dispatchEvent(event);在Firefox中运行上面的代码,会在指定的文本框中输入字母A。同样,也可以依此模拟keyup和keydown事件。(但是亲测在Firefox上没有出来效果是最骚的)在其他浏览器中,则需要创建一个通用的事件,然后再向事件对象中添加键盘事件特有的信息。例如:var textbox = document.getElementById("myText");//创建事件对象var event = document.createEvent("Events");//初始化事件对象event.initEvent(type,bubbles,cancelable);event.view = document.defaultView;event.altKey = false;event.ctrlKey = false;event.shiftKey = false;event.metaKey = false;event.keyCode = 65;event.charCode = 65;//触发事件textbox.dispatchEvent(event);以上代码首先创建一个通用事件,然后调用initEvent()对其进行初始化,最后又为其添加了 键盘事件的具体信息。在此必须要使用通用事件,而不能使用UI事件,因为UI不允许向event对象中再添加新属性(Safari除外)。像这样模拟事件虽然会触发键盘事件,但却不会向文本框中写入文本,这是由于无法精确模拟键盘事件造成的。3.模拟其他事件指模拟变动事件和HTML事件。一句“浏览器中很少使用变动事件和HTML事件,因为使用它们会受到一些限制。”4.自定义DOM事件自定义事件不是由DOM原生触发的,它的目的是让开发人员创建自己的事件。要创建新的自定义事件,可以调用createEvent(“CustomEvent”)。返回的对象有一个名为initCustomEvent()的方法,接收如下4个参数:心态崩了,阴雾天很疲,而且腿超级酸痛,屁股也痛是最气的。13.6.2IE中的事件模拟IE8及之前版本中模拟事件与在DOM中模拟事件的思路相似:先创建event对象,然后为其指定相应信息,然后再使用该对象来触发事件。当然IE在实现每个步骤时都采用了不一样的方式。调用document.createEventObject()方法可以在IE中创建event对象,但与DOM方式不同,这个方法不接受参数,结果会返回一个通用的event对象。然后,你必须手工为这个对象添加所必要的信息。最后一步就是在目标上调用fireEvent()方法,这个方法接受两个参数:事件处理程序的名称和event对象。在调用fireEvent()方法时,会自动为event对象添加srcElement和type属性;其他属性都是必须手工添加的。换句话说,模拟任何IE支持的事件都采用相同的模式。例如,下面的代码模拟了在一个按钮上触发click事件过程:var btn = document..getElementById("myBtn");//创建事件对象var event = document.createEvent();//初始化事件对象event.screenX = 100;event.screenY = 0;event.clientX = 0;event.clientY = 0;event.ctrlKey = false;event.altKey = false;event.shiftKey = false;event.button = 0;//触发事件btn.fireEvent("onclick",event);由于鼠标事件、键盘事件以及其他事件的event对象并没有什么不同,所有可以使用通用对象来触发任何类型的事件。不过,正如在DOM中模拟键盘事件一样,运行这个例子也不会因模拟了keypress而在文本框中看到任何字符,即使触发了事件处理程序也没有用。三天,终于搞定第十三章,三点,第十四章走起。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"第十二章 DOM2和DOM3","date":"2017-12-05T12:56:28.000Z","path":"2017/12/05/第十二章-DOM2和DOM3/","text":"DOM1级主要定义的是HTML和XML文档的底层结构。DOM2和DOM3则在这个结构的基础上引入了更多的交互能力。为此,DOM2和DOM3级分为许多模块(模块间具有某种联系),分别描述了DOM的某个非常具体的子集。这些模块如下:·DOM2级核心(DOM Level 2 Core):在1级核心的基础上,为节点添加更多方法和属性。·DOM2级视图(DOM Level 2 Views):为文档定义了基于样式信息的不同视图。还不知道有什么用·DOM2级事件(DOM Level 2 Events):说明了如何使用事件与DOM文档交互。·DOM2级样式(DOM Level 2 Style):定义了如何以编程的方式访问和改变CSS样式信息。·DOM2级遍历和范围(DOM Level 2 Traversal and Range ):引入了遍历DOM文档和选择其特定部分的新接口。·DOM2级HTML(DOM Level 2 HTML):在1级HTML基础上构建,添加了更多属性、方法和新接口。现在疑惑的就是2级视图,遍历和范围,HTML是什么。 12.1DOM变化DOM2级和3级的目的在于扩展DOM API,以满足操作XML的所有需求(只有XML?),同时提供更好的处理错误及特性检测能力。从某种意义上讲,实现这一目的很大程度上意味着对命名空间的支持(命名空间是什么?)。DOM2级核心没有引入新类型,只是在DOM1级上通过增加新方法和新属性来增强既有类型。DOM3级核心既引入了新类型,又增强了既有类型。DOM2级视图和DOM2级HTML模块也增强了DOM接口,提供了新的属性和方法。由于这两个模块很小,因此作者讲它们与DOM2级核心放在一起,讨论基于JavaScript对象的变化。通过下列代码确定浏览器是否支持这些DOM模块:var supportsDOM2Core = document.implementation.hasFeature("Core","2.0");var supportsDOM3Core = document.implementation.hasFeature("Core","3.0");var supportsDOM2HTML = document.implementation.hasFeature("HTML","2.0");var supportsDOM2Views = document.implementation.hasFeature("Views","2.0");var supportsDOM2XML = document.implementation.hasFeature("XML","2.0");各大浏览器的最新版包括Edge都能全部支持,只有Safari的supportsDOM3Core是返回false。不过就算返回true,能说明DOM2,3的全部属性和方法都支持吗?IE11、IE10、IE9对DOM3级核心的检测也是返回false。IE7、8对上面全部返回false。说明IE7,8不支持DOM2,3级。12.1.1针对XML命名空间的变化没写过XML文档,不知道XML文档有什么用,所以读起来没什么兴趣。12.1.2其他方面的变化感觉这一章就是告诉你很多DOM2,3级新带来的属性和方法,并不会对你在认识JavaScript和网页等方面有什么新的见解,就是提供更多的API,能让你做更多的事或做得更方便。看起来很多方法都很少用到,写了也未必记得住,就挑一些看起来有用的写吧。16:43.今天周六,图书馆要关门咯。12.2样式在HTML中定义样式有三种,通过<link/>元素包含外部样式表、使用<style/>元素定义嵌入式样式、以及使用style特性定义针对特定元素的样式。“DOM2级样式”模块围绕这3种应用样式的机制提供了一套API,要确定浏览器是否支持DOM2级定义的CSS能力,用下面代码:var supportsDOM2CSS = document.implementation.hasFeature("CSS","2.0");var supportsDOM2CSS2 = document.implementation.hasFeature("CSS2","2.0");IE7-10都不支持上面两个?IE11不支持第一个,Safari不支持第二个。其他浏览器都返回true。12.2.1访问元素的样式任何支持style特性的HTML元素在JavaScript中都有一个对应的style属性。这个style对象是CSSStyleDeclaration的实例。包含着通过HTML的style特性指定的所有样式信息,但不包含与外部样式表或嵌入样式表经层叠而来的样式。在style特性中指定的任何CSS属性都将表现为这个style对象的相应属性。对于使用短划线(分隔不同的词汇,例如background-color)的CSS属性名,必须将其转换为驼峰大小写形式,才能通过JavaScript访问。多数情况下,都可以通过简单地转换属性名的格式来实现转换。其中一个不能直接转换的CSS属性是float。由于float是JavaScript的保留字,因此不能做属性名。“DOM2级样式”规范规定样式对象上相应的属性名应该是cssFloat。Firefox、Safari、Opera和Chrome都支持这个属性,而IE支持的则是styleFloat(浪事向多)。只要取得一个有效的DOM元素的引用(这句话太经典太规范了。忍不住写下来),都可以随时使用JavaScript为其设置样式,举个宽高的例子:var div = document.getElementById("id");div.style.width = "100px";div.style.height = "200px"; //都要加单位✎:在标准模式下,所有度量值都必须指定一个度量单位,在混杂模式下,可以不带单位,浏览器会假设它是“XXpx”。但在标准模式下这样设置会被忽略,所以在实践中最好始终带单位。通过style对象可以取得在style特性中指定的样式。例子:<div id="myDiv" style = "background-color:blue;width:10px;height:25px"></div>只有行内样式的会被得到。console.log(myDiv.style.backgroundColor) //“blue”//假设在头部的<style>规定了字体大小font-size:10pxconsole.log(myDiv.style.fontSize) //"" 空字符串1.CSS样式属性和方法“DOM2级样式”规范还为style对象定义了一些属性和方法。这些属性和方法在提供元素的style特性值的同时,也可以修改样式。下面列出了这些属性和方法:·cssText:如前所述,通过它能够访问到style特性中的CSS代码。(只有style特性里的)。·length:应用给元素的CSS属性的数量。(很奇怪我只给div的style特性设置了一个border-top属性,返回的length却是3,删掉这个特性就变0。如果单独设置font-size属性返回的length就是1)(后来用document.getElementById("dd").style[1]测试了一下,发现原来边框的样式(style),颜色(color),粗细(width)各占一个CSS样式长度)·parentRule:表示CSS信息的CSSRule对象,后面将讨论CSSRule类型。·getPropertyCSSValue(propertyName):返回包含给定属性值的CSSValue对象。·getPropertyPriority(propertyName):如果给定的属性使用了!important设置,则返回“important”;否则,返回空字符串。·getPropertyValue(propertyName):返回给定属性的字符串。·item(index):返回给定位置的CSS属性的名称。·removeProperty(propertyName):从样式中删除给定属性。·setProperty(propertyName,value,priority):将给定属性设置为相应的值,并加上优先权标志(“important”或一个空字符串)。通过cssText属性可以访问style特性中的CSS代码。在读模式下,cssText返回浏览器对style特性中CSS代码的内部表示。在写模式下,赋给cssText的值会重写整个style特性的值。例子:myDiv.style.cssText = "width:20px;height:100px;background-color:green";console.log(myDiv.style.cssText);连IE5都支持cssText属性!!!设置length属性的目的,就是将其与item()方法配套使用,以便迭代在元素中定义的CSS属性。在使用length和item()时,style对象实际上就相当于一个集合,都可以使用方括号语法来代替item()来取得给定位置的CSS属性。例子:for(var i=0,length=myDiv.style.length; i<len; i++){ console.log(myDiv.style[i]); //或者myDiv.style.item(i)}用myDiv.style[i]只能取得CSS属性名,要取得属性值,还要使用getPropertyValue()取得属性值。例子:var prop,value,i,len;for(i=0,len=dd.style.length;i<len;i++){ prop = dd.style[i]; value = dd.style.getPropertyValue(prop); console.log(prop + ":" + value);}//返回border-top-width:2px border-top-style:solid border-top-color:rgb(204, 204, 204) 竟然是rgb()格式的。IE9开始才能返回这个结果。亲测IE9以前的版本不能用style特性+方括号的方法访问属性名。要知道CSS属性值更加详细的信息,可以使用getPropertyCSSValue()方法,它返回一个包含两个属性的对象。这两个属性分别是:cssText和cssValueType。其中,cssText属性的值与getPropertyValue()返回的值相同,而cssValueType属性则是返回一个数值产量,表示值的类型:0表示继承的值,1表示基本的值,2表示值列表,3表示自定义的值。以下代码既输出CSS属性值,也输出值的类型:var prop,value,i,len;for(i=0,len=dd.style.length;i<len;i++){ prop = dd.style[i]; value = dd.style.getPropertyCSSValue(prop); console.log(prop + ":" + value.cssText+"("+value.cssValueType+")");}不知道为什么Chrome说getPropertyCSSValue()不是function,Firefox说最后一行value is null。书里说实际开发中getPropertyCSSValue()使用得比getPropertyValue()少得多。IE9+、Safari 3+、Chrome支持这个方法。Firefox7及之前版本也提供这个访问,但调用总返回null。要从元素的样式中移除某个CSS属性,需要使用removeProperty()方法。使用这个方法移除一个属性,意味着将为该属性应用默认样式。(亲测这句话有问题,如果你在<style>里设置了一个相同的属性,移除后就会用那个属性显示,并不会出现默认属性)2.计算的样式虽然style对象能够提供支持style特性的任何元素样式信息,但它不包含那些从样式表层叠而来并影响到当前元素的样式信息。为此,“DOM2级样式”增强了document.defaultView,提供了方法。这个方法接受两个参数:要取得计算样式的元素和一个伪元素字符串(例如“:after”)。如果不需要伪元素信息,第二个参数可以是null。getComputedStyle()方法返回一个CSSStyleDeclaration对象(与style属性的类型相同),其中包含当前元素的所有计算的样式。以下面这个HTML页面为例:<head><style>#myDiv{ background-color: blue; width:100px; height:200px;}</style></head><body> <div id="myDiv" style="background-color:red;border:1px solid black"></div></body>引用给<div>元素的样式一方面来自嵌入式样式表,另一方面来自style特性。以下代码可以取得这个元素计算后的样式:var myDiv = document.getElementById("myDiv");var computedStyle = document.defaultView.getComputedStyle(myDiv,null); //这个方法是document对象下的defaultView属性的方法console.log(computedStyle.backgroundColor); //"red"console.log(computedStyle.width); //"100px"console.log(computedStyle.height); //"200px"console.log(computedStyle.border); //在某些浏览器是“1px solid rgb(0,0,0)”在这个元素计算后的样式中,背景颜色是“red”,“blue”在自身的style特性中已经被覆盖了。边框属性可能会也可能不会返回样式表中实际的border规则(Opera会返回,其他浏览器不会)。存在这个差别的原因是不同浏览器解释综合(rollup)属性(如border)的方式不同,因为设置这种属性实际上会设计其他很多的属性。在设置border时,实际上设置了四个边的边框宽度、颜色、样式属性(border-top-color、border-top-color、border-botom-style,等等)。因此,,即使computedStyle.border不会在所有浏览器中都返回值,但computedStyle.borderLeftWidth会返回值。亲测Chrome、Opera会返回computedStyle.border,Firefox、Safari、IE9+、Edge都不返回。IE不支持getComputedStyle()方法,但它有一种类似的概念。在IE中,每个具有style属性的元素还有一个curentStyle属性。这个属性是CSSStyleDeclaration的实例,包含当前元素全部计算后的样式。使用方法:var myDiv = document.getElementById("myDiv");var computedStyle = myDiv.currentStyle;console.log(computedStyle.backgroundColor);上面说了,currentStyle属性是每个具有style属性的元素的属性。所以是在元素上使用这个属性。无论在哪个浏览器中,最重要的一条是记住所有计算的样式都是只读的;不能修改计算后样式对象中的CSS属性。所以用getComputedStyle()方法和currentStyle属性只可以得到属性值,不能修改。12.2.2操作样式表要理清楚思路,这节开始讲的已经不是行内样式了,开始讲嵌入式样式表和从外部引入的样式表了,所以下面提到的style等同名属性已经指的不是同一种东西。CSSStyleSheet类型表示的是样式表,包括通过<link>元素包含的样式表和在<style>元素中定义的样式表(不包括行内样式哦这么说)。<link>和<style>两个元素分别是由HTMLLinkElement和HTMLStyleElement类型表示的。但是,CSSStyle类型相对更加通用一些。上述两个针对元素的类型允许修改HTML特性,但CSSStyleSheet对象只是一套只读的接口(有一个属性例外)使用下面代码可以确定浏览器是否支持DOM2级样式表。var supportsDOM2StyleSheets = document.implementation.hasFeature("StyleSheets","2.0");CSSStyleSheet继承自StyleSheet,后者可以作为一个基础接口来定义非CSS样式表。从StyleSheet接口继承而来的属性如下:好多,晚上补图。应用于文档中的所有样式是通过document.styleSheets集合来表示的。通过这个集合的length属性可以知道文档中样式表的数量,可以通过方括号或item()访问每一个样式表。这部分讲的都是如何获取样式表及样式表属性,就不写那么多了。1.CSS规则CSSRule对象表示样式表中的每一条规则。实际上,CSSRule是一个供其他多种类型继承的基类型。其中最常见的就是CSSStyleRule类型,表示样式信息(其他规则还有@import、@font-face、@page和@charset,但这些规则很少通过脚本访问)。CSSStyleRule对象包含下列属性:·cssText:返回整条规则相应的文本(与style.cssText不同,下面有解释)。·parentRule:如果当前规则是导入规则,这个属性引用的就是导入规则,否则,这个属性返回null。IE不支持这个属性。·parentStyleSheet:当前规则所属的样式表(应该也是个引用;sheet是表的意思)。IE不支持这个属性。·selectorText:返回当前规则的选择符文本。·style:一个CSSStyleDeclaration对象可以通过它设置和取得规则中特定的样式值。·type:表示规则类型的常量值。对于样式规则,这个值是1。IE不支持这个属性。其中最常用的属性是cssText、selectorText和style。cssText属性与style.cssText属性类似,但并不相同。前者包含选择符文本和围绕样式信息的花括号,后者只包含样式信息(类似于元素的style.cssText)。此外,cssText是只读的,style.cssText可以被重写。大多数情况下,仅使用style属性就可以满足所有操作样式规则的需求了。这个对象就像每个元素上的style属性一样(难道不是一个东西??真的不一样:这个style属性是样式表里样式的style,元素上的style属性是行内样式的style特性),可以通过它读取和修改规则中的样式信息。以下面的CSS规则为例:div.box{ //一个这样的选择符+花括号算一条规则,花括号里的样式在rules[0]里的style属性里。 background-color:blue; width:100px; height:200px;}假设这条规则位于页面中的第一个样式表,而且这个样式表中只有这一条规则,那么通过下列代码可以取得这条规则的各种信息:var sheet = document.styleSheets[0];var rules = sheet.cssRules || sheet.rules; //应该是浏览器间获取规则集合的方式不同var rule = rules[0];console.log(rule.selectorText); //"div.box"console.log(rule.style.cssText); //完整的css代码console.log(rule.style.backgroundColor); //“blue”console.log(rule.style.width); //“100px”console.log(rule.style.height); //“200px”理一理,document.styleSheets是文档中样式表的集合,document.styleSheets[0]就是文档的第一个样式表。样式表里有规则(计算机里就是这样,此规则,跟现实生活中的“规则”解释不一样,要习惯),每一个规则就是上面的“选择符+花括号”这一块东西,规则就是CSSStyleRule,是个对象,对象里有各种属性(selectorText、cssText、parentStyleSheet、style等),花括号里定义的样式就在style属性里,style属性里的各种属性就是定义的样式。2.创建规则DOM规定,要向现有样式表中添加规则,需要使用insertRule()方法。这个方法接收两个参数:规则文本和表示在哪里插入规则的索引。例子:sheet.insertRule("body{background-color:silver}",0); //DOM方法前提是要先用document.styleSheets[ ]取到一个样式表,再往表里添加规则。插入的规则将成为样式表中的第一条规则(插入到位置0)——规则的次序在确定层叠之后应用到文档的规则时至关重要。Firefox、Safari、Opera和Chrome都支持insertRule()方法。IE又搞事了,IE8及更早版本支持一个类似方法,名叫addRule()方法,也接收两个必选参数:选择符文本和CSS样式信息;一个可选参数:插入的位置(就是把上面的两个参数拆成了3个)。在IE中插入与前面例子相同的规则,用如下代码:sheet.addValue("body","background-color:silver",0); //仅对IE有效要以跨浏览器的方式向样式中插入规则,可以使用下面的函数:var sheet = document.styleSheets[0];function insertRule(sheet,selectorText,cssText,position){ if(sheet.insertRule){ sheet.insertRule(selectorText+"{"+cssText+"}",position); }else if(sheet.addRule){ sheet.addRule(selectorText,cssText,position); }}insertRule(sheet,"body","background-color:silver",0)亲测这个跨浏览器的函数连IE5都有效!以前只知道innerHTML,cssText这种修改行内样式的方法,今天才get到修改样式表的方法,就是inertRule()和addRule()。666666虽然可以像这样来添加规则,但随着添加的规则增多,这种方式会变得非常繁琐。因此,如果要添加的规则非常多,建议还是采用第十章介绍过的动态加载样式表的技术。3.删除规则从样式表中删除规则的方法是deleteRule(),这个方法接收一个参数,要删除的规则的位置。例如,要删除样式表中的第一条规则,可以用如下代码:sheet.deleteRule(0);IE支持的类似的方法叫removeRule()。使用方法相同。不太好用啊,要是以后往样式表中加入新的规则改变了规则的位置不就GG了吗,问题可能找都找不到。12.2.3元素的大小本节介绍的属性和方法并不属于“DOM2级样式”规范,但与HTML元素的样式息息相关。DOM中没有规定如何确定页面中元素的大小,IE为此率先引入了一些属性,目前,所有主要的浏览器都支持这些属性。1.偏移量[只读]偏移量(offset dimension)包括元素在屏幕上占用的所有可见的空间。元素的可见大小由其高度、宽度决定,包括所有内边距、滚动条和边框大小(注意,不包括外边距)。通过下列4个属性可以取得元素的偏移量:·offsetHeight:元素在垂直方向上占用的空间大小,包括元素的高度、(可见的)水平滚动条的高度、上边框高度和下边框高度。·offsetWidth:元素在水平方向上占用的空间大小,包括元素宽度、(可见的)垂直滚动条的宽度、左边框宽度和右边框宽度。·offsetLeft:元素的左外边框至包含元素的左內边框之间的像素距离。(从左到左?)·offsetTop:元素的上外边框至包含元素的上內边框之间的像素距离。其中,offsetLeft和offsetRight属性与包含元素(包含元素是把它包住的元素还是它包在里面的元素?懂这句话什么意思了,在下面有写)有关,包含元素的引用保存在offsetParent属性中,offsetParent属性不一定与ParentNode的值相等。有张图表示了四个量表示的量。晚上补图。偏移量还不如叫占用量,offsetHeight和offsetWidth根本就是元素占用的位置大小。注意这四个量都是只读的。要想知道某个元素在页面上的偏移量,将这个元素的offsetLeft和offsetTop与其offsetParent的相同属性相加,如此循环至根元素,就可以得到一个基本准确的值。以下两个函数可以用于分别取得元素的左和上偏移量:function getElementLeft(element){ var actualLeft = element.offsetLeft; var current = element.offsetParent; while(current !== null){ actualLeft += current.offsetLeft; current = current.offsetParent; } return actualLeft;}function getElementTop(element){ var actualTop = element.offsetTop; var current = element.offsetParent; while(current !== null){ actualTop += current.offsetTop; current = current.offsetParent; } return actualTop;}这两个函数利用offsetParent属性在DOM层次中逐级向上回溯,将每个层次中的偏移量属性合计到一块。对于简单的CSS布局的页面,这两个函数可以得到非常准确的结果。对于使用表格和内嵌框架布局的页面(然而现在已经很少用),由于不同浏览器实现这些元素的方式不同,因此得到的值就不太准确了。一般来说,页面中的所有元素都会被包含在几个<div>元素中,而这些<div>元素的offsetParent又是<body>元素,所以这两个元素返回的值与offsetLeft和offsetTop返回的值相同。上面说offsetTop和offsetLeft的值与包含的元素有关,是因为如果一个元素被另一个元素包含在里面,这个元素的偏移量是指在包含元素(这里自然而然地用到了包含元素这个词,说明包含元素指把所指元素包含起来的元素)中的偏移量,所以要把这个元素的偏移量加上包含元素的偏移量,如此循环至根元素,才能得到的是元素在页面上的偏移量。但是上面说元素被包含在几个<div>中后得到的offsetLeft和offsetTop会跟两个函数返回的值相同我就不懂了。亲测确实返回的值是一样的,那要这两个函数有何用?✎:所有这些偏移属性都是只读的。每次访问它们都要重新计算。因此,应尽量避免重复访问这些属性。可以将他们保存在变量中,以提高性能。2.客户区大小客户区大小(client dimension)指的是元素内容及其内边距所占据的空间大小。有关客户区大小的属性有两个:clientWidth和clientHeight。其中clientWidth和clientHeight属性是元素内容区宽度加上左右内边距宽度,clientHeight属性是元素内容区高度加上下内边距高度。图晚上补。从字面上看,客户区就是元素内部的空间大小,因此滚动条占用的空间不计算在客户区内。要确定浏览器视口的大小,可以使用document.documentElement或document.body(在IE7之前的版本中)的clientWidth和clientHeight。例子:function getViewport(){ if(document.compatMode == "BackCompat"){ return{ width:document.body.clientWidth, height:document.body.clientHeight }; }else{ return{ width:document.documentElement.clientWidth, height:document.documentElement.clientHeight }; }}函数首先检查document.compatMode属性,以确定浏览器是否运行在混杂模式。返回的值表示浏览器视口(<html>或<body>元素)的大小。✎:与偏移量相似,客户区大小也是只读的,也是每次访问都要重新计算。3.滚动大小滚动大小指的是包含滚动内容的元素的大小。有些元素(例如<html>)即使没有执行任何代码,也能自动添加滚动条,但另外一些元素,则需要通过CSS的overflow属性进行设置才能滚动。以下是4个与滚动大小相关的属性:·scrollHeight:在没有滚动条的情况下,元素内容总高度。·scrollWidth:在没有滚动条的情况下,元素内容总宽度。·scrollLeft:被隐藏在内容区域左侧的像素数。通过设置这个属性可以改变元素的滚动位置。·scrollTop:被隐藏在内容区域上方的像素数。通过设置这个属性可以改变元素的滚动位置。下图展现了这些属性代表的大小。scrollWidth和scrollHeight主要用于确定元素内容的实际大小。因此,带有垂直滚动条的页面(<html>标签)总高度就是document.documentElement.scrollHeight。对于不包含滚动条的页面而言,scrollWidth和scrollHeight与clientWidth和clientHeight之间的关系并不十分清晰。各浏览器对这两对的大小关系与代表的是文档内容区域还是视口大小都是有不同意见的:·Firefox中这两组的属性始终都是相等的,但大小表示的是文档内容区域的实际尺寸,而非视口的尺寸。(然而亲测并不相等)·Opera、Safari3.1及更高版本,Chrome中的这两组是有差别的,其中scrollWidth和scrollHeight等于视口大小,clientWidth和clientHeight等于文档内容区域大小。·IE(标准模式下)这两组也是不相等的,scrollWidth和scrollHeight等于文档内容区域大小,clientWidth和clientHeight等于视口大小。好操蛋啊啊啊啊因此,在确定文档总高度和宽度时,必须取得scrollHeight/clientHeight和scrollWidth/clientWidth中的最大值才能保障在跨浏览器的环境下取得精确的结果。下面就是这样的例子:var docHeight = Math.max(document.documentElement.scrollHeight,document.documentElement.clientHeight);var docWidth = Math.max(document.documentElement.scrillHeight,document.documentElement.clientWidth);注意,对于运行在混杂模式下的IE,需要用document.body代替document.documentElement。通过scrollLeft和scrollTop属性既可以确定元素当前滚动状态,也可以设置元素的滚动位置(说明scrollLeft和scrollTop可读可写)。在元素尚未滚动时,这两个属性的值为0.如果元素被垂直滚动了。那么scrollTop的值会大于0,且表示元素上方不可见内容的像素高度。如果元素被水平滚动了,那么scrollLeft的值会大于0,且表示元素左侧不可见内容的像素宽度。因此,将元素的scrollLeft和scrollTop设置为0,就可以重置元素的滚动位置。下面这个函数会检测元素是否位于顶部,如果不是就将其滚到顶部:function scrollTop(element){ if(element.scrollTop != 0){ element.scrollTop = 0; }}4.确定元素的大小这一节叫“确定元素的大小”,但给的函数好像是确认元素在页面中相对与视口的位置的。利用的是为每个元素提供的(注意是元素的)getBoundingClientRect()方法。最后给了个跨浏览器版本的,直接给出:function getBoundingClientRect(element){ var scrollTop = document.documentElement.scrollTop; var scrollLeft = document.documentElement.scrollLeft; if(element.getBoundingClientRect){ if(typeof arguments.callee.offset != "number"){ var temp = document.createElement("div"); tmp.style.cssText = "position:absolute;left:0;top:0;"; document.body.appendChild(temp); arguments.callee.offset = -tmp.getBoundingClientRect().top-scrollTop; document.body.removeChild(temp); temp = null; } var rect = element.getBoundingClientRect(); var offset = arguments.callee.offset; return{ left:rect.left + offset, right:rect.right + offset, top:rect.top+offset, bottom:rect.bottom+offset }; }else{ var actualList = getElementLeft(element); var actualTop = getElementTop(element); return{ left:actualLeft - scrollLeft; right:actualLeft + element.offsetWidth - scrollLeft, top:actualTop - scrollTop, bottom:actualTop + element.offsetHeight-scrollTop } }}会用到前面的getElementLeft()和getElementTop()函数。12.3遍历“DOM2级遍历和范围”模块定义了两个用于辅助完成顺序遍历DOM结构的类型(这两个是“类型”):NodeIterator和TreeWalker。这两个类型能够基于给定的起点对DOM结构执行深度优化(depth-first)的遍历操作。在与DOM兼容的浏览器中,都可以访问到这些类型的对象。IE不支持DOM遍历,使用下列代码可以检测浏览器对DOM2级遍历能力的支持情况:var supportsTraversals = document.implementation.hasFeature("Traversal","2.0");var supportsNodeIterator = (typeof document.createNodeIterator == "function");var supportsTreeWalker = (typeof document.createTreeWalker == "function");12.3.1NodeIteratorNodeIterator类型是两者中比较简单的一个,可以使用document.createNodeIterator()方法创建它的实例(类型有实例。HTMLDocument类型的实例是document对象)。这个方法接受4个参数:·root:想要作为搜索起点的树中的节点。·whatToShow:表示要访问哪些节点的数字代码.·filter:是一个NodeFilter对象,或者一个表示应该接受还是拒绝某种特定节点的函数。·entiryReferenceExpansion:布尔值,表示是否要扩展实体引用。这个参数在HTML页面中没有用。因为其中的实体引用不能扩展。whatToShow参数是一个位掩码,通过应用一或多个过滤器(filter)来确定要访问哪些节点。这个参数的值以常量形式在NodeFilter类型中定义,如下所示:(印象笔记的文档功能还挺不错)可以使用按位或操作符来组合多个选项,如下面例子所示:var whatToShow = NodeFilter.SHOW_SELEMENT | NodeFilter.SHOW_TEXT;也可以通过createNodeIterator()方法的filter参数来指定自定义的NodeFilter对象,或者指定一个功能类似节点过滤器(node filter)函数。每个NodeFilter对象只有一个方法,即acceptNode()。如果应该访问给定的节点,该方法返回NodeFilter.FILTER_ACCEPT,如果不应该访问给定的节点,该方法返回NodeFilter.FILTER_SKIP。由于NodeFilter是个抽象的类型,因此不能直接创建它的实例(抽象类型不能直接创建实例)。必要时,只要创建一个包含acceptNode()方法的对象,然后将这个对象传入createNodeIterator()中即可。例如,下列代码展示了如何创建一个只展示<p>元素的节点迭代器:var filter = { acceptNode:function(node){ return node.tagName.toLowerCase() == "p"? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; }};var iterator = document.createNodeIterator(root,NodeFilter.SHOW_ELEMENT,filter,false);所以这个方法相当于有两个选择器,一个是HowToShow参数,一个是filter里定义的NodeFliter对象里的acceptNode()方法。第三个参数也可以是一个与accpetNode()方法类似的参数,如下所示://最推荐的写法var filter = function(node){ return node.tagName.toLowerCase()=="p"? NodeFilter.FILTER_ACCEPT: NodeFilter.FILTER_SKIP;};var iterator = document.createNodeIterator(root,NodeFilter.SHOW_ELEMENT,filter,false);一般来说,这就是在JavaScript中使用这个方法的形式,这种形式比较简单,而且也跟其他的JavaScript代码很相似。如果不需要指定过滤器,第三个参数应该设置为null。下面的代码创建了一个能访问所有类型节点的简单的NodeIterator:var iterator = docuent.createNodeIterator(document.NodeFilter.SHOW_ALL,null,false);NodeIterator类型的两个主要方法是nextNode()和 previousNode()。顾名思义,在深度优化的DOM子树遍历中,nextNode()用于向前前进一部,而previousNode()用于向后退一步。在刚刚创建的NodeIterator对象中,有一个内部指针指向根节点,因此第一次调用nextNode()会返回根节点。当遍历到DOM字数的最后一个节点时,nextNode()返回Null。previousNode()方法的工作机制类似。当遍历到DOM子树的最后一个节点,且previousNode()返回根节点之后,再次调用它就会返回Null。前面两段代码只是教你写第三个参数,真正遍历还没开始,不要乱,接下来就以下面的HTML代码为例:<div id="div1"> <p><b>Hello</b>world!</p> <ul> <li></li> <li></li> <li></li> </ul></div>假设我们要遍历<div>元素中的所有元素,那么使用下列代码:var div = document.getElementById("div1");var iterator = document.createNodeIterator(div,NodeFilter.SHOW_ELEMENT,null,false);var node = iterator.nextNode();while(node !== null){ console.log(node.tagName); //输出标签名 node = iterator.nextNode();}//输出结果:也许我们不需要那么多信息,只想要返回遍历中遇到的<li>元素。很简单,只要使用一个过滤器即可。例子:var div = document.getElementById("div1");var filter = function(node){ return node.tagName.toLowerCase() == "li"? NodeFilter.FILTER_ACCEPT: NodeFilter.FILTER_SKIP;};var iterator = document.createNodeIterator(div,NodeFilter.SHOW_ELEMENT,filter,false);var node = iterator.nextNode();while(node !== null){ console.log(node.tagName); node = iterator.nextNode();}由于nextNode()和previousNode()方法都基于NodeIterator在DOM结构中的内部指针工作,所以DOM结构的变化会反映在遍历结果中(所以频繁操作也是会消耗性能的)。✎:Firefox 3.5之前的版本没有实现createNodeIterator()方法,但支持下一节要讨论的createTreeWalker()方法。(亲测现在版本的Firefox已经支持这个方法,IE9也支持啊)12.3.2TreeWalkerTreeWalker是NodeIterator的一个更高级的版本。除了包括nextNode()和previousNode()在内的相同功能之外,这个类型还提供了下列用于在不同方向上遍历DOM结构的方法:·parentNode():遍历到当前节点的父节点。·firstChild():遍历到当前节点的第一个子节点。·lastChild():遍历到当前节点的最后一个子节点。·nextSibling():遍历到当前节点的下一个同辈节点。·previousSibling():遍历到当前节点的上一个同辈节点。创建TreeWalker类型的实例对象要使用document.createTreeWalker()方法。这个方法接受的4个参数与document.createNodeIterator()方法相同,用法也相同。但是,TreeWalker与NodeIterator类型还有一点不同,就是filter可以返回的值有所不同。除了NodeFilter.FILTER_ACCEP 和 NodeFilter.FILTER_SKIP之外,还可以使用NodeFilter.FILTER_REJECT。NodeIterator对象也有NodeFilter.FILTER_REJECT,但是作用与NodeFilter.FILTER_SKIP相同:跳过指定节点,但在使用TreeWalker对象时,NodeFilter.FILTER_SKIP会跳过相应节点继续前进到子树中的下一个节点,而NodeFilter.FILTER_REJECT会跳过相应节点及该节点的整个子树。当然,TreeWalker真正的强大之处在能够在DOM结构中沿任意方向移动。还是原来那个HTML为例子,使用TreeWalker遍历DOM数,即使不定义过滤器,也可以取得所有<li>元素,如下代码所示:var div = document.getElementById("div1");var walker = document.createTreeWalker(div,NodeFilter.SHOW_ELEMENT,null,false);walker.firstChild(); //转到<p>,这个时候变量walker已经不是div了,已经转到<p>了walker.nextSibling(); //从<p>转到相邻的下一个同辈节点<ul>,现在是<ul>var node = walker.firstChild(); //现在是<li>while(node !== null){ console.log(node.tagName); node = walker.nextSibling(); //遍历出所有的相邻节点}</sTreeWalker类型还有一个属性,名叫currentNode,表示任何遍历方法在上一次遍历中返回的节点。通过设置这个属性还可以修改遍历继续进行的起点(修改指针指的位置)例子:var node = walker.nextNode();console.log(node === walker.currentNode); //truewalker.currentNode = document.body; //修改起点12.4范围“DOM2级遍历和范围”模块定义了一个“范围(range)”接口。通过范围可以选择文档中的一个区域,而不必考虑节点的界限。在常规的DOM操作不能有效修改文档的时候,使用范围往往可以达到目的。Firefox、Opera、Safari和Chrome都支持DOM范围,IE以专有方式实现自己的范围特性。12.4.1DOM中的范围DOM2级在Document类型中定义了createRange()方法。在姜戎DOM的浏览器中,这个方法属于document对象(在document对象上调用)。使用hasFeature()方法或者直接检测该方法,可以确定浏览器是否支持范围:var supportsRange = document.implementation.hasFeature("Range","2.0");var alsonSupportsRange = (typeof document.createRange == "function");如果浏览器支持范围,那么可以使用createRange()来创建DOM范围,如下所示:var range = document.createRange();与节点类似,新创建的范围也直接与创建它的文档关联在一起(变成文档的一部分吗?)、不能用于其他文档。创建了范围并设置了其位置之后,还可以针对范围得到尼日融执行很多种操作,实现对底层DOM树更精细的控制。每一个范围由一个Range类型的实例表示,这个实例拥有很多属性和方法。下列属性提供了当前范围在文档中的位置信息:·startContainer:包含范围起点的节点(即选区中第一个节点的父节点)。·startOffset:范围在startContainer中起点的偏移量,如果startContainer是文本节点、注释节点或CDATA节点,那么startOffset就是范围起点之前跳过的字符数量。否则startOffset就是范围中第一个子节点的索引。·endContainer:包含范围终点的节点(即选区中最后一个节点的父节点)。·endOffset:范围在endContainer中终点的偏移量(与startOffset遵循相同的取值规则)。·commonAncestorContainer:startContainer和endContainer共同的祖先节点在文档中位置最深的那个。再把范围放到文档中特定的位置时,这些属性就会被赋值。1.用DOM范围实现简单选择要使用范围来选择文档中的一部分,最简单的方式就是使用selectNode()或selectNodeContents()。这两个方法都接受一个参数,即一个DOM节点,然后使用该节点的信息来填充范围。其中,selectNode()方法选择整个节点;而selectNodeContents()方法则只选择节点的子节点。以下面HTML代码为例:<html><body> <p id="p1"><b>Hello</b>world!</p></body></html>我们可以使用下列代码来创建范围:var range1 = document.createRange();var range2 = document.createRange();var p1 = document.getElementById("p1");renge1.selectNode(p1);range2.selectNodeContents(p1);这里创建的两个范围包含文档中不同的部分,如图所示:在调用selectNode()时,startContainer、endContainer和commonAncestorContainer都等于传入节点的父节点,也就是例子中的document.body。而startOffset属性等于给定节点在其父节点的childNodes集合中的索引(在这个例子中是1——因为兼容DOM的浏览器将空格算作一个文本节点),endOffset等于startOffset加1(因为只选择了一个节点)。(不懂)在调用selectNodeContents()时,startContainer、endContainer和commonAncestorContainer等于传入的节点,即这个例子中的P元素。而startOffset属性始终等于0,因为范围从给定节点的第一个子节点开始。最后,endOffset等于子节点的数量(node.childNodes.length),在这个例子中是2。为了更精细地控制将哪些节点包含在范围中,还可以使用下列方法:·setStartBefore(refNode):将范围的起点设置在refNode之前,因此refNode也就是范围选区中的第一个子节点。同时会将startContainer属性设置为refNode.parentNode,将startOffset属性设置为refNode在其父节点的childNodes集合中的索引。还有五个属性,但是我不知道这一部分知识有什么用啊,放着吧就。直接第十三章走起。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"《JavaScript高级程序设计》 第11章 DOM扩展","date":"2017-12-05T12:42:17.000Z","path":"2017/12/05/《JavaScript高级程序设计》-第11章-DOM扩展/","text":"简而言之,人们觉得现有的DOM作为API还不够用,出现了很多其他有用的扩展,后来W3C把一些有用的,成为事实标准的专有扩展标准化并写入规范当中。对DOM做的两个主要扩展是Selectors API和HTML5,再介绍一些专有扩展。 11.1选择符API(Selectors API)JQuery中就有的一项功能,根据CSS选择符选择某个模式匹配的DOM元素。Selectors API是由W3C发起制定的一个标准。致力于让浏览器原生支持CSS查询。把这个功能变成原生API后,解析和树查询操作可以在浏览器内部通过编译后的代码来完成,极大地改善了性能。Selectors API Level 1的核心是两个方法:querySelector()和querySelectorAll()。在兼容的浏览器中,可以通过Document及Element类型的实例来调用他们。现在完全支持Selectors API Level 1的浏览器有IE8+、Firefox 3.5+、Safari 3.1+、Chrome和Opera10+。(兼容性还挺好的)11.1.1querySelector()方法querySelector()方法接收一个css选择符,返回与该模式匹配的第一个元素,如果没有找到,返回null。例子://取得body元素var body = document.querySelector("body");//取得ID为“myDiv”的元素var myDiv = document.querySelector("#myDiv");//取得类为"selected"的第一个元素var selected = document.querySelector(".selected");//取得类为"button"的第一个图像元素var img = document.body.querySelector("img.button"); //这里为什么要加一个body属性?亲测加不加都可以啊如果只能兼容到IE8那当然没什么人用啊,还不如用Jquery。通过Document类型调用querySelector()方法时(document.querySelector()),会在文档元素的范围内查找匹配的元素。而通过Element类型调用querySelector()方法时,只会在该元素后代元素范围内查找匹配元素。(在这里体会下Document类型和Element类型的定义。Document类型表示整个文档,是一组分层节点的根节点(<html>是它的子节点))。11.1.2querySelectorAll()方法该方法返回的是所有匹配的元素而不仅只有一个,这个方法返回的是一个NodeList的实例。如果没有找到匹配的元素,NodeList就是空的。能够调用querySelectorAll()方法的类型包括Document、DocumentFragment和Element。例子跟上面的相同。返回单个元素的方法跟访问ChildNodes类数组的方法相同,可按索引访问可按名字访问。11.1.3matchesSelector方()法Selectors API Level2规范为Element类型新增了一个matchesSelector方法。也是接收一个CSS选择符为参数,如果调用元素与该选择符匹配,则返回true。反之返回false。例子:if(document.body.matchesSelector("body.page1")){ //true}截止2011年底,还没有浏览器支持这个方法。([捂脸]那还用个毛,不想写了)。I书后面说IE9+、Safari、Firefox和Chrome都根据自己的前缀实现了这个方法,要用还要自己再封装一个函数。搏死。11.2元素遍历因为有些浏览器将空白符视为节点有些不视为节点,导致childNodes和firstChild等属性的行为不一致。为了弥补这个差异,Element Traversal规范新定义了一组属性解决这个问题。支持Element Traversal规范的浏览器有:IE9+、Firefox3.5+、Safari 4+、Chrome和Opera 10.兼容性也一般啊。·childElementCount:返回子元素个数(不包括文本节点和注释);(不包括文本节点就说明不包括空白符)·firstElementChild:指向第一个子元素;firstChild元素版(元素版?亲测返回的不是空白符了,是第一个元素节点,大概因为这样叫元素版吧,文本节点不是元素类型(Element)是Text类型)。·lastElementChild:lastChild元素版。·previousElementSibling:previousSibling元素版。·nextElementSibling:nextSibling的元素版。11.3HTML511.3.1与类相关的扩充1.getElementsByClassName()方法原来getElementsByClassName()方法是HTML5添加的。这个方法最早出现在JavaScript库中,是通过既有的DOM功能实现的,而原生的实现具有极大的性能优势。getElementsByClassName()方法可以接收一个包含一或多个类名的字符串。返回的也是一个NodeList。传入多个类名时,类名的先后顺序不重要。例子://取得所有类中包含“username”和“current”的元素,类名的先后顺序无所谓var allCurrentUsernames = document.getElementsByClassName("username current");//取得类名为“selected”中ID名为“myDiv”的元素var selected = document.getElementsByClassName("selected").getElementById("myDiv")因为返回的是一个NodeList,所以与其他返回NodeList的方法一样具有性能问题。支持getElementsByClassName()方法的浏览器有IE9+、Firefox 3+、Safari 3.1+,Chrome和Opera 9.5。(五大都支持,只是支持程度跟ECMAScript5定义的方法差不多)。2.classList属性在以前操作类名,需要通过className属性添加,删除和替换类名。因为className中是一个字符串,即使只修改字符串的一部分,也必须每次都设置整个字符串的值。比如下面的HTML代码:<div class="bd user disabled"><div>这个div元素有三个类名,只需要把这三个类名拆开,删除不想要的那个,然后再把其他类名拼成一个新字符串。例子://删除“user”类//首先,取得类名字符串并拆分成数组var classNames = div.className.split(/\\s+);//找到要删除的类名var pos=-1,i,len;for(i=0, len=classNames.length; i<len; i++){ if(classNames[i] == "user"){ pos=i; break; }}//删除类名className.splice(pos,1); //书里是className.splice(i,1)应该是打错了吧//把剩下的类名拼成字符串并重新设置div.className = classNames.join(" ");为了将类名“user”从类名中删除,这些代码都是必须的。HTML5新增了一种操作类名的方法,就是为所有元素添加classList属性。这个属性是新集合类型DOMTokenList的实例。DOMTokenList也有leng属性,表示自己包含多少元素(这个元素不是值element类型吧)。这个新类型定义了如下方法:·add(value):将给定的字符串值添加到列表中,如果值已经存在了,就不添加。·contains(value):判断列表是否存在给定的值,如果存在返回true,否则返回false。·remove(value):从列表删除给定字符串。·toggle(value):如果列表存在给定的值,就删除它,如果没有,添加它。这样,前面的代码用下面一行代码就可以代替了:div.classList.remove("user");deimo,支持classList属性的浏览器只有Firefox 3.6+、Chrome。11.3.2焦点管理可以让开发人员知道哪个元素被focus,或者指定某元素被focus。HTML5添加了document.actievElement属性。这个属性个始终引用DOM中当前获得焦点的元素。元素获得焦点的方式有:页面加载、用户输入和在代码中调用focus()方法。例子:var button = document.getElementById("myButton");button.focus();console.log(document.activeElement === button); //true默认情况下,文档刚刚加载完成时,document.activeElement属性中保存的是document.body元素的引用。文档加载期间,document.activeElement的值为Null。还有一个新增的方法:document.hasFocus()方法。用于确定文档是否获得了焦点。(hasFocus()不能用来判断元素是否获得焦点)通过检测文档是否获得焦点,可以知道用户有没有在与页面交互。实现了这两个属性(一个属性一个是方法吧)的浏览器包括IE4+、Firefox 3+、Safari 4+、Opera 8+。兼容性不错。11.3.3HTMLDocument的变化HTML5扩展了HTMLDocument。增加了新功能。什么是HTMLDocument?第十章讲过,HTMLDocument继承自Document类型,是个构造函数,Document对象是HTMLDocument的一个实例。1.readyState功能Document的readyState属性有两个可能的值:·loading:正在加载的文档。·complete:已经加载的文档。可以用这个功能实现一个提示文档已经加载完成的指示器。用法如下:if(document.readyState == ‘complete’){ //执行操作}支持readyState属性的浏览器有:IE4+、Firefox 3.6+、Safari、Chrome和Opera 9+。2.兼容模式自从IE6开始区分渲染页面的模式是混杂还是标准,检测页面兼容模式就成为浏览器必要功能。IE为此首先为document添加了一个名为compatMode的属性,这个属性就是为了告诉开发人员浏览器现在在用什么模式。在标准模式下,document.compatMode的值等于“CSS1Compat”,而在混杂模式下,document.compatMode的值为“BackCompat”。例子:if(document.compatMode == "CCS1Compat"){ console.log("标准模式");}else{ console.log("混杂模式");}各大浏览器都支持了readyState属性3.head属性document.body可以直接引用<body>元素,<head>元素说它也要,所以就有了document.head属性,不过支持的浏览器只有Chrome和Safari 5。document.head == document.getElementsByTagName("head")[0];11.3.4字符集属性HTML5新增了几个与文档字符集有关的属性。其中,charset属性(!!一直在用的charset属性是HTML5制定后才有的??)表示文档中实际使用的字符集。默认情况下,这个属性的值为“UTF-16”。但可以通过<meta>元素、响应头部或直接设置charset属性修改这个值。例子:document.charset = "UTF-8";另一个属性是defaultCharset,表示当前文档默认的字符集根据默认浏览器及操作系统的设置。如果用户没有使用默认的字符集,那charset和defaultCharset属性的值可能会不一样。五大浏览器都支持document.charset。支持document.defaultCharset属性的浏览器只有IE、Safari和Chrome。(但是亲测这几个浏览器这个属性的值都是undefined哦,除了Safari返回"ISO-8859-1"这是浏览器的字符集吧不是操作系统的)11.3.5自定义属性从HTML5规定开始,可以为元素添加非标准属性,但要添加data-前缀。目的是为元素提供与渲染无关的信息,或者添加语义化信息。添加了自定义属性后,可以通过元素的dataset属性来访问自定义属性的值。dataset属性的值是DOMStringMap的一个实例。是一个名值对儿的映射。在这个映射中,每个data-name形式的属性都有一个对应的属性,只不过属性名没有data-前缀。比如自定义属性是data-myname,映射中对应的属性就是myname。在跟踪链接或混搭应用中,通过自定义属性可以方便地知道点击来自页面哪个部分。支持dataset属性的浏览器只有Firefox 6+和Chrome。11.3.6插入标记(标记就是HTML元素)以下与插入标记相关的DOM扩展已经纳入了HTML5规范。1.innerHTML属性在读模式下,inner属性返回调用元素的所有子节点(包括元素,注释和文本节点)对应的HTML标记。在写模式下,inner属性会根据给定的值创建新的DOM树,用这个DOM树完全替换调用元素的所有子节点。如果设置的值仅是文本而没有HTML标签,那么结果就是设置纯文本。读模式下,不同浏览器返回的文本格式有所不同。IE和Opera会将所有标签转换为大写形式,而Safari、Chrome和Firefox会原原本本地按照文档中的格式返回HTML,包括空格和缩进。写模式下,innerHTML的值会被解析为DOM字数,因为它的值被认为是HTML,所以其中的所有标签都会按照浏览器处理HTML的标准方式转换为元素(同样,转换的结果也是因浏览器而异)。为innerHTML设置的包含HTML的字符串与解析后innerHTML的值不大相同,看下面例子:div.innerHTML = "Hello & welcome,<b> \\"reader\\"!</b>";以上操作得到的结果:<div id="div">Hello &amo; welcome,<b>&quot;reader&quot;!</b></div>原因在于返回的字符串是根据原始HTML字符串创建的DOM树经过序列化之后的结果。使用innerHTML要在文本中添加特殊符号时要记得这一点。不过亲测在innerHTML中写特殊符号,在网页中显示并不会被重新编码,只是用innerHTML返回的值是被转码的。例子:<div id="dd2"></div>dd2.innerHTML=" <&> "; //页面中可以显示<&>console.log(dd2.innerHTML); // &lt;&amp;&gt;使用innerHTML属性也有一些限制。比如,在大多数浏览器中,通过innerHTML插入<script>元素并不会执行里面的脚本。不过大多数浏览器都支持以直观的方式插入<style>元素,例如:div.innerHTML = "<style type=\\"text/css\\">body{background-color:red;}</style>";但在IE及跟早的版本中还是不行,原理就不写了。除非像下面这样给它前置一个“有作用域的元素”,事后再删除:div.innerHTML = "_<style type=\\"text/css\\">body{background-color:red;}</style>";div.removeChild(div.firstChild);并不是所有元素都支持innerHTML属性。不支持innerHTML属性的元素有:<col>、<colgroup>、<frameset>、<head>、<html>、<style>、<table>、<tbody>、<thead>、<tfoot>、<tr>。此外,在IE8及更早版本中,<title>元素也没有innerHTML属性。✎:要注意的是,在用innerHTML里的HTML字符串替换节点原来的子节点前,要删除脚本节点和事件处理程序属性。2.outerHTML属性读模式下,返回调用它的元素及所有子节点的HTML标签。在写模式下,outerHTML会根据给定的HTML字符串创建新的DOM子树,然后用这个DOM子树完全替换调用元素(与innerHTML的不同之处,连调用它的元素也被返回)。例子:<div id="content"> <p></p> <ul> <li></li> <li></li> <li></li> </ul></div>如果在div上调用outerHTML,会返回与上面一样的代码。包括<div>本身。使用outerHTML属性以下面的方式设置值:div.outerHTML = "<p>This is a paragraph.</p>";结果就是新创建的<p>元素会取代DOM树中的这个<div>元素。等效于下列代码:var p = document.createElement("p");p.appendChild(document.createTextNode("This is a paragraph"));div.parentNode.replaceChild(p,div);测试了下replaceChild方法:两节点相互替换,子节点也会被一起替换过去。支持outerHTML属性的浏览器有IE4+、Safari 4+、Chrome 和 Opera8+。Firefox 7及之前版本都不支持outerHTML属性。(innerHTML全浏览器全版本支持,outerHTML有版本限制,大概就是少用的原因。)3.insertAdjacentHTML()方法一个IE、Firefox 8+、Safari、Opera和Chrome支持的方法,本来不想写,好像支持的还挺好。它接收两个参数:插入的位置和要插入的HTML文本。第一个参数必须是下列值之一:·“beforebegin”:在当前元素之前插入一个紧邻的同辈元素;·“afterbegin”:在当前元素之下插入一个新的子元素或在第一个子元素之前再插入新的子元素;·“beforeend”:在当前元素之下插入一个新的子元素或在最后一个子元素之后再插入新的子元素;·“afterend”:在当前元素之后插入一个紧邻的同辈元素。命名不够清晰啊,有点难记。如果浏览器无法解析这些字符串,就会抛出错误。以下是方法的基本用法实例://作为前一个同辈元素插入element.insertAdjacentHTML("beforebegin","<p>Hello World</p>");//作为第一个子元素插入element.insertAdjacentHTML("afterbegin","<p>Hello World</p>");//作为最后一个子元素插入element.insertAdjacentHTML("beforeend","<p>Hello World</p>");//作为后一个同辈元素插入element.insertAdjacentHTML("afterend","<p>Hello World</p>");4.内存与性能问题使用本节介绍的方法替换子节点可能会导致浏览器的内存占用问题。尤其在IE中,问题更加明显。在删除带有事件处理程序或引用了其他JavaScript对象子树时,就有可能导致内存占用问题。假设有一个带有事件处理程序或引用了一个JavaScript对象作为属性(什么意思?)的元素,在使用前述某个属性将元素从文档树中删除后,元素与事件处理程序(或JavaScript对象)之间的绑定关系在内存中并没有一并删除。如果这种情况频繁出现,页面占用内存数量就会明显增加。因此,使用innerHTML()、outerHTML属性或insertAdjacentHTML()方法时,最好先手工删除要被替换元素的所有事件处理程序和JavaScript对象属性。因为在设置innerHTML或outerHTML时,会创建一个HTML解析器,这个解析器是在浏览器级别的代码(通常是C++编写的)基础上运行的,因此比执行JavaScript快得多。不可避免地,创建和销毁HTML解析器也会带来性能损失。所以最好把使用innerHTML等方法的次数控制在合理范围内。例如下面这个频繁操作的例子:for(var i=0,len=values.length; i<len; i++){ ul.innerHTML += "<li>"+values[i] + "</li>"; //避免这种频繁操作!!}最好的做法是单独构建字符串,然后再一次性将字符串赋给innerHTML。例子:for(var i=0,len=values.length; i<len; i++){ itemsHtml += "<li>"+values[i] + "</li>";}ul.innerHTML = itemsHtml;1.3.7scrollIntoView()方法scrollIntoView()方法可以在所有HTML元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。如果给这个方法传入true作为参数,或者不传入任何参数,那么窗口滚动之后会让调用元素的顶部与视口顶部尽可能平齐。如果传入false,调用元素会尽可能全部出现在视口中(可能的话,调用元素的底部会与视口顶部平齐(exm?这样不就看不见了吗?)),例如://让元素可见document.forms[0].scrollIntoView();实际上,为某个元素设置焦点也会导致浏览器滚动并获得焦点的元素。不知道为什么在浏览器用focus()为什么没效果,用了document.activeElement返回的值始终是<body>。推测focus()方法这是对一些表单元素起作用。亲测就是这样。支持scrollIntoView()方法的浏览器有IE、Firefox、Safari和Opera。(没有Chrome哦)11.4专业扩展专业扩展就不写啦,不通用。“有空”再看看吧。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"《JavaScript高级程序设计》 第十章DOM","date":"2017-12-05T12:37:13.000Z","path":"2017/12/05/《JavaScript高级程序设计》-第十章DOM/","text":"现在还搞不清楚node类型、document类型、html、类型的区别,他们是对象吗?DOM(document object module)是针对HTML、XML文档的一个API(应用程序接口)。DOM描绘了层次化的节点树,允许开发人员添加、移除和修改页面的某一部分。IE(应该是说IE几开始呢?)中的所有DOM对象都是以COM对象的形式实现的。这以为着IE中的DOM对象与原生JavaScript对象的行为或活动特点并不一致。 10.1节点层次(文档元素是什么)DOM将HTML或XML文档描绘成一个由多层节点构成的结构(DOM描绘了这个结构哦)。节点分为几种不同的类型。每种类型分别表示文档中不同的信息及标记。节点都有各自的特点、数据和方法。节点之间的关系构成了层次。所有页面标记则表现为一个以特定节点(文档节点,<html>是文档节点的子节点)为根节点的树形结构。例子:<html> <head> <title>Sample Page</title> </head> <body> <p>Hello World</p> </body></html>文档节点是每个文档(比如HTML文档,不知道XML文档是不是)的根节点。在这个例子中(只是这个例子里文档节点只有一个子节点),文档节点只有一个子节点,即<html>元素,我们称之为文档元素。文档元素是文档的最外层元素,文档中所有其他元素都包含在文档元素中。每个文档只能有一个文档元素。在HTML中,<html>元素始终都是文档元素。在XML中,没有预定义的元素,因此任何元素都可能是文档元素。每一段标记都可以通过树中的一个节点来表示:HTML元素通过元素节点表示(HTML元素是什么?元素节点是什么?),特性(attribute)通过特性节点表示(特性节点?),注释通过注释节点表示。总共有12种节点类型,这些类型都继承自一个基类型。10.1.1Node类型(12个节点类型,判断是某种节点类型的方法)DOM1级定义了一个Node接口,该接口由DOM中的所有类型节点实现(实现是什么意思?)。Node接口在JavaScript中是作为Node类型实现的。除了IE外,其他所有浏览器都可以访问到这个类型(Node类型)。JavaScript中的所有节点类型都继承自Node类型,因此所有节点类型都共享着相同的基本属性和方法。每个节点都有一个nodeType属性,用于表明该节点的类型。节点类型有12个,任何节点其类型必居其一,无出其右。·Node.ELEMENT_NODE(1) element_node·Node.ATTRIBUTE_NODE(2) attribute_node·Node.TEXT_NODE(3) text_node·Node.CDATA_NODE(4) cdata_node·Node.ENTITY_REFERENCE_NODE(5) entity_reference_node·Node.ENTITY_NODE(6) entity_node·Node.PROCESSING_INSTRUCTION_NODE(7) processing_instruction_node·Node.COMMENT_NODE(8) ·Node.DOCUMENT_NODE(9) document.node·Node.DOCUMENT_TYPE_NODE(10) document_type_node·Node.DOCUMENT_FRAGMENT_NODE(11) document_fragment_node·Node.NOTATION_NODE(12) notation_node通过比较上面的常量,可以很容易地确定节点的类型,例如:if(someNode.nodeType == Node.Element_NODE){ console.log("Node is an element"); //IE中无效,IE中不能访问Node类型,不能说没有Node类型吧}通过比较可以确定someNode是不是一个元素。由于IE无法访问Node;类型,因此上面的代码在IE会导致错误。为了确保兼容,最好将nodeType属性与数字值进行比较。例子:if(someNode.nodeType == 1){ //适用所有浏览器 console.log("Node is an element.")}并不是12个节点类型都受到Web浏览器的支持。开发人员最常用的就是元素节点和文本节点,本章后面将详细讨论每个节点类型的受支持情况及使用方法。1.nodeName和nodeValue属性使用nodeName和nodeValue属性可以了解节点的具体信息,使用前,最好先检查节点类型,判断是不是一个元素。对于元素节点,nodeNode中保存的始终都是元素的标签名(大写),nodeValue始终是null。2.节点关系(几个属性,NodeList类数组对象)就是父子这类的关系。每个节点都有一个childNodes属性,其中保存着NodeList对象。Nodelist是一种类数组对象。保存着一组有序的节点。要注意的是,虽然可以通过方括号语法来访问NodeList的值,其这个对象有length属性,但它并不是Array的实例。NodeList对象的独特之处在于:它是基于DOM结构动态执行查询的结果,DOM结构的变化能立即反应在NodeList对象中。所以说NodeLIst对象是有生命、会呼吸的对象。下面展示了访问在NodeList中的节点的方法:var firstChild = someNode.childNodes[0];var secondChild = someNode.childNodes.item(1);var count = someNode.childNodes.length;两种访问方法无差别,但第一种最常用。亲测IE8里nodeName,nodeType这些属性都是可以访问的,不知道为什么前面说无法访问。书里说“IE没有公开Node类型的构造函数”。我们可以把arguments这个类数组对象用Array.prototype.slice()方法转换为数组,NodeList对象也可以转换为数组。例子://IE8及之前版本无效var arrayOfNodes = Array.prototype.slice.call(someNode.childNodes,0);由于IE8及之前将NodeList实现为一个COM对象,而我们不能像使用JScript对象那样使用这种对象,所以上面的代码会报错。要在IE中将NodeList转换为数组,必须手动枚举所有成员,下面是在所有浏览器都可以运行的代码:function convertToArray(nodes){ var array = null; try{ array = Array.prototype.slice.call(node,0); }catch(ex){ array = new Array; for(var i = 0; i<nodes.length; i<len; i++){ array.push(nodes[i]); } } return array;}按能力检测的原则,先创建最容易实现的方式,报错了,就进入catch块,这是另一种检测怪癖的形式。每个节点都有一个parentNode属性,指向文档树中的父节点。拿ChildNodes列表(不知道书里这样说对不对,ChildNodes列表,ChildNodes里的NodeList是个类数组,列表是NodeList,那这么说也对)举例,他们的parentNode都指向同一个节点。他们相互之间是同胞节点,通过使用列表中每个节点的previousSibling和nextSibling属性,可以访问同一列表中的其他节点。hasChildNodes()也是一个非常有用的方法,该方法在节点至少包含一个子节点的情况下就会返回true。可以判断某节点有没有子节点。所有节点都有的最后一个属性是ownerDocument,该属性指向表示整个文档的文档节点(换句话说,文档节点表示整个文档)。这种关系表示的是任何节点都属于它所在的文档。通过这个属性,我们就不用使用parentNode属性层层往上回溯,直接可以访问文档节点(可是访问文档节点要干嘛?)。3.操作节点(使用appendChild()的特殊情况)因为关系指针(parentNode、previousSibling、nextSibling等)都是可读的(想起上次写代码的时候好像想修改父节点的值什么的,怪不得无端报错),所以DOM提供了一些操作节点的方法(DOM提供的,不是node类型、document类型等提供的)。最常用的是appendChild(),用于向childNodes列表的末尾添加一个节点。appendChild()的返回值是新增的那个节点。例子:var returnNode = someNode.appendChild(newNode); console.log(returnedNode == newNode); //trueconsole.log(someNode.lastChild == newNode); //true如果传入的节点已经是文档的一部分(不只是childNodes的一部分),那结果就是将节点从原来的位置移(不是复制)到新位置。因为任何DOM节点不能同时出现在文档中的多个位置。因此,如果把父节点中的第一个子节点用appendChild()重新传入父节点中,那么该节点就会变成父节点的最后一个子节点,而不是复制出一份。例子://someNode有多个子节点var returnedNode = someNode.appendChild(someNode.firstChild);console.log(returnedNode == someNode.firstChild); //falseconsole.log(returnedNode == someNode.lastChild); //true亲自实验appendChild()方法很无语,在部分浏览器中,节点之间的空格也会被视为一个节点,如果是插入firstChild很可能插入的是空格,appendChild()方法返回的值就是“#text”,去了括号就添加成功。例子:<div id="dd"> <div></div> <div></div> <div></div> </div> <div id="dd1"> <div id="subdd1"></div> //如果是这种情况,returned=#text <div></div> <div></div> </div></body><script>var dd1 = document.getElementById("dd1");var dd = document.getElementById("dd");var returned = dd.appendChild(dd1.firstChild);console.log(returned) //#text</script> 把空格去掉后添加成功,看代码运行后的文档树也证明了如果是文档中已经存在的节点,则该节点会转移到插入目的地的最后,而不是复制一份。如果想把节点放在childNods列表的特定位置,而不是末尾,可以使用insertBefore()方法。该方法接受两个参数:要插入的节点和作为插入参照的节点。插入节点后,被插入的节点会变成参照节点的前一个同胞节点(previousSibling),该放回返回的也是插入的那个节点。如果第二个参数,插入参照点是Null,则insertBefore()与appendChild()无异。例子://插入后成为最后一个子节点returnedNode = someNode.insertBefore(newNode,null);console.log(newNode == someNode.lastChild);//插入后成为第一个子节点var returnNode = someNode.insertBefore(newNode,someNode.first.firstChild);console.log(returnNode == newNode);console.log(newNode == someNode.firstChild);//插入到最后一个子节点前面returnedNode = someNode.insertBefor(newNode,someNode.lastChild);console.log(newNode == someNode.childNodes[someNode.childNodes.length-2]);前面两个方法都是插入节点的,要移除节点,可以使用下面介绍的这个replaceChild()方法。该方法接收两个参数:要插入的节点和要替换的节点。被替换的节点作为这个方法的返回值,并从文档树中被移除。从技术上讲,这个节点还在文档中,但它已经在文档中没有了自己的位置(好惨,被人上位)。如果想直接移除而不是替换,可以使用removeChild()方法。与replaceChild()方法一样,被移除的节点仍然为文档所有,但已经失去了自己的位置。可以发现,上面的4个方法操作的都是某节点的子节点,所以,要使用这4个方法就需要取得节点的父节点。4.其他方法不是所有节点都可以有子节点,所以上面的方法不是每个节点都有。有两个方法是所有类型的节点都有的。第一个是cloneNode(),顾名思义复制节点。该方法接收一个布尔值参数。参数为true,执行深复制,把节点及其整个子节点树复制过去。参数为false,执行浅复制,只复制节点本身。复制后返回的节点就归文档所有,但是它无依无靠,因为没有为克隆节点指定父元素。所以还要通过appendChild()、insertBefore()或replaceChild()将它添加到文档中。例子:假设有下面的HTML代码:<ul id="myList"> <li>item 1</li> <li>item 2</li> <li>item 3</li></ul>假设我们已经取得<ul>元素的引用并保存在变量myList中,下列代码就是cloneNode()方法的两种模式:var deepList = myList.cloneNode(true); //要先把克隆结果保存在一个变量中,很“不直接”的一个方法console.log(deepList.childNodes.length); //3(IE<9)或7(4个空格)var shallowList = myList.cloneNode(false);console.log(shallowList.childNodes.length); //0IE9之前的版本不会为空白符创建节点。✎:cloneNode()不会复制添加到DOM节点中的JavaScript属性,例如事件处理程序。这个方法只复制特性(id,class,style等)、子节点(true时),其他一切都不会复制。IE在此存在一个BUG,连事件处理程序也会复制过去,所以作者建议在复制前移除事件处理程序(指绑定在节点里面的那种事件处理程序吧?0级事件处理程序)。10.1.2Document类型JavaScript通过Document类型表示文档(这句话有玄机,不是很懂)。在浏览器中,document对象是HTMLDocument(继承自Document类型)的一个实例,表示整个HTML页面,而且,document对象是window对象的一个属性(global对象不是window对象的属性,是window对象可以代表global对象),所以我们可以把document对象作为全局对象来访问。Document节点(文档节点)具有下列特征:·nodeType的值为9(节点类型:Node.DOCUMENT_NODE(9));·nodeName的值为“#document”;·nodeValue的值为null;·parentNode的值为null;·ownerDocument的值为null;(其他节点的ownerDocument指向文档节点,所以它的ownerDocument是他本身)·其子节点可能是一个DocumentType(最多一个)、Element(最多一个)、ProcessingInstruction或Comment。Document类型可以表示HTML页面或者其他基于XML的文档(Document类型可以表示这两种文档?)。不过最常用的是作为HTMLDocument实例的document对象(HTMLDocument继承自Document类型,document对象又是HTMLDocument的实例,所以HTMLDocument也是一个对象,亲测,HTMLDocument是一个构造函数)✎:在Firefox、Safari、Chrome和Opera中,可以通过脚本访问Document类型的构造函数和原型。但在所有浏览器都可以访问HTMLDocument类型的构造函数和原型,包括IE8。1.文档的子节点虽然DOM标准规定Document节点(文档节点)的子节点可以是DocumentType、Element、ProcessingInstruction或Comment,但还有两个内置的访问其子节点的快捷方式(这个虽然…但是句真的不是病句吗?)。第一个就是documentElement属性,该属性始终指向HTML页面中的<html>元素(包括里面的子节点!不是只有<html>这个标签!)。另一个方式是通过childNodes列表访问文档元素。但通过documentElement属性更快捷、更直接。例子:<html> <body> </body></html>下面通过两种方法访问<html>元素:var html = document.documentElement;console.log(html === document.childNodes[0]); //htmlconsole.log(html === document.firstChild); //html这个例子说明三个方法都指向<html>元素。作为HTMLDocument的实例,document对象还有一个body属性(两者有关系吗?)直接指向<body>元素。因为<body>使用频率非常高,所以document.body在JavaScript中出现的频率也非常高。所有浏览器都支持document.documentElement和document.body属性(终于有所有浏览器都支持的属性了!)。Document的另一个子节点是DocumentType。即<!DOCTYPE>。通常将<!DOCTYPE>标签看一个与文档其他部分不同的实体。可以通过doctype属性来访问它的信息。与document.body不同,浏览器对document.doctype属性的支持差别很大,书中说出了不同,最后说因为支持不一致,所以这个属性用处有限,就不写了。从技术上说,出现在<html>元素外部的注释(Comment)也算是文档的子节点。但不同的浏览器对注释算不算子节点的看法也是有很大差异的。这种浏览器差异也使<html>元素外部的注释没什么用处,而且要在<html>元素外注释什么啊。2.文档信息(title、URL、domain和referrer)作为HTMLDocument对象的一个实例,document对象还有一些标准的Document对象没有的属性(document对象和Document对象是不同的哦)。这些属性提供了document对象所表现的网页的一些信息。其中第一个属性是title。包含着<title>元素中的文本(体会一下,title属性不是<title>标签,是里面的文本)。可以通过document.title修改<title>元素里的文本信息。接下来要介绍的3个属性与网页的请求有关,他们是URL、domain和referrer。URL属性中包含完整的URL,domain属性中只包含页面域名,referrer属性中保存着链接到当前页面的那个页面的URL,如果网页没有来源,referrer属性中可能包含空字符串(是空字符串,不是null不是null)。所有这些信息都保存于请求的HTTP头部。亲测window.URL和document.URL是不一样的啊,window.URL是个构造函数,不知道是干嘛的前面忘了有没有讲过。这三个属性中,只有domain是可以设置的。而且不是什么值都可以设置。如果URL中包含一个子域名例如:p2p.wrox.com,那只能将domain设置为“wrox.com”,不能将这个属性设置为URL中不包含的域://假设页面来自p2p.wrox.com域document.domain = "wrox.com" //成功document.domain = "nczonline.net"//出错!上面的代码是将域由紧(tight)的改成松(loose)的是可以的,但不能把“松”的改成“紧”的://假设页面来自p2p.wrox.com域document.domain = "wrox.com" //改成松散的(成功)document.domain = "p2p.wrox.com" //紧绷的(失败)当页面包含来自不同子域的框架或内嵌框架时,出于安全考虑不同子域的页面无法通过JavaScript通信。但是如果将每个页面的document.domain设置为相同的值,这些页面就可以互相访问对方包含的JavaScript对象了。(不知道这样设置会不会使页面跳转,应该不会吧。框架就是frame不是jQuery这种吧2333)3.查找元素取得元素的操作可以使用document对象的几个方法来完成。其中Document类型为此提供了两个方法:getElementById()和getElementsByTag()(这两个不是document对象的方法吗,为什么说是Document类型提供的)不细说了,就讲个IE7及以前版本的怪癖:name特性与给定ID匹配的表单元素(<input>、<textarea>、<button>、<select>)也会被getElementById()返回。所以现在都要求表单元素的name特性与id相同。了解了解这种历史问题。说说getElementsByTag(),在HTML文档中,这个方法会返回一个HTMLCollection对象,作为一个“动态”集合,该对象与NodeList非常类似。也可以使用方括号或item()方法来访问HTMLCollection中的项。这个对象中元素的数量则可以通过其length属性来取得。HTMLCollection对象还有一个方法,叫namedItem(),使用这个方法可以通过元素的name特性取得集合中的项。例子://假设还有其他<img>元素<img src="myimage.gif" name="myImage">var images = document.getElementsByTag("img");var myImage = images.namedItem("myImage");除了提供按索引(0,1,2,3…)访问项,HTMLCollection还支持按名称访问项,例子:var myImage = images["myImage"]; //用法跟按索引访问相同原理:对HTMLCollection而言,当我们用索引访问项的时候,后台就会调用item(),当我们传入的是字符串的时候,后台就会调用namedItem()。亲测childNodes属性返回的不是HTMLCollection,还是叫类数组吧,HTMLCollection是HTMLCollection。第三个方法,也是只有HTMLDocument类型才有的方法:getElementsByName()。这个方法返回的也是一个HTMLCollection对象。也可以用item()和namedItem()方法访问项。但是name是可以重复的,所以用namedItem()访问的时候只会取得第一项。4.特殊集合除了属性和方法document对象还有一些特殊的集合。这些集合都是HTMLCollection对象。为访问文档常用的部分提供了快捷方式(书中没说这几个集合在IE能不能用啊),包括:·document.anchors:包含文档中所有带name特性的<a>元素;·document.forms:包含文档中所有<form>元素;·document.links:包含文档中所有带href特性的<a>元素(<a>元素?);·document.images:包含文档中的所有img元素。5.DOM一致性检测由于DOM分为多个级别,也包含多个部分,因此检测浏览器实现了DOM的哪些部分就十分必要了。document.implementation属性就是为此提供相应信息和功能的对象,与浏览器对DOM的实现直接对应。DOM1级只为document.implementation规定了一个方法,即hasFeature()。这个方法接收两个参数:要检测的DOM功能的名称,版本号。如果浏览器支持,则返回true。例子:var hasXmlDom = document.implementation.hasFeature("XML","1.0");晚上补图hasFeature()也有缺点,即有些浏览器在有些DOM功能的实现上只实现了一部分,但hasFeature()还是返回true。所以在使用DOM的某些特殊功能之前,用hasFeature()的同时使用能力检测。6.文档写入write()、writeln()、open()、close()方法。write()和writeln()方法可以输出HTML代码,创建DOM元素,例子:<script> document.write("<strong>"+(new Date()).toString()+"</strong>");</script>此外,还可以动态地包含外部资源,例如JavaScript文件,写入的时候要注意将</script>标签的正斜杠和标签中双引号用反斜杠转义。例子:<script> document.write("<script type=\\"text/javscript\\" src=\\"file.js\\">"+"<\\/script>");上面的例子都是在页面被呈现的过程中使用document.write()方法,如果是在文档已经加载结束之后再调用document.write()方法,那么输出的内容会重写整个页面。方法open()和close()是用来打开和关闭网页的输出流,如果是在页面加载期间使用write()或writeln()方法,则不需要用到这两个方法。10.1.3Element类型除了Document类型,Element类型算是Web编程中最常用的元素。Element类型用与表现XML或HTML元素,提供了对元素标签名、子节点及特性的访问。Element节点具有以下特征:·nodeType的值为1;(Node.ELEMENT_NODE(1))·nodeName的值为元素的标签名;·nodeValue的值为null;·parentNode可能是Document或Element;·子节点可能是Element、Text、Comment、ProcessingInstruction、CDATASection或EntityReference。要访问元素的标签名,即可以使用nodeName属性,也可以使用tagName属性,这两个属性会返回相同的值。但要注意:在HTML文档中,返回的标签名都是大写的;在XML文档中,返回的标签名跟源文档的大小写一致,所以在不确定自己的脚本会用在HTML文档或XML文档的情况下,建议在比较之前将标签名转换为相同的大小写格式。✎:可以在任何浏览器中通过脚本访问Element类型的构造函数及原型。包括IE8及之前版本。在Safari 2 之前和Opera 8之前的版本中,不能反问Element类型的构造函数。(访问Element类型的构造函数有什么用?)1.HTML元素所有HTML元素都由HTMLElement类型表示(不是通过Element类型表示),不是通过这个类型,也是通过它的子类型来表示。HTMLElement类型直接继承至Element类型并添加了一些属性。添加的这些属性分别对应于每个HTML元素中都存在的下列标准特性:·id:元素在文档中的唯一标识符;·title:有关元素的附加说明信息;·land:元素内容的语言代码,很少使用;·dir:语言方向;·className:与元素的class特性相对应。上面的特性都可以被取得和被修改。(不像parentNode、lastChild可读不可写)前面说过所有HTML元素都是用HTMLElement类型或者更具体的子类型来表示的。下表展示了各元素和他们都是由HTML类型或HTML类型的什么子类型表示的。晚上补图2.取得特性操作特性的DOM方法主要有三个:分别是getAttribute()、setAttribute()和removeAttribute()。这三个方法可以针对任何特性使用。注意:传递给getAttribute()方法的特性名与实际特性名相同,即使是className,也要用“class”访问。className只有在通过对象属性访问特性时(比如myList.className)才使用。如果给定名称的特性不存在,getAttribute()会返回null。特性的名称不区分大小写,即“ID”与“id”代表同一个特性。getAttribute()也可以取得自定义特性。BTW,根据HTML5规范,自定义特性应该加上data-前缀以便验证。有两类特殊的特性,他们虽然有对应的属性名,但属性的值与通过getAttribute()返回的值并不相同。第一类特性就是style,通过getAttribute()访问时,返回的style特性值返回的是CSS文本,而通过属性来访问它则会返回一个对象。亲测:通过属性访问返回的是所有CSS样式(即使没有设置)的对象,通过getAttribute()只返回定义在标签内style写的样式,在<style>里定义的不返回。书中说是因为“style属性是用于以编程方式访问元素样式的(本章后面讨论),因此style属性没有直接映射到style特性。”第二个特性是onclick这样的事件处理程序,如果用getAttribute()方法访问,会返回相应代码的字符串。如果是访问onclick属性,则返回一个JavaScript函数。由于这些差别,在通过JavaScript操作DOM时,开发人员不经常使用getAttribute()方法,而是直接使用对象的属性。只在取自定义特性值的情况下,才会使用这个方法。(我还以为经常用呢)3.设置特性setAttribute()方法。如果特性已存在,则替换原来的值。如果不存在,就创建并赋值。既可以操作HTML特性,也可以操作自定义特性。因为所有特性都是属性,所以直接给属性赋值就可以设置特性的值。例子:div.id="someOtherId";div.align="left";但是自定义特性就不可以那样做了:div.mycolor = "red";console.log(div.getAttribute("mycolor")); //null(IE除外)✎:因为setAttribute()在IE7及之前版本有BUG。作者推荐优先通过属性来设置特性。突然明白为什么JavaScript修改CSS样式要用“XX.style.width=XX”去修改具体样式,因为style是元素节点的属性,是个对象,样式都是style对象里的属性,所以要那样修改。一句话:具体样式是style属性的属性。最后一个方法是removeAttribute()。用于彻底删除元素特性,不仅删除特性的值,也会从元素完全删除特性。这个方法并不常用,但在序列化DOM元素时,可以通过它来确切地指定要包含哪些特性。4.attributes属性Element类型是唯一使用attributes属性的DOM节点类型。attributes属性中包含一个NamedNodeMap,与NodeList类似,也是一个“动态”的集合。元素的每个特性都由一个Attr节点表示并保存在NamedNodeMap对象中。NameNodeMap对象拥有下列方法:·getNamedItem(name):返回nodeName属性等于name的节点;·removeNamedItem(name):从列表中移除nodeName属性等于name的节点;·setNamedItem(node):向列表中添加节点,以节点的nodeName属性为索引;·item(pos):返回位于数字pos位置处的节点;其实简单来说,即使每个HTML元素都有特性,特性有attributes属性,这个属性是个NamedNodeMap,里面包含着这个特性的名称,特性的值。element类型的attributes属性的方法可以用来修改、添加特性,修改特性的值。下面举例用两种方法取得元素的id特性:var id = element.attributes.getNamedItem("id").nodeValue;//使用方括号语法通过特性名称访问节点的简写方式:var id = element.attributes["id"].nodeValue;记得ChildNodes属性也是一个NodeList类型,也能用方括号代替namedItem()访问。还有设置特性的值,删除特性,修改特性的值因为作者说这些attributes属性的方法都不够方便,开发人员更喜欢使用get/set/removeAttribute()方法。不过,作者说如果想要遍历元素特性,attributes属性可以派上用场。在需要将DOM结构序列化成XML或HTML字符串时,会涉及遍历元素特性,但是我也不知道序列化有什么用,直接贴针对IE7的怪癖的改进版吧:function outputAttribute(element){ var pairs = new Array(),attrName,attrValue,i,len; for(i=0, len=element.attributes.length; i<len; i++){ attrName = element.attributes[i].nodeName; attrValue = element.attributes[i].nodeValue; if(element.attributes[i].specified){ pairs.push(attrName + "=\\""+attrValue+"\\""); } } return pairs.join(" ");}这个改进的函数可以确保即使在IE7及更早的版本中,也会返回指定的特性。5.创建元素使用document.createElement()方法可以创建元素。该方法只接收一个参数,就是要创建元素的标签名。这个标签名在HTML文档中不区分大小写,在XML文档中区分大小写。创建的元素直接使用元素的特性属性设置特性(没必要用setAttribute()),然后再用appendChid()、insertBefore、insertAfter()、replaceChild()方法插入文档中。例子:var div = document.creatElement("div");div.id = "myNewDiv";div.className = "box";document.body.appendChild(div); //插入文档的<body>元素中在IE中,要用另一种方式使用createElement()方法,即为方法传入完整的元素标签。例子:var div = document.createElement("<div id=\\"myNewDiv\\" class=\\"box\\"><div>");这种方式是为了避开IE7及更早版本中用动态创建元素会带来的问题。问题在书中写了就不打进来了。只有IE支持这种写法,其他浏览器都不支持这种写法。createElement()方法会返回一个DOM元素的引用,利用这个引用把元素插入文档中,或对其加以增强(添加特性)。6.元素的子节点讲的就是子节点内存在空格的问题。例子:<ul id="myList"> <li>Item1</li> <li>Item2</li> <li>Item3</li></ul>如果是IE解析这些代码,那么<ul>元素有3个子节点,如果是其他浏览器,就会有7个子节点。如果需要通过childNodes属性遍历子节点,不要忘记浏览器之间的这种差别。这意味着在执行某些操作前,通常要先检查nodeType属性。例子:for(var i=0,len=element.childNodes.length; i<len; i++){ if(element.childNodes[i].nodeType == 1){ //执行某些操作 }}上面代码表示只在子节点的nodeType等于1(表示是元素节点)的情况下,才会执行这些操作。如果想通过某个特定的标签名取得子节点或后代节点,可以在元素上使用getElementsByTagName()方法。返回的结果只会返回当前元素的后代,注意:如果它包含更多层次的后代元素,那么哥哥层次中包含的元素也会被返回。(应该是变成二维数组)例子:var ul = document.getElementById("myList");var items = ul.getElementsById("li");10.1.4Text类型就是HTML代码中的文本。Text节点具有以下特征:·nodeType的值为3;(Node.TEXT_NODE)·nodeName的值为“#text”;(空格的nodeName就是“#text”)·nodeValue的值为节点所包含的文本;·parentNode是一个element;·不支持子节点节点中包含的文本可以通过nodeValue属性或data属性访问到。两个属性中包含的值相同。以下是操作节点中文本的方法:·appendData(text):将text添加到节点末尾。·deleteData(offset,count);从offset指定的位置开始删除count个字符。·insertData(offset,text):从offset指定的位置插入text。·replaceData(offset,count,text):用text替换从offset指定位置到offset+count为止处的文本。·splitText(offset):从offset位置将当前文本节点分成两个文本节点。·substringData(offset,count):提取从offset位置开始到offset+count为止处的字符串。文本节点还有length属性,保存着节点中字符的数目。而且nodeValue.length和data.length也保存同样的值。默认情况下,每个可以包含内容的元素最多只能有一个文本节点,而且必须有内容存在。例子<!–没有内容,没有文本节点–><div></div><!–有一个空格,有文本节点–><div> </div><!–有内容,有文本节点–><div>Hello World</div>跟自己想的有出入,以为文本节点是<p>或<span>里的才叫文本节点,然而只要在可以包含内容的元素内的文本,都叫文本节点。如此一说,就知道为什么空格也被算一个节点了,因为空格也是文本。上面又说默认情况下只有一个文本节点。还记得上面的只有三个<li>子元素的<ul>元素有7个子元素,说明有4个文本节点,这就有别于默认情况了。文本内容可以通过nodeValue或data属性修改,无法在文本里写入HTML代码。例子:div.firstChild.nodeValue = "some <strong>other</strong>message";//输出的结果是:"some &lt;other&gt;message"1.创建文本节点使用document.createTextNode()创建新文本节点。创建后再插入文档中。如果使用了两次这个方法,再分别插入同一个元素节点中,就会变成两个文本节点(并不会融合为一个文本节点)2.规范化文本节点为了解决相邻节点容易导致混乱问题,于是催生了normalize()方法能够将相邻文本节点合并。例子:var element = document.createElement("div");element.class="message";var textNode = document.createTextNode("Hello World"); //创建了第一个文本节点element.appendChild(textNode);var anotherTextNode = document.createTextNode("Yippee!"); //创建了第二个文本节点element.appendChild(anotherTextNode);document.body.appendChild(element);console.log(element.childNodes.length); //2element.normalize(); //调用这个方法就启动合并console.log(element.childNodes.length); //1console.log(element.firstChild.nodeValue); //Hello WorldYippee!浏览器解析文档时觉得不会创建相邻文本节点,这种情况只会作为DOM操作的结果出现(现在做的事叫DOM操作)。3.分割文本节点使用splitText()方法可以把一个文本节点按指定位置切割成两个相邻文本节点。分隔文本节点是从文本节点中提取数据的一种常用的DOM解析技术。10.1.5Comment类型注释在DOM中是通过Comment类型来表示的,Comment节点具有下列特征:·nodeType的值是8;·nodeName的值是“#comment”;·nodeValue的值是注释的内容;·parentNode可能是Document或Element;·不支持子节点。Comment类型与Text类型继承自相同的基类。因此它拥有除splitText()之外的所有字符串操作方法。也可以通过nodeValue或data属性来取得注释内容。可以用document.createComment()方法创建注释节点,参数是注释文本。10.1.6CDATASection类型CDATASection类型只针对基于XML的文档。表示的是CDATA区域。与comment类似,CDATASection类型继承自Text类型(前面说的是来自相同的基类吧),因此它拥有除splitText()之外的所有字符串操作方法。CDATASection节点具有下列特征:·nodeType的值是4;·nodeName的值是“#cdata-section”;·nodeValue的值是CDATA区域中的内容;·parentNode可能是Document或Element;·不支持子节点。CDATA区域只会出现在XML文档中,因此多数浏览器会把CDATA区域错误地解析为Comment或Element.在真正的XML文档中可以使用document.createCDataSection()来创建CDATA区域。10.1.7DocumentType类型DocumentType类型在Web浏览器中并不常用。前面说过支持的情况各浏览器都不同。仅有Firefox、Safari和Opera支持它。DocumentType包含着doctype有关的所有信息,它具有下列特征:·nodeType的值是10;·nodeName的值是doctype的名称;·nodeValue的值是null;·parentNode可能是Document;·不支持子节点。这种支持情况不太好的类型就不细写了。10.1.8DocumentFragment类型在所有节点类型中,只有DocumentFragment类型在文档中没有对应的标记。DOM规定文档片段(document fragment)是一种“轻量级”的文档,可以包含和控制节点,但不会像完整的文档那样占用额外的资源。DocumentFragement节点具有以下特征:·nodeType的值是11;·nodeName的值是“#document-fragment”;·nodeValue的值是null;·parentNode的值是null;·子节点可以是Element、ProcessingInstruction、Comment、Text、CDATASection或EntityReference。虽然文档片段不能添加到文档中,但可以将它作为一个“仓库”来使用,可以在里面保存将可能会添加到文档中的节点。要创建文档片段,可以使用document.createDocumentFragment()方法。例子:var fragment = document.createDocumentFragment();文档片段继承了Node的所有方法,通常用于执行那些针对文档的DOM操作。如果将文档中的节点添加到文档片段中,就会从文档树中移除该节点。可以通过appendChild()或insertBefor()方法将文档片段中的内容添加到文档中。在将文档片段作为参数传递给两个方法时,只会将文档片段中的所有子节点添加到相应位置,文档片段永远不会成为文档树的一部分。例子:<ul id="myList"></ul>假设我们想为<ul>元素添加三个<li>,如果逐个地添加,将会导致浏览器反复渲染(呈现)新信息。为避免这个问题,可以像下面这样使用一个文档片段来保存创建的列表项,然后再一次性地将他们添加到文档中:var fragment = document.createDocumentFragment();var ul = document.getElementById("myList");var li = null;for(var i=0; i<3; i++){ li = document.createElement("li"); li.appendChild(document.createTextNode("Item"+(i+1))); //给元素插入文本节点也是用appendChild()方法 fragment.appendChild(li); //把元素插入文档片段中}ul.appendChild(fragment); //把文档片段插入ul执行最后一步的时候,文档片段的所有子节点都会在文档片段中被删除并转移到<ul>元素中。10.1.9Attr类型元素的特性在DOM中以Attr类型表示(换句话说,将元素特性归在DOM中的Attr类型中)。从技术上讲,特性就是存在于元素的attributes属性中的节点。特性节点具有下列特征:·nodeType的值为2;·nodeName的值是特性的名称;·nodeValue的值是特性的值;·parentNode的值为Null;·在HTML中不支持子节点;·在XML中子节点可以是Text或EntityReference。尽管特性节点也是节点,但特性节点不被认为是文档树的一部分。开发人员通常使用getAttribute()、setAttribute()、removeAttribute()方法,很少直接引用特性节点。Attr对象有3个属性:name、value和specified。name是特性名称,value是特性值,而specified是一个布尔值,用以区别特性是否在代码中被设置。作者说并不推荐直接访问特性节点,使用getAttribute()、setAttribute()、removeAttribute()方法更方便。就不细讲这个类型了,知道有就行了。10.2DOM操作技术10.2.1动态脚本(动态地引入脚本或传入脚本代码)使用<script>元素传入JavaScript代码的方式有两种。一种是通过src特性引入外部文件,另一种是直接在<script>标签中写代码。这一节要讨论的动态脚本,指的是在页面加载时不存在,但将来的某一时刻通过修改DOM动态添加的脚本。创建动态脚本的方法也有两种:插入外部文件或直接插入JavaScript代码。下面的例子展示动态加载外部的JavaScript文件:var script = document.createElement("script");script.type="text/javascript";script.src="client.js";document.body.appendChild(script);只有在执行到最后一行代码把<script>元素添加到页面中,才会下载外部文件。我们可以把整个过程用下面的函数封装起来:function loadScript(url){ var script = document.createElement("script"); script.type = "text/javascript"; script.src = url; document.body.appendChild(script);}loadScript("client.js");另一种指定JavaScript代码的方式是行内方式,例如这个:<script> function sayHi(){console.log("Hello World!");}</script>从逻辑上讲,下面的DOM代码是有效的:var script = document.createElement("script");script.type = "text/javascript";script.appendChild(document.createTextNode("function sayHi(){console.log("Hello World!");}"));document.body.appendChild(script);在Firefox、Safari、Chrome和Opera中,这些代码是可以正常运行的。但在IE中,则会导致错误。因为IE将<script>视为一个特殊的元素,不允许DOM访问<script>子节点。但是,我们可以用<script>元素的text属性来指定JavaScript代码。例子:var script = document.createElement("script");script.type = "text/javascript";script.text = "function sayHi(){console.log("Hello World!");}";document.body.appendChild(script);上面代码在各大浏览器都可以运行,除了Safari3及以前的版本。但是Safari 3是2008年的东西了,应该不用特地写吧。还是写写兼容Safari3及以前的添加JavaScript代码的封装函数:function loadScriptString(code){ var script = document.createElement("script"); script.type = "text/javascript"; try{ script.appendChild(document.createTextNode(code)); }catch(ex){ script.text = code; } document.body.appendChild(script);}loadScriptString("function sayHi(){console.log(‘hi’)}");实际上,这样执行代码,跟把相同的字符串传入eval()是一样的。(惊了)其实为什么要只有写代码呢,直接写进全局作用域不好吗。10.2.2动态样式能够把CSS样式包含到HTML页面的元素有两个。其中<link>用来包含来自外部的文件,<style>用来指定嵌入的样式。动态样式也是在代码运行的过程中动态添加到页面中的。下面函数的样式载入方法在所有主流浏览器都可以运行:function loadStyles(url){ var link = document.createElement("link"), link.rel = "stylesheet", link.type = "text/css", link.href = url; var head = document.getElementsByTagName("head")[0]; head.appendChild(link);}loadStyles("styles.css");加载外部样式文件是异步的,也就是加载样式与执行JavaScript代码的过程没有固定的次序。另一种定义样式的方式是用<style>元素来包含嵌入式CSS(这种是嵌入式,那写在标签里的呢?),如下所示:<style type="text/css">body{ background-color:red;}</style>按照相同的逻辑,下面的代码应该是有效的:var style = document.createElement("style");style.type = "text/css";style.appendChild(document.createTextNode("body{background-color:red}"));var head = document.getElementsByTagName("head")[0];head.appendChild(style);意思代码可以在Firefox、Opera、Safari和Chrome中运行,在IE中报错。IE将<style>视为与<script>一样特殊的,不允许访问其子节点的节点。要解决这个方法,就要先访问<style>元素的styleSheet属性,再访问这个属性里的cssText属性,该属性可以接受CSS代码。让我们把添加嵌入式样式封装为一个函数,通用的解决方法如下:function loadStyleString(css){ var style = document.createElement("style"); style.type = "text/css"; try{ style.appendChild(document.createTextNode(css)); }catch(ex){ style.styleSheet.cssText = css; //cssText是在style的styleSheet属性里的 } var head = document.getElementsByTagName("head")[0]; head.appendChild(style);}loadStyleString("body{background-color:red}");✎:针对IE编写代码时,使用cssText属性要特别小心,在重用同一个<style>元素并再次设置这个属性时,还有将cssText属性设置为空字符串时,这两个情况都会导致浏览器崩溃,至于是在版本几会这样就未可知了。10.2.3操作表格总而言之,就是如果要创建一个下面的表格:<table border="1" width="100%"> <tbody> <tr> <td></td> <td></td> </tr> <tr> <td></td> <td></td> </tr> </tbody></table>需要反反复复用大量的createElement()和appendChild()方法,代码很长。为了方便创建表格,HTML DOM为<table>、<tbody>、<tr>元素添加了一些属性和方法,具体晚上补图吧。但是记得上次软件设计大赛直接用document.write()写入元素节点更加简单粗暴。10.2.4使用NodeList理解NodeList及其“近亲”HTMLCollection和NamedNodeMap,是从整体上透彻理解DOM的关键所在。这三个集合都是“动态的”;每当文档结构发生变化,他们都会马上更新。例如,下列代码会导致无限循环:var divs = document.getElementsByTagName("div"),i,div;for(i=0; i < divs.length; i++){ div = document.createElement("div"); document.body.appendChild(div);}因为document.getElementsByTagName()返回的是一个HTMLCollection,每在循环中插入一个div,divs.length就+1,所以会陷入无限循环。所以如果想迭代一个NodeList,最好是使用length初始化第二个变量,如下所示:var divs = document.getElementsByTagName("div"),i,div;for(i=0,len=divs.length; i <len; i++){ div = document.createElement("div"); document.body.appendChild(div);}由于len中保存着对divs.length在循环开始时的一个快照,因此避免了上一个例子中出现的无限循环问题。(说明每次for循环只会在i<len,,i++和下面的代码块上跑N次不会再确认第一个参数?)一般来说,应该尽量减少访问NodeList的次数,因为每访问一次NodeList,都会运行一次基于文档的查询。可以考虑从NodeList中取得的值缓存起来(缓存起来?保存起来?)。不止NodeList,HTMLCollection和NamedNodeMap也可以缓存起来吧。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"《JavaScript高级程序设计》 第九章 客户端检测","date":"2017-11-14T01:32:17.000Z","path":"2017/11/14/《JavaScript高级程序设计》-第九章-客户端检测/","text":"目录9.1能力检测9.1.1更可靠的能力检测(用typeof检测某属性是不是自己要的类型,避免重名)9.1.2能力检测,不是浏览器检测(不要用能力检测推测浏览器)9.2怪癖检测9.3用户代理检测9.3.1用户代理字符串的历史9.3.2用户代理字符串检测技术1.识别呈现引擎2.识别浏览器3.识别平台4.识别window操作系统9.3.3完整代码9.3.4使用方法就是最蛋疼的跨浏览器检测。作者做只要能找到更通用的方法,就优先采用更通用的方法。先设计最通用的方案,然后再使用特定于浏览器的技术增强该方案。9.1能力检测最常见且被广泛接受的客户端检测形式是能力检测(又称特定检测)。能力检测的目标不是识别特定的浏览器。而是识别浏览器的能力。用这种方式不用顾及特定的浏览器如何如何,只要确定浏览器支持特定的能力,就可以给出解决方案。能力检测的基本模式如下:if(object.propertyInQuestion){ //使用object.propertyInQuestion}else if(object.otherPropertyInQuestion){ //第二种方法}else{ //第三种或者抛出错误}要理解能力检测,必须理解两个重要的概念:第一个概念就是先检测达成目的最常用的特性,后检测比较刁钻的特性。第二个概念是必须测试实际要用到的特性,不要用其他特性。一个特性存在,不一定以为着另一个特性也存在(或者不意味着就是你项针对的那个浏览器)。例子:function getWindowWidth(){ if(document.all){ //假设是IE return document.documentElement; //错误的用法 }else{ return window.innerWidth; }}document.all是IE的用法,小伙子以为检测document.all通过,就说明这是个IE浏览器,然而其实Opera也支持document.all方法。所以直接拿想用的特定去检测别用其他特性,就是第二个概念。9.1.1更可靠的能力检测先看一个错误的例子:function isSortable(object){ return !!object.sort}双非运算符:根据自己的检测,应该是可以把所有类型的值转换成布尔值的运算符比如 !!0返回false、!!true返回true、!!字符串返回true。使用这个运算符就可以不用写 if 语句直接知道真假。上面的例子通过检测对象是否存在sort()方法,来确定对象是否支持排序方法。问题是,如果有一个自定义对象定义了一个sort属性(不是方法),那这个对象也会返回true:var result = isSortable({sort:"Nicholas"});所以检测某个属性是否存在不能确定对象是否支持排序,更好的方式:不如直接检测sort是不是一个函数:function isSortable(object){ return typeof object.sort == "function";}如果检测确定sort是个函数,那就可以调用它对数据排序。在可能的情况下,要尽量使用typeof进行能力检测。特别是,宿主对象(DOM对象)没有义务让typeof返回合理的值(那还用typeof???)。最令人发指的事就发生在IE中(233333),大多数浏览器在检测到document.createElement()存在的时候,都会返回true。但是在IE8及之前版本中,会返回false [摊手]。因为typeof document.createElement返回的是"object",而不是"function"。如前所属,DOM对象是宿主对象,IE及更早的版本中的宿主对象是通过COM而非JScript实现的。因此,document.createElement()函数确实是一个COM对象没毛病。IE9纠正了这个问题,对所有DOM方法都返回"function"。结论就是不能用typeof操作符检测IE8及之前版本的一些DOM方法。关于typeof的行为不标准,IE中还可以举出例子来。Active对象(只有IE支持)与其他对象的行为差异很大。例如,不使用typeof测试某个属性会导致错误。例子:var xhr = new ACtiveXObject("Microsoft.XMLHttp");if(xhr.open){ //这里会发生错误 //执行操作}像这样直接把函数作为属性访问会导致JavaScript错误。使用typeof操作符会靠谱一点,但IE对typeof xhr.open会返回"unknown"。这就意味着,在浏览器环境下测试任何对象的某个特性是否存在,要使用下面这个函数:function isHostMethod(object,property){ var t = typeof object[property]; return t == ‘function’||(!!(t=’object’ && object[property]))||t==’known’; 第二个判断条件是类型要是对象,且属性存在,是对象不就存在了吗?}result = isHostMethod(xhr,"open"); //falseresult = isHostMethod(xhr,"foo"); //false虽然这个函数现在还算可靠,但是不能保证在未来宿主对象保持目前的实现方式不变,也不一定会模仿已有的宿主对象的行为(来达到统一)。所以这个函数,包括其他类似能力检测函数,都不能百分百保障永远可靠。刚刚测试了一下,Edge里没有ActiveObject对象!好奇怪,百度了一下,果然,Edge不支持ActiveX这种老技术了!9.1.2能力检测,不是浏览器检测简而言之,不能通过判断几个特性存不存在就确定浏览器。来看这个错误的例子://错误!不够具体var isFirefox = !!(navigator.vendor && navigator.vendorSub);//错误!假设过头了var isIE = !!(document.all && document.uniqueID);上面代码就是典型的能力检测误用。以前确实可以通过检测navigator.vendor和navigator.vendorSub来确定Firefox浏览器,但是后来Safari也实现了相同的属性,于是这个方法就不能使用了。为检测IE,代码测试了document.all和document.uniqueID。这就相当于假设IE将来的版本会继续存在这两个属性,同时假设未来其他浏览器也没这两个属性,很显然这种不向未来看的检测方法确定浏览器是不靠谱的。实际上,根据浏览器不同将能力组合起来是更可取的方式。如果你知道自己的应用程序需要使用哪些浏览器特性,那么一次性检测所有相关特性,不要分别检测是最好选择。例子://确定是否有支持Netscape插件var hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);//确定是否浏览器具有DOM1级规定的能力var hasDOM1 = !!(document.getElementById && document.createElement && document.getElementByTagName);感觉这种代码是一口气知道自己的应用程序会用在哪才能做的能力检测吧,就是先检测,再根据能力写代码。不能根据能力的不同做出相应措施。总结:不要用能力检测的结果去确定用户使用的浏览器。9.2怪癖检测与能力检测类似,怪癖检测(quirks detection)的目标是识别浏览器的特殊行为。但与能力检测确认浏览器有什么能力不同,怪癖检测是(ˇ?ˇ) 想检查浏览器有没有什么BUG。通常需要运行一段小代码来确定某特性能不能工作。例如,IE8及更早版本存在一个BUG:如果某个实例属性与[[Enumerable]]标记为false的某个原型属性同名,那么该实例属性将不会出现在 for - in 循环中。可以用下面这段代码检测:var hasDontEnumQuirk = function(){ var o = {toString : function(){}}; for(var prop in o){ if(prop == "toString"){ return false; //函数是检测有怪癖,return false就是没有怪癖。 } } return true;}();以上代码在局部环境创建了一个对象并重写toString()(刻意与原型属性同名),并用 for - in循环,判断toString()有没有在循环中,有则确定不是IE8(最好不要这样说,犯了用能力检测判断浏览器的忌讳)没这个quirks。还有一个怪癖在Safari 3以前的版本:会枚举被隐藏的属性(通过继承得到的属性吧)。可以用下面的代码检测:var hasEnumShadowQuirk = function(){ var o = {toString:function(){}}; var count = 0; for(var prop in o){ if(prop == "toString"){ count++; } } return (count > 1);}();枚举的时候当然没必要把toString()、toLocalestring()等从Object继承过来的方法枚举出来啦,所以算是被“隐藏”的属性。如果是Safari 3以前版本,就会返回两个toString的实例。9.3用户代理检测第三种,也是争议最大的一种客户端检测技术,叫做用户代理检测。通过检测用户代理字符串来确定实际使用的浏览器。在每一次HTTP请求过程中,用户代理字符串是作为响应首部发送的。用户代理字符串可以通过JavaScript的navigator.userAgent属性访问。在服务器端,通过检测用户代理字符串来确定用户使用的浏览器是常用且广为接受的做法。而在客户端,用户代理检测被视为一种万不得已的做法,其优先级在能力检测和怪癖检测之后。9.3.1用户代理字符串的历史用那么大篇幅讲的竟然是用户代理字符串的历史。总而言之,就是所有浏览器都为了使自己的浏览器能被服务器认可,用户代理字符串都不写自己的真实的浏览器名称,基本都叫做“Mozilla”。除了Opera的几个版本是“Opera”。但是后来Opera好像很狗地伪装成其他的浏览器。用户代理字符串还有很多其他信息,就不列出来了,因为根据浏览器版本不同,格式,显示信息都不同。(亲测当前版本的各浏览器开头都是“Mozilla5.0”)9.3.2用户代理字符串检测技术由于各种历史遗留问题,通过用户代理字符串检测浏览器不是件容易的事。因此,首先要确定你要多么具体的浏览器信息。一般情况下,确定呈现引擎和最低限度的版本就能决定正确的操作方法了。最好不要只为特定的浏览器版本写代码,例如:if(IE6 || IE7){ //不推荐 //代码}上面显示代码在浏览器是IE6,7的时候会执行,这样的代码其实很脆弱。因为它要依据特定的版本来做什么。如果是IE8怎么办?只要有新版本出来,代码就要随之更新。像下面这样就可以避免此问题:if(ieVer >= 6){ //代码}上面代码检测浏览器是否是IE6及6以上。这样能确保代码将来也能起作用。下面的浏览器检测脚本就是本着这种思路写的(向后兼容?)。1.识别呈现引擎我们编写的脚本主要检测五大引擎:IE、Gecko、WebKit、KHTML和Opera(Opera是呈现引擎名字?)为了不在全局作用域添加多余的变量,我们使用模块增强模式来封装检测脚本。检测脚本的基本代码结构如下:function client = function(){ var engine = { //呈现引擎 ie:0, gecko:0, webkit:0, khtml:0, opera:0, //版本号 ver:null }; var browser = { //浏览器 ie:0, firefox:0, safari:0, konq:0, opera:0, chorme:0, //具体的版本 ver:null } var system = { win:false, mac:false, xll:false //移动设备 iphone:false, ipod:false, ipad:false, ios:false, android:false, nokiaN:false, winMobile:false //这里是检测呈现引擎、平台和设备的代码 return { engine:engine browser:browser };}();代码中的私有变量browser用于保存每个浏览器的注意属性。如果是当前使用的浏览器,则这个属性保存的是浮点数值形式的版本号。同样,ver属性中在必要时将会包含字符串形式的浏览器完整版本号。对Opera和IE而言,browser对象中的值等于engine对象中的值。对Konqueror而言,browser.konq和browser.ver属性分别等于engine.khtml和engine.ver属性。最后的代码超级长,好多正则,看得陈某头都大。代码可以检测浏览器,平台,内核,网上也有代码。在P242。9.3.4使用方法前面强调过,不到万不得已不要使用用户代理检测。用户代理检测适用于下列情形:·不能直接准确地使用能力检测或怪癖检测。·同一款浏览器在不同平台下具备不同的能力,这个时候就要确定浏览器在哪个平台。·为了分析跟踪等目的需要确切地知道浏览器。原来以前写的跨浏览器代码叫能力检测。能力检测,怪癖检测,用户代理检测统称客户端检测。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"《JavaScript高级程序设计》 第八章 BOM","date":"2017-11-12T02:45:54.000Z","path":"2017/11/12/《JavaScript高级程序设计》-第八章-BOM/","text":"目录8.1window对象8.1.1全局作用域(window对象)8.1.2窗口关系及框架8.1.3窗口位置(一直没统一的属性。screenLeft、screenTop;screenX、screenY,几个移动窗口的方法,不过一些浏览器没反应)8.1.4窗口大小(仍未统一。innerWidth、innerHeight、outerHeight、outerHeight;clientWidth、clientHeight。几个名词:可见视口、布局视口。几个调整窗口大小的方法)8.1.5导航和打开窗口1.弹出窗口(window.open())2.安全限制3.弹出窗口屏蔽程序(检测弹出窗口是否被阻止弹出) 8.1.6间歇调用和超时调用(一个间歇调用在开始计时等待执行的那段时间里会往下执行其他代码。它们都会在调用后返回一个数值ID,用于取消它们)8.1.7系统对话框(alert()、confirm()、prompt())8.2location对象8.2.1查询字符串参数8.2.2位置操作(location.assign()、location.replace()、location.reload())8.3navigator对象8.3.1检测插件8.3.2注册处理程序8.4screen对象(“用处不大”)8.5history对象 JavaScript的核心是ECMAScript,但如果要在Web中所以JavaScript,那么BOM(浏览器对象模型,Browser Object Module)无疑才是真正的核心。BOM提供了很多对象,用于访问浏览器的功能。这些功能与任何网页内容无关。多年来,缺少事实上的闺房导致BOM既有意思又有问题。W3C为了把浏览器中JavaScript最基本部分的标准化,已经将BOM的主要方面纳入了HTML5规范中。8.1window对象BOM的核心对象——window。它代表浏览器的一个实例。window对象在浏览器有双重角色,它既是通过JavaScript访问浏览器窗口的一个接口,又是ECMAScript规定的Global对象。这意味着在网页中定义的任何一个对象、变量和函数,都以window作为其Global对象,因此有权访问parseInt()等方法。8.1.1全局作用域因为ECMAScript中Global对象的角色由window对象扮演,所以所有在全局作用域中声明的变量、函数都会变成window对象的属性和方法。虽然全局变量会成为window对象的属性。但是定义全局变量与在window对象上直接定义属性还是有一点差别的:全局变量不能通过delete操作符删除,而直接在window对象上定义的属性可以。(delete可以删除对象属性,但不能删除用var关键字定义的变量)例子:var age = 29;window.color = "red"; //直接在window对象上定义属性作为全局变量delete window.age; //IE<9会报错,其他浏览器返回false(删除失败)delete window.color //IE<9会报错,其他浏览器返回true(删除成功)console.log(window.age) //29 变量仍存在console.log(window.color) //undefined 已删除真正的原理:通过var语句添加的window属性有一个名为[[Configurable]]的特性,这个特定的值被设置为false,因此这样定义的属性不可以通过delete操作符删除。有意思的是,我想试试能不能通过Object.defineProperty(obj,prop,descriptor)把age的[[Configurable]]修改为true然后删除,直到敲了代码,运行报错了才想起来,[[Configurable]]被设置为false后就不能再改回true(其他三个特性还能修改,以前以为configurable为false后其他三个也不能修改了)。意味着var关键字定义的变量永远不能被delete操作符删除。另外,记住一件事:尝试访问未声明的变量会抛出错误。但是通过查询window对象,可以知道某个可能未声明的变量是否存在。例如://这里抛出错误,因为oldValue未定义var newValue = oldValue; //这就叫访问一个变量//这里不会报错,因为这是一次属性查询var newValue = window.newValue //undefined本章后面讨论的很多JavaScript对象(location 和navigator)实际上都是window对象的属性(对象是一个对象的属性,要习惯这件事)8.1.2窗口关系及框架如果页面包含框架,则每个框架都拥有自己的window对象,并且保存在frames集合中。在frames集合中,可以通过数组索引或者框架名称来访问相应的window对象。每个window对象都有一个name属性,其中包含框架的名称,看了数觉得框架这部分会很少用到,写了也用处不大,要就看书P194。8.1.3窗口位置用来确定和修改window对象位置的属性和方法有很多。IE、Safari、Opera和Chrome都提供了screenLeft和screenTop属性,分别表示窗口相对于屏幕左边和上边的位置。Firefox则在screenX和screenY属性中提供相同的窗口位置信息.。Safari和Chrome也同时支持这两个属性。Opera表面上支持,但与screenLeft和screenRight属性并不对应,因此不建议在Opera中属于它们。下面的代码是跨浏览器获得窗口左边和上边的位置:var leftPos = (typeof window.screenLeft == "number")?window.screenLeft : window.screenX;var topPos = (typeof window.screenTop == "number")?window.screenTop : window.screenY;优先使用screenLeft和screenTop,不行再用Firefox的screenX、Y。浏览器蛋疼时间:在IE和Opera(亲测Opera最新版没这个问题)中,screenLeft和screenTop中保存的是从屏幕左边和上班到由window对象表示的页面可见区域的距离。换句话说,假设window对象是最外层对象,而且浏览器紧贴屏幕最上端——即y轴左边为0,那么screenTop的值即是位于页面可见区域上方的浏览器工具栏的像素高度。但是在Chrome、Firefox和Safari中,screenY和screenTop中保存的是整个浏览器窗口相对于屏幕的坐标值,即在窗口的y轴左边为0时返回0。(意思就是IE即使整个浏览器顶在左上角,Y轴也不为0,因为有浏览器的工具栏在上面使页面无法真的贴在左上角)更蛋疼的是,Firefor、Safari和Chrome始终返回页面中每个框架的top.screenX和top.screenY值。即使在页面由于被设置了外边距而发生偏移的情况下,相对于window对象用screenX和screenY每次也都会返回相同的值。而IE和Opera则会给出框架相对于屏幕边界的精确坐标值(top对象:top对象始终指向最高(最外)层的框架,也就是指向浏览器窗口)。(这段不懂)最终结果,就是无法在跨浏览器的条件下取得窗口左边和上边的精确坐标值(这部分浏览器和那部分浏览器返回的数值不一致)。然而,使用moveTo()和moveBy()方法倒是有可能将窗口精确地移动到一个新位置。这两个方法都接收两个参数,但参数意思不同。·moveTo():接收的是新位置的x和y坐标。(其实看to和by就知道参数会不同)·moveBBy():接收的是在水平和垂直方向上移动的像素数。亲测只有IE11、Safari(10及以前都没用)是有效果的。Opera,Chrome,Edge都没反应。8.1.4窗口大小跨浏览器确定窗口大小不是一件简单的事(gg)。IE9+、Firefox、Safari、Opera、Chrome均为此提供了4个属性:innerWidth、outerHeight、innerHeight和outerWidth。在IE9+、Safari和Firefox中,outerWidth和outerHeight返回浏览器窗口本身的尺寸(无论从最外层的window对象还是从某个框架访问)。在Opera中,这两个属性的值表示页面视图容器(值Opera中单个标签页对应的浏览窗口)的大小。而innerWidth和innerHeight则表示该容器中页面视图区的大小。在Chrome中。outerWidth、outerHeight与innerWidth和innerHeight返回相同的值。即视口(viewport)大小而非浏览器大小。(又是无法统一)IE8及之前没有提供获得当前浏览器尺寸的属性。不过,它通过DOM提供了页面可见区域的相关信息。在IE、Firefox、Safari、Opera、Chrome中,document.documentElement.clientWidth和document.documentElement.clientHeight中保存了页面视口(viewport)信息。IE6中,这些属性必须在标准模式下才有效。如果是混杂模式,就必须通过document.body.clientWidth和document.body.clientHeight取得相同的信息。虽然最终无法确定浏览器窗口的大小,但可以取得页面视口(viewport)的大小,如下:if(typeof pageWidth != "number"){ if(document.compatMode == "CSS1Compat"){ pageWidth = document.documentElement.clientWidth; pageHeight = document.documentElement.clientHeight; }else{ pageWidth = document.body.clientWidth; pageHeight = document.body.clientHeight; }}document.compatMode将在第十章讨论,通过它来确定页面是否处于标准模式。是就用documentElement那个,混杂模式就用body那个。对于移动设备,window.innerWidth和window.innerHeight保存着可见视口,也就是屏幕上可见页面区域的大小(不是整个屏幕,也不是整个页面)。移动IE浏览器不支持这些属性。但通过document.documentElement.clientWidth和document.documentElement.clientHeight可以得到相同的信息。随着页面缩放,这些值也会相应变化。在其他移动浏览器中,document.documentElement度量的是布局视口,即渲染后页面的实际大小(与可见视口不同,可见视口只是整个页面的一小部分)。移动IE浏览器把布局视口信息保存在document.body.clientWidth和document.body.clientHeight中。布局视口的值不会随缩放变化(理所应当)。使用resizeTo()和resizeBy()方法可以调整浏览器窗口的大小。都接收两个参数,参数意义:·resizeTo():浏览器窗口的新宽度和新高度。·resizeBy():新窗口与原窗口的宽度和高度之差。这两个方法也可能被浏览器禁用,没试过。同样两个方法不适用于框架。只对最外层window对象使用。8.1.5导航和打开窗口window.open()方法既可以导航到一个特定的URL,也可以打开一个新的浏览器窗口。这个方法可以接收4个参数:要加载的URL、窗口目标、一个特性字符串以及一个表示新页面是否取代浏览历史记录中当前加载页面的布尔值。通常只需传递第一个参数,最后一个参数只在不打开新窗口的情况下使用。如果为window.open()传递第二个参数(窗口目标),而且该参数是已有窗口或框架的名称,那么就会在具有该名称的窗口或框架中加载第一个参数指定的URL。例子:window.open("http://www.wrox.com","topFrame");//等价于:<a href="http://www.wrox.com" target="topFrame"></a>如果没有窗口或框架叫“topFrame”,则会创建一个新窗口并将其命名为“topFrame”。此外,第二个参数可以是下列任何一个特殊的窗口名称:_self、_parent、_top或_blank(要加引号,_parent、_self、_top都是从自己窗口打开,只有_blank会打开新窗口,不知道前三个有什么区别)。1.弹出窗口如果给window.top()传递的第二个参数不是一个已经存在的窗口或框架,那么该方法就会根据第三个参数位置上传入的字符串创建一个新窗口或新标签页。如果没有传入第三个参数,那么就会打开一个带有全部默认设置(工具栏、地址栏、状态栏等)的新浏览器窗口(或者打开一个新标签页——根据浏览器设置)。在不会打开新窗口的情况下,会忽略第三个参数。第三个参数是一个逗号分隔的设置字符串,表示在新窗口中都显示哪些特性。下表列出可以出现在这个字符串中的设置选项:注意:整个字符串不允许出现空格。例子:window.open("http://www.baidu.com","","height=600,width=600,top=100,left=500,toolbar=yes,location=no");//第二个参数没有给窗口目标就会默认弹出新窗口那些toolbar,location,menubar设置yes,no好像都没什么变化啊。又试了试,Firefox可以显示工具栏,其他都不可以,Safari根本没弹出新窗口。window.open()方法会返回一个指向新窗口的引用(window.open()有返回值)。引用的对象与其他window对象大致相似,但我们可以对其进行更多控制。例如,有些浏览器默认不允许我们针对主浏览器窗口调整大小或移动位置,但却允许我们针对通过window.open()创建的窗口调整大小或移动位置。通过这个返回的对象,可以像操作其他窗口一样操作新打开的窗口。例子:var wroxWin = window.open("http://www.baidu.com","","height=600,width=600,top=100,left=500,toolbar=yes,resizable=yes");wroxWin.resizeTo(600,500); //设置打开窗口大小的方法wroxWin.moveTo(300,100); //设置打开坐标的方法wroxWin.close();如果Safari的设置里“阻止弹出式窗口”是被打勾的,Safari就会一点反应都没有,连阻止了弹窗都不说一声,其他浏览器都测试正常。对于浏览器的主窗口,如果没有用户允许是不能关闭它的。不过,弹出窗口可以调用top.close()在不经用户允许的情况下关闭自己。弹出窗口关闭后,窗口的引用仍然还在,但除了能被检测到closed属性之外,已经没有用处了。wroWin.close();console.log(wroWin.closed); //true弹出窗口虽然有一个指针指向打开它的原始窗口,但原始窗口并没有这样的指针指向弹出窗口。因此,我们只能在必要的时候自己手动来实现跟踪。有些浏览器(如IE和Chrome)会在独立的进程中运行每个标签页。当一个标签页打开另一个标签页时,如果两个window对象之间需要彼此通信,那么新标签页就不能运行在独立的进程中。如果想让新创建的标签页在独立的进程中运行,只要把新标签页的opener属性设置为null即可。例子:var wroxWin = window.open("http://www.baidu.com","","height=600,width=600,top=100,left=500,toolbar=yes,resizable=yes");wroWin.opener = null; //把“打开者”属性关掉,就能在独立进程中运行标签之间的联系一旦被切断,将没有办法恢复。2安全限制简单的说为了防止弹出的网页伪装成系统提示框让用户误点,浏览器的弹出页面都会加上状态栏或地址栏。各浏览器对弹出窗口做出的各种限制。3.弹出窗口屏蔽程序其实我觉得标题应该叫“弹出窗口检测程序”合适。屏蔽弹出窗口一般用户有两种方法:浏览器自己把窗口屏蔽;利用第三方浏览器插件屏蔽。情况不同,window.open()方法在无法打开时的返回值就不同。第一种无法打开很可能会返回null,第二种无法打开window.open()会报错。有时候我们的弹出是为了实现某些功能什么的,窗口屏蔽了可能导致一些信息或功能无法给用户,所以我们要检测我们的窗口是不是被屏蔽了。针对第一种情况:var wroxWin = window.open("http://www.baidu.com","_blank");if(wroxWin == null){ console.log("The popup was blocked!");}针对第二种情况,因为window.open()会抛出错误,所以,在检测返回值的同时,将对window.open()封装在一个try-catch块中。如下所示:var blocked=false; //因为要用在最下面的if判断,所以要把布尔值放在一个变量中try{ var wroxWin = window.open("http://www.baidu.com","_blank"); if(wroxWin == null){ blocked = true; //try-catch语句修改的blocked在外部环境会同步,说明try-catch语句块不是一个局部环境 }}catch(ex){ blocked = true; //会进到catch,说明抛出错误了,所以blocked在这里为true}if(blocked){ console.log("The popup was blocked!");}8.1.6间歇调用和超时调用JavaScript是单线程语言。超时调用需要用到window对象的setTimeout()方法。第一个参数是可以包含JavaScript代码字符串(和eval()函数中使用的字符串一样)。例子://不建议传递字符串setTimeout("alert(‘Hello World’)",1000);//推荐的调用方式setTimeout(function(){ console.log("Hello World");},1000)由于传递字符串可能导致性能损失,因此不建议以字符串作为第一个参数。调用setTimeout()调用后,会返回一个数值ID,表示超时调用。这个数值ID是计划执行代码的唯一标识符,可以通过它来取消超时调用。要取消尚未执行的超时调用计划,可以调用clearTimeout()方法并将相应的超时调用ID作为参数传递给它。//设置超时调用var timeoutId = setTimeout(function(){ console.log("Hello World");},1000);//把它取消:clearTimeout(timeoutId);只要在指定时间尚未过去之前调用setTimeout(),就完全可以取消超时调用。上面的代码在设置超时调用后立即取消,结果就跟什么也没有发生一样(亲测:原来超时调用在等待调用的时候会继续昂下执行代码。)。✎:超时调用的代码是在全局作用域中执行,因此this在非严格模式指向window对象,严格模式下是undefined。间歇调用与超时调用一样会返回一个见间歇调用ID。取消间歇调用的重要性要重于取消超时调用,因为在不加干涉的情况下,间歇调用会一直执行到页面卸载。一般认为,使用超时调用来模拟间歇调用是一种最佳模式。在开发环境下,很少真正使用间歇调用。原因:一个间歇调用可能在前一个间歇调用结束前就启动。使用超时调用可以避免这一点,所以,作者建议最好不要使用间歇调用(亲测超时调用也会在上一个未结束就执行下一个啊)。8.1.7系统对话框alert()、confirm()和prompt()方法能调用系统对话框向用户显示 信息(是用这些方法去,调用,系统对话框);这几个对话框都是同步和模态的。也就是说,有对话框显示时,代码停止执行,关闭对话框后才恢复执行。alert()就不解释了。·confirm():会有两个按钮,一个OK,一个Cancel(取消)按钮。为了检测用户是点击了OK还是Cancel,可以检查confirm()返回的布尔值。例子://这一段写在全局环境会马上执行!if(confirm("Are you sure")){ //就不用变量去接返回值了 console.log("I’m sure");}else{ console.log("NO");}·prompt():这是一个“提示”框,提示用户输入一些文本。prompt()接收两个参数:要显示给用户的文本,文本输入域默认值(可以是空字符串)、例子:prompt("What’s your name","Michael");如果点击了OK按钮,prompt()方法返回文本输入值;如果单击了Cancel或没有单击OK按钮,通过其他方式关闭对话框,该方法返回null。(亲测除了Safari点多少次都没有复选框,其他浏览器第一次点就有复选框了)现在的浏览器都会有“如果当前脚本执行过程中会打开两个或多个对话框,那么从第二个对话框开始,每个对话框会有一个复选框,问你要不要阻止后续的对话框”,除非用户刷新页面。如果用户勾选了,那么后续的系统对话框(包括警告框、确认框和提示框)都会被屏蔽。浏览器不会就对话框有没有显示向开发人员提示任何信息,以为着我们不知道后续的对话框有没有打开成功。书里说“如果两次独立的用户操作分别打开两个警告框(仅限警告框),那么这两个警告框中都不会显示复选框。而如果是同一次用户操作会生成两个警告框,那么第二个警告框中就会显示复选框”这句不是很懂。8.2location对象location是最有用的BOM对象之一。它提供给了与当前窗口加载的文档有关的信息,更有导航功能。location对象既是window对象的属性,又是document对象的属性。也就是说,document.location和window.location引用的是同一个对象。location不止保存着当前文档的信息,还表现在它将URL解析为独立的片段,让开发人员根据不同属性访问这些片段。下面是location对象的所有属性:8.2.1查询字符串参数尽管location的search属性可以访问URL包含的查询字符串,但有时候我们想单独获得每个查询字符串参数。可以像下面这样创建一个函数,解析(decode)查询字符串,然后返回包含所有参数的一个对象:function getQueryStringArgs(){ //取得查询字符串并去掉开头的问号 var qs = (location.search.length > 0?location.search.substring(1) : ""), //保存数据的对象 args= {}, //取得每一项 items = qs.length ? qs.split("&") : []; //把qs里的字符串按遇“&”就放进一个项,split()返回一个数组 item = null, name = null, value = null, 在for循环里使用 i = 0, len = items.length; //逐个每一项添加到args对象中 for(i=0; i<len; i++){ item = items[i].split("="); //遇"="放在一个数组,这个数组只有一项 name = decodeURIComponent(item[0]); value = decodeURIComponent(item[1]); if(name.length){ args[name] = value; //name是变量,使用中括号连接 } } return args;}8.2.2位置操作使用location对象可以通过很多方式来改变浏览器的位置。最常用的是assign()方法,参数是一个URL。location.assign("http://www.wrox.com&quot;);上面代码可以立即打开新URL并在浏览记录里生成一条记录。如果是将location.href或window.location设置为一个URL值,也会以该值调用assign()方法。下面两行代码与显式调用assign()方法效果一样(亲测都不能后退):location.href = "http://www.baidu.com&quot;window.location = "http://www.baidu.com&quot;第一次看到这种看起来像属性,用起来却跟方法一样的属性。在改变浏览器位置的方法中,最常用的是设置location.href属性。此外,还可以通过改变location对象的其他属性改变当前加载的页面。下面的例子通过将hash、search、hostname、pathname、和port属性设置为新值来改变URL。(可以用来像PHP添加&后面的值来实现效果诶)://假设初始URL为"http://www.wrox.com/WileyCDA/&quot;//将URL修改为"http://www.wrox.com/WileyCDA/#section1&quot; hash:URL中的散列location.hash = "#section1";//将URL修改为"http://www.wrox.com/WileyCDA/?qijavascript&quot; search:URL的查询字符串location.search = "?q = javascript";//将URL修改为"http://www.yahoo.com/WileyCDA/&quot; hostname:不带端口号的服务器名称location.hostname = "www.yahoo.com"//将URL修改为"http://www.yahoo.com/mydir/&quot; pathname:URL中的目录和文件名location.pathname = "mydir";//将URL修改为"http://www.yahoo.com:8080/mydir/&quot; port:端口号location.port = 8080;通过以上方法修改URL之后,浏览器的历史记录就会生成一条记录。所以用户可以通过“后退”按钮导航到前一个页面。要禁用这种行为,可以使用replace()方法。replace()方法接收一个参数,即要导航到的URL。这个方法跳转浏览器位置不会在历史记录中生成新纪录,就无法回到前一个页面:location.replace("http://www.baidu.com&quot;)与位置有关的最后一个方法是reload();作用是重新加载当前显示的页面。如果没有给这个方法传递参数,页面就会用最有效的方式重新加载:从浏览器缓存中重新加载。如果要强制从服务器重新加载,则需要加true参数:location.reload(); //重新加载(有可能从缓存中加载)location.reload(true); //重新加载(从服务器重新加载)位于reload()方法后面的代码可能执行也可能不执行,这取决于网络延迟与系统资源等因素。8.3navigator对象navigator对象现在已经成为识别客户端浏览器的事实标准。虽然其他浏览器提供了其他相同或相似的信息(例如IE中的window.clientInformation和Opear中的window.opera),但navigator对象却是所有支持JavaScript的浏览器共有的。与其他BOM对象(location对象、window对象)一样,navigator对象也有自己的一套属性,下列列出了存在所有浏览器的属性和方法,以及支持他们的浏览器版本。8.3.1检测插件plugins数组可以用来检测浏览器是否安装了特定的插件。该数组中的每一项都包含下列属性(这么说每一项都是数组咯)。·name:插件的名字。·description:插件的描述。·filename:插件的文件名。·length:插件所处理的MIME类型。检测插件的方法:检测插件时,需要像下面这样循环迭代每个插件并将插件的name与给定的名字进行比较://检测插件(在IE中无效)function hasPlugin(name){ name = name.toLowerCase(); for(var i=0; i<navigator.plugins.length; i++){ //navigator.plugins的每一项都是对象 if(navigator.plugins[i].name.toLowerCase().indexOf(name)>-1){ //返回的name有一大串,只要找到里面有插件名就好 return true; } } return false;}console.log(hasPlugin("Flash")); //trueconsole.log(hasPlugin("QuickTime")); //false一开始以为这个函数可以循环出所有插件名称,结果原来是要传参只能检测一个太垃圾,后来又发现navigator.name里已经有所有插件的名称,只要for循环出来就好。这个方法在Firefox、Safari、Chrome、Opera中都可以用来检测插件。✎:每个插件本身也是一个MimeType对象的数组,这些对象可以通过方括号语法来访问。每个MimeType对象有4个属性:包含MIME类型描述的description、回指插件对象的enabledPlugin、表示与MIME类型对应的文件扩展名的字符串suffixes(以逗号分隔)和表示完整MIME类型字符串的type。亲测我的浏览器有39个插件,用了第6个插件试试,打印出第6个插件,发现前6个属性都是MimeType对象,真的有上面那4个属性。我以为作者说插件是MimeType对象,原来是MimeType对象的数组,但是我检测了又不是数组啊:console.log(navigator.plugins[5] instanceof Array) // falseconsole.log(navigator.plugins[5])//Plugin {0: MimeType, 1: MimeType, 2: MimeType, 3: MimeType, 4: MimeType, 5: MimeType, name: "iTrusChina iTrusPTA,XEnroll,iEnroll,hwPTA,UKeyInstalls Firefox Plugin", filename: "npcombrg.dll", description: "iTrusPTA&XEnroll hwPTA,IEnroll,UKeyInstalls for FireFox,version=1.0.0.2", length: 6}好像只计算了前面六个MimeType为属性,后面的name、filename、description是什么?还是前面的6个是数组,后面那些是属性?console.log(navigator.plugins[5][0]) //用方括号访问,点号无法访问检测IE中的插件比较麻烦,因为IE不支持Netscape式的插件。在IE中检测插件的唯一方式就是使用专有的ActiveXObject类型,并尝试创建一个特定插件的实例。IE是以COM对象的方式实现插件的,而COM对象使用唯一标识符来标志。因此,要想检查特定插件,就必须知道其COM对象标识符(要知道插件的COM对象标识符)。下面的例子找到了Flash的标识符ShockwaveFlash.ShockwaceFlash。知道标识符就可以编写下面代码检测IE中的插件:function hasIEPlugin(name){ try{ new ActiveXObject(name); //实例化ActiveXObject,用try-catch证明如果无法穿件带某个标识符为参数的实例((书中说叫创建未知COM对象会抛出错误),会报错,而不是返回false return true; }catch(x){ return false; }}console.log(hasIEPlugin("ShockwaveFlash.ShockwaceFlash"));如果实例化成功,函数返回true,否则抛出错误,返回false。因为IE是要传入特定插件标识符来判断存不存在某个插件的,所以不可能创建一个万能跨浏览器函数来检测是否含有某些插件,只能一个个写,像这样:function hasFlash(){ //单独检测Flash插件的函数 var result = hasPlugin("Flash"); if(!resule){ result = hasIEPlugin("ShockwaveFlash.ShockwaceFlash") } return result}console.log(hasFlash());plugins集合有一个名叫refresh()的方法,用于刷新plguins以反映最新安装的插件。接收一个参数:表示是否刷新页面的布尔值。如果为true,则会重新加载包含插件的所有页面;否则只更新plugins集合,不重新加载页面。8.3.2注册处理程序这一节不太懂啊,什么RSS,MIME类型,RSS。Firefox2为navigator对象新增了registerContentHandler()和registerProtocolHandler()方法(这两个方法是在HTML5定义的)。这两个方法可以让一个站点指明它可以处理特定类型的信息(病句?)。随着RSS阅读器和在线电子邮件程序的兴起,注册处理程序就为像使用桌面应用程序一样默认使用这些在线应用程序提供了一种方式。registerContentHandler()方法接收三个参数:要处理的MIME类型、可以处理该MIME类型的页面的URL、应用程序的名称。举个例子,要将一个站点注册为处理RSS源的处理程序(将站点注册为程序?):navigator.registerContentHandler("application/rss+xml","http://www.somereader.com?feed=%s","Some Reader");第一个参数是RSS源的MIME类型,第二个参数是接收RSS源URL的URL,其中表示RSS源URL(把源URL加在接收这个URL的URL的后面),浏览器会自动插入。下一次请求RSS源时,浏览器就会打开指定的URL,而相应的Web应用程序就以适当方式来处理该请求。类似的调用方法也适用于registerProtocolHandler()方法,也接收三个参数:要处理的协议(例如mailto或ftp)(第一个方法接收的是MIME类型)、处理该协议的页面的URL和应用程序的名称。例如,想将一个应用程序注册为默认的邮件客户端:navigator.registerProtocolHandler("mailto","http://www.somemailclient.com?cmd=%s","Some Mail Client");这个例子注册(feed)了一个mailto协议的处理程序,该程序指向一个基于Web的电子邮件客户端。同样,第二个参数仍然是处理相应请求的URL,%s表示原始的请求。8.4screen对象JavaScript中有几个在编程总用处不大的对象,而screen对象就是其中之一(233333)。这些信息经常居中出现在测定客户端能力的站点跟踪工具中,但通常不会用于影响功能。不过,有时候也可能会用到其中的信息来调整浏览器窗口的大小,使其占据屏幕的可能空间,例如:window.resizeTo(screen.availWidth,screen.availHeight);然而几大浏览器都测了,就Firefox听话。浏览器会变得跟屏幕一样大,但浏览器不会移动位置,即变大那部分会被藏在屏幕看不到的地方。8.5history对象history对象是window对象的属性。出于安全考虑,开发人员无法知道用户浏览过的URL。但可以借助用户访问过的页面列表,在不知道实际URL的情况下实现前进和后退。使用go()方法可以在用户的历史记录中任意跳转。可向前可向后。接受一个参数,表示向前向后跳转的页面数的一个整数值。负数退后,正数前进。例子:history.go(-1); //后退一页history.go(1); //前进一页history.go(2); //前进两页神奇的是可以传递一个字符串,浏览器会根据历史记录中包含该字符串的第一个位置——可能后退,可能前进,看哪个位置更近。如果历史记录不包含该字符串,就什么都不做。例子://跳转到最近的wrox.com页面history.go("wrox.com");//跳转到最近的nczonline.net的页面history.go("nczonline.net");这个方法没返回值?回去试试。另外,两个简写方法back()和forward()来代替go()。顾名思义,这两个方法模仿浏览器的“后退”和“前进”按钮。不解释。history对象还有一个length属性,保存历史记录的数量。既包括向前的也包括向后的记录。对刚打开的第一个页面而言,history.length等于0。下面的函数可以检测你的网站是不是用户第一个打开的页面(可以检测是不是被设置为首页吧):if(history.length==0){ //进到这里说明是打开浏览器第一个访问的网站}✎:当页面的URL改变时,就会生成一条历史记录。在IE8及更高的版本、Opera、Firefox、Safari 3及更高版本、Chrome中,改变URL中的hash值也会生成一条历史记录。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"《JavaScript高级程序设计》 第七章 BOM","date":"2017-11-11T07:07:05.000Z","path":"2017/11/11/《JavaScript高级程序设计》-第七章-函数表达式/","text":"目录7.1递归(arguments.callee()的作用和用法)7.2闭包(活动对象是函数内的东西,执行环境,作用域链,闭包与内存)7.2.1闭包与变量(闭包保存的是整个变量对象的指针,用立即执行函数获得每次for循环的i值)7.2.2this对象(that = this保存this对象使闭包访问到某个对象)7.2.3内存泄漏(循环引用)7.3模仿块级作用域(利用匿名函数模仿块级作用域; (function())()这种特殊的函数表达式)7.4私有变量(利用闭包+构造函数模式创建公有方法来访问私有变量)7.4.1静态私有变量(构造函数模式+原型模式,不加var的全局函数)7.4.2模块模式(用对象字面量创造单例模式)7.4.3增强的模块模式 定义函数的方式有两种,一种是函数声明,一种是函数表达式。函数声明的语法function functionName(arg0,arg1){}函数表达式的语法var functionName = function(arg0,arg1){}用函数表达式创建的函数是匿名函数!这样只算将匿名函数赋值给了一个变量,可以通过这个变量调用这个函数,但不能说这个变量是这个函数的名字。函数声明和函数表达式的不同之处一、JavaScript引擎在解析Javascript代码时会函数声明提升(Function declaration Hoisting)当前执行环境(作用域)上的函数声明,而函数表达式必须等到JavaScript引擎执行到它所在行时,才会从上而下一行一行地解析函数表达式。二、函数表达式后面可以加括号立即调用该函数,函数声明不可以,只能以functionName( )形式调用。因为有函数声明提升的存在,所以不建议利用函数声明在判断条件下定义函数//不要这样做!if(condition){ function sayHi(){ alert(‘Hi’); }}else{ function sayHi(){ alert(‘Yo’); }}这在ECMAScript中属于无效语法,JavaScript引擎会修正错误。但是浏览器不同修复的结果不同,所以这样做很危险。不过如果是使用函数表达式,就没什么问题。//可以这样做var sayHi;if(condition){ sayHi = function(){ alert(‘Hi’); }}else{ sayHi = function(){ alert(‘Yo’); }}7.1递归本节重点介绍的不是递归,而是保证递归能够被正确使用的两种方法,及arguments对象中arguments.callee( )方法的使用有一个递归函数like this:function factorial(num){ if(num<=1){ return 1; }else{ return numfactorial(num-1); }}以上函数没毛病,但下面的代码会导致它出错var anotherFactorial = factorial;factorial = null;alert(anotherFactorial(5));代码先把factorial( )函数保存在变量anotherFactorial中,然后将factorial变量设置为null,结果指向原始函数的引用只剩下一个。但在接下来调用anotherFactorial( )时,由于必须执行factorial()而factorial已经被设置为Null,所以就导致出错,因为此时函数相当于function anotherFactorial(num){ if(num<=1){ return 1; }else{ return numfactorial(num-1); //此时factorial是null所以调用出现错误 }}这种情况下,使用arguments.callee可以解决这个问题。arguments.callee是一个指向正在执行的函数的指针(专业解释,其实就是arguments.callee()可以等价与现在正在执行中的那个函数),因此可以用它来实现对函数的递归调用,例如:function factorial(num){ if(num<=1){ return 1; }else{ return numarguments.callee(num-1); //此时arguments.callee()等价与factorial() }}所以通过arguments.callee()来代替函数名总比使用函数名更保险。但arguments对象在严格模式下是被禁止使用的,我们可以使用命名函数表达式来达成相同的结果,例如:var factorial = (function f(num){ if(num<1){ return 1; }else{ return numf(num-1); }})以上代码创建了一个名为f( )的命名函数表达式,然后将它赋值给变量factorial。即便把函数赋值给了另一个变量,函数的名字f仍然有效,所以递归调用照样能正确使用。这种方式在严格模式和非严格模式下都行得通。(亲测过即使在外面把 f 设置为null ( f=null )也可以完成调用,因为 f 是在(function f(){ })括号里定义的,括号里定义的变量不受外部的影响。试过在括号内的函数定义 i = 100,再在外部alert( i )会报错 i 未定义。7.2闭包闭包:指有权访问另一个函数函数作用域中的变量的函数(闭包,是一个函数)要理解闭包,就要先理解作用域和作用域链,这对理解闭包至关重要。所以下面会讲作用域和作用域链。以前面的createComparisonFunction()函数为例:function creatComparisonFunction(propertyName){ return function(obj1,obj2){ var val1 = obj1[propertyName]; var val2 = obj2[propertyName]; if(val1<val2){ return -1; }else if(val1>val2){ return 1; }else{ return 0; } };}作为返回值返回的函数也算内部函数。很显然内部函数可以使用外部函数的propertyName变量。因为内部函数的作用域中包含createComparisonFunction()的作用域。当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象(activation object)。在作用域中,外部函数的活动对象(即外部对象的arguments和其他命名参数的值)始终位于第二位,外部函数的外部函数的活动对象位于第三位,直到作用域链终点的全局执行环境。在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量,例子:function compare(val1,val2){ if(val1<val2){ return -1; }else if(val1>val2){ return 1; }else{ return 0; }}var result = compare(5,10);以上代码先定义了compare()函数,然后又在全局作用域中调用了它、当调用compare()时,会创建一个包含arguments,val1和val2的活动对象。全局执行环境的变量对象(包含result和compare)在compare()执行环境的作用域链则处于第二位。图展示了包含上述关系的compare()函数执行时的作用域链:第0位是compare()的活动对象,第1位是全局变量对象。后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。在创建compare()函数时(还没开始调用),会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。当调用compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链(执行环境在函数调用时才产生,执行环境里有作用域链)。此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的最前端(作用域链里是变量对象)。对于这个例子中compare()函数的执行而言,其作用域链包含两个变量对象:本地活动对象(被当变量对象)和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实例包含变量对象(倒不如说作用域链是人为从JavaScript中看出来的一个“规律”,不可能真的JavaScript里有一个指针“表”吧。)。无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕之后,局部活动对象就会被销毁,内存中仅保存全局作用域。但是闭包的情况又有所不同。在另一个函数内部定义的函数会被包含函数(即外部函数)的活动对象添加到它的作用域链中,因此,在createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象(arguments,定义的变量 )。图展示了当下列代码执行时,包含函数与内部匿名函数的作用域链:var compare = createComparisonFunction("name");var result = compare({name:"Nicholas"},{name:"Greg"}) //createComparisonFunction()返回的是一个函数,所以这行调用了compare()在匿名函数从createComparisonFunction()被返回后,它的作用域链被初始化为包含createComparisonFunction()函数的活动对象(arguments,定义的变量 )和全局变量对象。这样,匿名函数就可以访问在createComparisonFunction()中定义的所有变量。compare()就可以访问createComparisonFunction()中定义的变量。更重要的是,当createComparisonFunction()执行完毕后,其活动对象(arguments,定义的变量)不会被销毁。因为匿名函数的作用域链仍然在引用这个活动对象(销毁了不就引用不了了)。换句话说,当createComparisonFunction()函数返回后,其执行环境的作用域链被销毁(包含函数执行完毕后执行环境的作用域链就销毁),但它的活动对象仍然会留在内存中(活动对象就还在内存中);直到匿名函数也被销毁,createComparisonFunction()的活动对象才会被销毁。例如://创建函数var compareNames = createComparisonFunction("name");//调用函数var result = compareNames({name:"Nicholas"},{name:"Greg"}); //别忘了compareNames是函数,没毛病//解除对匿名函数的引用(以便释放内存)compareNames = null; //所以乱用闭包会占用大量内存首先,把返回的比较函数保存在变量compareNames中。最后通过对compareNames设置null解除该函数的引用(体会一下,解除引用,变量compareNames 只是指针),等于通知垃圾收集机制将其清除。随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也都可以安全地销毁了。下图展示了调用compareNames()的过程中产生的作用域链之间的关系:✎:由于闭包会写到包含它的函数的作用域,因此会占用很多内存过度使用闭包会导致内存占用过多,作者建议谨慎使用闭包。也可以手动关闭闭包。内存占用如果过多,在IE可能造成内存泄漏。7.2.1闭包与变量作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。下面这个例子可以清晰地说明这个问题:function createFunction(){ var result = new Array(); for (var i = 0; i <10; i++) { result[i] = function(){ //数组result里保存的是一个个函数,不是i return i; }; } return result; //一个有10个函数在里面的数组}这个函数会返回一个函数数组(项里保存着函数的数组)。表面上看,似乎每个函数都应该返自己的索引值,即位置0的函数返回0,位置1的函数返回1,以此类推。但实际上,每个函数都返回10。因为每个函数的作用域链中都保存着createFunction()的活动对象,所以他们引用的都是同一个变量i。当createFunction()函数返回后(createFunction() 调用结束后),变量 i 的值是10(createFunction() 的变量对象中 i = 10),此时每个函数都引用着(体会下!引用!,函数里并不保存 i ,i是createFunction() 他们家的)保存变量 i 的同一个变量对象,所以在每个函数内部 i 的值都是10。(自己的理解:函数被返回但并没有调用,闭包只有被调用时,指针才会去找(引用)那个外部函数的变量对象,此时 i 已经是10。意味着闭包里变量的值不是在赋值时马上保存进变量(这么说并不正确,i 并不是赋值来的),变量只是指针,指向有那个变量的地方(外部函数的变量对象))。一点小亲测:上面的代码,如果这样打印数组的项,只会返回一个函数的源代码:var a = createFunction(); //返回一个数组,a是个数组(说得好别扭,明明a只是一个指向一个数组的指针)console.log(a[1]); //function(){ return i} 就是i,而不是一个数值函数源代码里是return i 而不是return 10.要得到 i 的值,要调用那个函数:console.log(a1); //10就是觉得自己能想到调用这个函数觉得很吊233333所以用下面的立即调用函数返回后马上调用函数,闭包就能得到 i 是0,1,2,3…的变量对象:function createFunction(){ var result = new Array(); for (var i = 0; i <10; i++) { result[i] = function(num){ return function(){ return num //return的是num,return写错写成i的话每个数组里的函数都还是返回10 }; }(i); //立即执行函数最后的括号里带参数,参数的值会赋给函数的命名参数,所以每立即执行一次,num=i } return result;}(这里又让我觉得困惑了:这样数组里每一项函数都会返回各自从0—9的数,因为带参数num的函数被马上执行,环境对象里的num分别是0-9,有10个环境对象,难道有10条作用域链,10个函数在内存里?不会被重写?后来觉得想明白了因为是匿名函数所以不会被重写,但我给带num参数的函数命名之后,还是能成功返回0-9的值,难道没有被重写?)在重写了前面的createFunction()函数后,每个函数都会返回各自不同的索引值了。在这个版本中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里匿名函数有一个参数num,也就是最终的函数要返回的值。在调用每个匿名函数时,我们传入了变量 i 。由于函数参数是按值传递的,所以就会将变量 i 的当前值复制给参数num。而在这个匿名函数内部,又创建并返回了一个访问num的闭包。这样一来,result数组中的每个函数都有自己num变量的一个副本。因此就可以返回各自不同的数组了。7.2.2this对象 在闭包中使用this对象也可能会导致一些问题。我们知道this对象是在运行时基于函数的执行环境绑定的:在全局环境中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this通常指向window。但有时候由于编写闭包的方式不同,这一点可能不会那么明显。例子:var name="The window";var object={ name:"My Object", getNameFunc:function(){ return function(){ //返回一个闭包 return this.name } }};console.log(object.getNameFunc()();) //“The Window” getNameFunc()返回的是一个函数,所以再加个括号让那个函数执行以上代码先创建一个全局变量name,又创建一个包含name属性的对象。这个对象包含一个方法——getNameFunc(),它返回一个匿名函数,匿名函数返回this.name。调用匿名函数的结果返回一个字符串:The Window。为什么匿名函数没有取得其包含作用域的this对象呢?前面曾经提到过,每个函数在被调用时都会自动取得两个特殊的变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量(难道这两个变量不在活动对象里?活动对象前面说过有arguments和属性,难道只有window环境下的活动对象里的属性才能被纳入活动对象里?)(后来去找了资料想知道什么是活动对象,有人说活动对象的定义是:活动对象就是作用域链上正在被执行和引用的变量对象。要正处于被执行和引用状态下的变量对象才称为活动对象,window下的变量对象应该都是属于被执行和引用的,所以会被闭包找到。而object对象的name变量并没有被执行和引用,所以不会被找到吧)一句话:内部函数(闭包)只会找活动变量,不活的(没有被执行和引用的)不要!不过,把外部作用域(指object对象)的this保存在闭包能够保存的变量里,就可以让闭包访问到该对象了,例子:var name="The window";var object={ name:"My Object", getNameFunc:function(){ var that = this; return function(){ //返回一个闭包 return that.name } }};console.log(object.getNameFunc()();) //"My Object"闭包确实访问不到object环境对象,但你把object环境对象的指针this包装好了放在他面前,它也就只能用这个环境对象下的this了。或者用call()函数object.getNameFunc()。call(object)也能访问到object对象里的name属性。✎:arguments和this一样也存在这个问题。如果想访问作用域中的arguments对象,必须将对该对象的引用保存在另一个闭包能够访问到的变量里。后面的几个关于this的问题与闭包无关。在几种特殊的情况下,this的值可能会意外地改变。比如:下面的代码是修改前面例子的结果:var name="The window";var object={ name:"My Object", getName:function(){ return this.name; }};以下是几种调用object.getName()的方式和产生的不同结果:object.name; //"My Object"(object.name)(); //"My Object"(object.getName=object.getName)(); //"The Window" 非严格模式下第一行不解释。第二行在调用这个方法前给它加上了括号。虽然加上括号后,就好像只是在引用一个函数(引用一个函数?),但this的值得到维持,因为object.name和(object.getName()的定义是相同的)。第三句代码先执行了一条再赋值语句,然后再调用赋值后的结果。因为这个赋值表达式的值是函数本身,所以this的值不能得到维持,结果就返回了“The window”(也就是说赋的值只是getName函数的源代码,已经与object对象无关)。当然我们很少会用到第二三行的那种写法,不过,这个例子有助于说明即使是语法的细微变化,都有可能意外地改变this的值。7.2.3内存泄漏由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程(第四章讨论过),因此闭包在IE的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML元素,那么意味着该元素将无法被销毁。例子:function assignHandler(){ var element = document.getElementById("someElement"); element.onclick = function(){ console.log(element.id); };}以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用(有吗?自己引用自己?)。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数(垃圾回收机制的引用计数机制)。只要匿名函数存在,element的引用数至少会是1.因此它所占用的内存就永远不会不会被回收。这个问题可以通过下面的代码得到解决:function assignHandler(){ var element = document.getElementById("someElement"); var id=element.id element.onclick = function(){ console.log(id); }; element=null;}上面的代码通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用,但做到这一步还无法解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象(看到这里好像懂了为什么上面在一个对象字面量下的闭包不引用那个在对象里的属性,因为闭包只搜索作用域链里的活动对象,而活动对象是函数里的活动对象),而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把element变量设置为null。这样,就能够解除对DOM对象的引用,顺利地减少引用数,确保正常回收其占用的内存。当然这个只是IE9之前的问题,其他浏览器没这个问题。7.3模仿块级作用域前面说过,JavaScript没有块级作用域,如下面这个例子,在块语句中定义的变量,实际上是在包含函数中而非语句中创建的,例子:function outputNumber(count){ for (var i = 10; i >= 0; i–) { console.log(i) } console.log(i) //-1}即使像下面这样错误地(不知道是这种方式不好还是“错误地”又定义了一边)重新声明一个变量,也不会改变它的值:function outputNumber(count){ for (var i = 10; i >= 0; i–) { console.log(i) } var i; console.log(i) //-1}JavaScript不会跟你说你多次声明了同一个变量,遇到这种情况,JavaScript只会对这个后续声明视而不见。匿名函数可以用来模仿块级作用域并避免这个问题。 用作块级作用域(一般成为私有作用域)的匿名函数的语法如下所示:(function(){ //注意这里,用了函数声明的样子,却没有函数名 //这里是块级作用域})();上面的代码定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。怎么理解这是个函数表达式呢,正常的函数表达式是这样的:var someFunction = function(){}; someFunction(); 如果我们直接对函数表达式加括号立即调用,将会导致出错:function(){ //块级作用域}(); //出错!因为JavaScript将function关键字当作一个函数声明的开始,而函数声明后面不能跟圆括号。但是函数表达式后面可以跟圆括号啊,要将函数声明转换成函数表达式,只要在函数声明上加上一对圆括号即可:(function(){ //这里是块级作用域 //把一个函数表达式不赋给变量,而是用括号“包装”起来,就有种“被装起来”的感觉吧})()无论在什么地方,只要临时需要一些变量,就可以使用私有作用域,例如:function outputNumbers(count){ (function(){ for(var i=0; i<count; i++){ console.log(i); } })(); console.log(i); //报错:i is not defined} 使用这种特别的函数表达式方法,大概是为了不产生多余的函数名,变量名,因为真的想不出其他让完全无名的函数(这都不能算匿名函数吧,匿名函数还有一个变量在左边)立即执行的方法。 私有作用域中可以访问变量count,是因为这个匿名函数是一个闭包,它能够访问包含作用域中的所有变量。✎:这种做法可以减少闭包占用内存的问题,因为没有指向匿名函数的引用( i 没有在其他地方用到了)。只要函数执行完毕,就可以立即销毁其作用域链了。7.4私有变量严格来讲, JavaScript中没有私有变量的概念,所有对象属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数,局部变量好在函数内部定义的其他函数。例子:function add(num1,num2){ var sum=num1+num2; return sum;}在这个函数中,有三个私有变量:num1和num2和sum,外部无法访问。如果这个函数内部有闭包,那么闭包通过自己的作用域链可以访问这些变量。利用这点,可以创建用于访问私有变量的公有方法。(函数内部的变量可以被访问到)我们把有权访问私有变量的公有方法称为特权方法(privileged method)。有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下:function MyObject(){ //这是构造函数 var privateVariable = 10; function privateFunction(){ return false; } //特权方法 this.publicMethod = function(){ //不知道为什么要加this privateVariable++; return privateFunction(); }; //函数表达式本质是给变量赋值,还是要分号}只有作为闭包的特权方法有权访问在构造函数中定义的所有变量和函数(privateVariable和函数privateFunction())。在创建MyObject的实例后,除了使用publicMethod()这一个途径外,没有其他方法可以访问到构造函数中定义的变量和函数。利用私有和特权成员,可以隐藏那些不应该被直接修改的数据。例如:function Person(name){ this.getName = function(){ return name; }; this.setName = function(value){ name = value; };}var person = new Person("Nicholas");console.log(person.getName());person.setName("Greg");console.log(person.getName());以上代码定义了两个特权方法:getName()和setName()。这两个方法都可以在构造函数外部使用(当然是要在构造函数的实例下,怪不得要用构造函数,用普通函数这两个方法还怎么调用)。私有变量name在Person的每一个实例中都不相同,因为每次调用构造函数都会重新创建这两个方法(构造函数模式每次方法都会重新创建,原型模式则是共享方法,记得吗)。不过,在构造函数中定义特权方法也有缺点,就是必须使用构造函数来达到这个目的,如上面说的对每个实例都会创建一组同样的新方法。使用静态私有变量来实现特权方法就可以避免这个问题(可以没有一大堆方法在内存)。7.4.1静态私有变量通过在私有作用域((function(){})())中定义私有变量或函数创建特权方法:基本模式如下:(function(){ //私有变量和私有函数 var privateVariable=10; function privateFunction(){ return false; } //构造函数 MyObject = function(){}; //注意这里MyObject变量没有用var关键字,是全局变量 //公有/特权方法 MyObject.prototype.publicMethod = function(){ privateVariable++; return privateFunction(); }})()从代码可以看到,公有方法定义在原型上。这点体现了典型的原型模式。要注意的是,这个方式在定义构造函数时没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数(开始不知道,现在知道为什么了,函数声明没有用到var关键字,在一个函数里,就是一个局部函数了,而函数表达式只要那个变量不使用var关键字,就能变全局函数)。因此,Object就成了一个全局变量,能够在私有作用域之外被访问到。但也要知道,严格模式下定义变量不使用var关键字会报错。这个模式用到了原型模式,所以还是那个问题,方法共享,方法里要修改的变量修改的结果也会被共享:(function(){ var name=""; Person = function(value){ name = value; }; Person.prototype.getName = function(){ return name; } Person.prototype.setName =function(value){ name = value; };})()var person1 = new Person("Nicholas");console.log(person1.getName()); //"Nicholas"person1.setName("Greg");console.log(person1.getName()); //"Greg"var person2 = new Person("Michael");console.log(person1.getName()); //"Michael"console.log(person2.getName()); //"Michael"例子中的Person构造函数与getName()和setName()方法一样,都有权访问私有变量name。在这种模式下,变量name就变成了一个静态的、由所有实例共享的属性。也就是说,在一个实例上调用setName()会影响所有的实例,而结果就是影响所有实例都会返回相同的值。已这种方式创建静态私有变量会因为使用原型而增加代码复用,但每个实例都没有自己的私有变量(别人想改就改叫私有变量?)。到底是使用实例变量还是静态私有变量,最终还是视具体需求而定。7.4.2模块模式前面的模式是用于为自定义类型创建私有变量和特权方法的(一句话总结上面)。而道格拉斯所说的模块模式(module pattern)则是为单例创建私有变量和特权方法的。所谓单例(singleton),指的就是只有一个实例的对象。JavaScript以对象字面量的方式来创建单例对象:var singleton = { name:value, method:function(){ //方法的代码 }}模块模式通过为单例添加私有变量和特权方法能够使其得到增强。语法形式如下:var singleton = function(){ //私有变量和私有函数 var privateVariable = 10; function privateFunction(){ return false; } //特权/公有方法和属性被返回 return{ //返回一个对象字面量,就是对象 publicProperty:true, publicMethod:function(){ privateVariable++; return privateFunction(); } }}();这个模块模式使用了一个返回对象的匿名函数(singleton不是函数名,只是一个接收返回值的变量)。在这个匿名函数内部,首先定义了私有变量和函数,然后,将一个象字面量作为函数的值返回。返回的对象字面量只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。从本质上讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的,例如:var application = function(){ var components = new Array(); components.push(new BaseComponent()); //把构造函数BaseComponent的实例push进数组components return{ getComponentCount:function(){ return components.length; //只返回components的长度,外面无法看到内容 }, registerComponent:function(component){ if(typeof component == "object"){ //小写的"object",typeof返回的小写的object components.push(component); } } };};function BaseComponent(){ //假设有个BaseComponent构造函数 var name="Micheal";}var o={ //假设有个对象o name:"XS"}var person = application();console.log(person.getComponentCount()) //1person.registerComponent(o); //把对象 o push进去console.log(person.getComponentCount()) //2在Web应用程序中,经常需要使用一个单例来管理应用程序级的信息。这个简单的例子创建了一个用于管理组件的application对象。在创建这个对象的过程中,首先声明了一个私有的components数组,并向数组添加了一个BaseComponent的新实例(书中没给出这个构造函数的例子,我自己写了一个,目的是初始化components数组)。返回对象的getComponentCount()和registerComponent()方法,都有权访问数组componts的特权方法。前者只放回已注册的组件的数目,后者用于注册主件。.(不明白为什么叫单例,明明再多实例出几个也是可以的,而且对每个实例push数组项并没有互相影响)简而言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是Object的实例,因为返回的都是对象字面量。事实上,这也没什么;毕竟,单例通常都是作为全局对象存在的,我们不会将它传递给一个函数。因此,也就没什么必要使用instanceof操作符检查其对象类型了。(要传给一个对象就要用instanceof操作符检查它的对象类型吗?)7.4.3增强的模块模式有人进一步增强了模块模式。即在返回对象之前对齐增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。例子:var singleton = function(){ //私有变量和私有函数 var privateVariable = 10; function privateFunction(){ return false; } //创建对象 var object = new CustomType(); //不用关心CustomType()的代码 //公有方法和属性都属于object对象 object.publicProperty = true; object.publicMethod = function(){ privateVariable++; return privateFunction(); }; return object;}();如果前面演示模块模式的例子中的application对象必须是BaseComponent的实例,那么即可以使用以下代码:var application = function(){ //私有变量和函数(这里没私有函数) var components = new Array(); //初始化 components.push(new BaseComponent()); //创建一个application的局部副本 var app = new BaseComponent(); //公共接口 app.getComponentCount = function(){ return components.length; }; app.registerComponent = function(component){ if(typeof component == "object"){ components.push(component); } }; //返回这个副本 return app;}();这个重写后的应用程序(application)单例中,首先也是像前面例子中一样定义了私有变量。主要的不同之处在于命名对象app的创建过程,因为它必须是BaseCompoonent的实例。这个实例实际上是application对象的局部变量版(这句不懂)。此后,我们又为app对象添加了能够访问私有变量的公有方法。最后一步是返回app对象,结果仍然是将它赋给全局变量application。(看到最后一句我就知道为什么我上面对模块模式的使用实例是错的,且知道为什么叫单例模式了。)其实我并不用,也不能再创建一个person变量去“接”application返回出来的对象,因为返回的对象已经被application这个全局变量“接住”了。直接用application调用返回对象的公有方法就好了,var person = application相当于复制了一份对象指针,就不能叫单例了。第七章完结撒花 一会开始第八章","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"《JavaScript高级程序设计》第六章 面向对象的程序设计","date":"2017-11-11T05:06:25.000Z","path":"2017/11/11/《JavaScript高级程序设计》第六章-面向对象的程序设计/","text":"目录6.1理解对象6.1.1属性类型(对象有两种属性:数据属性和访问器属性) 1.数据属性([ [Configurable] ]、[ [Enumerable] ]、[ [Writable] ]、[ [Value] ] ;修改默认特性的方法) 2.访问器属性([ [Configurable] ] 、[ [Enumerable] ] 、[ [Get] ] 、[ [Set] ];Object.defineProperty()修改属性特性 )6.1.2定义多个属性(Object.defineProperties()方法修改多个属性特性)6.1.3读取属性的特性(Object.getOwnPropertyDescriptor())6.2创建对象6.2.1工厂模式(缺点是没解决对象识别问题)6.2.2构造函数模式(缺点是每实例化一个对象就要重写一次方法;实例的constructor属性)1将构造函数当作函数(把构造函数当构造函数用和当普通函数用,函数里的方法的作用环境对象不同)2构造函数的问题6.2.3原型模式(prototype属性、原型对象的constructor属性;实例的[[Prototype]]属性;isPrototypeOf()、getPrototypeOf()、hasOwnProperty()) 1.理解原型对象2.原型与in操作符(in操作符、for-in循环、getOwnPropertyNames()、keys())3.更简单的原型语法(用对象字面量法重写原型属性)4原型的动态性(对象字面量修改法要放在使用实例的前面,否则修改在旧实例下无效)5.原生对象的原型(原生对象的方法都是写在原生对象的原型里的)6.原型对象的问题(属性是引用类型值的时候,修改结果也会被共享)6.2.4组合使用构造函数模式和原型模式(属性写在构造函数,方法写在原型)6.2.5动态原型模式6.2.6寄生构造函数模式(存在无法对象识别的问题)6.2.7稳妥构造函数模式(问题同上)6.3继承(JavaScript只有实现继承)6.3.1原型链(利用实例内部指向超类型原型对象的指针[[Prototype]]实现继承)1.别忘记默认的原型(Object是所有函数的默认原型)2.确定原型和实例的关系(使用instanceof操作符和isPrototypeOf()方法确定原型和实例的关系)3.谨慎地定义方法(子类添加或改写超类型方法的代码一定要写在替换原型的语句之后;对象字面量法重写原型链会导致继承关系断裂)4原型链的问题(还是引用类型值放在原型属性中的问题)6.3.2借用构造函数(有缺陷的方法)1.传递参数2.借用构造函数的问题(还是方法无法复用的问题)6.3.3组合继承6.3.4原型式继承(create()就是实现原型式继承的方法)6.3.5寄生式继承首先要知道的:ECMAScript中没有类这个概念。ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值,对象或者函数。”每个对象都是基于一个引用类型创建的,这个引用类型可以是第五章讨论的原生类型(Array,Object,Function等),也可以是开发人员定义的类型。6.1理解对象这节就是告诉我们定义对象的时候不要用Object的构造函数那种方法,直接用对象字面量创建对象更好。例子:var person = { name:"Nicholas", age:29, job:"Software Engineer", sayName:function(){ console.log(this.name); }};6.1.1属性类型ECMAScript的对象中有两种不同的属性:数据属性和访问器属性。ECMA-262第5版在定义只有内部才有的特性(attribute)时,描述了属性(property)的各种特征。ECMA-262定义特性是为了实现JavaScript引擎用的,所以在JavaScript中无法直接访问特性。为了表示特性是内部值,该规范把他们放在了两对方括号中。例如[[Enumerable]]([[特性]])1.数据属性数据属性包含一个数据值的位置。在这个位置可以读取和写入值(这两句还不懂)。数据属性有4个描述其行为的特性:·[ [Configurable] ]:表示能否通过delete删除属性,能否修改属性的特性,能否把属性从数据属性修改为访问器属性。直接在对象上定义的属性,[ [Configurable] ]的默认值一般为true。·[ [Enumerable] ]:表示能否通过for - in循环返回属性。直接在对象上定义的属性,[ [Enumerable] ] 的默认值一般为true。·[ [Writable] ]:表示该属性是否可写(属性值能不能修改)。直接在对象上定义的属性,[ [Writable ] ] 的默认值一般为true。·[ [Value] ]:包含这个属性的数据值,读取属性值的时候,就是在这个位置读。写入属性值的时候,新值就保存在这个位置。这个特性的默认值为undefined。例子:var person = { name:"Nicholas",};这里创建了一个名为name的属性,为它指定的值是“Nicholas”。也就是说,[[value]]特性将被设定为“Nicholas”,而对这个值的任何修改都将反映在这个位置。name属性的[Configurable] ],[ [Writable] ],[ [Enumerable] ]都默认为true。要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字、一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable、value中的其中一个或多个。例子:var person = {};Object.defineProperty(person,"name",{ writable:false, value:"Nicholas" //利用Object.defineProperty给属性name赋值,顺便设置了这个本来没有的属性});console.log(person.name); //"Nicholas"person.name = "Greg";console.log(person.name); //"Nicholas"这里例子创建了一个名为name的属性,设置name属性的[ [Writable] ]为不可写。如果尝试为它指定新值,在非严格模式下,赋值操作被忽略,在严格模式下,赋值操作将会导致抛出错误。类似的“非忽严错”的规则也适用于把[ [Configurable] ]设置为false的属性:var person = {};Object.defineProperty(person,"name",{ configurable:false, value:"Nicholas"});console.log(person.name); //"Nicholas"delete person.nameconsole.log(person.name); //"Nicholas"configurable被设置为false,则无法从对象中删除属性,若进行删除操作,则会“非忽严错”。注意:一旦把属性定义为不可配置的,就不能把它变回可配置的了(有意思)。此时,再调用Object.defineProperty()方法修改特性,就会抛出错误。注意:在调用Object.defineProperty()修改属性的特性时,如果不指定,则configurable、enumerable、writable的默认值就会变成false!属性就会自动变成不可配置,不能通过for - in循环返回属性。所以设置的时候最好四个都写上去。不过作者说,在多数情况下,没有必要利用到Object.defineProperty()。不过,理解这些概念对理解JavaScript对象非常有用。✎IE8是第一个实现Object.defineProperty()的浏览器版本,但是仍然存在限制,实现不完全。建议不要在IE8上使用这个方法。2.访问器属性需要使用getter和setter函数才能读写的属性。(但是这两个函数都不是必需的)。在读取访问器属性时,会调用getter函数,在写入访问器属性时,会调用setter函数并传入新值。访问器属性有4个特性:·[ [Configurable] ]:表示能否通过delete删除属性,能否修改属性的特性,能否把属性从数据属性修改为访问器属性。直接在对象上定义的属性,[ [Configurable] ]的默认值一般为true。·[ [Enumerable] ]:表示能否通过for - in循环返回属性。直接在对象上定义的属性,[ [Enumerable] ] 的默认值一般为true。·[ [Get] ]:在读取属性时调用的函数。默认值为undefined。·[ [Set] ]:在写入属性时调用的函数。默认值为undefined。访问器属性不能直接定义,必须使用Object.defineProperty()定义。例子:var book = { _year:2004, edition:1};Object.defineProperty(book,"year",{ get:function(){ return this._year; }, set:function(newValue){ if(newValue>2004){ this._year = newValue; this.edition += newValue-2004; } }});book.year = 2005;console.log(book.edition); // 2以上代码创建了一个book对象,并给它定义两个默认的属性:_year和edition。_year前面的下划线是一种常用的人为规定的记号(没有程序上的作用),用于表示只能通过对象方法访问的属性(亲测console.log(book._year)可以访问到,大概是因为有用到对象方法访问)。注意这个_year属性并不是访问器属性。访问器属性是year。year包含一个getter函数和setter函数。getter函数返回_year的值,setter函数通过计算来确定正确的版本。因此,把year属性修改为2005会导致_year变成2005(setter属性set的属性不仅是自己的,还可以是同一对象下的其他属性)。而edition变为2。这是使用访问器属性的常见方式,即设置一个属性的值会导致其他属性发生变化。支持ECMAScript5的这两个方法的浏览器有IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。在这个方法之前,要创建访问器,一般都使用两个非标准的方法:defineGetter()和defineSetter()。这两个方法最初都是由Firefox引入的,后来Safari 3、Chrome 1和Opera 9.5也给出了相同的实现。使用这两个遗留的方法,可以像下面这样重写前面的例子:var book = { _year:2004, edition:1};book.defineGetter("year",function(){ return this._year;});book.defineSetter("year",function(newValue){ if(newValue>2004){ this._year = newValue; this.edition += newValue-2004; }});book.year = 2005;console.log(book.edition);console.log(book.year); // 2在不支持Object.defineProperty()方法的浏览器中不能修改[ [Configurable] ]和[ [Enumerable] ]特性。6.1.2定义多个属性由于为对象定义多个属性的可能性很大,ECMAScript5又定义了一个Object.defineProperties()方法。利用这个方法可以一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加或修改属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。例子:var book1 = {};object.defineProperties(book,{ _year:{ writable:true, value:2004 }, edition:{ writable:true, value:1 }, year:{ get:function(){ return this._year; }, set:function(){ if(newValue > 2004){ if(newValue > 2004){ this._year = newValue; this.edition += newValue -2004; } } } }});支持Object.defineProperties() 方法的浏览器有:IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。 6.1.3读取属性的特性使用ECMAScript5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象。是访问器属性就有访问器属性的四个特性,是数据属性就有数据属性的四个特性。例如上面的book对象为例子:console.log(Object.getOwnPropertyDescriptor(book,"_year"));//返回:Object {value: 2004, writable: true, enumerable: true, configurable: true}console.log(Object.getOwnPropertyDescriptor(book,"year"));//返回:Object {enumerable: false, configurable: false} 很奇怪set和get特性要通过Object.getOwnPropertyDescriptor(book,"year").get和~.set去得到里面set和get里面的函数。在JavaScript中,可以针对任何对象——包括DOM和BOM对象,使用Object.getOwnPropertyDescriptor()方法。支持这个方法的浏览器有:IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。6.2创建对象为了不写重复代码讨论了很多创建对象的方法。6.2.1工厂模式工厂模式是软件工程领域广为人知的设计模式,这种模式抽象了创建具体对象的过程(把创建对象的过程抽象简化)。因为在ECMAScript中没有类,开发人员发明了一种函数,用函数来封装以特定接口创建对象的细节。例子:function createPerson(name,age,job){ var o = new Object(); //显式地创建对象 o.name = name; o.age = age; o.job = job; o.sayName = function(){ console.log(this.name) }; return 0; //有return语句,不知道有什么不好}var person = createPerson("Nicholas",29,"Software Engineer");工厂模式解决了创建多个相似对象要写重复代码的问题,但没有解决对象识别的问题(即怎样知道一个对象的类型,后面的由Person对象实例化的对象的类型就是Person类型而不是Object类型,解决了对象识别问题)。随着JavaScript的发展,又一个模式出现了。6.2.2构造函数模式前面几章说过,ECMAScript中的构造函数可以用来创建特定类型的对象(?说过?还不知道原来特定类型的对象可以自己定义,比如后面的Person对象)。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。例如,可以使用构造函数模式将前面的例子重写如下:function Person(name,age,job){ //构造函数首字母大写,普通函数首字母小写 this.name = name; this.age = age; this.job = job; this.sayName = function(){ console.log(this.name); };}var person1 = new Person("Nicholas",29,"Software Engineer");var person2 = new Person("Greg",27,"Doctor");console.log(person1.name);在这个例子中,Person()与createPerson()的不同之处在于:没有显式地调用对象,直接将属性和方法赋给了this对象,没有return语句。✎注意:遵照其他OO语言的惯例,构造函数始终都以大写字母开头,非构造函数用小写字母开头。目的是为了区别于ECMAScript中的其他函数。要创建Person对象的实例,必须使用new操作符。以这种方式调用构造函数会经历以下4个步骤:(1)创建一个新对象;(2)将构造函数的作用域赋给新对象(环境变量对象就变成了这个对象,因此this就指向了这个对象);(3)执行构造函数中的代码(为这个新对象添加属性);(4)返回新对象。(对象实例诞生!)在前面例子的最后,person1和person2分别保存着Perso的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person(亲测console.log(person1.constructor)打印出了整个构造函数Person的源代码)。例子:console.log(person1.constructor == Person); //trueconsole.log(person1.constructor) //打印出上面构造函数Person的源代码对象的constructor属性最初是用来标识对象类型的。但是想要检测对象类型,还是instanceOf操作符更可靠一些。例子中创建的所有对象既是Object的实例,也是Person的实例,这一点可以通过instanceOf()操作符验证:console.log(person1 instanceof Object); //trueconsole.log(person1 instanceof Person); //true创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式完胜工厂模式的地方。在这个例子中person1和person2之所以同时是Object的实例,是因为所有对象均继承自Object(详细内容稍后讨论)。✎:以这种方式定义的构造函数是定义在Global对象(在浏览器中是window对象)中的。(???又说所有对象继承自Object???)第八章会详细讨论浏览器对象模型(BOM)1将构造函数当作函数构造函数与其他函数的唯一区别,就在于调用他们的方式不同。但是归根到底构造函数还是函数。任何函数,只要通过new操作符调用,它就可以作为构造函数。而构造函数如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。例如,前面例子中的Person()函数,可以用下面的任一方式调用,例子的重点是,体会用构造函数的方式调用和用普通方式调用两种方法,他们的作用域有什么不同://当作构造函数调用var person1 = new Person("Nicholas",29,"Software Engineer");person1.sayName();//作为普通函数调用Person("Greg",27,"Doctor");window.sayName();//作为普通函数在另一个对象的作用域中调用var o = new Object();Person.call(o,"Kristen",25,"Nurse");o.sayName();第一种用法是构造函数的经典用法。第二种用普通的函数调用法:属性和方法都添加给了window对象(window.sayName()正确的过程是,方法内的this总是指向Global对象,在浏览器中就变成了window对象)。第三种是重点:使用call()在对象o的作用域中调用Person函数,类似在window对象中调用Person,调用后o也拥有 了所有属性和sayName()方法。2构造函数的问题还是有人挑出了构造函数的问题。构造函数的问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例,因为他们在不同的对象环境中创建。从逻辑上讲,此时的构造函数可以这样定义:function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = new Function("console.log(this.name)"); //与声明函数在逻辑上是等价的}从这个角度来看构造函数,就更容易明白每个Person实例都包含一个不同的Function实例(但他们的用途完全一样,就是显示name属性)(感觉学到了JavaScript不可言传的东西,这两个方法看起来一模一样,但是他们是两个不同的实例,要纠结他们是不同的实例的原因是,这样会产生不同的作用域链和标识符解析)。但创建Function新实例的机制仍然是相同的。因此,不同对象实例上的同名函数是不相等的,即person1的sayName()不等于person2的sayName()。以下代码可以证明这点:console.log(person1.sayName == person2.sayName); // false然而,创建两个完成同样任务的Function实例的确没有必要(导致产生不同的作用域链和标识符解析 );况且有this对象在,根本不用在执行代码前把函数绑定到每一个要实例的对象上,因此,可以像下面这种“等一下就要被作者推翻的方法”一样,把函数定义转移到构造函数外面来解决这个问题:function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName sayName; //与声明函数在逻辑上是等价的}function sayName(){ console.log(this.name);}不知道这样会不会产生不同的作用域链,但是这样的好处是Function不用实例两次吧,可能是Function的作用域链不会产生两个,但是实例对象的作用域链还是会产生的。亲测console.log(person1.sayName == person2.sayName)返回的是true。证明他们用到的是一样的在全局环境下的sayName()函数。但是这样又有一个问题,你在全局作用域定义的函数实际上只被某一个对象调用,如果对象需要很多个这样的方法,那不就要定义很多的全局函数吗。如此一来,我们这个自定义的引用类型就没有丝毫封装性可言了(太多函数暴漏在全局作用域中)。好在,我们可以用原型模式解决这个问题。6.2.3原型模式我们创建的每个函数都有一个prototype(原型)属性。这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的对象的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象(刚又说prototype是函数属性,现在又说是那个又构造函数实例出来的对象的原型对象?)使用原型对象的好处是可以让所有对象实例共享他们所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中(相当于对属性和方法设置一个默认值,在没有明确赋值的情况下,属性和方法的值就等于这个默认值)。例子:function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){ console.log(this.name);};var person1 = new Person();person1.sayName();在此,我们将所有属性和sayName()直接添加到Person的prototype属性中,构造函数变成了空函数。即便如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法,而且这些属性和方法是所有对象实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个sayName()函数。console.log(person1.sayName == person2.sayName)//true.要理解原型模式的工作原理,必须先理解ECMAcript中原型对象的性质。1.理解原型对象无论什么情况,只要创建了一个新函数,就会根据一组特定的规则(没有详讲)为该函数创建一个prototype属性(所有函数自带prototype属性)。这个属性指向函数的原型对象(前面又说prototype是原型对象现在又说是指向原型对象,大概因为prototype是个指针指向了原型对象,所以也可以说prototype属性是原型对象吧,有点函数名是函数的指针,所以也可以说这个函数是XX函数名,XX函数名是这个函数的意思)。在默认情况下,所有原型对象(暂且说是prototype,因为prototype指向原型对象)都会自动获得一个constructor(构造函数)属性(每个原型对象自带constructor属性),这个属性是一个指向prototype属性所在函数的指针。就拿前面的例子来说,Person.prototype.constructor指向Person这个函数,这个构造函数(也就是说一个函数里的prototype属性里的constructor属性是个指针,指向这个函数自己)。创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法和属性,则都是从Object继承而来。当调用构造函数创建一个实例后(注意,这里开始讲的是实例,不是原来那个构造函数了),该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262管这个指针叫[[Prototype]]。虽然在JavaScript中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个属性protp;而在其他浏览器中,[[Prototype]]则是完全不可见的。要明确的一点是,这个连接是在实例与构造函数的原型对象之间,而不是在实例与构造函数之间(即[[Prototype]]指向构造函数的prototype而不是指向构造函数)。以前面的Person构造函数和Person.prototype创建实例的代码为例,图6-1展示了各个对象之间的关系:上图展示了Person构造函数、Person的原型属性以及Person现有的两个实例之间的关系。在此Person.prototype指向了原型对象,而原型对象中的constructor属性又指向了Person.prototype。原型对象中除了包含constructor属性之外,还包括后来添加的其他属性。Person的每个实例——person1和peron2都包含一个内部属性[[Prototype]],该属性又指向了原型对象(书里这里写的指向Person.prototype,但应该是指向原型对象才对);换句话说,他们与构造函数没有直接关系(实例与构造函数没有直接关系!?)。此外,要格外注意的是,虽然这两个实例都不包括属性和方法,但我们却可以调用person1.sayName()。这是通过查找对象属性的过程来实现的。虽然没有标准的方式访问[[Prototype]](书里说所有实现(即浏览器)都无法访问到[[Prototype]],但前面已经说了有三个浏览器可以用proto访问,亲测也确实可以),但可以通过isPrototypeOf()方法来确定对象之间知否存在这种关系。从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的这个对象(Person.prototype),那么这个方法就返回true。例子:console.log(Person.prototype.isPrototypeOf(person1)); //trueconsole.log(Person.prototype.isPrototypeOf(person2)); //trueperson1,2的原型对象是Person.prototype,所以返回true,也说明person1,2的[[Prototype]]指向Person.prototype。记住[[Prototype]]是实例对象的内部属性,是一个指针。ECMAScript5还增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现(浏览器)中,这个方法返回[[Prototype]]的值。例子:console.log(Object.getPrototypeOf(person1)==Person.prototype); //true,相等,可以证明person1的[[Prototype]]是指向Person.prototype的console.log(Object.getPrototypeOf(person1).name); //"Nicholas" Object.getPrototypeOf()可以用来获取对象原型的属性第一行代码确定了Object.getPrototypeOf()返回的对象实际就是这个对象的原型(又跟我自己在注释里写的见解不一样)。第二行代码取得对象原型中name属性的值,即“Nicholas”。使用Object.getPrototypeOf()可以方便地取得一个对象的原型(prototype)。而这在利用原型实现继承的情况下是非常重要的。支持这个方法的浏览器有: IE9+、Firefox 3.5+、Safari 5+、Opera 12+和Chrome。知道了构造函数有prototype属性之后,我们可以知道,当代码读取某个对象的某个属性时,都会执行一次搜索,目的是找到具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性值;如果没有找到,就继续搜索指针([[Prototype]])指向的原型对象(prototype),在原型对象中查找具有给定名字的属性,如果在原型对象中找到这个属性,就返回这个属性的值。所以我们在调用实例对象的属性和方法时会执行两次搜索。这正是多个对象实例共享原型对象所保存的属性和方法的基本原理。✎:前面提到过,原型对象最初只包含一个constructor属性,这个属性也是共享的,可以通过对象实例访问。(亲测访问这个属性会返回构造函数的源代码,之前亲测过了?)虽然我们可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加一个与原型对象中存在的同名属性,则我们就在实例中创建该属性。该属性会屏蔽掉原型中那个属性。例子:function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){ console.log(this.name);};var person1 = new Person();var person2 = new Person();person1.name="Mike"person1.sayName(); //“Mike” ←来自实例person1.sayName(); //"Niicholas" ←来自原型当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性(书中对“屏蔽”两个字黑体加粗,但是我觉得屏蔽的原理估计又有一大篇文章要讲)。如果把实例中的属性设置为null,也只会在实例中设置这个属性,无法恢复其指向原型的连接。要恢复其指向原型的连接,方法就是使用delete操作符完全删除实例属性,从而我们就可以重新访问原型中的属性。例子:person1.name = "Mike";person1.name = null;person1.sayName(); //nulldelete person1.name person1.sayName(); //"Nicholas"使用hasOwnProperty()(注意是Property不是prototype了,property是属性的意思)方法可以检测一个属性是存在实例中,还是原型中。这个方法(不要忘了这个方法继承自Object)在检测的属性是存在实例中的时候,返回true。例子:var person1 = new Person();person1.name="Mike"delete person1.name;console.log(person1.hasOwnProperty("name")); //false,把第三行注释掉就变成true通过hasOwnProperty()方法,我们访问属性的时候到底访问的是原型对象中的属性还是实例重新定义的属性就一清二楚了。(做了有趣的实验,把实例中的属性保存为跟原型对象属性一样的值,用hasOwnProperty()方法返回的是true。可不可以说明他们存放的空间不一样?)person1.name="Nicholas" 与原型对象中的属性值相同console.log(person1.hasOwnProperty("name")); //true✎:ECMAScript5的Object.getOwnPropertyDescriptor()方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescroptor()方法。原来前面已经讲过这个方法竟然忘了。不知道什么是属性的描述符,测试了一次发现返回的是对象的属性的四个特性:console.log(Object.getOwnPropertyDescriptor(person1,"name"));//Object {value: "Nicholas", writable: true, enumerable: true, configurable: true}注意上面的那句话啊!!!每个词都是关键啊!上面说要在原型对象上调用,不是直接用构造函数名调用啊,用构造函数名调用很奇怪,console.log(Object.getOwnPropertyDescriptor(person1,"name"));返回的描述符中的value是“Person”,第二个参数改成“job”或“age”返回的却是undefined。用console.log(Object.getOwnPropertyDescriptor(Person.prototype,"name"));就能返回value是“Nicholas”的描述符。例子:console.log(Object.getOwnPropertyDescriptor(Person,"job")); //undefinedconsole.log(Object.getOwnPropertyDescriptor(Person,"name")); //Object {value: "Person", writable: false, enumerable: false, configurable: true}console.log(Object.getOwnPropertyDescriptor(Person.prototype,"name"));//Object {value: "Nicholas", writable: true, enumerable: true, configurable: true}后来又继续测试console.log(Person.name);返回的也是“Person”,难道name是每个函数都有的属性返回函数的函数名?2.原型与 in 操作符in操作符的使用有两种方式,一种是单独使用和在for - in循环中使用。在单独使用时,in操作符用于判断某一属性是否在对象中,无论是在实例中还是原型中,只要有就返回true。例子:console.log("name" in person1) //true 无论name是在实例中还是原型中console.log("name" in Person) //顺便做了个实验,name属性确实在Person构造函数中console.log("name" in Person) //false这让我想到一个问题,Person已经有一个name属性的值是“Person”,我们又通过原型对象给原型对象中添加了name属性的值是Nicholas,那值为“Person”的name属性又是来自哪里??做了个实验,创建一个叫zoo的空函数:function zoo(){}console.log("name" in zoo); //truein操作符判断后也是返回true,这个name不在原型对象中,也不在实例中,仍然返回true,这个name到底是放在哪?有意思。又做了好多实验,发现用函数声明,函数表达式创建出来的函数都有name属性,但是把这个自定义的函数当构造函数用,实例出来的对象就没有这个name属性。所以还是只有函数有咯。用Object()构造函数实例的对象也没有name属性。利用 in操作符,结合hasOwnProperty()方法,我们可以创建一个函数来判断一个属性值到底是来自实例还是原型:function hasPrototypeProperty(object,name){ return !object.hasOwnProperty(name)&&(name in object);}这个是书里定义的函数,我觉得有点绕。这个是检测属性是原型属性返回true。是实例中的属性就返回假,跟我想的相反。例子:function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){ console.log(this.name);};var person = new Person();console.log(hasPrototypePerty(person,"name")) //trueperson.name = "Greg";console.log(hasPrototypePerty(person,"name")) //falsefor - in循环的作用,返回的是所有能通过对象访问的,可枚举的(enumerated)属性,其中既包括实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性([[Enumerable]]:false)的实例属性也会在for - in属性中返回(意思是:比如toString()方法是Object的原生方法,一般这类方法都是不可枚举的,如果你在实例对象中重写了toString(),那toString()就变成了可枚举的,会在for - in循环中被列出来)。因为根据规定,所有开发人员定义的属性都是可枚举的。但是IE8及更早版本会有BUG,即屏蔽不可枚举属性的实例属性也不会出现在for - in循环中。例如:var o = { toString : function(){ //重写toString()方法 return "My Object"; }};for(var prop in o){ if(prop == "toString"){ console.log("Found String"); //IE8中不会显示 }}上面的例子,本来我已经改写了toString()了,应该是可以被for - in循环枚举出来的,但是在IE8及更早版本并不会被枚举出来。这个BUG会影响默认不可枚举的所有属性和方法,包括:hasOwnProperty()、propertyIsEnumerable()、toLocalString()、toString()和valueOf()。ECMAScript5把constructor和property属性的[[Enumerable]]特性设置为false,但并不是所有浏览器都乖乖听ECMAScript的。要想获取对象上所有可枚举的实例属性(实例属性!原型对象中的属性不会被列举出来),可以使用ECMAScript5的Object.keys()方法,这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。例子:function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){ console.log(this.name);};var keys = Object.keys(Person.prototype);console.log(keys) //["name", "age", "job", "sayName"]var peron1 = new Person();person1.name = "Mike";console.log(Object.keys(person1)) //["name"]有什么实例,数组里才有什么属性,property里定义的属性是Person.prototype对象的实例属性,不是person1的实例属性,person1的实例属性只有一个。如果想要得到所有实例属性,无论是否可枚举,可以使用Object.getOwnPropertyNames()方法。例子:console.log(Object.getOwnPropertyNames(Person.prototype)); //["constructor", "name", "age", "job", "sayName"]person1.age=28;Object.defineProperty(person1,"name",{ writable:false, value:"Mike", enumerable:false}); console.log(Object.keys(person1)); // ["age"]console.log(Object.getOwnPropertyNames(person1)); // ["name", "age"]还记得前面说的ECMAScript5把constructor(构造函数)属性也设置为不可枚举吗?看来这个浏览器遵循ECMAScript5的规定。Object.keys()和Object.getOwnPropertyNames()都可以用来替代for - in循环。支持这两个方法的浏览器有:IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。记住他们返回的都是实例属性,不包括原型对象中的属性。3.更简单的原型语法作者介绍给我们,前面的每添加一个原型属性就要敲一遍Person.prototype的方法太傻了,为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。例子:function Person(){}Person.prototype = { constructor : Person, name : "Nicholas", age :29, job : "Software Engineer", sayName : function(){ console.log(this.name); }}理清除,Person.property是指针指向原型对象,所以用对象字面量法给原型对象添加属性和方法是没毛病的。但是这种写法本质上重写了Person.prototype对象,而每个prototype属性都自带有constructor属性,如果在重写中没有加入constructor属性,为constructor重新赋值,则constructor属性将从原型对象中消失。说消失是不准确的,这时候原型对象的constructor属性不再指向Person构造函数,而是指向Object构造函数。但是Person对象的实例仍然是Person的实例。下面的例子假设重写时没有加入constructor属性:console.log(friend instanceof Object); //trueconsole.log(friend instanceof Person); //trueconsole.log(friend.constructor == Person); //falseconsole.log(friend.constructor == Object); //true还有一点要注意的是,重写之后的constructor属性,它的[[Enumerable]]特性会被设置为true(当然是在ECMAScript5已经规定了constructor属性的[[Enumerable]]特性 默认为false且浏览器听它的的情况下)。如果想让它重新变为false,可以人工用Object.defineProperty()方法设置。4.原型的动态性由于在原型中查找值是一次搜索,所以即使先创建实例后修改原型也是可以的。例子:var friend = new Person();Person.prototype.sayHi = function(){ console.log("Hi");};friend.sayHi(); //"Hi" (正常运行)可以看到我们先实例化了Person对象为friend,再修改Person的原型对象。firend仍然能访问到sayHi()方法。其原因可以归结为实例与原型之间的松散连接关系。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的sayHi属性并返回保存在那里的函数。尽管可以随时为原型添加属性和方法,但如果是重写整个原型对象,情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototyoe]]指针,而把原型重写为另外一个对象就等于切断了构造函数与最初原型之间的联系(从书中关系图来看,最初原型(旧原型对象)仍指向构造函数,但是构造函数指向了新的原型对象,所以也不是完全切断吧)。作者写到这里突然要我们记住:实例中的指针仅指向原型,不指向构造函数(实例与构造函数之间没有联系)例子:function Person(){}var friend = new Person();Person.prototype = { constructor : Person, name : "Nicholas", age :29, job : "Software Engineer", sayName : function(){ console.log(this.name); }};friend.sayName(); //报错: friend.sayName is not a function在这个例子中,我们先创建了一个Person对象,先实例化出一个friend,再重写了原型对象。然后在调用friend.sayName()的时候就出现了错误。因为friend指向的原型是旧原型,不包含以该名字命名的属性。图6-3展示了这个过程。可以看到重写原型对象会切断现有原型与任何之前已经存在的对象实例之间的关系;之前已经存在的对象实例 仍然引用的是最初的原型。也就是说用重写原型的方式给原型对象添加属性的方法要在重写的那段代码之后再实例化对象,如果是用Person.prototype.属性 = XX这种方法则不存在这种问题。5.原生对象的原型这节讲的是原生对象的原型,什么是原生对象?就是原生的引用类型(Array,Object,String and so on)所有原生引用类型都在其构造函数的原型上(注意原型是谁的属性(prototype是个属性,这个属性是个指针),是构造函数的属性)定义了方法。例如Array类型的sort()方法就是定义在Array.prototype中的。String类型的substring()方法就是定义在String.prototype中的。证明例子:console.log(typeof Array.prototype.sort); //functionconsole.log(typeof String.prototype.substring); //functionconsole.log(typeof Array.sort); //undefinedtypeof Array.sort竟然是undefined是令我惊讶的,我以为后台会自动规定sort是Array的方法(所以以前的每次array.sort( )的调用都是在调用Array对象的原型对象中的方法,向Array.prototype致敬!)通过原生对象的原型,不仅可以取得所有默认方法的引用,而且可以定义新方法,可以像修改自定义对象的原型一样修改原生对象的原型,在里面添加新的方法。亲测可以改写原本已经定义的方法,比如toString()这些。但是不要这样做。书中给出了在原生对象的原型中添加方法的例子,但是他建议不哟这样做,避免命名冲突。就不举例了。6.原型对象的问题原型对象也是有问题的(不能说BUG,合情合理)。原型对象的好处是可以共享属性,但最大的问题也处在了共享的问题上,属性的值是基本类型值还好,如果是包含引用类型的属性,就会出问题了:function Car(){}Car.prototype={ constructor:Car, friends:["Shelby","Court"]}var car1 = new Car();var car2 = new Car();car1.friends.push("Van");console.log(car1.friends); //["Shelby", "Court", "Van"]console.log(car2.friends); //["Shelby", "Court", "Van"]本来只是像给实例对象car1的friends属性中的数组加入“Van”而已,但是最后car2也有了。因为car1并没有自己重写friends属性,而是直接在默认有的属性值中添加数据,而这个数组是保存在Person.prototype中的,所以刚刚提到的修改也会通过person2.friends反映出来。所以很少有人单独使用原型模式。6.2.4组合使用构造函数模式和原型模式创建自定义类型的最常见方式,是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。这样,每个实例都有自己的一份实例属性的副本(不懂这个实例属性的副本是什么意思),但同时共享对方法的引用,最大限度地节省了内存。重写前面的代码的例子:function Person(name,age,job){ //通过对构造函数传参给独特的属性赋值 this.name = name; this.age = age; this.job = job; this.friends = ["Shelby","Court"]; //引用类型写在构造函数中}Person.prototype = { constructor:Person, //别忘了重写constructor属性 sayName:function(){ //一样的方法可以共用 console.log(this.name); }}var person3 = new Person("Nicholas",29,"Software Engineer");var person4 = new Person("Greg",27,"Doctor");person3.friends.push("Van");console.log(person3.friends); //["Shelby", "Court", "Van"]console.log(person4.friends); //["Shelby", "Court"]console.log(person3.friends == person4.friends); //falseconsole.log(person3.sayName == person4.sayName); //true这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。后面几节讲的都是定义引用类型的几种方法,既然说上面的是最好用的,后面的就不写了。6.2.5动态原型模式为看不惯上面的写法的有其他OO语言开发人员使用的方法。6.2.6寄生构造函数模式作者不推荐这种方法,因为这个方法返回的对象与构造函数或者与构造函数的原型属性之间没有关系。也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。也就是说假设用创建了一个Person构造函数,用这个构造函数实例出来的对象并不属于Person,而是属于Object。6.2.7稳妥构造函数模式这个方法也是创建的对象与构造函数之间没有什么关系。6.3继承许多OO语言都支持两个继承方式:接口继承和实现继承。接口继承只继承方法签名。而实现继承则继承实际的方法。如前面章节讲过的,由于函数没有签名(函数没有“函数名”只是一个指针,所以叫没有签名?),在ECMAScript中无法实现接口继承。实现继承主要是依靠原型链实现的。6.3.1原型链ECMAScript中描述了原型链的概念。并将原型链作为实现继承的主要方法。其基本思想是:利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型链实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针([[Prototype]])。那么,如果我们让原型对象等于另一个类型的实例,会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针。相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是所谓原型链的基本概念。实现原型链的基本模式如下:function SuperType(){ this.property = true; //定义的变量名有点复杂,看得有点晕,其实就是一个值为true的变量}SuperType.prototype.getSuperValue = function(){ return this.property;};function SubType(){ this.subproperty = false; //另一个false的变量}//继承了SuperTypeSubType.prototype = new SuperType(); //让一个构造函数A的原型等于另一个构造函数B的实例,这个实例又有一个指向B的原型的指针SubType.prototype.getSubValue = function(){ //这里看得有点懵,其实就是给子对象的原型定义了一个方法,跟上面的代码无关,也没用上 return this.subproperty;}var instance = new SubType(); //创建子对象的实例console.log(instance.getSuperValue()); //true 发现子对象的实例可以调用父对象的方法,返回保存在父对象的变量以上代码定义了两个类型:SuperType和SubType。每个类型各有一个属性一个方法。他们的区别是SubType继承了SuperType。继承的步骤是:通过创建SuperType的实例(new SupperType),并将该实例赋给了SubType.prototype实现。实现的本质是重写原型对象,给原型对象赋值为一个新类型的实例(所以SubType.prototype里的constructor属性也改变了,不是没有了,变成了新类型实例里指向父构造函数的constructor属性,此时constructor为SuperType)。换句话说,原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。在确定了继承关系后,我们又不知道有什么卵用地给SubType.prototype添加了一个方法。这样就在继承了SuperType的实例的属性和方法后,又有了自己的方法。这个例子中的实例以及构造函数和原型之间的关系如图:在代码中,我们没有使用SubType默认提供的原型,而是把它重写成了一个新原型,这个新原型就是SuperType的实例。于是,新原型不仅具有作为一个SuperType的实例所拥有的全部属性和方法,而且内部还有一个指针([[Prototype]],通了,一开始觉得叫[[Prototype]] 不恰当,想明白了根本不会!因为这个[[Prototype]]属性本来就是SuperType实例中有的),指向SuperType的原型。结果就是这样:instance的[[Prototype]]指向SubType的原型,SubType的原型里的[[Prototype]]又指向SuperType的原型。注意:getSuperValue()方法仍然还在SuperType.prototype里,SubType.prototype里没有,但property则在SubType.prototype中。书中的解释是:因为property是一个实例属性,而getSuperValue()是一个原型方法(实例属性会跟着继承在子对象中,原型方法仍然留在父对象的原型中)。解释:既然SubType.prototype现在是SuperType的实例,那么property当然位于实例中了,而getSuperValue()是在原型中,不在实例中。此外,要注意instance.constructor现在指向的是SuperType,这是因为原来Subtype的原型指向了另一个对象SuperType 的实例,这个实例指向了对象SuperType的原型,SuperType的原型里的constructor属性就是指向SuperType。(书里这样写,可是为什么instance会有constructor属性?constructor不是原型对象才有吗?亲测每个实例都有这个属性,返回这个实例的构造函数。字符串类型的constructor返回到是String()构造函数)通过原型链的介绍,原型搜索机制又要重新扩展:当以读取模式访问一个实例属性时,首先会在实例中搜索该属性,如果没有找到该属性,就继续搜索实例的原型,在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用instance.getSuperValue()会经历三个搜索步骤:1)搜索实例;2)搜索SubType.prototype;3)搜索SuperType.prototype,最后一步找到这个方法。搜索过程总是这样一环一环地走到原型链末端才停下来。1.别忘记默认的原型事实上,前面例子中展示的原型链少了一环。我们知道,所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。大家要记住,所有函数的默认原型都是Object的实例(new Object( )),因此默认原型都会包含一个内部指针,指向Object.prototype(又不说这个指针的名字)。这也正是所有自定义类型都会继承toString()等默认方法的原因(上面讲原型对象的时候说过,原生引用类型的方法都是定义在prototype上的)。所以所有函数的toString()等默认方法不是在他们自己的prototype属性中,而是通过指针,一层层地找到那些放在Object.prototype里的原型方法的,这样就减少了还能多内存占用吧。所以上面例子展示的原型链中还应包括另外一个继承层次,如图才是真正完整的原型链:2.确定原型和实例的关系可以通过两种方式来确定原型和实例之间的管理,第一种方式使用instanceOf操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。例子:console.log(instance instanceof Object) //trueconsole.log(instance instanceof SubType) //trueconsole.log(instance instanceof SuperType) //true由于原型链的关系,我们可以说instance是Object、SuperType、SubType中任何一个类型的实例。因此三个构造函数的结果都返回true。第二种方式是使用isPrototypeOf()方法,同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()也会返回true:console.log(Object.prototype.isPrototypeOf(instance)); //trueconsole.log(SubType.prototype.isPrototypeOf(instance)); //trueconsole.log(SuperType.prototype.isPrototypeOf(instance)); //true3.谨慎地定义方法子类型有时候会需要覆盖超类型中的某个方法,或者添加超类型中不存在的某个方法。但无论如何,给原型添加方法的代码一定要放在替换原型的语句之后。例子:function SuperType(){ this.property = true;}SuperType.prototype.getSuperValue = function(){ return this.property;};function SubType(){ this.subproperty = false;}//继承了SuperTypeSubType.prototype = new SuperType(); //无论是覆盖还是添加,都要放在替换原型的语句之后//添加新方法SubType.prototype.getSubValue = function(){ return this.subproperty;}//重写超类型中的方法SubType.prototype.getSuperValue = function(){ return false;}var instance = new SubType();console.log(instance.getSuperValue()); //false在上面代码中,加粗部分是两个方法的定义。第一个方法getSubValue()被添加到了SubType中,第二个方法getSuperValue()是原型链中已经存在的一个方法,但重写这个方法将会屏蔽原来的那个方法。换句花说,当通过SubType的实例(instance)调用getSuperValue()时,调用的是这个重新定义的方法;但通过SuperType的实例调用getSuperValue()时,还会继续调用原来的那个方法。这里要格外注意的是,必须在用SuperType的实例替换原型之后,再定义这两个方法。从前面的原型链关系图我们知道SuperType的实例调用的是SuperType的原型里的getSuperValue(),SubType的实例一开始是用指针调用SuperType的原型里的getSuperValue(),后来就通过重写在自己的SubType里添加了自己的getSuperValue()。还有一点要注意到是,在通过原型链实现了继承之后,不能使用对象字面量法重写原型方法,因为这样就重写了原型链,继承就断了。例子:function SuperType(){ this.property = true;}SuperType.prototype.getSuperValue = function(){ return this.property;};function SubType(){ this.subproperty = false;}//继承了SuperTypeSubType.prototype = new SuperType();//使用字面量添加新方法,会导致上一行代码失效SubType.prototype = { getSubValue : function(){ return this.subproperty; }. someOtherMethod : function(){ return this.false; }}var instance = new SubType();console.log(instance.getSuperValue()); //error! 继承关系已断以上代码展示了刚把SuperType的实例赋给Subtype的原型,下面的代码马上就把SubType的原型给重写了,相当于上面的那行赋值被替换了,无效了。相当于现在的原型里包含的是Object的实例(所有函数的默认原型都是Object的实例),而不是SuperType的实例。原型链被切断,SubType和SuperType之间已经没有关系了。4.原型链的问题通过原型链的继承还是存在问题的,就是包含引用类型的那个问题。包含引用类型值的原型属性会被所有实例共享;当其中一个实例修改了原型属性中的引用类型值,整个引用类型值就被修改了。例子:function SuperType(){ this.colors = ["red","blue","green"];}function SubType(){}SubType.prototype = new SuperType();var instance1 = new SubType();instance1.colors.push("black");console.log(instance1.colors); //["red", "blue", "green", "black"]var instance2 = new SubType();console.log(instance2.colors); //["red", "blue", "green", "black"]还是引用类型值共享会导致修改类型值会直接修改原型属性里的值的问题。原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数,实际上,应该说是没有办法在不影响所有对象的情况下,给超类型的构造函数传递参数(意思是说不能在SubType.prototype = new SuperType() 给构造函数赋值吗?)。由于前面的两个问题,很少会单独使用原型链。6.3.2借用构造函数为了解决前面的问题,开始人员开始使用一种叫借用构造函数(constructor stealing)的技术(有时候也叫做伪造对象或经典继承)。这种技术的思想相当简单:在子类型构造函数的内部调用超类型的构造函数。别忘了,函数只不过是在特定环境中执行代码的对象。因此,通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。例子:function SuperType(name){ //另一个好处是可以给构造函数赋值 this.colors = ["red","blue","green"]; this.name = name;}function SubType(){ //继承了SuperType ,同时传递了参数 SuperType.call(this,"Nicholas"); //不知道这个this有什么用,this不是指向window吗,可能“将来”实例化变成对象之后就指向SubType对象了 this.age = 29}var instance1 = new SubType(); //前面说的将来就是指这个时候,调用了内部的SuperType构造函数instance1.colors.push("black");console.log(instance1.colors); //["red", "blue", "green", "black"]var instance2 = new SubType();console.log(instance2.colors); //["red", "blue", "green"]console.log(instance1.name) //"Nicholas"console.log(instance1.age) //29代码中加粗那行代码“借调”了超类型的构造函数。通过使用call()方法,我们实际上是在(未来将要)新创建的SubType实例的环境(this指向的是SubType实例的环境 )下调用SuperType构造函数(不一定有new才能调用构造函数)。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果SubType的每个实例都会有自己的colors属性的副本了。但是这种方式并没有使用到SubType.prototype哦。1.传递参数传递参数的例子被我写在上面一起讲了。代码中SuperType接收一个参数name,并在函数内直接给name属性赋值。在SubType构造函数内部调用SuperType构造函数时,实际上是为SubType的实例设置了name的默认属性,虽然这个默认属性不在prototype上。为了确保SuperType构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。2.借用构造函数的问题如果只使用借用构造函数,那么也将出现构造函数模式出现的问题——方法都在构造函数中,每实例一个对象就重新复制了一份函数中的方法,影响性能。因此函数的复用就无从谈起。而且,在超类型中定义的方法,对子类型而言是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数技术也很少使用。6.3.3组合继承组合继承(combination inheritance)有时候也叫伪经典继承。指的是将原型链和借用构造函数的技术组合到一块。背后的思路是:是哦那个原型链实现对原型属性和方法的继承、而通过借用构造函数来实现对实例的继承。例子:function SuperType(name){ this.colors = ["red","blue","green"]; //属性写进构造函数里 this.name = name;}SuperType.prototype.sayName = function(){ console.log(this.name);};function SubType(name){ //继承属性 SuperType.call(this,name); ] this.age = age;}//继承方法SubType.prototype = new SuperType();SubType.prototype.constructor = SubType; //如果没有这一步,SubType的原型里concerned属性就是指向SuperTypeSubType.prototype.sayAge = function(){ //方法写进原型里 console.log(this.age);}var instance1 = new SubType("Nicholas",29); instance1.colors.push("black");console.log(instance1.colors); //["red", "blue", "green", "black"]instance1.sayName(){}; //"Nicholas"instance1.sayAge(){}; //29var instance2 = new SubType("Greg",27);console.log(instance2.colors); //["red", "blue", "green"]instance2.sayName(); //"Greg"instance2.sayAge(); //27在例子中,SuperType构造函数定义了两个属性:name和colors。SuperType的原型定义了一个方法sayName()。SubType构造函数在调用SuperType构造函数时传入了name参数,紧接着又定义了自己的属性age。然后,将SuperType的实例赋值给SubType的原型,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让两个不同的SubType实例既分别拥有自己的属性——包括colors属性,又可以使用相同的方法了。组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中 n继承模式。而且instanceOf和isPrototypeOf()也能够用于识别基于组合继承创建的对象。6.3.4原型式继承一个有缺陷的继承方式,但是ECMAScript5为这个方式创建了一个create()方法,跟原型式继承原理相似。这种方法没有使用严格意义上的构造函数。道格拉斯·克罗克福德(提出者)的想法是:借助原型可以基于已有的对象创建新对象。同时还不必因此创建自定义类型(意思就是那个子构造函数(比如那个SubType构造函数)都不用创建了,下面那个方法的返回值直接赋给一个变量,那个变量就是实例了)。下面就是那个函数:function object(o){ function F(){} F.prototype = o; //这个构造函数的原型等于传进来的那个对象,所以不用创建自定义类型,这个F就相当于那个自定义类型了 return new F(); //返回一个构造函数的实例,赋给一个变量就相当于实现了继承}在Object()函数内部,先创建了一个临时性的构造函数(F),然后将闯入的对象作为这个构造函数的原型( F.prototype = o ),最后返回这个临时类型的新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。看下面例子:var person = { name:"Nicholas", friends:["Shelby","Court","Van"]};var anotherPerson = object(person); //调用object(),传进去的person相当于超类型anotherPerson.name = "Greg";anotherPerson.friends.push("Rob"); //注意这里给friends数组引用类型值推入一个数据的只是anotherPersonvar yetAnotherPerson = object(person);yetAnotherPerson.name = "Linda";yetAnotherPerson.friends.push("Barbie"); //注意这里第二个子实例yetAnotherPerson也给数组推入一个Barbieconsole.log(person.friends); //["Shelby", "Court", "Van", "Rob", "Barbie"] //超类型里的数组里也多了Rob和Barbie✎:反正直接在对象字面量里定义值为引用类型值的属性被改写就会GG(一开始打这句话的时候是写“直接在原型里定义值为引用类型值的属性就GG”,想想不对,上面的person是在自己那里定义的引用类型值属性不是在原型里啊。object()方法返回的是F()的实例,是F的原型(F.prototype)复制了person的属性哦,所以anotherPerson和yetAnotherPerson对象改的应该是F.prototype里的数组吧,为什么person也会受影响呢?想想,可能是:person对象作为函数参数传入了函数object(),虽然JavaScript是按值传递的,但person是对象,F.prototype = o 相当于F.prototype成了对象person的指针(相当于复制了对象的指针),所以看起来改的是F.prototype,但其实F.prototype里没有这个属性,本质上改的应该是person对象里的属性,况且F在object()函数执行完后已经被销毁,但anotherPerson和yetAnotherPerson 保存了F的实例,也就保存了F的prototype原型对象)。如果有一个超类型的对象在的话,我们就可以使用object( )函数,把超类型作为参数传入,然后函数就会返回一个新对象。在这个例子中,作为超类型的对象是person对象(注意对象首字母是小写,它的作用不是作为构造函数),把person放入object()传入函数,返回一个新对象,这个新对象将person作为原型(其实应该说 F 将person作为原型),所以它的原型中就包含一个基本类型值属性和一个引用类型值属性。这意味着person.friend不仅属于person所有,而且也会被anotherPerson和yetAnotherPerson共享。实际上,这就相当于又创建了person对象的两个副本(说副本还是有门道的,是副本不是指针,如果是指针的话,anotherPerson和yetAnotherPerson 修改name属性的话,person也会被改,但亲测并不会,虽然F.prototype是person的指针,但new F(),F的实例不是指针,算了解释不下去了,以前是SubType.portotype = new SuperType()好歹是别人的实例就一定不是指针,但 F.prototype = o 是指针无疑啊,为什么anotherPerson和yetAnotherPerson 会有自己的name属性呢?)。ECMAScript5通过新Object.create()方法规范了原型式继承。这个方法接收两个参数:一个用作新对象的父对象和一个为新对象定义额外的属性的对象(可选,意思就是用对象字面量的方法定义新对象的额外属性)。在传入一个参数的情况下,Object.create()与object()方法的行为相同。var person = { name:"Nicholas", friends:["Shelby","Court","Van"]};var anotherPerson = Object.create(person);anotherPerson.name = "Greg"; //后面的代码会教不用这样写anotherPerson.friends.push("Rob");var yetAnotherPerson = Object.create(person);yetAnotherPerson.name = "Linda";yetAnotherPerson.friends.push("Barbie");console.log(person.friends); //["Shelby", "Court", "Van", "Rob", "Barbie"] 仍然存在的问题Object.create()的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性:var anotherPerson = Object.create(person,{ name:{ value:"Greg" //好麻烦啊这样写 }})console.log(anotherPerson.name) //"Greg"果然用上面的参数格式,其他三个特性不写,全部会由默认true变成默认false:console.log(Object.getOwnPropertyDescriptor(anotherPerson,"name"))//Object {value: "Greg", writable: false, enumerable: false, configurable: false}console.log(Object.getOwnPropertyDescriptor(yetAnotherPerson,"name"))//Object {value: "Linda", writable: true, enumerable: true, configurable: true}默认的自定义对象的三个特性都是true,如果用Object.defineProperties()的格式修改描述符的定义,没有列出来,则默认会被后台修改为false。6.3.5寄生式继承寄生式(parasitic)继承也是克罗克福德提出来的,与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工程模式类似,既创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。Conflicting modification on 2017年2月15日 下午11:36:25:目录6.1理解对象6.1.1属性类型(对象有两种属性:数据属性和访问器属性) 1.数据属性([ [Configurable] ]、[ [Enumerable] ]、[ [Writable] ]、[ [Value] ] ;修改默认特性的方法) 2.访问器属性([ [Configurable] ] 、[ [Enumerable] ] 、[ [Get] ] 、[ [Set] ] )6.1.2定义多个属性(Object.defineProperties( )用对象字面量的方式传入第二个参数)6.1.3读取属性的特性(Object.getOwnPropertyDsecriptor()方法)6.2创建对象6.2.1工厂模式(没有解决对象识别问题)6.2.2构造函数模式(每多一个实例就添加很多重复的方法;实例对象的constructor属性;自定义函数是定义在Global对象中的)1.将构造函数当作函数2.构造函数的问题(每个实例上都重新创建了一次方法)6.2.3原型模式1.理解原型对象(prototype属性,prototype里的constructor属性,实例的[[prototype]]属性;isprototypeOf();getPrototypeOf();hasOwnProperty()判断是原型属性的值还是有重写)2.原型与in操作符()首先要知道的:ECMAScript中没有类这个概念。ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值,对象或者函数。”每个对象都是基于一个引用类型创建的,这个引用类型可以是第五章讨论的原生类型(Array,Object,Function等),也可以是开发人员定义的类型。6.1理解对象这节就是告诉我们定义对象的时候不要用Object的构造函数那种方法,直接用对象字面量创建对象更好。例子:var person = { name:"Nicholas", age:29, job:"Software Engineer", sayName:function(){ console.log(this.name); }};6.1.1属性类型ECMAScript的对象中有两种不同的属性:数据属性和访问器属性。ECMA-262第5版在定义只有内部才有的特性(attribute)时,描述了属性(property)的各种特征。ECMA-262定义特性是为了实现JavaScript引擎用的,所以在JavaScript中无法直接访问特性。为了表示特性是内部值,该规范把他们放在了两对方括号中。例如[[Enumerable]]([[特性]])1.数据属性数据属性包含一个数据值的位置。在这个位置可以读取和写入值(这两句还不懂)。数据属性有4个描述其行为的特性:·[ [Configurable] ]:表示能否通过delete删除属性,能否修改属性的特性,能否把属性从数据属性修改为访问器属性。直接在对象上定义的属性,[ [Configurable] ]的默认值一般为true。·[ [Enumerable] ]:表示能否通过for - in循环返回属性。直接在对象上定义的属性,[ [Enumerable] ] 的默认值一般为true。·[ [Writable] ]:表示该属性是否可写(属性值能不能修改)。直接在对象上定义的属性,[ [Writable ] ] 的默认值一般为true。·[ [Value] ]:包含这个属性的数据值,读取属性值的时候,就是在这个位置读。写入属性值的时候,新值就保存在这个位置。这个特性的默认值为undefined。例子:var person = { name:"Nicholas",};这里创建了一个名为name的属性,为它指定的值是“Nicholas”。也就是说,[[value]]特性将被设定为“Nicholas”,而对这个值的任何修改都将反映在这个位置。name属性的[Configurable] ],[ [Writable] ],[ [Enumerable] ]都默认为true。要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字、一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable、value中的其中一个或多个。例子:var person = {};Object.defineProperty(person,"name",{ writable:false, value:"Nicholas" //利用Object.defineProperty给属性name赋值,顺便设置了这个本来没有的属性});console.log(person.name); //"Nicholas"person.name = "Greg";console.log(person.name); //"Nicholas"这里例子创建了一个名为name的属性,设置name属性的[ [Writable] ]为不可写。如果尝试为它指定新值,在非严格模式下,赋值操作被忽略,在严格模式下,赋值操作将会导致抛出错误。类似的“非忽严错”的规则也适用于把[ [Configurable] ]设置为false的属性:var person = {};Object.defineProperty(person,"name",{ configurable:false, value:"Nicholas"});console.log(person.name); //"Nicholas"delete person.nameconsole.log(person.name); //"Nicholas"configurable被设置为false,则无法从对象中删除属性,若进行删除操作,则会“非忽严错”。注意:一旦把属性定义为不可配置的,就不能把它变回可配置的了(有意思)。此时,再调用Object.defineProperty()方法修改特性,就会抛出错误。注意:在调用Object.defineProperty()修改属性的特性时,如果不指定,则configurable、enumerable、writable的默认值就会变成false!属性就会自动变成不可配置,不能通过for - in循环返回属性。所以设置的时候最好四个都写上去。不过作者说,在多数情况下,没有必要利用到Object.defineProperty()。不过,理解这些概念对理解JavaScript对象非常有用。✎IE8是第一个实现Object.defineProperty()的浏览器版本,但是仍然存在限制,实现不完全。建议不要在IE8上使用这个方法。2.访问器属性需要使用getter和setter函数才能读写的属性。(但是这两个函数都不是必需的)。在读取访问器属性时,会调用getter函数,在写入访问器属性时,会调用setter函数并传入新值。访问器属性有4个特性:·[ [Configurable] ]:表示能否通过delete删除属性,能否修改属性的特性,能否把属性从数据属性修改为访问器属性。直接在对象上定义的属性,[ [Configurable] ]的默认值一般为true。·[ [Enumerable] ]:表示能否通过for - in循环返回属性。直接在对象上定义的属性,[ [Enumerable] ] 的默认值一般为true。·[ [Get] ]:在读取属性时调用的函数。默认值为undefined。·[ [Set] ]:在写入属性时调用的函数。默认值为undefined。访问器属性不能直接定义,必须使用Object.defineProperty()定义。例子:var book = { _year:2004, edition:1};Object.defineProperty(book,"year",{ get:function(){ return this._year; }, set:function(newValue){ if(newValue>2004){ this._year = newValue; this.edition += newValue-2004; } }});book.year = 2005;console.log(book.edition); // 2以上代码创建了一个book对象,并给它定义两个默认的属性:_year和edition。_year前面的下划线是一种常用的人为规定的记号(没有程序上的作用),用于表示只能通过对象方法访问的属性(亲测console.log(book._year)可以访问到,大概是因为有用到对象方法访问)。注意这个_year属性并不是访问器属性。访问器属性是year。year包含一个getter函数和setter函数。getter函数返回_year的值,setter函数通过计算来确定正确的版本。因此,把year属性修改为2005会导致_year变成2005(setter属性set的属性不仅是自己的,还可以是同一对象下的其他属性)。而edition变为2。这是使用访问器属性的常见方式,即设置一个属性的值会导致其他属性发生变化。支持ECMAScript5的这两个方法的浏览器有IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。在这个方法之前,要创建访问器,一般都使用两个非标准的方法:defineGetter()和defineSetter()。这两个方法最初都是由Firefox引入的,后来Safari 3、Chrome 1和Opera 9.5也给出了相同的实现。使用这两个遗留的方法,可以像下面这样重写前面的例子:var book = { _year:2004, edition:1};book.defineGetter("year",function(){ return this._year;});book.defineSetter("year",function(newValue){ if(newValue>2004){ this._year = newValue; this.edition += newValue-2004; }});book.year = 2005;console.log(book.edition);console.log(book.year); // 2在不支持Object.defineProperty()方法的浏览器中不能修改[ [Configurable] ]和[ [Enumerable] ]特性。6.1.2定义多个属性由于为对象定义多个属性的可能性很大,ECMAScript5又定义了一个Object.defineProperties()方法。利用这个方法可以一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加或修改属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。例子:var book1 = {};object.defineProperties(book,{ _year:{ writable:true, value:2004 }, edition:{ writable:true, value:1 }, year:{ get:function(){ return this._year; }, set:function(){ if(newValue > 2004){ if(newValue > 2004){ this._year = newValue; this.edition += newValue -2004; } } } }});支持Object.defineProperties() 方法的浏览器有:IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。 6.1.3读取属性的特性使用ECMAScript5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象。是访问器属性就有访问器属性的四个特性,是数据属性就有数据属性的四个特性。例如上面的book对象为例子:console.log(Object.getOwnPropertyDescriptor(book,"_year"));//返回:Object {value: 2004, writable: true, enumerable: true, configurable: true}console.log(Object.getOwnPropertyDescriptor(book,"year"));//返回:Object {enumerable: false, configurable: false} 很奇怪set和get特性要通过Object.getOwnPropertyDescriptor(book,"year").get和~.set去得到里面set和get里面的函数。在JavaScript中,可以针对任何对象——包括DOM和BOM对象,使用Object.getOwnPropertyDescriptor()方法。支持这个方法的浏览器有:IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。6.2创建对象为了不写重复代码讨论了很多创建对象的方法。6.2.1工厂模式工厂模式是软件工程领域广为人知的设计模式,这种模式抽象了创建具体对象的过程(把创建对象的过程抽象简化)。因为在ECMAScript中没有类,开发人员发明了一种函数,用函数来封装以特定接口创建对象的细节。例子:function createPerson(name,age,job){ var o = new Object(); //显式地创建对象 o.name = name; o.age = age; o.job = job; o.sayName = function(){ console.log(this.name) }; return 0; //有return语句,不知道有什么不好}var person = createPerson("Nicholas",29,"Software Engineer");工厂模式解决了创建多个相似对象要写重复代码的问题,但没有解决对象识别的问题(即怎样知道一个对象的类型,后面的由Person对象实例化的对象的类型就是Person类型而不是Object类型,解决了对象识别问题)。随着JavaScript的发展,又一个模式出现了。6.2.2构造函数模式前面几章说过,ECMAScript中的构造函数可以用来创建特定类型的对象(?说过?还不知道原来特定类型的对象可以自己定义,比如后面的Person对象)。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。例如,可以使用构造函数模式将前面的例子重写如下:function Person(name,age,job){ //构造函数首字母大写,普通函数首字母小写 this.name = name; this.age = age; this.job = job; this.sayName = function(){ console.log(this.name); };}var person1 = new Person("Nicholas",29,"Software Engineer");var person2 = new Person("Greg",27,"Doctor");console.log(person1.name);在这个例子中,Person()与createPerson()的不同之处在于:没有显式地调用对象,直接将属性和方法赋给了this对象,没有return语句。✎注意:遵照其他OO语言的惯例,构造函数始终都以大写字母开头,非构造函数用小写字母开头。目的是为了区别于ECMAScript中的其他函数。要创建Person对象的实例,必须使用new操作符。以这种方式调用构造函数会经历以下4个步骤:(1)创建一个新对象;(2)将构造函数的作用域赋给新对象(环境变量对象就变成了这个对象,因此this就指向了这个对象);(3)执行构造函数中的代码(为这个新对象添加属性);(4)返回新对象。(对象实例诞生!)在前面例子的最后,person1和person2分别保存着Perso的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person(亲测console.log(person1.constructor)打印出了整个构造函数Person的源代码)。例子:console.log(person1.constructor == Person); //trueconsole.log(person1.constructor) //打印出上面构造函数Person的源代码对象的constructor属性最初是用来标识对象类型的。但是想要检测对象类型,还是instanceOf操作符更可靠一些。例子中创建的所有对象既是Object的实例,也是Person的实例,这一点可以通过instanceOf()操作符验证:console.log(person1 instanceof Object); //trueconsole.log(person1 instanceof Person); //true创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式完胜工厂模式的地方。在这个例子中person1和person2之所以同时是Object的实例,是因为所有对象均继承自Object(详细内容稍后讨论)。✎:以这种方式定义的构造函数是定义在Global对象(在浏览器中是window对象)中的。(???又说所有对象继承自Object???)第八章会详细讨论浏览器对象模型(BOM)1将构造函数当作函数构造函数与其他函数的唯一区别,就在于调用他们的方式不同。但是归根到底构造函数还是函数。任何函数,只要通过new操作符调用,它就可以作为构造函数。而构造函数如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。例如,前面例子中的Person()函数,可以用下面的任一方式调用,例子的重点是,体会用构造函数的方式调用和用普通方式调用两种方法,他们的作用域有什么不同://当作构造函数调用var person1 = new Person("Nicholas",29,"Software Engineer");person1.sayName();//作为普通函数调用Person("Greg",27,"Doctor");window.sayName();//作为普通函数在另一个对象的作用域中调用var o = new Object();Person.call(o,"Kristen",25,"Nurse");o.sayName();第一种用法是构造函数的经典用法。第二种用普通的函数调用法:属性和方法都添加给了window对象(window.sayName()正确的过程是,方法内的this总是指向Global对象,在浏览器中就变成了window对象)。第三种是重点:使用call()在对象o的作用域中调用Person函数,类似在window对象中调用Person,调用后o也拥有 了所有属性和sayName()方法。2构造函数的问题还是有人挑出了构造函数的问题。构造函数的问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例,因为他们在不同的对象环境中创建。从逻辑上讲,此时的构造函数可以这样定义:function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = new Function("console.log(this.name)"); //与声明函数在逻辑上是等价的}从这个角度来看构造函数,就更容易明白每个Person实例都包含一个不同的Function实例(但他们的用途完全一样,就是显示name属性)(感觉学到了JavaScript不可言传的东西,这两个方法看起来一模一样,但是他们是两个不同的实例,要纠结他们是不同的实例的原因是,这样会产生不同的作用域链和标识符解析)。但创建Function新实例的机制仍然是相同的。因此,不同对象实例上的同名函数是不相等的,即person1的sayName()不等于person2的sayName()。以下代码可以证明这点:console.log(person1.sayName == person2.sayName); // false然而,创建两个完成同样任务的Function实例的确没有必要(导致产生不同的作用域链和标识符解析 );况且有this对象在,根本不用在执行代码前把函数绑定到每一个要实例的对象上,因此,可以像下面这种“等一下就要被作者推翻的方法”一样,把函数定义转移到构造函数外面来解决这个问题:function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName sayName; //与声明函数在逻辑上是等价的}function sayName(){ console.log(this.name);}不知道这样会不会产生不同的作用域链,但是这样的好处是Function不用实例两次吧,可能是Function的作用域链不会产生两个,但是实例对象的作用域链还是会产生的。亲测console.log(person1.sayName == person2.sayName)返回的是true。证明他们用到的是一样的在全局环境下的sayName()函数。但是这样又有一个问题,你在全局作用域定义的函数实际上只被某一个对象调用,如果对象需要很多个这样的方法,那不就要定义很多的全局函数吗。如此一来,我们这个自定义的引用类型就没有丝毫封装性可言了(太多函数暴漏在全局作用域中)。好在,我们可以用原型模式解决这个问题。6.2.3原型模式我们创建的每个函数都有一个prototype(原型)属性。这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的对象的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象(刚又说prototype是函数属性,现在又说是那个又构造函数实例出来的对象的原型对象?)使用原型对象的好处是可以让所有对象实例共享他们所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中(相当于对属性和方法设置一个默认值,在没有明确赋值的情况下,属性和方法的值就等于这个默认值)。例子:function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){ console.log(this.name);};var person1 = new Person();person1.sayName();在此,我们将所有属性和sayName()直接添加到Person的prototype属性中,构造函数变成了空函数。即便如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法,而且这些属性和方法是所有对象实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个sayName()函数。console.log(person1.sayName == person2.sayName)//true.要理解原型模式的工作原理,必须先理解ECMAcript中原型对象的性质。1.理解原型对象无论什么情况,只要创建了一个新函数,就会根据一组特定的规则(没有详讲)为该函数创建一个prototype属性(所有函数自带prototype属性)。这个属性指向函数的原型对象(前面又说prototype是原型对象现在又说是指向原型对象,大概因为prototype是个指针指向了原型对象,所有也可以说prototype属性是原型对象吧,有点函数名是函数的指针,所以也可以说这个函数是XX函数名,XX函数名是这个函数的意思)。在默认情况下,所有原型对象(暂且说是prototype,因为prototype指向原型对象)都会自动获得一个constructor(构造函数)属性(每个原型对象自带constructor属性),这个属性是一个指向prototype属性所在函数的指针。就拿前面的例子来说,Person.prototype.constructor指向Person这个函数,这个构造函数(也就是说一个函数里的prototype属性里的constructor属性是个指针,指向这个函数自己)。创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法和属性,则都是从Object继承而来。当调用构造函数创建一个实例后(注意,这里开始讲的是实例,不是原来那个构造函数了),该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262管这个指针叫[[Prototype]]。虽然在JavaScript中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个属性protp;而在其他浏览器中,[[Prototype]]则是完全不可见的。要明确的一点是,这个连接是在实例与构造函数的原型对象之间,而不是在实例与构造函数之间(即[[Prototype]]指向构造函数的prototype而不是指向构造函数)。以前面的Person构造函数和Person.prototype创建实例的代码为例,图6-1展示了各个对象之间的关系:上图展示了Person构造函数、Person的原型属性以及Person现有的两个实例之间的关系。在此Person.prototype指向了原型对象,而原型对象中的constructor属性又指向了Person.prototype。原型对象中除了包含constructor属性之外,还包括后来添加的其他属性。Person的每个实例——person1和peron2都包含一个内部属性[[Prototype]],该属性又指向了原型对象(书里这里写的指向Person.prototype,但应该是指向原型对象才对);换句话说,他们与构造函数没有直接关系(实例与构造函数没有直接关系!?)。此外,要格外注意的是,虽然这两个实例都不包括属性和方法,但我们却可以调用person1.sayName()。这是通过查找对象属性的过程来实现的。虽然没有标准的方式访问[[Prototype]](书里说所有实现(即浏览器)都无法访问到[[Prototype]],但前面已经说了有三个浏览器可以用proto访问,亲测也确实可以),但可以通过isPrototypeOf()方法来确定对象之间知否存在这种关系。从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的这个对象(Person.prototype),那么这个方法就返回true。例子:console.log(Person.prototype.isPrototypeOf(person1)); //trueconsole.log(Person.prototype.isPrototypeOf(person2)); //trueperson1,2的原型对象是Person.prototype,所以返回true,也说明person1,2的[[Prototype]]指向Person.prototype。记住[[Prototype]]是实例对象的内部属性,是一个指针。ECMAScript5还增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现(浏览器)中,这个方法返回[[Prototype]]的值。例子:console.log(Object.getPrototypeOf(person1)==Person.prototype); //true,相等,可以证明person1的[[Prototype]]是指向Person.prototype的console.log(Object.getPrototypeOf(person1).name); //"Nicholas" Object.getPrototypeOf()可以用来获取对象原型的属性第一行代码确定了Object.getPrototypeOf()返回的对象实际就是这个对象的原型(又跟我自己在注释里写的见解不一样)。第二行代码取得对象原型中name属性的值,即“Nicholas”。使用Object.getPrototypeOf()可以方便地取得一个对象的原型(prototype)。而这在利用原型实现继承的情况下是非常重要的。支持这个方法的浏览器有: IE9+、Firefox 3.5+、Safari 5+、Opera 12+和Chrome。知道了构造函数有prototype属性之后,我们可以知道,当代码读取某个对象的某个属性时,都会执行一次搜索,目的是找到具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性值;如果没有找到,就继续搜索指针([[Prototype]])指向的原型对象(prototype),在原型对象中查找具有给定名字的属性,如果在原型对象中找到这个属性,就返回这个属性的值。所以我们在调用实例对象的属性和方法时会执行两次搜索。这正是多个对象实例共享原型对象所保存的属性和方法的基本原理。✎:前面提到过,原型对象最初只包含一个constructor属性,这个属性也是共享的,可以通过对象实例访问。(亲测访问这个属性会返回构造函数的源代码,之前亲测过了?)虽然我们可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加一个与原型对象中存在的同名属性,则我们就在实例中创建该属性。该属性会屏蔽掉原型中那个属性。例子:function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){ console.log(this.name);};var person1 = new Person();var person2 = new Person();person1.name="Mike"person1.sayName(); //“Mike” ←来自实例person1.sayName(); //"Niicholas" ←来自原型当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性(书中对“屏蔽”两个字黑体加粗,但是我觉得屏蔽的原理估计又有一大篇文章要讲)。如果把实例中的属性设置为null,也只会在实例中设置这个属性,无法恢复其指向原型的连接。要恢复其指向原型的连接,方法就是使用delete操作符完全删除实例属性,从而我们就可以重新访问原型中的属性。例子:person1.name = "Mike";person1.name = null;person1.sayName(); //nulldelete person1.name person1.sayName(); //"Nicholas"使用hasOwnProperty()(注意是Property不是prototype了,property是属性的意思)方法可以检测一个属性是存在实例中,还是原型中。这个方法(不要忘了这个方法继承自Object)在检测的属性是存在实例中的时候,返回true。例子:var person1 = new Person();person1.name="Mike"delete person1.name;console.log(person1.hasOwnProperty("name")); //false,把第三行注释掉就变成true通过hasOwnProperty()方法,我们访问属性的时候到底访问的是原型对象中的属性还是实例重新定义的属性就一清二楚了。(做了有趣的实验,把实例中的属性保存为跟原型对象属性一样的值,用hasOwnProperty()方法返回的是true。可不可以说明他们存放的空间不一样?)person1.name="Nicholas" 与原型对象中的属性值相同console.log(person1.hasOwnProperty("name")); //true✎:ECMAScript5的Object.getOwnPropertyDescriptor()方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescroptor()方法。原来前面已经讲过这个方法竟然忘了。不知道什么是属性的描述符,测试了一次发现返回的是对象的属性的四个特性:console.log(Object.getOwnPropertyDescriptor(person1,"name"));//Object {value: "Nicholas", writable: true, enumerable: true, configurable: true}注意上面的那句话啊!!!每个词都是关键啊!上面说要在原型对象上调用,不是直接用构造函数名调用啊,用构造函数名调用很奇怪,console.log(Object.getOwnPropertyDescriptor(person1,"name"));返回的描述符中的value是“Person”,第二个参数改成“job”或“age”返回的却是undefined。用console.log(Object.getOwnPropertyDescriptor(Person.prototype,"name"));就能返回value是“Nicholas”的描述符。例子:console.log(Object.getOwnPropertyDescriptor(Person,"job")); //undefinedconsole.log(Object.getOwnPropertyDescriptor(Person,"name")); //Object {value: "Person", writable: false, enumerable: false, configurable: true}console.log(Object.getOwnPropertyDescriptor(Person.prototype,"name"));//Object {value: "Nicholas", writable: true, enumerable: true, configurable: true}后来又继续测试console.log(Person.name);返回的也是“Person”,难道name是每个函数都有的属性返回函数的函数名?2.原型与 in 操作符in操作符的使用有两种方式,一种是单独使用和在for - in循环中使用。在单独使用时,in操作符用于判断某一属性是否在对象中,无论是在实例中还是原型中,只要有就返回true。例子:console.log("name" in person1) //true 无论name是在实例中还是原型中console.log("name" in Person) //顺便做了个实验,name属性确实在Person构造函数中console.log("name" in Person) //false这让我想到一个问题,Person已经有一个name属性的值是“Person”,我们又通过原型对象给原型对象中添加了name属性的值是Nicholas,那值为“Person”的name属性又是来自哪里??做了个实验,创建一个叫zoo的空函数:function zoo(){}console.log("name" in zoo); //truein操作符判断后也是返回true,这个name不在原型对象中,也不在实例中,仍然返回true,这个name到底是放在哪?有意思。又做了好多实验,发现用函数声明,函数表达式创建出来的函数都有name属性,但是把这个自定义的函数当构造函数用,实例出来的对象就没有这个name属性。所以还是只有函数有咯。用Obect()构造函数实例的对象也没有name属性。利用 in操作符,结合hasOwnProperty()方法,我们可以创建一个函数来判断一个属性值到底是来自实例还是原型:function hasPrototypeProperty(object,name){ return !object.hasOwnProperty(name)&&(name in object);}这个是书里定义的函数,我觉得有点绕。这个是检测属性是原型属性返回true。是实例中的属性就返回假,跟我想的相反。例子:function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){ console.log(this.name);};var person = new Person();console.log(hasPrototypePerty(person,"name")) //trueperson.name = "Greg";console.log(hasPrototypePerty(person,"name")) //falsefor - in循环的作用,返回的是所有能通过对象访问的,可枚举的(enumerated)属性,其中既包括实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性([[Enumerable]]:false)的实例属性也会在for - in属性中返回(意思是:比如toString()方法是Object的原生方法,一般这类方法都是不可枚举的,如果你在实例对象中重写了toString(),那toString()就变成了可枚举的,会在for - in循环中被列出来)。因为根据规定,所有开发人员定义的属性都是可枚举的。但是IE8及更早版本会有BUG,即屏蔽不可枚举属性的实例属性也不会出现在for - in循环中。例如:var o = { toString : function(){ //重写toString()方法 return "My Object"; }};for(var prop in o){ if(prop == "toString"){ console.log("Found String"); //IE8中不会显示 }}上面的例子,本来我已经改写了toString()了,应该是可以被for - in循环枚举出来的,但是在IE8及更早版本并不会被枚举出来。这个BUG会影响默认不可枚举的所有属性和方法,包括:hasOwnProperty()、propertyIsEnumerable()、toLocalString()、toString()和valueOf()。ECMAScript5把constructor和property属性的[[Enumerable]]特性设置为false,但并不是所有浏览器都乖乖听ECMAScript的。要想获取对象上所有可枚举的实例属性(实例属性!原型对象中的属性不会被列举出来),可以使用ECMAScript5的Object.keys()方法,这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。例子:function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){ console.log(this.name);};var keys = Object.keys(Person.prototype);console.log(keys) //["name", "age", "job", "sayName"]var peron1 = new Person();person1.name = "Mike";console.log(Object.keys(person1)) //["name"]有什么实例,数组里才有什么属性,property里定义的属性是Person.prototype对象的实例属性,不是person1的实例属性,person1的实例属性只有一个。如果想要得到所有实例属性,无论是否可枚举,可以使用Object.getOwnPropertyNames()方法。例子:console.log(Object.getOwnPropertyNames(Person.prototype)); //["constructor", "name", "age", "job", "sayName"]person1.age=28;Object.defineProperty(person1,"name",{ writable:false, value:"Mike", enumerable:false}); console.log(Object.keys(person1)); // ["age"]console.log(Object.getOwnPropertyNames(person1)); // ["name", "age"]还记得前面说的ECMAScript5把constructor(构造函数)属性也设置为不可枚举吗?看来这个浏览器遵循ECMAScript5的规定。Object.keys()和Object.getOwnPropertyNames()都可以用来替代for - in循环。支持这两个方法的浏览器有:IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。记住他们返回的都是实例属性,不包括原型对象中的属性。3.更简单的原型语法作者介绍给我们,前面的每添加一个原型属性就要敲一遍Person.prototype的方法太傻了,为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。例子:function Person(){}Person.prototype = { constructor : Person, name : "Nicholas", age :29, job : "Software Engineer", sayName : function(){ console.log(this.name); }}理清除,Person.property是指针指向原型对象,所以用对象字面量法给原型对象添加属性和方法是没毛病的。但是这种写法本质上重写了Person.prototype对象,而每个prototype属性都自带有constructor属性,如果在重写中没有加入constructor属性,为constructor重新赋值,则constructor属性将从原型对象中消失。说消失是不准确的,这时候原型对象的constructor属性不再指向Person构造函数,而是指向Object构造函数。但是Person对象的实例仍然是Person的实例。下面的例子假设重写时没有加入constructor属性:console.log(friend instanceof Object); //trueconsole.log(friend instanceof Person); //trueconsole.log(friend.constructor == Person); //falseconsole.log(friend.constructor == Object); //true还有一点要注意的是,重写之后的constructor属性,它的[[Enumerable]]特性会被设置为true(当然是在ECMAScript5已经规定了constructor属性的[[Enumerable]]特性 默认为false且浏览器听它的的情况下)。如果想让它重新变为false,可以人工用Object.defineProperty()方法设置。4.原型的动态性由于在原型中查找值是一次搜索,所以即使先创建实例后修改原型也是可以的。例子:var friend = new Person();Person.prototype.sayHi = function(){ console.log("Hi");};friend.sayHi(); //"Hi" (正常运行)可以看到我们先实例化了Person对象为friend,再修改Person的原型对象。firend仍然能访问到sayHi()方法。其原因可以归结为实例与原型之间的松散连接关系。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的sayHi属性并返回保存在那里的函数。尽管可以随时为原型添加属性和方法,但如果是重写整个原型对象,情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototyoe]]指针,而把原型重写为另外一个对象就等于切断了构造函数与最初原型之间的联系(从书中关系图来看,最初原型(旧原型对象)仍指向构造函数,但是构造函数指向了新的原型对象,所以也不是完全切断吧)。作者写到这里突然要我们记住:实例中的指针仅指向原型,不指向构造函数(实例与构造函数之间没有联系)例子:function Person(){}var friend = new Person();Person.prototype = { constructor : Person, name : "Nicholas", age :29, job : "Software Engineer", sayName : function(){ console.log(this.name); }};friend.sayName(); //报错: friend.sayName is not a function在这个例子中,我们先创建了一个Person对象,先实例化出一个friend,再重写了原型对象。然后在调用friend.sayName()的时候就出现了错误。因为friend指向的原型是旧原型,不包含以该名字命名的属性。图6-3展示了这个过程。可以看到重写原型对象会切断现有原型与任何之前已经存在的对象实例之间的关系;之前已经存在的对象实例 仍然引用的是最初的原型。也就是说用重写原型的方式给原型对象添加属性的方法要在重写的那段代码之后再实例化对象,如果是用Person.prototype.属性 = XX这种方法则不存在这种问题。5.原生对象的原型这节讲的是原生对象的原型,什么是原生对象?就是原生的引用类型(Array,Object,String and so on)所有原生引用类型都在其构造函数的原型上(注意原型是谁的属性(prototype是个属性,这个属性是个指针),是构造函数的属性)定义了方法。例如Array类型的sort()方法就是定义在Array.prototype中的。String类型的substring()方法就是定义在String.prototype中的。证明例子:console.log(typeof Array.prototype.sort); //functionconsole.log(typeof String.prototype.substring); //functionconsole.log(typeof Array.sort); //undefinedtypeof Array.sort竟然是undefined是令我惊讶的,我以为后台会自动规定sort是Array的方法(所以以前的每次array.sort( )的调用都是在调用Array对象的原型对象中的方法,向Array.prototype致敬!)通过原生对象的原型,不仅可以取得所有默认方法的引用,而且可以定义新方法,可以像修改自定义对象的原型一样修改原生对象的原型,在里面添加新的方法。亲测可以改写原本已经定义的方法,比如toString()这些。但是不要这样做。书中给出了在原生对象的原型中添加方法的例子,但是他建议不哟这样做,避免命名冲突。就不举例了。6.原型对象的问题原型对象也是有问题的(不能说BUG,合情合理)。原型对象的好处是可以共享属性,但最大的问题也处在了共享的问题上,属性的值是基本类型值还好,如果是包含引用类型的属性,就会出问题了:function Car(){}Car.prototype={ constructor:Car, friends:["Shelby","Court"]}var car1 = new Car();var car2 = new Car();car1.friends.push("Van");console.log(car1.friends); //["Shelby", "Court", "Van"]console.log(car2.friends); //["Shelby", "Court", "Van"]本来只是像给实例对象car1的friends属性中的数组加入“Van”而已,但是最后car2也有了。因为car1并没有自己重写friends属性,而是直接在默认有的属性值中添加数据,而这个数组是保存在Person.prototype中的,所以刚刚提到的修改也会通过person2.friends反映出来。所以很少有人单独使用原型模式。6.2.4组合使用构造函数模式和原型模式创建自定义类型的最常见方式,是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。这样,每个实例都有自己的一份实例属性的副本(不懂这个实例属性的副本是什么意思),但同时共享对方法的引用,最大限度地节省了内存。重写前面的代码的例子:function Person(name,age,job){ //通过对构造函数传参给独特的属性赋值 this.name = name; this.age = age; this.job = job; this.friends = ["Shelby","Court"]; //引用类型写在构造函数中}Person.prototype = { constructor:Person, //别忘了重写constructor属性 sayName:function(){ //一样的方法可以共用 console.log(this.name); }}var person3 = new Person("Nicholas",29,"Software Engineer");var person4 = new Person("Greg",27,"Doctor");person3.friends.push("Van");console.log(person3.friends); //["Shelby", "Court", "Van"]console.log(person4.friends); //["Shelby", "Court"]console.log(person3.friends == person4.friends); //falseconsole.log(person3.sayName == person4.sayName); //true这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。后面几节讲的都是定义引用类型的几种方法,既然说上面的是最好用的,后面的就不写了。6.2.5动态原型模式为看不惯上面的写法的有其他OO语言开发人员使用的方法。6.2.6寄生构造函数模式作者不推荐这种方法,因为这个方法返回的对象与构造函数或者与构造函数的原型属性之间没有关系。也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。也就是说假设用创建了一个Person构造函数,用这个构造函数实例出来的对象并不属于Person,而是属于Object。6.2.7稳妥构造函数模式这个方法也是创建的对象与构造函数之间没有什么关系。6.3继承许多OO语言都支持两个继承方式:接口继承和实现继承。接口继承只继承方法签名。而实现继承则继承实际的方法。如前面章节讲过的,由于函数没有签名(函数没有“函数名”只是一个指针,所以叫没有签名?),在ECMAScript中无法实现接口继承。实现继承主要是依靠原型链实现的。6.3.1原型链ECMAScript中描述了原型链的概念。并将原型链作为实现继承的主要方法。其基本思想是:利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型链实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针([[Prototype]])。那么,如果我们让原型对象等于另一个类型的实例,会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针。相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是所谓原型链的基本概念。实现原型链的基本模式如下:function SuperType(){ this.property = true; //定义的变量名有点复杂,看得有点晕,其实就是一个值为true的变量}SuperType.prototype.getSuperValue = function(){ return this.property;};function SubType(){ this.subproperty = false; //另一个false的变量}//继承了SuperTypeSubType.prototype = new SuperType(); //让一个构造函数A的原型等于另一个构造函数B的实例,这个实例又有一个指向B的原型的指针SubType.prototype.getSubValue = function(){ //这里看得有点懵,其实就是给子对象的原型定义了一个方法,跟上面的代码无关,也没用上 return this.subproperty;}var instance = new SubType(); //创建子对象的实例console.log(instance.getSuperValue()); //true 发现子对象的实例可以调用父对象的方法,返回保存在父对象的变量以上代码定义了两个类型:SuperType和SubType。每个类型各有一个属性一个方法。他们的区别是SubType继承了SuperType。继承的步骤是:通过创建SuperType的实例(new SupperType),并将该实例赋给了SubType.prototype实现。实现的本质是重写原型对象,给原型对象赋值为一个新类型的实例(所以SubType.prototype里的constructor属性也改变了,不是没有了,变成了新类型实例里指向父构造函数的constructor属性,此时constructor为SuperType)。换句话说,原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。在确定了继承关系后,我们又不知道有什么卵用地给SubType.prototype添加了一个方法。这样就在继承了SuperType的实例的属性和方法后,又有了自己的方法。这个例子中的实例以及构造函数和原型之间的关系如图:在代码中,我们没有使用SubType默认提供的原型,而是把它重写成了一个新原型,这个新原型就是SuperType的实例。于是,新原型不仅具有作为一个SuperType的实例所拥有的全部属性和方法,而且内部还有一个指针([[Prototype]],通了,一开始觉得叫[[Prototype]] 不恰当,想明白了根本不会!因为这个[[Prototype]]属性本来就是SuperType实例中有的),指向SuperType的原型。结果就是这样:instance的[[Prototype]]指向SubType的原型,SubType的原型里的[[Prototype]]又指向SuperType的原型。注意:getSuperValue()方法仍然还在SuperType.prototype里,SubType.prototype里没有,但property则在SubType.prototype中。书中的解释是:因为property是一个实例属性,而getSuperValue()是一个原型方法(实例属性会跟着继承在子对象中,原型方法仍然留在父对象的原型中)。解释:既然SubType.prototype现在是SuperType的实例,那么property当然位于实例中了,而getSuperValue()是在原型中,不在实例中。此外,要注意instance.constructor现在指向的是SuperType,这是因为原来Subtype的原型指向了另一个对象SuperType 的实例,这个实例指向了对象SuperType的原型,SuperType的原型里的constructor属性就是指向SuperType。(书里这样写,可是为什么instance会有constructor属性?constructor不是原型对象才有吗?亲测每个实例都有这个属性,返回这个实例的构造函数。字符串类型的constructor返回到是String()构造函数)通过原型链的介绍,原型搜索机制又要重新扩展:当以读取模式访问一个实例属性时,首先会在实例中搜索该属性,如果没有找到该属性,就继续搜索实例的原型,在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用instance.getSuperValue()会经历三个搜索步骤:1)搜索实例;2)搜索SubType.prototype;3)搜索SuperType.prototype,最后一步找到这个方法。搜索过程总是这样一环一环地走到原型链末端才停下来。1.别忘记默认的原型事实上,前面例子中展示的原型链少了一环。我们知道,所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。大家要记住,所有函数的默认原型都是Object的实例(new Object( )),因此默认原型都会包含一个内部指针,指向Object.prototype(又不说这个指针的名字)。这也正是所有自定义类型都会继承toString()等默认方法的原因(上面讲原型对象的时候说过,原生引用类型的方法都是定义在prototype上的)。所以所有函数的toString()等默认方法不是在他们自己的prototype属性中,而是通过指针,一层层地找到那些放在Object.prototype里的原型方法的,这样就减少了还能多内存占用吧。所以上面例子展示的原型链中还应包括另外一个继承层次,如图才是真正完整的原型链:2.确定原型和实例的关系可以通过两种方式来确定原型和实例之间的管理,第一种方式使用instanceOf操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。例子:console.log(instance instanceof Object) //trueconsole.log(instance instanceof SubType) //trueconsole.log(instance instanceof SuperType) //true由于原型链的关系,我们可以说instance是Object、SuperType、SubType中任何一个类型的实例。因此三个构造函数的结果都返回true。第二种方式是使用isPrototypeOf()方法,同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()也会返回true:console.log(Object.prototype.isPrototypeOf(instance)); //trueconsole.log(SubType.prototype.isPrototypeOf(instance)); //trueconsole.log(SuperType.prototype.isPrototypeOf(instance)); //true3.谨慎地定义方法子类型有时候会需要覆盖超类型中的某个方法,或者添加超类型中不存在的某个方法。但无论如何,给原型添加方法的代码一定要放在替换原型的语句之后。例子:function SuperType(){ this.property = true;}SuperType.prototype.getSuperValue = function(){ return this.property;};function SubType(){ this.subproperty = false;}//继承了SuperTypeSubType.prototype = new SuperType(); //无论是覆盖还是添加,都要放在替换原型的语句之后//添加新方法SubType.prototype.getSubValue = function(){ return this.subproperty;}//重写超类型中的方法SubType.prototype.getSuperValue = function(){ return false;}var instance = new SubType();console.log(instance.getSuperValue()); //false在上面代码中,加粗部分是两个方法的定义。第一个方法getSubValue()被添加到了SubType中,第二个方法getSuperValue()是原型链中已经存在的一个方法,但重写这个方法将会屏蔽原来的那个方法。换句花说,当通过SubType的实例(instance)调用getSuperValue()时,调用的是这个重新定义的方法;但通过SuperType的实例调用getSuperValue()时,还会继续调用原来的那个方法。这里要格外注意的是,必须在用SuperType的实例替换原型之后,再定义这两个方法。从前面的原型链关系图我们知道SuperType的实例调用的是SuperType的原型里的getSuperValue(),SubType的实例一开始是用指针调用SuperType的原型里的getSuperValue(),后来就通过重写在自己的SubType里添加了自己的getSuperValue()。还有一点要注意到是,在通过原型链实现了继承之后,不能使用对象字面量法重写原型方法,因为这样就重写了原型链,继承就断了。例子:function SuperType(){ this.property = true;}SuperType.prototype.getSuperValue = function(){ return this.property;};function SubType(){ this.subproperty = false;}//继承了SuperTypeSubType.prototype = new SuperType();//使用字面量添加新方法,会导致上一行代码失效SubType.prototype = { getSubValue : function(){ return this.subproperty; }. someOtherMethod : function(){ return this.false; }}var instance = new SubType();console.log(instance.getSuperValue()); //error! 继承关系已断以上代码展示了刚把SuperType的实例赋给Subtype的原型,下面的代码马上就把SubType的原型给重写了,相当于上面的那行赋值被替换了,无效了。相当于现在的原型里包含的是Object的实例(所有函数的默认原型都是Object的实例),而不是SuperType的实例。原型链被切断,SubType和SuperType之间已经没有关系了。4.原型链的问题通过原型链的继承还是存在问题的,就是包含引用类型的那个问题。包含引用类型值的原型属性会被所有实例共享;当其中一个实例修改了原型属性中的引用类型值,整个引用类型值就被修改了。例子:function SuperType(){ this.colors = ["red","blue","green"];}function SubType(){}SubType.prototype = new SuperType();var instance1 = new SubType();instance1.colors.push("black");console.log(instance1.colors); //["red", "blue", "green", "black"]var instance2 = new SubType();console.log(instance2.colors); //["red", "blue", "green", "black"]还是引用类型值共享会导致修改类型值会直接修改原型属性里的值的问题。原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数,实际上,应该说是没有办法在不影响所有对象的情况下,给超类型的构造函数传递参数(意思是说不能在SubType.prototype = new SuperType() 给构造函数赋值吗?)。由于前面的两个问题,很少会单独使用原型链。6.3.2借用构造函数为了解决前面的问题,开始人员开始使用一种叫借用构造函数(constructor stealing)的技术(有时候也叫做伪造对象或经典继承)。这种技术的思想相当简单:在子类型构造函数的内部调用超类型的构造函数。别忘了,函数只不过是在特定环境中执行代码的对象。因此,通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。例子:function SuperType(name){ //另一个好处是可以给构造函数赋值 this.colors = ["red","blue","green"]; this.name = name;}function SubType(){ //继承了SuperType ,同时传递了参数 SuperType.call(this,"Nicholas"); //不知道这个this有什么用,this不是指向window吗,可能“将来”实例化变成对象之后就指向SubType对象了 this.age = 29}var instance1 = new SubType(); //前面说的将来就是指这个时候,调用了内部的SuperType构造函数instance1.colors.push("black");console.log(instance1.colors); //["red", "blue", "green", "black"]var instance2 = new SubType();console.log(instance2.colors); //["red", "blue", "green"]console.log(instance1.name) //"Nicholas"console.log(instance1.age) //29代码中加粗那行代码“借调”了超类型的构造函数。通过使用call()方法,我们实际上是在(未来将要)新创建的SubType实例的环境(this指向的是SubType实例的环境 )下调用SuperType构造函数(不一定有new才能调用构造函数)。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果SubType的每个实例都会有自己的colors属性的副本了。但是这种方式并没有使用到SubType.prototype哦。1.传递参数传递参数的例子被我写在上面一起讲了。代码中SuperType接收一个参数name,并在函数内直接给name属性赋值。在SubType构造函数内部调用SuperType构造函数时,实际上是为SubType的实例设置了name的默认属性,虽然这个默认属性不在prototype上。为了确保SuperType构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。2.借用构造函数的问题如果只使用借用构造函数,那么也将出现构造函数模式出现的问题——方法都在构造函数中,每实例一个对象就重新复制了一份函数中的方法,影响性能。因此函数的复用就无从谈起。而且,在超类型中定义的方法,对子类型而言是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数技术也很少使用。6.3.3组合继承组合继承(combination inheritance)有时候也叫伪经典继承。指的是将原型链和借用构造函数的技术组合到一块。背后的思路是:是哦那个原型链实现对原型属性和方法的继承、而通过借用构造函数来实现对实例的继承。例子:function SuperType(name){ this.colors = ["red","blue","green"]; //属性写进构造函数里 this.name = name;}SuperType.prototype.sayName = function(){ console.log(this.name);};function SubType(name){ //继承属性 SuperType.call(this,name); ] this.age = age;}//继承方法SubType.prototype = new SuperType();SubType.prototype.constructor = SubType; //如果没有这一步,SubType的原型里concerned属性就是指向SuperTypeSubType.prototype.sayAge = function(){ //方法写进原型里 console.log(this.age);}var instance1 = new SubType("Nicholas",29); instance1.colors.push("black");console.log(instance1.colors); //["red", "blue", "green", "black"]instance1.sayName(){}; //"Nicholas"instance1.sayAge(){}; //29var instance2 = new SubType("Greg",27);console.log(instance2.colors); //["red", "blue", "green"]instance2.sayName(); //"Greg"instance2.sayAge(); //27在例子中,SuperType构造函数定义了两个属性:name和colors。SuperType的原型定义了一个方法sayName()。SubType构造函数在调用SuperType构造函数时传入了name参数,紧接着又定义了自己的属性age。然后,将SuperType的实例赋值给SubType的原型,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让两个不同的SubType实例既分别拥有自己的属性——包括colors属性,又可以使用相同的方法了。组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中 n继承模式。而且instanceOf和isPrototypeOf()也能够用于识别基于组合继承创建的对象。6.3.4原型式继承一个有缺陷的继承方式,但是ECMAScript5为这个方式创建了一个create()方法,跟原型式继承原理相似。这种方法没有使用严格意义上的构造函数。道格拉斯·克罗克福德(提出者)的想法是:借助原型可以基于已有的对象创建新对象。同时还不必因此创建自定义类型(意思就是那个子构造函数(比如那个SubType构造函数)都不用创建了,下面那个方法的返回值直接赋给一个变量,那个变量就是实例了)。下面就是那个函数:function object(o){ function F(){} F.prototype = o; //这个构造函数的原型等于传进来的那个对象,所以不用创建自定义类型,这个F就相当于那个自定义类型了 return new F(); //返回一个构造函数的实例,赋给一个变量就相当于实现了继承}在Object()函数内部,先创建了一个临时性的构造函数(F),然后将闯入的对象作为这个构造函数的原型( F.prototype = o ),最后返回这个临时类型的新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。看下面例子:var person = { name:"Nicholas", friends:["Shelby","Court","Van"]};var anotherPerson = object(person); //调用object(),传进去的person相当于超类型anotherPerson.name = "Greg";anotherPerson.friends.push("Rob"); //注意这里给friends数组引用类型值推入一个数据的只是anotherPersonvar yetAnotherPerson = object(person);yetAnotherPerson.name = "Linda";yetAnotherPerson.friends.push("Barbie"); //注意这里第二个子实例yetAnotherPerson也给数组推入一个Barbieconsole.log(person.friends); //["Shelby", "Court", "Van", "Rob", "Barbie"] //超类型里的数组里也多了Rob和Barbie✎:反正直接在对象字面量里定义值为引用类型值的属性被改写就会GG(一开始打这句话的时候是写“直接在原型里定义值为引用类型值的属性就GG”,想想不对,上面的person是在自己那里定义的引用类型值属性不是在原型里啊。object()方法返回的是F()的实例,是F的原型(F.prototype)复制了person的属性哦,所以anotherPerson和yetAnotherPerson对象改的应该是F.prototype里的数组吧,为什么person也会受影响呢?想想,可能是:person对象作为函数参数传入了函数object(),虽然JavaScript是按值传递的,但person是对象,F.prototype = o 相当于F.prototype成了对象person的指针(相当于复制了对象的指针),所以看起来改的是F.prototype,但其实F.prototype里没有这个属性,本质上改的应该是person对象里的属性,况且F在object()函数执行完后已经被销毁,但anotherPerson和yetAnotherPerson 保存了F的实例,也就保存了F的prototype原型对象)。如果有一个超类型的对象在的话,我们就可以使用object( )函数,把超类型作为参数传入,然后函数就会返回一个新对象。在这个例子中,作为超类型的对象是person对象(注意对象首字母是小写,它的作用不是作为构造函数),把person放入object()传入函数,返回一个新对象,这个新对象将person作为原型(其实应该说 F 将person作为原型),所以它的原型中就包含一个基本类型值属性和一个引用类型值属性。这意味着person.friend不仅属于person所有,而且也会被anotherPerson和yetAnotherPerson共享。实际上,这就相当于又创建了person对象的两个副本(说副本还是有门道的,是副本不是指针,如果是指针的话,anotherPerson和yetAnotherPerson 修改name属性的话,person也会被改,但亲测并不会,虽然F.prototype是person的指针,但new F(),F的实例不是指针,算了解释不下去了,以前是SubType.portotype = new SuperType()好歹是别人的实例就一定不是指针,但 F.prototype = o 是指针无疑啊,为什么anotherPerson和yetAnotherPerson 会有自己的name属性呢?)。ECMAScript5通过新Object.create()方法规范了原型式继承。这个方法接收两个参数:一个用作新对象的父对象和一个为新对象定义额外的属性的对象(可选,意思就是用对象字面量的方法定义新对象的额外属性)。在传入一个参数的情况下,Object.create()与object()方法的行为相同。var person = { name:"Nicholas", friends:["Shelby","Court","Van"]};var anotherPerson = Object.create(person);anotherPerson.name = "Greg"; //后面的代码会教不用这样写anotherPerson.friends.push("Rob");var yetAnotherPerson = Object.create(person);yetAnotherPerson.name = "Linda";yetAnotherPerson.friends.push("Barbie");console.log(person.friends); //["Shelby", "Court", "Van", "Rob", "Barbie"] 仍然存在的问题Object.create()的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性:var anotherPerson = Object.create(person,{ name:{ value:"Greg" //好麻烦啊这样写 }})console.log(anotherPerson.name) //"Greg"果然用上面的参数格式,其他三个特性不写,全部会由默认true变成默认false:console.log(Object.getOwnPropertyDescriptor(anotherPerson,"name"))//Object {value: "Greg", writable: false, enumerable: false, configurable: false}console.log(Object.getOwnPropertyDescriptor(yetAnotherPerson,"name"))//Object {value: "Linda", writable: true, enumerable: true, configurable: true}默认的自定义对象的三个特性都是true,如果用Object.defineProperties()的格式修改描述符的定义,没有列出来,则默认会被后台修改为false。6.3.5寄生式继承寄生式(parasitic)继承也是克罗克福德提出来的,与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工程模式类似,既创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"《Javascript》高级程序设计 第五章 详细解释引用类型(下)","date":"2017-07-10T14:02:24.000Z","path":"2017/07/10/《Javascript高级程序设计》-第五章-详细解释引用类型(下)/","text":"我的文章会在我的Blog同步更新,Blog刚搭了几天,用于记录我的学习过程:Million 5.5Function类型作者说ECMAScript中最有意思的是函数,有意思的根源在于,函数实际上是对象。每个函数都是Function类型的实例。与其他引用类型一样,函数类型也有属性和方法。由于函数是对象,因此函数名实际上只是一个指向函数对象的指针,不会与某个函数绑定。 函数通常是使用函数声明语法定义的,如下面的例子所示:123function sum(num1,num2){ return num1+num2;} 这与下面使用函数表达式定义函数的方法几乎相差无几:123var sum = function(num1,num2){ return num1+num2;}; // ← 注意最后的分号,声明完变量必加的分号 第二段代码定义了变量sum并将其初始化为一个函数(注意这句话的顺序,把一个定义好的变量初始化为一个函数)。我们可以发现用函数表达式创建的函数,function关键字后面没有函数名。因为没有必要。通过变量sum即可以引用到函数(注意,你不能说变量sum是函数名)。另外,注意函数末尾有一个分号,因为说到底sum还是个变量,只是变量的值是一个函数!最后一种定义函数的方式,因为函数是对象,所以可以使用Function构造函数。Function可以接收任意个数量的参数,最后一个参数始终被当作函数体,前面的都是函数的参数。例子:1var sum = new Function("num1","num2","num3","return num1+num2"); //不推荐 从技术的角度讲,这是一个函数表达式,但是不推荐使用。因为这种语法会导致解析两次代码(第一次是解析常规ECMAScript,第二次是解析传入构造函数中的字符串),从而影响性能。不过这种语法有利于理解“函数是对象,函数名是指针”这个概念。由于函数名仅仅是指针(好唏嘘,函数名以为用它的名字创建的函数就是它的,结果,它只是一个指针。)一个对象可以有很多指针,所以一个函数也可以有多个名字。例子:123456789function sum(num1,num2){ return num1+num2;}var anotherSum = sum;alert(anotherSum(10,10)); //20sum = null;alert(anotherSum(10,10)); //20 即使把sum给设置为null,但因为这个指针已经复制了一份给anotherSum,所以这个函数对象还是可以被引用到,不会因为sum不见了,函数就跟着没了。(亲测PHP似乎就不能用这种方法“转换函数名”)注意,使用不带圆括号的函数名是访问函数指针,使用带括号的函数名就表示要调用这个函数。 5.5.1没有重载把函数名想象为指针,就能理解为什么ECMAScript没有重载。例子:12345678function sum(num1,num2){ return num1+100;}function sum(num1){ return num1+200;}console.log(sum(100,100)); // 300 显然,例子中声明了两个同名函数,而结果是后面的函数覆盖了前面的函数。因为在创建第二个函数时,实际上覆盖了引用第一个函数的变量addSomeNumber。 5.5.2函数声明与函数表达式实际上,解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明并使其在执行任何代码之前可用(可以访问);至于函数表达式,就必须等到解析器执行到它所在的代码行,才会真正被解释执行。(所以函数声明有函数声明提升(function declaration hoisting),函数表达式没有)函数表达式不能写在要调用这个函数的代码之后。函数声明提升:Javascript引擎在第一遍会声明函数并将它们放到源代码树的顶部。所以即使声明函数的代码在调用它的代码后面。JavaScript也能把函数声明提升到顶部。除了什么时候可以通过变量访问函数这一点区别之外,函数声明与函数表达式的语法其实是等价的。再无其他差别。 ✎ 有人觉得不妨在使用函数表达式的同时给函数命名,like this : var sum = function sum(){ }但这种语法在 Safari 中会出错。 5.5.3作为值的函数因为ECMAScript中的函数名本身就是变量,所以函数也可以作为一个值来使用。也就是说,不仅可以像传递参数一样把一个函数当作参数传递给一个函数,而且可以将一个函数作为另一个函数的结果返回。例子:123function callSomeFunction(someFunc,someArgument){ return someFunc(someArgument)} 这个函数接收两个参数,一个是函数,一个是普通参数,这个普通参数给传进来的函数使用。然后,就可以像下面这样传递函数了:123456789101112function add10(num){ return num+10;}var result1 = callSomeFunction(add10,10); //注意函数作为参数传递是不带括号的,带括号的函数会执行,参数就变成这个函数执行完成返回的那个值了alert(result1); //20 function getGreeting(name){ return "Hello " + name;}var result2 = callSomeFunction(getGreeting,"Nicholas");alert(result2); // Hello Nicholas 注意,要访问函数的指针而不执行函数的话,必须去掉函数名后面的括号。因此上面例子给callSomeFunction()传递的是add10 和 getGreeting。而不是执行它们之后的结果。可以从一个函数中返回一个函数,是一项极为有用的技术。例如,假设有一个对象数组(由对象组成的数组),我们想要根据对象中的某一个属性对数组进行排序,而传递给sort()方法的比较函数要接收两个参数,即要比较的值。可是,我们需要一种方式来告诉sort()按照哪个对象属性排序。要解决这个问题,可以定义一个函数,它接收一个属性名,然后根据属性名来创建一个匿名比较函数。下面就是这个函数的定义:1234567891011121314function creatComparisonFunction(propertyName){ return function(obj1,obj2){ var val1 = obj1[propertyName]; //细节,用方括号访问保存在变量propertyName中的属性,终于知道用处了 var val2 = obj2[propertyName]; if(val1<val2){ return -1; }else if(val1>val2){ return 1; }else{ return 0; } };} 看起来很复杂,其实就是嵌套了一个函数,而且内部函数前面加了一个return操作符。在内部函数接收到propertyName参数后,它会用方括号表示法来取得给定属性的值。取得这个对象属性后,就用这个属性进行比较。完成对象数组的排序。用法:123456var data = [{name:"Zachary",age:28},{name:"Nicholas",age:29}];data.sort(creatComparisonFunction("name"));console.log(data[0].name); //Nicholasdata.sort(creatComparisonFunction("age"));console.log(data[0].name); //Zachary 5.5.4函数内部的属性在函数内部,有两个特殊的对象(函数对象中的对象):arguments和this。arguments在第三章介绍过,是一个类数组对象,包含这个传入函数的所有参数。虽然arguments的主要用途是保存所有函数参数,但arguments对象还有一个名叫callee的属性,该属性是一个指针。arguments.callee( )可以指代这个函数本身。用处是可以消除函数的执行与函数名之间的耦合。还是那个阶乘的例子:1234567function factorial(num){ if(num<=1){ return 1; }else{ return num*factorial(num-1); }} 如果我把factorial这个函数名改为另一个函数的函数名:12345var trueFactorial = factorial;function factorial(){ return 0;}console.log(trueFactorial(5)) // 0 这个时候trueFactorial()函数就出错了,要降低这种耦合,使用arguments.callee( )是最好方法。1234567function factorial(num){ if(num<=1){ return 1; }else{ return num*arguments.callee(num-1); //即使factorial被改写,arguments.callee()仍代表当前函数。 }} 函数内部的另一个特殊对象是this,其行为与Java和C#中的this大致类似。换句话说,this引用的是函数据以执行的环境对象。当在网页的全局作用域中调用函数时,this对象引用的就是window。例子:123456789indow.color = "red";var o = {color:"blue"};function sayColor(){ console.log(this.color);}sayColor(); //redo.sayColor=sayColor;o.sayColor(); //blue 函数sayColor( )首先是在全局作用域中定义的,它引用了this对象。但在调用它之前,并不确定是在哪个作用域中调用。当在全局作用域中调用sayColor( ),this引用的是全局对象,所以this.color = window.color。结果返回red。当把这个函数赋给对象o,并调用o.sayColor( )时,this引用的是对象o,因此this.color = o.color。结果返回blue。(亲测把sayColor()函数放在另一个函数中去调用,最后得到的结果还是window,再嵌多一层函数也还是window,说明this引用的不是作用域,而是函数据以执行的环境对象,还有一个原因是当你把函数嵌套进另一个函数的时候,返回是window的原因是你此时调用的实际上已经不是一个方法,而是一个函数。) ECMAScript5还规范化了另一个函数对象的属性caller。除了Opera早期版本不支持,其他浏览器IE,Firefox,Chrome,和Safari的所有版本及Opear9.6都支持caller属性。这个属性保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null。例如:1234567function outer(){ inner();}function inner(){ console.log(inner.caller); //会打印出outer()的源代码}outer(); 以上代码会打印出outer( )的源代码,因为outer()调用了inner(),所以inner.caller就指向outer()。当然不能试图用inner.caller( )来调用outer。否则会陷入无限回调使代码出错。为了更松散的耦合,也可以通过arguments.callee.caller()来访问相同的信息。1234567function outer(){ inner();}function inner(){ console.log(arguments.callee.caller); //会打印出outer()的源代码}outer(); arguments在严格模式下无法使用,arguments对象也有一个caller属性,但这个值永远是undefined。定义这个属性是为了分清arguments.caller和函数的caller属性,这个变化是出于增强这门语言的安全性。严格模式还有一个限制:不能为函数的caller属性赋值,否则会导致错误。 5.5.5函数属性和方法前面提过ECMAScript中函数是对象,所以函数就有属性和方法,上面介绍了一个caller。还有两个是length和prototype。length属性表示函数希望接收的命名参数的个数。命名参数有1个,length的值就是1,参数有两个,length的值就是2。不解释。 作者说,在ECMAScript核心所定义的全部属性中,最耐人寻味的就要数prototype属性了(不止函数的prototype,其他对象的prototype属性也耐人寻味!)对与ECMAScript中的引用类型而言,prototype是保存他们所有实例方法的真正所在。换句话说,诸如valueOf(),toString()等方法实际上都是保存在了prototype属性名下。只不过是通过各自对象的实例访问到了。在创建自定义引用类型以及实现继承时,prototype属性的作用是极为重要的(第6章会详细介绍)。在ECMAScript中,prototype属性是不可枚举的,因此无法用for-in无法发现prototype这个属性。(对property属性的讲解到此为止) 每个函数都包含两个非继承而来的方法:apply()和call()。这两个方法的用途都是在特定的作用域中调用函数。实际上等于设置函数体内this对象的值。两个方法的区别只是在传入参数的方法上有区别而已。首先,apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是Array的实例(就是普通数组),也可以是arguments对象。例子:12345678910111213function sum(num1,num2){ return num1+num2;}function callSum1(num1,num2){ return sum.apply(this,arguments);}function callSum2(num1,num2){ return sum.apply(this,[num1,num2]);}console.log(callSum1(10,10)); //20console.log(callSum2(10,10)); //20 在上面这个例子中,callSum1( )在执行sum()函数时传入了 this 作为 this 值(???)(因为是在全局作用域中调用的,所以传入的就是window对象)和arguments对象,而callSum2传入的第二个参数则是数组型。这两个函数都会正常执行并返回正确结果。(这个例子只是为了说明两种传入参数的方式都可以,还没体现出apply()和call()的作用) ✎在严格模式下,未指定环境对象而用函数,则this值不会转型为window(严格模式下的this不会默认转型为window)除非明确把函数添加到某个对象或者调用apply()或call(),否则this值将是undefined。 call()方法与apply()方法的作用相同,区别仅在于接收参数的方式不同。对于call()方法,第一个参数仍是this不变,变化的是其余参数都是直接传递给函数。换句话说,在使用call()方法时,参数都是逐个列举出来的。例子:12345678function sum(num1,num2){ return num1+num2;}function callSum1(num1,num2){ return sum.call(this,num1,num2);}console.log(callSum1(10,10)); // 20 在使用call()方法的情况下,callSum( )必须明确地传入每一个参数。结果与apply()没有什么不同。使用哪一种完全是看你觉得哪个更方便(MD以前还觉得这两个方法多深奥ZZ)。apply()和call()的真正强大之处,是能够扩充函数赖以运行的作用域。例子:12345678910window.color = "red";var o = {color:"blue"};function sayColor(){ console.log(this.color)}sayColor(); //redsayColor.call(window); //redsayColor.call(o); //blue 第一次调用sayColor()时在全局作用域中调用它,会显示“red”——因为对this.color的求值会转换成对window.color的求值。当运行sayColor。call(o)时,函数的执行环境对象就不一样了,因此此时函数体内的this对象指向了O,于是结果显示“blue”。使用call()和apply()的最大好处,就是对象不需要与方法有任何耦合关系(不用为了在这个对象内调用方法而把函数写进对象中)。在书中前面的第一个例子中(笔记没有),为了在对象o中调用sayColor( ),要把这个函数写进对象o中,再通过o调用,而在这里的例子中,就不需要那么多步骤了。ECMAScript5又定义了一个方法:bind()(所以函数对象的方法有三个)。这个方法会创建一个函数的实例(会创建一个函数),其this值会被绑定到传给bind()函数的值。例子:12345678window.color = "red";var o = {color:"blue"};function sayColor(){ console.log(this.color)}var objSayColor = sayColor.bind(o);objSayColor(); 在这里,sayColor()调用bind()并传入对象o,创建了objSayColor()函数。objSayColor()的this值等于o。因此无论在哪个环境对象中调用这个函数,都会看到“blue”。这种技巧的优点参考第22章。因为是ECMAScrip5才有的方法,所以可以使用bind() 的浏览器有: IE9+,Firefox4+,Safari 5.1+,Opera 12+和Chrome。(兼容性挺低的) 最后说每个引用类型值都有继承的,都会被重写的toLocaleString()和toString(),valueOf(),这三个方法都始终返回函数的代码。返回代码的格式根据浏览器的不同会有差异。因为有返回值差异,所以返回的结果无法被用来实现任何功能,不过这些信息在调试代码时很有用。 5.6基本包装类型读到这里又刷新了三观。这里说Boolean、Number、String是3个ECMAScript提供的特殊引用类型。这些类型与本章介绍的其他引用类型相似,但同时也具有与各自的基本类型相应的特殊行为。实际上,每创建一个基本类型的时候,后台就会创建一个对应的基本包装类型对象(WTF???),从而让我们能够调用一些方法来操作这些数据(这解释了为什么三种基本类型值也有自带的属性和方法)。例子,我们可以这样使用基本类型值的方法:12var s1 = "some text";var s2 = s1.subString(2); 可以看到我们可以调用String类型自带的方法,但如下面的例子,我们不能在运行时为基本类型值添加属性和方法:123var s1 = "some text";s1.color = "red";console.log(s1.color); //undefined 我们知道,基本类型不是对象,因而从逻辑上它不应该有方法(尽管如我们所愿,它有方法)。其实,为了让我们能用方法操作基本类型值,后台已经自动完成了一系列的处理(注意后面是原理):当第一段代码的第二行(var s2 = s1.subString(2); )访问s1时,访问过程处于一种读取模式,也就是要从内存中读取这个字符串的值。而在读取模式中访问字符串时,后台都会自动完成下列处理:(1)创建String类型的一个实例;(2)在实例上调用指定的方法;(3)销毁这个实例。以上三个步骤用代码表示就是:123var s1 = new String("some txt");var s2 = s1.subString(2); //把结果放回给另一个变量s1 = null; //销毁这个对象 上面的步骤也适用于Boolean,Number类型。这也就解释了为什么不能给基本类型值添加属性和方法,因为基本类型值对象在调用完方法后就会被后台自动销毁。 引用类型和基本包装类型的主要区别就是对象的生存期。使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中(离开了作用域后可能就会被标记,然后被垃圾收集机制回收)。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁(亲测手动用new操作符创建的基本类型对象,可以添加属性和方法。)所以我们无法为基本类型值添加属性和方法。当然如果没有必要的话,就不要显示地调用Boolean和Number、String来创建基本包装类型对象(不要var s1 = new String(“some txt”)这样写)。因为这样容易让人分不清自己是在处理基本类型还是引用类型的值。 Object构造函数也会像工厂方法一样,根据传入值的类型返回相应基本包装类型的实例。例如:123var obj = new Object("some text");console.log(obj instanceof String); //trueconsole.log(obj instanceof Object); //true 就是把字符串传给Object构造函数,就会创建String的实例,传入数值参数就会得到Number实例,传入布尔值参数就会得到Boolean实例,但是你检测它是以上基本类型值会返回true,检测是不是Object也会返回true。下面会讲每个基本包装类型提供的操作相应值的便捷方法。 5.6.1Boolean类型这节的重点是理解基本类型的布尔值与Boolean对象之间的区别。当然,作者的建议是永远不要使用Boolean对象,这节就当冷知识吧。 创建Boolean对象可以像下面这样调用Boolean构造函数并传入true或false值。1var booleanObject = new Boolean(false) Boolean类型的实例重写了的valueOf( )方法会返回基本类型的true和false,重写了的toString( )会返回字符串型的“true”和“false”。书中前面提到过,但是当时不懂有什么用就没记的一段话:布尔表达式中的所有对象都会被转换为true。例子:123var falseObject = new Boolean(false);var result = falseObject && true;console.log(result); //true 在这个例子中我们用false值创建了Boolean对象的实例falseObject,并对这个对象与true进行&&运算,却发现返回到结果是true。因为,示例中的代码是对falseObject而不是对它的值(false)求值。而布尔表达式中的所有对象都会被转换为true,结果true&&true当然就等于true了。 5.6.2Number类型Number类型也重写了valueOf()、toLocaleString()、toString()。valueOf()返回这个对象表示的基本类型的数值(亲测就是一个数)。toLocaleString()、toString()返回字符串类型的数值。Number的toString()方法可以传递一个表示基数的参数,参数是几,就按几进制的数字的字符串形式返回。12var num = 10;console.log(num.toString(2)) //"1010" 注意返回的是字符串形式的。 上面的几个是继承的方法,Number类型提供了一些可以将数值格式化的方法。 toFixed():按照指定的小数位返回数字的字符串表示,且支持四舍五入。比如25.005用toFixed(2),会变成“25.01”。 toExponential(():返回以指数表示法表示的数值的字符串形式。接收的参数也是指定输出结果中的小数位数。 12345var num = 10.005;console.log(num.toFixed(2)) //"10.01"var num = 10;console.log(num.toExponential(1))//"1.0e+1" 5.6.3String类型String对象继承的valueOf()、toLocaleString()、toString()返回的都是这个对象表示的基本字符串值。String类型的每个实例都有一个length属性,表示字符串中有多少个字符(亲测中文也是有多少个字长度就是几)。书中说了,即使字符串中包含双字节字符(不是占一个字节的ASCII字符,每个字符也仍然算一个字符)。 String类型提供的方法很多,看到都怕。 1.字符方法两个用于访问字符串中特定位置字符的方法:charAt( )和charCodeAt( )。两个方法都接收一个参数,即基于0的字符位置。两个方法的区别是charAt( )返回的是给定位置的那个字符。charCodeAt( )返回的是那个字符的字符编码。例子:123var stringValue = "hello World";console.log(stringValue.charAt(1)); // "e"console.log(stringValue.charCodeAt(1)); // "101" 是小写字母e的字符编码 ECMAScript5定义了另一个访问个别字符的方法,可以用方括号加数字索引来访问字符串中的特定字符。例子:12var stringValue = "hello World";console.log(stringValue[1]); //"e" 类似于把字符串当成一个“数组”,按位置取出所在位置的字符。亲测可以直接在字符串后面加方括号,例子:1console.log("sdsdsdsd"[2]); //"s" 66666 支持这个语法的浏览器有IE8,Firefox,Safari,Opera 和Chrome所有版本的支持。冷知识:在IE7及更早版本使用这种语法会返回undefined值,尽管不是特殊的undefined值(不是很懂最后一句的意思,我以为返回的那个undefined是字符串型的,但是不是) 2.字符串操作方法操作字符串的方法有几个,第一个是concat( ),可以将一或多个字符串拼接起来,参数就是要添加进去的字符串,参数数量不限。返回得到的新字符串。数组类型也有这个方法,不解释。不过在实践中使用最多的是加号操作符(+)(MDZZ)。ECMAScript还提供了三个基于子字符串创建新字符串的方法:slice()、substr()、substring()。三个方法都返回被操作字符串的一个子字符串。三个方法的第一个参数都是指定子字符串的开始位置。slice()和substring()的第二个参数指定的是子字符最后一个字符后面的位置(忽略结尾那个位置)。而substr() 的第二个参数指定的则是返回字符的个数。如果没有第二个参数,则默认将字符串的长度作为结束的位置。(太乱实际用再百度)。与concat()方法一样,这三个方法也不会修改本身的那个字符串。对原始字符串无任何影响。例子:1234567var stringValue = "Hello World";console.log(stringValue.slice(3)); //"lo world"console.log(stringValue.substring(3)); //"lo World"console.log(stringValue.substr(3)); //"lo World"console.log(stringValue.slice(3,7)); //"lo W"console.log(stringValue.substring(3,7)); //"lo W"console.log(stringValue.substr(3,7)); //"lo Worl" substr()第二个参数表示要截取的字符个数 看起来三个方法作用很容易理解,但是传递给这些方法的参数是负数的情况下,他们的行为就不尽相同了。其中,slice()会将传入的负值与字符串的长度相加,substr()将负的第一个参数加上字符串的长度,而将第二个负的参数转换为0.最后,substring()方法会将所有负值参数都转换为0.很恐怖,不想举例子了,实践中没事不要用负数就行。需要再看书。 3.字符串位置方法有两个可以从字符串中查找子字符串的方法:indexOf( )和lastIndexOf( )。这两个方法都是从一个字符串中搜索给定的子字符串,然后返回子字符串的位置。查找不到就返回-1.两个方法的区别是一个从头向后查询,一个从后向前查询。例子:123var stringValue = "hello world";console.log(stringValue.indexOf("o")); //4console.log(stringValue.lastIndexOf("o")) //7 这两个方法都接收第二个参数,表示从字符串哪个位置开始搜索。换句话说,indexOf()从该参数指定的位置向后搜索,忽略之前的所有字符;而lastIndexOf()则会从指定的位置向前搜索,忽略该位置之后的所有字符(为什么在讲第二次的时候才讲这么清楚)。例子:123var stringValue = "hello world";console.log(stringValue.indexOf("o",6)); //7console.log(stringValue.lastIndexOf("o",6))//4 indexOf( )从位置6(字母“w”)开始向后搜索,在位置7找到“o”。lastIndexOf( )从位置6开始向前搜索,找到的是“hello”中的“o”,所以返回4。 我们可以在使用第二个参数的情况下,通过循环把所有匹配的子字符串找出来,例子:123456789var stringValue = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";var positions = new Array();var pos = stringValue.indexOf("e");while(pos>-1){ positions.push(pos); pos = stringValue.indexOf("e",pos+1);}console.log(positions); //[3, 24, 32, 35, 52] 4.trim()方法ECMAScript5定义了trim()方法,该方法会创建一个字符串副本,删除前置和后置的所有空格,然后返回结果。支持这个方法的浏览器有:IE9+,Firefox 3.5+,Safari 5+,Opera 10.5+和Chrome。此外,Firefox 3.5+,Safari 5+和Chrome 8+还支持非标准的trimLeft( )和trimRight( ) ,分别用于删除字符串开头和末尾的空格。 5.字符串大小写转换方法toLowerCase()、toLocaleLowerCase()、toUpperCase()和toLocaleUpperCase()。为什么会有toLocaleLowerCase()、toLocaleUpperCase()这两个方法是因为有些地区比如土耳其语回味iUnicode大小写转换应用特殊的规则也是醉了,所以用这两个针对地区的方法来保证正确的转换。 6.字符串的模式匹配方法正则看得头大 7.localCompare方法tolocalCompare()方法就是用字符串跟方法的字符串参数比较,如果字符串在字母表中应该排在字符串参数之前,则返回一个负数(大多数情况下是-1),如果字符串等于字符串参数,则返回0;如果字符串在字母表中应该排在字符串参数之后,则返回一个正数(大多数情况下是1)。而且localCompare()方法有个与众不同的地方,大小写是否区分要视使用方法的地区不同而定。比如美国以英语作为ECMAScript实现的标准语言,因此localCompare()就是区分大小写的,则大写字母在字母表中会排在小写字母前面。所以是否区分大小写要根据地区而定。(亲测中国地区区分大小写且大写字母在小写字母之前) 8.fromCharCode()方法String构造函数(是构造函数的方法不是普通字符串的方法)本身还有一个静态方法:fromCharCode( )。这个方法可以接收一或多个字符编码,将他们转换为字符串。例子: 9.HTML方法作者建议尽量不用这些方法,因为他们创建的标记通常无法表达语义(不懂)。方法有12个,需要再查书吧。 5.7单体内置对象ECMA-262对内置对象的定义是:“由ECMAScript实现提供的,不依赖宿主环境的对象,这些对象在ECMAScript程序执行之前就已经存在了,所以开发人员不用显示地实例化内置对象,因为他们已经实例化了。”前面介绍的Object、Array和String等都是内置对象。ECMA-262还定义了两个单体内置对象:Global和Math。 5.7.1Global对象Global(全局)对象是ECMAScript中最特别的一个对象,因为你不管从什么角度上看,这个对象都是不存在的(???)。ECMAScript中的Global对象在某种意义上是作为一个“兜底儿对象”。换句话说,不属于任何对象的属性和方法,都是global对象的属性和方法。事实上,没有全局变量和全局函数,因为所有在全局作用域中定义的属性和函数,都是Global对象的属性(只是属性,函数也是Global对象的属性)。本书前面介绍过的那些函数,比如isNan(),isFinite()、parseInt()以及parseFloat(),都是Global对象的方法(array,object等类型对象的方法当然不是Global对象的,是array、object等这些对象的,不要混淆)。除此之外,Global还有其他一些方法。下面介绍这几个方法: 1.URI编码方法Global对象的encodeURI()和encodeURIComponent()方法可以对URI(Uniform Resource Identifiers,通用资源标识符)进行编码,以便发送给浏览器。有效的URI不能包含某些字符,例如空格,而这两个URI编码方法就会对URI进行编码,用特殊的UTF-8字符替换所有无效的字符,让浏览器能够接受和理解。encodeURI()和encodeURIComponent()的第一个区别是,encodeURI()只会把URI中无效的字符替换掉,encodeURIComponent()会把正确的特殊符号例如冒号、正斜杠、问号和井号也给替换掉。第二个区别是,encodeURI()用于整个URI,而encodeURIComponent()用于对URI中的某一段进行编码。例子:12345var uri = "http://www.wrow.com/illegal value.html#start";console.log(encodeURI(uri)); //http://www.wrow.com/illegal%20%20value.html#startconsole.log(encodeURIComponent(uri)); //http%3A%2F%2Fwww.wrow.com%2Fillegal%20%20value.html%23start 第二个方法中连12345有编码的函数,就有解码的函数,与这两个函数对应的函数是**decodeURI()和decodeURIComponent()**。其中,decodeURI()**只能对非法字符进行解码**,decodeURIComponent()可以对**所有被编译的符号**进行解码。例如,decodeURI()可以将%20替换成空格,但不能对%23做任何处理,因为%23表示井字号(#),而井字号是合法符号。但是decodeURIComponent()就%20和%23都可以解码。<h4>2.eval()方法</h4>最后一个Global对象的方法,大概是**整个ECMAScript语言中最强大的一个方法:eval()**(见仁见智,有些人觉得这个方法弊端多多)。eval()就像一个**完整的ECMAScript解析器**,它只接收一个参数,即**要执行的ECMAScript(或JavaScript)字符串**。例子: eval(“alert(‘hi’)”);1以上代码等价于下面这行(so what??): alert(‘hi’);1当解析器发现代码中调用eval()方法时,它会将传入的参数当作实际的ECMAScript语句来解析,然后把执行结果插入到原来位置。**通过eval()方法执行的代码被认为是包含该次调用的执行环境的一部分**,因此被执行的代码具有与该执行环境相同的**作用域链**。这意味着通过eval()执行的代码**可以引用在包含环境中定义的变量**。例子: var msg = “hello world”;eval(“alert(msg)”);1可见,变量msg是在eval()调用的环境之外定义的,但其中调用的alert()仍然能够显示“hello world”。这是因为上面第二行代码最终被替换成了一行真正的代码(so?那干嘛要加个eval(),直接写进执行环境不就好了吗)。同样地,我们也可以在eval()调用中定义一个函数,然后在外部代码中引用这个函数: eval(“function sayHi(){ alert(‘Hi’); }”);sayHi();12345显然,函数sayHi()是在eval()内部定义的。但由于对eval()的调用最终会被替换成定义函数的实际代码,因此可以在下一行的外部代码中调用sayHi( )。在eval()中创建的仍和变量或函数**都不会被提升**,原理:因为在解析代码的时候,他们被包含在一个字符串中,它们只在eval()执行的时候创建。严格模式下,在外部访问不到eval()中创建的任何变量或函数,因此前面的那个例子就会导致错误。在严格模式下,为eval赋值也会导致错误: “use strict”;eval = “hi”; //causes error123456<h4>3.Global对象的属性</h4>前面说过了,没有明确对象的属性,最后都是Global的属性,例如,特殊的值undefined、NaN以及Infinity等。此外,所有原生引用类型的**构造函数**,像Object,Function也都是Global对象的属性。书中列出了所有Global对象的所有属性。在P133.ECMAScript 5明确禁止给undefined、NaN和Infinity赋值,这样做即使在非严格模式下也会导致错误。<h4>4.window对象</h4>ECMAScript没有明确告诉我们如何直接访问Global对象,但Web浏览器都是将这个全局对象作为window对象的一部分加以实现的(在浏览器中,Global对象是**window对象的一部分**,注意范围:浏览器中,window对象的一部分。)例子: var color = “red”; function sayColor(){ alert(window.color);}window.sayColor(); //“red”12345在这里我们定义了一个名为color的全局变量和一个sayColor()全局函数。在sayColor()内部,我们用window.color来访问变量,前面我们说过,全局环境中定义的变量和函数,实际上都是Global的属性,所以**window.color等价于Global.color**。以此说明,全局变量是window对象的属性。然后,又使用window.color来直接通过window.sayColor( )直接通过window对象调用这个函数。>✎JavaScript中的window对象除了扮演ECMAScript规定的Global对象的角色外,还承担了很多任务,所以说Global对象这是window对象的一部分。第8章在讨论浏览器对象模型时将详细介绍window对象。另一种取得Global对象的方法是使用以下代码: var global = function(){ return this; // 亲测return出来的是 window}();```这是个立即调用的函数表达式,返回this的值。如前所述,在没有给函数明确指定this值的情况下,this值等于Global对象。而像这样通过简单地返回this来取得Global对象,在任何执行环境下都是可行的。(因为是立即执行函数的原因?) 5.7.2Math对象不想写了这个,就是Math下的各种方法。可以直接在书中查阅。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"Postcss使用体验:在未来写CSS","date":"2017-07-07T12:24:46.000Z","path":"2017/07/07/Postcss使用体验:在未来写CSSSS/","text":" 接触Postcss的起因是在用Npm安装模块的时候看到的提示语句,一番接触后发现:我以后可能不需要写Sass了。 今天用Npm安装模块的时候出现了一段提示语句: 大致了解了一下Postcss是一个平台,上面有各种插件用来处理CSS,不过里面讲的东西,我觉得没必要用得这么麻烦。 重点是他推荐的大漠老师POSTCSS深入学习,进了这个指引,你对Postcss的理解,最重要使用就能非常清晰了。 说说我对Postcss的误解,一开始我以为它是像Sass、Less一样的预处理器,心想又要去学一种预处理器吗?后来又发现它竟然还有autoprefixer(自动添加浏览器前缀),压缩代码等功能,后来又觉得,这些功能能做的,gulp也能做,那我还要Postcss干嘛? 通过学习后发现,它就是gulp+Sass的结合版,最重要的是一下几点: 一、清晰、简短的配置 只要你引入了Npm中各种“postcss-”前缀的插件,只要你不是很想特别定制,只要在处理css的‘gulp’的’task’中给一个数组,写进你要添加的处理模块,gulp就能编译出CSS代码,相比gulp的插件,要写一个个方法,一些方法的配置还要去查文档,Postcss的配置简直简单到“引入即用”的感觉。 这个就是我gulpfile.js中Postcss部分的配置: 123456789101112131415161718192021222324var gulp = require('gulp');var postcss = require('gulp-postcss');var autoprefixer = require('autoprefixer'); //自动添加前缀var opacity = require('postcss-opacity'); //opacity属性的降级处理var pseudoelements = require('postcss-pseudoelements'); //让IE8支持::的伪元素var vmin = require('postcss-vmin'); //为IE9支持viewport相对单位vminvar pixrem = require('pixrem'); //给rem添加px作为降级处理为IE8var will_change = require('postcss-will-change'); //为will-change属性添加回退var cssnext = require('cssnext'); //写未来的CSSvar precss = require('precss'); //用函数的方法写CSSvar color_rgba_fallback = require('postcss-color-rgba-fallback'); //给rgba()提供降级方案为IE8var atImport = require('postcss-import'); //可以使用@import引入其他CSS文件,减少Http请求var mqpacker = require('css-mqpacker'); //合并媒体查询var size = require('postcss-size'); //CSS中一个size属性同时写height和widthgulp.task('css', function () { //will_change必须在autoprefixer之前 var processors = [ will_change, autoprefixer, cssnext,color_rgba_fallback, opacity, pseudoelements, vmin, pixrem,precss,atImport,mqpacker,size ]; return gulp.src('./src/*.css') .pipe(postcss(processors)) .pipe(gulp.dest('./dest'));}); 可以看到除了长长的引用模块外,想要使用Postcss中的插件,只需要在processors数组中添加你要使用的模块,用pipe()处理即可。 二、极大减少CSS书写量 如果你已经厌倦了一次次地写重复的垂直居中、水平居中,Postcss中,有postcss-center帮你搞定,只要你引入了postcss-center,top:center用于实现垂直居中,left:center用于实现水平居中:1234.centered { top:center; left:center;} 编译出来的CSS:1234567.centered{ position: absolute; top: 50%; right: 50%; margin-right: -50%; transform: translate(-50%, -50%);} 两段代码来自大漠老师教程 诸如此类,清除浮动,设置定位,设置水平,垂直间距,输出颜色代码等等等,都分别有postcss-clearfix,postcss-position,postcss-verthorz,postcss-color-short等插件可以实现,极大提升写代码效率。因为我还在学习中,这些东西我觉得还是先自己写明白了比较适合,所以只使用了postcss-size这个插件,就是使用后height和width可以同时写在同一行,我早就想这么做啦23333:123.img { size: 200px 200px;} 编译后的CSS:1234img { height: 200px; width: 200px;} 三、类似Sass的语法 并不是说postcss有类似Sass的语法,而是因为postcss有cssnext插件,可以用未来使用的css语法,再转化为css现在浏览器支持的语法,而类似Sass中的mixin,extend,定义变量,函数等功能,未来的CSS语法也都有,只是与Sass有少许差异,而且如果你想用Sass,Less编写也可以,只要调整一下编译顺序,让编译Sass的命令在postcss命令之前即可。 总而言之,postcss上这么多插件,合理搭配使用可以极大地提高开发效率,我觉得既然未来的CSS语法已经摆在了你面前,就像ES6语法有了babel的支持一样,可以借助postcss而提前使用,那么为什么不早点使用迟早会成为标准的语法呢,我已经打算开始尝试结合gulp上的其他配置,开始学习“未来的CSS”,尝试postcss的使用,在未来写CSS,一起试试吧!","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"CSS","slug":"CSS","permalink":"https://millionqw.github.io/tags/CSS/"}]},{"title":"《Javascript》高级程序设计 第五章 详细解释引用类型(上)","date":"2017-07-06T13:45:07.000Z","path":"2017/07/06/《Javascript》高级程序设计 第五章 详细解释引用类型(上)/","text":"第五章 引用类型引用类型的值(对象)是引用类型的一个实例。如前所述,对象是某个特定引用类型的实例。新对象是使用new操作符后跟一个构造函数来创建的。构造函数本身也是一个函数,只不过该函数是出于创建新对象的目的而定义的。例子:1var person = new Object(); 这行代码创建了一个Object这个引用类型的新实例,然后把该实例保存在了变量person中。(这句话特别有意思,如果我用自己的话说,会说“创建了一个叫person的Object的新实例,而不会说先创建了实例再把实例保存在person中”)使用的构造函数就是Object( )。它为新对象(person定义了默认的属性和方法)。ECMAScript提供了很多原生引用类型(例如Object( ),Date( ),RegExp( ))。以便开发人员完成常见的计算任务。 5.1Object类型创建Object类型的方法有两种。第一种,使用new操作符后跟Object构造函数。例子:123var person = new Object();person.name = "Nicholas";person.age = 29; 第二种,用对象字面量表示法。这种方法更被开发人员所接受,最推荐使用。代码量少,且给人封装数据的感觉。实际上,对象字面量也是向函数传递大量可选参数的首选方式。例子:1234var person = { name : "Nicholas", //属性名也可以用字符串如:"name" : "Nicholas" age : 29 // "age" : 29} 在对象字面量中使用逗号来分隔不同的属性,因此“Nicholas”后面记得加逗号,最后一个属性后面不能加逗号。否则IE7及更早版本和opera会出现错误。✎注意:使用对象字面量定义对象时,并没有调用到Object函数(即内在原理不是Object函数,Firefox 2以及更早版本的Firefox会调用Object函数,但Firefox3之后就不会了。) ✎关于表达式上下文(expression context)和语句上下文(statement context)在用对象字面量定义新对象的例子中,左边的花括号“{”表示对象字面量的开始,因为它出现在了表达式上下文(expression context)中(”person=”,等号的后面)。ECMAScript中的表达式上下文指的是该上下文期待一个值(表达式)。赋值操作符(“=”)表示后面是一个值(这里的值是一个对象,不要直觉觉得值就是一个数字),所以左花括号在这里表示一个表达式的开始。同样的花括号如果出现在一个语句上下文(statement context)中,例如跟在 if 语句条件的后面,则表示一个语句块的开始。 对象字面量除了可以用来定义新对象,当函数需要大量可选参数时,使用对象字面量也是不错的选择。例子:12345678910111213141516171819202122function displayInfo(args){ var output = ""; if(typeof args.name == "string"){ output += "Name:" + args.name + "\\n"; console.log(output); } if(typeof args.age == "number"){ output=""; output += "Age" +args.age + "\\n"; console.log(output); }}//用对象字面量传递参数,这时候args就是对象,name,age就是args的属性displayInfo({ name:"Nicholas", age:29})displayInfo({ name:"Greg"}) 这个例子中,函数displayInfo()接受一个名为args的参数,这个参数可能带有一个名为name或age的属性,有可能同时有也可能都没有。在函数内部用typeof操作符检测每个属性是否存在,再根据相应属性来构建显示信息。对象字面量传递参数法特别适合在需要向函数传递大量可选参数的情形。最好的做法是:对那些必需值使用命名参数(直接写在括号内),用对象字面量来封装多个可选参数。 ✎访问对象属性时用点表示法或者是方括号“[ ]”的取舍仍然以上面的person对象为例子。使用方括号访问对象属性的时候,千万千万记得要给对象属性加双引号(亲测单引号也可以)才能真正访问到那个对象属性,这是个很容易出现错误但找不到原因的地方。例子:12alert(person["name"]); // Nicholasalert(person[name]); // undefined 使用方括号的优点一是,可以使用变量间接地访问对象属性。例子:123var propertyName = "name"; // 必须加双引号,否则是undefinedalert(person[propertyName]); // Nicholasalert(person.name); // undefined 方括号的优点二:如果对象属性名中包含会导致语法错误的字符(比如空格),或者属性名使用的是关键字或保留字,也可以使用方括号表示法。例子:123person["first name"] = "Nicholas";person["last-name"] = "Tom";alert(person["first name"]); 有空格,“ - ”等会导致错误的属性名,只能通过方括号语法赋值(对象字面量也不行)和访问。(亲测)当然,除非是使用变量来访问属性,或者属性中带有特殊字符,否则还是建议用点表示法访问。 5.2Array类型avaScript的数组类型与其他语言的数组类型的最大不同,就是JavaScript数组的每一项可以保存任何类型的数据,比如第一项保存数字,第二项保存字符串,第三项保存对象。而且可以随着数据的添加自动增长以容纳新增数据。创建数组的两种方法。方法一是使用Array构造函数,例子:1var colors = new Array(); 如果预先知道了数组是多少位,也可以给Array构造函数直接传递数量,该数字就会成为该数组的length属性的值1var colors = new Array(20); // colors.length = 20 也可以直接向Array构造函数直接传递数组包含的值1var colors = new Array("red", "blue", "orange"); 在使用Array构造函数的时候,可以省略“new”操作符。省略“new”操作符的效果与添加“new”的效果相同。第二种创建数组的方法:数组字面量表示法。(与对象一样,使用数组字面量表示法时也不会调用Array( )构造函数(Firefox3及更早的版本除外))1234var colors = [ "red", "blue", "orange" ];var names = [];var values = [1,2,] //不要这样做,IE8及之前的版本会出BUGvar options = [, , , , ,] //不要这样做,IE8及之前的版本会出BUG 第三种情况在IE8及之前的版本中,数组values会有1,2,undefined三个值,其他浏览器则只有1和2.第四种的情况与第三种类似,IE会创建6个值,其他浏览器会创建5个,因为有浏览器差异,所以强烈建议不这么做。不过正常人谁这么做。把玩Array引用类型的属性length数据的length属性有一个特点——可写可读。如果设置这个属性的值比现有数组长度短,则设置的长度后面的数组的值就会被删除。如果设置length的值比现有的length大(比如设置array.length=100),则数组的长度变为100,空位置为undefined。利用length也可以很方便地在数组末尾添加新的值。例子:12var colors = ["red", "blue", "orange"]; // colors.length = 3;colors[colors.length] = "black" // clolors[3] = "black"; 当把一个值放在超出当前数组大小的位置,数组就会重新计算长度,长度值变成最后一项的索引(索引就是从0开始的)加一。例子:123var colors = ["red", "blue", "orange"];var colors[99] = "black";alert(colors.length) //100 中间colors[3]到colors[99]的值都是undefined。 数组的最大长度可以到4294967295(算冷知识吧)如果超过这个数就会出现异常。如果一个数组的大小接近这个上限值,会导致运行时间超长的脚本错误。 5.2.1检测数组对于一个网页或一个全局作用域而言,使用instanceof操作符就能得到满意的结果。而instanceof操作符的问题在于,它假定只有一个全局执行环境。如果网页红包含多个框架,那实际上就会出现两个及以上的执行环境,从而存在两个以上不同版本的Array构造函数,如果从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。为了解决这个问题,ECMAScript5新增了 Array.isArray( ) 方法,这个方法的目的是最终确定这个值到底是不是数组,而不管它是在哪个全局执行环境中创建的(我也不知道为什么多个执行环境下有多个不同的构造函数就使instanceof操作符无法检测数组。)该方法用法如下:123if(Array.isArray(value)){ //对数组执行操作} 5.2.2转换方法前面章节提过,所有对象都自带 toLocalString( )、toString( ) 和 valueOf( )方法。其中,调用数组的toString( )方法会返回由数组中每个值的字符串形式拼接而成的一个以逗号分隔的字符串。而调用valueOf( )返回的还是数组。实际上,toString( )方法为了创建这个字符串会调用数组每一项的toString()方法(原理)。例子:1234var colors = ["red", "blue", "orange"];alert(colors.toString()); //red,blue,orangealert(colors.valueOf()); //red,blue,orangealert(colors); //red,blue,orange 第一个alert显式地调用了toString方法(相对第三个alert),每个值的字符串表示拼接成了一个字符串,中间用逗号分隔。第二个alert调用valueOf方法,最后一行代码直接将数组传递给alert。由于alert( ) 要接收字符串参数,所以它会在后台自动调用toString方法(相对第一个alert的显示调用),由此得到与第一个alert相同的结果。(用typeof 和 valueOf亲测 colors.valueOf( )的类型是Array,colors.toString( )的类型是String) ✎toString()和toLocalString()的不同toLocaleString 方法返回一个 String 对象,这个对象中包含了用当前区域设置的默认格式表示的日期。 toLocaleString 只是用来显示结果给用户;最好不要在脚本中用来做基本计算,因为返回的结果是随机器不同而不同的。示 例:下面这个例子说明了 toLocaleString 方法的用法。12345var d, s; // 声明变量。d = new Date(); // 创建 Date 对象。s = "Current setting is ";s += d.toLocaleString(); // 转换为当前区域。根据所在地区(中国,欧洲)的不同,返回的日期格式不同return(s); // 返回转换的日期。 数组继承的前面三个方法,在默认情况一都会以逗号分隔的字符串的形式返回数组项(有点争议,valueOf( )返回的类型应该是Array类型,或对象型)而如果使用join( ) 方法,则可以使用不同的分隔符来构建这个字符串。join()方法只接受一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。例子:123var colors = ["red", "blue", "orange"];alert(colors.join("-")); //red-blue-orangealert(colors.join("||")); //red||blue||orange 5.2.3栈方法就是数据结构中的栈。栈是一种LIFO(Last-In-First-Out,后进先出)的数据结构(碟子模型)。栈中项的推入和弹出都发生在一个位置——栈的顶部(数组的尾部)。ECMAScript为数组提供了push( )和pop()方法,以便实现类似栈的行为。push( )(push: v. 推入)方法可以接收任意数量的参数(!),把他们逐个添加到数组的末尾,并返回修改后数组的长度。pop()(pop: n. 啪的一声,数据弹出的声音),把数组末尾的最后一项移除,减少length的值,然后返回那个被移出的项。 5.2.4队列方法队列数据结构的访问规则是FIFO(Firest-In-First-Out,先进先出,派对模型),队列在列表的末端添加项,从列表的前端移除项。实现这两个操作的数组方法是shift(),它能移出数组的第一项,并返回该项。同时将数组长度减1。结合使用shift()和 push()方法,可以像使用队列一样使用数组。 5.2.5重排序方法数组中已经存在两个可以直接用来重排序的方法:reverse()和sort()。 reverse()故名思义会翻转数组项的顺序。数组是被直接覆盖,不是翻转它的副本。在默认情况下,sort()方法按升序排列数组项——即最小的值位于前面,最大的值排在最后面。但sort()的大小即使数组是数字也不是按照数字大小排序的,而是都会先在内部通过toString( )转换为字符串,通过比较字符串按大到小排序。例子:123var values = [0,1,5,10,15];values.sort( );alert(values); //0,1,10,15,5 可见,即使例子中值的顺序没有问题,但sort( ) 方法也会根据测试字符串的结果改变原来的顺序。(其实JS中的sort()方法是按比较值的Unicode顺序进行排序的)这种排序方式很明显不是最佳方案,因此sort()方法可以接收一个比较函数作为参数,以便我们指定哪个值位于哪个值前面。比较函数接受两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等,则返回0,如果第一个参数应该位于第二个参数之后,则返回一个正数。以下就是一个简单的比较函数:123456789function compare(value1,value2){ if(value1<value2){ return -1; }else if(value1>value2){ return 1; }else{ return 0; }} 只要将其作为参数传递给sort()方法即可。例子:123var values = [0,1,5,10,15];varlues.sort(compare);alert(values); //0,1,5,10,15 如果想要产生降序的结果,只要交换比较函数返回的值即可。(把return 1 和return -1交换位置) ✎注意:reverse()和sort()方法的返回值是经过排序之后的数组(不会变成字符串)。对于数值类型或者其valueOf( ) 方法会返回数值类型的对象类型,可以使用一个更简单的比较函数。对于这个函数只要用第二个值减第一个值即可。1234//用两行代码实现数值类型数组的升降排序function compare(value1,value2){ return value2-value1; //return value1-value2就是降序} 由于比较函数通过返回一个小于零,等于零,或大于零的值来影响排序结果,因此减法操作就可以适当地处理所有这些情况。 5.2.6操作方法操作方法就是讲操作数组的各种方法。第一个方法是concat()。可以基于当前数组中的所有项创建一个新数组。具体来说,这个方法会先创建当前数组一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。如果没有给concat()传递参数,它就只会复制当前数组并返回副本。如果传递给concat()方法的是一或多个数组,则该方法会将这些数组中的每一项都添加到结果数组中,如果传递的值不是数组,这些值就会被简单地添加到结果数组的末尾。例子:12345var colors = ["red", "green", "blue"];var colors2 = colors.concat("yellow", ["black", "brown"]);alert(colors); //red,green,bluealert(colors2); //red,green,blue,yellow,black,brown 下一个方法是slice()。它能够基于当前数组中的一或多个项创建一个新数组。slice()方法可以接受一或两个参数。即要返回项的起始位置和结束位置(位置从0开始算)。只有一个参数:slice()返回从该参数指定位置开始到当前数组末尾的所有项。两个参数:返回起始和结束位置之间的项——但不包括结束位置的项。且slice()方法不会影响原始数组。例子:123456var colors = ["red", "green", "blue", "yellow", "purple"];var colors2 = colors.slice(1);var colors3 = colors.slice(1,4);alert(colors2); //green,blue,yellow,purplealert(colors3); //green,blue,yellow ✎注意:如果slice方法的参数中有一个负数,则用数组长度加上该数来确定相应的位置。例如,在一个包含5项的数组上调用slice(-2,-1)与调用slice(3,4)得到的结果相同。如果结束位置小于起始位置,则返回空数组。 第三个是splice()方法。最强大的数组方法,其有很多种用法。splice()的主要用途是向数组的中部插入项,但使用这种方法的方式有如下3种。 删除:可以删除任意数量的项,只需指定2个参数:要删除的第一项的位置和要删除的项数。例如:splice(0,2)会从位置0开始,删除数组中的前两项。 插入:可以向指定位置插入任意数量的项。只需3个参数:起始位置,0(要删除的项数,不删就是0)和要插入的项,如果要插入多个项,可以再传入第四,第五等任意多个项。例如,splice(2,0,”red”,”green”)会从当前数组的位置2开始插入字符串“red”和“green”(“red”会成为在位置2的值,“green”在位置3) 替换:可以向指定位置插入任意数量的项,同时删除一定数量的项,插入的项数不必与删除的项数相等。例如:splice(2,1,“red”,”green”)会删除当前数组位置2的项,然后从位置2开始插入字符串“red”和“green”。“red”在位置2。splice()方法始终都会返回一个数组,该数组中包含从原始数组中删除的项(如果没有删除任何项就返回一个空数组)。下面是三种用法的例子:1234var colors = ["red", "green", "blue"];var removed = colors.splice(0,1);alert(colors); //green,bluealert(colors); //red,返回被删除的那一项 5.2.7位置方法ECMAScript5为数组添加的两个位置方法:indexOf( ) 和lastIndexOf( ) 都接收两个参数:要查找到项,表示查找起点的索引(可选,起点也可以被搜索到)。indexOf()是从前向后查找,lastIndexOf()是从后向前查找。两个方法返回的都是查找的项在数组中的位置。查找不到则返回-1。lastIndexOf()返回的位置也是从头开始计的,虽然是从最后开始找。在用第一个参数与数组中的项比较时,使用的是全等操作符(“===”),非常严格。例子:123456789101112131415var numbers = [1,2,3,4,5,4,3,2,1];alert(numbers.indexOf(4)) // 3alert(numbers.lastIndexOf(4)) // 5alert(number.indexOf(4,4)); //5alert(number.lastIndexOf(4,4)) //3 第二个参数是4,则相当于数组只有前面五项[1,2,3,4,5]以5为最后一项开始找。var person = {name : "Nicholas"};var people = {name : "Nicholas"};var morePlople = [person];alert(people.indexOf(person)); //-1alert(morePeople.indexOf(person))//0 比较难懂的是lastIndexOf()加第二个参数,测试了一下终于自己测出来。lastIndexOf()的第二个参数查找起点的索引也是从头开始计算的,比如第二个参数是2,则相当于整个数组只剩前面的0,1,2.后面的项相当于“不见了”,然后以第2为最后一个位置开始查找数组。支持indexOf( ) 和lastIndexOf( )的浏览器有 IE9+(IE9+的意思用IE9也能成功),Safari 3,Opera 5和Chrome. 5.2.8迭代方法ECMAScript5为数组定义了5个迭代方法。每个方法都接收两个参数:要在每一项上运行的函数和运行该函数的作用域对象(可选)——影响this的值。传入这些方法中的函数(函数由我们自己编写)会接收三个参数(要写在函数的参数栏里):数组的项的值(不能说“数组的项”,因为数组的项包括数组的项的值和位置),项的位置,数组对象(迭代方法是数组对象的方法,对象才有方法,是数组,对象)本身。 以下是这5个迭代方法: every():让数组中的每一项都过一遍every()里的那个函数,全部返回true了,every()方法才返回true。 some():让数组中的每一项都过一遍some()里的那个函数,只要有一个返回true,some()方法就返回true。 filter():筛选器。数组的每一项都过一遍filter()里的函数,只返回一个通过了“考验”的数组。 forEach():对数组运行给定的函数,这个方法没有返回值。 map():对数组每一项过一遍map()里的函数,返回经过这个函数“加工”过的结果组成的数组。 下面是every()和some() 的例子,返回true,false123456789var numbers = [1,2,3,4,5,6];var result = numbers.every(function(item,index,array){ return item>2;})console.log(result); //falsevar result = numbers.some(function(item,index,array){ return item>2;})console.log(result); //true 两个例子的ES6写法:12345let result = numbers.every((item,index,array) => item>2);console.log(result); //falselet result = numbers.some((item,index,array) => item>2);console.log(result); 使用的是ES6的箭头函数,适合简短的函数,不需要命名,箭头后面默认是return,具体看阮一峰的文档。 下面是filter()的例子,返回一个由通过函数筛选条件的数组12345var numbers = [1,2,3,4,5,6];var result = numbers.filter(function(item,index,array){ return item>2;})console.log(result); // [3,4,5,6] 下面是运行map()的例子,返回一个经过map()内函数加工的数组12345var numbers = [1,2,3,4,5,6];var result = numbers.map(function(item,index,array){ return item*2;})console.log(result); // [2,4,6,8,10,12] 下面是forEach()的例子,这个方法(请严谨地说——方法,不是函数,它是数组对象的,方法)没有返回值。本质上相当于对封装了一个for循环给数组。1234var numbers = [1,2,3,4,5,6];numbers.forEach(function(item,index,array){ console.log(item); //会像使用了for循环打印7次}) 亲测在forEach()里对数组的项进行赋值、计算得到的结果不会影响本来的那个数组,甚至在函数内打印出对每项经过计算后的数组,得到的数组也是没有改变的原来的数组。因为函数的参数是按值传递的。例子:1234567var numbers = [1,2,3,4,5,6];numbers.forEach(function(i,u,a){ i=i+1; console.log(i); //每一项都打印出来,都被+1 console.log(a); //打印七次数组,每次都是原来那个数组[1,2,3,4,5,6]})console.log(numbers); //外面的数组没有改变,仍是[1,2,3,4,5,6] 以上every(),filter() ,some() ,map() ,forEach()只在IE9+,Firefox2+,Safari 3+,Opera 9.5+和Chrome有效。 打完上面那句因为函数的参数是按值传递的后发现一个有意思的事情,函数参数是按值传递没错,但数组是对象,函数的参数也是原来那个函数的指针,在函数里改变数组内的值,原来的那个数组也会被改变才对。经过亲自测试,当把数组当作参数传入函数,在函数内对数组内的值+1,在外部打印的数组也是经过+1的了!所以上面好像也不是因为函数的参数是按值传递的这个原因。大概forEach()方法不会返回任何值所以也不会改变任何值吧,而console.log(a)打印出来的就是参数里的那个a,所以是原来那个参数。例子:123456789101112131415161718var numbers = [1,2,3,4,5,6];function add(array){ for (var i = array.length - 1; i >= 0; i--) { array[i]+=1; } console.log(array);}add(numbers); //把数组作为参数传入函数,对每个值+1console.log(numbers); //外部的numbers也被改变[2,3,4,5,6,7]//于此同时用基本类型值number做同样的实验,全局的num在函数内经过计算后在全局打印出来的num还是等于6var num=6;function addOne(num){ num+=1; console.log(num);}addOne(num);console.log(num); //6 5.2.9归并方法ECMAScript5还新增了两个归并数组的方法:reduce()和reduceRight()这两个数组会迭代所有项,然后构建一个最终返回的值。两个方法只有一个区别:一个从头开始归并,一个从后向前归并。两个方法都接收两个参数:一个在每项上调用的函数和作为归并基础的初始值。传进这两个方法的函数接收4个参数:前一个值,当前值,项的索引和数组的对象。这个函数返回的任何值都会作为第一个参数(“前一个值”)自动传给下一项。第一次迭代发生在数组的第二项上(在这句话体会“迭代这个词的意思”),前一个值在第一次迭代时是数组的第一项,第二个参数是数组的第二项。第二次迭代的第一个参数就是第一项和第二项迭代(迭代有可能是加减乘除或其他计算方式)后的值。下面是用reduce()方法求数组uoyou值之和的操作:12345var values = [1,2,3,4,5];var sum = values.reduce(function(prev,cur,index,array){ return prev+cur;});alert(sum); //15 第一次执行回调函数,prev是1,cur是2.第二次,prev是3(1加2的结果),cur是3(数组第三项)。这个过程持续到把数组最后一项访问到。最后返回结果。reduceRight( )方法不讲了区别只是开始的顺序是从后面开始罢了。 支持这两个归并函数的浏览器有: IE9+,Firefox3+,Safari 4+,Opera 10.5+和Chrome。(可以看出这两个方法比上面5个迭代方法要更难兼容。所有浏览器的版本都上了一个数,IE仍是9+才可以访问)。 5.3Date类型要创建一个日期对象,同样使用new操作符和Date构造函数即可。1var now = new Date(); 不向Date构造函数传递函数的情况下,新创建的对象默认自动获得当前日期和时间。如果想根据特定的日期和时间(日期:年月日;时间:时分秒),理论上应该传入表示该日期的毫秒数(即从UTC时间1970年1月1日午夜起至该日期经过的毫秒数)。当然JavaScript不可能这么不人道,ECMAScript提供了两个方法:Date.parse( ) 和Date.UTC( )这两种方法可以让你直接写入正常人类的日期即可使日期对象具备特定的日期和时间。两种方法表示日期的格式不同。而且也不用傻傻地写123//不用这么做!var someDate = new Date(Date.parse("May 25,2004"));var allFives = new Date(Date.UTC(2005,4,5,17,55,55)); Date构造函数已经自带了这两个方法,如果直接写入“May 25,2004”,函数会自动调用Date.parse( )。如果写入的是2005,4,5,17,55,55,构造函数也会自动调用Date.UTC( )。所以不用像上面那样写。正确的写法:12var someDate = new Date("May 25,2004");var allFives = new Date(2005,4,5,17,55,55); ✎注意:第二个日期表示的是2005年5月5日,因为月份是从0开始计算的,0-11。 ECMAScript5添加了Date.now( )方法,返回程序运行到这个方法时的日期和时间的毫秒数,可以用来当计时器用,简化了使用Date对象分析代码的工作(不用专门构建Date对象就可以调用的方法)。例子:12345var start = Date.now( );doSomething();var stop = Date.now();result = stop - start; //通过start-stop可以得出运行doSomething()具体用了多少时间 又是ECMAScript5才发布的方法,所以能用的浏览器包括: IE9+,Firefox3+,Safari 3+,Opera 10.5+和Chrome。不支持这个方法的浏览器中,可以使用+操作符获取Date对象的时间戳,也可以达到同样的目的。12345var start = +new Date();doSomething();var stop = +new Date();result = stop - start; 5.3.1继承的方法与其他引用类型一样,Date类型也重写了toLocaleString()、toString()和valueOf()方法。总而言之这一段就是告诉我们用toLocaleString()和toString()写出来的日期格式在同一个浏览器中是不一样的。恐怖的是根据浏览器的不同,每个浏览器用toString()输出的结果也是不一样的。toLocalString()同理。书里还有一句话:事实上,toLocaleString()和toString()在浏览器上显示格式的差别仅在调试代码时比较有用,在显示日期和时间上没有什么价值。 至于Date类型中的valueOf()方法则根本不会返回字符串,而是返回日期的毫秒表示(ZZ)。 还有一点是:日期是可以比较大小的,下面例子:12345var date = new Date(2007,0,1);var date = new Date(2007,1,1);alert(date1<date2); //truealert(date1>date2); //false 比较早的日期小于比较晚的日期。 5.3.2日期格式化方法介绍了toDateString(),toTimeString() ,toLocalDateString() ,toLocaleDateString() ,toLocaleTimeString() ,toUTCString() ,这几个根据特定实现的格式或特定地区的格式显示的日期格式。与toLocaleString()和toString()相同,以上几个方法的显示格式也会因浏览器的不同有差异。没有哪一个方法能够用来在用户界面中显示一致的日期信息。 5.3.3日期/时间组件方法介绍了一大堆getFullYear()、getMonth()等方法,有用到自然会去百度,不写。 5.4RegExp类型正则网上有很多现成的正则案例,本人真的看到正则头大,所以这部分是跳过的,有兴趣的可以先看看JavaScript正则表达式视频教程-慕课网学习。","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"《JS高级程序设计》第四章 作用域和内存问题","date":"2017-07-03T04:38:37.000Z","path":"2017/07/03/《JS高级程序设计》第四章-变量、作用域和内存问题/","text":"  把2016年寒假写的对《JavaScript高级程序设计》的笔记写在博客上,同时回看加修改,同时也更新到简书上。尽量一天一篇一章。 第四章 变量、作用域和内存问题4.1基本类型和引用类型的值ECMAScript变量可能包含两个不同类型数据的值:基本类型值和引用类型值。基本类型值指的是简单的数据段(Boolean类型、Number类型、String类型、Undefined、Null) 引用类型值指那些可能由多个值构成的对象(Object类型、Array类型、Date类型、RegExp类型、Function类型)>ES6中新增了Symbol,是JavaScript的第七种数据类型。4.1.1动态的属性基本类型值和引用类型值的区别一:对于引用类型值,我们可以为其添加或删除属性和方法,但是基本类型值没有属性和方法。例子:var person = new Object(); var name = “Nicholas”person.name = “Nicholas”; name.age = 27;alert(person.name); //“Nicholas” alert(name.age) //undefined以上代码一创建了一个对象并给他一个name属性,又通过alert访问成功。代码二给字符串name定义了一个age属性,但当我们访问的时候会发现这个属性不存在。这说明只能给引用类型值动态地添加属性,以便将来使用。4.1.2复制变量值基本类型值和引用类型值的区别二:在复制变量的时候,复制基本类型值和引用类型值也是有区别的。如果只是复制基本类型值,那就是简单复制到为新变量分配的位置上没毛病。当复制的是引用类型的值时,同样会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个新的副本实际上是一个指针,复制结束后,两个变量实际上将引用同一个对象。因此,如果复制的是引用类型值,当改变其中一个变量,就会影响另一个变量。例子:var obj1 = new Object();var obj2 = obj1;obj1.name=”Nicholas”;alert(obj2.name); //“Nicholas”变量对象中的变量保存在堆中的对象之间的关系如图:图片来自《JavaScript高级程序设计》可以看到当变量复制后,指针仍然指向一开始的Object,而不是复制出多一个Object。.4.1.3传递参数ECMAScript中所有函数的参数都是按值传递的。(无论参数是引用类型值和基本类型值)。也就是说,把函数外部的值复制给函数内部的参数,就和4.1.2复制变量值的原理一样,把一个变量复制到另一个变量(函数的参数)一样。有不少开发人员在这点会感到困惑,因为访问变量有按值和按引用两种方式,而参数只能按值传递。————————–讨论参数传递的是引用类型值的情况————————–function setName(obj){ obj.name = “Nicholas”;}var person = new Object();setName(person);alert(person.name); //“Nicholas”以上代码创建了一个对象person,这个变量被传递到setName()函数中后被复制给了obj,在这个函数内部,obj和person引用的是同一个对象。换句话说,即使这个变量是按值传递的,obj也会按引用来访问同一个对象(遵循4.1.2的复制变量值原理)。于是当为函数内部为obj添加name属性后,函数外部的person也会有所反映。因为person指向的对象在堆内存中只有一个,而且是全局对象。有很多开发人员错误地认为:在局部作用域中修改的对象会在全局作用域中反映出来,就说明参数是按引用传递的(大错特错)。为了证明对象是按值传递的。看下面的例子:function setName(obj){ obj.name = “Nicholas”; obj = new Object(); obj.name = “Greg”;}var person = new Object();setName(person);alert(person.name); //“Nicholas”这段代码增加了两行,为obj重新定义了一个对象,第二行为该对象定义了一个带有不同值的name属性。如果person是按引用传递的,那么person最后会被自动修改为指向其name属性值为“Greg”的新对象。但是在函数外访问person.name时,显示的值仍然是”Nicholas”。这说明即使在函数内部修改了参数的值,但原始的引用仍然保持未变。实际上,当在函数内部重写obj时,这个变量引用的就是一个局部对象了(这个函数范围的局部对象)。而这个局部对象会在函数执行完毕后立即被销毁。4.1.4检测类型检测变量类型的方法有两种,一种是检测基本类型值的,用typeof,另一种是检测引用类型值的,用instanceof。typeof操作符是确定一个变量是string,boolean,number,undefined的最佳工具,如果变量是对象(根据规定,所有引用类型的值都是Object的实例)或null,则typeof操作符返回的值会是“object”。例子:var s = “Nicholas”, b = true, i = 22, u , n = null, o = new Object(), d = new Date();alert(typeof s); //stringalert(typeof i); //numberalert(typeof b); //booleanalert(typeof u); //undefinedalert(typeof n); //objectalert(typeof i); //objectalert(typeof d); //object因为typeof只能检测基本类型值,检测引用类型值时只会返回object,所以ECMAScript又提供了一个insanceof操作符,用法跟typeof不同,且只返回true 或 false。✎另类的情况使用typeof操作符检测函数时,该操作符会返回”function”。在Safari 5 及之前版本和Chrome 7及之前的版本中使用typeof检测正则表达式时,由于规范的原因,这个操作符也返回“function”。ECMA-262规定任何在内部实现 [ [ call ] ] 方法的对象都应该在应用typeof操作符时返回“function“。由于上述浏览器(Safari 5,Chrome 7)中的正则表达式也实现了这个方法,因此对正则表达式应用typeof会返回“function”。在IE和Firefox中,对正则表达式应用typeof会返回“object”. 如果变量是给定引用类型(根据它的原型链来识别,第6章将介绍原型链。#原书句)的实例那么instanceof操作符就会返回true。例子: alert(person instanceof Object); //变量person是Object吗? alert(colors instanceof Array); //变量colors是Array吗? alert(pattern instanceof RegExp); //变量pattern是RegExp吗? //亲测左右两边位置不可互换,互换不会出现提示框 因为根据规定,所有引用类型的值都是Object的实例,因此把Date,Array,RegExp等引用类型值用instanceof 与Object验证时,始终都会返回true。用instanceof操作符检测基本类型值时,该操作符时钟返回false,因为基本类型不是对象。 4.2执行环境及作用域 作用域链重要的一点就是内部执行环境可以使用其外部环境的变量和函数,并且可以改变那个变量的值,只要那个变量不是被当作参数传进去的而是直接使用的。(当作参数传入的是按值传递,改变的是复制出来的变量,不会改变原来的变量) 执行环境(execution context)和作用域其实超级简单。每个执行环境都有一个与之关联的变量对象(variable object),环境变量中定义的所有变量和函数都保存在这个对象中。但是我们无法用代码访问到这个变量对象。但解析器在处理数据时会在后台使用它。 全局执行环境是最外围的执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为是window对象(第七章将详细讨论),因此,所有全局变量和函数都是作为window对象的属性和方法创建的(window对象是个变量对象,全局变量和函数是它的属性和方法)。某个执行环境(例如一个函数)中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出(网页关闭或浏览器关闭时才被销毁)) >   在Node.js中的全局执行环境是global 每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权交给之前的执行环境。ECMAScript程序中的执行流正是由这个方便的机制在控制。 当代码在其中一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的最前端,始终都是当前执行代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中不存在)。作用域链中的下一个变量对象来自包含它的外部环境,而再下一个变量对象则来自下一个包含环境。这样,一直延伸到全局执行环境; 标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程从作用域链最前端开始,然后逐级向后回溯,直到找到标识符为止(如果找不到,就会发生错误) 例子:var color = \"blue\"; function changeColor(){ if(color == \"blue\"){ color = \"red\"; } } changeColor(); alert(color); // \"red\" 在这个例子中,函数changeColor( )的作用域链包含两个对象:它自己的变量对象(其中定义着arguments对象)和全局环境的变量对象。可以在函数内部访问变量color,就是因为可以在这个作用域链中找到它。 此外,在局部作用域中定义的变量可以在局部环境中与全局变量互换使用,如下面这个例子所示: var color = \"blue\"; function changeColor(){ var anotherColor = \"red\"; function swapColors(){ var tempColor = anotherColor; anotherColor = color; color = tempColor; //这里可以访问color,anotherColor 和 tempColor } //这里可以访问color和anotherColor,但不能访问tempColor swapColors(); } //这里只能访问color changeColor();以上代码涉及三个执行环境:全局环境、changeColor()的局部环境和swapColor() 的局部环境。swapColor的局部变量中有一个变量tempColor,该变量只有在swapColor环境中能访问到,但是swapColor()内部可以访问其他两个环境中的所有变量。 越在内部的局部环境,作用域链越长。对于这个例子中的swapColor()而言,其作用域链中包含3个对象:swapColor( )的变量对象、changeColor()的变量对象和全局对象。swapColor()的局部环境开始时会现在自己的变量对象中搜索变量和函数名,如果搜索不到则再搜搜上一级作用域链。changeColor()的作用域链中只包含两个对象:它自己的变量对象和全局对象。也就是说,它不能访问swapColor()的环境。 4.2.1延长作用域 有些语句可以在作用域链前端临时增加一个变量对象,该变量对象会在代码执行后被移除。在两种情况下会发生这种现象,具体来说,就是当执行流执行到下列任何一个语句时,作用域链会得到增长 * try-catch语句的catch块 * with语句 这两个语句都会在作用域链的前端添加一个变量对象。对with语句来说,会将指定的对象添加到作用域链中,对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。例子: function buildUrl(){ var qs = \"?debug=true\"; with(location){ var url = href + qs; } return url; } ✎添加一个with语句块的知识点当在with语句块中使用方法或者变量时,程序会检查该方法是否是本地函数,变量是否是已定义变量,如果不是,它将检查伪对象(with的参数),看它是否为该对象的方法,属性。如上面例子with语句块中的href,本地无定义,则with语句块会自动加上location.href,所以href实际上为href。这个就是with的功能。with 语句是运行缓慢的代码块,尤其是在已设置了属性值时。大多数情况下,如果可能,最好避免使用它。在此,with语句接收的是Location对象,因此其变量对象中就包含了location对象的所有属性和方法,而这个变量对象被添加到了作用域链的最前端,buildUrl()函数中定义了一个变量qs。当在with语句中引用变量href时(实际引用的是location.href)。可以在当前执行环境的变量对象中找到。当引用变量qs时,引用的则是在buildUrl( )中定义的那个变量,而该变量位于函数环境的变量对象中。至于with语句的内部,则定义了一个名为url的变量,因而url就成了函数执行环境的一部分,所以可以作为函数的值被返回。 4.2.2没有块级作用域 ✎添块级作用域任何一对花括号中的语句都属于一个块,在这之中定义的所有变量在代码块之外都是不可见的,我们称之为块级作用域。作用域有两种,块级作用域和函数作用域讲到这就好理解。JS没块级作用域就是说在for循环和if语句块中定义的变量是可见的,可以被外部使用的,但像其他的语言Java,C,C#语言中,在for,if语句中定义的变量在语句执行完毕之后就会被销毁。但在JavaScript中,if语句中的变量声明会将变量添加到当前执行环境中。注意只是当前执行环境,如果for循环是在一个函数里,则定义的i在函数里是确定的数,在全局环境中仍然是not defined。例子: if(true){ var color = \"blue\"; } alert(color) //\"blue\" for(var i=0; i","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"高程","slug":"高程","permalink":"https://millionqw.github.io/tags/高程/"}]},{"title":"第一个hexo博客终于建成","date":"2017-07-02T13:20:13.000Z","path":"2017/07/02/第一个hexo博客终于建成/","text":"折腾了半个下午加半个晚上,终于建成了自己的第一个hexo博客,也是自己的第一个博客ヽ(^・ω・^)丿 从开始接触技术以来,就一直知道技术人员都要有一个自己的博客,接触了两年技术,学习过程中也有把知识记录下来的习惯,但是都是记录在印象笔记里,也在博客园开过一个博客,但一直都没在那里动过笔,大概就是等一个用Hexo博客的机会吧,如果我早在博客园写日记,估计就懒得搬过来了。 虽然hexo的SEO并不好(看到别人的博客都是在简书上转过来的),但是有了博客就有了一个精神角落,放自己的技术,也督促自己记录下学到的东西吧。 第一篇博文,就记录一下自己是怎么搭建这个博客的吧! 真的想动手搭博客是6月以来看到github上一些大神的博客,看到他们的博客都好漂亮,后来才知道是用hexo搭起来的(之前一直觉得“自己的博客”应该是从html到css都是自己写的),后来也看了一些搭hexo博客的文章,终于在6月底考完试后开始在今天做自己的博客。 首先,是阅读了 @代码咖啡 在简述上发的这篇 20分钟教你使用hexo搭建github博客 文章非常浅显易懂,这里在这篇文章的基础上补上一些我遇到的问题和我是怎么解决的: 文章中的一个步骤: 这里的终端(terminal)指的是git这个版本控制系统,需要下载在电脑中,具体的使用教程看廖雪峰大大的教程,但是在搭博客过程中我们知道怎么安装就好,然后把git bash当命令行用。文章中的命令行都是在git bash输入的。 后面的node.js和hexo的安装因为自己有学一点点node的知识所以够用,不懂的可以留言,我可以帮助你解决问题! 还有一个是初始化博客后博客文件夹的数量会与作者的不一样,貌似会少一两个,不过不要紧接着进行。 后面的 最后的timezone不要因为你不是上海的就改成你当地的地名,根据标准,中国区只能写Asia/Shanghai、Asia/Hongkong、Asia/Taipei、Asia/Harbin. 之后@代码咖啡 又发了一篇博文 【干货】2个小时教你hexo博客添加评论、打赏、RSS等功能 是对hexo博客扩展功能的补充,他使用的博客主题是NexT,所以就这个主题做了介绍。 我使用的是yilia这个主题,似乎很多人用的都是这个类似的主题,非常受欢迎,github上有3.6k+个star。使用的时候要对配置进行一些修改。 注意在README里说到的配置_config.yml是配置themes/yilia下的_config.yml,不是根目录下的_config.yml。可以在主题里的_config.yml加上你的微博,github,知乎等链接,如果没有的,可以使用#井号注释掉。 另外当你使用这个主题的时候,点开左侧边栏的”所有文章/关于我”,打开的侧边栏会提示缺少模块,按提示下载模块后在主目录下的_config.yml配置给出的那一段配置即可,注意缩进,_config.yml的配置如果缩进错误是不生效的。 使用博客中出现的问题及解决办法 刚开始用这个主题的时候是没有头像的,一开始把图片放在source的img下用相对路径引入发现没有效果,后来用的是微博的图床,一些人似乎用了图床后也显示不出来,解决方法参考:解决头像的问题 当你更新了themes下的_config.yml且git pull后用了hexo clean hexo g //产生静态内容 hexo d //发布到github 仍然没有反应的时候,不要急,确认自己的配置无误的话,只要多刷新几遍即可,应该是网络原因造成的更新延迟。几秒后更改就会生效 我出现的问题是头像可以显示,但是样式不好看: 用F12修改了一下样式,参考了上一个问题的解决方法,在themes\\yilia\\layout_partial下找到left-col.ejs文件,在第6行中的 <a href="/" class="profilepic"> <img src="<%=theme.avatar%>" class="js-avatar"> </a> 在img标签里添加一行行内样式: style=”position:absolute; left:-30%; top:-5%; max-width:150%; left和top的值根据你自己的图片位置调整。 如果在移动端上头像的显示仍然不理想,可以在themes\\yilia\\layout_partial下找到mobile-nav.ejs文件,在第十行<img src="<%=theme.avatar%>" class="js-avatar">中添加一句style=”min-width:130%;”即可,具体数值仍然根据自己的情况调整。 几个常用的hexo命令 防止忘了又要百度,把几个命令记下来。 新建文章 hexo new \"文章标题\" 清除缓存、生成静态文件、发布 hexo clean hexo g hexo d 修改主题配置后更新 cd themes/yilia git pull 要总结的似乎就这些,接下来计划把上个寒假花了10几天写的《Javascript高级程序设计》笔记搬到上面来,可以的话,一天整理一篇。同时回顾学过的东西,再添加修改原有的笔记。最后,晚安~","tags":[{"name":"前端","slug":"前端","permalink":"https://millionqw.github.io/tags/前端/"},{"name":"生活","slug":"生活","permalink":"https://millionqw.github.io/tags/生活/"}]},{"title":"文章标题","date":"2017-07-02T08:20:23.000Z","path":"2017/07/02/文章标题/","text":"你好,欢迎来到我的个人技术博客。","tags":[]}]