同样以文法、语义、执行过程来学习。从新的语法、对象类型入手,再到实际执行过程中的机制、表现。

函数

默认参数
不定长参数
增强Function构造函数
name属性
函数的用途
块级函数
箭头函数
尾调用优化

扩展对象

扩展的简写语法
属性简写、方法简写、可计算属性简写。
同名属性的覆盖
混入模式。对象组合方式。
两个静态方法
Object.is 修正了 === 运算符,+0与-0相等、NaN与自身不等的两个错误。
自由属性枚举顺序
getOwnPropertiesName、Reflect.keys。for-in、Object.keys,没有实现。
原型的访问和设置
setPrototype、super。

symbol

Symbol 是 es6 新增的数据类型,符号实例是唯一的、不可变的。Symbol 出现之前,对象属性只能是字符串,现在定义属性可以用 symbol 类型。Symbo 的用途就是确保对象属性的唯一性,不会发生覆盖和冲突。symbol 不可能用来实现私有属性的,因为有 api 可以获取对象的 symbol 属性。另一个用途是公开符号。

创建
直接调用 Symbol 函数,let sym = Symbol('description')。传入的字符串作为这个符号的描述,这段描述保存在内部的 [[Description]] 属性中,当调用 toString() 方法的时候会读取这个属性。

使用
Symbol 作为对象属性时要和计算属性一起用。可以通过计算属性用于字面量对象、Object.defineProperty()、Object.defineProperties() 中。
有需求,在不同的库、代码中想要访问同一个 Symbol,es6 提供了全局 Symbol 注册表。创建和获取全局共享符号都用 Symbol.for() 静态方法。Symbol.keyFor() 方法可以反向查找符号对应的描述。
Symbol 没有与其他类型逻辑等价的值,所以在一些隐式转换类型的时候需要注意。
获取对象属性的方法 Object.keys()、Object.getOwnPropertyNames() 可以获取对象所有属性,但是却没法获取对象的符号属性,可以通过 Object.getOwnPropertySymbols() 获取到。

公开符号
从 es5 开始,就有一个中心主旨,将 js 中一些内置的部分暴露出来,es6 也遵循这个宗旨,通过公开符号重构了整个对象系统,在原型链上定义与 Symbol 相关的属性暴露更多的内部逻辑。
暴露了很多内部操作,Symbol.hasInstance/isConcatSpreadable/iterator/match/replace/search/species/split/toPrimitive/toStringTag/unscopables。

Symbol.hasInstance 符号表示一个方法,用来确定对象是不是函数的实例。定义在 Function.prototype 中,所有的函数都默认继承这个符号,这个属性是不可写、不可配置、不可枚举的。instanceof 运算符不过是这个符号的简写语法。
然而 Object.defineProperty() 方法可以改写不可写属性。这个符号方法接受一个对象作为参数,如果不是对象,总是返回 false。

Symbol.species,一个方法,用来创建派生类的构造函数。

Symbol.toStringTag 符号表示一个属性。前端中有个有趣的地方,不同 iframe 的全局作用域是独立的,将某个数组从一个领域传递到另一个领域,相同构造函数的地址是不一样的,因此用 instanceof 来检测的话就会出错。这时候可以用 Object 原型上的 toString() 方法来识别对象的数据类型。对象都有一个 [[class]] 内置属性,只能通过 toString 方法访问。在当时还没有这个公开符号,所以这个内置属性无法被修改,都用这个内置属性来区别自定义对象和原生对象。
这个符号可以改变对象的身份标识,定义在 Object.prototype 上,默认值是 “Object”。es6 没有限制对它的使用,所以通过 Object 原型上的 toString 方法来识别对象也不一定靠谱了。最佳实践的话,最好不要修改 js 内建对象的这个属性值。

