书摘&心得
此处知识与红宝书重复部分不再记录,过于复杂的知识也不做记录,额外记录一些“你不知道”的知识。
1ES?
- ES6只是ESMAScript的一个版本,后续的发展无穷无尽
建议关注ESMAScript双月报告,靠谱的中文版整理。
2 语法
大多数内容都是老生常谈的,这边摘录了一些我觉得有趣的知识。
- …操作符在函数声明的参数中使用被称为收集,在其他地方则是展开
- undefined 意味着缺失。也就是说,undefined 和缺失是无法区别的,至少对于函数参数来说是如此。
- 函数参数默认值甚至可以是表达式
- 是惰性求值的——只在需要的时候运行
- 函数参数默认值会优先搜寻函数体内的变量
- 利用解构可以不借助临时变量,交换两个变量的值
- 简写 ```javascript // 老办法: var o = { x: function(){ .. }, y: function(){ .. } };
// 简写: var o = { x() { .. }, y() { .. } }; var o = { foo() { .. } }; var o = { “f” + “oo” { .. } // 计算出的简洁方法 “b” + “ar” { .. } // 计算出的简洁生成器 };
- 箭头函数总是函数表达式;并不存在箭头函数声明。
- 单例(singleton)模式:只允许自己被创建一次
<a name="Q43MM"></a>
## 标签模板字面量
- 标签部分可以为任意结果为函数的表达式
- 访问strings.raw属性可以拿到未处理的值
```javascript
function foo(strings, ...values) {
console.log( strings );
console.log( values );
}
var desc = "awesome";
foo`Everything is ${desc}!`; // 标签是foo
// [ "Everything is ", "!"]
// [ "awesome" ]
function bar() {
return function foo(strings, ...values) {
console.log( strings );
console.log( values );
}
}
var desc = "awesome";
bar()`Everything is ${desc}!`; // 标签是bar()
// [ "Everything is ", "!"]
// [ "awesome" ]
for of 与 for in
- for..in 在数组的键 / 索引上循环,而 for..of 在数组的值上循环。
- 谐音记忆:银(in)剑(键)
只能在能够产生迭代器,供循环使用的对象上使用
- 迭代器详见:第7章 迭代器与生成器
- 字符串也可以
var a = ["a","b","c","d","e"];
for (var idx in a) {
console.log( idx );
}
// 0 1 2 3 4
for (var val of a) {
console.log( val );
}
// "a" "b" "c" "d" "e"
与for of 等价的for循环:
for (var v, res; (res = it.next()) && !res.done; ) {
v = res.value;
console.log( v );
}
RegExp.prototype.text()不会向前移动匹配,只会向后匹配
新增标识
u 标识符表示正则表达式用 Unicode(UTF-16)字符来解释处理字符串
- 用于特殊字符的匹配
// 只匹配起始处并在普通文本 "-clef" 之前有一个字符的情形
/^.-clef/ .test( " -clef" ); // false
/^.-clef/u.test( " -clef" ); // true
- 用于特殊字符的匹配
y 标识为定点标识,指定正则开始匹配的位置
var re2 = /foo/y, // <-- 注意定点标识y
str = "++foo++";
re2.lastIndex; // 0
re2.test( str ); // false--0处没有找到"foo"
re2.lastIndex; // 0
re2.lastIndex = 2;
re2.test( str ); // true
re2.lastIndex; // 5--更新到前次匹配之后位置
re2.test( str ); // false
re2.lastIndex; // 0--前次匹配失败后重置
3 代码组织
3.1 迭代器
关于迭代器,红宝书和本书都描述得很多,本书更偏向底层,红宝书更偏向应用。
- 迭代器不是一个全新的主题
- ES6把迭代器标准化了
- 迭代器是一种有序的、连续的、基于拉取的用于消耗数据的组织方式
- Symbol.iterator
- 表示可以为这个对象产生迭代器的方法。
- 迭代器的next()方法只能接受一个参数
- 关于这点本书的描述存在问题
- 应该和promise.resolve(),函数返回值保持一致——只有一个值
- 是异步函数科里化的前提
- 传递2个参数时TS报错:
IteratorResult 对 象
yield*本质上是yield委托,将代码流程委托到另一个生成器上
- dva的yield take本质上就是生成器委托
生成器核心功能
在所有 JavaScript 代码中,唯一最重要的代码组织模式是模块,而且一直都是
- ES6模块特性
- 基于文件:一个文件一个模块
- API是静态的:后续无法补充
- 单例
- 模块只有一个实例,其中维护了它的状态。
- 每次向其他模块导入这个模块的时候,得到的是对单个中心实例的引用。
- 模块的公开 API 中暴露的属性和方法是到内部模块定义中的标识符的实际绑定(几乎类似于指针)
- 导入模块时使用一个字符串值表示去哪里获得这个模块(URL、文件路径等)
- 这个值对于你的程序来说是透明的,只对加载器本身有意义。
- 作用域
- 没有用 export 标示的一切都在模块作用域内部保持私有。
- 在模块内没有全局作用域。
- 但是直接import xxxx可以将xxxx的作用域引入到当前作用域,因而可以访问另一个文件的全局变量。
- 命名空间导入
- import * as xxx fomr ‘yyyy’
- 导入的模块有默认导出,它的名字是default
- 导入不变性的重要性
- 因为如果没有这样的保护,你的修改就会最终影响这个模块的所有其他用户(别忘了:单例)
- 这会导致出乎意料的副作用!
- 作为 import 结果的声明是“提升的”
系统模块加载器
- import 语句使用外部环境(浏览器、Node.js 等)提供的独立机制。
- 把模块标识符字符串解析成可用的指令,用于寻找和加载所需的模块。
- 本身不是由 ES6 指定的。它是独立的、平行的标准
命名导出与默认导出的微妙区别
1、默认导出
上面的代码,导出的是此时到函数表达式值的绑定,而不是标识符 foo。换句话说,如果之后在你的模块中给 foo 赋一个不同的值,模块导入得到的仍然是原来导出的函数,而不是新的值。function foo(..) {
// ..
}
export default foo;
2、命名导出
上面的代码,默认导出绑定实际上绑定到 foo 标识符而不是它的值,所以如果之后修改了 foo 的值,在导入一侧看到的值也会更新。function foo(..) {
// ..
}
export { foo as default };
导出时刻的值无关紧要。导入时刻的值也无关紧要。绑定是活连接,所以重要的是访问这个绑定时刻的当前值。3.4 类
javascript只能模拟类
- class Foo表明创建一个(具体的)名为Foo的函数
- constructor(..)指定Foo(..)函数的签名以及函数体内容。
class Foo {
constructor(a, b) {
this.x = a;
this.y = b;
}
gimmeXY() {
return this.x * this.y;
}
}
// 相当于
function Foo(a,b) {
this.x = a;
this.y = b;
}
Foo.prototype.gimmeXY = funciton() {
return this.x * this.y;
}
extends与super与“继承”
class Bar extends Foo {
constructor(a, b, c) {
super(a,b);
this.z = c;
}
gimmeXYZ() {
return super.gimmeXY() * this.z;
}
}
extends提供了一个语法糖
- 用来在两个函数、及其原型之间建立[[Prototype]]委托链接
- 通常被误称为“继承”
- 或者令人迷惑地标识为“原型继承”
- 在构造器中,super自动指向“父构造器”
- 在前面的例子中就是Foo(..)。
- 在方法中,super会指向“父对象”
- 这样就可以访问其属性/方法了,比如super.gimmeXY()。
super通常与类相关,但其实对普通对象的简洁方法一样有效
var o1 = {
foo() {
console.log("o1:foo");
}
};
var o2 = {
foo() {
super.foo();
console.log("o2:foo");
}
}
Object.setPrototypeOf(o2, o1);
o2.foo();
// o1:foo
// o2:foo
super恶龙
super是静态绑定
- 看起来似乎super像this一样可以被函数上下文驱动
- 不能重载
- 写它继承谁,它就继承谁,定义的时候就定死了
- 所以当动态改变类的继承关系时,super的指向会出乎意料
- 所以最好不要动态改变类的继承关系,或者放弃类,转而拥抱基于原型链的继承模式
子类构造器中调用super(..)之后才能访问this
ES6之前手动连接原型对象并不能真正支持array的特有特性
- 自如自动更新length属性
真正的Error对象创建时,会自动捕获特殊的stack信息,包括生成错误时的行号和文件名。
static方法(不只是属性)
- 直接添加到这个类的函数对象上的,而不是在这个函数对象的prototype对象上。 ```javascript class Foo { static cool() { console.log(“cool”); } } class Bar extends Foo { static awesome() { super.cool(); console.log(“awesome”); } } var b = new Bar(); Bar.awesome(); // cool awesome Bar.cool(); // cool b.awesome // undefined b.cool // undefined
<a name="up4mM"></a>
# 4 异步流程控制
- 生成器可以yield一个promise出来恢复生成器
- 使用运行器runner【下卷184】
- 这种模式过于常用:async await诞生了:
```javascript
// 使用运行器runner
run( function *main() {
var ret = yield step1();
try {
ret = yield step2( ret );
}
catch (err) {
ret = yield step2Failed( err );
}
ret = yield Promise.all([
step3a( ret ),
step3b( ret ),
step3c( ret )
]);
yield step4( ret );
} ).then(
function fulfilled(){
// *main()成功完成
},
function rejected(reason){
// 哎呀,出错了
}
);
// aysnc await
async function main() {
var ret = await step1();
try {
ret = await step2( ret );
}
catch (err) {
ret = await step2Failed( err );
}
ret = await Promise.all( [
step3a( ret ),
step3b( ret ),
step3c( ret )
] );
await step4( ret );
}
main().then(
function fulfilled(){
// main()成功完成
},
function rejected(reason){
// 哎呀,出错了
}
);
async function弊端
没有办法从外部取消一个正在运行的async funciton实例
更多异步流程控制方案移步:
js异步流程管理
第11章 期约与异步函数
5 集合
- 要从 map 中得到一列值/键,可以使用 values(..)/keys(..),它会返回一个迭代器。
- Set和Map相比Object的优势在于可以使用对象做键
- Set 是一个值的集合,其中的值唯一(重复会被忽略)。
更多详见第五、六章 引用类型
6 新增API
Array.of(..)
取代了 Array(..) 成为数组的推荐函数形式构造器
【陷阱】Array构造函数传入单个数字,会构造一个长度为传入数字的空数组
var a = Array( 3 );
a.length; // 3
a[0]; // undefined
var b = Array.of( 3 );
b.length; // 1
b[0]; // 3
Array.from()
转换(类)数组
var arrLike = {
length: 4,
2: "foo"
};
Array.from( arrLike );
// [ undefined, undefined, "foo", undefined ]
槽位值为undefined不代表是空槽位
- 空槽位可能导致意想不到的Bug
- 使用Array.from()永远不会产生空槽位
第二个参数是映射
var arrLike = {
length: 4,
2: "foo"
};
Array.from( arrLike, function mapper(val,idx){
if (typeof val == "string") {
return val.toUpperCase();
}
else {
return idx;
}
});
// [ 0, 1, "FOO", 3 ]
Array.prototype.fill()
填充数组,也可以取代已有值
var a = [ null, null, null, null ].fill( 42, 1, 3 );
a; // [null,42,42,null]
var b = [1, 2, 3].fill(3);
b; // [3, 3, 3]
Number.isNaN()
修正了isNaN()的行为
var a = NaN, b = "NaN", c = 42;
isNaN( a ); // true
isNaN( b ); // true --oops!
isNaN( c ); // false
Number.isNaN( a ); // true
Number.isNaN( b ); // false--修正了!
Number.isNaN( c ); // false
Number.isInteger(..)
JavaScript 的数字值永远都是浮点数
Number.isInteger( 4 ); // true
Number.isInteger( 4.0 ); // true
Number.isInteger( 4.2 ); // false
String.proptotype.repeat()
"foo".repeat( 3 ); // "foofoofoo"
7 元编程
太复杂了想哭 T.T
- 什么是元编程:操作目标是程序本身的行为特性的编程
- 代码查看自身
- 用for..in 循环枚举对象的键
- 检查一个对象是否是某个“类构造器”的实例
- 代码修改自身
- 代码修改默认语言特性,以此影响其他代码
- proxy
- 代码查看自身
- 出人意料的是内置Symbol符号是这一章的主力军
- 元编程描述了javascirpt很多内置方法的底层原理
- 特性测试也属于一种元编程
- 就是检测这个方法能不能用,那个方法支持不支持的代码
尾递归调用TCO也属于元编程
- 详见第10章 函数
代理
- 详见第10章 函数
可以把代理看作是对目标对象的“包装”。
- 代理在先
- 代理成为了代码交互的主要对象,而实际目标对象保持隐藏 / 被保护的状态。
- 可能这么做是因为你想要把对象传入到某个无法被完全“信任”的环境
- 传递对象的代理可能比传递对象本身更安全。
- 代理在后
- 把 proxy 对象放到主对象的[[Prototype]] 链中
- 在对象本身上找不到的时候,访问对象的原型链,查询到对象的代理
- 代理只作为最后的保障
更多详见:第9章 代理与反射
属性排序
总之属性的顺序很难维持为你想要的顺序
- 在 ES6 之前,一个对象键 / 属性的列出顺序是依赖于具体实现
- 多数引擎按照创建的顺序进行枚举
ES6 之后,拥有属性的列出顺序是由 [[OwnPropertyKeys]] 算法定义
匿名函数的函数名推导
- 提供了构造器调用方式这样的信息的元属性
- 通过公开符号可以覆盖原本特性,比如对象到原生类型的类型转换。
- 代理可以拦截并自定义对象的各种底层操作,Reflect 提供了工具来模拟它们。
- 特性测试
- 可尾递归优化
- 把焦点从你的程序转移到 JavaScript 引擎功能本身。
- 通过更多地了解环境能力,你的程序可以在运行时调整自己达到最优效果。
8 ES6之后
- 发展节奏从每隔几年更新一次进化到每年一个正式版本更新(因此基于年度命名)
- 所以要保持学习,关注会议提案https://github.com/tc39/ecma262#current-proposals
- 本章记录了未来javascript的发展