ES2015 一点点总结(2)

122 阅读10分钟

Symbol

Symbol 是一个新的原始类型,用来表示一个独一无二的值,可以通过 Symbol() 函数来创建一个 Symbol 类型的值,为了加以区分,可以传入一个字符串作为其描述:

let s1 = Symbol("foo");
let s2 = Symbol("foo");
s1 === s2; // false
  • Symbol 类型无法通过数学运算符进行隐式类型转换,但是可以通过 String() 显示转成字符串或者通过 Boolean() 显示转成布尔值:
let s = Symbol("foo");
String(s); // "Symbol('foo')"
s.toString(); // "Symbol('foo')"
Boolean(s); // true
  • 引入 Symbol 最大的初衷其实就是为了让它作为对象的属性名而使用,这样就可以有效避免属性名的冲突了:
let foo = Symbol("foo");
let obj = {
  [foo]: "foo1",
  foo: "foo2",
};
obj[foo]; // 'foo1'
obj.foo; // 'foo2'
  • Symbol 属性的不可枚举性,不会被 for...in、for...of、Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 等枚举:
let person = {
  name: "布兰",
  [Symbol("age")]: 12,
};
for (let x in person) {
  console.log(x); // 'name'
}
Object.keys(person); // ['name']
Object.getOwnPropertyNames(person); // ['name']
JSON.stringify(person); // '{"name":"布兰"}'

Object.getOwnPropertySymbols() 获取到对象的所有 Symbol 属性名,返回一个数组:

// 基于上面的代码
Object.getOwnPropertySymbols(person); // [Symbol(age)]

静态方法

  • Symbol.for() 按照描述去全局查找 Symbol,找不到则在全局登记一个:
let s1 = Symbol.for("foo");
let s2 = Symbol.for("foo");
s1 === s2; // true

Symbol.for() 的这个全局登记特性,可以用在不同的 iframe 或 service worker 中取到同一个值。

  • Symbol.keyFor() 根据已经在全局登记的 Symbol 查找其描述:
let s = Symbol.for("foo");
Symbol.keyFor(s); // 'foo'

Symbol 的内置值

  • Symbol.hasInstance:指向一个内部方法,当其他对象使用 instanceof 运算符判断是否为此对象的实例时会调用此方法;
  • Symbol.isConcatSpreadable:指向一个布尔,定义对象用于 Array.prototype.concat() 时是否可展开;
  • Symbol.species:指向一个构造函数,当实例对象使用自身构造函数时会调用指定的构造函数;
  • Symbol.match:指向一个函数,当实例对象被 String.prototype.match() 调用时会重新定义 match()的行为;
  • Symbol.replace:指向一个函数,当实例对象被 String.prototype.replace() 调用时会重新定义 replace() 的行为;
  • Symbol.search:指向一个函数,当实例对象被 String.prototype.search() 调用时会重新定义 search() 的行为;s
  • Symbol.split:指向一个函数,当实例对象被 String.prototype.split() 调用时会重新定义 split() 的行为;
  • Symbol.iterator:指向一个默认遍历器方法,当实例对象执行 for...of 时会调用指定的默认遍历器;
  • Symbol.toPrimitive:指向一个函数,当实例对象被转为原始类型的值时会返回此对象对应的原始类型值;
  • Symbol.toStringTag:指向一个函数,当实例对象被 Object.prototype.toString() 调用时其返回值会出现在 toString() 返回的字符串之中表示对象的类型;
  • Symbol.unscopables:指向一个对象,指定使用 with 时哪些属性会被 with 环境排除;

SET

Set 是一种新的数据结构,类似数组,但是它没有键只有值,且值都是唯一的。可以通过构造函数生成一个新实例,接收一个数组或者可迭代数据结构作为参数:

new Set([1, 2, 3]); // Set {1, 2, 3}
new Set("abc"); // Set {'a', 'b', 'c'}
  • Set 判断两个值是不是相等用的是 sameValueZero 算法,类似于 ===,唯一的区别是,在 Set 里 NaN 之间被认为是相等的:
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set.size; // 1
  • 相同对象的不同实例也被 Set 认为是不相等的:
let set = new Set();
let a = { a: 1 };
let b = { a: 1 };
set.add(a);
set.add(b);
set.size; // 2
  • Set 是有顺序的,将按照插入的顺序进行迭代,可以使用 for...of 迭代:
let set = new Set([1, 3]);
set.add(5);
set.add(7);
for (let x of set) {
  console.log(x);
}

Set 实例属性和方法

  • Set.prototype.constructor:构造函数,默认就是 Set 函数;
  • Set.prototype.size:返回 Set 实例的成员总数;
  • Set.prototype.add(value):添加某个值,返回 Set 结构本身;
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功;
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为 Set 的成员;
  • Set.prototype.clear():清除所有成员,没有返回值;
  • Set.prototype.keys():返回键名的遍历器;
  • Set.prototype.values():返回键值的遍历器;
  • Set.prototype.entries():返回键值对的遍历器;
  • Set.prototype.forEach():使用回调函数遍历每个成员;
let set = new Set([1, 3]);
set.add(5); // Set {1, 3, 5}
set.size; // 3
set.delete(1); // true,1 已被删除
set.has(1); // false
set.keys(); // SetIterator {3, 5}
set.clear();
set.size; // 0

Set 应用场景

  • 数组去重:
[...new Set([1, 3, 6, 3, 1])]; // [1, 3, 6]
Array.from(new Set([1, 3, 6, 3, 1])); // [1, 3, 6]
  • 字符串去重:[...new Set('abcbacd')].join('') // 'abcd'

  • 求两个集合的交集/并集/差集:

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]); // Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter((x) => b.has(x))); // set {2, 3}

// (a 相对于 b 的)差集
let difference = new Set([...a].filter((x) => !b.has(x))); // Set {1}
  • 遍历修改集合成员的值:
let set = new Set([1, 2, 3]);

// 方法一
let set1 = new Set([...set].map((val) => val * 2)); // Set {2, 3, 6}

// 方法二
let set2 = new Set(Array.from(set, (val) => val * 2)); // Set {2, 4, 6}

WeakSet

WeakSet 对象允许将弱保持对象存储在一个集合中:

let ws = new WeakSet();
let foo = {};
ws.add(foo); // WeakSet {{}}
ws.has(foo); // true
ws.delete(foo); // WeakSet {}

和 Set 的区别

  • WeakSet 只能是对象的集合,而不能是任何类型的任意值;
  • WeakSet 持弱引用:集合中对象的引用为弱引用。如果没有其他的对 WeakSet 中对象的引用,那么这些对象会被当成垃圾回收掉。这也意味着 WeakSet 中没有存储当前对象的列表。正因为这样,WeakSet 是不可枚举的,也就没有 size 属性,没有 clear 和遍历的方法。

实例方法:

  • WeakSet.prototype.add(value):添加一个新元素 value;
  • WeakSet.prototype.delete(value):从该 WeakSet 对象中删除 value 这个元素;
  • WeakSet.prototype.has(value):返回一个布尔值, 表示给定的值 value 是否存在于这个 WeakSet 中;

Map

Map 是一种类似于 Object 的这种键值对的数据结构,区别是对象的键只能是字符串或者 Symbol,而 Map 的键可以是任何类型(原始类型、对象或者函数),可以通过 Map 构造函数创建一个实例,入参是具有 Iterator 接口且每个成员都是一个双元素数组 [key, value] 的数据结构:

let map1 = new Map();
map1.set({}, "foo");

let arr = [
  ["name", "布兰"],
  ["age", 12],
];
let map2 = new Map(arr);
  • Map 中的 和 Set 里的值一样也必须是唯一的,遵循 sameValueZero 算法,对于同一个键后面插入的会覆盖前面的,
let map = new Map();
let foo = { foo: "foo" };
map.set(foo, "foo1");
map.set(foo, "foo2");
map.get(foo); // 'foo2'
  • 对于键名同为 NaN 以及相同对象而不同实例的处理同 Set 的值一样:
let a = NaN;
let b = NaN;
let map = new Map();
map.set(a, "a");
map.set(b, "b");
map.size; // 1
map.get(a); // 'b'