Symbol.unscopables 属性是为了解决 with 的局部绑定问题。该属性定义为一个对象,对象中的键为属性名,值为 true,以此来表示 with 语句中,增强的上下文对象应该忽略绑定哪些属性。Array.prototype 就默认内置了该符号属性,这个符号表示的对象里面的属性就不会作为 with 中作用域对象的属性了,就可以访问到外部的同名变量。

  1. Array.protoype[Symbol.unscopables] = Object.assign(Object.create(null), {
  2. copyWithin: true,
  3. entries: true,
  4. fill: true,
  5. find: true,
  6. findIndex: true,
  7. keys: true,
  8. values: true,
  9. });
  10. let colors = ['red', 'green', 'blue'], values = [1,2,3], color = 'black';
  11. with(colors) {
  12. push(color);
  13. push(...values); // values 就是外部的变量,而不是 colors 的 values 方法。
  14. }

set 与 map

Set 集合通常用来查询某个值是否在集合中,Map 哈希表通常用来缓存频繁存取的数据。Set 和 Map 内部用 Object.is() 来检查键是否重复。

在 es6 之前,用 Object.crate(null) 创建一个没有原型的对象来模拟集合和哈希表的功能。通过条件语句来检查某个属性是否存在,或者通过属性来存取值。然而这样的做法有两个问题,对象属性的类型限制,条件语句的类型转换。
对象的属性只能是字符串类型、符号类型。相同类型的对象、数字字符串与数字作为键时,会被自动转换为字符串,而且字符串值相同,那么属性值会被改写,没法区分独立开。
如果属性值是数字0,作为 map 的话,是保存了实际的值的,但是经过条件语句判断时,会判定为假值(键值不存在)。

除了 if 条件语句,还可以用 in 运算符,但是 in 运算符在检测没有原型的对象的属性时比较可靠,如果对象有原型的话,in 运算符也会去检测对象的原型。

Set 构造函数接受可迭代对象(数组、Set、Map),Map 构造函数与之相似,不过数组的元素得是一个长度为2的数组。
Set 用 add() 添加元素,Map 用 set(),其他删查的方法和属性相同,delete()、clear()、has()、size 属性。都支持 forEach() 方法。Map 有 get() 方法,Set 没有。

当调用 forEach() 遍历的时候,是按照添加时的顺序来获取元素的。迭代 set、map 还有另一种方式,迭代器,set/map 支持 keys()、values()、entries() 方法。

Set 转换为数组,用扩展运算符。一个简单的应用,过滤数组中的重复项。

如何选择 Object 还是 Map?
从内存占用、插入性能、查找速度、删除性能4个方面考虑。map 可以比 Object 多存储一半的键值对数据。map 的插入速度比 Object 略快,在插入数据量大的时候显然 map 更好。少量键值对的话,Object 的速度更快,大量查找操作 Object 的速度也会更快。map 的删除操作更快。用 delete 删除 Object 属性的性能一直不好,伪删除操作一般都是设置值为 undefined/null。

WeakSet 和 WeakMap 保存的键(WeakSet 的键值一样哈)必须是对象,而且只有键是弱引用,也就是说该对象的最后一个引用如果是弱引用,那么垃圾回收程序启动时,会被自动回收。如果允许原始值作为弱映射的键,那么没法区分两个值相同字符串。
弱Set和Map并没有暴露太多方法和属性出来,只有 add()、set()/get()、has()、delete() 方法。当对象的引用置空之后,就没有可用的键来获取弱集合和弱哈希表中存储的值了。
弱Set和Map 不可迭代,也就没有暴露出迭代器。因为弱映射中的键的引用随时可能被垃圾回收,所以没必要提供迭代键值对的能力。同时也是为了降低对用户的可见度,尽量少的暴露弱键的访问。

当某个对象值的其他引用都不存在的时候,自己代码中对它的引用也没用了。就比如 DOM 元素,已经被移除了,代码也不需要做到这个元素了,自然希望回收掉这块内存。当这样的引用多的时候,如果仅靠自己管理,很难做到及时的释放。这时候就可以用弱集合。

当某个 DOM 元素作为键的时候,根据 dom 元素来保存其他值。如果某个第三方库保存过 dom 元素,而 dom 元素已经被移除没用了,第三方库很难及时释放 dom 的引用,第三方库不方便得知 dom 元素没用的消息,这样的话需要新的机制来实现。这时候就可以用弱哈希表。

