《你不知道的JavaScript》笔记
本文是我阅读《你不知道的JavaScript》过程中记录下来的笔记。书我挺早就买了,买来看了几次,后来就放书架上积灰了。时隔多年,已经忘光了🥲趁这段时间有空又看了一遍,把笔记记了下来。
上卷
作用域和闭包
JavaScript编译
- JavaScript代码片段通常在执行前就进行编译,并且马上执行。
- 传统编译过程(三步)
- 分词/词法分析:将由字符串组成的字符串分解成有意义的代码块。这些代码块被称为”词法单元”。
- 解析/语法分析:将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树(AST)”。
- 代码生成:将AST转换为可执行代码的过程。
- 编译器:负责语法分析及代码生成等工作。
- 引擎:负责整个JavaScript程序的编译及执行过程。
- LHS查询:取到赋值操作的目标。目的是为了对变量进行赋值。
- RHS查询:取到赋值操作的源头。目的是为了获取变量的值。
- 异常:
- 当RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎会抛出ReferenceError异常;
- 当LHS查询在全局作用域也无法找到目标变量,在严格模式下会抛出类似ReferenceError的异常,在非严格模式下会自动创建一个全局变量;
- 当对通过RHS查询到的变量进行不合理操作时,会抛出TypeError类型的异常,如引用null中的属性。
作用域
- 作用域负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。即根据标识符名称查找变量的一套规则。
- 作用域嵌套:在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,知道找到该变量或抵达全局作用域为止。
- 词法作用域:JavaScript采用的是词法作用域,定义过程发生在代码的书写阶段,词法作用域由写代码时将变量和块作用域写在哪里决定的。
- 和另一种动态作用域的区别是,动态作用域是在运行时确定的,关注函数从何处调用,很像this机制。
- 遮蔽效应:作用域查找会在找到第一个匹配的标识符时停止,所以可以在多层嵌套作用域中定义同名的标识符,内部标识符将“遮蔽”外部标识符。
- 欺骗词法:在运行时改变词法作用域,但会导致性能下降。均不推荐使用,并且有被严格模式所限制。
- eval函数:将传入的字符串参数作为代码段来执行,用于执行动态创建的代码。
- setTimeout、setInterval函数:第一个参数可为字符串,该字符串可被解释为动态生成的函数代码。
- new function(..):最后一个参数可为代码字符串,并将其转化为动态生成的函数。
- with关键字:重复引用同一个对象中的多个属性的快捷方式。本质上是通过将一个对象的引用当做作用域来处理,将对象属性当做作用域中的标识符来处理,从而创建了一个新的词法作用域。
- 函数作用域:属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
- 立即执行函数:能够解决函数名污染所在作用域的问题,并且能够自动运行。有两种书写形式
(function foo(){..})()
、(function(){..}())
- 立即执行函数:能够解决函数名污染所在作用域的问题,并且能够自动运行。有两种书写形式
- 块级作用域:变量和函数在指定的代码块里(通常是{..})才能访问。例如with关键字、try/catch、let、const。
- 提升:所有的声明(变量和函数)都被“移动”到各自作用域的最顶端。
- JavaScript引擎会将
var a = 2
看成两个声明,第一个是var a
定义声明在编译阶段进行,第二个是a = 2
赋值声明被留在原地等待执行。 - 每个作用域都会进行提升操作,只有声明本身会被提升,包括函数表达式的赋值在内的赋值操作或其他运行逻辑会留在原地。
- 函数声明和变量声明都会被提升,但函数会被优先提升到普通变量之前
- JavaScript引擎会将
作用域闭包
- 当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
- 应用:定时器、事件监听器、AJAX请求、跨窗口通信、Web workers或者任何其他的异步(或同步)任务中,只要使用了回调函数,实际上就是在使用闭包。
- 模块模式必要条件:必须有外部的封闭函数,该函数必须至少被调用一次;封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
this和对象原型
this关键字
- this是在函数被调用时发生的绑定,它的上下文取决于函数调用时的各种条件,this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
- 根据绑定规则判断this:需要直接找到函数的直接调用位置,优先级 new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
- new绑定:函数是否在new中调用?
- this绑定的是新创建的对象。
var bar = new foo()
- 使用
new
来调用函数,或者说发生构造函数调用时,会自动执行以下操作- 创建(构造)一个全新的对象;
- 新对象会被执行
[[prototype]]
连接; - 新对象会被绑定到函数对象的this;
- 如果函数没有返回其他对象,那么
new
表达式中的函数调用会自动返回这个新对象;
- 显式绑定:函数是否通过call、apply或者硬绑定(显式绑定的一个变种,this指针不会丢失,如bind)?
- this绑定的是指定的对象。
var bar = foo.call(obj2)
- 隐式绑定:函数是否在某个上下文对象中调用?
- this绑定的是那个上下文对象。
var bar = obj1.foo()
- 参数传递其实就是一种隐式赋值;调用回调函数的函数可能会修改this。
- 默认绑定:都不是的话,使用独立函数调用,即默认绑定
- 严格模式下绑定到undefined,否则绑定到全局对象。
var bar = foo
- new绑定:函数是否在new中调用?
- this绑定例外:
- 把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。常见场景是使用apply来“展开”一个数组并当做参数传入函数,类似地,bind可以对参数进行柯里化(部分求值)。
- 被创建了“间接引用”的函数会应用默认绑定规则。
- 软绑定softBind(..)会对指定的函数进行封装,检查调用时的this,如果this绑定到全局对象或者undefined,就把指定的默认对象obj绑定到this,否则不会修改this,防止函数调用应用默认绑定规则,实现了和硬绑定相同的效果,同时保留了隐式绑定或者显式绑定修改this的能力。
- 箭头函数:不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this,且箭头函数的绑定无法被修改。箭头函数和
self = this
机制一样。
对象
- 两种定义形式:声明(文字)形式和构造形式。二者唯一的区别是文字声明可以添加多个键/值对,构造形式必须逐个添加属性。
- 六种语言类型:string、number、boolean、null、undefined、object
- 简单基本类型(string、number、boolean、null、undefined)本身不是对象,
typeof null
会返回“object”是个bug。- 不同的对象在底层都表示为二进制,二进制前三位都为0的话会被判断为object类型,null的二进制表示全是0,所以typeof会返回“object”。
- JavaScript中的函数是“一等公民”,是对象的一个子类型,本质上和普通对象一样(只是可以调用)。
- 简单基本类型(string、number、boolean、null、undefined)本身不是对象,
- 内置对象:对象子类型(注意大写的首字母),String、Number、Boolean、Object、Function、Array、Date、RegExp、Error。
- 这些内置对象可以当做构造函数来使用,从而构造一个对应子类型的新对象。
- null和undefined没有对应的构造形式,只有文字形式;Date只有构造没有文字形式。
- 内容:对象的内容由一些存储在特定命名位置的值组成,称为属性。存储在对象容器内部的是属性的名称,属性名永远是字符串,就像指针一样指向这些值真正的存储位置。
- 可计算属性名:ES6新增功能,可以在文字形式中使用
[]
包裹一个表达式来当做属性名。 - 属性和方法:函数永远不会“属于”一个对象,即使在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象,只是对于相同函数对象的多个引用。
- 复制对象:
var newObj = JSON.parse(JSON.stringify(someObj))
,需要保证对象是JSON安全的,只适用于部分情况。object.assign(..)
会遍历一个或多个源对象的所有可枚举的自有键并把它们复制到目标对象,最后返回目标对象,实现浅拷贝。
- 属性描述符:从ES5开始所有属性都具备了属性描述符,包含
value
(属性值)、writable
(控制是否可以修改属性值)、enumerable
(控制是否会出现在对象的属性枚举中)、configurale
(控制是否允许配置,修改成false是单项操作无法撤销)特性。- 创建普通属性时,属性描述符会使用默认值,也可以使用
Object.defineProperty(..)
来添加一个新属性或者修改一个已有属性并对特性进行设置。
- 创建普通属性时,属性描述符会使用默认值,也可以使用
- 不变性
- 对象常量:结合
writable:false
和configurable:false
就可以创建真正的常量属性(不可修改、重定义或者删除)。 - 禁止扩展:
Object.preventExtensions(..)
可以禁止一个对象添加新属性并且保留已有属性。 - 密封:
Object.seal(..)
会创建一个“密封”的对象,在现有对象上调用Object.preventExtensions(..)
并把所有现有属性标记为configurable:false
。 - 冻结:
Object.freeze(..)
会创建一个冻结对象,在现有对象上调用Object.seal(..)
并把所有“数据访问”属性标记为writable:false
,这是可以应用在对象上的级别最高的不可变性(不过这个对象引用的其他对象是不受影响的)
- 对象常量:结合
- **[[Get]]和[[Put]]]**:对象默认的[[Get]]和[[Put]]]操作分别可以控制属性值的获取和设置。
- Getter和Setter:getter和setter都是隐藏函数,可以改写默认操作,但是只能应用在单个属性上。
- getter会在获取属性值时调用,setter会在设置属性值时调用。
- 当给一个属性定义getter、setter或者两者都有时,这个属性会被定义为“访问描述符”,JavaScript会忽略它们的value和writable特性,取而代之的是关心set、get、configurable和enumerable特性。
- 通常来说getter和setter是成对出现的。
- 属性不一定包含值,它们可能是具备getter/setter的“访问描述符”。
- 存在性:
"a" in myObject
使用in操作符可以检查属性是否在对象及其[[Prototype]]
链中。myObject.hasOwnPrototype("a")
只会检查属性是否存在myObject对象中,不会检查[[Prototype]]
链。myObject.prototypeIsEnumerable("a")
可以检查给定的属性名是否直接存在于对象中,而不是在原型链上,并且满足enumerable:true
。Object.keys(myObject)
会返回一个数组,包含所有可枚举属性,只会查找对象直接包含的属性。Object.getOwnPrototypeNames(myObject)
会返回一个数组,包含所有属性,无论它们是否可枚举,只会查找对象直接包含的属性。
- 遍历:
for..in
会遍历对象的可枚举属性列表,包括[[Prototype]]
链。forEach(..)
会遍历数组中的所有值并忽略回调函数的返回值。every(..)
会一直运行直到回调函数返回false。some(..)
会一直运行直到回调函数返回true。for..of
会直接遍历值而不是数组下标或者对象属性。它首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值。
- 可计算属性名:ES6新增功能,可以在文字形式中使用
混合对象“类”
- 类理论:
- JavaScript没有类,但提供了一些近似类的语法,js只有对象,可以不通过类直接创建对象。
- 面向类的设计模式:实例化、继承、(相对)多态。
- 面向对象编程强调的是数据和操作数据的行为本质上是互相关联的。
- 多态(在继承链的不同层次名称相同但是功能不同的函数)是类的一个核心概念,父类的通用行为可以被子类用更特殊的行为重写,相对多态性允许从重写行为中引用基础行为。
- 类实例是由一个特殊的类方法构造的,这个方法名通常和类型相同,被称为构造函数。这个方法的任务就是初始化实例需要的所有信息(状态)。
- 混入:可以用来模拟类的复制行为,但是通常会产生丑陋并且脆弱的语法,比如显式伪多态
otherObj.methodName.call(this, ...)
。- 显式混入:实际上显式混入并不能完全模拟面向类的语言中的复制,因为对象只能复制引用。
mixin(..)
会遍历sourceObj的属性,如果在targetObj没有这个属性就会进行复制。- JavaScript中的函数无法用标准可靠的方法真正的赋值,所以只能复制对共享函数对象的引用。
- 寄生继承是显式混入模式的一种变体,既是显式又是隐式的。先复制一份父类对象的定义,然后混去子类对象的定义,然后用这个复合对象构建实例。
- 显式混入:实际上显式混入并不能完全模拟面向类的语言中的复制,因为对象只能复制引用。
- 隐式混入:通过在构造函数调用或者方法调用中使用
Something.cool.call(this)
,借用了Something.cool()在Another的上下文中调用了它(通过this绑定),最终Something.cool()中的赋值操作都会应用在Another对象上而不是Something对象上,这样就把Something的行为混入了Another中。但是要避免使用这种结构,已保证代码的整洁和可维护性。
原型
- 所有普通的
[[Prototype]]
链最终都会指向内置的Object.prototype。 - 屏蔽:
- 如果属性名foo既出现在myObject中也出现在myObject的
[[Prototype]]
链上层,myObject中包含的foo属性就会屏蔽原型链上层的所有foo属性,myObject.foo总会选择原型链中最底层的foo属性。 - 如果foo不直接存在于myObject中而是存在于原型链上层时,
myObject.foo = "bar"
会出现的三种情况- 如果在
[[Prototype]]
链上层存在名为foo的普通数据访问属性并且没有标记为只读,就会直接在在myObject中添加一个名为foo的新属性,它是屏蔽属性。 - 如果在
[[Prototype]]
链上层存在foo,但被标记为只读,那么无法修改已有属性或者在myObject上创建屏蔽属性。严格模式下会抛错,否则这条赋值语句会被武略。 - 如果在
[[Prototype]]
链上层存在foo并且是一个setter,那就会调用这个setter,foo不会被添加到myObject,也不会重新定义foo这个setter。 - 如果希望在上述的第二和第三种情况下也屏蔽foo,就不能使用=操作符来赋值,而是使用
Object.defineProperty(..)
来向myObject添加foo。
- 如果在
- 如果属性名foo既出现在myObject中也出现在myObject的
- “类”函数:
- 所有的函数默认都会拥有一个名为prototype的公有并且不可枚举的属性,它会指向Foo的原型。
new Foo()
会生成一个新对象,这个新对象的内部链接[[Prototype]]
关联的是Foo.prototype对象。实际上我们并没有初始化一个类,也没有从“类”中复制任何一个行为到一个对象中,只是让两个对象互相关联。- 构造函数:
Foo.prototype
默认有一个公有并且不可枚举的属性.constructor
,这个属性引用的是对象关联的函数(本例中是Foo)。- 当且仅当使用new时,函数调用就会变成“构造函数调用”。new会劫持所有普通函数并用构造对象的形式来调用它。
Foo.prototype.constructor
默认指向Foo,但实际上,.constructor
引用是被委托给了Foo.prototype
。
- 原型继承:在JavaScript中并不会将一个对象(“类”)复制到另一个对象(“实例”),只是将它们关联起来,这个机制被称为原型继承。但是继承意味着复制操作,js并不会复制,所以事实上“委托”这个术语描述会更加准确。
- 创建关联对象:需要调用
Object.create(..)
,它会凭空创建一个“新”对象并把新对象内部的[[prototype]]
关联到指定的对象。缺点是需要抛弃旧对象,不能修改已有的默认对象,和轻微性能损失(抛弃的对象需要进行垃圾回收),但实际上比ES6及其之后的方法更短、可读性更高。 - ES6前只能通过设置
.__proto__
属性来修改对象的[[prototype]]
关联,.__proto__
存在于内置的Object.prototype
中,它引用了内部的[[Prototype]]
对象,但无法兼容所有浏览器。ES6添加了Object.setPrototypeOf(..)
这个标准并且可靠的修改关联的辅助函数。 - 验证委托关联:
- ES5的标准方法:
Object.getPrototype(a) === Foo.prototype
- 非标准方法:
a.__proto__ === Foo.prototype
- ES5的标准方法:
- 创建关联对象:需要调用
行为委托
- XYZ通过
Object.create(..)
创建,它的[[prototype]]
委托了Task对象。这种编码风格称为“对象关联”。 - 委托行为意味着某些对象(XYZ)在找不到属性或者方法引用时会把这个请求委托给另一个对象(Task)。
- 行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript的
[[prototype]]
机制本质上就是行为委托机制。 - 我们可以选择在JavaScript中努力实现类机制,也可以拥抱更自然的
[[prototype]]
委托机制,类并不适用于JavaScript。 - 行为委托和类设计模式的不同之处:
- 通常来说,在
[[prototype]]
委托中最好把状态保存在委托者(XYZ、ABC)而不是委托目标(Task)上。 - 尽量避免在
[[prototype]]
链的不同级别中使用相同的命名,否则就要用笨拙脆弱的语法来消除歧义。 this.setID(ID)
,XYZ中没有这个方法名,会通过[[prototype]]
委托关联到Task中找到这个方法,由于调用位置触发了this的隐式绑定规则,运行时this会绑定到XYZ,这正是我们想要的。- 使用类构造函数需要在同一个步骤中实现构造和初始化,对象关联则更好地支持关注分离原则,创建和初始化不需要合并为一个步骤。
- 对象关联能让代码更加简洁、更具扩展性,简化了代码结构,用一种简单的设计实现了同样的功能。
- 通常来说,在
- 互相委托(禁止):无法在两个或者两个以上互相(双向)委托的对象之间创建循环委托。如果引用了一个两边都不存在的属性或者方法,就会在
[[prototype]]
链上产生一个无限递归的循环。 - 对象关联风格的实现方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};
Bar = Object.create(Foo);
Bar.speak = function() {
alert("Hello, " + this.identify() + ".");
};
var b1 = Object.create(Bar);
b1.init("b1");
var b2 = Object.create(Bar);
b2.init("b2");
b1.speak();
b2.speak();
ES6的class
- class不是向JavaScript引入了一种新的“类”机制,而是现有
[[prototype]]
委托机制的一种语法糖。 - class并不会像传统面向类的语言一样在声明时静态复制所有行为,如果修改或者替换了父“类”中的一个方法,子“类”的所有实例都会受到影响。
- 优点:
- 语法更好看,不再引用杂乱的
.prototype
,不再需要使用Object.create(..)
、.__proto__
、Object.setPrototypeOf(..)
; - 可以通过
super(..)
来实现多态,任何方法都可以引用原型链上层的同名方法,还可以解决构造函数无法互相应引用的问题; - class字面语法不能声明属性,只能声明方法,可以帮你避免犯错;
- 可以通过
extends
很自然的扩展对象(子)类型,甚至是内置的对象(子)类型。
- 语法更好看,不再引用杂乱的
- 缺点:
- 加深了过去二三十年对于JavaScript中“类”的误解,让
[[prototype]]
机制变得非常别扭; - class语法无法定义类成员属性,只能定义方法,如果为了追踪势力之间共享状态必须要这么做,只能使用丑陋的
.prototype
语法,这违背了class语法的本意; - 依然面临意外屏蔽的问题,如id属性屏蔽了id()方法;
- 出于性能考虑,super是在声明时“静态”绑定的,根据应用方式的不同,super可能不会绑定到合适对象,这时需要用toMethod(..)来手动绑定。
- 加深了过去二三十年对于JavaScript中“类”的误解,让
中卷
类型和语法
类型
- js的类型定义:对语言引擎和开发人员来说,类型是值的内部特征,定义了值的行为特征,以使其区别于其他值。
- JS不做类型强制:JavaScript中的变量没有类型,只有值才有,变量可以随时持有任何类型的值。
- 七种内置类型:空值null、未定义undefined、布尔值boolean、数字number、字符串string、对象object、符号symbol
- typeof运算符:typeof可以用来判断值的类型,返回的是类型的字符串值,内置类型除了null返回的是object,其他都有同名字符串值与之对应。typeof处理undeclared变量有一个特殊的安全防范机制,会阻止抛出ReferenceError错误,而是返回undefined。可以用来检查变量是否存在。
- undefined和undeclared:已在作用域中声明但还没有赋值的变量时undefined的,还没有在作用域中声明过的变量时undeclared的。
值
- array、string、number是一个程序最基本的组成部分。
- 数组:数组可以容纳任何类型的值。数组通过数字进行索引,还可以包含字符串键值/属性,但这些并不计算在数组长度内。
- 类数组:
Array.prototype.slice.call(..)
、Array.from(..)
可以将类数组(一组通过数字索引的值)转换为真正的数组。 - 字符串:字符串不可变,字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。字符串和数组很相似,但数组可变,它的成员函数可以在原始值上操作。字符串可以“借用”数组的非变更方法来处理字符串,如
Array.prototype.join.call(a. "-")
,但无法“借用”可变更方法,因为字符串不可变。 - 数字:js只有一种数值类型——number,包括“整数”(js没有真正意义上的整数)和带小数的十进制数。
- js中的数字类型是基于IEEE 754标准(该标准通常称为“浮点数”)来实现的,使用的是“双精度”格式,即64位二进制。
0.1 + 0.2 === 0.3; // false
:这是二进制浮点数最大的问题,一些数字无法用二进制准确的表示出来。ES6中内置了一个误差范围值——Number.EPSILON
。Number.isInteger(42)
可以检测是否是整数。- ES6中能被“安全”呈现的最大整数是
Number.MAX_SAFE_INTEGER
,最小整数是Number.MIN_SAFE_INTEGER
。Number.isSafeInteger(Math.pow(2, 53)-1)
可用来检测是否是安全的整数。 - NaN:NaN用于指出数字类型中的错误情况。
- 如果数字运算的操作数不是数字类型或者无法解析为常规的十进制或者十六进制数组,就会返回NaN。
- NaN和自身不相等,是唯一一个非自反的值,
NaN != NaN
为true。 - 可以用
isNaN(..)
来判断一个值是否是NaN,但有个bug,参数既不是数字也不是NaN也会返回true,所以要尽量使用ES6的Number.isNaN(..)
。 - 无穷除以无穷,即
Infinity / Infinity
是一个未定义操作,结果为NaN;有穷整数除以Infinity结果为0。
- 负零:有些应用程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字的符号位用来代表其他信息(比如移动的方向)。而
-0 === 0; // true
,可以使用isNegZero(..)
这样的工具函数来辨别。 - 特殊等式:ES6新增的
Object.is(..)
可以判断两个值是否绝对相等,就可以处理NaN和-0在相等比较的问题。
- null:指空值,曾赋过值,但目前没有值;名称既是类型也是值;是一个特殊关键字,不是标识符,不能将其当做变量来使用和赋值。
- undefined:指从未赋值;名称既是类型也是值;也是一个标识符,可以被当做变量来使用和赋值。
- void运算符返回undefined,所以要将代码中的值(如表达式的返回值)设为undefined,就可以使用void。
- 引用:
- JavaScript没有指针,变量不可能成为指向另一个变量的引用,js对值和引用的赋值/传递在语法上没有区别,完全根据值的类型来决定。
- 简单值(即标量基本类型值,包括null、undefined、string、number、boolean、symbol)总是通过值赋值的方式来赋值/传递。
- 复合值——对象(包括数组和封装对象)和函数,总是通过引用复制的方式来赋值/传递。
- 由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。
原生函数
- 常见原生函数:
String()
、Number()
、Boolean()
、Array()
、Object()
、Function()
、RegExp()
、Date()
、Error()
、Symbol()
,也叫内建函数。 - 原生函数可以被当做构造函数来使用,通过构造函数(如
new String("abc")
)创建出来的是封装了基本类型值(如"abc"
)的封装对象。 - **内部属性
[[Class]]
**:所有typeof返回值为“Object”的对象(如数组)都包含一个内部属性[[Class]]
,这个属性无法直接访问,一般通过Object.prototype.toString(..)
来查看。Object.prototype.toString.call([1,2,3]); // "[object Array]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(42); // "[object Number]"
- 封装对象包装:js会自动为基本类型值包装一个封装对象,这样基本类型值就能访问.length和.toString()这样的属性和方法。
- 当原生函数作为构造函数:
- **构造函数Array(..)**:
- 不要求必须带new关键字,不带时会被自动补全。
Array(1,2,3)
和new Array(1,2,3)
是一样的。 - 只带一个数字参数时,该参数会被作为数组的预设长度,而非充当数组中的一个元素。
- 包含至少一个“空单元”的数组称为“稀疏数组”。
- 永远不要创建和使用空单元数组。
- 不要求必须带new关键字,不带时会被自动补全。
- **构造函数Object(..)**:实际情况中没必要使用new Object()来创建对象,因为这样无法像常量形式那样一次设定多个属性,而必须逐一设定。
- **构造函数Function(..)**:只有在极少数情况下有用,比如动态定义函数参数和函数体的时候。
- **构造函数RegExp(..)**:在动态定义正则表达式的时候比较有用,通常更建议使用常量形式来定义正则表达式,语法简单执行效率也更高,因为js在代码执行前会对它们进行预编译和缓存。
- **构造函数Date(..)**:创建日期对象必须使用
new Date()
,如果调用Date()不带new关键字则会得到当前日期的字符串值。 - **构造函数Error(..)**:带不带new关键字都可。创建错误对象主要是为了获得当前运行栈的上下文,以便于调试。
- **构造函数Symbol(..)**:Symbol(符号)是具有唯一性的特殊值,用它来命名对象属性不容易导致重名。使用时不能带new关键字。
- **构造函数Array(..)**:
- 原生原型:原生构造函数有自己的
.prototype
对象,这些对象包含其对应子类型所特有的行为特征。
强制类型转换
- 值类型转换:
- 类型转换发生在静态类型语言的编译阶段,而强制类型转换发生在动态类型语言的运行时。
- 强制类型转换:是隐式的类型转换。总是返回标量基本类型,不会返回对象和函数,
- 抽象值操作:
- ToString:负责处理非字符串到字符串的强制类型转换,
toString()
可以被显式调用,或者在需要字符串化时自动调用。- 基本类型值的字符串化规则为:null转换为“null”,undefined转换为“undefined”,true转换为“true”,数字的字符串化则遵循通用规则。
- 对普通对象来说,除非自行定义,否则
toString()
(Object.prototype.toString()
)返回内部属性[[Class]]的值。 - 数组的默认
toString()
方法经过了重新定义,将所有单元字符串化以后再用“,”连接起来。 JSON.stringify(..)
和toString()
类似,但它并不是强制类型转换,它能将安全的JSON值字符串化。
- **ToNumber()**:
- true转换为1,false转换为0。
- undefined转换为NaN,null转换为0。
- 对字符串的处理基本遵循数字常量的相关规则/语法,处理失败时返回NaN。
- 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型,则在遵循以上规则将其强制转换为数字。
- 例如,
Number(""); // 0
、Number([]); // 0
、Number(["abc"]); // NaN
- ToBoolean():
- 假值:undefined、null、false、+0、-0、NaN、””,假值的布尔强制类型转换结果为false。
- 假值对象:封装了假值的对象,例如
new Boolean(false)
、new Number(0)
、new String("")
、document.all
(类数组对象,已被废止,用来判断是否是老版本的IE) - 真值:假值列表之外的值,包括所有字符串、[]、{}、function(){}等等。
- ToString:负责处理非字符串到字符串的强制类型转换,
- 显式强制类型转换:
- 字符串和数字之间的显式转换:
String(..)
、Number(..)
、a.toString()
、一元运算符+ - 显式解析数字字符串:
- 解析允许字符串中含有非数字字符,解析从左到右的顺序,遇到非数字字符就停止。而转换不允许出现非数字字符,否则失败并返回NaN。
parseInt(..)
可以用来解析字符串中的数字,parseFloat(..)
用来解析字符串中的浮点数。向它们传递非字符串参数,会首先被强制类型转换为字符串,应避免传递非字符串参数。
- 显式转换为布尔值:
Boolean(..)
(不带new)是显式的ToBoolean强制类型转换。- 一元运算符
!
显式地将值强制类型转换为布尔值,同时会将真值反转为假值(或假值反转为真值)。所以!!
是显式强制类型转换为布尔值最常用的方法。 - 在
if(..)..
这样的布尔值上下文中,会自动隐式地进行toBoolean转换。 - 三元运算符
? :
判断是否为真是一种“显式的隐式”的情况,建议使用Boolean(a)
和!!a
来进行显式强制类型转换。
- 字符串和数字之间的显式转换:
- 隐式强制类型转换:
- 字符串和数字之间的隐式强制类型转换:如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+将进行拼接操作;如果其中一个操作数是对象(包括数组),则首先对其调用ToPrimitive抽象操作,该抽象操作再调用[[DefalutValue]],以数字作为上下文。简单来说就是,如果+的其中一个操作数是字符串则执行字符串拼接,否则执行数字加法。
- 隐式强制类型转换为布尔值的情况:
if(..)
语句中的条件判断表达式。for(..;..;..)
语句中的第二个条件判断表达式。while(..)
和do..while(..)
循环中的条件判断表达式。? :
中的条件判断表达式。- 逻辑运算符
||
(逻辑或)和&&
(逻辑与)左边的操作数(作为条件判断表达式)。
- **
||
和&&
**:||
如果条件判断结果为true就返回第一个操作数的值,否则返回第二个操作数的值。通过这种方式来设置默认值很方便。&&
如果条件判断结果为true就返回第二个操作数的值,否则返回第一个操作数的值。也叫做“守护运算符”,即前面的表达式为后面的表达式“把关”,js代码压缩工具常用a && foo()
这种方式。
- 符号的强制类型转换:
- ES6允许从符号到字符串的显式强制转换,然而隐式强制类型转换会发生错误。
String(Symbol("cool")); // "Symbol(cool)"
、Symbol("not cool") + ""; // TypeError
- 符号可以被强制类型转换为布尔值(显式和隐式结果都是true),但不能被强制类型转换为数字(显式和隐式都会产生错误)
- ES6允许从符号到字符串的显式强制转换,然而隐式强制类型转换会发生错误。
- 宽松相等和严格相等:
===
允许在相等比较中进行强制类型转换,而==
不允许。- **抽象相等
==
**:- 如果两个值的类型相同,就仅比较它们是否相等;两个对象指向同一个值时即视为相等,不发生强制类型转换。
- 特殊情况:NaN不等于NaN;+0等于-0;
- 字符串和数字之间的相等比较:
- 如果Type(x)是数字,Type(y)是字符串,则返回
x == ToNumber(y)
的结果。 - 如果Type(x)是字符串,Type(y)是数字,则返回
ToNumber(x) == y
的结果。
- 如果Type(x)是数字,Type(y)是字符串,则返回
- 其他类型和布尔类型之间的相等比较:
- 如果Type(x)是布尔类型,则返回
ToNumber(x) == y
的结果。 - 如果Type(y)是布尔类型,则返回
x == ToNumber(y)
的结果。
- 如果Type(x)是布尔类型,则返回
- null和undefined之间的相等比较:在==中null和undefined相等,它们也与其自身相等。
- 对象和非对象之间的相等比较:
- 如果Type(x)是字符串或数字,Type(y)是对象,则返回
x == ToPrimitive(y)
的结果。 - 如果Type(x)是对象,Type(y)是字符串或数字,则返回
ToPrimitive(x) == y
的结果。 - 上面没提到布尔值,原因是布尔值会先被强制类型转换为数字。
- 例如:
42 == [42]; // true
、var a = "abc"; a == Object(a); // true
(拆封,即打开封装对象返回其中的基本数据类型值)
- 如果Type(x)是字符串或数字,Type(y)是对象,则返回
- 安全运用隐式强制类型转换:
- 如果两边的值中有true或者false,千万不要使用==
- 如果两边的值中有[]、””或者0,尽量不要使用==
- 抽象关系比较:
- 比较双方首先调用ToPrimitive,如果结果出现非字符串,就根据ToNumber规则将双方强制类型转换为数字来进行比较。
- 比较双方都是字符串,则按字母来进行比较。
语法
- 语法和表达式:
- 代码块的结果值就如同一个隐式的返回,即返回最后一个语句的结果值。
- JSON:
- JSON是JavaScript语法的一个子集,但它并不是合法的JavaScript语法。
- 如果通过
<script src=..>
标签加载只包含JSON数据的JS文件,它就被会当做合法的JS代码来解析,但其内容无法被程序代码访问。JSON-P将JSON数据封装为函数调用,比如foo({"a": 42})
,能将JSON转换为合法的JS语法。 [] + {}; // [object Object]
:{}
被当做一个值(空对象)来处理,[]
会被强制类型转换为“”,而{}
被强制类型转换为“[object Object]”。{} + []; // 0
:{}
被当做一个独立的空代码块(不执行任何操作),代码块结尾不需要分号,所以语法没问题。最后+ []
将[]
显示强制类型转换为0。
- 运算符优先级:运算符执行的先后顺序,完整列表参见《运算符优先级》
- 短路:对
&&
和||
来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数,这种现象称为“短路”,即执行最短路径。 - 关联:多个运算符的组合方式。运算符的关联不是从左到右就是从右到左,这取决于组合是从左开始还是从右开始。
- 如果运算符优先级/关联规则能够另代码更为简洁,就使用运算符优先级/关联规则;而如果
()
有助于提高代码可读性,就使用()
。
- 短路:对
- 自动分号:如果JavaScript解析器发现代码行可能因为缺失分号而导致错误,那么它会自动补上分号,即自动分号插入(ASI)。并且只有在代码行末尾与换行符之间除了空格和注释之外没有别的内容时,它才会这么做。
- 错误:JavaScript不仅有各种类型的运行时错误(typeError、ReferenceError、SyntaxError等,可以通过try..catch捕获),语法中也定义了一些编译时错误。编译时发现的代码错误叫做“早期错误”,无法被捕获,语法错误是早期错误的一种。
- 函数参数:尽量不要使用arguments,更不要同时访问命名参数和其对应的arguments数组单元。
- try..finally:finally中的代码总在try之后执行,如果有catch则在catch之后执行。finally中的return会覆盖try和catch中return的返回值。
- 混合环境:
- 官方ECMAScript规范包括Annex B,其中介绍了由于浏览器兼容性问题导致的与官方规范的差异。
- Web ECMAScript规范中介绍了官方ECMAScript规范和目前基于浏览器的JavaScript实现之间的差异。
- “宿主对象”(包括内建对象和函数)是由宿主环境(浏览器等)创建并提供给JavaScript引擎的变量。例如console。
- 由于浏览器演进的历史遗留问题,在创建带有id属性的DOM元素时也会创建同名的全局变量。
- 对于将来可能成为标准的功能,按照大部分人赞同的方式来预先实现能和将来的标准兼容的polyfill,我们称为polyfill。polyfill能有效地为不符合最新规范的老版本浏览器填补缺失的功能,让你能够通过可靠的代码来支持所有你想要支持的运行环境。
<script>
:- 在
<script>..</script>
和<script src=..>..</script>
中,全局变量作用域提升机制在这些边界中不适用。 - 如果script中的代码(无论是内联代码还是外部代码)发生错误,它会像独立的JavaScript程序那样停止,但是后续的script中的代码(仍然共享global)依然会接着运行,不会受影响。
- 内联代码和外部文件中的代码之间有个区别,即在内联代码中不可以出现
</script>
字符串,一旦出现即被视为代码块结束。
- 在
异步和性能
异步:现在与将来
- 分块的程序:
- JavaScript程序总是分为两个块:第一块现在运行;下一块将来运行,以响应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访问,所以对状态的修改都是在之前积累的修改之上进行的。
- 同步AJAX请求会锁定浏览器UI(按钮、菜单、滚动条等),并阻塞所有的用户交互。
- 任何时候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、Ajax响应等)时执行,你就是在代码中创建了一个将来执行的块,也由此在这个程序中引入了异步机制。
- 事件循环:
- JavaScript引擎运行在各种宿主环境中,如浏览器、服务器,这些环境都提供了一种机制来处理程序中多个块的执行,且执行每块时调用JavaScript引擎,这种机制被称为事件循环。JavaScript引擎并没有时间的概念,只是一个按需执行JavaScript任意代码片段的环境。“事件”调度总是由包含它的环境进行。
- ES6引入了Promise,精确指定了事件循环的工作细节,意味着在技术上将其纳入了JavaScript引擎的势力范围,而不是只由宿主环境来管理,意味着JavaScript才真正内建有直接的异步概念。
- 一旦有事件需要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个tick。用户交互、IO和定时器会向事件队列中加入事件。
- 并行线程:
- 异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。
- 并行计算最常见的工具是进程和线程。进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。
- 事件循环把自身的工作分成一个个任务并顺序执行,不允许对共享内存的并行访问和修改。通过分立线程中彼此合作的事件循环,并行和顺序执行可以共存。
- 由于JavaScript的单线程特性,
foo()
(以及bar()
)中的代码具有原子性,一旦foo()
开始运行,它的所有代码都会在bar()
中的任意代码运行之前完成,或者相反,这称为完整运行特性。
- 并发:
- 并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时在运行(尽管在任意时刻只处理一个事件)。
- 单线程事件循环是并发的一种形式。
- 并发协作的目标是取到一个长期运行的“进程”,并将其分割成多个步骤或多批任务,使得其他并发“进程”有机会将自己的运算插入到事件循环队列中交替运行。
- 任务队列:是挂在事件循环队列的每个tick之后的一个队列。在事件循环的每个tick中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前tick的任务队列末尾添加一个项目(一个任务)。
回调
- 回调函数是JavaScript异步的基本单元。但是随着JavaScript越来越成熟,对于异步编程领域的发展,回调已经不够用了。
- 把自己程序一部分的执行控制交给某个第三方,称为控制反转。回调会受到控制反转的影响,存在一系列麻烦的信任问题。
- 通过回调表达程序异步和管理并发存在两个缺陷:缺乏顺序性和可信任性。
Promise
- 什么是Promise:
- 绝大多数JavaScript/DOM平台新增的异步API都是基于Promise构建的。
- Promise决议后就是外部不可变的值,可以安全地把这个值传递给第三方,并确信它不会被有意无意地修改。
- Promise是一种封装和组合未来值的易于复用的机制。
new Promise(function(..){..})
模式通常称为revealing constructor。传入的函数会立即执行,有resolve和reject两个参数,都是promise的决议函数,resolve(..)
通常标识完成,reject(..)
则标识拒绝。- Promise一旦决议,就会一直保持其决议结果(完成或拒绝)不变,可以按照需要多次查看。
- 具有then方法的鸭子类型:
- 根据一个值的形态(具有哪些属性)对这个值的类型做出一些假定。这种类型检查一般用术语鸭子类型来表示——“如果它看起来像鸭子,叫起来像只鸭子,那它一定就是只鸭子”。
- 识别Promise(或者行为类似于Promise的东西)就是定义某种称为thenable的东西,将其定义为任何具有
then(..)
方法的对象和函数。我们认为,任何这样的值就是Promise一致的thenable。
- Promise信任问题:
- 对一个Promise调用
then(..)
的时候,即使这个Promise已经决议,提供给then(..)
的回调也总会被异步调用。 - 一个Promise决议后,这个Promise上所有的通过
then(..)
注册的回调都会在下一个异步时机点上依次被立即调用,这些回调中的任意一个都无法影响或延误对其他回调的调用。 - 没有任何东西(甚至JavaScript错误)能阻止Promise向你通知它的决议(如果它决议了的话)。
- Promise的定义方式使得它只能被决议一次。如果出于某种原因,Promise创建代码试图调用
resolve(..)
或reject(..)
多次,或者试图两者都调用,那么这个Promise只会接受第一次决议,并忽略任何后续调用。 - Promise至多只能有一个决议值(完成或拒绝)。如果使用多个参数调用
resolve(..)
或reject(..)
,第一个参数之后的所有参数都会被忽略。 - 在Promise创建过程中或在查看其决议结果过程中的任何时间点上出现了一个JavaScript异常错误,这个异常就会被捕获,并且使这个Promise被拒绝。
- 如果向
Promise.resolve(..)
传递一个非Promise、非thenable的立即值,就会得到一个用这个值填充的promise。Promise.resolve(..)
提供了可信任的Promise封装工具,能够保证总会返回一个Promise结果。 - Promise这种模式通过可信任的语义把回调作为参数传递,使得这种行为更可靠更合理。通过把回调的控制反转反转回来,我们把控制权放在了一个可信任的系统中,这种系统的设计目的就是为了使异步编程更清晰。
- 对一个Promise调用
- 链式流:
- 使链式流程控制可行的Promise固有特征:
- 每次对Promise调用
then(..)
都会创建并返回一个新的Promise,可以将其链接起来。 - 不管从
then(..)
调用的完成回调(第一个参数)返回的值是什么,它都会自动设置为被链接Promise(第一点中的)的完成。 - 当传递给
Promise.resolve(..)
的是一个Promise或thenable而不是最终值时,Promise.resolve(..)
会直接返回接收到的真正Promise,或展开接收到的thenable值,并在持续展开thenable的同时递归地前进。 - 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise就相应地决议。
- 每次对Promise调用
- 使链式流程控制可行的Promise固有特征:
- Promise模式:
Promise.all([..])
:从Promise.all([..])
返回的主Promise在且仅在所有的成员promise都完成后才会完成,如果这些promise中有任何一个被拒绝的话,主Promise.all([..])
promise就会立即被拒绝,并丢弃来自其他所有promise的全部结果。这种模式传统上被称为门。如果向它传入空数组,它会立即完成- 。
Promise.race([..])
:一旦有任何一个Promise决议为完成,Promise.race([..])
就会完成;一旦有任何一个Promise决议为拒绝,它就会拒绝。这种模式传统上被称为门闩。如果向它传入空数组,就会被挂住,且永远不会决议。Promise.none([..])
:与all类似,所有的Promise都要被拒绝,即拒绝转化为完成值,反之亦然。Promise.any([..])
:与all类似,但是会忽略拒绝,所以只需要完成一个而不是全部。Promise.first([..])
:与any类似,只要第一个Promise完成,就会忽略后续的任何拒绝和完成。Promise.last([..])
:与first类似,但却是只有最后一个完成胜出。Promise.map([..])
:接收一个数组的值,外加要在每个值上运行一个函数作为参数,本身返回一个promise,其完成值是一个数组,该数组(保持映射顺序)保存任务执行之后的异步完成值。
- Promise API概述:
new Promise(..)
构造器:- 有启示性的构造器
Promise(..)
必须和new一起使用,并且必须提供一个函数回调。这个回调是同步的或立即调用的。这个函数接受两个函数回调,用以支持promise的决议,通常把这两个函数称为resolve(..)
和reject(..)
。 resolve(..)
既可能完成promise,也可能拒绝,要根据传入参数而定。如果传的是一个非Promise、非thenable的立即值,这个promise就会用这个值完成。如果传的是一个真正的Promise或thenable值,这个值就会被递归展开,并且(要构造的)promise将取用其最终决议值或状态。reject(..)
就是拒绝这个promise。
- 有启示性的构造器
Promise.resolve(..)
:创建一个已完成的Promise的快捷方式。会展开thenable值,返回的Promise采用传入的这个thenable的最终决议值,可能是完成也可能是拒绝。如果传入的是真正的Promise,那么它什么都不会做,只会直接把这个值返回。Promise.reject(..)
:创建一个已被拒绝的Promise的快捷方式。then(..)
和catch(..)
:每个Promise实例都有这两个方法,通过这两个方法可以为这个Promise注册完成和拒绝处理函数。Promise决议后就会立即异步调用这两个处理函数之一。then(..)
接受一个或两个参数,第一个用于完成回调,第二个用于拒绝回调。两者中的任何一个被省略或者作为非函数值传入的话,就会替代为相应的默认回调。默认完成回调只是把消息传递下去,而默认拒绝回调则只是重新抛出(传播)其接收到的出错原因。catch(..)
只接受一个拒绝回调作为参数,并自动替换默认完成回调,即等价于then(null,..)
。then(..)
和catch(..)
都会创建并返回一个新的promise,这个promise可以用于实现Promise链式流程控制。
- Promise局限性:
- 顺序错误处理:Promise链中的错误很容易被无意中忽略掉。很多时候并没有为Promise链序列的中间步骤保留的引用,就无法关联错误处理函数来可靠地检查错误。
- 单一值:Promise只能有一个完成值或一个拒绝理由。
- 单决议:Promise只能被决议一次(完成或拒绝)。
- 惯性:运动状态(使用回调的)的代码库会一直保持运动状态(使用回调的),直到受到一位聪明的、理解Promise的开发者的作用。
- 无法取消的Promise:一旦创建了一个Promise并为其注册了完成和/或拒绝处理函数,如果出现某种情况使得这个任务悬而未决的话,没有办法从外部停止它的进程。
- Promise性能:没有真正提供可信任性保护支持的列表以供选择。
生成器
- 打破完整运行:
- 生成器是一类特殊的函数,可以一次或多次启动和停止,并一定非得要完成。
- 生成器除了能够接收参数并提供返回值之外,甚至提供了内建消息输入输出能力,通过
yield
和next(..)
构成了一个双向消息传递系统。 - 规范和所有兼容浏览器都会丢弃给第一个
next(..)
的任何东西,只有暂停的yield
才能接受通过next(..)
传递的值。 - 每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器来控制的是这个生成器实例。
- 生成器产生值:
- 迭代器是一个定义良好的接口,用于从一个生产者一步步得到一系列值。JavaScript迭代器的接口与多数语言类似,每次想要从生产者得到下一个值的时候调用
next(..)
。 - 生成器可以在运行当中(完全保持其状态)暂停,并且将来再从暂停的地方恢复运行。这种交替的暂停和恢复是合作性的而不是抢占式的,通过关键字
yield
实现,只有控制生成器的迭代器具有恢复生成器的能力(通过next(..)
)。 - iterable(可迭代)指一个包含可以在其值上迭代的迭代器的对象。
- 从ES6开始,从一个iterable中提取迭代器的方法是:iterable必须支持一个函数,其名称是专门的ES6符号值
Symbol.iterable
。调用这个函数时,它会返回一个迭代器。 - 可以把生成器看作一个值的生产者,通过迭代器接口的
next(..)
调用一次提取出一个值。 - 因为生成器会在每个
yield
处暂停,憨憨*something()
的状态(作用域)会被保持,即意味着不需要闭包在调用之间保持变量状态。 for..of
循环的“异常结束”(即提前终止),通常由break、return或者未捕获异常引起,会向生成器的迭代器发送一个信号使其终止。也可以在外部通过return(..)
手工终止生成器的迭代器实例。如果生成器内try..finally
语句,生成器终止就会出发finally语句。
- 迭代器是一个定义良好的接口,用于从一个生产者一步步得到一系列值。JavaScript迭代器的接口与多数语言类似,每次想要从生产者得到下一个值的时候调用
- 异步迭代生成器:
- 从本质上而言,我们把异步作为实现细节抽象了出去,使得我们可以以同步顺序的形式追踪流程控制:“发出一个Ajax请求,等它完成之后打印出结果”。
- 生成器yield暂停的特性意味着我们不仅能够从异步函数调用得到看似同步的返回值,还可以同步捕获来自这些异步函数调用的错误。
- 在异步控制流程方面,生成器的关键优点是:生成器内部的代码是以自然的同步/顺序方式表达任务的一系列步骤。其技巧在于,我们把可能的异步隐藏在了关键字yield的后面,把异步移动到控制生成器的迭代器的代码部分。
- 生成器+Promise:生成器yield出Promise,然后其控制生成器的迭代器来执行它,直到结束。
- 生成器委托:
- yield委托的主要目的是代码组织,以达到与普通函数调用的对称。
- 保持生成器分离有助于程序的可读性、可维护性和可调试性。
- 和yield委托透明地双向传递消息的方式一样,错误和异常也是双向传递的。
- 形实转换程序:JavaScript中的thunk是指一个用于调用另一个函数的函数,没有任何参数。从更大的角度来说,thunk本身基本上没有任何可信任性和可组合性保证,而这些是Promise的设计目标所在。单独使用thunk作为Promise的替代在这个特定的生成器异步模式里是可行的,但是与Promise具备的优势相比,这应该并不是一种理想方案。
程序性能
- Web Worker:
- JavaScript没有任何支持多线程执行的能力,但浏览器可以提供多个JavaScript引擎实例,各自运行在自己的线程上,这样就可以在每个线程上运行不同的程序。程序中每一个这样的独立多线程部分被称为一个(Web)Worker。这种类型的并行化被称为任务并行,重点在于把程序划分为多个块来并发运行。
var w1 = new Worker("http://some.url.1/mycoolworker.js")
:这样通过URL创建的Worker称为专用Worker。- Worker之间以及它们和程序之间,不会共享任何作用域或资源,而是通过一个基本的事件消息机制相互联系。
- 专用Worker和创建它的程序之间是一对一的关系,所以“message”事件没有任何歧义需要消除,它只能来自这个一对一的关系。
- Worker w1对象侦听事件:
w1.addEventListener("message", function(evt){ // evt.data })
- 发送事件给Worker w1对象:
w1.postMessage("something cool to say")
- Worker w1内部接收消息:
addEventListener("message", function(evt{ // evt.data }))
- Worker w1内部发送消息:
postMessage("a really cool reply")
- 要在创建Worker的程序中终止Worker,可以调用Worker对象上的
terminate()
。突然终止Worker线程不会给它任何机会完成它的工作或者清理任何资源。 - Web Worker非常适用于把长时间的或资源密集型的任务卸载到不同的线程中,以提高主UI线程的响应性。通常应用于处理密集型数学计算、大数据集排序、数据处理(压缩、音频分析、图像处理等)、高流量网络通信。
- 如果要在线程之间通过事件机制传递大量的信息,可以使用Transferable对象(对象所有权转移)或者结构化克隆算法(高效复制)。
- SharedWorker是一个整个站点或者app的所有页面实例都可以共享的中心Worker。
- SIMD:
- 单指令多数据(SIMD)是一种数据并行方式,与Web Worker的任务并行相对。这里的重点是并行处理数据的多个位。
- SIMD把CPU级的并行数学运算映射到JavaScript API,以获得高性能的数据并行运算,比如在大数据集上的数字处理。
- 通过SIMD,线程不再提供并行,取而代之的是现在CPU通过数字“向量”以及可以在所有这些数字上并行操作的指令,来提供SIMD功能,这是利用低级指令级并行的底层运算。
- asm.js:这个标签是指JavaScript语言中可以高度优化的一个子集。通过小心避免某些难以优化的机制和模式(垃圾收集、类型强制转换等等),asm.js风格的代码可以被JavaScript引擎识别并进行特别激进的底层优化。
性能测试与调优
- 性能测试:性能测试工具——Benchmark.js、众包性能测试工具——jsPerf.com。
- 尾调用优化(TCO):尾调用就是一个出现在另一个函数“结尾”处的函数调用。调用一个新的函数需要额外的一块预留内存来管理调用栈,称为栈帧。TCO允许一个函数在结尾处调用另一个函数来执行,不需要任何额外资源,这意味着对递归算法来说,引擎不再需要限制栈深度。ES6确保了JavaScript可以在所有符合ES6+的浏览器中依赖这个优化。
下卷
起步上路
深入编程
- 语句由一个或多个表达式组成。一个表达式是对一个变量或值的引用,或者是一组值和变
量与运算符的组合。 - JavaScript 引擎实际上是动态编译程序,然后立即执行编译后的代码。
- 在值上执行动作需要运算符,执行各种类型的动作需要值和类型,在程序的执行过程中需要变量来保存数据(也就是状态)。
- 有关运算符的更多细节以及这里没有覆盖到的更多介绍,参见Mozilla开发者网络的《表达式与运算符》。
- 代码注释是编写可读代码的一种有效方法,能让你的代码更易于理解和维护,如果以后出 现问题的话也更加容易进行修复。
- 弱类型(也称为动态类型)允许一个变量在任意时刻存放任意类型的值。JavaScript采用了动态类型,变量可以持有任意类型值而不存在类型强制。
- 常量,即声明一个变量,赋予一个特定值,然后这个值在程序执行过程中保持不变。通常来说,在JavaScript中这些常量的声明通常放在程序的开头,作为常量的变量用大写表示,多个单词之间用下划线
_
分隔。 - 在JavaScript中,使用一对大括号
{ .. }
在一个或多个语句外来表示块。通常来说,块会与其他某个控制语句组合在一起,比如 if 语句或循环。 - 程序中有很多种方法可以用于表示条件判断(也就是决策)。最常用的是 if 语句。本质上就是在表达“如果这个条件是真的,那么进行后续这些……”。
- while循环和do..while循环形式展示了重复一个语句块直到一个条件判断求值不再为真这个概念。唯一实际区别是,条件判断在第一次迭代执行前(while)检查还是在第一次迭代后(do..while)检查。
- 编程需要函数将代码组织为逻辑上可复用的块。通常来说,函数是可以通过名字被“调用”的已命名代码段,每次调用,其中的代码就会运行。
- 在JavaScript中,每个函数都有自己的作用域。作用域基本上是变量的一个集合以及如何通过名称访问这些变量的规则。
- 只有函数内部的代码才能访问这个函数作用域中的变量。
- 同一个作用域内的变量名是唯一的。
- 作用域是可以彼此嵌套的,一个作用域内的代码可以访问这个作用域内以及任何包围在它之外的作用域中的变量。
深入JavaScript
- 值与对象:
- JavaScript的值有类型,但变量无类型,变量只是这些值的容器。以下是可用的内置类型:字符串、数字、布尔型、null、undefined、对象、符号(ES6 中新增的)。
- 对象类型是指一个组合值,你可以为其设置属性(命名的位置),每个属性可以持有属于
自己的任意类型的值。 - 数组是一个持有(任意类型)值的对象,这些值不是通过命名属性 / 键值索引,而是通过 数字索引位置。
- 函数也同样是对象的一个子类型,typeof返回”function”。function可以拥有属性,但通常只在很少的情况下才会使用函数的对象属性。
- JavaScript程序中有两种主要的值比较:相等与不等。不管比较的类型是什么,任何比较
的结果都是严格的布尔值(true 或者 false)。- 相等运算符有四种:
==
、===
、!=
和!==
。==
检查的是允许类型转换情况下的值的相等性,而===
(严格相等)检查不允许类型转换情况下的值的相等性。- 如果要比较的两个值的任意一个(即一边)可能是
true
或者false
值,那么要避免使 用==
,而使用===
。 - 如果要比较的两个值中的任意一个可能是特定值(
0
、""
或者[]
——空数组),那么避 免使用==
,而使用===
。 - 在所有其他情况下,使用
==
都是安全的。不仅仅只是安全而已,这在很多情况下也会 简化代码,提高代码的可读性。 - 如果是比较两个非原生值的话,比如对象(包括函数和数组),因为这些值通常是通过引用访问的,所以
==
和===
比较只是简单地检查这些引用是否匹配,而完全不关心其引用的值是什么。
- 如果要比较的两个值的任意一个(即一边)可能是
- 运算符
<
,>
、<=
和>=
用于表示不等关系,在规范中被称为“关系比较”。
- 相等运算符有四种:
- JavaScript中有显式与隐式两种类型转换。显式的类型转换就是可以在代码中看到的类型由一种转换到另一种,而隐式的类型转换多是某些其他运算可能存在的隐式副作用而引发的类型转换。
- JavaScript中“假”值的详细列表如下:””(空字符串)、0、-0、NaN( 无效数字)、null、undefined、false;任何不在“假”值列表中的值都是“真”值。
- 变量:
- 在JavaScript中,变量的名称(包括函数名称)必须是有效的标识符。标识符必须由 a
z、AZ、$ 或 _ 开始。它可以包含前面所有这些字符以及数字 0~9。 - 函数作用域:
- 如果使用关键字var声明一个变量,那么这个变量就属于当前的函数作用域,如果声明是 发生在任何函数外的顶层声明,那么这个变量则属于全局作用域。
- 提升:无论var出现在一个作用域中的哪个位置,这个声明都属于整个作用域,在其中到处都是可以访问的。var声明概念上“移动”到了其所在作用域的最前面。
- 嵌套作用域:声明后的变量在这个作用域内是随处可以访问的,包括所有低层/内层的作用域。试图在一个作用域中访问一个不可访问的变量,那么就会抛出ReferenceError。试图设定尚未声明的变量,那么就会导致在顶层全局作用域创建这个变量或者出现错误。
- 条件判断:if语句、switch语句、条件运算符。
- 严格模式:ES5 为这个语言新增了“严格模式”,严格限制了某些行为的规则。这些限制可以将代码保持在一个更安全、更适当的规范集合之内,也更容易让引擎优化你的代码。
- 在JavaScript中,变量的名称(包括函数名称)必须是有效的标识符。标识符必须由 a
- 作为值的函数:不仅可以向函数传入值(参数),函数本身也可以作为值赋给变量或者向其他函数传入,又或者从其他函数传出。
- 立即调用函数表达式:
(function IIFE(){ .. })();
函数表达式外面的( .. )
就是JavaScript语法能够防止其成为普通函数声明的部分。表达式最后的()
(即})();
这一行)实际上就表示立即执行前面给出的函数表达式。 - 闭包:可以将闭包看作“记忆”并在函数运行完毕后继续访问这个函数作用域(其变量)的一种方法。在JavaScript中,闭包最常见的应用是模块模式。模块允许你定义外部不可见的私有实现细节(变量、函数),同时也可以提供允许从外部访问的公开API。
- 立即调用函数表达式:
- this标识符:如果一个函数内部有一个this引用,那么这个this通常指向一个对象。但它指向的是哪个对象要根据这个函数是如何被调用来决定。
- 原型:当引用对象的某个属性时,如果这个属性并不存在,那么JavaScript会自动使用对象的内部原型引用找到另外一个对象来寻找这个属性。
- 从一个对象到其后备对象的内部原型引用的链接是在创建对象时发生的。展示这一点的最简单的方法就是使用内置工具
Object.create(..)
。这个特性最常见的使用(我认为是误用)方式就是模拟/伪装带“继承”关系的“类”机制。 - 更自然应用原型的方式是被称为“行为委托”的模式,其设计意图是,被链接对象能够将 其所需要的行为委托给另外一个对象。
- 从一个对象到其后备对象的内部原型引用的链接是在创建对象时发生的。展示这一点的最简单的方法就是使用内置工具
- 旧与新:可以使用两种主要的技术,即polyfilling和transpilling,向旧版浏览器“引入”新版的JavaScript特性。
- polyfill用于表示根据新特性的定义,创建一段与之行为等价但能够在旧的JavaScript环境中运行的代码。
- transpiling通过工具将新版代码转换为等价的旧版代码。
- 非JavaScript:最常见的非JavaScript就是DOM API,比如document、console、alert等,通常被称为“宿主对象”。
ES6及更新版本
ES? 现在与未来
- 版本:
- JavaScript 标准的官方名称是“ECMAScript”(简称“ES”)。
- 第一个流行起来的JavaScript版本是ES3,它成为浏览器IE6-8和早前的旧版Android 2.x移动浏览器的JavaScript标准。
- 2009年,ES5正式发布(然后是 2011 年的 ES5.1),在当代浏览器(包括Firefox、Chrome、Opera、Safari 以及许多其他类型)的进化和爆发中成为JavaScript广泛使用的标准。
- 下一个JavaScript版本(发布日期从2013年拖到2014年,然后又到2015年)标签,之前的共识显然是ES6。
- transpiling:
- transpiling(transformation + compiling,转换+编译)利用 专门的工具把你的ES6代码转化为等价(或近似!)的可以在ES5环境下工作的代码。
- polyfill:也称为shim,在可能的情况下,polyfill会为新环境中的行为定义在旧环境中的等价行为。语法不能polyfill,而API通常可以。
- 保持JavaScript发展更新的最好战略就是在你的代码中引入polyfill shim,并且在构建过程中加入transpiler步骤。
语法
- 块作用域声明:
- let声明:
- 和传统的var声明变量不同,不管出现在什么位置,var都是归属于包含它的整个函数作用域。let声明归属于块作用域,但是直到在块中出现才会被初始化。
- 过早访问let声明的引用导致的这个ReferenceError严格说叫作临时死亡区(Temporal Dead Zone,TDZ)错误——在访问一个已经声明但没有初始化的变量。
- let+for:for循环头部的
let i
不只为for循环本身声明了一个i,而是为循环的每一次迭代都重新声明了一个新的i。这意味着loop迭代内部创建的闭包封闭的是每次迭代中的变量。
- const声明:
- const用于创建常量,是一个设定了初始值之后就只读的变量。
- const声明必须要有显式的初始化。
- 常量是对赋值的那个变量的限制,只是赋值本身不可变。如果这个值是复杂值,比如对象或者数组,其内容仍然是可以修改的。
- 块作用域函数:块内声明的函数,其作用域在这个块内。
- let声明:
- spread/rest:
...
是展开或收集的运算符,取决于它在哪/如何使用。- 当
...
用在数组之前时(实际上是任何iterable),它会把这个变量“展开”为各个独立的值。也为我们提供了可以替代apply(..)
方法的一个简单的语法形式。 - 另外一种常见用法基本上可以被看作反向的行为
function foo(x, y, ...z) { console.log( x, y, z ); } foo( 1, 2, 3, 4, 5 ); // 1 2 [3,4,5]
- 与把一个值展开不同,
...
把一系列值收集到一起成为一个数组。 - 如果没有命名参数的话,
...
就会收集所有的参数。 - 它为弃用很久的arguments提供了一个非常可靠的替代形式。
- 默认参数值:
- 为缺失参数赋默认值。
- 默认值表达式是惰性求值的,只在需要的时候运行,即参数的值省略或为undefined的时候。
- 函数声明中形式参数是在它们自己的作用域中(可以把它看作是就在函数声明包裹的
( .. )
的作用域中),而不是在函数体作用域中。这意味着在默认值表达式中的标识符引用首先匹配到形式参数作用域,然后才会搜索外层作用域。 - 如果需要在没有指定其他函数情况下的默认cb是一个没有操作的空函数调用,那么这个声明可以是
cb = Function.prototype
,Function.prototype
本身就是一个没有操作的空函数,这样就省去了在线函数表达式的创建过程。
- 解构:是一个结构化赋值方法,专用于数组解构和对象解构。
- 对象属性赋值模式:
{ x, .. }
是省略掉了x:
部分,这里的语法模式是souce: target(或者说是value:variable-alias)。 - 解构是一个通用的赋值操作,不只是声明。对于对象解构形式来说,如果省略了var/let/const声明符,就必须把整个赋值表达式用
( )
括起来。因为如果不这样做,语句左侧的{..}
作为语句中的第一个元素就会被当作是一个块语句而不是一个对象。 - 对象解构形式允许多次列出同一个源属性(持有值类型任意)。意味着可以解构子对象/数组属性,同时捕获子对象/类的值本身。
- 对象或者数组解构的赋值表达式的完成值是所有右侧对象/数组的值。通过持有对象/数组的值作为完成值,可以把解构赋值表达式组成链。
- 对象属性赋值模式:
- 太多,太少,刚刚好:
- 如果为比解构/分解出来的值更多的值赋值,那么多余的值会被赋为undefined。符合“undefined就是缺失”原则。
- 使用与前面默认函数参数值类似的
=
语法,解构的两种形式都可以提供一个用来赋值的默认值,可以组合使用默认值赋值和前面介绍的赋值表达式语法。 - 如果解构的值中有嵌套的对象或者数组,也可以解构这些嵌套的值,可以把嵌套解构当作一种展平对象名字空间的简单方法。
- 可以用箭头IIFE代替一般的
{ }
块和let
声明来实现块封装。解构赋值/默认值会被放在参数列表中,而重组的过程会被放在函数体的return
语句中。
- 对象字面量扩展:
- 简洁属性:如果需要定义一个与某个词法标识符同名的属性的话,可以把
x: x
简写为x
。 - 简洁方法:
- 关联到对象字面量属性上的函数也有简洁形式,对象中的
x: function(){ .. }
可简写为x() { .. }
。 - 简洁方法意味着匿名函数表达式,应该只在不需要它们执行递归或者事件绑定/解绑定的时候使用。否则就按照老式的
something: function something(..)
方法来定义吧。
- 关联到对象字面量属性上的函数也有简洁形式,对象中的
- 简洁属性:如果需要定义一个与某个词法标识符同名的属性的话,可以把
- 计算属性名:
- 用来支持指定一个要计算的表达式,其结果作为属性名。
- 对象字面定义属性名位置的
[ .. ]
中可以放置任意合法表达式。 - 计算属性名最常见的用法可能就是和Symbols共同使用。
var o = {[Symbol.toStringTag]: "really cool thing"};
。 - super只允许在简洁方法中出现,而不允许在普通函数表达式属性中出现。也只允许以
super.XXX
的形式(用于属性/方法访问)出现,而不能以super()
的形式出现。
- 模板字面量:
- 使用`作为界定符的字符串字面量,支持嵌入基本的字符串插入表达式,其中任何
${..}
形式的表达式都会被立即在线解析求值。 - 插入字符串字面量的一个优点是它们可以分散在多行,插入字符串字面量中的换行(新行)会在字符串值中被保留。
- 在插入字符串字面量的
${..}
内可以出现任何合法的表达式,包括函数调用、在线函数表达式调用,甚至其他插入字符串字面量。 - 插入字符串字面量有点像是一个IIFE,在它出现的词法作用域内,没有任何形式的动态作用域。
- 标签模板字面量:
- 这是一类不需要
( .. )
的特殊函数调用。 - 标签(tag)部分,即`..`字符串字面量之前的 foo 这一部分 , 是一个要调用的函数值。
- 标签字符串字面量就像是一个插入表达式求值之后,在最后的字符串值编译之前的处理步骤,这个步骤为从字面值产生字符串提供了更多的控制。
- 标签函数接收第一个名为
strings
的数组参数,可以通过strings.raw
属性访问字符串的原始未处理版本。ES6提供了一个内建函数可以用作字符串字面量标签:String.raw(..)
。
- 这是一类不需要
- 使用`作为界定符的字符串字面量,支持嵌入基本的字符串插入表达式,其中任何
- 箭头函数:
- 箭头函数定义包括一个参数列表(零个或多个参数,如果参数个数不是一个的话要用
( .. )
包围起来),然后是标识=>
,函数体放在最后。 - 只有在函数体的表达式个数多于1个,或者函数体包含非表达式语句的时候才需要用
{ .. }
包围。如果只有一个表达式,并且省略了包围的{..}
的话,则意味着表达式前面有一个隐含的return
,就像(x,y) => x + y
。 - 箭头函数总是函数表达式;并不存在箭头函数声明。
- 箭头函数是匿名函数表达式,它们没有用于递归或者事件绑定/解绑定的命名引用。
- 箭头函数支持普通函数参数的所有功能,包括默认值、解构、rest参数,等等。
=>
箭头函数转变带来的可读性提升与被转化函数的长度负相关。这个函数越长,=>
带来的好处就越小;函数越短,=>
带来的好处就越大。=>
箭头函数的主要设计目的就是以特定的方式改变this的行为特性,解决this相关编码的一个特殊而又常见的痛点。在箭头函数内部,this绑定不是动态的,而是词法的。=>
是var self = this
的词法替代形式。- 箭头函数还有词法arguments——它们没有自己的arguments数组,而是继承自父层——词法super和new.target也是一样。
- =>适用时机规则:
- 如果有一个简短单句在线函数表达式,其中唯一的语句是
return
某个计算出的值,且这个函数内部没有this引用,且没有自身引用(递归、事件绑定/解绑定),且不会要求函数执行这些,那么可以安全地把它重构为=>
箭头函数。 - 如果你有一个内层函数表达式,依赖于在包含它的函数中调用
var self = this hack
或者.bind(this)
来确保适当的this绑定,那么这个内层函数表达式应该可以安全地转换为=>
箭头函数。 - 如果你的内层函数表达式依赖于封装函数中某种像
var args = Array.prototype.slice.call(arguments)
来保证arguments的词法复制,那么这个内层函数应该可以安全地转换为=>
箭头函数。 - 所有的其他情况——函数声明、较长的多语句函数表达式、需要词法名称标识符(递归等)的函数,以及任何不符合以上几点特征的函数——一般都应该避免
=>
函数语法。
- 如果有一个简短单句在线函数表达式,其中唯一的语句是
- 底线:
=>
是关于 this、arguments和super的词法绑定。
- 箭头函数定义包括一个参数列表(零个或多个参数,如果参数个数不是一个的话要用
- for..of 循环:
- 在迭代器产生的一系列值上循环,循环的值必须是一个iterable,或者说它必须是可以转换/封箱到一个iterable对象的值。terable就是一个能够产生迭代器供循环使用的对象。
- JavaScript中默认为(或提供)iterable的标准内建值包括:Arrays、Strings、Generators、Collections/TypedArrays。
- 默认情况下平凡对象并不适用
for..of
循环,因为它们并没有默认的迭代器。 - 和其他循环一样,
for..of
循环也可以通过break
、continue
、return
(如果在函数中的话)提前终止,并抛出异常。在所有这些情况中,如果需要的话,都会自动调用迭代器的return(..)
函数(如果存在的话)让迭代器执行清理工作。
- 正则表达式:
- Unicode标识:
- JavaScript字符串通常被解释成16位字符序列,这些字符对应基本多语言平面(BMP)中的字符。但还有很多UTF-16字符在这个范围之外,所以字符串中还可能包含这些多字节字符。在ES6中,u标识符表示正则表达式用Unicode(UTF-16)字符来解释处理字符串,把这样的扩展字符当作单个实体来匹配。
- u标识使得
+
和*
这样的量词把整个Unicode码点作为单个字符而应用,而不仅仅是应用于字符的低位(也就是符号的最右部分),在字符类内部出现的Unicode字符也是一样。
- 定点标识:
- 定点主要是指在正则表达式的起点有一个虚拟的锚点,只从正则表达式的lastIndex属性指定的位置开始匹配。
test(..)
使用lastIndex
作为str
中精确而且唯一的位置寻找匹配。不会向前移动去寻找匹配——要么匹配位于lastIndex
位置上,要么就没有匹配。- 如果匹配成功,
test(..)
会更新lastIndex指向紧跟匹配内容之后的那个字符。如果匹配失败,test(..)
会把lastIndex
重置回0。 - 一般的没有用
^
限制输入起始点匹配的非定点模式可以自由地在输入字符串中向前移动寻找匹配内容。而定点模式则限制了模式只能从lastIndex
开始匹配。 - 另一种理解思路是把
y
看作一个在模式开始处的虚拟锚点,限制模式(也就是限制匹配的起点)相对于lastIndex
的位置。 - 可以应用y模式在字符串中执行重复匹配最适合的场景可能就是结构化的输入字符串。
- ^ 是一个总是指向输入起始处的锚点,和lastIndex完全没有任何关系。
y
加上^
再加上lastIndex > 0
是一个不兼容的组合,总是会导致匹配失败。
- 正则表达式flags:
- 从source属性的内容中解析出正则表达式对象应用的标识。
- ES6规范中规定了表达式的标识按照这个顺序列出:”gimuy”,无论原始指定的模式是什么。
- ES 的另一个调整是如果把标识传给已有的正则表达式,
RegExp(..)
构造器现在支持flags。
- Unicode标识:
- 数字字面量扩展:在非十进制数字字面量表示方面,ES6继续支持旧有的修改/变体。同时现在有了一个正式八进制形式、一个补充的十六进制形式,以及一个全新的二进制形式。唯一合法的小数形式是十进制的。八进制、十六进制和二进制都是整数形式。
- Unicode:
- Unicode字符范围从0x0000到0xFFFF,包含可能看到和接触到的所有(各种语言的)标准打印字符。这组字符称为基本多语言平面(BMP)。
- 在ES6中有了可以用于作Unicode转义(在字符串和正则表达式中)的新形式,称为Unicode码点转义。
- 可以在查询长度之前使用ES6的
String#normalize(..)
工具对这个值执行Unicode规范化。normalize(..)
接受像"e\u0301"
这样的一个序列,然后把它规范化为"\xE9"
。甚至如果有合适的Unicode符号可以合并的话,规范化可以把多个相邻的组合符号合并。 - 组合
String.fromCodePoint(..)
和codePointAt(..)
能获得支持Unicode的charAt(..)
。 - Unicode也可以用作标识符名(变量、属性等)。
- 符号:
- symbol没有字面量形式。下面是创建symbol的过程:
var sym = Symbol( "some description" );
- 不能也不应该对
Symbol(..)
使用new
。它并不是一个构造器,也不会创建一个对象。 - 传给
Symbol(..)
的参数是可选的。如果传入了的话,应该是一个为这个symbol
的用途给出用户友好描述的字符串。 typeof
的输出是一个新的值(“symbol”),这是识别symbol的首选方法。- 符号本身的内部值——称为它的名称(name)——是不在代码中出现且无法获得的。可以把这个符号值想象为一个自动生成的、(在应用内部)唯一的字符串值。
- 符号的主要意义是创建一个类(似)字符串的不会与其他任何值冲突的值。
- 可以在对象中直接使用符号作为属性名/键值,比如用作一个特殊的想要作为隐藏或者元属性的属性。
Symbol.for(..)
在全局符号注册表中搜索,来查看是否有描述文字相同的符号已经存在,如果有的话就返回它。如果没有的话,会新建一个并将其返回。换句话说,全局注册表把符号值本身根据其描述文字作为单例处理。这也意味着只要使用的描述名称匹配,可以在应用的任何地方通过Symbol.for(..)
从注册表中获取这个符号。- 可以使用
Symbol.keyFor(..)
提取注册符号的描述文本(键值)。 - 如果把符号用作对象的属性/键值,那么它会以一种特殊的方式存储,使得这个属性不出现在对这个对象的一般属性枚举中。可以使用
Object.getOwnPropertySymbols( o ); // [ Symbol(bar) ]
取得对象的符号属性列表。 - 规范使用
@@
前缀记法来指代内置符号,最常用的一些是:@@iterator
、@@toStringTag
、@@toPrimitive
。
- symbol没有字面量形式。下面是创建symbol的过程:
代码组织
- 迭代器:
- 迭代器是一种有序的、连续的、基于拉取的用于消耗数据的组织方式。
- 接口:
- Iterator接口:
Iterator [required]
next() {method}
: 取得下一个IteratorResult
- 有些迭代器还扩展支持两个可选成员:
Iterator [optional]
return() {method}
:停止迭代器并返回IteratorResultthrow() {method}
:报错并返回IteratorResult
- IteratorResult接口指定如下:
IteratorResult
value {property}
:当前迭代值或者最终返回值(如果undefined为可选的)done {property}
:布尔值,指示完成状态
@@iterator
是一个特殊的内置符号,表示可以为这个对象产生迭代器的方法:Iterable
@@iterator() {method}
:产生一个 Iterator
IteratorResult
接口指定了从任何迭代器操作返回的值必须是下面这种形式的对象:{ value: .. , done: true / false }
- 严格来说,如果不提供
value
可以被当作是不存在或者未设置,就像值undefined
,那么value
是可选的。因为访问res.value
的时候,不管它存在且值为undefined
,还是根本不存在,都会产生undefined
,这个属性的存在/缺席更多的是一个实现细节或者优化技术(或者二者兼有),而非功能问题。
- Iterator接口:
- next()迭代:
var arr = [1,2,3]; var it = arr[Symbol.iterator](); it.next();
- 每次在这个
arr
值上调用位于Symbol.iterator
的方法时,都会产生一个全新的迭代器。 - 代码在提取值3的时候,迭代器it不会报告
done: true
。必须得再次调用next()
,越过数组结尾的值,才能得到完成信号done: true
。 - 迭代器的
next(..)
方法可以接受一个或多个可选参数。绝大多数内置迭代器没有利用这个功能,尽管生成器的迭代器肯定有。 - 包括所有内置迭代器,在已经消耗完毕的迭代器上调用
next(..)
不会出错,而只是简单地继续返回结果{ value: undefined, done: true }
。
- **可选的return(..)和throw(..)**:
return(..)
被定义为向迭代器发送一个信号,表明消费者代码已经完毕,不会再从其中提取任何值。- 这个信号可以用于通知生产者(响应
next(..)
调用的迭代器)执行可能需要的清理工作,比如释放/关闭网络、数据库或者文件句柄资源。 - 如果迭代器存在
return(..)
,并且出现了任何可以自动被解释为异常或者对迭代器消耗的提前终止的条件,就会自动调用return(..)
。也可以手动调用return(..)
。 return(..)
就像next(..)
一样会返回一个IteratorResult
对象。一般发送给return(..)
的可选值将会在这个IteratorResult
中作为value返回。
- 这个信号可以用于通知生产者(响应
throw(..)
用于向迭代器报告一个异常/错误,但并不一定意味着迭代器的完全停止。
- 迭代器循环:
- ES6的
for..of
循环直接消耗一个符合规范的iterable。 - 如果一个迭代器也是一个iterable,那么它可以直接用于
for..of
循环。你可以通过为迭代器提供一个Symbol.iterator
方法简单返回这个迭代器本身使它成为 iterable。
- ES6的
- 自定义迭代器:除了标准的内置迭代器,也可以自定义迭代器。要使得它们能够与ES6的消费者工具(比如,
for..of
循环以及...
运算符)互操作,所需要做的就是使其遵循适当的接口。 - 迭代器消耗:
for..of
循环、spread运算符...
会一个接一个地消耗迭代器项目。数组解构可以部分或完全(如果和rest/gather运算符...
配对使用的话)消耗一个迭代器。
- 生成器:
- 生成器可以在执行当中暂停自身,可以立即恢复执行也可以过一段时间之后恢复执行。
- 在执行当中的每次暂停/恢复循环都提供了一个双向信息传递的机会,生成器可以返回一个值,恢复它的控制代码也可以发回一个值。
- 语法:
function *foo() { .. }
- 和普通函数主要的区别是,执行生成器并不实际在生成器中运行代码。相反,它会产生一个迭代器控制这个生成器执行其代码。
- 生成器在每次被调用的时候都产生了一个全新的迭代器。实际上,可以同时把多个迭代器附着在同一个生成器上。
- **
yield
**:生成器中用来标示暂停点。- 生成器中
yield
可以出现任意多次。 yield ..
表达式可以出现在所有普通表达式可用的地方,它不只发送一个值,没有值的yield
等价于yield undefined
,而且还会接收(也就是被替换为)最终的恢复值。- 因为
yield
关键字的优先级很低,几乎yield..
之后的任何表达式都会首先计算,然后再通过yield
发送。只有spread运算符...
和逗号运算符,
拥有更低的优先级,也就是说它们会在yield
已经被求值之后才会被绑定。 - 和
=
赋值一样,yield
也是“右结合”的,也就是说多个yield
表达式连续出现等价于用(..)
从右向左分组。所以,yield yield yield 3
会被当作yield(yield(yield 3))
。
- 生成器中
- **yield***:
- 也称为yield委托,语法上说,
yield *..
行为方式与yield..
完全相同。 yield * ..
需要一个iterable,然后会调用这个iterable的迭代器,把自己的生成器控制委托给这个迭代器,直到其耗尽。- 例如
function *foo() { yield *[1,2,3]; }
,值[1,2,3]
产生了一个迭代器,一步输出一个值,所以*foo()
生成器会随着消耗这些值把它们yield
出来。
- 也称为yield委托,语法上说,
- 迭代器控制:
- 假定所有都被计算,生成器完整运行到结束,
next()
调用总是会比yield
语句多1个。 - 可以把生成器看作是值的产生器,其中每次迭代就是产生一个值来消费。
- 第一个
next()
调用初始的暂停状态启动生成器,运行直到第一个yield
。在调用第一个next()
的时候,并没有yield..
表达式等待完成。如果向第一个next()
调用传入一个值,这个值会马上被丢弃,因为并没有yield
等待接收这个值。
- 假定所有都被计算,生成器完整运行到结束,
- 提前完成:
- 生成器上附着的迭代器支持可选的
return(..)
和throw(..)
方法。这两种方法都有立即终止一个暂停的生成器的效果。 return(..)
:return(x)
有点像强制立即执行一个return x
,这样就能够立即得到指定值。一旦生成器完成,或者正常完毕或者像前面展示的那样提前结束,都不会再执行任何代码也不会返回任何值。return(..)
除了可以手动调用,还可以在每次迭代的末尾被任何消耗迭代器的ES6构件自动调用,比如for..of
循环和spread运算符...
。- 目的是通知生成器如果控制代码不再在它上面迭代,那么它可能就会执行清理任务(释放资源、重置状态等)。和普通的函数清理模式相同,完成这一点的主要方式是通过
finally
子句。 - 不要把
yield
语句放在finally
子句内部,这样会延后你的return(..)
调用的完成,因为任何在finally
子句内部的yield..
表达式都会被当作是暂停并发送消息。
throw(..)
:- 调用
throw(x)
基本上就相当于在暂停点插入一个throw x
。 - 除了对异常处理的不同,
throw(..)
同样引起提前完成,在当前暂停点终止生成器的运行。 - 和
return(..)
不同,迭代器的throw(..)
方法从来不会被自动调用。 - 如果在调用
throw(..)
的时候有try..finally
子句在生成器内部等待,那么在异常传回调用代码之前finally
子句会有机会运行。
- 调用
- 生成器上附着的迭代器支持可选的
- 错误处理:
- 生成器的错误处理可以表达为
try..catch
,它可以在由内向外和由外向内两个方向工作。 - 错误也可以通过
yield *
委托在两个方向上传播。
- 生成器的错误处理可以表达为
- Transpile生成器:Facebook的Regenerator工具。
- 生成器使用:主要适用于两种模式——产生一系列值、顺序执行的任务队列。
- 模块:
- 旧方法:传统的模块模式基于一个带有内部变量和函数的外层函数,以及一个被返回的“public API”,这个“public API”带有对内部数据和功能拥有闭包的方法。其中常用的是异步模块定义(AMD),还有一种是通用模块定义(UMD)。
- ES6模块和过去处理模块的方式之间的概念区别:
- ES6使用基于文件的模块,即一个文件一个模块。意味着如果想要把ES6模块直接加载到浏览器Web应用中,需要分别加载,而不是作为一大组放在单个文件中加载。
- ES6模块的API是静态的。即需要在模块的公开API中静态定义所有最高层导出,之后无法补充。
- ES6模块是单例,模块只有一个实例,其中维护了它的状态。每次向其他模块导入这个模块的时候,得到的是对单个中心实例的引用。
- 模块的公开API中暴露的属性和方法并不仅仅是普通的值或引用的赋值。它们是到内部模块定义中的标识符的实际绑定(几乎类似于指针)。对于ES6来说,导出一个局部私有变量,即使当前它持有一个原生字符串/数字等,导出的都是到这个变量的绑定。如果模块修改了这个变量的值,外部导入绑定现在会决议到新的值。
- 导入模块和静态请求加载(如果还没加载的话)这个模块是一样的。如果是在浏览器环境中,这意味着通过网络阻塞加载;如果是在服务器上(比如 Node.js),则是从文件系统的阻塞加载。
- 新方法:
- 支撑ES6模块的两个主要新关键字是
import
和export
。 import
和export
都必须出现在使用它们的最顶层作用域。举例来说,不能把import或export放在if条件中;它们必须出现在所有代码块和函数的外面。- 导出API成员:
export
关键字或者是放在声明的前面,或者是作为一个操作符(或类似的)与一个要导出的绑定列表一起使用。没有用export
标示的一切都在模块作用域内部保持私有。- 模块导出不是像你熟悉的赋值运算符
=
那样只是值或者引用的普通赋值。实际上,导出的是对这些东西(变量等)的绑定(类似于指针)。 - 如果在你的模块内部修改已经导出绑定的变量的值,即使是已经导入的,导入的绑定也将会决议到当前(更新后)的值。
- 尽管显然可以在模块定义内部多次使用
export
,ES6绝对倾向于一个模块使用一个export
,称之为默认导出(default export)。默认导出把一个特定导出绑定设置为导入模块时的默认导出。绑定的名称就是default
,每个模块定义只能有一个default
。 - 除了
export default ...
形式导出一个表达式值绑定,所有其他的导出形式都是导出局部标识符的绑定。对于这些绑定来说,如果导出之后在模块内部修改某个值,外部导入的绑定会访问到修改后的值。如果并不打算更新默认导出的值,那么使用export default ..
就好。如果确实需要更新这个值,就需要使用export { .. as default }
。 - JavaScript引擎无法静态分析平凡对象的内容,这意味着它无法对静态import进行性能优化。让每个成员独立且显式地导出的优点是引擎可以对其进行静态分析和优化。
- ES6模块机制的设计意图是不鼓励模块大量导出。如果API已有多个成员,通常建议有一个单独的默认导出,同时又有其他命名导出,例如
export { foo as default, bar, baz, .. };
。 - 如果有大量API并且通过重构拆分模块不实际或者不想这么做的时候,就都用命名导出好了,提供文档说明模块用户会用
import * as ..
(名字空间导入)方法来一次把所有API引入到某个名字空间中。
- 导入API成员:
- 如果想导入一个模块API的某个特定命名成员到你的顶层作用域,可以使用下面语法:
import { foo, bar, baz } from "foo";
- 字符串”foo” 称为模块指定符(module specifier)。因为整体目标是可静态分析的语法,模块指定符必须是字符串字面值,而不能是持有字符串值的变量。
- 以对导入绑定标识符重命名,就像这样:
import { foo as theFooFunc } from "foo";
- 如果这个模块只有一个你想要导入并绑定到一个标识符的默认导出:
import foo from "foo";
或者import { default as foo } from "foo";
- ES6模块哲学强烈建议的方法是,只从模块导入需要的具体绑定。除了代码更清晰,窄导入的另一个好处是使得静态分析和错误检测(比如意外使用了错误的绑定名称)更加健壮。
- 可以把整个 API 导入到单个模块命名空间绑定,称为命名空间导入:
import * as foo from "foo";
- 如果通过
* as ..
导入的模块有默认导出,它在指定的命名空间中的名字就是 default。还可以在这个命名空间绑定之外把默认导入作为顶层标识符命名。import foofn, * as hello from "world";
- 所有导入的绑定都是不可变和/或只读的。
- 作为
import
结果的声明是“提升的”,“提升”了在模块作用域顶层的声明,使它在模块所有位置可用。
- 如果想导入一个模块API的某个特定命名成员到你的顶层作用域,可以使用下面语法:
- 支撑ES6模块的两个主要新关键字是
- 模块依赖环:A导入B,B导入A,本质上说,相互导入,加上检验两个
import
语句的有效性的静态验证,虚拟组合了两个独立的模块空间(通过绑定),这样foo(..)
可以调用bar(..)
,反过来也是一样。这和如果它们本来是声明在同一个作用域中是对称的。import
语句的静态加载语义意味着可以确保通过import
相互依赖的”foo”和”bar”在其中任何一个运行之前,二者都会被加载、解析和编译。所以它们的环依赖是静态决议的,就像期望的一样。 - 模块加载:
- import语句使用外部环境(浏览器、Node.js 等)提供的独立机制,来实际把模块标识符字符串解析成可用的指令,用于寻找和加载所需的模块。这个机制就是系统模块加载器。
- 如果在浏览器中,环境提供的默认模块加载器会把模块标识符解析为 URL,(一般来说)如果在像Node.js这样的服务器上就解析为本地文件系统路径。默认行为方式假定加载的文件是以ES6标准模块格式编写的。
- 在模块之外加载模块:直接与模块加载器交互的一个用法是非模块需要加载一个模块的情况。工具
Reflect.Loader.import(..)
把整个模块导入到命名参数(作为一个命名空间),就像前面讨论的命名空间导入import * as foo ..
一样。 - 自定义加载:另外一种与模块加载器直接交互的用法,就是需要通过配置甚至重定义来自定义其行为的情况。
- 类:
- class:
- 新的ES6类机制的核心是关键字
class
,表示一个块,其内容定义了一个函数原型的成员。 - 和对象字面量不一样,在class定义体内部不用逗号分隔成员。
- 前ES6形式和新的ES6 class形式的区别:
- 由于前ES6可用的
Foo.call(obj)
不能工作,class Foo
的Foo(..)
调用必须通过new
来实现。 function Foo
是“提升的”,而class Foo
并不是;extends ..
语句指定了一个不能被“提升”的表达式。所以,在实例化一个class
之前必须先声明它。- 全局作用域中的
class Foo
创建了这个作用域的一个词法标识符Foo
,但是和function Foo
不一样,并没有创建一个同名的全局对象属性。
- 由于前ES6可用的
- 因为
class
只是创建了一个同名的构造器函数,所以现有的instanceof
运算符对ES6类仍然可以工作。
- 新的ES6类机制的核心是关键字
- extends和super:
- ES6类通过面向类的常用术语
extends
提供了一个语法糖,用来在两个函数原型之间建立[[Prototype]]
委托链接——通常被误称为“继承”或者令人迷惑地标识为“原型继承”。 - 在构造器中,
super
自动指向“父构造器”。在方法中,super
会指向“父对象”,这样就可以访问其属性/方法了,比如 super.gimmeXY()。 Bar extends Foo
的意思就是把Bar.prototype
的[[Prototype]]
连接到Foo.prototype
。所以,在像gimmeXYZ()
这样的方法中,super
具体指Foo.prototype
,而在Bar
构造器中super
指的是Foo
。- super恶龙:
- super的行为根据其所处的位置不同而有所不同。
super(..)
意味着调用new Foo(..)
,但是实际上并不是指向Foo
自身的一个可用引用。super.constructor
指向函数Foo(..)
,但这个函数只能通过new
调用。new super.constructor
是合法的,不过它在多数情况下没什么用处,因为你无法让这个调用使用或引用当前的this对象上下文。- super并不像this那样是动态的。构造器或函数在声明时在内部建立了super引用(在class声明体内),此时super是静态绑定到这个特定的类层次上的,不能重载(至少在ES6中是这样)。
- 子类构造器:
- 对于类和子类来说,构造器并不是必须的;如果省略的话那么二者都会自动提供一个默认构造器。
- 子类构造器中调用
super(..)
之后才能访问this
。因为创建/初始化实例this的实际上是父构造器。 - 默认子类构造器自动调用父类的构造器并传递所有参数。可以把默认子类构造器看成下面这样:
constructor(...args) { super(...args); }
- 扩展原生类:新的class和extend设计带来的最大好处之一是可以构建内置类的子类,比如Array。
- new.target:
new.target
也称为元属性。在任何构造器中,new.target
总是指向new
实际上直接调用的构造器,即使构造器是在父类中且通过子类构造器用super(..)
委托调用。在一般函数中它通常是undefined
。- 除了访问静态属性/方法之外,类构造器中的
new.target
元属性没有什么其他用处。
- static:
- 在为一个类声明了static方法(不只是属性)的情况下,它是直接添加到这个类的函数对象上的,而不是在这个函数对象的prototype对象上。static成员在函数构造器之间的双向/并行链上。
static
适用的一个地方就是为派生(子)类设定Symbol.species getter
(规范内称为@@species
)。如果当任何父类方法需要构造一个新实例,但不想使用子类的构造器本身时,这个功能使得子类可以通知父类应该使用哪个构造器。
- ES6类通过面向类的常用术语
- class:
异步流控制
- Promise:
- Promise在回调代码和将要执行这个任务的异步代码之间提供了一种可靠的中间机制来管理回调。可以被看作是同步函数返回值的异步版本。
- Promise的决议结果只有两种可能:完成或拒绝,附带一个可选的单个值。如果Promise完成,那么最终的值称为完成值;如果拒绝,那么最终的值称为原因(也就是“拒绝的原因”)。
- Promise只能被决议(完成或者拒绝)一次。之后再次试图完成或拒绝的动作都会被忽略。因此,一旦Promise被决议,它就是不变量,不会发生改变。
- 构造和使用Promise:
- 可以通过构造器
Promise(..)
构造promise
实例:var p = new Promise( function(resolve,reject){ // .. } );
- 如果调用
reject(..)
,这个promise被拒绝,如果有任何值传给reject(..)
,这个值就被设置为拒绝的原因值。 - 如果调用
resolve(..)
且没有值传入,或者传入任何非promise值,这个promise就完成。 - 如果调用
resove(..)
并传入另外一个promise,这个promise就会采用传入的 promise的状态(要么实现要么拒绝)——不管是立即还是最终。
- 如果调用
- Promise有一个
then(..)
方法,接受一个或两个回调函数作为参数。- 前面的函数(如果存在的话)会作为promise成功完成后的处理函数。
- 第二个函数(如果存在的话)会作为promise被显式拒绝后的处理函数,或者在决议过程中出现错误/异常的情况下的处理函数。
- 如果某个参数被省略,或者不是一个有效的函数——通常是null,那么一个默认替代函数就会被采用。默认的成功回调把完成值传出,默认的出错回调会传递拒绝原因值。
then(..)
和catch(..)
都会自动构造并返回另外一个promise实例,这个实例连接到接受原来的promise的不管是完成或拒绝处理函数(实际调用的那个)的返回值。- 第一个
fulfilled(..)
内部的异常(即被拒绝的promise)不会导致第一个rejected(..)
被调用,因为这个处理函数只响应第一个原始promise的决议。而第二个promise会接受这个拒绝,这个promise是在第二个then(..)
上调用的。
- 可以通过构造器
- Thenable:
- 任何提供了
then(..)
函数的对象(或函数)都被认为是thenable。Promise机制中所有可以接受真正promise状态的地方,也都可以处理thenable。 - 从根本上说,thenable就是所有类promise值的一个通用标签,这些类promise不是被真正的
Promise(..)
构造器而是被其他系统创造出来。从这个角度来说,通常thenable的可靠性要低于真正的Promise。 - 在ES6之前,并没有对
then(..)
方法名称有任何特殊保留。最可能出现的误用thenable的情况是那些使用了then(..)
方法,但是并没有严格遵循Promise风格的异步库,所以要避免把可能被误认为thenable的值直接用于Promise机制。
- 任何提供了
- Promise API:
Promise.resolve(..)
创建了一个决议到传入值的promise。对于任何还没有完全确定是可信promise的值,甚至它可能是立即值,都可以通过把它传给Promise.resolve(..)
来规范化。Promise.reject(..)
创建一个立即被拒绝的promise。它并不区分接收的值是什么。所以,如果传入promise或thenable来拒绝,这个promise/thenable本身会被设置为拒绝原因,而不是其底层值。Promise.all([ .. ])
接受一个或多个值的数组(比如,立即值、promise、thenable)。它返回一个promise,如果所有的值都完成,这个promise的结果是完成;一旦它们中的某一个被拒绝,那么这个promise就立即被拒绝。Promise.race([ .. ])
等待第一个完成或者拒绝。Promise.all([])
将会立即完成(没有完成值),Promise.race([])
将会永远挂起。建议永远不要用空数组使用这些方法。
- 生成器 + Promise:Promise是一种把普通回调或者thunk控制反转反转回来的可靠系统。因此,把Promise的可信任性与生成器的同步代码组合在一起有效解决了回调所有的重要缺陷。
集合
- TypedArray:
- 带类型的数组更多是为了使用类数组语义(索引访问等)结构化访问二进制数据。名称中的“type(类型)”是指看待一组位序列的“视图”,本质上就是一个映射,比如是把这些位序列映射为8位有符号整型数组还是16位有符号整型数组,等等。
- arr的映射是按照运行JavaScript的平台的大小端设置(大端或小端)进行的。大小端的意思是多字节数字中的低字节位于这个数字字节表示中的右侧还是左侧。目前Web上最常用的是小端表示方式。
- 单个buffer可以关联多个视图。可以从非0的位置开始带类型数组视图,也可以不消耗整个buffer长度。
- 带类数组构造器支持以下形式:
[constructor\](buffer,[offset, [length]])
、[constructor\](length)
、[constructor\](typedArr)
、[constructor\](obj)
。 - 带类数组构造器的实例几乎和普通原生数组完全一样。一些区别包括具有固定的长度以及值都属于某种“类型”。ES6提供了下面这些带类数组构造器:
- Int8Array(8 位有符号整型),Uint8Array(8 位无符号整型) ——Uint8ClampedArray(8 位无符号整型,每个值会被强制设置为在 0-255 内);
- Int16Array(16 位有符号整型), Uint16Array(16 位无符号整型);
- Int32Array(32 位有符号整型), Uint32Array(32 位无符号整型);
- Float32Array(32 位浮点数,IEEE-754);
- Float64Array(64 位浮点数,IEEE-754)。
- Map:
- Map就像是一个对象(键/值对),但是键值并非只能为字符串,而是可以使用任何值,甚至是另一个对象或map。
- map 的本质是允许你把某些额外的信息(值)关联到一个对象(键)上,而无需把这个信息放入对象本身。
var m = new Map()
- 使用
get(..)
和set(..)
方法从map中设置和获取值。m.set( x, "foo" );
、m.get( x );
- 使用
delete()
方法从map中删除一个元素。m.delete( x );
- 通过
clear()
清除整个map的内容。m.clear();
- 使用
size
属性得到map的长度(也就是键的个数)。m.size;
- 使用
has(..)
方法确定一个map中是否有给定的键。m.has( x )
- 使用
values(..)
从map中得到一列值。m.values()
- 使用
keys(..)
从map中得到一列键,会返回map中键上的迭代器。m.keys()
- 使用
Map(..)
构造器也可以接受一个iterable,这个迭代器必须产生一列数组,每个数组的第一个元素是键,第二个元素是值。- 也可以在
Map(..)
构造器中手动指定一个项目(entry)列表(键/值数组的数组)
- WeakMap:
- WeakMap是map的变体,二者的多数外部行为特性都是一样的,区别在于内部内存分配(特别是其GC)的工作方式。
- WeakMap(只)接受对象作为键。这些对象是被弱持有的,也就是说如果对象本身被垃圾 回收的话,在 WeakMap 中的这个项目也会被移除。
- WeakMap的API是类似的,要比map更少一些。WeakMap没有
size
属性或clear()
方法,也不会暴露任何键、值或项目上的迭代器。
- Set:
- Set与数组(值的序列)类似,是一个值的集合,但是其中的值是唯一的;set的唯一性不允许强制转换,如果新增的值是重复的,就会被忽略。
- set的API和map类似。只是
add(..)
方法代替了set(..)
方法(某种程度上说有点讽刺),没有get(..)
方法,因为不会从集合中取一个值。 Set(..)
构造器形式和Map(..)
类似,都可以接受一个iterable,比如另外一个set
或者仅仅是一个值的数组。但是和Map(..)
接受项目(entry)列表(键/值数组的数组)不同,Set(..)
接受的是值(value)列表(值的数组)。- set的迭代器方法和map一样。对于set来说,二者行为特性不同,但它和map迭代器的行为是对称的。
keys()
和values()
迭代器都从set中yield
出一列不重复的值。
- WeakSet:就像WeakMap弱持有它的键(对其值是强持有的)一样,WeakSet对其值也是弱持有的(这里并没有键)。WeakSet的值必须是对象,而并不像set一样可以是原生类型值。
新增API
- Array:
- **静态函数 Array.of(..)**:
Array(..)
构造器有一个众所周知的陷阱,就是如果只传入一个数字参数,就会构造一个空数组,其length属性为这个数字,这个动作会产生诡异的“空槽”行为。Array.of(..)
取代了Array(..)
成为数组的推荐函数形式构造器,因为Array.of(..)
并没有这个特殊的单个数字参数的问题。
- **静态函数 Array.from(..)**:
- JavaScript中的“类(似)数组对象”是指一个有length属性,具体说是大于等于0的整数值的对象。新的ES6
Array.from(..)
可以把它们转换为真正的数组。 - 如果把类数组对象作为第一个参数传给
Array.from(..)
,它的行为方式和slice()
(没有参数)或者apply(..)
是一样的,就是简单地按照数字命名的属性从0开始直到length值在这些值上循环。 Array.from(..)
第二个参数是一个映射回调(和一般的Array#map(..)
所期望的几乎一样),如果设置了的话,这个函数会被调用,来把来自于源的每个值映射/转换到返回值。Array.from(..)
接收一个可选的第三个参数,如果设置了的话,这个参数为作为第二个参数传入的回调指定this绑定。否则,this将会是undefined。
- JavaScript中的“类(似)数组对象”是指一个有length属性,具体说是大于等于0的整数值的对象。新的ES6
- 创建数组和子类型:
of(..)
和from(..)
都使用访问它们的构造器来构造数组。所以如果使用基类Array.of(..)
,那么得到的就是Array实例;如果使用MyCoolArray.of(..)
,那么得到的就是MyCoolArray
实例。 - **原型方法 copyWithin(..)**:
- 是一个新的修改器方法,所有数组都支持。
copyWithin(..)
从一个数组中复制一部分到同一个数组的另一个位置,覆盖这个位置所有原来的值。 - 参数是target(要复制到的索引)、start(开始复制的源索引,包括在内)以及可选的end(复制结束的不包含索引)。如果任何一个参数是负数,就被当作是相对于数组结束的相对值。
copyWithin(..)
方法不会增加数组的长度。到达数组结尾复制就会停止。
- 是一个新的修改器方法,所有数组都支持。
- **原型方法 fill(..)**:用指定值完全(或部分)填充已存在的数组。
fill(..)
可选地接收参数start
和end
,它们指定了数组要填充的子集位置。 - **原型方法 find(..)**:
- 在数组中搜索一个值的最常用方法一直是
indexOf(..)
方法,这个方法返回找到值的索引,如果没有找到就返回-1
。相比之下,indexOf(..)
需要严格匹配===
。 - ES6的
find(..)
基本上和some(..)
的工作方式一样,除了一旦回调返回true/真值,会返回实际的数组值。 find(..)
接受一个可选的第二个参数,如果设定这个参数就绑定到第一个参数回调的this。否则this就是undefined。
- 在数组中搜索一个值的最常用方法一直是
- **原型方法 findIndex(..)**:返回传入一个测试条件(函数)符合条件的数组第一个元素位置。如果需要严格匹配的索引值,那么使用
indexOf(..)
;如果需要自定义匹配的索引值,那么使用findIndex(..)
。 - **原型方法 entries()、values()、keys()**:
entries()
方法返回一个数组的迭代对象,该对象包含数组的键值对 (key/value)。values()
方法返回一个新的Array Iterator对象,该对象包含数组每个索引的值。keys()
方法用于从数组创建一个包含数组键的可迭代对象。
- **静态函数 Array.of(..)**:
- Object:
- **静态函数 Object.is(..)**:执行比
===
比较更严格的值比较。 - **静态函数 Object.getOwnPropertySymbols(..)**:它直接从对象上取得所有的符号属性。
- **静态函数 Object.setPrototypeOf(..)**:设置对象的
[[Prototype]]
用于行为委托。 - **静态函数 Object.assign(..)**:第一个参数是
target
,其他传入的参数都是源,它们将按照列出的顺序依次被处理。对于每个源来说,它的可枚举和自己拥有的(也就是不是“继承来的”)键值,包括符号都会通过简单=
赋值被复制。Object.assign(..)
返回目标对象。
- **静态函数 Object.is(..)**:执行比
- Math:
- 三角函数:
cosh(..)
双曲余弦函数、acosh(..)
双曲反余弦函数、sinh(..)
双曲正弦函数、asinh(..)
双曲反正弦函数、tanh(..)
双曲正切函数、atanh(..)
双曲反正切函数、hypot(..)
平方和的平方根(也即:广义勾股定理)。 - 算术:
cbrt(..)
立方根、clz32(..)
计算 32 位二进制表示的前导0个数、expm1(..)
等价于exp(x) - 1
、log2(..)
二进制对数(以 2 为底的对数)、log10(..)
以10为底的对数、log1p(..)
等价于log(x + 1)
、imul(..)
两个数字的32位整数乘法 - 元工具:
sign(..)
返回数字符号、trunc(..)
返回数字的整数部分、fround(..)
向最接近的 32 位(单精度)浮点值取整
- 三角函数:
- Number:
- 静态属性:
Number.EPSILON
:任意两个值之间的最小差(2^-52),也是浮点数算法的精度误差值。Number.MAX_SAFE_INTEGER
:JavaScript可以用数字值无歧义“安全”表达的最大整数(2^53 - 1)Number.MIN_SAFE_INTEGER
:JavaScript可以用数字值无歧义“安全”表达的最小整数(-(2^53 - 1) 或 (-2)^53 + 1)
- **静态函数 Number.isNaN(..)**:确定传递的值是否为
NaN
,并且检查其类型是否为Number,是原来的全局isNaN()
的更稳妥的版本。 - **静态函数 Number.isFinite(..)**:用来检测传入的参数是否是一个有穷数。和全局的
isFinite()
函数相比,这个方法不会强制将一个非数值的参数转换成数值,这就意味着,只有数值类型的值,且是有穷的(finite),才返回true
。 - 整型相关静态函数:
Number.isInteger(..)
:JavaScript 的数字值永远都是浮点数,这个方法可以用来判断给定的参数是否为整数。Number.isSafeInteger(..)
:用来判断传入的参数值是否是一个“安全整数”,检查一个值以确保其为整数并且在Number.MIN_SAFE_INTEGER
-Number.MAX_SAFE_INTEGER
范围之内(全包含)
- 静态属性:
- 字符串:
- Unicode函数:字符串原型方法
normalize(..)
用于执行Unicode规范化,或者把字符用“合并符”连接起来或者把合并的字符解开。 - **静态函数 String.raw(..)**:内置标签函数提供,与模板字符串字面值一起使用,用于获得不应用任何转义序列的原始字符串。
- **原型函数 repeat(..)**:构造并返回一个新字符串,该字符串包含被连接在一起的指定数量的字符串的副本。
- 字符串检查函数:新增了3个用于搜索/检查的新方法,
startsWith(..)
、endsWidth(..)
和includes(..)
。
- Unicode函数:字符串原型方法
元编程
- 元编程是指操作目标是程序本身的行为特性的编程。换句话说,它是对程序的编程的编程。
- 元编程关注以下一点或几点:代码查看自身、代码修改自身、代码修改默认语言特性,以此影响其他代码。
- 元编程的目标是利用语言自身的内省能力使代码的其余部分更具描述性、表达性和灵活性。
- 函数名称:默认情况下函数的词法名称(如果有的话)会被设为它的name属性。默认情况下,name属性不可写,但可配置,也就是说如果需要的话,可使用
Object.defineProperty(..)
来手动修改。 - 元属性:元属性以属性访问的形式提供特殊的其他方法无法获取的元信息。
- 公开符号:定义这些内置符号主要是为了提供专门的元属性,以便把这些元属性暴露给JavaScript程序以获取对JavaScript行为更多的控制。
Symbol.iterator
:表示任意对象上的一个专门位置(属性),语言机制自动在这个位置上寻找一个方法,这个方法构造一个迭代器来消耗这个对象的值。Symbol.toStringTag
:原型(或实例本身)的@@toStringTag
符号指定了再[object___]
字符串化时使用的字符串值。Symbol.hasInstance
:@@hasInstance
符号是在构造器函数上的一个方法,接受实例对象值,通过返回true或false来指示这个值是否可以被认为是一个实例。Symbol.species
:控制要生成新实例时,类的内置方法使用哪一个构造器。Symbol.toPrimitive
:在任意对象值上作为属性的符号@@toPrimitivesymbol
都可以通过指定一个方法来定制这个ToPrimitive强制转换。- 正则表达式符号:对于正则表达式对象,有4个公开符号可以被覆盖,它们控制着这些正则表达式在4个对应的同名
String.prototype
函数中如何被使用。@@match
:正则表达式的Symbol.match
值是一个用于利用给定的正则表达式匹配一个字符串值的部分或全部内容的方法。如果传给String.prototype.match(..)
一个正则表达式,那么用它来进行模式匹配。@@replace
:正则表达式的Symbol.replace
值是一个方法,String.prototype.replace(..)
用它来替换一个字符串内匹配给定的正则表达式模式的一个或多个字符序列。@@search
:正则表达式的Symbol.search
值是一个方法,String.prototype.search(..)
用它来在另一个字符串中搜索一个匹配给定正则表达式的子串。@@split
:正则表达式的Symbol.split
值是一个方法,String.prototype.split(..)
用它把字符串在匹配给定正则表达式的分隔符处分割为子串。
Symbol.isConcatSpreadable
:@@isConcatSpreadable
可以被定义为任意对象(比如数组或其他可迭代对象)的布尔型属性(Symbol.isConcatSpreadable
),用来指示如果把它传给一个数组的concat(..)
是否应该将其展开。Symbol.unscopables
:符号@@unscopables
可以被定义为任意对象的对象属性(Symbol.unscopables
),用来指示使用with
语句时哪些属性可以或不可以暴露为词法变量。
- Proxy代理:
- ES6中新增的最明显的元编程特性之一是Proxy(代理)特性。
- 代理是一种由你创建的特殊的对象,它“封装”另一个普通对象——或者说挡在这个普通对象的前面。你可以在代理对象上注册特殊的处理函数(也就是 trap),代理上执行各种操作的时候会调用这个程序。这些处理函数除了把操作转发给原始目标/被封装对象之外,还有机会执行额外的逻辑。
- 下面所列出的是在目标对象/函数代理上可以定义的处理函数,以及它们如何 / 何时被触发:
get(..)
:通过[[Get]],在代理上访问一个属性(Reflect.get(..)、.属性运算符或[ .. ]属性运算符)。set(..)
:通过[[Set]],在代理上设置一个属性值(Reflect.set(..)、赋值运算符 = 或目标为对象属性的解构赋值)。deleteProperty(..)
:通过 [[Delete]],从代理对象上删除一个属性(Reflect.deleteProperty(..) 或 delete)。apply(..)
(如果目标为函数):通过[[Call]],将代理作为普通函数/方法调用(Reflect.apply(..)、call(..)、 apply(..) 或 (..) 调用运算符)。construct(..)
(如果目标为构造函数):通过 [[Construct]],将代理作为构造函数调用(Reflect.construct(..) 或 new)。getOwnPropertyDescriptor(..)
:通过 [[GetOwnProperty]],从代理中提取一个属性描述符(Object.getOwnPropertyDescriptor(..) 或 Reflect.getOwnPropertyDescriptor(..))。defineProperty(..)
:通过 [[DefineOwnProperty]],在代理上设置一个属性描述符(Object.defineProperty(..) 或 Reflect.defineProperty(..))。getPrototypeOf(..)
:通过 [[GetPrototypeOf]], 得 到 代 理 的 [Prototype]、 Reflect.getPrototypeOf(..)、__proto__、Object#isPrototypeOf(..) 或 instanceof)。setPrototypeOf(..)
:通 过 [[SetPrototypeOf]], 设 置 代 理 的 [Prototype]、 Reflect.setPrototypeOf(..) 或 proto)。preventExtensions(..)
:通过 [[PreventExtensions]],使得代理变成不可扩展的(Object.prevent Extensions(..) 或 Reflect.preventExtensions(..))。isExtensible(..)
:通过 [[IsExtensible]],检测代理是否可扩展(Object.isExtensible(..) 或 Reflect. isExtensible(..))。ownKeys(..)
:通过 [[OwnPropertyKeys]],提取代理自己的属性和 / 或符号属性(Object.keys(..)、 Object.getOwnPropertyNames(..)、Object.getOwnSymbolProperties(..)、Reflect. ownKeys(..) 或 JSON.stringify(..))。enumerate(..)
:通过 [[Enumerate]],取得代理拥有的和“继承来的”可枚举属性的迭代器(Reflect. enumerate(..) 或 for..in)。has(..)
:通过 [[HasProperty]],检查代理是否拥有或者“继承了”某个属性(Reflect.has(..)、 Object#hasOwnProperty(..) 或 “prop” in obj)。
- 代理局限性:可以在对象上执行的很广泛的一组基本操作都可以通过这些元编程处理函数trap。但有一些操作是无法(至少现在)拦截的。
- 可取消代理:可取消代理用
Proxy.revocable(..)
创建,这是一个普通函数,而不像Proxy(..)
一样是构造器。除此之外,它接收同样的两个参数:target
和handlers
,Proxy.revocable(..)
的返回值不是代理本身,而是一个有两个属性(proxy和revode)的对象。一旦可取消代理被取消,任何对它的访问(触发它的任意 trap)都会抛出TypeError。 - 使用代理:
- 通常可以把代理看作是对目标对象的“包装”。在这种意义上,代理成为了代码交互的主要对象,而实际目标对象保持隐藏/被保护的状态。
- 代理在先:首先(主要、完全)与代理交互的模式,让代理与目标交流,称为代理在先设计。
- 代理在后:让目标与代理交流,代码只能与主对象交互,代理只作为最后的保障的模式,称为代理在后设计。这个回退方式的最简单实现就是把 proxy 对象放到主对象的
[[Prototype]]
链中。 - **”No Such Property/Method”**:这里的代理在后设计更简单一些。如果
[[Get]]
或[[Set]]
进入我们的pobj
回退,此时这个动作已经遍历了整个[[Prototype]]
链并且没有发现匹配的属性。这时我们可以自由抛出错误。 - 代理hack[[Prototype]]链:
[[Prototype]]
机制运作的主要通道是[[Get]]
运算。当直接对象中没有找到一个属性的时候,[[Get]]
会自动把这个运算转给[[Prototype]]
对象处理。这意味着可以使用代理的get(..)trap来模拟或扩展这个[[Prototype]]
机制的概念。
- Reflect API:
- Reflect对象是一个平凡对象(就像Math),不像其他内置原生值一样是函数/构造器。它持有对应于各种可控的元编程任务的静态函数。这些函数一对一对应着代理可以定义的处理函数方法(trap)。
- 这些函数中的一部分看起来和Object上的同名函数类似,一般来说这些工具和
Object.*
的对应工具行为方式类似。但是有一个区别是如果第一个参数(目标对象)不是对象的话,Object.* 相应工具会试图把它类型转换为一个对象。而这种情况下Reflect.*
方法只会抛出一个错误。 - 可以使用下面这些工具访问/查看一个对象键:
Reflect.ownKeys(..)
:返回所有“拥有”的(不是“继承”的)键的列表。Reflect.enumerate(..)
:返回一个产生所有(拥有的和“继承的”)可枚举的(enumerable)非符号键集合的迭代器。Reflect.has(..)
:实质上和in运算符一样,用于检查某个属性是否在某个对象上或者在它的[[Prototype]]
链上。
- 函数调用和构造器调用可以通过使用下面这些工具手动执行,与普通的语法(比如,
(..)
和new
)分开:Reflect.apply(..)
:举例来说,Reflect.apply(foo,thisObj,[42,"bar"])
以thisObj
作为this
调用foo(..)
函数,传入参数42
和"bar"
。Reflect.construct(..)
:举例来说,Reflect.construct(foo,[42,"bar"])
实质上就是调用new foo(42,"bar")
。
- 可以使用下面这些工具来手动执行对象属性访问、设置和删除:
Reflect.get(..)
举例来说,Reflect.get(o,"foo")
提取o.foo
。Reflect.set(..)
:举例来说,Reflect.set(o,"foo",42)
实质上就是执行o.foo = 42
。Reflect.deleteProperty(..)
:举例来说,Reflect.deleteProperty(o,"foo")
实质上就是执行delete o.foo
。
- Reflect的元编程能力提供了模拟各种语法特性的编程等价物,把之前隐藏的抽象操作暴露出来。比如,你可以利用这些能力扩展功能和API,以实现领域特定语言(DSL)。
- 属性排序:
- 对于ES6来说,拥有属性的列出顺序是由
[[OwnPropertyKeys]]
算法定义的,这个算法产生所有拥有的属性(字符串或符号),不管是否可枚举。对于ES6来说,Reflect.ownKeys(..)
、Object.getOwnPropertyNames(..)
和Object.getOwnPropertySymbols(..)
的顺序都是可预测且可靠的,这由规范保证:- (1) 首先,按照数字上升排序,枚举所有整数索引拥有的属性;
- (2) 然后,按照创建顺序枚举其余的拥有的字符串属性名;
- (3) 最后,按照创建顺序枚举拥有的符号属性。
[[Enumerate]]
算法只从目标对象和它的[[Prototype]]
链产生可枚举属性。它用于Reflect.enumerate(..)
和for..in
。可以观察到的顺序和具体的实现相关,不由规范控制。Reflect.enumerate(..)
、Object.keys(..)
、for..in
和JSON.stringify(..)
这4种机制都会匹配同样的与具体实现相关的排序,尽管严格上说是通过不同的路径。
- 对于ES6来说,拥有属性的列出顺序是由
- 特性测试:
- 特性测试就是一种由你运行的用来判断一个特性是否可用的测试。
- 测试程序的运行环境,然后确定程序行为方式,这是一种元编程技术。
- JavaScript中最常用的特性测试是检查一个API是否存在,如果不存在的话,定义一个polyfill。
- 如果在你的 JavaScript 应用程序的引导程序(bootstrapper)中有一组这样的特性测试,就可以通过测试环境来确定你的ES6代码是能够直接加载运行,还是需要加载代码的transpile版本,这种技术叫作分批发布。
- 尾递归调用(TCO):
- 通常,在一个函数内部调用另一个函数的时候,会分配第二个栈帧来独立管理第二个函数调用的变量/状态。这个分配不但消耗处理时间,也消耗了额外的内存。
- 有一些称为尾调用的函数调用模式,可以以避免额外栈帧分配的方式进行优化。
- 尾调用是一个return函数调用的语句,除了调用后返回其返回值之外没有任何其他动作。这个优化只在strict模式下应用。
- trampolining:相当于把每个部分结果用一个函数表示,这些函数或者返回另外一个部分结果函数,或者返回最终结果。然后就只需要循环直到得到的结果不是函数,得到的就是最终结果。
- 如果真的需要深度优化(不需考虑可复用性),那么可以丢弃闭包状态,用一个循环把acc信息的状态追踪在线化放在一个函数作用域内。这种技术一般称为递归展开。
ES6之后
- 异步函数:
async function
本质上就是生成器+promise+run(..)模式的语法糖,它们底层的运作方式是一样的。 - **Object.observe(..)**:
- 可以通过工具
Object.observe(..)
建立一个侦听者(listener)来观察对象的改变,然后在每次变化发生时调用一个回调。 - 支持观察add、update、delete、reconfigure、setPrototype、preventExtensions这六种内置改变事件,也可以侦听和发出自定义改变事件。默认情况下,你可以得到所有这些类型的变化的通知,也可以进行过滤只侦听关注的类型。
- 可以通过
Object.unobserve(..)
来停止观测一个对象的改变事件。
- 可以通过工具
- 幂运算符:用于执行幂运算的运算符——
**
。 - **Array#includes(..)**:在值数组中搜索一个值。
Array#includes(..)
使用的匹配逻辑能够找到NaN值,但是无法区分-0 和0。