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 命令是异步加载,有一个独立的模块依赖的解析阶段;