diff --git a/.gitignore b/.gitignore deleted file mode 100644 index f1861eb..0000000 --- a/.gitignore +++ /dev/null @@ -1,18 +0,0 @@ -lib-cov -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.gz - -pids -logs -results - -npm-debug.log -node_modules -dist -.sass-cache -.idea \ No newline at end of file diff --git a/Browser_Scripting/DOM_Scripting/EventAPI.html b/Browser_Scripting/DOM_Scripting/EventAPI.html new file mode 100644 index 0000000..6a75b20 --- /dev/null +++ b/Browser_Scripting/DOM_Scripting/EventAPI.html @@ -0,0 +1,143 @@ + + +
+事件机制无时无刻伴随着JavaScript开发。在浏览器环境里有各种各样的事件。正是这些事件驱动着脚本的运行。
+JavaScript的事件机制也是一个标准的观察者模式(Observer Pattern)的应用。通过抽象出订阅者(Subscriber)和发布者(Publisher)来达到接偶某个事物的目的。这样一种普遍的设计模式,不仅仅可以用在DOM里的事件机制,应用程序的脚本也可以构建自己的事件模型,产生自定义的事件来推动应用程序的进行,例如Backbone.Event就是一种常见的EventEmitter实现,它允许任何对象进行事件派发,而不局限于DOM中的文档元素。
作为浏览器中最完善也是最复杂的DOM事件,自然是JavaScript程序员首先要掌握的。
+DOM内的事件传播(或事件派发)总是沿着其文档节点和其附元素所构成的有序列表进行的。
+在应用JavaScript进行DOM编程时时,事件的产生源头很多,同时,事件的订阅也存在多种形式。从事件源头看,拥有能够接受事件DOM事件的对象,都实现了EventTarget接口。下面这三个方法,大家应该都不陌生。
EventTarget.addEventListener ()
+EventTarget.removeEventListener ()
+EventTarget.dispatchEvent ()
+DOM内的文档元素,大多都有如下的继承状态:
+EventTarget -> Node -> |-> Element -> HTMLElement -> ...
+ |-> Document
+ |-> ...
+如上所述,大多素我们获取的节点,都是有相关的事件方法的。但由于历史原因,有大量DOM0时期(即没有标准化)的代码,可以使用以下两种办法注册事件监听:
+通过HTML属性
+ <button onclick="alert('Hello world!')">
+通过DOM元素属性
+ myButton.onclick = function(event){alert('Hello world');};
+事件可以通过调用EventTarget.dispatchEvent()进行派发,浏览器自己也需要遵循该方法的行为自动派发相应事件。整个事件派发的流程如下:
首先,浏览器实现要决定时间对象的传播路径。事件传播路径是事件将要通过的节点组成的有序列表。在DOM里面,这个事件传播路径的最后一点就是事件对象(Event Target)本身,其之前的元素就是事件对象的祖先元素。
+
关于事件派发的路径,有两个特性:
+一旦这个传播路径被确定,即使后续的操作改变了文档结构,也不会影响这个有序列表中的内容,更不会影响事件的派发
+事件处理函数(callback、handler、subscriber)的处理过程中出现了没有捕获的异常,也不会影响事件的派发
+事件派发的过程,本身有三个阶段:
+一个事件对象有很多相关的属性,来标记这三个阶段的状态,例如Event.bubbles描述一个事件是否可以经历冒泡阶段,Event.eventPhase来描述正在传播的这个事件正处于哪个阶段。
一个事件对象也有相关的方法对事件传播产生影响,例如Event.stopPropagation()会阻止事件的继续传播。
接着,浏览器实现会确定派发路径上每个对象所拥有的事件监听者(event listeners)。此时确定的监听者列表,在后续的操作中也不会被改变。
+最后,浏览器实现会按照如下三个条件来决定向哪些事件监听者派发事件:
+Event.stopImmediatePropagation()来立刻停止事件传播当一个事件完成了所有传播周期后,它的Event.currentTarget必须设置为null,Event.eventPhase必须为0。事件的其他属性保持不变。
事件派发是可递归产生的,如果在事件处理函数中,浏览器被要求派发新的事件,新产生的事件派发会被同步执行,并且只有在新的事件派发完毕后,之前的事件传播才会继续。
+可以被阻止的事件(Event.preventDefault()),通常包含有一些浏览器默认行为。当事件被取消,这些默认行为会被抛弃。有一些事件的默认行为是在事件传播之前就执行了的,那么这些行为所产生的后果将被重置。事件的Event.cancelable描述该事件是否可以被取消;事件的Event.defaultPrevented描述一个事件的默认行为是否已经被阻止。如果一个事件是有脚本派发的,那么EventTarget.dispatchEvent方法的返回值也会反应这个事件是否被阻止执行默认行为。
激活事件(Activataion event)是有用户操作或者另外一个事件连带触发的一种事件。激活事件本身比较抽象,而且大多数浏览器都不支持,但是它和时间的默认行为相关。
+例如,当用户在已经聚焦(Focused)的链接上敲击Enter键,那么页面会跳转或重新定位。此时,激活事件的触发者(Activation Trigger)是keydown事件,激活行为(Activation Behavior)是链接的跳转。
有趣的是,当激活事件并不是由一个click事件产生的,那么激活事件的默认行为会产生一个模拟的click事件,来描述一个鼠标的左键点击。即,除了keypress事件之外,还会产生一个click事件。
想象一下语音控制和触屏交互下的网页浏览场景,这样的做法是有道理的,也保证了大代码的兼容性。
+有用户交互DOM本身发生变化所产生的事件是可信事件。由DocumentEvent.createEvent()方法创建的事件、Event.initEvent()方法初始化的事件、EventTarget.dispatchEvent()方法派发的事件以及合成事件都是不可信的。事件的Event.isTrusted描述一个事件是否可信。大部分不可信事件不允许触发默认行为,click事件和DOMActivate事件除外。
常见事件分类
+eval函数Function构造函数setTimeout和setInterval函数onclick等)最基本,最常用的脚本引入方式。例如:
+<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
+尽管在HTML4和XHTML里面,要求开发者使用type属性来制定脚本的类型。但是主流浏览器都默认认为脚本类型是text/javascript。
在HTML5的规范内1,script标签的type属性是完全可选的。
eval is evileval有访问本地scope的权利var a = 1;
+eval("a=2");
+a === 2; // ==> true
+new Function(arg1, arg2, ..., fnStr)Function3构造函数本质是创建一个函数对象;其创建的函数执行也并不能访问其所在创建环境的闭包,只能访问本地作用域(local scope)和全局作用域(global scope)Function()和new Function()效果一样(function() {
+ var a = 1;
+ var func = new Function("a=2");
+ func();
+ a === 2; // ==> false
+}());
+a === 2; // ==> true
+setTimeout("alert('haha')", 0);
+这个和eval有异曲同工之妙,对作用域的访问也是类似的。
+另外要说名,以上几点,除了script标签的方法之外,其他方法都在strict模式4下不可用。
<a href='#hello' onclick="alert(this.href)">Say hello</a>
+这样如同在click事件的Target Phase运行了一个回调。this指向目标元素本身。
利用MessageChannel等新特性可以触发一些函数的执行5。也许JavaScript的其他的角落也有不少其他执行脚本的入口吧。
+http://www.w3.org/html/wg/drafts/html/master/scripting-1.html#the-script-element
+↩https://developer.mozilla.org/en-US/docs/functional-javascript/First-class_citizen
+↩https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function
+↩https://github.com/kriskowal/asap/blob/master/asap.js
+↩这里详细聊聊和script标签相关的脚本执行顺序。
+几个首要特性:
+defer或async属性)的会阻止文档渲染。相关脚本会立即下载并执行。document.currentScript可以获得当前正在运行的脚本(Chrome 29+, FF4+)假设如下简单代码1,最终会产生三个alert依次为“A”、“B”、“C”。
+<!-- HTML Code -->
+<script>alert("A");</script>
+<script>alert("B");</script>
+<script>alert("C");</script>
+我们再考虑有网络请求的情况2:
+<!-- HTML code -->
+<script src="https://snipt.net/raw/7b08744009c450e07c0bfc1d606fc72e/"></script>
+<script src="https://snipt.net/raw/a2e8c05c1f6fc0e47d259aa899304e89/"></script>
+<script src="https://snipt.net/raw/4fab3017d3d46cbfc4bbd88aab006650/"></script>
+三个文件都需要先下载再运行,且第二个文件的尺寸远大于另外两个文件。但结果依然是弹出三个alert,内容分别是”A”、”B”、”C”。
+从上面两个例子,可以充分了解到script标签的柱塞式执行。
+async属性是HTML5的新特性3,这意味着其兼容性并不乐观(IE10+)。
+async表示该script标签并不柱塞,也不同步执行。浏览器只需要在脚本下载完毕后再执行即可——不必柱塞页面渲染等待该脚本的下载和执行。
+如下代码4,会得到三个alert,但是alert的内容分别是”A”,”C”,”B”。
+<!-- HTML code -->
+<script src="https://snipt.net/raw/7b08744009c450e07c0bfc1d606fc72e/"></script>
+<script src="https://snipt.net/raw/a2e8c05c1f6fc0e47d259aa899304e89/" async=true></script>
+<script src="https://snipt.net/raw/4fab3017d3d46cbfc4bbd88aab006650/"></script>
+可以看到,第二个script标签在加入async并没有阻止后续文档解析和脚本执行。
考究这个属性产生的原有,其实有大量的脚本加载器在做这样的事情:
+var script = document.createElement("script");
+script.src = "file.js";
+document.body.appendChild(script);
+不难想象,通过脚本异步插入的script标签达到的效果和带async属性的script标签是一样的。换句话说,由脚本插入的script标签默认是async的。
+另外,对內联脚本设置async属性是没有意义的,也不产生其他效果。其包含的脚本总是立即执行的。
+带有defer属性的脚本,同样会推迟脚本的执行,并且不会阻止文档解析。就如同这个脚本,放置到了文档的末尾(</body>之前)。
如下代码5的宏观现象和加了async属性的例子是一样的,都会得到”A”、”C”、”B”的三个alert。但是其原理是不一样的。
+<!-- HTML code -->
+<script src="https://snipt.net/raw/7b08744009c450e07c0bfc1d606fc72e/"></script>
+<script src="https://snipt.net/raw/a2e8c05c1f6fc0e47d259aa899304e89/" defer=true></script>
+<script src="https://snipt.net/raw/4fab3017d3d46cbfc4bbd88aab006650/"></script>
+defer属性是会确保脚本在文档解析完毕后执行的——即使这个脚本在文档解析过程中就已经下载完毕变成可执行的状态,浏览器也会推迟这个脚本的执行,直到文档解析完毕6,并在DOMContentLoaded之前7。
+同时,带有defer的脚本彼此之间,能保证其执行顺序。
+注意,defer属性并不是每个浏览器支持,即便支持的浏览器,也会因为版本不一样导致具体行为不一致。另外,大家可以通过将script标签放置到文档末尾这种简单的做法达到defer属性一样的效果。
+defer属性早在IE4就被支持,但是这个defer属性和现代浏览器的行为是有区别的。只有IE10以上,才开始按照标准执行defer属性。
+参考W3C的官方文档8,defer和async两个属性是可以互相影响的:
+++There are three possible modes that can be selected using these attributes. If the async attribute is present, then the script will be executed asynchronously, as soon as it is available. If the async attribute is not present but the defer attribute is present, then the script is executed when the page has finished parsing. If neither attribute is present, then the script is fetched and executed immediately, before the user agent continues parsing the page.
+
简单的归纳:
+规范里没有提到两种属性都有时的效果,但这是文档中被允许的。这样的具体效果会在后面讨论。
+docuemnt.write允许向打开的文档流中写入文档内容;内嵌到HTML里面的docuemnt.write可以就地添加文档内容。考虑到docuemnt.write写入script标签的情况9:
+<!-- HTML code -->
+<script src="https://snipt.net/raw/7b08744009c450e07c0bfc1d606fc72e/"></script>
+<script>document.write("\<script src=https://snipt.net/raw/a2e8c05c1f6fc0e47d259aa899304e89 \/\>\<\/script\>");</script>
+<script src="https://snipt.net/raw/4fab3017d3d46cbfc4bbd88aab006650/"></script>
+观察到执行顺序和普通的script标签没有区别。即使你插入的标签带有async或defer,其行为也是没有区别的。
+让人纠结的是反过来10使用。由于第二个脚本是通过document.write写入的。被延迟的脚本在执行时,document已经关闭,document.write是没有任何效果的。所以,不管使用defer还是async,第二个脚本始终没有运行。
http://www.w3.org/html/wg/drafts/html/master/scripting-1.html#attr-script-async
+↩http://peter.sh/experiments/asynchronous-and-deferred-javascript-execution-explained/
+↩https://www.webkit.org/blog/1395/running-scripts-in-webkit/
+↩http://www.w3.org/html/wg/drafts/html/master/scripting-1.html#attr-script-defer
+↩本文将简单列举ES5的核心特性。ES5多半是扩展原生对象的功能,让Object、Array、Function更加强大。其他的特性包括strict mode和一下期待已久的工具方法(例如JSON.parse等)。
ES5的大部分特性1都在主流浏览器(IE9+)中支持了。而且大部分特性,都可以通过JavaScript垫片(pollyfill)在运行时环境实现2。
+所有对象操作中,如果o不是Object类型,将会抛出TypeError异常。
获取给丁对象的prototype对象。等价于以前的o.__proto__。
获取对象描述。和Object.defineProperty的相关方法。
获取自有属性名列表。结果列表将不包含原型链上的属性。
+以给丁对象o为prototype创建新的对象并返回。如果对象描述p存在,就使用其定义刚创建的对象(类似调用Object.defineProperties(obj,p))。
根据规则attrs定义对象o上,属性名为p的属性
根据对象描述props来定义对象o,通常props包含多个属性的定义。
一个对象在默认状态下,
+Object.seal会改变这两个特性,既不能扩展新属性,也不能修改已有属性的特性。
将对象的每个自有自有属性(own property)做如下操作:
+writable特性置为falseconfigurable特性置为false同时,该对象将不可扩展。可见,该方法比Object.seal更加严格的限制了对一个对象的未来改动。
将对象置为不可扩展。
+判断一个对象是否sealed:
configurable特性为true,则返回falseextensible的,那么返回falsetrueconfigurable或writable特性为true,则返回falseextensible的,那么返回falsetrue判对一个对象是否可扩展。
+返回对象o的所有可枚举(enumerable)属性的名称。
检查对象是否是位于给定对象v的原型链上。
检查一个对象上的属性p是否可枚举。
判断a是否为为真正的Array。
使用“严格等”来判断元素e在数组中的索引号。一个可选的搜索起点i。
获取元素e在数组中最后出现的位置。起始位置i为可选。
测试数组中的每个元素都满足测试t。之后介绍的所有数组遍历方法,都支持一个可选的上下文对象c,可以灵活设置回调函数的执行上下文。传递给数组的测试函数、遍历函数通常有如下签名:
function(item, index, array) {}
+测试数组中是否有元素满足测试t。
使用函数f遍历每个数组的元素。
使用函数f修改每个数组的每个元素。按顺序收集f的每个返回值,并返回这个新组成的数组。
收集通过函数测试f的书组元素。
从左向右,使用函数r聚集数组的每个元素。可以可选的制定一个初始值v。
Array.prototype.reduce的从右向左的版本。
去掉字符串两头的空白符和换行符。
+//property access on strings
+"abc"[2] === "b"
+为了指定当前函数的上下文对象和运行参数,该函数创建一个新的函数,保留给定的this对象和运行参数。
根据rfc462722标准解析JSON文本。
+将指定的对象obj序列化为JSON文本。
获取当前时间距1970.1.1 00:00:00的毫秒数。
根据ISO860123生成时间字符串。
+(new Date).toISOString()
+'2014-04-02T08:31:53.049Z'
+http://kangax.github.io/es5-compat-table/
+↩https://github.com/es-shims/es5-shim
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/GetPrototypeOf
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/create
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperty
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperties
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/seal
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/freeze
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/preventExtensions
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isSealed
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isFrozen
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isExtensible
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isPrototypeOf
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/lastIndexOf
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/reduce
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/reduceRight
+↩https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
+↩http://www.ietf.org/rfc/rfc4627.txt
+↩http://en.wikipedia.org/wiki/ISO_8601
+↩http://stackoverflow.com/questions/8099270/use-of-reserved-words-in-javascript
+↩http://ejohn.org/blog/javascript-getters-and-setters/
+↩ES6比较ES5新特性更多。新加入的特性大致氛围三类:
+由于新的语法特性非常复杂,本篇只描述部分ES6中新加入的API。之后后分篇描述目前已经比较成熟的语法特性(例如Generator和Iterator)。
+从UTF16代码转换字符。这里笔者也不太清楚,应该和UTF编码有关1。
+从字符串的字符上取CodePoint。
+"abc".repeat(2) // "abcabc"
+判断字符串是否以a开头;检索的起始位置p是可选的。
判断字符串是否以a结尾;检索的起始位置p是可选的。
判断字符串是否包含子串a;检索的起始位置p是可选的。
根据类数组对象arrayLike创建数组;一个可选的map方法和其上下文对象thisArg。
从给定参数创建数组。
+寻找通过指定函数cb测试的第一个元素。
同上,但返回该元素的索引号。
+在数组索引s和e之间添入多个元素v。
获取对象o上属性p的特性描述对象。在搜寻属性时,不在原型链上搜索。
获取对象o上属性p的特性描述对象。
获取对象自身上可枚举和不可枚举的键名数组。注意,该方法会返回那些enumerable属性已经设置为false的propety。
+检测两个给定对象的值是否相同。该方法不会进行如同==操作符那样去进行数值转换。与===也有细微差别。仅当符合下面任意条件才返回true:
undefinednulltrue或falsenumber,并且满足以下任一条件:+0-0NaNNaN,并且数值一样将对象o的原型修改为proto。和对象的__proto__属性行为一致。修改单个对象的prototype一般是不被推荐的。
类似underscore和lodash的_.extend。将多个对象的值合并到一个对象。
数字和算数的API复杂而且不常用,但是却必备。
+判断数字是否为有穷。判断过程不尝试将参数转换为number。
Number.isFinite(Infinity); // false
+Number.isFinite(NaN); // false
+Number.isFinite(-Infinity); // false
+
+Number.isFinite(0); // true
+Number.isFinite(2e64); // true
+判断是否为正整数。
+不将参数强制转行为number。判断是否确实为NaN。
判断是否为在MAX_SAFE_INTEGER范围内的正整数。这里说明一下,NUMBER.MAX_SAFE_INTEGER是2^53-1。NUMBER.MAX_VALUE是1.7976931348623157 × 10308,这是IEE754中定义的double的最大值[^13]。
一个常量,代表正整数1与大于1的最小值之差。大约为: 2.2204460492503130808472633361816 x 10^16。
CountLeadingZeroes32。计算一个数字在32位无符号位整形数字的二进制形式开头有多少个0。
以32位正数的乘法方式来计算给定参数。该方法的一种可能的JavaScript实现:
+function imul(a, b) {
+ var ah = (a >>> 16) & 0xffff;
+ var al = a & 0xffff;
+ var bh = (b >>> 16) & 0xffff;
+ var bl = b & 0xffff;
+ // the shift by 0 fixes the sign on the high part
+ // the final |0 converts the unsigned value into a signed value
+ return ((al * bl) + (((ah * bl + al * bh) << 16) >>> 0)|0);
+}
+判断一个数的符号位
+Math.sign(3) // 1
+Math.sign(-3) // -1
+Math.sign("-3") // -1
+Math.sign(0) // 0
+Math.sign(-0) // -0
+Math.sign(NaN) // NaN
+Math.sign("foo") // NaN
+Math.sign() // NaN
+lg(x)
+log2(x)
+ln(1+x)
+e^x-1
+计算给定参数的平方平均数
+function trunc(x) {
+ return x < 0 ? Math.ceil(x) : Math.floor(x);
+}
+返回数值的最接近的单精度浮点。
+求x的立方根
+ES标准14在快速发展之中,部分讨论15都已经扩展到ES8的规划之内。就像W3C的新版标准一样,他们的出现往往被开发者轻视。前段开发者以兼容性、实用性为借口选择性忽略,但是不知不觉中,这些开发者已经成为了守旧者,那些曾经被视为华而不实的Canvas、ApplicationStorage、Websocket等技术,如今已是大量在线应用的基础设施。
+ES6标准中部分已经被广泛应用到node应用的开发之中,也许不久的未来,nodejs的开发者所写的JavaScript已经和浏览器端开发者使用的JavaScript是两种不同的语言了。
+ES6不少API层面的特性可以通过shim16进行兼容性支持。部分语法特性,通过二次编译17,可以支持。所以,已经不是找理由不了解ECMAScript的时候了。
+http://stackoverflow.com/questions/3744721/javascript-strings-outside-of-the-bmp
+↩https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
+↩https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
+↩https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/contains
+↩http://people.mozilla.org/~jorendorff/es6-draft.html#sec-22.1.2.1
+↩http://people.mozilla.org/~jorendorff/es6-draft.html#sec-22.1.2.1
+↩https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
+↩http://people.mozilla.org/~jorendorff/es6-draft.html#sec-22.1.3.6
+↩https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
+↩https://people.mozilla.org/~jorendorff/es6-draft.html#sec-number.issafeinteger
+↩http://en.wikipedia.org/wiki/Double-precision_floating-point_format
14: * http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts
http://esdiscuss.org/topic/es6-es7-es8-and-beyond-a-proposed-roadmap
+↩https://github.com/paulmillr/es6-shim/
+↩https://github.com/addyosmani/es6-tools
+↩ES6新加入的数据类型有:
+ +这些数据结构的支持并不广泛,在写这篇文章的时候。仅有新版本的Firefox和Node v0.11.x以上版本(开启--harmony参数后)支持。
提供传统意义上的Map。支持任意对象作为key。
+new Map(iterable)
iteralbe是Array或其他可枚举的对象,其每个元素是key、value的2元数组。
+重要的属性和方法:
+传统意义上的Set。
Weak开头的Set和Map不对key持有引用,不影响GC。因此,他们没有办法对自身entries的key进行直接的枚举。
+构造函数和普通的Map相同:
+new WeakMap(iterable)
new WeakSet(iterable)
类似Ruby的Struct的,但是目前没有任何引擎实现。
+var proxy = Proxy(target, handler);
+将target的函数调用转向到handler之上。目前除了Firefox支持,没有其他任何JavaScript引擎支持。
笔者还在理解中。目前新版的Chrome和node支持。
+原生版本的Promise API,有关Promise的内容,会在另外一篇文章内详细说明: JavaScript Promise。
+Proxy是JavaScript元编程的一道大门。JavaScript在语言层面无法去重载操作符,但是通过Proxy API,我们可以彻底的修改一个对象的各种行为。这种强大的行为已经在node --harmony和Firefox中支持了。
待补充。可以参考:
+http://wiki.ecmascript.org/doku.php?id=harmony:typed_objects
+↩https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
+↩http://people.mozilla.org/~jorendorff/es6-draft.html#sec-symbol-constructor
+↩https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
+↩ES6包含了很多万众期待的特性支持:
+里面众多的特性都是让JavaScript看起来更规范的好东西,但是大部分都没有被广泛支持。我们仅介绍其中已经至少被一种浏览器和node --harmony下支持的。
在写这篇文章的时候,有如下特性是较为广泛支持的:
+ +对,就这么多了。前三个是为了解决变量声明、定义的问题,而最后一个则影响最大。会在单独篇幅中介绍。下文只介绍前三个特性。
+var is scoped to the nearest function block (or global if outside a function block)let is scoped to the nearest enclosing block (or global if outside any block),很多文献、书籍都建议将for循环的起始变量i、len等放置到函数作用于的顶部声明,以避免后续变量持续存在所造成的迷惑。
function() {
+ for(var i=0,len=5;i<len;i++) {
+ //body
+ }
+ console.log(i,len);=> 5,5
+}
+这是因为ES5的JavaScript的不支持块级作用域,变量仅仅被限制到函数作用域内。
+注意在node中,你需要同时加入--harmony和--use-strict来启动,才会支持let。否则会报错: SyntaxError: Illegal let declaration outside extended mode。
在ES6内,可以通过let来定义块级作用域的变量:
+function() {
+ for(let i=0,len=5;i<len;i++) {
+ //body
+ }
+ console.log(i,len) // throw Reference Error
+}
+最后一个,函数定义的作用域问题:
+function f() { console.log('I am outside!'); }
+(function () {
+ if(false) {
+ // What should happen with this redeclaration?
+ function f() { console.log('I am inside!'); }
+ }
+
+ f();
+}());
+如上代码,在ES5时代,每个浏览器都会得出不同的结果。但是ES6中,函数定义只在块级作用域内有效,结果很明确。
+const关键字定义一个块级作用域的常量变量。
+const a = "You shall remain constant!";
+
+// SyntaxError: Assignment to constant variable
+a = "I wanna be free!";
+yield后面有一连串有关Generator和Iterator的内容,会在另外一片文章内详细介绍: JavaScript Generator。
本文从异步风格讲起,分析JavaScript中异步变成的技巧、问题和解决方案。具体的,从回调造成的问题说起,并谈到了利用事件、Promise、Generator等技术来解决这些问题。
+异步,是没有线程模型的JavaScript的救命稻草。说得高大上一些,就是运用了Reactor设计模式1。
JavaScript的一切都是围绕着“异步”二子的。无论是浏览器环境,还是node环境,大多数API都是通过“事件”来将请求(或消息、调用)和返回值(或结果)分离。而“事件”,都离不开回调(Callback),例如,
+var fs = require("fs");
+fs.readFile(__filename, function(e, data) {
+ console.log("2. in callback");
+});
+console.log("1. after invoke");
+fs模块封装了复杂的IO模块,其调用结果是通过一个简单的callback告诉调用者的。看起来是十分不错的,我们看看Ruby的EventMachine:
require "em-files"
+
+EM::run do
+ EM::File::open(__FILE__, "r") do |io|
+ io.read(1024) do |data|
+ puts data
+ io.close
+ end
+ EM::stop
+ end
+end
+由于Ruby的标准库里面的API全是同步的,异步的只有类似EventMachine这样的第三方API才能提供支持。实际风格上,两者类似,就我们这个例子来说,JavaScript的版本似乎更加简洁,而且不需要添加额外的第三方模块。
+异步模式,相比线程模式,损耗更小,在部分场景性能甚至比Java更好2。并且,non-blocking的API是node默认的,这使nodejs和它的异步回调大量应用。
例如,我们想要找到当前目录中所有文件的尺寸:
+fs.readdir(__dirname, function(e, files) {//callback 1
+ if(e) {
+ return console.log(e);
+ }
+ dirs.forEach(function(file) {//callback 2
+ fs.stat(file, function(e, stats) {//callback 3
+ if(e) {
+ return console.log(e);
+ }
+ if(stats.isFile()) {
+ console.log(stats.size);
+ }
+ });
+ });
+});
+非常简单的一个任务便造成了3层回调。在node应用爆发的初期,大量的应用都是在这样的风格中诞生的。显然,这样的代码风格有如下风险:
+不少程序员,因为第一条而放弃nodejs,甚至放弃JavaScript。而关于第二条,各种隐性bug的排除和性能损耗的优化工作在向程序员招手。
+等等,你说我一直再说node,没有提及浏览器中的情况?我们来看个例子:
+/*glboal $ */
+// we have jquery in the `window`
+$("#sexyButton").on("click", function(data) {//callback 1
+ $.getJSON("/api/topcis", function(data) {//callback 2
+ var list = data.topics.map(function(t) {
+ return t.id + ". " + t.title + "\n";
+ });
+ var id = confirm("which topcis are you interested in? Select by ID : " + list);
+ $.getJSON("/api/topics/" + id, function(data) {//callback 3
+ alert("Detail topic: " + data.content);
+ });
+ });
+
+});
+我们尝试获取一个文章列表,然后给予用户一些交互,让用户选择希望详细了解的一个文章,并继续获取文章详情。这个简单的例子,产生了3个回调。
+事实上,异步的性质是JavaScript语言本身的固有风格,跟宿主环境无关。所以,回调漫天飞造成的问题是JavaScript语言的共性。
+JavaScript程序员也许是最有创造力的一群程序员之一。对于回调问题,最终有了很多解决方案。最自然想到的,便是利用事件机制。
+还是之前加载文章的场景:
+var TopicController = new EventEmitter();
+
+TopicController.list = function() {//a simple wrap for ajax request
+ $.getJSON("/api/topics", this.notify("topic:list"));
+ return this;
+};
+
+TopicController.show = function(id) {//a simple wrap for ajax request
+ $.getJSON("/api/topics/" + id, this.notify("topic:show", id));
+ return this;
+};
+
+TopicController.bind = function() {//bind DOM events
+ $("#sexyButton").on("click", this.run.bind(this));
+ return this;
+};
+
+TopicController._queryTopic = function(data) {
+ var list = data.topics.map(function(t) {
+ return t.id + ". " + t.title + "\n";
+ });
+ var id = confirm("which topcis are you interested in? Select by ID : " + list);
+ this.show(id).listenTo("topic:show", this._showTopic);
+};
+
+TopicController._showTopic = function(data) {
+ alert(data.content);
+};
+
+TopicController.listenTo = function(eventName, listener) {//a helper method to `bind`
+ this.on(eventName, listener.bind(this));
+};
+
+TopicController.notify = function(eventName) {//generate a notify callback internally
+ var self = this, args;
+ args = Array.prototype.slice(arguments, 1);
+ return function(data) {
+ args.unshift(data);
+ args.unshift(eventName);
+ self.emit.apply(self, args);
+ };
+};
+
+TopicController.run = function() {
+ this.list().lisenTo("topic:list", this._queryTopic);
+};
+
+// kickoff
+$(function() {
+ TopicController.run();
+});
+可以看到,现在这种写法B格就高了很多。各种封装、各种解藕。首先,除了万能的jQuery,我们还依赖EventEmitter,这是一个观察者模式的实现3,比如asyncly/EventEmitter2。简单的概括一下这种风格:
如果你硬要挑剔的话,也有缺点;
+利用高阶函数,可以顺序、并发的将函数递归执行。
+我们可以编写一个高阶函数,让传入的函数顺序执行:
+var runInSeries = function(ops, done) {
+ var i = 0, next;
+ next = function(e) {
+ if(e) {
+ return done(e);
+ }
+ var args = Array.prototype.slice.call(arguments, 1);
+ args.push(next);
+ ops[0].apply(null, args);
+ };
+ next();
+};
+还是我们之前的例子:
+var list = function(next) {
+ $.getJSON("/api/topics", function(data) { next(null, data); });
+};
+
+var query = function(data, next) {
+ var list = data.topics.map(function(t) {
+ return t.id + ". " + t.title + "\n";
+ });
+ var id = confirm("which topcis are you interested in? Select by ID : " + list);
+ next(null, id);
+};
+
+var show = function(id, next) {
+ $.getJSON("/api/topics/" + id, function(data) { next(null, data); });
+};
+
+$("#sexyButton").on("click", function() {
+ runInSeries([list, query, show], function(e, detail) {
+ alert(detail);
+ });
+});
+看起来还是很不错的,简洁并且清晰,最终的代码量也没有增加。如果你喜欢这种方式,去看一下caolan/async会发现更多精彩。
+++A promise represents the eventual result of an asynchronous operation. The primary way of interacting with a promise is through its then method, which registers callbacks to receive either a promise’s eventual value or the reason why the promise cannot be fulfilled.
+
除开文绉绉的解释,Promise是一种对一个任务的抽象。Promise的相关API提供了一组方法和对象来实现这种抽象。
+Promise的实现目前有很多:
+虽然标准很多,但是所有的实现基本遵循如下基本规律:
+then([fulfill], [reject])方法,让使用者分别处理成功失败done([fn])、fail([fn])方法reject和resolve方法,来完成一个Promise笔者会在专门的文章内介绍Promise的具体机制和实现。在这里仅浅尝辄止,利用基本随处可得的jQuery来解决之前的那个小场景中的异步问题:
+$("#sexyButton").on("click", function(data) {
+ $.getJSON("/api/topcis").done(function(data) {
+ var list = data.topics.map(function(t) {
+ return t.id + ". " + t.title + "\n";
+ });
+ var id = confirm("which topcis are you interested in? Select by ID : " + list);
+ $.getJSON("/api/topics/" + id).done(function(done) {
+ alert("Detail topic: " + data.content);
+ });
+ });
+});
+很遗憾,使用Promise并没有让回调的问题好多少。在这个场景,Promise的并没有体现出它的强大之处。我们把jQuery官方文档中的例子拿出来看看:
+$.when( $.ajax( "/page1.php" ), $.ajax( "/page2.php" ) ).done(function( a1, a2 ) {
+ // a1 and a2 are arguments resolved for the page1 and page2 ajax requests, respectively.
+ // Each argument is an array with the following structure: [ data, statusText, jqXHR ]
+ var data = a1[ 0 ] + a2[ 0 ]; // a1[ 0 ] = "Whip", a2[ 0 ] = " It"
+ if ( /Whip It/.test( data ) ) {
+ alert( "We got what we came for!" );
+ }
+});
+这里,同时发起了两个AJAX请求,并且将这两个Promise合并成一个,开发者只用处理这最终的一个Promise。
+例如Q.js或when.js的第三方库,可以支持更多复杂的特性。也会让你的代码风格大为改观。可以说,Promise为处理复杂流程开启了新的大门,但是也是有成本的。这些复杂的封装,都有相当大的开销6。
ES6的Generator引入的yield表达式,让流程控制更加多变。node-fiber让我们看到了coroutine在JavaScript中的样子。
var Fiber = require('fibers');
+
+function sleep(ms) {
+ var fiber = Fiber.current;
+ setTimeout(function() {
+ fiber.run();
+ }, ms);
+ Fiber.yield();
+}
+
+Fiber(function() {
+ console.log('wait... ' + new Date);
+ sleep(1000);
+ console.log('ok... ' + new Date);
+}).run();
+console.log('back in main');
+但想象一下,如果每个JavaScript都有这个功能,那么一个正常JavaScript程序员的各种尝试就会被挑战。你的对象会莫名其妙的被另外一个fiber中的代码更改。
+也就是说,还没有一种语法设计能让支持fiber和不支持fiber的JavaScript代码混用并且不造成混淆。node-fiber的这种不可移植性,让coroutine在JavaScript中并不那么现实7。
+但是yield是一种Shallow coroutines,它只能停止用户代码,并且只有在GeneratorFunction才可以用yield。
笔者在另外一篇文章中已经详细介绍了如何利用Geneator来解决异步流程的问题。
+利用yield实现的suspend方法,可以让我们之前的问题解决的非常简介:
$("#sexyButton").on("click", function(data) {
+ suspend(function *() {
+ var data = yield $.getJSON("/api/topcis");
+ var list = data.topics.map(function(t) {
+ return t.id + ". " + t.title + "\n";
+ });
+ var id = confirm("which topcis are you interested in? Select by ID : " + list);
+ var detail = yield $.getJSON("/api/topics/");
+ alert("Detail topic: " + detail.content);
+ })();
+});
+为了利用yield,我们也是有取舍的:
yield的一些约束yield所产生的代码风格,可能对部分新手造成迷惑yield所产生堆栈及其难以调试说了这么多,异步编程这种和线程模型迥然不同的并发处理方式,随着node的流行也让更多程序员了解其与众不同的魅力。如果下次再有C或者Java程序员说,JavaScript的回调太难看,请让他好好读一下这篇文章吧!
+http://en.wikipedia.org/wiki/Reactor_pattern
+↩http://strongloop.com/strongblog/node-js-is-faster-than-java/
+↩en.wikipedia.org/wiki/Observer_pattern
+↩http://wiki.ecmascript.org/doku.php?id=strawman:concurrency
+↩http://promises-aplus.github.io/promises-spec/
+↩http://thanpol.as/javascript/promises-a-performance-hits-you-should-be-aware-of/
+↩http://calculist.org/blog/2011/12/14/why-coroutines-wont-work-on-the-web/
+↩ES6中的Generator的引入,极大程度上改变了JavaScript程序员对迭代器的看法,并为解决callback hell1提供了新方法。
+Generator是一个与语言无关的特性,理论上它应该存在于所有JavaScript引擎内,但是目前真正完整实现的,只有在node --harmony 下。所以后文所有的解释,都以node环境举例,需要的启动参数为node --harmony --use_strict。
V8中所实现的Generator和标准之中说的又有区别,这个可以参考一下MDC的相关文档2。而且,V8在写作这篇文章时,并没有实现Iterator。
+我们以一个简单的例子3开始:
+function* argumentsGenerator() {
+ for (let i = 0; i < arguments.length; i += 1) {
+ yield arguments[i];
+ }
+}
+我们希望迭代传入的每个实参:
+var argumentsIterator = argumentsGenerator('a', 'b', 'c');
+
+// Prints "a b c"
+console.log(
+ argumentsIterator.next().value,
+ argumentsIterator.next().value,
+ argumentsIterator.next().value
+);
+我们可以简单的理解:
+argumentsGenerator被称为GeneartorFunction,也有些人把GeneartorFunction的返回值称为一个Geneartor。yield可以中断GeneartorFunction的运行;而在下一次yield时,可以恢复运行。Iterator上,有next成员方法,能够返回迭代值。其中value属性包含实际返回的数值,done属性为布尔值,标记迭代器是否完成迭代。要注意的是,在done属性为true后继续运行next方法会产生异常。完整的ES实现中,for-of循环正是为了快速迭代一个iterator的:
// Prints "a", "b", "c"
+for(let value of argumentsIterator) {
+ console.log(value);
+}
+可惜,目前版本的node不支持for-of。
说到这里,大多数有经验的JavaScript程序员会表示不屑,因为这些都可以通过自己编写一个函数来实现。我们再来看一个例子:
+function* fibonacci() {
+ let a = 0, b = 1;
+ //1, 2
+ while(true) {
+ yield a;
+ a = b;
+ b = a + b;
+ }
+}
+
+for(let value of fibonacci()) {
+ console.log(value);
+}
+fibonacci序列是无穷的数字序列,你可以用函数的迭代来生成,但是远没有用Generator来的简洁。
+再来个更有趣的。我们可以利用yield*语法,将yield操作代理到另外一个Generator。
let delegatedIterator = (function* () {
+ yield 'Hello!';
+ yield 'Bye!';
+}());
+
+let delegatingIterator = (function* () {
+ yield 'Greetings!';
+ yield* delegatedIterator;
+ yield 'Ok, bye.';
+}());
+
+// Prints "Greetings!", "Hello!", "Bye!", "Ok, bye."
+for(let value of delegatingIterator) {
+ console.log(value);
+}
+yield可以暂停运行流程,那么便为改变执行流程提供了可能4。这和Python的coroutine类似。
co已经将此特性封装的非常完美了。我们在这里简单的讨论其实现。
+++The classic example of this is consumer-producer relationships: generators that produce values, and then consumers that use them. The two generators are said to be symmetric – a continuous evaluation where coroutines yield to each other, rather than two functions that call each other.
+
Geneartor之所以可用来控制代码流程,就是通过yield来将两个或者多个Geneartor的执行路径互相切换。这种切换是语句级别的,而不是函数调用级别的。其本质是CPS变幻,后文会给出解释。
+这里要补充yield的若干行为:
+GeneratorFunction的return语句等同于一个yield假设我们希望有如下语法风格:
+GeneratorFunctionyield,看起来像同步调用GeneratorFunction的返回值和执行过程的错误都会会传入全局的回调函数更具体的,如下例子:
+var fs = require("fs");
+suspend(function*(resume) {
+ var content = yield fs.readFile(__filename, resume);
+ var list = yield fs.readdir(__dirname, resume);
+ return [content, list];
+})(function(e, res) {
+ console.log(e,res);
+});
+上面分别进行了一个读文件和列目录的操作,均是异步操作。为了实现这样的suspend和resume。我们简单的封装Generator的API:
var slice = Array.prototype.slice.call.bind(Array.prototype.slice);
+
+var suspend = function(gen) {//`gen` is a generator function
+ return function(callback) {
+ var args, iterator, next, ctx, done;
+ ctx = this;
+ args = slice(arguments);
+
+ next = function(e) {
+ if(e) {//throw up or send to callback
+ return callback ? callback(e) : iterator.throw(e);
+ }
+ var ret = iterator.next(slice(arguments, 1));
+ if(ret.done && callback) {//run callback is needed
+ callback(null, ret.value);
+ }
+ };
+
+ resume = function(e) {
+ next.apply(ctx, arguments);
+ };
+
+ args.unshift(resume);
+ iterator = gen.apply(this, args);
+ next();//kickoff
+ };
+};
+目前我们只支持回调形势的API,并且需要显示的传入resume作为API的回调。为了像co那样支持更多的可以作为yield参数。co中,作者将所有形势的异步对象都归结为一种名为thunk的回调形式。
那什么是thunk呢?thunk就是支持标准的node风格回调的一个函数: fn(callback)。
首先我们将suspend修改为自动resume:
+var slice = Array.prototype.slice.call.bind(Array.prototype.slice);
+
+var suspend = function(gen) {
+ return function(callback) {
+ var args, iterator, next, ctx, done;
+ ctx = this;
+ args = slice(arguments);
+ next = function(e) {
+ if(e) {
+ return callback ? callback(e) : iterator.throw(e);
+ }
+ var ret = iterator.next(slice(arguments, 1));
+
+ if(ret.done && callback) {
+ return callback(null, ret.value);
+ }
+
+ if("function" === typeof ret.value) {//shold yield a thunk
+ ret.value.call(ctx, function() {//resume function
+ next.apply(ctx, arguments);
+ });
+ }
+
+ };
+
+ iterator = gen.apply(this, args);
+ next();
+ };
+};
+注意,这个时候,我们只能yield一个thunk,我们的使用方法也要发生改变:
var fs = require("fs");
+read = function(filename) {//wrap native API to a thunk
+ return function(callback) {
+ fs.readFile(filename, callback);
+ };
+};
+
+suspend(function*() {//return value of this generator function is passed to callback
+ return yield read(__filename);
+})(function(e, res) {
+ console.log(e,res);
+});
+接下来,我们要让这个suspend更加有用,我们可以支持如下内容穿入到yield
+var slice = Array.prototype.slice.call.bind(Array.prototype.slice);
+
+var isGeneratorFunction = function(obj) {
+ return obj && obj.constructor && "GeneratorFunction" == obj.constructor.name;
+};
+
+var isGenerator = function(obj) {
+ return obj && "function" == typeof obj.next && "function" == typeof obj.throw;
+};
+
+var suspend = function(gen) {
+ return function(callback) {
+ var args, iterator, next, ctx, done, thunk;
+ ctx = this;
+ args = slice(arguments);
+ next = function(e) {
+ if(e) {
+ return callback ? callback(e) : iterator.throw(e);
+ }
+ var ret = iterator.next(slice(arguments, 1));
+
+ if(ret.done && callback) {
+ return callback(null, ret.value);
+ }
+
+ if(isGeneratorFunction(ret.value)) {//check if it's a generator
+ thunk = suspend(ret.value);
+ } else if("function" === typeof ret.value) {//shold yield a thunk
+ thunk = ret.value;
+ } else if(isGenerator(ret.value)) {
+ thunk = suspend(ret.value);
+ }
+
+ thunk.call(ctx, function() {//resume function
+ next.apply(ctx, arguments);
+ });
+
+ };
+
+ if(isGeneratorFunction(gen)) {
+ iterator = gen.apply(this, args);
+ } else {//assume it's a iterator
+ iterator = gen;
+ }
+ next();
+ };
+};
+在使用时,我们可以传入三种对象到yield:
+var fs = require("fs");
+read = function(filename) {
+ return function(callback) {
+ fs.readFile(filename, callback);
+ };
+};
+
+var read1 = function*() {
+ return yield read(__filename);
+};
+
+var read2 = function*() {
+ return yield read(__filename);
+};
+
+suspend(function*() {
+ var one = yield read1;
+ var two = yield read2();
+ var three = yield read(__filename);
+ return [one, two, three];
+})(function(e, res) {
+ console.log(e,res);
+});
+当然,到这里,大家应该都明白如何让suspend兼容更多的数据类型,例如Promise、数组等。但更多的扩展,在这里就不再赘述。这里的suspend可以就说就是精简的co了。
yield的引入,让流程控制走上了一条康庄大道,不需要使用复杂的Promise、也不用使用难看的async。同时,从性能角度,yield可以通过V8的后续优化,性能进一步提升,目前来说yield的性能并不差5。
yield的本质是一个语法糖,底层的实现方式便是CPS变换6。也就是说yield是可以用循环和递归重新实现的,根本用不着一定在V8层面实现。但笔者认为,纯JavaScript实现的”yield”会造成大量的堆栈消耗,在性能上毫无优势可言。从性能上考虑,V8可以优化yield的编译,实现更高性能的转换。
关于CPS变换的细节,会在之后的文章中详细解说。
+https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
+↩https://github.com/JustinDrake/node-es6-examples#generators
+↩http://dailyjs.com/2013/05/31/suspend/
+↩http://dailyjs.com/2013/10/17/yield/
+↩http://en.wikipedia.org/wiki/Continuation-passing_style
+↩Async PRograming with Promise
+ ++ ++
then方法,其行为符合此规范的对象或者函数数。then方法的对象或者函数。undefined, 一个thenable, 一个promise)throw语句抛出。promise被reject的原因。一个promise必须是下面三种状态之一:pending, fulfilled, rejected
当一个promise是pending状态:
++++
+- 可以转变到fulfilled状态或者rejected状态
+
当一个promise是fulfilled状态:
++++
+- 不可以转变到其他任何状态
+- 必须有一个不能改变的value
+
当一个promise是rejected状态:
++++
+- 不可以转变到其他任何状态
+- 必须有一个不可改变的reason
+
在这里,“不能改变”意味着不可改变身份(即 ===),但并不意味着深不变性。
一个promise必须提供一个then方法来访问当前或者最终的value或者reason。
+一个promise的then方法,接受两个参数:
+promise.then(onFulfilled, onRejected)
+onFulfilled和onRejected都是可选参数:++如果
+onFulfilled或者onRejected不是函数,将会被忽略
如果onFulfilled是一个函数:
+++
+- 当promise是fulfilled状态的时候被调用,promise的值将作为其第一个参数。
+- 只能执行一次
+
如果onRejected是一个函数:
+++
+- 当promise是rejected状态的时候被调用,promise的值将作为其第一个参数
+- 该函数只能执行一次
+
onFulfilled或者onRejected只有当执行上下文栈里只包含平台代码1的时候才会被调用执行。onFulfilled或者onRejected只有是函数的时候才会被调用(即没有this值).2then在同一个promise里可以被调用多次。++当promise是fulfilled或者rejected状态的时候,onFulfilled和onRejected回调函数的调用顺序将会按照在then里定义的顺序进行调用。
+
then必须返回一个promise3
promise2 = promise1.then(onFulfilled, onRejcted);
+onFulfilled或者onRejected返回一个值x,那么将会执行Promise处理程序[[Resolve]](promise2, x)。onFulfilled或者onRejected抛出一个异常e,那么promise2将会因为异常e被rejected。onFulfilled不是一个function,而且promise1是fulfilled,那么promise2也必须以相同的值被fulfilled。onRejected不是一个function,而且promise1是rejected,那么promise2也必须以相同的原因被rejectedPromise处理程序是一个输入promise和值的抽象操作,我们把它表示成:[[Resolve]](promise, x)。如果x是thenable,并假设x的行为至少在某种情况下是一个promise,那么它将会试图根据x的状态做出promise。否则x的值满足promise。
对于thenables的处理,允许promise实现互操作,只要它们公开一个与Promise/A+兼容的方法。它也允许Promise/A+的实现,可以不符合标准,但是合理的实现。
+要运行[[Resolve]](promise, x),请执行下列步骤:
x是对象或者函数:[[Resolve]](promise, y)x去fulfill promisex去fulfill promise如果promise通过一个圆形的thenable链被resolve,那么[[Resolve]](promise, x)的递归性质将最终导致[[Resolve]](promise, x)被再次调用,根据上述算法将导致无限递归。我们推荐去实现,但不是必需的,检测这样的递归和通过一个异常TypeError来reject promise。
如有不尽之处,请联系本人: robinqu@gmail.com
+| 项目 | +链接 | +版权 | +
|---|---|---|
| Promise A+ Spec | +http://promises-aplus.github.io/promises-spec/ | +![]() |
+
| Promise A+ 规范 | +http://hussion.me/2013/10/19/promises-a/ | +通过作者hussion授权 | +
这里的“平台代码”是指引擎,环境,和promise执行代码。在实践中,需要确保onFulfilled和onRejected在一个新的堆栈中,并在事件循环then后异步执行。这可以实现任何一个“宏任务”机制,例如:setTimeout或setImmediate,或“微任务”的机制,如MutationObserver或process.nextTick。由于promise的实现被认为是平台的代码,它本身可能包含在该处理程序的调用的任务调度队列或“蹦床”。
也就是说,在严格模式下,this将是不确定的 ;在非严格模式下,this将是全局对象。
+↩实现可能允许promise2 === promise1,提供了符合所有要求的实现。每个实现应说明是否promise2 === promise1会在某种条件下可以产生。
+↩一般来说,如果它来自当前的实现,x将是一个真正的promise。并允许根据符合promise的状态,使用特定的实现手段。
+↩程序第一次执行的时候会保存x.then的引用,然后并会测试该引用,并调用该引用,避免了多次访问x.then属性。并能确保一致性。
+↩实现不应设置没有任何深度限制的thenable链,否则将会导致无限递归。只有正常的周期才会导致一个TypeError异常,如果遇到一个无限递归thenables链,递归永远是正确的行为.
数组本质上是object(type of [ ] == ‘object’);
+所以要判断是不是数组,需要通过判断constructor。
+[].constructor//Array
使用length属性获取元素的个数。
数组的length属性是可写的。当length属性小于元素个数时,数组中索引值大于length属性的元素会被删掉。
push 从数组尾部添加unshift 从数组头部添加pop 从尾部弹出shift 从头部弹出join 将数组中所有元素转换成字符串并连接在一起reverse 将数组中成员颠倒排序sort 将数组元素排序,可以指定一个排序函数contact 将数组连接起来slice 返回指定数组的一个片段或子数组splice 从数组中插入或删除元素var a = [1, 2, 3, 4];
+var b = a.splice(1,2);//a = 1,4,b = 2,3
+forEach 从头到尾遍历数组,为每个元素调用制定的函数map 把数组的每个元素传给指定的函数,并返回一个数组。var a = [1, 2, 3];
+var b = a.map(function(x) {
+ return x*x;
+}); //b = [1,4,9]
+filter 把数组的每个元素传给指定的函数,通过函数返回的布尔值决定是否在返回数组中添加该元素 var a = [1, 2, 3];
+ var b = a.filter(function(x){
+ return x % 2 !== 0;
+});//b = [1, 3]
+every 把数组的每个元素传给指定的函数,如果全部调用返回true则every函数返回truesome 把数组的每个元素传给指定的函数,如果有调用返回true则every函数返回truereduce 用指定的函数对数组进行组合,生成单个值var a = [1, 2, 3];
+var b = a.reduce(function(x, y){
+ return x + y;
+}, 0); //b = 6;
+indexOf/lastIndexOf在整个数组中搜索制定的元素通过为对象增加length自增的特性或者其他特性,可以生成一个‘类数组对象’,可以通过length进行遍历。例如函数的Arguments对象就是这样
+ +表达式是JavaScript中的一个短语,解释器会将其计算出一个结果。程序中的常量是最简单的一类表达式。
+将简单的表达式在组合成复杂的表达式最常用的方法就是使用运算符。运算符按照特定运算规则对操作数进行运算。
+原始表达式包括常量,关键字和变量.
+数组初始化表达式是通过一对方括号和其内由逗号隔开的列表构成的。例如
+[]
+[1+2,3+4]
+也可以进行嵌套:
+[[1,2,3],[4,5,6]];
+也可以通过逗号省略某些元素:
+[1,,,,5]
+对象初始化跟数组初始化非常相似,只是方括号被花括号代替,并且每个字表达式都包含一个属性吗和一个冒号作为前缀:
+var p = {x:2.3, y:-1.2}
+var quare = function(x) { return x * x;}
+o.x
+f(0)
+Math.max(x, y, z)
+a.sort()
+new Object()
+new Point(2,3)
+JavaScript中有许多运算符用于算术表达式,比较表达式,逻辑表达式,赋值表达式。多数运算符都是由标点符号表示,比如”+”和”=”。另外一些运算符则是由关键字表示,比如delete和instanceof。
+运算符可以根据其操作数的个人进行分类。多数的运算符为二元运算符 例如*。 同样也有一些一元运算符,例如,表达式-x中“-”运算符,条件判断运算符 ?: 是一个三元运算符。
+一些运算符可以作用与任何数据类型,但是仍然希望它们的操作数是指定类型的数据,并且大多数运算符返回一个特定类型的值。通常会根据需要对操作数进行类型转换
+左值是一个古老的术语,它是指 表达式只能出现在赋值运算符的左侧。在JavaScript中,变量,对象属性和数组元素均是左值。ECMAScript规范允许内置函数返回一个左值,但自定义的函数则不能返回左值。
+1 + 2
+"hello" + " "
+"1" + "2"
+关系运算符用于测试两个值之间的关系,关系表达式总是返回一个布尔值,通常在if, while或者for语句中使用关系表达式,用以控制程序的执行流程。
+== 和 ===运算符用于比较两个值是否相等,他们对相等的定义不尽相同。两个运算符允许任意类型的操作数,如果操作数相等则返回true,否则返回false。====也称为严格相等运算符,它用来检测两个操作数是否严格相等。== 运算符称做相等运算符,它用来检测两个操作数是否相等,这个相等的定义非常宽松,可以允许进行类型转换。由于 == ===的结合性都是从左到右,所以在执行==操作的时候,执行左侧的类型转换。 != !==运算符的检测规则是==和===运算符的求反。
+比较运算符用来检测两个操作数的大小关系。例如 < > <= >=. 比较操作符的操作数可能是任意类型的,然而之后数字和字符串才是真正执行比较操作符,因为那些不是数字和字符串的操作数都将进行类型转换。
+in运算符希望它的左操作数是一个字符串或可以转换为字符串,希望它的右操作数是一个对象。如果右侧的对象拥有一个名为左操作数值的属性名,那么表达式返回true,例如:
+var point = {x:1, y:1};
+"x" in point
+instanceof运算符希望左操作数是一个对象,右操作数标识对象的类。如果左侧的对象是右侧类的实例,则表达式返回true。例如:
+var d = new Date();// 通过Date()构造函数来创建一个新对象
+d instanceof Date;// 计算结构为true, d是由Date()创建的
+逻辑运算符 && || !是对操作数进行布尔算术运算,经常和关系运算符一起使用
+if(a === b && c == d ) {
+ // some code
+}
+JavaScript使用”=”运算符来给变量或者属性赋值 例如:
+i = 0
+o.x = 1
+=运算符希望它的左操作数是一个左值, 右操作数可以使任意类型的任意值。
+除了常规的赋值运算,JavaScript还支持许多其他的赋值运算符,这些运算符将赋值运算符和其他运算符连接起来,提供了一种更为快捷的运算方式。例如:
+total += sales_tax
+等价于
+total = total + sales_tax
+和其他许多解释性语言一样,JavaScript同样可以解释运行由JavaScript源代码组成的字符串, 并产生一个值。JavaScript通过全局函数eval()来完成这个工作:
+eval("3+2") //=>5
+条件运算符(?:)是JavaScript中唯一的一个三元操作符:
+x > 0 ? x : -x //求x的绝对值
+typeof运算符是一元运算符,放在其单个操作数的前面,操作数可以使任意类型。返回值为表示操作数类型的一个字符串:
+typeof最常用的用法是写在表达式中, 就像这样:
+(typeof value == "string") ? "'" + value + "'" : value
+delete是一元操作符, 它用来删除对象属性或者数组元素.
+var o = {x:1, y:2}//定义一个对象
+delete o.x //删除一个属性
+"x" in o //=> false:这个属性在对象中不再存在
+
+JavaScript中,要记住函数是first-class citizen。
+ function plus(x ,y) {
+
+ }
+ var plus = function (x, y) {
+
+ }
+ function a(){};
+ a();
+作为方法调用
+ a={};
+ a.x = function(){};
+ a.x();
+通过call和apply间接调用函数(改变this)
+call 和 apply带有多个参数,call和apply把当前函数的this指向第一个参数给定的函数或对象中,并传递其余所有的参数作为当前函数的参数。
+var O = function () {
+ this.foo = 'hello';
+ this.hello = function () {
+ return 'world';
+ }
+};
+
+var fn = function () {
+ console.log('call', this);
+};
+
+var o = new O();
+
+fn.call(o);//此时fn的this指向o
+call和apply的不同之处,在于call传递的参数是作为arguments依次传入的,例如
+fn.call(o, 1, 2, 3);
而apply传递的参数是以一个数组的方式传入的,例如fn.apply(o, [1, 2, 3]);
当传入参数少于函数声明的参数时,留空的参数的值是undefined。
JavaScript允许传入参数的个数大于声明时制定的参数个数。可以用arguments来访问这些参数
function f(){
+ var i;
+ for( i = 0; i < arguments.length ; i++) {
+ console.log(arguments[i]);
+ }
+}
+
+f(1,2,3,4,5,6);
+函数通过取得arguments的长度得到传入参数的个数,使用一个循环获取每一个参数。
+arguments还有两个属性,callee和callercallee表示正在执行的function对象,caller表示调用当前function的function
例如
+function f(){
+ console.log(arguments.callee);//[Function: f]
+ console.log(arguments.callee.caller);[Function: g]
+ var i;
+ for( i = 0; i < arguments.length ; i++) {
+ console.log(arguments[i]);
+ }
+}
+
+function g(){
+ f(1,2,3,4,5,6);
+}
+
+g();
+ callee 的重要用法之一是在匿名函数中实现递归
var result = function (x) {
+ if (x <= 1) return 1;
+ return x * arguments.callee(x - 1);
+}(3);
+
+console.log(result);
+上例使用了一个匿名函数和callee实现了一个阶乘。
+javascript中的函数可以作为值来传递
+function square(x) {
+ return x * x;
+}
+
+var s = square;
+s(4);
+(function() {
+
+}());
+JavaScript函数对象的内部状态不仅包含着函数的代码逻辑,还引用当前的作用域链。函数对象通过作用域链相互关联起来,函数体内部变量包含在函数作用域内,这就叫闭包。
+例如
+var scope = 'global scope';
+function checkscope() {
+ var scope = 'local scope';
+ function f() {
+ return scope;
+ }
+ return f;
+}
+
+checkscope()();
+这段checkscope声明了一个局部变量,定义了一个函数f,函数f返回了这个局部变量的值,最后返回了这个函数f。在定义函数f的作用域外调用f,得到的返回仍然是函数f创建时所在的作用域的局部变量scope。
+又例如
+var counter = (function() {
+ var count = 0;
+ return function () {
+ return count++ ;
+ }
+}());
+代码定义了一个立即执行函数并返回给counter,这个函数定义了一个局部变量count,返回了一个子函数,该子函数每次调用,都会吧count加一并返回。
+闭包的注意事项
+观察下面的示例:
+var add_the_handlers = function (nodes) {
+ var i;
+ for (i = 0; i < nodes.length; i += 1) {
+ nodes[i].onclick = function (e) {
+ alert(i);
+ };
+ }
+};
+这个函数期望的结果,是在运行的时候为每个node在onclick的时候alert出各自的序号,但是实际运行的结果却不同:所有的node在单击的时候alert出来的数字总是同一个。
+这是因为alert所在的匿名函数的闭包中存放的i是第一行的i,而不是在循环中获得的i的当前值。
+所以如果希望达到预期结果,应该在循环中创建多个闭包,在闭包中存放当前循环的i的值:
+var add_the_handlers = function (nodes) {
+ var i;
+ for (i = 0; i < nodes.length; i += 1) {
+ nodes[i].onclick = function (i) {
+ return function(e){
+ alert(e);
+ };
+ }(i);
+ }
+};
+这里使用一个立即执行函数并传递当前的i的值,返回一个新生成的函数。在这个新生成的函数的闭包中就保存了当前的i的值。
+在一个对象中的this始终引用当前对象,但是在函数中,特别是在闭包中,this有一些特殊的行为。
+函数中的this对象始终绑定在函数运行时的上下文环境上。所以在普通模式下调用一个全局函数,this始终指向window(客户端),在严格模式下调用一个全局函数,this始终是undefined
+示例
+var name = "The Window";
+var object = {
+ name: "My Object",
+ getNameFunc: function () {
+ return function () {
+ return this.name;
+ };
+ },
+ getName : function () {
+ return this.name;
+ }
+};
+
+console.log(object.getNameFunc()());
+console.log(object.getName());
+getNameFunction()返回了一个匿名函数,这个匿名函数在调用的时候,上下文是window(浏览器中),所以在浏览器中输出的是the Window
+而getName()调用的时候上下文是object,所以成功输出object的name
+其实以上代码中
object.getNameFunc()()
等效于
var fnc = object.getNameFunc();//这时候的fnc已经脱离了object对象
fnc();
所以如果想要getNameFunction()正确返回Object的Name,需要在返回的匿名函数的闭包中保存在函数声明时的this,
+getNameFunc: function () {
+ var that = this;
+ return function () {
+ return that.name;
+ };
+ },
+这样就可以了。。
+函数柯里化是指,把接受多个参数的函数转换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
+示例
+var add1 = add.curry(1);
+console.log(add1(2));
+其中,add是接受两个参数的函数,add调用了curry返回一个只接受一个参数的新函数,之后调用add1便等效于调用add(1, 2);
+javascript并不原生支持curry,可以用prototype来模拟
+Function.prototype.curry = function () {
+ var slice = Array.prototype.slice,
+ args = slice.apply(arguments),
+ that = this;
+ return function () {
+ return that.apply(null, args.concat(slice.apply(arguments)));
+ };
+};
+
+
+function add(n1, n2) {
+ return n1 + n2;
+}
+
+var add1 = add.curry(1);
+console.log(add1(2));
+curry创建了一个新函数,在新函数的闭包中保存了原先传递的参数。
+length 函数的length表示函数实参的数量,是只读的prototype 指向一个该函数的原型对象的引用toString 返回一个字符串JavaScript程序是用Unicode字符集。支持地球上几乎所有在用的语言。
+var π = 3.14;
+JavaScript是区分大小写的语言的。需要注意的是,HTML并不区分大小。
+online 与 Online 在JavaScript是不同的,在HTML是相同的。
+JavaScript会忽略程序中标识之前的空格。多数情况下也会忽略换行符。这样我们可以通过空格和换行,保持整齐,一致的编码风格。需要注意的是,如果当前语句和随后的非空格字符不能当成一个整句,JavaScript不会忽略换行符而是在语句行结束处填补分号。例如:
+var a
+a
+=
+3
+console.log(a)
+JavaScript将其解析为:
+var a; a = 3; console.log(a);
+在有些计算机硬件和软件里,无法显示或输入Unicode字符全集。为了支持那些使用老旧技术的程序员,JavaScript定义了一种特殊序列,使用6个ASCII字符代表任意16位Unicode内码,一般为转义序列均以\u为前缀,其后跟随4个十六进制数
+é // \u00E9
+JavaScript支持两种格式的注释:
+//这里是单行注释
+/*这里是一段注释*/
+所谓直接量,就是程序中直接使用的数据值:
+12 //数字
+1.2 // 小数
+"hello world" // 字符串
+'Hi'// 字符串
+true // 布尔值
+false // 另一个布尔值
+/javascript/gi //正则表达式直接量
+标识符就是一个名字,在JavaScrpt中,标识符用来对变量和函数进行命名
+JavaScript标识符必须以字母,下划线或者美元符开始
+var _secret, $dom;
+后续的字符可以使字母,数字,下划线或美元符
+var 1234567, super_secret, $;
+通常使用ASCLL字符
+var iVar;
+常用的保留字
+break delete case do catch else continue false debugger finally default for function return typeof if switch var in this void instanceof throw while new true with null try
+ECMAScript 5 的保留字
+class const enum export extends import super
+在严格模式下的保留字
+implements let private public yield interface package protected static
+ECMAScript 3, 保留了java语言中所有的关键字
+abstract double goto boolean enum implements byte export import char extends int class final interface const float long native static package super private synchronized protected throws public transient short volatile
+预定义的全局变量
+arguments encodeURI Infinity Array encodeURIComponent isFinite Boolean Error isNaN Date eval JSON decodeURI EvalError Math decodeURIComponent Function NaN Number Object parseFloat parseInt RangeError ReferenceError RegExp String SyntaxError TypeError undefined URIError
+两个例子
+如果一个换行符后面没有任何字符,JavaScript会把它解释为分号
+ function() {
+ return
+ 1;
+ }
+var o = {
+ foo : "bar"
+}
+构造函数
+var o = new Object();
+var p = Object.create(o);
+JavaScript对象拥有自有属性和继承属性。
+在查询对象o的属性x时,先查找o中的属性x,如果没找到,则查找o的原型对象中的x属性,直到查找到x或者一个原型是null的对象为止
+在给对象o的x属性赋值时,如果o中已经有一个自有属性x,则改变x的值,若o中不存在属性x,则为o创建一个x属性并赋值
+也就是说,只有在查询时原型链才会起作用。
+var O = {
+ x : 1
+};
+
+function P() {
+ this.y = 2;
+}
+
+P.prototype = O;
+
+var t = new P();
+console.log(t);
+console.log('x' in t);//true
+console.log(t.hasOwnProperty('x'));//false
+可以使用in 或者 hasOwnProperty 来判断对象中是否存在属性。
+可以使用 for..in 来遍历对象的属性
使用for..in时会遍历到原型链上的属性。遍历顺序是以广度优先遍历
所以使用hasOwnProperty便可以判断是否是对象自有的属性。
+使用Object.getOwnPropertyDescriptor()获取对象特定属性的描述符
+可写性(writable) 表示对象属性是否可写
+例如
+var o = {
+ foo : 'bar'
+}
+
+Object.defineProperty(o, "foo", { writable : false });
+
+o.foo = 'world';
+console.log(o.foo);//仍然输出bar
+可枚举性(enumerable) 表示对象属性是否可枚举
+例如
Array中的length等属性的 enumerable是false,所以,
for (p in Array) {
+ console.log(p);
+}
+什么也不输出
+可配置性(configurable) 表示可否修改属性的可配置性和可枚举性
+可以用Object.defineProperties来定义这些配置属性。
+Object.defineProperty(o, "foo", { writable : false });
+Get 表示获取对象属性的方法
Set 表示设置对象属性的方法
示例
+var book = {
+ _year: 2004,
+ edition: 1
+};
+Object.defineProperty(book, "year", {
+ get: function () {
+ console.log('get year');
+ return this._year;
+ },
+ set: function (newValue) {
+ console.log('set year');
+ if (newValue > 2004) {
+ this._year = newValue;
+ this.edition += newValue - 2004;
+ }
+ }
+});
+book.year = 2005;//控制台输出‘set year’
+console.log(book.year);//控制台输出‘get year’和year的值
+toString 将对象转换成字符串,默认的转换会是[object Object]之类的东西,所以需要转成json格式的话可以用JSON.stringify
valueOf 需要将对象转换成其他类型的时候要用到。同样的,默认转换没什么值得说的。
通过如下方法可以创建一个可执行对象
+function bar(o) {
+ var f = function() { return "Hello World!"; }
+ o.__proto__ = f.__proto__;
+ f.__proto__ = o;
+ return f;
+}
+
+var o = { x: 5 };
+var foo = bar(o);
+
+console.log(foo());
+console.log(foo.x);
+console.log(typeof foo);//function
+既可以当作对象来使用(有原型链),也可以当作函数来直接调用
+ +语句就是JavaScript的整句或者命令。JavaScript的语句是以;结束的。
+表达式语句是JavaScript中最简单的语句:
例如:
greeting = "Hello" + name;
+i *= 3;
+counter++;
+这些都是简单的赋值语句。赋值语句是一类比较重要的表达式语句。
delete运算符一般作为语句使用,而不是复杂表达式的一部分:
delete o.x;
+函数调用是表达式语句的另一个大类,例如:
+alert(greeting):
+window.close();
+JavaScript中可以将多条语句联合在一起,形成一条符合语句,只须用花括号将多条语句括起来:
+{
+ x = Math.PI;
+ cx = Math.cos(x);
+}
+这样的语句块有几点需要注意的:
+在JavaScript中,当希望多条语句被当做一条语句使用时,使用复合语句来替代。空语句则恰好相反,它允许包含0条语句。空语句如下所有:
+;
+空语句有时很有用处例如:
+for(i=0; i<a.length; a[i++]=0);
+这里需要注意的是,在for循环,while循环或if语句在右圆括号后面的分号很不起眼,这很可能造成一些致命bug,而且这些bug很难定位,例如:
+if((a == 0) || (b == 0));//这行代码什么都没做。
+
+0 = null; //这行代码总会执行。
+var和function都是声明语句。
+var语句用来声明一个或者多个变量,声明的可以带有初始化表达式,用于指定变量的初始值,例如:
+var i;
+
+var j = 0;
+
+var p, q;
+
+var greeting = "hello" + name;
+
+var x = 2, y = x*x;
+如果var语句出现在函数体内,那么它定义的是一个局部变量,其作用域就是这个函数,如果在顶层代码中使用了var语句,它声明的是全局变量,在整个JavaScript的程序中都可见的。全局变量是全局对象的属性。然而和其他全局对象属性不同的是, var声明的变量是无法通过delete删除的。
+如果var语句中的变量没有指定初始化表达式,那么这个变量的初始值就是undefined,变量在声明他们的脚本或者函数中都有定义的,变量声明语句会被“提前”至脚本或者函数的顶部。但是初始化的操作还是在原来var语句的位置执行,在声明语句之前变量的值是undefined。例如:
+for(var i=0; i<10; i++); //var i; 会被提前到整个函数的顶部
+function用来定义函数。例如:
+条件语句是通过判断指定表达式的值来决定执行还是跳过某些语句。
+下面介绍JavaScript基本条件语句,例如if/else, switch。
+if语句是一种基本的控制语句,这种语句有两种形式,第一种是:
+if (expresion)
+ statement
+在这种形式中, 需要计算expression的值,如果计算结果是真值, 那么就执行statement。如果expression的值是假值,那么就不执行statement。
+if语句的第二种形式引进了else从句,当expression的值为false的时候,执行else中的逻辑:
+if (expression)
+ statement1
+else
+ statement2
+和大多数的编程语言一样,JavaScript中的if, else匹配规则是,else总是和就近的if语句匹配。
+if/else语句通过判断一个表达式的计算结果来选择执行两条分支中的一条。当代码中有多条分支,一种解决办法是使用else if语句。else if语句并不是真正的JavaScript语句,它只是多条if/else语句连在一起的一种写法。
+if( n == 1 ) {
+ 执行代码块 1
+}
+else if(n == 2) {
+ 执行代码块 2
+}
+else if(n == 3) {
+ 执行代码块 3
+}
+else {
+ 之前的条件都为false, 则执行这里的代码块 4
+}
+像这样的有多条分支的情况, else if 不是最佳的解决方案。switch语句正适合处理这种情况。关键字switch之后紧跟着圆括号括起来的一个表达式,随后是一对花括号括起来的代码块:
+switch(expression) {
+ statements
+}
+switch语句的完整语法要复杂一些,代码中可以使用多个由case关键字标识的代码片段,case之后是一个表达式和一个冒号,当执行这条switch语句的时候,它首先计算expression的值,然后查找case子句的表达式是否和expression的值相同(按照“===”比较)。如果找到匹配的case,那么将会执行这个case对应的代码块。如果找不到匹配的case.那么将执行default标签的代码。
+注意的是,通常情况下case语句结尾处使用关键字break.break语句可以使解释器跳出switch语句或循环句。如果没有break语句,那么switch语句就会从与expression的值相匹配的case标签处的代码块开始执行,依次执行后续的语句。
+如果switch表达式与所有case表达式都不匹配,则执行标识为default的语句块,如果没有default标签,则switch的整个语句块都将跳过。
+循环语句就是程序路径的一个回路,可以让一部分代码重复执行。 JavaScript中有4种循环语句: while, do/while, for和for/in。
+while语句是一个基本的语句,它的语法如下:
+while (expression)
+ statement
+在执行while语句之前,JavaScript解释器首先计算expression的值, 如果它的值是假值,那么程序就跳过循环体中的逻辑statement转而执行程序中的下一个语句。反之,则执行statement.
+通常来说,我们并不想让JavaScript反复执行同一操作。在几乎每一次循环中,都有一个或多个变量随着循环的迭代而改变,如果这些变量在expression中用到,那么每次循环表达式的值也不同。这一点非常重要。
+循环最常用的用法就是用来遍历数组例如:
+var a[10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+var i = 0;
+while(i < 10) {
+ console.log(a[i]);
+ i++;
+}
+do/while循环和while循环非常相似,只不过它是在循环的尾部而不是在顶部检测循环表达式,这就意味着循环体至少会执行一次:
+do
+ statement
+while (expression);
+do/while循环并不像while那么常用。
+for语句提供了一种比while语句更加方便的循环控制结构。语法:
+for(initialize; test; increment)
+ statement
+initialize, test和increment三个表达式之间用分号分隔,他们分别负责初始化操作,循环条件判断和计数器变量的更新。
+由于for语句的特点,for语句比while语句更适合遍历数组:
+ var a[10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+ for(var i = 0; i < 10; i++) {
+ console.log(a[i]);
+ }
+for/in语句也使用for关键字,但它是和常规的for循环完全不同的一类循环。语法:
+for (variable in object)
+ statement
+variable通常是一个变量的名,也可以使一个可以产生左值的表达式或一个通过var语句声明的变量,object是一个表达式,这个表达式计算的结果是一个对象。for/in 循环用来方便的遍历对象属性成员:
+for(var p in o ) {
+ console.log(o[p]);
+}
+for/in循环并不会遍历对象所有属性,只有“可枚举”的属性才会遍历到。由JavaScript语言核心所定义的内置方法就不是“可枚举的”,例如所有对象都有方法toString().
+JavaScript中另一类语句是跳转语句。例如break语句是跳转到循环或者其他语句的结束。 continue语句是终止本次循环的执行并开始下一次循环的执行。JavaScript中的语句可以命名或带有标签,break和continue可以标识目标循环或者其他语句标签。
+return语句让解释器跳出函数体的执行, 并提供本次调用的返回值。throw语句触法或者”抛出”一个异常,他是与try/catch/finally语句一通使用的。
+语句是可以添加标签的,标签是由语句前的标识符和冒号组成:
+identifier: statement
+JavaScript中允许break关键字后面跟随一个语句标签:
+break labelname;
+当break和标签一块使用时,程序将跳转到这个标签所标识的语句块的结束,或者直接终止这个闭合语句块的执行。 如果没有这个闭合语句块,就会产生一个语法错误。
+单独使用break语句的作用是立即退出最内层的循环或switch语句。如果希望通过break来跳出非就近的循环体或者switch语句时,就会用到带标签的break语句.
+continue语句和break语句非常类似,但它不是退出循环, 而是转而执行下一次循环。
+函数中的return语句既是指定函数调用后的返回值。
+return expression;
+return语句只能出现在函数体内。如果没有return语句,调用表达式的结果是undefined
+JavaScript中,当产生运行时错误或者程序使用throw语句时就会显示地抛出异常。使用try/catch/finally语句可以捕获异常,
+throw语句的语法:
+throw expression;
+expression的值可以是任意类型的。JavaScript解释器抛出的异常的时候通常采用Error类型和其子类型,例如:
+function factorial(x) {
+ if(x < 0) throw new Error("x不能是负数");
+ for(var f = 1; x > 1; f *= x, x--);
+ return f;
+}
+Error中有几个比较重要的属性 比如name 错误的名称 message 错误的信息 stack 错误发生时调用堆栈。
+常见的几种Error类型:
+SyntaxError: 语法错误,无法通过解释器
+RangeError: 数值超出范围
+TypeError: 变量的类型不是预期的
+ReferenceError: 引用不可用
+当抛出异常时, JavaScript解释器会立即停止当前正在执行的逻辑,并跳转到就近的异常处理程序。异常处理程序是用try/catch/finally语句的catch从句编写的。
+try/catch/finally语句是JavaScript的异常处理机制。其中try从句定义了需要处理的异常所在的代码块。 catch从句跟随在try从句之后, 当try块内某处发生了异常时,调用catch内的代码逻辑。 catch从句后跟随finally快,后者中放置清理代码。不管try块中是否产生异常,finally块内的逻辑总是会执行。
+这里讨论三种JavaScript语句 with, debugger和 use strict
+with语句用于临时扩展作用域链,语法:
+with (object)
+statement
+这条语句将object添加到作用域链的头部,然后执行statement,最后把作用域链恢复到原始状态。
+通常情况不推荐使用with语句。一般可以使用with语句来简化代码编写。例如:
+with(document.forms[0]) {
+ name.value = "";
+ address.value = "";
+ email.value = "";
+}
+debugger语句,用来产生一个断点,代码的执行会停在断点的位置,这时使用调试器来输出变量的值。
+ +++本文上一个版本盗用了别人的文章,经读者指出后我就删掉了。由于起草的时间在去年,我也不太清楚当初是怎么把别人的文章复制进来的。本文除了介绍所谓的Strict Mode之外,还会介绍其他关联内容。
+
JavaScript并不是一个完美的语言。事实上,第一个版本是Brendan Eich1花费十天的时间创造的,你不能对它期望太多。之后,JavaScript在浏览器大战中,成为各方角逐的主要战场。各大厂商各显神通,其副作用是各种奇奇怪怪的行为和各式不一的API。在之后,W3C和其他社区团体花费了大量的精力来通过标准化来“净化”所有Web开发相关的技术标准。
+但尴尬的是,浏览器厂商并不是那么完全的实现了W3C和ECMAScript的各种标准。最后,经验丰富的Javascript程序员,通过约束自身对Javascript的使用方法,来达到让Javascript更高的可拥度。可能大部分人都读过《JavaScript语言精粹》2这本书,其讲述的就是如何在JavaScript语言中,取其精华,然后去其糟粕。
+而JavaScript的严格模式,则是另一种紧箍咒,它的约束力来自运行时本身,而不是用户的主观行为。也就是说,有很多模棱两可,或是错误却被允许的操作,被彻底禁止了。目前支持严格模式的支持范围3从IE10起跳,其他常青浏览器也都是支持的。
+ + +开启全局模式只需在所有语句之前放置"use strict"字符串常量。
全局开启严格模式:
+"use strict"
+var v = "Hello world";
+但注意,这样会导致整个脚本内的代码都在严格模式中执行。假如之前有些代码并没有考虑严格模式,这可能让你的整个应用程序突然失效。
+我们更为推荐的是,在某个函数内开启严格模式:
+function mySuperMethod() {
+ "use strict";
+ var v = "Hello world";
+}
+
+function mySuckingMethod {
+ //not in strict mode
+}
+大家有需要记住一堆语言特性了。但是,还好这些内容是把“歪”的掰“直”了。有少数代码例子来自于MDC4。
+ReferenceError试图隐式创建全局变量
+ ```
+ "use strict"
+ hello = "world"//throw
+ ```
+TypeError试图修改已经被定义为不可写的属性
+ ```
+ "use strict";
+ var o = {};
+ Object.defineProperty(o, "hello", {value:"world", writable:false});
+ o.hello = "bad boy";//throw
+ ```
+其他类似的还有:
+试图删除不可删除的属性
+ ```
+ "use strict";
+ delete Object.prototype; //throw
+ ```
+arguments.callee不能被返回、删除、修改;
```
+ "use strict";
+ var fun = function() {
+ return arugments.callee;//throw
+ };
+ ```
+SyntaxError重复定义属性名
+ ```
+ "use strict";
+ var o = {hello: 1, hello: 2};//throw
+ ```
+禁用八进制字面量
+ ```
+ "use strict";
+ var hello = 015;//throw
+ ```
+不允许重复参数名
+ ```
+ function myMethod(a, b, b) {//throw
+ "use strict";
+ }
+ ```
+不能使用with
```
+ "use strict";
+ var obj = {};
+ with (obj) {};//throw
+ ```
+不允许对eval或arguments赋值
```
+ var fun = function(){
+ "use strict";
+ eval=16
+ }();
+ ```
+不可将eval或arguments作为参数名、变量名
```
+ var fun = function(){
+ "use strict";
+ var obj = {
+ set p(arguments) {}
+ };
+ }();
+ ```
+eval被限制在临时的本地作用域eval不再有权限直接修改其所在作用于,而只能影响自身创建的作用域。
+var hello = "world";
+var evalHello = eval("'use strict'; var hello = "girl"; hello");
+// hello === "world"
+// evalHello === "girl"
+arguments不再追踪实际参数值变化function f(hello) {
+ "use strict";
+ hello = "girl";
+ return [hello, arguments[0]];
+}
+var pair = f("world");
+// pair[0] === "girl"
+// pair[1] === "world";
+this不做任何修改null或undefined,引擎也不会重新指定全局对象作为this"use strict";
+function fun() { return this; }
+// fun() === undefined
+// fun.call(2) === 2
+// fun.apply(null) === null
+// fun.call(undefined) === undefined
+// fun.bind(true)() === true
+以往,我们可以通过函数的caller和arguments来投影整个调用堆栈。但是,在严格模式中我们做不到。
function restricted() {
+ "use strict";
+ restricted.caller; // throws a TypeError
+ restricted.arguments; // throws a TypeError
+}
+implements, interface, let, package, private, protected, public, static, yield
+很多开发者喜欢如下代码风格,这在严格模式中会报错。
+function foo() {
+ "use strict";
+ return g;
+ function g() { }//throw SyntaxError
+}
+这个改变的原因是,JavaScript的Hoisting特性会让很多人迷惑:
function g() { }
+function foo() {
+ if (true)
+ function g() { }
+ return g;
+}
+ES6 Draft中引入了一个新的概念5,叫Extend Mode,然后又被撤销了6。但不幸的是,V8中已经支持了这个新模式。所以,作为事实标准,目前依赖V8的所有Javascript运行环境都有如下三个模式:
这个模式是备受争议的。这个模式的产生,也体会出制作一个标准的困难之处——你总要考虑新标准对老标准的兼容,尤其是Web技术。
+有稍微了解ES6的同学都应该清楚,module、class这些东西已经完全颠覆了传统JavaScript的很多常识。但也有不少东西,开发者是可以接受,并立马去尝试的。于是乎,关于如何让代码部分进入extended mode也就成了最初讨论的重点7。
实际表现上,node的0.11.x的版本,有些特性,仅仅使用--harmony并不能完全使用,还需加上--use_strict。在这里,已经可以看出V8团队有多纠结了8。他们也没有想清楚,该如何进入extended mode,索性,也叫strict吧。
目前仅在extended mode下可用的ES6特性:
关于ES6的特性,请参考本书的相关章节。
+http://en.wikipedia.org/wiki/Brendan_Eich
+↩http://book.douban.com/subject/3590768/
+↩http://caniuse.com/#feat=use-strict
+↩https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Strict_mode
+↩http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts
+↩https://lists.webkit.org/pipermail/webkit-dev/2011-December/018903.html
+↩https://code.google.com/p/v8/source/detail?r=10062
+↩计算机程序的运行需要对值进行造作。在编程语言中,能够表示并操作的值的类型称做数据类型。
+JavaScript的数据类型分为两类: 原始类型和对象类型。JavaScript中的原始类型包括数字,字符串和布尔值。
+1
+"hello world"
+true
+JavaScript中有两个特殊的原始值: null和undefined。
+null
+undefined
+JavaScript除了上述的就是对象了,对象是属性的集合,每个属性都由 键值对组成。
+var i = {x: 1, y: 2}
+通常对象是无序,JavaScript定义了一种特殊对象数组,他是有序的集合。
+var a[10];
+JavaScript还定义了另一种特殊的对象-函数。函数是具有与它相关联的可执行代码的对象。通过调用函数来运行执行的代码,并返回结果。
+function a(){
+ console.log("hello world");
+}
+文字:
+Rounding-error
+ var x = .3 - .2
+ var y = .2 - .1
+ x == y
+ x == .1 // => false .3 -.2 不等于 ./
+ y == .1 // => true .2 - .1 等于 .1
+布尔值指代真或假,这个类型只有两个值, 保留字true和false
+通常比较语句的结果是布尔值 例如:
+a==4
+这段代码检测a是否等于4
+任何JavaScript的值都可以转换为布尔值。下面这些值会被转换为false
+undefined
+null
+0
+-0
+NaN
+"" // the empty string
+所有其他的值,包括所有对象都会转换成true
+null表示一个特殊值,常用来描述空值。 对null执行typerof,结果返回object, 也就是null被认为是一种特殊的对象值,含义是非对象。
+JavaScript使用undefined标识变量没有初始化。例如函数没有返回值,则返回undefined。undefined是预定义的全局变量(与null不一样,它不是关键字)。
+JavaScript语言核心包括Date()构造函数, 用来创建表示日期和时间的对象。这些日期对象的方法为日期计算通过了简单的API:
+var then = new Date(2011, 0, 1); // 2011年1月1日
+var later = new Date(2011, 0, 1, 17, 10, 30) //同一天,当地时间5:10:30pm,
+var now = new Date(); //当前日期和时间
+var elapsed = now - then; //日期减法: 计算时间间隔的毫秒数
+later.getFullYear() // => 2011
+later.getMonth() // => 0 从0开始计数的月份
+later.getDate() // => 1 从1开始计数的天数
+later.getDay() // => 5 得到星期几, 0代表星期日, 5代表星期五
+later.getHours() // => 当地时间17 5pm
+later.getUTHours() // 使用UTC表示小时的时间,基于时区
+字符串是一组由16位值组成的不可变的有序序列,每个字符通常来自于Unicode字符集。
+var i = "abcdefg";
+在JavaScript字符串中,反斜线\有着特殊的用途,反斜线符号后加一个字符,就不再表示它们的字面含义了,不如\n就是一个转义字符,它表示一个换行符。
+'You\'re right, it can\'t be a quote'
+JavaScript的内置功能之一就是字符串连接:
+msg = "Hello, " + "world";
+字符串的length属性可以查看字符串的长度:
+s.length
+除了length属性, 字符串还提供许多可以调用的方法:
+var s = "hello, world" //定义一个字符串
+s.charAt(0) // => "h" 第一个字符
+s.charAt(s.length-1) // => "d" 最后一个字符
+s.substring(1, 4) // => "ell" 第2-4个字符
+s.slice(1,4) // => "ell" 同上
+s.slice(-3) // => "rld": 最后三个字符
+s.indexOf("l") // => 2 字符l首次出现的位置
+s.lastIndexOf("l") // => 10: 字符l最后一次出现的位置
+s.indexOf("l", 3) // => 在位置3及之后首次出现字符l的位置
+s.split(",") // => ["hello", "world"]分割成子串
+s.replace("h", "H") // => "Hello, world": 全文字符替换
+s.toUpperCase() // => "HELLO WORLD"
+全局对象在JavaScript中有着重要的用途: 全局对象的属性是全局定义的符号, JavaScript程序可以直接使用,当解释器启动时,它将创建一个新的全局对象,并给它一组定义的初始属性:
+看这样一个例子:
+var s = "hello world!";
+var word = s.substring(s.indexOf(" ")+1, s.length);
+字符串既然不是对象,为什么它会有属性呢?只要引用了字符串s的属性,JavaScript就会将字符串值通过调用new String(s)的方式转换成对象,
同字符串一样,数字和布尔值也有相应的方法。其他类似的包装类:
原始值是不可改变的,原始值的比较是值比较, 对象的比较并非值比较:即使两个对象包含同样的属性以及相同的值,他们也不是相等的。我们通常将对象称为引用类型,对象的值都是引用,对象的比较也是引用的比较,当且仅当他们引用同一个基独享,他们才相等;
+当期望使用一个布尔值的时候,可以提供任意类型值,JavaScript将根据需要自行转换类型。类型转换可以分为隐式转换和显式转换,所谓隐式转换即程序在运行时进行的自动转换,显式转换则是人为的对类型进行强制转换。
+通过手动进行类型转换,JavaScript提供了以下转型函数:
+转换为数值类型:Number(mix)、parseInt(string,radix)、parseFloat(string)
+转换为字符串类型:toString(radix)、String(mix)
+转换为布尔类型:Boolean(mix)
+Number(mix)函数,可以将任意类型的参数mix转换为数值类型。其规则为:
+如果是布尔值,true和false分别被转换为1和0
+ Number(true); //=> 1
+如果是数字值,返回本身。
+ Number(5); //=> 5
+如果是null,返回0.
+ Number(null); //=>0
+如果是undefined,返回NaN。
+ Number(undefined); //=> NaN
+如果是字符串,遵循以下规则:
+如果字符串中只包含数字,则将其转换为十进制(忽略前导0)
+ Number("00001"); //=> 1
+如果字符串中包含有效的浮点格式,将其转换为浮点数值(忽略前导0)
+ Number("1.234"); //=> 1.234
+如果是空字符串,将其转换为0
+ Number(""); //=> 0
+如果字符串中包含非以上格式,则将其转换为NaN
+下表列出了对象的valueOf()的返回值:
+对象 返回值
+Array 数组的元素被转换为字符串,这些字符串由逗号分隔,连接在一起。其操作 与 Array.toString 和 Array.join 方法相同。
+Boolean Boolean 值。
+Date 存储的时间是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC。
+Function 函数本身。
+Number 数字值。
+Object 对象本身。这是默认情况。
+String 字符串值。
+parseInt(string, radix)函数,将字符串转换为整数类型的数值。它也有一定的规则:
+如果指定radix参数,则以radix为基数进行解析
+ parseInt("12", 10); // => 12
+ parseInt("12", 16); // => 18
+ parseInt("1E", 10); // => 1
+ parseInt("E", 10); // => NaN
+parseFloat(string)函数,将字符串转换为浮点数类型的数值。
它的规则与parseInt基本相同,但也有点区别:字符串中第一个小数点符号是有效的,另外parseFloat会忽略所有前导0,如果字符串包 含一个可解析为整数的数,则返回整数值而不是浮点数值。
parseFloat("1.222.2") // => 1.222
+parseFloat("1.0") // => 1
+toString(radix)方法。除undefined和null之外的所有类型的值都具有toString()方法,其 作用是返回对象的字符串表示。
+对象 操作
+Array 将 Array 的元素转换为字符串。结果字符串由逗号分隔,且连接起来。
+Boolean 如果 Boolean 值是 true,则返回 “true”。否则,返 回 “false”。
+Date 返回日期的文字表示法。
+Error 返回一个包含相关错误信息的字符串。
+Function 返回如下格式的字符串,其中 functionname 是被调 用 toString 方法函数的名称:
+function functionname( ) { [native code] }
+Number 返回数字的文字表示。
+String 返回 String 对象的值。
+默认 返回 “[object objectname]”,其中 objectname 是 对象类型的名称。
+String(mix)函数,将任何类型的值转换为字符串,其规则为:
+Boolean(mix)函数,将任何类型的值转换为布尔值。
以下值会被转换为false:false、””、0、NaN、null、undefined,其余任何值都会被转换为true。
考虑一下情况:
+在某些情况下,即使我们不提供显示转换,JavaScript也会进行自动类型转换,主要情况有:
+isNaN()函数,经测试发现,该函数会尝试将参数值用Number()进行转换,如果结果为“非数值”则返回true,否则返回false。
+这些操作符适用于任何数据类型的值,针对不同类型的值,该操作符遵循以下规则(经过对比发现,其规则与Number()规则基本相同):
+加号运算操作符在JavaScript也用于字符串连接符,所以加号操作符的规则分两种情况:
+可以看出,加法运算中,如果有一个操作值为字符串类型,则将另一个操作值转换为字符串,最后连接起来。
+这些操作符针对的是运算,所以他们具有共同性:如果操作值之一不是数值,则被隐式调用Number()函数进行转换。具体每一种运算的详细规则请参 考ECMAScript中的定义。
+逻辑非(!)操作符首先通过Boolean()函数将它的操作值转换为布尔值,然后求反。
逻辑与(&&)操作符,如果一个操作值不是布尔值时,遵循以下规则进行转换:
逻辑或(||)操作符,如果一个操作值不是布尔值,遵循以下规则:
+与上述操作符一样,关系操作符的操作值也可以是任意类型的,所以使用非数值类型参与比较时也需要系统进行隐式类型转换:
+注:NaN是非常特殊的值,它不和任何类型的值相等,包括它自己,同时它与任何类型的值比较大小时都返回false。
+相等操作符会对操作值进行隐式转换后进行比较:
+需要注意的是一个值转换为另一个值并不代表两个值相等。
+在使用一个变量之前应该先声明:
+var i;
+var sum;
+也可以将声明和赋值写在一起:
+var message = "hello";
+重复声明一个变量是合法而且无害的,但是如果试图读取一个没有声明的变量值,JavaScript会报错。在非严格的模式下,如果给一个未声明的变量赋值,JavaScript会给全局对象创建一个同名属性,好像声明了一个全局变量,这样做可能会带来很多bug.
+一个变量的作用域是程序源代码中定义这个变量的区域。全局变量拥有全局作用域,函数内的变量,只有在函数体能才有定义。在函数体内,局部变量的优先级高于同名的全局变量。
+ +诚惶诚恐的写下这篇文章。用JavaScript实现继承模型,已经是非常成熟的技术,各种大牛也已经写过各式的经验总结和最佳实践。在这里,我只能就我所能,写下我自己的思考和总结。
+在阅读之前,我们先假设几个在面向对象编程中的概念是大家熟悉的:
+由于讲解这些概念是十分复杂的,所以还请参阅其他资料。
+面向对象是当代编程的主流思想。无论是C++还是Java,都是面向对象的。严格上来讲,JavaScript并不是面向对象的,而是“基于对象的”(Object-based),因为它的确缺乏面向对象里的很多特性,例如:
+但再另一方面,JavaScript是基于原型(Prototype)的对象系统。它的继承体系,叫做原型链继承。不同于继承树形式的经典对象系统,基于原型的对象系统中,对象的属性和方法是从一个对象原型(或模板)上拷贝或代理(Delegation)的。JavaScript也不是唯一使用这种继承方法的编程语言,其他的例子如:
+那么,prototype在哪里呢?
// 访问Array的原型
+Array.prototype
+// 访问自定义函数Foo的原型
+var Foo = function() {}
+Foo.prototype
+__proto__不是标准属性,但是被大多数浏览器支持
var a = {}
+a.__proto__;
+使用ES5的Object.getPrototypeOf:
Object.getPrototypeOf([]) === Array.prototype;
+再来点绕弯的:
+[].constructor.prototype === Array.prototype
+new关键字大多数面向对象语言,都有new关键字。他们大多和一个构造函数一起使用,能够实例化一个类。JavaScript的new关键字是异曲同工的。
等等,不是说JavaScript不支持经典继承么!的确,其实new的含义,在JavaScript中,严格意义上是有区别的。
当我们,执行
+new F()
+实际上是得到了一个从F.prototype继承而来的一个对象。这个说法来自Douglas的很早之前的一篇文章1。在如今,如果要理解原型继承中new的意义,还是这样理解最好。
如果我们要描述new的工作流程,一个接近的可能流程如下:
constructor和F.prototype上的各式方法、属性。注意,这里执行的并不是拷贝,而是代理。后文会讲解这点。this指向这个对象),并执行构造函数我们来定义一个简单的“类”和它的原型:
+var Foo = function() {};
+Foo.prototype.bar = function() {
+ console.log("haha");
+};
+Foo.prototype.foo = function() { console.log("foo"); };
+我们在原型上定义了一个bar方法。看看我们怎么使用它:
var foo = new Foo();
+foo.bar(); // => "haha"
+foo.foo(); // => "foo"
+我们要继承Foo:
var SuperFoo = function() {
+ Foo.apply(this, arguments);
+};
+SuperFoo.prototype = new Foo();
+SuperFoo.prototype.bar = function() {
+ console.log("haha, haha");
+};
+var superFoo = new SuperFoo();
+superFoo.foo(); // => "foo"
+superFoo.bar(); // => "haha, haha"
+注意到几个要点:
+SuperFoo中,我们执行了父级构造函数SuperFoo中,我们让然可以调用foo方法,即使SuperFoo上没有定义这个方法。这是继承的一种表现:我们可以访问父类的方法SuperFoo中,我们重新定义了bar方法,实现了方法的重载我们仔细想想第二点和第三点。我们新指定的bar方法到底保存到哪里了?foo方法是如何找到的?
要回答上面的问题,必须要介绍原型链这个模型。相比树状结构的经典类型系统,原型继承采取了另一种线性模型。
+当我们要在对象上查找一个属性或方法时:
+prototype对象上查找,如果没有找到进行下一步prototype对象作为当前对象;如果当前对象存在prototype,就能继续,否则不存在则查找失败,退出;在该对象上查找,如果没有找到,将前面提到的“当前对象”作为起始对象,重复步骤3这样的递归查找终究是有终点的,因为:
+Object.prototype.__proto__ === null
+也就是Object构造函数上,prototype这个对象的构造函数上已经没有prototype了。
我们来看之前Foo和SuperFoo的例子,我们抽象出成员查找的流程如下:
superFoo本身 => SuperFoo.prototype => Foo.prototype => Object.prototype
+解读原型链的查找流程:
+superFoo本身意味着superFoo这个实例有除了能够从原型上获取属性和方法,本身也有存储属性、方法的能力。我们称其为own property,我们也有不少相关的方法来操作:SuperFoo.prototype:SuperFoo.prototype = new Foo();,也就是说SuperFoo.prototoye就是这个新创建的这个Foo类型的对象Foo.prototype上的方法和属性了Foo.prototype:SuperFoo.prototype的值,回想上一条Object.prototypeObject.prototype这个对象的原型是null,我们无法继续查找toString那么,当在SuperFoo上添加bar方法呢?这时,JavaScript引擎会在SuperFoo.prototype的本地添加bar这个方法。当你再次查找bar方法时,按照我们之前说明的流程,会优先找到这个新添加的方法,而不会找到再原型链更后面的Foo.prototype.bar。
也就是说,我们既没有删掉或改写原来的bar方法,也没有引入特殊的查找逻辑。
基本到这里,继承的大部分原理和行为都已经介绍完毕了。但是如何将这些看似简陋的东西封装成最简单的、可重复使用的工具呢?本文的后半部分将一步一步来介绍如何编写一个大体可用的对象系统。
+准备几个小技巧,以便我们在后面使用。
+如果要以一个对象作为原型,创建一个新对象:
+function beget(o) {
+ function F() {}
+ F.prototype = o;
+ return new F();
+}
+var foo = beget({bar:"bar"});
+foo.bar === "bar"; //true
+理解这些应该困难。我们构造了一个临时构造函数,让它的prototype指向我们所期望的原型,然后返回这个构造函数所创建的实例。有一些细节:
A.prototype = B.prototype这样的事情,因为你对子类的修改,有可能直接影响到父类以及父类的所有实例。大多数情况下这不是你想看到的结果F的实例,创建了一个本地对象,可以持有(own)自身的属性和方法,便可以支持之后的任意修改。回忆一下superFoo.bar方法。如果你使用的JavaScript引擎支持Object.create,那么同样的事情就更简单:
Object.create({bar:"bar"});
+要注意Object.create的区别:
Object.create(null)Object.create的文档2JavaScript的函数可以在运行时很方便的获取其字符串表达:
+var f = function(a) {console.log("a")};
+f.toString(); // 'function(a) {console.log("a")};'
+这样的能力其实时很强大的,你去问问Java和C++工程师该如何做到这点吧。
+这意味着,我们可以去分析函数的字符串表达来做到:
+thisJavaScript中的this是在运行时绑定的,我们往往需要用到这个特性,例如:
var A = function() {};
+A.methodA = function() {
+ console.log(this === A);
+};
+A.methodA();// => true
+以上这段代码有如下细节:
+A.methodA()运行时,其上下文对象指定的是A,所以this指向了Athis引用到类(构造函数)本身单纯实现一个extend方法:
var extend = function(Base) {
+ var Class = function() {
+ Base.apply(this, arguments);
+ }, F;
+ if(Object.create) {
+ Class.prototype = Object.create(Base.prototype);
+ } else {
+ F = function() {};
+ F.prototype = Base.prototype;
+ Class.prototype = new F();
+ }
+ Class.prototype.constructor = Class;
+ return Class;
+};
+
+var Foo = function(name) {
+ this.name = name;
+};
+Foo.prototype.bar = function() {
+ return "bar";
+};
+
+var SuperFoo = extend(Foo);
+var superFoo = new SuperFoo("super");
+console.log(superFoo.name);// => "super"
+console.log(superFoo.bar());// => "bar"
+由于过于简单,我就不做讲解了。
+XObjectextend,但是会有修改var extend = function(Base) {
+ var Class = function() {
+ Base.apply(this, arguments);
+ }, F;
+ if(Object.create) {
+ Class.prototype = Object.create(Base.prototype);
+ } else {
+ F = function() {};
+ F.prototype = Base.prototype;
+ Class.prototype = new F();
+ }
+ Class.prototype.constructor = Class;
+ return Class;
+};
+
+var merge = function(target, source) {
+ var k;
+ for(k in source) {
+ if(source.hasOwnProperty(k)) {
+ target[k] = source[k];
+ }
+ }
+ return target;
+};
+
+// Base Contstructor
+var XObject = function() {};
+
+XObject.extend = function(props) {
+ var Class = extend(this);
+ if(props) {
+ merge(Class.prototype, props);
+ }
+
+ // copy `extend`
+ // should not use code like this; will throw at ES6
+ // Class.extend = arguments.callee;
+ Class.extend = XObject.extend;
+
+ return Class;
+};
+
+
+var Foo = XObject.extend({
+ bar: function() { return "bar"; },
+ name: "foo"
+});
+
+var SuperFoo = Foo.extend({
+ name: "superfoo",
+ bar: function() { return "super bar"; }
+});
+
+var foo = new Foo();
+console.log(foo.bar()); // => "bar"
+console.log(foo.name); // => "foo"
+
+var superFoo = new SuperFoo();
+console.log(superFoo.name); // => "superfoo"
+console.log(superFoo.bar()); // => "super bar"
+上面的例子中,
+XObject是我们对象系统的根类XObject.extend可以接受一个包含属性和方法的对象来定义子类XObject的所有子类,都没有定义构造函数逻辑的机会!真是难以接受的:init方法来初始化对象,而将构造函数本身最简化init方法绕开了工厂方法的实现过程中,参数传递如何传递到构造函数的问题super属性、mixin特性等我们解决了一部分问题,又发现了一些新问题。但本文的主要内容在这里就结束了。一个更具实际意义的对象系统,实际随处可见,Ember和Angular中的根类。他们都有更强大的功能,例如:
但是,这些框架中对象系统的出发点都在本文所阐述的内容之中。作为教学,John Resig在2008年的一篇博客中3总结了一个现代JavaScript框架中的对象系统的雏形。
+我创建了docco代码注解来立即这段代码,本文也会结束在这段代码的注解。强力推荐大家去阅读该注解文档。
+还有一些更高级的话题和技巧,会在另外一篇文章中给出。
+