弱哈希表还有个作用,用来实现实例对象的私有属性。在 es6 之前,是通过 IIFE 实现的。但是这种方法的一个问题是,IIFE 内部无法得知对象实例何时被销毁,那么这个对象的私有属性就会保留下来。使用弱哈希表还能简化写法。

  1. var Person = (function() {
  2. var privateData = {}, privateId = 0;
  3. function Person(name) {
  4. Object.defineProperty(this, '_id', {value: privateId++});
  5. privateData[this._id] = {name: name}
  6. }
  7. Person.prototype.getName = function() {return privateData[this._id].name;}
  8. return Person;
  9. })();
  10. var PersonNew = (function() {
  11. var privateData = new WeakMap();
  12. // 实例对象自身为键,不用另外设置 _id 属性和 privateId 变量,搞这样一套单独的 id 体系。
  13. function Person(name) {privateData.set(this, {name: name});}
  14. Person.prototype.getName = function() {return privateData.get(this).name;}
  15. return Person;
  16. })();
  17. // 就算是 class 写法,也是这样写。

迭代器与生成器

map、set、array、定型数组,都有默认迭代器。拥有默认迭代器证明这4个类型都支持顺序迭代,都可以用 for-of 循环,并且支持扩展运算符。除了让这些属性支持顺序迭代和扩展操作,还有利于4种类型间相互转换。

生成器是一个函数,返回迭代器,但是类型并不是迭代器啊。map 的 keys、values 的返回值倒是 Iterator 类型的。?

命名类表达式和命名函数表达式表现不同,但是可以在多个场景中作为值使用,啥意思?咋用?

数组进化

Symbol.species 符号方法作用是啥来着?

书中所说的数值型索引,难道不是字符串类型的吗?对象中哪有数值型类型的属性,只有字符串类型和Symbol类型。

indexOf()、lastIndexOf() 的局限是每次只能查找一个值。所以才提出了 find()、findIndex()。

fill 的开始索引参数大于结束索引参数且都为正数的话,也不会从后到前循环数组去填充元素。

小端序是否就是操作系统中说的高字节在高位,低字节在低位。

使用创建定型数组的第二种方法创建定型数组。书中“在这种情况下,如果要访问新创建的缓冲区,则可以通过buffer属性来实现”,什么意思啊。

Int8Array.prototype 的原型是一个匿名对象,是从 Object 扩展来的,这个匿名对象的原型是 Object.prototype。

Promise与异步编程

js 是单线程的,js 引擎在同一时刻只能执行一个代码块,所以需要保存即将执行的代码块。即将执行的代码块放在一个任务队列里,事件循环就负责监控代码块的执行并管理任务队列。

事件模型与回调模式的区别在于回调模式中的函数是作为参数传入的。事件模型是 js 中最基础的异步编程形式,通过注册事件处理程序,在事件触发时,将处理程序添加到任务队列中,等待事件引擎调度放到执行栈里执行。回调模式也是类似的过程。关于这个二者的区别,其实还不是很理解。

node 也用了事件和回调模式,而且回调的风格是错误优先。回调模式很不错,可以很多好的连接异步任务,但是异步任务的调用链过长,就会陷入回调地狱。

为了解决回调地狱和其他控制权限为题,promise 出现了。promise 的出现,避免了回调函数复杂的嵌套形式,链式调用的方式实现更加复杂的异步任务调用。

事件处理程序中的 this 何时丢失?

promise 基础
promise 与回调模式不同,并不是将回调函数传递给目标,执行权限还是在自己的代码段。promise 相当于异步操作结果的占位符,目标代码只负责改变状态。

promise 对象内部 [[PomsieState]] 属性保存了 promise 对象的3种状态,pending/fulfilled/rejected。每个 promise 对象都有 then/catch 方法。通过 then/catch 注册的微任务,只要 promise 状态改变,就一定会被执行。

Promise 构造构造只接收一个函数参数,这个函数参数叫做执行器。Promise 构造函数内部又传递两个函数(resolve、reject)给这个执行器函数。目标函数通过调用这两个函数来改变 promise 对象的状态。

微任务的创建和编排入队可能不是紧挨着发生的。引擎解析 then/catch 方法时会创建微任务,但是不会立即插入任务队列。当 promise 实例的状态发生变化时才会编排进任务队列中。

