ES6

2021-05-02 16:07:22 Javascript 大约 16 分钟

ES6代表着 JS 这门语言最新的标准。目前以Chrome为首的现代浏览器已经基本实现了ES6的大多数新特性,即使在不支持ES6的浏览器上,开发者也可以通过转译工具如Babel将ES6代码转译为ES5代码来实现兼容。

# Symbol

# 概述

  1. ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednullBooleanStringNumberObject
  2. 直接调用Symbol函数即可生成一个Symbol。
    • 注意:Symbol函数前不能使用new命令,否则会报错。
  3. Symbol函数可以接受一个字符串作为参数,表示对 Symbol 的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
let s1 = Symbol('foo');
let s2 = Symbol('bar');

console.log(s1) // Symbol(foo)
console.log(s2) // Symbol(bar)
1
2
3
4
5

注意:Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。

// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();

s1 === s2 // false

// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');

s1 === s2 // false
1
2
3
4
5
6
7
8
9
10
11

# Symbol作为属性

Symbol 值可以作为标识符,用于对象的属性名,由于每一个 Symbol 值都是不相等的,这意味着就能保证不会出现同名的属性,能防止某一个键被不小心改写或覆盖的情况。

注意:在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。

let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
let a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Symbol 作为属性名,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。但是,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。

# Symbol.for

有时,我们希望重新使用同一个 Symbol 值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true
1
2
3
4

Symbol.for()Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值。

# 内置Symbol

除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。

  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.species
  • Symbol.match
  • Symbol.replace
  • Symbol.search
  • Symbol.split
  • Symbol.toPrimitive
  • Symbol.toStringTag
  • Symbol.unscopables
  • Symbol.iterator,对象的Symbol.iterator属性,指向该对象的默认生成遍历器的方法。

# 实例:

# 消除魔术字符串

function getArea(shape, options) {
  let area = 0;

  switch (shape) {
    case 'Triangle': // 魔术字符串
      area = .5 * options.width * options.height;
      break;
    /* ... more code ... */
  }

  return area;
}

getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面代码中,字符串Triangle就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。

常用的消除魔术字符串的方法,就是把它写成一个变量。

const shapeType = {
  triangle: 'Triangle'
};

function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case shapeType.triangle:
      area = .5 * options.width * options.height;
      break;
  }
  return area;
}

getArea(shapeType.triangle, { width: 100, height: 100 });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

但是如果没有办法保证有人写了一句

const shapeType = {
  triangle: 'Triangle',
  rectangle:'Triangle'
};
1
2
3
4

接下来一旦出现bug会很难调试

如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用 Symbol 值。

const shapeType = {
  triangle: Symbol(),
  rectangle:Symbol()
};
1
2
3
4

# 实现私有属性

可以简单的理解为:在函数的内部用 变量 声明的属性为私有属性;

第一种方式:用一个字符串或者下划线的方式(和别人约定的)

var Person = (function() {
    let _name = `_name` // 私有属性
    // 或者 let name = 'shfjkshfkjsjkf'
    function Person(name) {
        this[_name] = name;
    }

    Person.prototype.getName = function() {
        return this[_name];
    };

    return Person;
}());
1
2
3
4
5
6
7
8
9
10
11
12
13

缺点很显然,不是真正的私有,依然可以遍历Object.keys()

第二种方式:闭包

var Person = (function() {
    function Person(name) {
        this.getName = function() {
            return name;
        };
    }

    return Person;
}());
1
2
3
4
5
6
7
8
9

真的私有了,但是仍然存在缺点:实例无法共享方法,浪费内存空间

第三种方式:使用Symbol

var Person = (function() {
    var nameSymbol = Symbol('name');

    function Person(name) {
        this[nameSymbol] = name;
    }

    Person.prototype.getName = function() {
        return this[nameSymbol];
    };

    return Person;
}());
1
2
3
4
5
6
7
8
9
10
11
12
13

缺陷:仍然会被Object.getOwnPropertySymbols获取到属性,进而修改该属性对应的值

# Set和WeakSet

# Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。需要记录不同成员的又不希望重复记录的情况下可以用到Set

如何生成Set:

let set1 = new Set()
let set2 = new Set([1,2,3])
1
2

Set 实例的属性:

  • Set.prototype.size:返回Set实例的成员总数。

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。

四个操作方法:

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。

由于Set中值不会重复,可以用来Set来做数组去重

四个遍历方法:

  • Set.prototype.keys():返回键名遍历器
  • Set.prototype.values():返回键值遍历器
  • Set.prototype.entries():返回键值对遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

注意:Set实例中key和value是一样的,所以keys()values()这两个方法的结果是一样的

# 实例

Set中查找某个值是否已经存在的时间复杂度是O(1),而数组的indexOf方法时间复杂度是O(n),又由于Set中值不会重复,所以可以使用Set做数组去重:

//使用indexOf 缺点:时间复杂度O(n^2)性能低下, NaN要做特殊处理
function deduplicate1(arr) {
  let temp =[]
  for (let i = 0; i < arr.length; i++) {
    if(temp.indexOf(arr[i]) === -1){
      temp.push(arr[i])
    }
  }
  return temp
}

//使用对象解决性能问题 但是数组里不能有对象,null,undefined,Boolean值,也无法区分字符串和数字
function deduplicate2(arr) {
  let temp = {}
  for (let i = 0; i < arr.length; i++) {
    if(!temp[arr[i]]){
      temp[arr[i]] = true
    }
  }
  return Object.keys(temp)
}

//使用Set来去重
function deduplicate3(arr) {
  let temp = [...(new Set(arr))]
  return temp
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

  • WeakSet 的成员只能是对象,而不能是其他类型的值。

  • WeakSet 中的对象都是弱引用

如果一个对象没有任何引用,那么此对象会尽快被垃圾回收,释放掉它占用的内存。

即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

WeakSet 结构有以下三个方法。

  • WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在WeakSet 实例之中。

WeakSet 不能遍历,是因为成员都是弱引用,随时可能消失。

示例:

let div = document.querySelector('div')
let set = new Set()
set.add(div)
//...some code
document.body.removeChild(div)
div = null //dom对象仍在内存中,因为Set中仍然引用此对象
1
2
3
4
5
6
let div = document.querySelector('div')
let weakset = new WeakSet()
weakset.add(div)
//...some code
document.body.removeChild(div)
div = null //dom对象的已经没有引用,将被垃圾回收机制回收
1
2
3
4
5
6

# Map和WeakMap

# Map

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键,是一种更完善的 Hash 结构实现。

生成Map实例:

const map1 = new Map();
const map2 = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);
1
2
3
4
5

Map 实例的属性:

  • Map.prototype.size:返回Map实例的成员总数。

Map实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。

四个操作方法:

  • Map.prototype.set(key,value):设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
  • Map.prototype.get(key):读取key对应的键值,如果找不到key,返回undefined
  • Map.prototype.has(key):返回一个布尔值,表示某个键是否在当前 Map 对象之中。
  • Map.prototype.delete(key):删除某个键,返回true。如果删除失败,返回false
  • Map.prototype.clear():清除所有成员,没有返回值。

四个遍历方法:

  • Map.prototype.keys():返回键名遍历器
  • Map.prototype.values():返回键值遍历器
  • Map.prototype.entries():返回键值对遍历器
  • Map.prototype.forEach():使用回调函数遍历每个成员

# 实例1:扩展对象

当我们有一系列对象,想记录每个对象一种属性。假设有100只鸡,需要记录每只鸡的重量,有两种思路:

  1. 想办法用笔写到鸡身上
  2. 记录到一个本本上
class Chicken {
}
// 100只鸡
let chickenList = []
for (let i = 0; i < 100; i++) {
  chickenList.push(new Chicken())
}
                   
// 方法1:记录到鸡身上
chickenList.forEach(function(chicken, index){
	chicken.weight = getWeight(chicken);
});