let c = { c: "c" };
let d = { c: "c" };
map.set(c, "c");
map.set(d, "d");
map.size; // 3
map.get(c); // 'c'

实例属性和方法

  • Map.prototype.size:返回 Map 对象的键值对数量;
  • Map.prototype.set(key, value):设置 Map 对象中键的值。返回该 Map 对象;
  • Map.prototype.get(key): 返回键对应的值,如果不存在,则返回 undefined;
  • Map.prototype.has(key):返回一个布尔值,表示 Map 实例是否包含键对应的值;
  • Map.prototype.delete(key): 如果 Map 对象中存在该元素,则移除它并返回 true;
  • Map.prototype.clear(): 移除 Map 对象的所有键/值对;
  • Map.prototype.keys():返回一个新的 Iterator 对象, 它按插入顺序包含了 Map 对象中每个元素的键;
  • Map.prototype.values():返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的值;
  • Map.prototype.entries():返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的 [key, value] 数组;
  • Map.prototype.forEach(callbackFn[, thisArg]):按插入顺序遍历 Map;
let map = new Map();
map.set({ a: 1 }, "a");
map.set({ a: 2 }, "b");

for (let [key, value] of map) {
  console.log(key, value);
}
// {a: 1} 'a'
// {a: 2} 'b'

for (let key of map.keys()) {
  console.log(key);
}
// {a: 1}
// {a: 2}

WeakMap

类似于 Map 的结构,但是键必须是对象的弱引用,注意弱引用的是键名而不是键值,因而 WeakMap 是不能被迭代的;

let wm = new WeakMap();
let foo = { name: "foo" };
wm.set(foo, "a"); // Weak
wm.get(foo); // 'a'
wm.has(foo); // true
  • 虽然 wm 的键对 foo 对象有引用,但是丝毫不会阻止 foo 对象被 GC 回收。当引用对象 foo 被垃圾回收之后,wm 的 foo 键值对也会自动移除,所以不用手动删除引用。

实例方法:

  • WeakMap.prototype.delete(key):移除 key 的关联对象;
  • WeakMap.prototype.get(key):返回 key 关联对象, 或者 undefined(没有 key 关联对象时);
  • WeakMap.prototype.has(key):根据是否有 key 关联对象返回一个 Boolean 值;
  • WeakMap.prototype.set(key, value):在 WeakMap 中设置一组 key 关联对象,返回这个 WeakMap 对象;

Proxy

Proxy 用来定义基本操作的的自定义行为,可以理解为当对目标对象 target 进行某个操作之前会先进行拦截(执行 handler 里定义的方法),必须要对 Proxy 实例进行操作才能触发拦截,对目标对象操作是不会拦截的,可以通过如下方式定义一个代理实例

let proxy = new Proxy(target, handler);

let instance = new Proxy(
  { name: "布兰" },
  {
    get(target, propKey, receiver) {
      return `hello, ${target.name}`;
    },
  }
);
instance.name; // 'hello, 布兰'
  • 如果 handle 没有设置任何拦截,那么对实例的操作就会转发到目标对象身上:
let target = {};
let proxy = new Proxy(target, {});
proxy.name = "布兰";
target.name; // '布兰'
  • 目标对象被 Proxy 代理的时候,内部的 this 会指向代理的实例:
const target = {
  m: function () {
    console.log(this === proxy);
  },
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m(); // false
proxy.m(); // true

静态 方法

  • Proxy.revocable() 用以定义一个可撤销的 Proxy
let target = {};
let handler = {};
let { proxy, revoke } = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo; // 123
revoke();
proxy.foo; // TypeError

handle 对象的方法:

  • get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.foo 和 proxy['foo']。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如 proxy.foo = v 或 proxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截 delete proxy[propKey] 的操作,返回一个布尔值。
  • ownKeys(target):拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截 Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截 Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(...args)。

CLASS

可以用 class 关键字来定义一个类,类是对一类具有共同特征的事物的抽象,就比如可以把小狗定义为一个类,小狗有名字会叫也会跳;类是特殊的函数,就像函数定义的时候有函数声明和函数表达式一样,类的定义也有类声明和类表达式,不过类声明不同于函数声明,它是无法提升的;类也有 name 属性

// 类声明
class Dog {
  constructor(name) {
    this.name = name;
  }
  bark() {}
  jump() {}
}
Dog.name; // 'Dog'

// 类表达式:可以命名(类的 name 属性取类名),也可以不命名(类的 name 属性取变量名)
let Animal2 = class {
  // xxx
};
Animal2.name; // 'Animal2'
  • JS 中的类建立在原型的基础上(通过函数来模拟类,其实类就是构造函数的语法糖),和 ES5 中构造函数类似,但是也有区别,比如类的内部方法是不可被迭代的:
class Dog {
  constructor() {}
  bark() {}
  jump() {}
}
Object.keys(Dog.prototype); // []

// 类似于
function Dog2() {}
Dog2.prototype = {
  constructor() {},
  bark() {},
  jump() {},
};
Object.keys(Dog2.prototype); // ['constructor', 'bark', 'jump']
  • 基于原型给类添加新方法:
Object.assign(Dog.prototype, {
  wag() {}, // 摇尾巴
});
  • 类声明和类表达式的主体都执行在严格模式下。比如,构造函数,静态方法,原型方法,getter 和 setter 都在严格模式下执行。

  • 类内部的 this 默认指向类实例,所以如果直接调用原型方法或者静态方法会导致 this 指向运行时的环境,而类内部是严格模式,所以此时的 this 会是 undefined:

class Dog {
  constructor(name) {
    this.name = name;
  }
  bark() {
    console.log(`${this.name} is bark.`);
  }
  static jump() {
    console.log(`${this.name} is jump.`);
  }
}
let dog = new Dog("大黄");
let { bark } = dog;
let { jump } = Dog;
bark(); // TypeError: Cannot read property 'name' of undefined
jump(); // TypeError: Cannot read property 'name' of undefined

方法和关键字

  • constructor 方法是类的默认方法,通过 new 关键字生成实例的时候,会自动调用;一个类必须有 constructor 方法,如果没有显示定义,则会自动添加一个空的;constructor 默认会返回实例对象:
class Point {}

// 等同于
class Point {
  constructor() {}
}
  • 通过 get 和 set 关键字拦截某个属性的读写操作:
class Dog {
  get age() {
    return 1;
  }
  set age(val) {
    this.age = val;
  }
}
  • 用 static 关键字给类定义静态方法,静态方法不会存在类的原型上,所以不能通过类实例调用,只能通过类名来调用,静态方法和原型方法可以同名:
class Dog {
  bark() {}
  jump() {
    console.log("原型方法");
  }
  static jump() {
    console.log("静态方法");
  }
}
Object.getOwnPropertyNames(Dog.prototype); // ['constructor', 'bark', 'jump']
Dog.jump(); // '静态方法'
let dog = new Dog();
dog.jump(); // '原型方法'

公有字段和私有字段

静态公有字段和静态方法一样只能通过类名调用;私有属性和私有方法只能在类的内部调用,外部调用将报错:

class Dog {
  age = 12; // 公有字段
  static sex = "male"; // 静态公有字段
  #secret = "我是人类的好朋友"; // 私有字段
  #getSecret() {
    // 私有方法
    return this.#secret;
  }
}
Dog.sex; // 'male'
let dog = new Dog();
dog.#getSecret(); // SyntaxError

类的继承

类可以通过 extends 关键字实现继承,如果子类显示的定义了 constructor 则必须在内部调用 super() 方法,内部的 this 指向当前子类

class Animal {
  constructor(name) {
    this.name = name;
  }
  run() {
    console.log(`${this.name} is running.`);
  }
}
class Dog extends Animal {
  constructor(name) {
    super(name); // 必须调用
    this.name = name;
  }
  bark() {
    console.log(`${this.name} is barking.`);
  }
}
let dog = new Dog("大黄");
dog.run(); // '大黄 is running.'
  • 通过 super() 调用父类的构造函数或者通过 super 调用父类的原型方法;另外也可以在子类的静态方法里通过 super 调用父类的静态方法:
// 基于上面的代码改造
class Dog extends Animal {
  constructor(name) {
    super(name); // 调用父类构造函数
    this.name = name;
  }
  bark() {
    super.run(); // 调用父类原型方法
    console.log(`${this.name} is barking.`);
  }
}
let dog = new Dog();
dog.bark();
// '大黄 is running.'
// '大黄 is barking.'
  • 子类的 proto 属性,表示构造函数的继承,总是指向父类;子类 prototype 属性的 proto 属性,表示方法的继承,总是指向父类的 prototype 属性:
class Animal {}
class Dog extends Animal {}

Dog.__proto__ === Animal; // true
Dog.prototype.__proto__ === Animal.prototype; // true
  • 子类原型的原型指向父类的原型:
// 基于上面的代码
let animal = new Animal();
let dog = new Dog();
dog.__proto__.__proto__ === animal.__proto__; // true
  • 使用 extends 还可以实现继承原生的构造函数,如下这些构造函数都可以被继承:
    • String()
    • Number()
    • Boolean()
    • Array()
    • Object()
    • Function()
    • Date()
    • RegExp()
    • Error()
class MyString extends String {
  constructor(name) {
    super(name);
    this.name = name;
  }
  welcome() {
    return `hello ${this.name}`;
  }
}
let ms = new MyString("布兰");
ms.welcome(); // 'hello 布兰'
ms.length; // 2
ms.indexOf("兰"); // 1

Module

  • 浏览器传统加载模块方式:
// 同步加载
<script src="test.js"></script>
// defer异步加载:顺序执行,文档解析完成后执行;
<script src="test.js" defer></script>
// async异步加载:乱序加载,下载完就执行。
<script src="test.js" async></script>
  • 浏览器现在可以按照模块(加上 type="module")来加载脚本,默认将按照 defer 的方式异步加载;ES6 的模块加载依赖于 import 和 export 这 2 个命令;模块内部自动采用严格模式:

<script type="module" src="test.js"></script>

  • export 用于输出模块的对外接口,一个模块内只能允许一个 export default 存在,以下是几种输出模块接口的写法:
// person.js
// 写法一:单独导出
export const name = '布兰'
export const age = 12
  
// 写法二:按需导出
const name = '布兰', age = 12
export { name, age }
  
// 写法三:重命名后导出
const name = '布兰', age = 12
export { name as name1, age as age1 }
  
// 写法四:默认导出
const name = '布兰'
export default name
  • import 用于输入其他模块的接口:
// 按需导入
import { name, age } './person.js'
  
// 导入后重命名
import { name1 as name, age1 as age } from './person.js'
  
// 默认导入
import person from './person.js'
  
// 整体导入
import * as person from './person.js'
  
// 混合导入
import _, { each } from 'lodash'

import 导入的细节:

  • 导入的变量名必须与导出模块的名字一致,可以使用 as 进行重命名;

  • 导入的变量都是只读的,不能改写;

  • import 命令具有提升效果,会提升到整个模块的头部,首先执行;

  • import 是编译时导入,所以不能将其写到代码块(比如 if 判断块里)或者函数内部;

  • import 会执行所加载的模块的代码,如果重复导入同一个模块则只会执行一次模块;

  • import 和 export 的复合写法:export 和 import 语句可以结合在一起写成一行,相当于是在当前模块直接转发外部模块的接口,复合写法也支持用 as 重命名。以下例子中需要在 hub.js 模块中转发 person.js 的接口:

// person.js
const name = '布兰', age = 12
export { name, age }
// 按需转发接口(中转模块:hub.js)
export { name, age } from './person.js'
// 相当于
import { name, age } from './person.js'
export { name, age }
  
// person.js
const name = '布兰', age = 12
export default { name, age }
// 转发默认接口(中转模块:hub.js)
export { default } from './person.js'
// 相当于
import person from './person.js'
export default person

ES6 模块和 CommonJS 模块的差异:

  • CommonJS 模块输出的是一个值的拷贝(一旦输出一个值,模块内部的变化就影响不到这个值),ES6 模块输出的是值的引用(是动态引用且不会缓存值,模块里的变量绑定其所在的模块,等到脚本真正执行时,再根据这个只读引用到被加载的那个模块里去取值);
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口;
  • CommonJS 模块的 require() 是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段;