如果只是想给 resolve 传递一个简单的数值,就没有必要使用 Promise 构造函数,可以使用 Promise.resolve()/Promise.reject() 方法。

Promise.resolve/reject 没有任务编排的过程。直接返回一个完成态的 promise 实例。红宝书有一个使用小技巧,有点想不通

promise 对象都是 thenable 对象,不是所有 thenable 对象都是 promise。实现了 then 方法的对象都i叫 thenable 对象。通过这两个方法可以转换 thenable 对象为 promise 对象。thenable 对象的 then 方法中参数接收 resolve()/reject(),可以调用来改变状态,Promise.resolve 就是调用的 thenable.then 方法,所以 promise 的状态变化可以被检测到。

执行器内部有一个隐含的逻辑,相当于加了一层 try-catch。执行器内部抛出的错误都会被 reject 传递给错误处理程序。这样也存在一个问题,只有添加了错误处理程序才可以接收到 reject 传递的错误对象,的否则会被忽略。浏览器和 node 提供捕获已拒绝 promise 的钩子函数。node 环境中有两个事件“unhandledRejection”、“rejectionHandled”,浏览器的事件名也一样(注意DOM1、DOM2事件处理方式的不同)。

unhandledRejection 的回调接收两个参数,错误原因和promise实例。rejectionHandled 的回调接收一个参数,promise 实例。

串联 promise
书中为什么说promise看起来像是回调函数和 setTimeout 的结合,稍微做了些改进。

处理程序中抛出的错误由 promise 链来捕获。promise 链无法捕获内部异步操作抛出的错误,只能通过在抛出错误的地方套一层 try-catch,这层 try-catch 不能套在异步操作外面,这样检测不到。catch 捕获到之后再用 reject 传递给 promise 链。

  1. let promise = new Promise((resolve, reject) => {
  2. setTimeout(() => {
  3. try {
  4. throw new Error('test');
  5. } catch (e) {
  6. reject(e);
  7. }
  8. });
  9. });
  10. promise.then(null, (e) => { console.log('rejected', e)});
  1. <br />promise 链中处理程序的返回的值很有讲究,如果是原始值没什么好说的。如果是 thenable 对象,那么会由这个返回对象来决定下一个 promise 实例。promise 处理程序中不论返回什么值,都会包装成 promise 实例,要不然也没法链式起来。

链式的调用方式一定要注意错误处理程序能接收错误的范围。

处理多个promise
Promise.race 对多个 promise 进行竞选,状态先变得那个promise得值被封装到新得 promise 实例被返回。
Promise.all 对多个 promise 进行监控,所有成功返回所有,一个失败返回失败。

promise 异步任务执行

生成器实现异步任务执行器有2个问题,① 返回值是函数的函数中包裹每一个函数会令人感到困惑,② 无法区分用作任务执行器回调函数的返回值和一个不是回调函数的返回值。
@todo 这什么翻译,我就不懂这说的啥。。

使用 Promise 可以简化这一过程。要想用生成器写出这样的例子并且用 promise 进行改造,还挺难。改造之后的 run 函数可以运行所有使用 yield 实现异步代码的生成器。

  1. let fs = require('fs');
  2. function run(taskDef) {
  3. // 创建迭代器
  4. let task = taskDef();
  5. let result = task.next();
  6. // function step() {
  7. // if (!result.done) {
  8. // if (typeof result.value === 'function') {
  9. // result.value(function(err, data) {
  10. // if (err) {
  11. // result = task.throw(err);
  12. // return;
  13. // }
  14. // result = task.next(data);
  15. // step();
  16. // });
  17. // } else {
  18. // result = task.next(result.value);
  19. // step();
  20. // }
  21. // }
  22. // }
  23. // step();
  24. (function step() {
  25. if (!result.done) {
  26. let promise = Promise.resolve(result.value);
  27. promise.then(function(value) {
  28. result = task.next(value);
  29. step();
  30. }).catch(function(err) {
  31. result = task.throw(err);
  32. step();
  33. })
  34. }
  35. })();
  36. }
  37. // function readFile(filename) {
  38. // return function (callback) {
  39. // fs.readFile(filename, callback);
  40. // }
  41. // }
  42. function readFile(filename) {
  43. return new Promise((resolve, reject) => {
  44. fs.readFile(filename, function(err, data) {
  45. if (err) {
  46. reject(err);
  47. } else {
  48. resolve(data);
  49. }
  50. })
  51. })
  52. }
  53. run(function *() {
  54. let contents = yield readFile("test.mjs");
  55. console.log(contents);
  56. });