// 方法2:记录到本本上
let notebook = [];
chickenList.forEach(function(chicken, index){
	notebook[index] = getWeight(chicken);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第1种思路存在以下问题:

  1. 破坏了鸡的卖相,有时候这是很严重的事情,比如你想把一只5斤的鸡当成6斤卖出去,结果鸡身上直接写“我只有5斤”(修改了原有对象,可能导致意外的行为)
  2. 可能碰到一些战斗鸡,一个字都写不上去(对象冻结了或者有不可覆盖的属性)
  3. 可能写到一些本来就写了字的地方,导致根本看不清(与对象原有属性冲突)

再看第2种方法,存在以下问题:

  1. 本本无法和鸡精准地一一对应,只能靠一些索引或者标记(例如给每只鸡起一个名字)去(不可靠)地记录对应关系(无法精准地对比到是哪一个对象)

这时候就可以使用Map扩展对象

// 记录到另一个本本上
let notebook = new Map();
chickenList.forEach(function(chicken, index){
	notebook.set(chicken, getWeight(chicken));
});
1
2
3
4
5

# 实例2:完善私有属性的实现

回顾之前的Symbol实现的私有属性的版本里,仍然存在着可以被特殊api遍历的缺陷。

基于Map的解决思路:

用一个闭包内的Map来扩展每个生成的对象

var Person = (function() {
  var map = new Map();

  function Person(name) {
    map.set(this,name);
  }

  Person.prototype.getName = function() {
    return map.get(this);
  };

  return Person;
}());
1
2
3
4
5
6
7
8
9
10
11
12
13

# WeakMap

与之前介绍的WeakSet 类似,WeakMapMap 有两个区别。

  • WeakMap的键只能是对象,而不能是其他类型的值。
  • WeakMap 中对键的引用是弱引用

同样地,WeakMap 不能遍历,是因为成员都是弱引用,随时可能消失。

WeakMap只有四个方法可用:get()set()has()delete()

注意:WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

const wm = new WeakMap();
let key = {};
let obj = {foo: 1};

wm.set(key, obj);
obj = null;
wm.get(key)
1
2
3
4
5
6
7

# 实例:完善私有属性的实现

前面基于Map的实现还存在一个问题:

Person实例的外部引用消除时,闭包中的Map仍然有Person实例作为键的引用,Person实例不会被垃圾回收,必须等到所有的Person实例的外部引用消除,Map所在的闭包也会消除,最后Person实例才会被垃圾回收

为了解决这个问题,使用WeakMap进一步完善:

var Person = (function() {
  var wm = new WeakMap();

  function Person(name) {
    wm.set(this,name);
  }

  Person.prototype.getName = function() {
    return wm.get(this);
  };

  return Person;
}());
1
2
3
4
5
6
7
8
9
10
11
12
13

# Proxy

在ES6之前Object.defineProperty可以拦截对象属性的读取和修改操作,Proxy 可以理解成比这个API更强大的,在目标对象之前架设一层的“拦截”。外界对该Proxy对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

注意:只有对生成的 Proxy 实例操作才能起到拦截的作用

生成Proxy实例:

var proxy = new Proxy(target, handler);
1
  • target :需要代理的对象
  • handler :拦截函数的集合

如果handler是空对象则代表任何操作都不会拦截

let obj = {}

/*handler为空对象*/
let proxy = new Proxy(obj, {});
proxy.a = 1
//obj.a  //1
1
2
3
4
5
6

对属性的读取进行拦截:

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35
1
2
3
4
5
6
7
8
9

下面是 Proxy 支持的拦截操作一览,一共 13 种。

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['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),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(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)
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。

Proxy给了开发者拦截语言默认行为的权限,可以不改变原有对象或函数的情况下,轻松运用在很多场景。例如:统计函数调用次数,实现响应式数据观测(Vue 3.0),实现不可变数据(Immutable)等等

# Reflect

Reflect是 ES6 为了操作对象而提供的新 API。ES6把原先版本中很多语言层面的API,比如Object.defineProperty delete in等集中在了Reflect的静态方法上,引入Reflect的目的有这样几个。

(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

(2) 修改某些Object方法的返回结果,让其变得更合理。

// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

(3)将命令式操作转变为函数调用,避免更多的保留字占用。比如name in objdelete obj[name],对应Reflect.has(obj, name)Reflect.deleteProperty(obj, name)

// 老写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true
1
2
3
4
5

(4)Reflect对象的方法与Proxy对象的方法一一对应,想要调用默认行为,直接在Reflect上调用同名方法,简单可靠,省去人工写默认行为的代码。

let proxy = new Proxy({}, {
  set: function(target, name, value, receiver) {
    var success = Reflect.set(target, name, value, receiver);
    if (success) {
      console.log('property ' + name + ' on ' + target + ' set to ' + value);
    }
    return success;
  }
});
1
2
3
4
5
6
7
8
9

Reflect对象一共有 13 个静态方法。

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

上面这些方法的作用,与Proxy对象handler的方法是一一对应的。

# Iterator

Iterator(遍历器、迭代器) 是一个对象,Iterator对象需要包含一个next方法,该方法返回一个对象,此对象有两个属性,一个value表示当前结果,一个done表示是否可以继续迭代

let it = makeIterator();

function makeIterator() {
  let nextIndex = 0;
  return {
    next: function() {
      return nextIndex < 5 ?
      {value: nextIndex++, done: false} :
      {value: undefined, done: true};
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12

ES6 规定,如果数据结构的Symbol.iterator属性是一个方法,该方法返回Iterator对象,就可以认为此数据结构是“可遍历的”(iterable)

interface Iterable {
  [Symbol.iterator]() : Iterator,
}

interface Iterator {
  next(value?: any) : IterationResult,
}

interface IterationResult {
  value: any,
  done: boolean,
}
1
2
3
4
5
6
7
8
9
10
11
12

实例:

let obj = {
  [Symbol.iterator]:makeIterator
}
1
2
3

ES6中以下场合会默认调用 Iterator 接口(即Symbol.iterator方法),

  • for...of循环
  • 数组解构
  • 扩展运算符
  • yield*
  • 其他隐式调用的地方,例如 new Set(['a','b']),Promise.all()

ES6中以下数据结构默认为可遍历对象,即默认部署了Symbol.iterator属性

  • Array
  • Map
  • Set
  • String
  • 函数的 arguments 对象
  • NodeList 对象

# Generator函数

# Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍 Generator 函数的语法和 API,它的异步编程应用请看《Generator 函数的异步应用》一章。

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

# async函数

# async

上次编辑于: 2023年7月4日 09:36