学习ECMAScript标准和具体实现-JavaScript
时间 2016-03-27 16:42:59 阿潘道
原文 http://apsay.com/?p=1618
主题 ECMAScript JavaScript
ECMAScript是实现一种编程语言的标准理论。实践总是先于理论。ECMAScript标准理论主要来自JavaScript的实际应用。Mozilla基金会是JavaScript的官方组织,Mozilla Developer Network网站有JavaScript相关资料文档。因为MDN的文档是众包形式编辑,事后审查。微软的JavaScript文档也不错,而且翻译可靠,大公司就是比基金会财大气粗啊,都是官方出钱翻译。如果有拿不准的地方,以ECMAScript标准规范文档为准。
学习流程,请按顺序阅读,微软和MDN的JavaScript教程可以速读,后面的文章要精读,深入学习ECMAScript理论知识。在阅读示例代码时,有语句上不懂得地方,可以查阅微软和MDN的JavaScript 参考文档和ECMAScript标准文档。
尽量阅读英文资料,如果实在不懂,也最好先用谷歌翻译对着原文过一遍,再读翻译后的,特别是那些没有进过校验审核的翻译。阅读中文翻译资料的时候,如果遇到阅读不顺畅或者有歧义的词句,一定要查查字典,百科,文档的英语原文解释。
JavaScript编程知识学习:
MSDN JavaScript 基础 https://msdn.microsoft.com/zh-cn/library/6974wx4d(v=vs.94).aspx
MSDN JavaScript 高级 https://msdn.microsoft.com/zh-cn/library/b9w25k6f(v=vs.94).aspx
MDN JavaScript 指南 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide
ECMAScript标准理论知识学习:
Novtopro的文章
http://novtopro.coding.io/2015/10/08/understanding-javascript-execution-context/
http://novtopro.coding.io/2015/10/08/understanding-javascript-lexical-environment-variable-environment/
Dmitry Soshnikov 的 JavaScript 系列文章
ECMA-262-3 in detail. http://dmitrysoshnikov.com/tag/ecma-262-3/ 翻译 bubkoo http://bubkoo.com/tags/ecmascript/
ECMA-262-5 in detail. http://dmitrysoshnikov.com/tag/es-5/
Jason Orendorff的ES6 In Depth https://hacks.mozilla.org/category/es6-in-depth/ 翻译 刘振涛 深入解析 ES6 http://www.infoq.com/cn/es6-in-depth/
Dr. Axel Rauschmayer http://exploringjs.com/ 翻译 https://github.com/es6-org/exploring-es6
文档手册:
MDN的JavaScript 参考文档 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference
微软MSDN的JavaScript 参考文档 https://msdn.microsoft.com/zh-cn/library/yek4tbz0(v=vs.94).aspx
ECMAScript最新标准文档 https://tc39.github.io/ecma262/ 老标准的中文翻译 http://yanhaijing.com/es5/
在线运行示例代码的环境:
repl.it https://repl.it/languages 请注意区分JavaScript/JavaScript(Web)/ES2015/Nodejs的不同。
codepen http://codepen.io/
转码器 Babel/Traceur https://github.com/ES-CN/es6-tools
因为JavaScript引擎/虚拟机/解释器对ECMAScript新标准的支持总是滞后的,所以需要转码器把程序员按新标准写的JavaScript转换成现在宿主环境(浏览器/服务器)已支持的老标准写法。
转码流程:第一种,提前在开发环境,把ES新标准源码转换成老标准源码后,提交到生产环境加载使用。比如src to lib;第二种,在生产环境实时转码。比如浏览器先加载转码器JS,然后加载新标准JS,并标示是需要转码的JS(Traceur:type=”module”)。 https://github.com/google/traceur-compiler/wiki/Getting-Started
学习总结,灵活性太高的代价,就是稳定性太差,容易出错。简单灵活的编程语言,其发展路径往往是越来越复杂的语法,更明确和严格的规范。
添加到 ES新标准中的新特性并不是无章可循,多数在其他语言中已有或被JS第三方类库已实现,而且被证明很有用。
学习ES新标准把One JavaScript同一个JavaScript,Lexical Environment词法环境,Iteration Protocols迭代协议,Internal Methods标准内部方法,Syntactic sugar 语法糖,这些概念理解透了,由此而新增加的相关语法规则,对象,关键字,运算符等就都知道了。
One JavaScript: avoiding versioning in ECMAScript 同一个JavaScript:在ECMAScript中防止出现多版本(兼容原则)。基于兼容原则,以前那些被成为JavaScript毒瘤的东西都会保留。标准委员会采用了严格模式的方式来清理。在新标准中新增加的特性的句法结构里都默认使用严格模式,比如模块。因为One JavaScript,各种新旧规则太多,都是些陈述性记忆性知识,需要的时候查下资料就行了。
http://www.2ality.com/2014/12/one-javascript.html
Implementation 大意是某个抽象事物的realization or execution。比如JavaScript以及其引擎是ECMAScript的Implementation。不同的JavaScript引擎实现方式也是不一样的。在Dmitry Soshnikov的文章里多次用到SpiderMonkey implementation,another implementation, eg Chrome’s V8等词句,在出现implementations复数的地方,也列出了各种JavaScript解释器引擎。
https://en.wikipedia.org/wiki/Implementation
ECMAScript 是基于对象的:基本语言和宿主设施都由对象提供,ECMAScript 程序是一组可通信的对象。ECMAScript 对象 (objects) 是 属性 (properties) 的集合,每个属性有零个或多个 特性 (attributes),它确定怎样使用此属性。属性是持有其他 对象 (objects), 原始值 (primitive values), 函数 (functions) 的容器。函数是可调用对象 (callable object)。A function that is associated with an object via a property is called a method。(注意:ES里的方法概念是指一个被赋值给一个对象属性,并且通过这个对象的属性调用的函数。请一定不要用Class-based programming里的方法来想当然。)
虽然新ECMAScript的语法加入了类术语,但是ECMAScript对象从根本上就不是基于类。不同于类,对象可以通过多样的方式创建,包括通过字面量符号,构造器 创建对象,然后执行代码,这段代码对它们的属性分配初始值,初始化它们的全部或一部分。每一个构造器都是一个函数,这个函数有一个名字叫做”prototype” 原型的属性,这个属性是用来实现基于原型对象的属性继承和共享。
在new表达式里,对象的创建是通过使用构造器。 每一个通过构造器创建的对象,它的构造器的原型属性在内部自动赋值了一个原型对象的引用(obj.constructor.prototype=prototypeObj),并且这个原型对象可能也有个不是null的原型对象引用,依此类推,这就叫做原型链。原型链也是分级的(hierarchical),然而由于动态性,它可以很容易地重新排列,从而改变层级和结构。
基于原型编程(prototype-based programming)是面向对象编程的一种方式。这种编程模型也可以叫做原型方式,面向原型,无类或者基于实例的编程。委托机制是支持基于原型编程语言的特征。
在基于原型编程的语言中使用委托机制delegation,语言运行时能够调用正确的方法或者找到正确的数据块,只是通过跟踪来自对象原型的委托指针delegation pointers序列或delegation links委托链接序列,直到发现一个匹配的。
ECMAScript就是依靠委托机制实现了基于原型对象的属性继承和共享。在ECMAScript里Cloning是指一个对象link到另一个对象。那种原样复制创建另一个对象的属性数据的实现叫做concatenative prototyping。新标准的Object.assign方法提供了几乎完全复制属性的功能。完全克隆:
function clone(orig) {
let origProto = Object.getPrototypeOf(orig);
return Object.assign(Object.create(origProto), orig);
}
学习ECMAScript的核心就是要理解这种委托机制,不管是Prototype Chain,还是Lexical Environment/Scope Chain,都是这种委托机制的具体实现。
一个对象是一个属性集合,并拥有一个独立的 prototype原型对象。这个 prototype 可以是一个对象或者 null。通过 Object.create(null) 方法可以得到prototype原型是null的对象。就像纯粹的哈希表。
Property是指对象的属性,而Attribute是指Property的属性,比如Attributes of a Data Property有[[Value]],[[Writable]],[[Enumerable]],[[Configurable]];Attributes of an Accessor Property有[[Get]],[[Set]],[[Enumerable]],[[Configurable]]。Dmitry也说,So from this viewpoint a property is as an object itself。property’s attributes的大意是属性对象的属性。property是一个属性对象,对象当然可以有自己的属性。MDN的翻译,是把Attribute翻译成特性,在文档中有些地方也用flavors代替attributes,而且和属性描述符descriptor概念息息相关。
一个“属性(property)”的概念在语义上并不细分为“键(key)”,“数组索引(array index)”,“方法(method)”或“属性(property)”。它们都是property属性。属性访问器(即 . 和 [])之间没有语义上的区别。
属性的读写是通过内部方法 [[Get]] 和 [[Put]] 来实现。这两个方法是通过调用属性访问器 —— 点符号或方括号。
除了对象的自有属性外,[[Get]] 方法也考虑到了对象原型链中的属性。因此原型中的属性也像对象的自有属性一样可以被访问到。
如果对原始值使用属性访问器取值,访问之前会先对原始值进行对象包装,然后通过包装的对象进行访问属性,属性访问以后,包装对象就会被删除。
对象有一些内部属性,这些属性是(编程语言实现引擎的一部分)a part of implementation,不能在 ECMAScript programs 中直接得到(一些引擎也允许访问其中的一些属性)。这些属性有外加两个方括号[[ ]] 。
对象的 prototype 是以内部的 [[Prototype]] 属性来引用的。 对象的[[Prototype]] 内部属性和构造器的prototype 属性是不同的。实例对象创建时的 [[Prototype]] 是从构造器的 prototype 属性上获得值,但是,对于构造器的 prototype 属性的重置不会影响到已创建对象的原型。改变的只是构造器的 prototype 属性!
由于实例对象的原型是独立于它的构造函数和构造函数的 prototype 属性的,构造函数在完成了它的主要目的 – 创建对象 – 之后可以被删除。原型对象将仍然存在,并通过 [[Prototype]] 属性引用.
ES 中的 Object.getPropertyOf(o) 方法,可以直接返回一个对象的 [[Prototype]] 属性 —— 实例的初始原型。然而和 __proto__ 不同,这个方法只是一个 getter,它不允许设定原型。JavaScript引擎SpiderMonkey,提供了对于对象原型的显示引用,通过一个非标准的 __proto__ 属性。
“foo instanceof Foo” instanceof 运算符只是获取左边实例对象的原型链 —— foo.[[Prototype]],检查原型链中是否有运算符右边的对象构造函数的原型 ——Foo.prototype。
为了内存占用的性能优化,方法通常定义在原型中。这意味着,通过一个构造器创建的所有实例对象,总是共用相同的方法。
新标准提供了Proxy object来自定义这些内部方法的行为。在代理类的捕获器方法内,如果需要用内部方法的默认行为,可以使用Reflect类,它是个类似Math的工具类,所有方法都是静态方法,直接使用类名.方法调用。
ECMAScript标准的内部方法 https://tc39.github.io/ecma262/#table-5
不要用C++,JAVA的术语体系来理解ECMAScript。ECMAScript是无类的classless。新标准提供了class等Class-based programming的语句,我反对,这纯粹是为了方便Class-based programming程序员的语法糖,作为曾经的AS3程序员,真不希望JavaScript变成另一AS3,那真是没了Script只有Java了。ECMAScript对象和Class-based programming里类的对象没有一点关系。Class-based programming基于类的编程是面向对象编程的另外一种方式。
原型委托实现写法:
// Define the PrototypeA constructor 定义原型对象A的构造函数
function PrototypeA() {
}
// Define the PrototypeB constructor 定义原型对象B构造函数
function PrototypeB() {
PrototypeA.call(this); // PrototypeB call PrototypeA constructor. 先调用原型对象A的构造函数,把原型对象A的初始化代码执行一遍,但是this是PrototypeB。
}
// 原型委托实现
PrototypeB.prototype = Object.create(PrototypeA.prototype); //把PrototypeB的原型对象属性赋值为一个新对象,这个新对象的原型是PrototypeA的原型对象。PrototypeB加入PrototypeA的原型链。
PrototypeB.prototype.constructor = PrototypeB; //把这个新对象的构造函数改为自己的,不改就还是PrototypeA的构造函数。prototypeB.constructor===PrototypeB.prototype.constructor===PrototypeB
var prototypeB = new PrototypeB();
类继承语法糖写法:
class Superclass {
constructor() {
}
}
class Subclass extends Superclass {
constructor() {
super();
}
}
https://tc39.github.io/ecma262/#sec-ecmascript-overview
http://yanhaijing.com/es5/#6
https://en.wikipedia.org/wiki/Prototype-based_programming
https://en.wikipedia.org/wiki/Delegation_(programming)
标识符identifier:变量名、函数名、形参,等等。
grammatical语法的,syntactic句法的,Lexical词法的。
JavaScript编译的过程包括:1.Lexing(tokenizing)2.Parsing 3.Compiling。代码执行之前,编译器对代码进行编译,在编译的Lexing / Tokenizing阶段,就确定所有的标识符是在哪声明、如何声明以及在执行阶段如何解析和查找。Lexical Scope是在Lexing / Tokenizing阶段被确定下来的作用域,所以Lexical Scope也被称为Static Scope。Lexical Scope可以等同于Lexical Environment。
词法环境和环境记录数据的值,只是纯粹理论上的空架子,不必对应到具体的ECMAScript实现(JavaScript解释器)中,在ECMAScript程序中不可能直接取得或者操作这样的数据值。Lexical Environments and Environment Record values are purely specification mechanisms and need not correspond to any specific artefact of an ECMAScript implementation. It is impossible for an ECMAScript program to directly access or manipulate such values.
一个词法环境是一个规范类型,根据ECMAScript代码的词法嵌套结构来定义标识符和具体的变量和函数的关联关系。一个词法环境由一个环境记录和一个外层词法环境(外层词法环境可能是null)组成。 通常一个词法环境与ECMAScript代码的一些具体句法结构有关,比如每次a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement这些代码的词法分析完成,一个新的词法环境就创建完成。
A global environment 是一个词法环境,它没有外层词法环境,它的外层是null。
A module environment 是一个词法环境,它的外层环境是全局环境global environment。
A function environment是一个词法环境,它可能建立一个新的this标识符绑定关系。
在realm领域的词法分析完成前,所有ECMAScript代码必须和一个领域关联。概念上,一个领域包括一组内部对象,一个ECMAScript全局环境-在这全局环境范围里面所有的 ECMAScript代码已经加载完毕,和其他相关的资源与状态。
在语句块词法分析的时候,一个新的声明环境记录被创建,并且块范围内每一个变量,常量,函数,函数生成器都被绑定,声明的类也会被实例化。
我觉得environment那套理论比之前的更容易理解些。这也是为什么在新标准中彻底废弃了scope相关概念,改用environment相关概念来表述。
Executable Code and Execution Contexts
可执行的代码和代码的执行上下文(脉络)。为了和Environment概念区分,Contexts不建议翻译成环境,脉络一词不错,能表达出代码按句法结构执行的时间流的意思。台湾人多数不翻译,还是别翻译了,Execution Contexts一般缩写为EC。
EC是纯粹的规范上的机制。ECMAScript code不可能直接访问或者观察一个EC。
An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript 解释器(比如V8)。在时间的任何点上,只存在一个EC,那是事实上正在执行的代码。这也叫做运行中的EC。
EC栈是用来跟踪EC的。运作中的EC始终是栈顶的元素。
执行中的代码有三种状态,perform执行, suspend暂停, and resume继续。生成器就是这种机制。
JavaScript解释器中每一个EC的执行都会经历以下两个阶段:1.创建阶段(Creation Stage)2.激活/运行阶段(Activation Stage / Code Execution Stage)
在EC Creation Stage,完成当前EC中的函数形参赋值,函数声明,变量声明等,此阶段赋值都是默认值undefined。注意语句块EC中的变量在声明语句前不可以被使用。
ECMAScript对象有两个链,一个是environment/scope chain(代码嵌套形式),一个是prototype chain (原型属性赋值引用形式)。
规范对于environment/scope chain的抽象定义,environment/scope chain是一个对象列表。它和EC有关,是所有EC数据对象的列表,用于在处理标识符时候进行变量查询。
在查询过程中,environment中的局部变量比外层environment的变量会优先被采用。
如果一个属性在对象中没有直接找到,查询将在原型链中继续。即常说的二维链查找。(1)environment/scope chain 环节;(2)prototype chain原型链环节。
当一个函数在其代码中引用的标识符不是内部变量/内部函数/形参,那么这个标识符就称为自由变量,查找这些自由变量时就需要用到chain。
ECMAScript的函数参数传递策略。学术上明确说法应该是按共享传递,原始值是传递值的拷贝副本,对象是传递引用地址的拷贝副本。引用传递是把一个值是对象引用的变量传递给函数,函数内的实参名是这个变量的别名,如果把实参重新赋值成另一个对象引用,也会改变外部这个变量的引用。而共享传递是拷贝了一份变量的对象引用地址传递进去了,在函数内的实参和外部变量本身已经没有任何关系了,只不过它们的值,是同一个对象的引用而已。
闭包是代码块和创建该代码块的EC中数据的结合。在 ECMAScript 中“代码块”就是函数。
对 ECMAScript 中的闭包作两个定义(即两种闭包):
从理论角度:所有的函数都是闭包。因为它们都在创建时保存了外层EC的数据。函数中使用全局变量也是在使用自由变量,用到了最外层EC数据。
从实践角度:闭包是创建时的外层EC已经销毁,但保存了外层EC数据,并使用了外层EC中变量的函数。
对于要实现将局部变量在EC销毁后仍然保存下来,基于栈的实现显然是不适用的(因为与基于栈的结构相矛盾)。 因此在这种情况下,上层作用域的闭包数据是通过动态分配内存的方式来实现的(基于“堆”的实现),配合使用垃圾回收器(garbage collector 简称 GC)和引用计数(reference counting)。这种实现方式比基于栈的实现性能要低,然而,任何一种实现总是可以优化的:可以分析函数是否使用了自由变量,函数式参数或者函数式值,然后根据情况来决定 —— 是将数据存放在栈中还是堆中。
https://tc39.github.io/ecma262/#sec-executable-code-and-execution-contexts
http://dmitrysoshnikov.com/ecmascript/es5-chapter-3-1-lexical-environments-common-theory/
http://yanhaijing.com/es5/#120
https://en.wikipedia.org/wiki/Scope_(computer_science)
内置对象和原生对象是由 ECMAScript 规范和实现器来定义的,它们之间的区别并不大。原生对象(native objects)是指由 ECMAScript 实现引擎提供的全部对象(其中一些是内助对象,另一些可以是在程序扩展中创建的,比如用户定义的对象)。
内置对象(built-in objects)是原生对象的子类型,它们会在程序开始前预先建立到 ECMAScript 中(比如parseInt,Math 等等)。
宿主对象(host objects)是由宿主环境(通常是一个浏览器)提供的对象,比如 window,alert 等。
注意,宿主对象可能是 ES 自身实现的,完全符合规范的语义。从这点来说,他们能称为“原生宿主”对象(尽快很理论),不过规范没有定义“原生宿主”对象的概念。
ECMAScript标准中定义了十种数据类型。
在编程语言引擎实现级别才能使用(accessible only at implementation level)的三种:
Reference (解释诸如 delete,typeof,this 等运算,它由一个基本对象(base object)和属性名组成。)
List (解释参数列表的行为,在 new 表达式和函数调用中)
Completion(解释 break,continue,return 和 throw 语句的行为。)
在程序编程中能直接使用(directly accessible)的七种:Undefined Null Boolean Number String Symbol Object
前六种是原始值类型。Object 类型是唯一用来表示 ECMAScript 对象的类型。Object 是一种无序的键值对的集合。虽然“对于(typeof 运算中)null 的值应该返回 object 字符串”来实现的(bug in ECMAScript, should be null),但ECMA-262-3 中定义 null 的类型为 Null。
undefined和null,都是程序中一个唯一的只读数据,内存中专门指定了一个地址存放这个数据,所以在程序中它们都是相等。变量声明后,如果没有赋值语句,则默认值就是undefined。它们的作用用来告诉解释器两种特殊情况:undefined是指解释器认为开发者不知道没有赋值或者开发者故意玩解释器也行,通常会抛出ReferenceError;null是指解释器认为开发者知道值为null或者认为开发者应该知道。
var x; var y; console.log(x===y);//true
console.log(undefined===undefined);//true
var x=null; var y=null; console.log(x===y);//true
console.log(null===null);//true
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/undefined
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/null
NaN和undefined、null的意思类似,也是表示一种特殊情况,在希望得到正常数字的地方,得到了一个当前语言不能表示的数。NaN是根据一个规则计算得到的值,所以NaN不等于NaN,因为每个NaN的内存地址是不一样的,但是却又在NaN的计算规则范围内,所以解释器可以识别这一类特殊数据。 “not a number”, has a specific meaning for numbers represented as IEEE-754 floating-point values.
console.log(NaN===NaN);//false
console.log(typeof NaN); //number
https://en.wikipedia.org/wiki/NaN
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
规范定义了四个对于原始值特殊包装类:Boolean-object String-object Number-object Symbol-object。这些对象的创建,是通过相应的内置构造器创建,并且包含原生值作为其内部属性。原始数据类型创建一个显式包装器对象从 ES6 开始不再被支持。 然而,现有原始包装对象,如 new Boolean、 new String以及new Number因为遗留原因仍可被创建。
对象转换为原始值可以通过valueOf/toString方法。返回默认值根据对象的类型而定。类型转换可以通过显示使用构造函数(constructor as a function)者new 运算符operator 。在使用一些运算符时,也可能会发生显式和隐式的类型转换。
Symbol 的设计初衷是为了避免冲突。Symbol 类型的值可以作为对象的属性名,且不与任何其它值相等,不会发生冲突。和数组一样,symbol-keyed 属性不能通过 . 运算符来访问,必须使用[]运算符。
Iteration protocols迭代协议是指两类协议:可遍历(可迭代)协议 iterable protocol和 迭代器模式(迭代子模式)协议iterator protocol。可遍历协议具体到实现就是有一个Symbol.iterator接口方法,方法返回一个迭代器iterator。迭代器模式协议具体实现就是迭代器iterator对象,它有一个next()接口方法并且方法返回一个有done和value属性的数据对象。
为了变成可遍历对象iterable, 一个对象必须实现 @@iterator 方法, 意思是这个对象(或者它原型链prototype chain上的某个对象)必须有一个名字是 Symbol.iterator 的属性.当一个对象需要被遍历的时候,它的@@iterator方法被调用并且无需参数,然后返回一个用于在遍历中获得值的迭代器iterator 。
如果一个可遍历对象的@@iterator方法不是返回一个迭代器对象,那么它就是一个non-well-formed 可遍历对象 。使用它会出现运行时异常或者buggy行为。
可以为任何对象实现一个 myObject[Symbol.iterator]() 方法。比如使所有的 jQuery 对象都支持 for-of 语句,jQuery.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
为什么[Symbol.iterator]()语法看起来如此奇怪?问题的关键在于方法名,本来正常方法命名可以是 iterator(),但是历史代码可能已经存在名为“iterator”的方法,这将导致代码混乱,违背了最大兼容性原则。所以,标准委员会引入全新的 Symbol类型,用Symbol.iterator 作为属性名,并用[]形式调用迭代方法,解决了兼容问题。
本来就奇怪,所以写法尽量用易懂的方式,比如MDN官方这种写法someString[Symbol.iterator] = function() {}, myIterable[Symbol.iterator] = function* () {}。很清晰的表明给一个对象的一个叫Symbol.iterator的属性赋值一个函数。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols
A generator object is both, iterator and iterable一个生成器对象既是迭代器对象也是可遍历对象。使用Generator 生成器是一种更简洁可靠的实现自定义迭代器的方式。它就像是迭代器的生产工厂。生成器对象的原型是迭代器对象,所以迭代器的属性方法都有,生成器实例的next()得到有done和value属性的数据对象。
通过generator function可以得到一个生成器对象,具体语法一般用function* 表达式。在generator function里面,不用写迭代器的next()方法和维护done的值,只用写业务逻辑和value。用yield 表达式 return value。yield* 表达式还可以返回另一个iterable object,并且开始这个可迭代对象的迭代器的单步序列。生成器的next()可以传参数给生成器,作为上一个yield表达式的值,所以第一个next()传参是没用的。示例代码:
function* GenFun() {
let item = (yield "yield1");
console.log(item);//next2
yield "yield2";
}
let genObj = GenFun();
console.log(genObj.next("next1"));//{ value: 'yield1', done: false }
console.log(genObj.next("next2"));//{ value: 'yield2', done: false }
生成器实例另外还提供gen.return(value),这个方法相当于直接执行生成器的最后一个yield 表达式,并且返回一个自定义的值,和最后一个yield表达式的value一样或者不一样都行,并且修改done的值为true。需要注意,如果done已经是true,再用return()方法,自定义值无效,和next()一样都是返回undefined。还有个gen.throw(exception)方法,抛出个错误。
Promise和Generator很像,MDN参考文档也把它们放一块,如果说Generator是封装了迭代器的细节实现代码,开发者只需要关注于逻辑和结果,那么Promise也是这样。简化了异步回调的实现。Generator可以和Promise结合使用实现Async Functions。
https://tc39.github.io/ecmascript-asyncawait/
String, Array, TypedArray, Map and Set 是所有内置可遍历对象, 因为它们的原型对象都有一个 @@iterator 方法.
the for-of loops, spread operator, yield*, and destructuring assignment通常用于可遍历对象操作。
spread operator允许一个表达式在某处展开,在多个参数(用于函数调用)或者多个元素(用于数组字面量)或者多个变量(用于解构赋值)的地方就会这样。…运算符只有用于可遍历对象才有效。
var obj = {"key1":"value1"};
function myFunction(x) {
console.log(x); // undefined
}
myFunction(...obj);
var args = [...obj];
console.log(args, args.length) //[] 0
Destructuring assignment解析赋值表达式,就是从表达式右边的数组或者对象结构中自动解析数据,并把解析得到的数据赋值给对应的变量。注意,如果左边的变量是数组方括号结构,则右边必须是可遍历对象。如果左边的对象花括号结构,则右边只要是对象就行,原始值有包装对象的会强制转换,也就是说除 null 和 undefined 外任何值。记住Object类不是iterable,当然你可以把它变成iterable。在MDN手册中,Destructuring assignment解析赋值也归类于Assignment operators赋值运算符。
let [num,...bars] = [1, 2, 3];
console.log(bars);// [ 2, 3 ]
let [,,bar] = [1, 2, 3];
console.log(bar);//3
在NDN的JavaScript Guide里,Array和Map,Set都属于collections of data。它们的区别就是,Array是ordered by an index value, Map,Set是ordered by a key。Map类似无属性的Object,Set类似Array,但是数据存取效率更高,适合大量数据。Map and Set的遍历顺序就是插入数据的顺序,先插入的先出来(FIFO—first in first out)。
Map and Set 用new生成实例的时候,构造函数参数只有是可遍历对象才能转换为集合数据,注意Map传入的可遍历对象的迭代器方法返回的value必须是能转换成key-value pair形式。示例代码:
function* gen() { yield 1; }
let g = gen();
let mapData = new Map(g); //TypeError: Iterator value 1 is not an entry object
function* gen() { yield [1]; }
let g = gen();
let mapData = new Map(g);
console.log([...mapData]); //[ [ 1, undefined ] ]
function* gen() { yield {"1":1}; }
let g = gen();
let mapData = new Map(g);
console.log([...mapData]); //[ [ undefined, 1 ] ]
var myArray = ["value1"];
var mySet = new Set(myArray);
let mapData = new Map(mySet[Symbol.iterator]()); //TypeError: Iterator value value1 is not an entry object
var kvArray = [["key1", "value1"]];
var myMap = new Map(kvArray);
let mapData = new Map(myMap[Symbol.iterator]());
console.log([...mapData]); //[ [ 'key1', 'value1' ] ]
Map用set方法插数据,key相等的只插一次。把Map自定义Key的行为去掉,就是Set,Set用add方法插值,在一个Set数据里相等的值只插一次。Map,Set的相等判断规则和Object.is()一样。
遍历Map得到数据是数组[key, value]形式。遍历方法一般用for…of ,Map写法for (let [key, value] of mapData){},Set写法for (let item of setData){};二般用迭代器,用Map,Set实例的迭代器方法,比如maporset[Symbol.iterator]() 可以得到这个实例的迭代器,用迭代器的next()方法可以按FIFO顺序单步得到集合的数据。除了遍历外,Map还可以通过get方法,Set不知道key没有get方法。
WeakMap,WeakSet是为了更无脑的GC创造的,就是弱引用的Map和Set,它们不是可遍历对象,不能遍历。Map的键名和Set的值只能是Object类型。
为了块级词法环境的具体实现,于是有了let和const,为什么不直接重新定义var,而搞个let,为了One JavaScript.
let和const声明关键字用来定义变量,它们的存活范围仅限于当前运行的EC的词法环境。在它们的词法环境实现的时候,这些变量被创建,但是没有任何方式获得它们,直到这些变量的词汇绑定器计算完成。在词汇绑定计算的时候,一个变量定义被一个词汇绑定器和初始化器根据它的初始化赋值表达式完成赋值。完成以上步骤变量才算创建完成。在词汇绑定器计算的时候,如果用let声明的变量没有赋值表达式,词汇绑定器会给这个变量赋值undefined.
在标准文档中,let and const的描述比var的描述,多了but may not be accessed in any way until。就是在完成赋值表达式之前不能使用,也叫暂存死区TDZ (Temporal Dead Zone) 。有人说let和const不存在变量提升,其实是错误的,只是限制accessed而已。标准明确说了The variables are created when their containing Lexical Environment is instantiated but may not be accessed。
新加的词法环境,都存在TDZ,ECMAScript的词法环境机制决定了标识符都会在词法分析阶段就绑定,也叫做提升,但是let,const,class,函数形参,模块在其语句执行完成前都不能使用。当然也可以改变提升的定义,把TDZ的情况排除到提升的范围外,比如提升就是指以前var那种表现,那之后新加的词法环境确实都不存在提升问题。
https://tc39.github.io/ecma262/#sec-declarations-and-the-variable-statement
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
let是一个更严格的var。(忘了var吧,let值得拥有)
let继承var的规范,如果var规范和以下规范冲突,则采用以下规范。
用let定义的全局范围变量,不会成为全局对象的属性。
let可以把变量的作用范围限制在块级。块级域,用一对花括号限定的区域,通常特指流程控制语句块 ,如 if,for等。for语句的圆括号部分和花括号部分属于同一块级域。
用let定义的变量,在变量所属作用域内,不包括嵌套域,再用let定义一个同名变量将引起 TypeError。
用let定义的变量,在变量所属作用域外部,使用它将引起 ReferenceError。
用let定义的变量,在定义语句之前,使用它将引起 ReferenceError。
const是一个更严格的let。
const继承let的规范,如果let规范和以下规范冲突,则采用以下规范。
const定义的变量,必须初始化赋值,只能赋值一次,以后不能更改,这就是常量。建议常量名大写。
如果常量的值是指向内存的地址,常量只是不能再通过赋值改变引用地址,管不着引用对象。
Note: => is not an operator, but the notation for Arrow functions.注意=>不是一个运算符,它不过是箭头函数的特有符号。箭头函数的核心就是=>符号,主要是精简函数表达式的写法,特别在函数只有一个参数和函数体只有一个分号的时候,可以省略function和return 关键字,圆括号,花括号,比如var foo = x => x; 够简单吧,呵呵,写起来爽,读起来就没那么直观了,就和用逻辑或代替if else的变量赋值表达式一样,var bar = x || 1; 而且在有多个参数和函数体有多个语句的时候,就只比标准写法省略个function关键字。当然还有this和arguments的特性,但是不能指定this值则更限制了它的使用范围。开发者显式指定this值是个好习惯也更安心,我更喜欢接近人类语言的代码书写方式。
一个ArrowFunction 并不能给arguments, super, this, or new.target定义本地绑定local bindings。在an ArrowFunction内arguments, super, this, or new.target的值引用必须在包裹箭头函数的外层词法环境中进行绑定。通常this会是句法上包裹箭头函数的那个函数的词法环境。箭头函数没有自己的词法环境,所以多个嵌套的箭头函数,它们的this都是最外层那个有独立词法环境的句法结构体的环境。
https://tc39.github.io/ecma262/#sec-arrow-function-definitions-runtime-semantics-evaluation
函数参数的新特性,Default parameters就是可以在函数声明的时候给圆括号里的形参使用赋值表达式,注意在函数调用时,默认参数赋值语句要比花括号里面的语句先执行,执行顺序是从左到右。Rest parameters 剩余参数,语法形式是spread operator …加形参名。可以用来取代arguments。 rest parameters are Array instances。
对象中函数方法的新写法,就是省掉冒号:和function关键字…
var obj = {
// 现在不再使用function关键字给对象添加方法
// 而是直接使用属性名作为函数名称。
method(args) { ... },
// 只需在标准函数的基础上添加一个“*”,就可以声明一个生成器函数。
*genMethod(args) { ... },
// 借助|get|和|set|可以在行内定义访问器。
// 只是定义内联函数,即使没有生成器。
// 注意通过这种方式装载的getter不能接受参数
get propName() { ... },
// 注意通过这种方式装载的setter至少接受一个参数
set propName(arg) { ... },
// []语法可以用于任意支持预计算属性名的地方,来满足上面的第4中情况。
// 这意味着你可以使用symbol,调用函数,联结字符串
// 或其它可以给property.id求值的表达式。
// 这个语法对访问器或生成器同样有效,我在这里只是举个例子。
[functionThatReturnsPropertyName()] (args) { ... }
};
this 是一个与EC密切相关的特殊对象,因此,它可以称为EC对象(context object)。任何对象都可以做为上下文中的 this 的值。
this 是EC的一个属性。this 与EC类型紧密相关,其值在进入EC阶段就确定了,并且在执行代码阶段不能被改变。
在全局EC中,this 就等于全局对象本身。在函数上下文的情况下,对函数的每次调用,其中的 this 值可能是不同的。函数上下文中 this 的值由调用者(caller)提供,并由调用表达式的形式确定(函数调用的语法)。相同的函数,调用形式和调用者不同,this的值也可能不同。可以使用Function.prototype.bind(),Function.prototype.call(),Function.prototype.apply()方法指定函数调用时内部的this对象。
this在严格模式下有特有的规则。
Template literals 模板字面量通常用来替代以前那种字符串+变量或函数表达式的拼接写法。Tagged template literals标签模板字面量,就是为模板字面量提供自定义行为处理函数Tag functions。模板字面量相关规则和形式,有点像以前String.prototype.replace()的$和replacement function 。
模块 Modules, 新Statements,export和import,本身没什么好说的,反正在新标准出来之前,已经在大量使用了,去学webpack之类的东西吧。