《ES6 标准入门》笔记心得
第1章 ECMAScript 6 简介
ES6被认为是 下一代 javasciprt 的标准。
第2章 let 和 const 命令
let:
- 不存在变量提升
- 暂时性死区
- 不允许重复声明
const:
本质:变量指向的内存地址不得改动。
块级作用域:
块级作用域内声明函数的行为类似于var 声明。
function f() { console.log('I am outside!') }
(function() {
if(false) {
function f() { console.log('I am inside!'); }
}
f();
}());
顶层对象的属性:
ES5中,顶层对象的属性与全局变量是等价的。
ES6中,var 命令和 function 命令声明的全局变量依旧是顶层对象的属性;而 let 命令、const 命令、class 命令声明的全局变量不属于顶层对象的属性。
第3章 变量的解构赋值
解构赋值本质:“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
只要某种数据结构具有 Iterator接口,都可以采用数组形式的解构赋值。
第4章 字符串的扩展
includes() startsWith() endsWith()
padStart() padEnd()
模板字符串 ``
第5章 正则的扩展
新增 u 修饰符,含义为 Unicode 模式;用于正确处理 UTF-16 编码
新增 y 修饰符,叫作“粘粘”修饰符
具名组匹配
正则表达式的分组
第6章 数值的扩展
Number.isInteger()
javascipt 能准确表示的整数范围在-2^53 到 2^53 之间(不含两个端点),超过这个范围就无法精确表示
Math 对象的扩展
Math.trunc():除去小数部分
Math.sign():判断正负零
Math.cbrt():计算立方根
Math.log10():log10(x)
Math.log2():log2(x)
指数运算符: 2 ** 3 === 8
第7章 函数的扩展
函数参数默认值
只有当传入的参数 强等于 undefined,才会触发默认值。
与解构赋值默认值结合使用
// 写法一
function m1({x = 0,y = 0} = {}) {
return [x,y];
}
// 写法二
function m2({x, y} = {x: 0, y: 0}) {
return [x,y];
}
通常情况下,默认值的参数应该是函数的尾参数。如果非尾参数的参数设置默认值,实际上这个参数无法省略。
function f(x = 1,y) {
return [x,y];
}
f(); // [1,undefined]
f(2); // [2,undefined]
f(,1); // 报错
f(undefined,1);// [undefined,1]
默认参数作用域
参数的默认值不是在定义时执行,而是在运行时执行。
rest参数
*箭头函数
注意事项:
- this对象为定义时的对象,而不是使用时所在的对象
- 不可用作构造函数,即不可new(因为它根本没有this)
- 不可使用 arguments 对象。一定要用的话,可以用 rest 参数代替
- 不可用yield 命令,不可用作 Generator 函数
- 由于没有this,也就不能用call()、apply()、bind()
尾调用
某个函数的最后一步是调用另一个函数
尾递归
尾调用自身。可是由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
// 阶乘函数
function factorial(n) {
if(n === 1) return 1;
return n * factorial(n - 1);
}
// 改造为 尾递归
function factorial(n,total) {
if(n === 1) return total;
return factorial(n - 1,n * total);
}
函数柯里化
将多参数的函数转换成单参数的形式。
第8章 数组的扩展
扩展运算符 (spread) …
找出数组中的最大元素
Math.max(...[14,3,77]);
// 等同于
Math.max(14,3,77);
扩展运算符的应用
- 合并数组
// es5
arr1.concat(arr2,arr3);
// es6
[...arr1,...arr2,...arr3];
- 与解构赋值结合,生成数组
如果扩展运算符用于数组赋值,则只能将其放在参数的最后一位,否则报错。 - 函数的返回值
- 将字符串转换成数组
// es5
const arr = str.split('');
// es6
[...'hello'];
// ['h','e','l','l','o'];
- 实现了 Iterator 接口的对象都可以用扩展运算符转为真正的数组
- Map和Set结构,Generator函数 也可以使用扩展运算符
Array.from()
Array.from()
方法用于将两类对象转换为真正的数组:
- 类数组对象(含 length 属性的对象)
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// es5 的写法
var arr1 = [].slice.call(arrayLike);// ['a','b','c'];
// es6 的写法
let arr2 = Array.from(arrayLike);
- 可遍历对象(实现了接口 iterable)
参数列表:还接收第二个参数,作用类似于数组的map方法;第三个参数,绑定this
Array.from(arrayLike,function(item) {
// map
});
Array.of()
将一组值转换为数组;为了弥补数组构造函数Array()的不足
模拟实现:
function ArrayOf() {
return [].slice.call(arguments);
}
数组实例的 entries()、keys() 和 values()
都返回一个遍历器对象,可用 for…of遍历;
还可以手动调用遍历器对象的 next 方法进行遍历。
数组实例的 includes()
在某种情况下,可以用于替换 indexOf ;indexOf 其内部用严格相等运算符,会导致 NaN 的误判
而 includes 使用的是不一样的判断算法,就没有这个问题
数组的空位
第9章 对象的扩展
属性的简洁表示法
属性名表达式
属性名表示法 和 简洁表示法
Object.is()
行为与 === 基本一样;不同之处只有两个:
- +0 不等于 -0
- NaN 等于自身
Object.assign()
执行的浅复制,而不是深复制。遇到同名属性,后面的属性替换前面的属性。
Object.assign()的常见用途
- 为对象添加属性
class Point {
constructor(x, y) {
Object.assign(this,{x,y});
}
}
相当于将x属性和y属性添加到Point类的对象实例中。
- 为对象添加方法
Object.assign(SomeClass,prototype, {
someMethod(arg1, arg2) {
},
anotherMethod() {
}
});
- 克隆对象
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign({},assign);
}
- 合并多个对象
- 为属性指定默认值
const options = Object.assign({},DEFAULTS,options);
由于Object.assign() 为浅复制,故会替换引用类型属性值,而不是拼接
属性的可枚举性
对象的每一个属性都有一个描述对象,用于控制该属性的行为
Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。如:
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj,'foo');
// 输出结果
{
value: 123,
writable: true,
enumerable: true,
configurable: true
}
其中 enumerable 属性称为“可枚举性”,如果为false,则有的操作会忽略当前属性
- for…in 循环:只遍历自身和继承的可枚举属性
- Object.keys():返回对象自身的所有可枚举属性的键名
- JSON.stringify():只串行化对象自身的可枚举属性
- Object.assign():只复制对象自身的可枚举属性
从上述描述中可以看出:在遍历对象自身的属性时,尽量不用使用 for…in 循环,而用 Object.keys() 代替。
属性的遍历
- for…in:自身,继承,可枚举,不含Symbol
- Object.keys(obj):自身,可枚举,不含Symbol
- Object.getOwnPropertyNames(obj):自身,可枚举,不可枚举,不含Symbol
- Object.getOwnPropertySymbols(obj):自身的所有Symbol
- Reflect.ownKeys(obj):自身全部属性,除继承
_proto_
属性
此处叫作隐式原型,本质上是一个内部属性,而不是一个正式的对外的API。
只有浏览器必须部署这个属性,其他运行环境不一定要部署。
因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性。取而代之用Object.setPrototypeOf()
写操作、Object.getPrototypeOf()
读操作或 Object.create()
生成操作。
Object.setPrototypeOf()
Object.setPrototypeOf(object, prototype);
// 相当于
function (obj, prototype) {
obj.__proto__ = prototype;
return obj;
}
Object.keys()/values()/entries()
Object.keys() 返回 不含继承的,自身的,所有可遍历的 属性的键名
Object.values() 返回 对象自身的,过滤属性名为Symbol 的可遍历属性
Object.entries() 返回 对象自身的,不含继承的所有可遍历的属性的键值对数组
此方法的另一个用处是将对象转为真正的Map 结构
对象的扩展运算符
解构赋值的复制是浅复制
解构赋值必须是最后一个参数,否则会报错。
扩展运算符的用法:
- 用于合并两个对象:b 会覆盖 a 的同名属性
let ab = { ...a, ...b };
// 等同于
let ab = Object.assgin({},a,b);
- 如果用户自定义的属性放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖。
let aWithOverrides = { ...a, x: 1, y: 2};
// 等同于
let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });
- 如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值
let aWithDefaults = { x: 1,y: 2, ...a};
// 等同于
let aWithDefaults = Object.assign({},{ x: 1, y: 2}, a);
Object.getOwnPropertyDescriptors()
ES2017引入该方法,返回指定对象所有属性(非继承)的描述对象
提案:Null 传导运算符
const firstName = message?.body?.user?.firstName || 'default';
第10章 Symbol
第11章 Set和Map 数据结构
Set
成员值唯一,没有重复。
Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
去除数组重复成员的方法:
[...new Set(array)]
注意:向Set加入值时不会发生类型转换。类似于精确相等运算符(===),主要区别是NaN 等于自身。
Set 实例的属性和方法
属性:
- Set.prototype.constructor:构造函数,默认是Set函数
- Set.prototype.size:返回Set 实例的成员总数
操作方法:
- add(value):添加某个值,返回Set结构自身
- delete(value):删除某个值,返回布尔值
- has(value):返回布尔值,表示参数是否为Set的成员
- clear(): void:清除所有成员
遍历方法:
- keys()
- values()
- entries():返回键值对的遍历器,注意,Set的键等于值
- forEach()
Set与离散数学
使用Set 可以很容易地实现并集(Union)、交集(Interset)和差集(Differrence)。
// 集合
let a = new Set([1,2,3]);
let b = new Set([4,3,2]);
// 并集
let union = new Set([...a,...b]);
// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
WeakSet
- WeakSet的成员只能是对象,不能是其他类型
- WeakSet的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用
规定WeakSet 不可遍历
Map
Map 的键的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
Map 的键实际上是和内存地址绑定的,只要内存地址不同,就视为两个键。
Map 实例的属性和方法
属性:
- size
操作方法:
- set(key, value)
- get(key)
- has(key)
- delete(key)
- clear()
遍历方法:
- keys()
- values()
- entries()
- forEach():可以接受第二个参数,用于绑定 this
WeakMap
- WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名。
- WeakMap的键名所指向的对象不计入垃圾回收机制
注意:WeakMap 弱引用的只是键名而不是键值。键值依然是正常引用的。
第12章 Proxy
第13章 Reflect
第14章 Promise
第15章 Iterator 和 for…of 循环
第16章 Generator 函数的语法
第17章 Generator 函数的异步应用
第18章 async 函数
第19章 class 的基本语法
ES6中的 class 可以看做只是一个语法糖
类的数据类型就是函数,类本身指向构造函数
**注意:**类的内部定义的所有方法都是不可枚举的
类必须用 new 来调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用 new 也可以执行。
class 表达式
使用 class 表达式,可以写出立即执行的Class
不存在变量提升
类不存在变量提升,这点与ES5完全不同
私有方法
私有方法虽是常见需求,但是ES6 不提供。
私有属性
提案:在属性面前加 # 号
this的指向
name属性
类的 name 属性总是返回紧跟在 class 关键字后面的类名。
Class 的取值函数(getter)和存值函数(setter)
在类的内部可以使用 get 和 set 关键字对某个属性设置getter 和 setter ,拦截该属性的存取行为。
存值函数和取值函数是设置在属性的 Descriptor 对象上的。
Class 的 Generator 方法
Class 的静态方法
在方法前添加 static 关键字
父类的静态方法可以被子类继承
静态方法也可以从super 对象上调用
提案:Class的静态属性
提案:Class 内部只有静态方法,没有静态属性。
提案:Class的实例属性
提案:实例属性用等式写入类的定义之中。
目前如果要给类加入实例属性,需要在构造函数中往 this 挂载属性
new.target属性
new 是从构造函数生成实例的命令。
返回new 命令所作用的构造函数。如果不是通过 new 命令调用的,则返回 undefined。
通过这个属性,可以知道一个函数是作为构造函数使用了,还是作为普通函数调用了。
第20章 Class 的继承
子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。
这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。
ES5 和 ES6 的继承实质:
- ES5的继承实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面
Parent.apply(this)
- ES6的继承机制完全不同,实质是先创造父类的实例对象 this(所以必须先调用super 方法),然后再用子类的构造函数修改this。即 子类实例的构建是基于父类实例加工。
基于ES6的原理;只有调用 super 之后才可以使用 this 关键字
Object.getPrototypeOf()
此方法可以从子类上获取父类,因此可以使用这个方法判断一个类是否继承了另一个类。
Object.getPrototypeOf(ColorPoint) === Point;
super 关键字
super 关键字既可以当函数使用,也可以当对象使用。
- super 作为函数调用时代表父类的构造函数。
**注意:**super 虽然代表了父类A 的构造函数,但是返回的是子类B 的实例,即 super 内部的 this 指向的是B ,因此 super() 在这里相当于 A.prototype.constructor.call(this)。
**注意:**作为函数时,super() 只能用在子类的构造函数之中,用在其他地方就会报错。 - super 作为对象时在普通方法中指向父类的原型对象;在静态方法中指向父类。
**注意:**ES6规定,通过 super 调用父类的方法时,super 会绑定子类的 this。
类的 prototype 属性和 __proto__
属性
Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__
属性,因此同时存在两条继承链。
- 子类的
__proto__
属性表示构造函数的继承,总是指向父类。 - 子类 prototype 属性的
__proto__
属性表示方法的继承,总是指向父类的 prototype 属性。
class A {
}
class B extends A {
}
B.__proto__ === A; // true
B.prototype.__proto__ === A.prototype; // true
类的继承是按照如下模式实现的:
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 的实例继承 A 的静态属性
Object.setPrototypeOf(B, A);
而上述代码等同于:
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;
extends 的继承目标
有三种特殊情况需要注意:
- 子类继承 Object 类
class A extends Object {
}
A.__proto__ === Object; // true
A.prototype.__proto__ === Object.prototype; // true
- 不存在任何继承
class A {
}
A.__proto__ === Function.prototype; // true
A.prototype.__proto__ === Object.prototype; // true
这种情况下,A作为一个基类(即不存在任何继承)就是一个普通函数,所以直接继承 Function.prototype。但是,A调用后返回一个空对象,即const a = new A(); a instanceof Object; //true
,所以
A.prototype.__proto__
指向构造函数(Object)的prototype 属性。
- 子类继承 null
class A extends null {
}
A.__proto__ === Function.prototype; // true
A.prototype.__proto__ === undefined; // true
原生构造函数的继承
ECMAScript 的原生构造函数大致有:
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
在ES5的继承中,这些原生构造函数是无法继承的。
这是因为:原生构造函数会忽略 apply 方法传入的 this,也就是说,原生构造函数的 this 无法绑定,导致拿不到内部属性。
可是ES6的继承可以自定义原生数据结构(比如Array、String等)的子类,这是ES5无法做到的。
如下是使用ES6实现的带版本功能的数组:
class VersionedArray extends Array {
constructor() {
super();
this.history = [];
}
commit() {
this.history.push(this.slice());
}
revert() {
this.splice(0,this.length,...this.history[this.history.length - 1]);
}
}
const x = new VersionedArray();
x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]
x.commit();
x.history // [[], [1, 2]]
x.push(3);
x // [1, 2, 3]
x.revert();
x // [1, 2]
但是需要注意的是,继承Object的子类有一个行为差异
这是因为ES6改变了 Object 构造函数的行为,一旦发现Object 方法不是通过 new Object() 这种形式调用,ES6规定 Object构造函数会忽略参数。
Mixin 模式的实现
Mixin 模式:将多个类的接口“混入”另一个类,ES6实现如下:
function mix(...mixins) {
class Mix {}
for(let mixin of mixins) {
copyProperties(Mix, mixin);
copyProperties(Mix.prototype, mixin.prototype);
}
return Mix;
}
function copyProperties(target, source) {
for(let key of Reflect.ownKeys(source)) {
if(key !== 'constructor' && key !== 'prototype' && key !=='name') {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
class A {
a() {
console.log('A.a()');
}
b() {
console.log('A.b()');
}
}
class B {
b() {
console.log('B.b()');
}
}
class AA extends A {
aa() {
console.log('AA.aa()');
}
}
// A 将 覆盖 B的 同名方法
// 且只能 mix 子类的方法, 父类的方法无法获取
class C extends mix(B,AA) {
}
const c = new C();
c.a(); // 报错