用 yield 调用同步或异步代码都可以正常运行,因为函数调用的返回值总是会被转换成一个 Promise,永远不需要检查返回值是否为 Promise。但是像 node 的一些异步函数,就需要包括一层 Promise。
@todo 这句话也不是很清楚。

写这本书的时候,async/await 还没有成为标准。async/await 的基本理念是用替代生成器函数和 yield。让用户可以用同步的方式书写异步代码,唯一的开销是基于迭代器的状态机

Proxy & Reflection

js 中有些功能早就实现了,但是开发者用不了。es5、es6 的目的之一就是让 js 更多的内部功能开放给开发者。es5 之前,就有不可枚举和不可写属性,es5 引入了 Object.defineProperty() 让开发者也能改变这些属性。es6 添加很多内建对象,也是想让开发者拥有更多访问 js 引擎的能力。

代理可以拦截并改变底层 js 引擎的行为。在新语言中通过代理暴露内部运行的对象,让开发者可以创建内建的对象(@todo 这句话我就不懂,什么暴露内部运行的对象,内部运行的能有啥对象)。

主要讲代理要解决的问题,如何有效创建并使用代理。

要解决的问题
拿数组举例,数组在 es6 被定义为奇异对象(相对是普通对象)。数组有个特性,索引和 length 属性会相互影响。es5 之前都没法模拟这种行为,通过继承也不行。

代理就可以,根据目标对象创建代理对象,可拦截 js 引擎内部对目标的操作。这些操作被拦截下来之后会触发钩子。这些钩子通过代理也可以被修改。钩子很多,譬如,get/set/ownKeys/has/defineProperty/apply/construct等等。

Reflect 是一个内建对象,除了继承自 Object 的原型方法,就只有与对象的底层操作对应的钩子函数相同的方法,命名参数都相同。代理可以覆写这些钩子函数,并且通过 Reflecct 内建对象再调用原有的钩子。

Set 陷阱
isNaN 与 Number.isNaN 的不同之处。isNaN 和 Number.isNaN 都是用来判断值是不是 NaN,是的话返回 true,不是的话返回 false。然而 isNaN 会对传入的值进行类型转换。

get 陷阱
js 中访问不存在的对象属性不会报错,而用 undefined 来代替不存在的属性值。在应用中,这会导致错误,用代理可以拦截做检查。

用 in 运算符检查 receiver 对象是有讲究的。防止 receiver 代理含有 has 陷阱,检查 trapTarget 可能会忽略 has 陷阱,得到错误的结果。

has 陷阱
通过 has 钩子函数,可以改变 in 运算符检测属性时的操作。所以才要上例 get 钩子才要检测 receiver。

deleteProperty 钩子
delete 一个不可配置的对象属性,在 strict 模式下会报错,非 strict 不会。使用 deleteProperty 钩子,可以让不可配置对象属性被 delete 的时候不报错。delete 删除属性后会返回一个 boolean 值。

死活实现不了。总是报错trap returned failish

原型代理钩子
getPrototypeOf/setPrototypeOf 钩子接受 trapTarget/proto 两个参数。使用原型代理钩子有2点规则,① getPrototypeOf 必须返回对象或 null,② setPrototypeOf 操作错误必须返回 false,然后抛出错误,否则都认为操作成功。

Reflect.getPrototypeOf 与 Object.getPrototypeOf 如此相似,却又很不同。Object. 是提供给开发者更上层的功能,Reflect. 是提供更底层的功能。这两个方法都是内部操作[[GetPrototypeOf]]打交道,Reflect. 包裹了相应的内部操作,仅提供输入验证,而 Object. 提供了更多的功能(类型转换、返回值为对象)。