ES6

202 阅读31分钟

ECMAScript 6 入门教程

由于 Babel 的强大和普及,现在 ES6/ES7 基本上已经是现代化开发的必备了。通过新的语法糖,能让代码整体更为简洁和易读。

let / const

1. let 命令

ES6 新增了 let 命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在 let 命令所在的代码块内有效。

  • 变量提升:var 命令会发生“变量提升”现象,即变量可以在声明之前使用,值为 undefined,let 声明的变量不存在变量提升
  • 暂时性死区:在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为
// “暂时性死区”也意味着 typeof 不再是一个百分之百安全的操作
{
    typeof x; // ReferenceError
    let x;
}
{
    typeof undeclared_variable // "undefined"
}
  • 不允许重复声明:不允许在相同作用域内,重复声明同一个变量
  • 块级作用域:let 和 const 声明的变量形成块级作用域
  • 省略 var 声明的变量会挂载在window上

2. const 命令

const 声明一个只读的常量。一旦声明,常量的值就不能改变

  • const 一旦声明必须赋值,不能使用 null 占位
  • 声明后不能再修改,如果声明的是复合类型数据,可以修改其属性

3. 声明变量的方法

ES5:var、function

ES6:var、function、let、const、import、class

4. 块级作用域与函数声明

ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但是,浏览器没有遵守这个规定。

ES6 的浏览器实现:

  • 允许在块级作用域内声明函数
  • 函数声明类似于 var,即会提升到全局作用域或函数作用域的头部
  • 函数声明还会提升到所在的块级作用域的头部

变量的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构,这种写法属于“模式匹配”。

1. undefined 和 null 、数值、布尔值的解构赋值

  • 由于 undefined 和 null 无法转为对象,所以对它们进行解构赋值,都会报错
  • 对于数值和布尔值类型,则会先转为对象
let { prop: x } = undefined; // TypeError

let {toString: s} = 123;
s === Number.prototype.toString // true

2. 字符串的解构赋值

  • 字符串也可以解构赋值,这是因为字符串被转换成了一个类似数组的对象
  • 类似数组的对象都有一个 length 属性,因此还可以对这个属性解构赋值
// a = 'h'; b = 'i'
const [a, b] = 'hi';

// len = 5
let {length : len} = 'hello';

3. 数组的解构赋值

  • 数组的解构可以用于嵌套结构的数组
  • 默认值生效的条件是,对象的属性值严格等于 undefined
  • 数组的元素是按次序排列的,变量的取值由它的位置决定
  • 只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值
// first = undefined; second = 1; third = null; end = ["bar", "baz"]
let [first, second = 1, third = 1, , ...end] = [, undefined, null, "foo", "bar", "baz"];

// x = 'a'
let [x, y, z] = new Set(['a', 'b', 'c']);

// 报错
let [foo] = undefined;

4. 对象的解构赋值

  • 对象的解构可以用于嵌套结构的对象
  • 默认值生效的条件是,对象的属性值严格等于 undefined
  • 对象的属性没有次序,变量必须与属性同名,才能取到正确的值
  • 由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构
// msg = "Something went wrong"
var { message: msg = 'Something went wrong' } = {};

// x = "Hello"; y = "World"
let { p: [x, { y }] } = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

// first = 1; last = 3;
let {0 : first, [arr.length - 1] : last} = [1, 2, 3];

5. 函数参数的解构赋值

  • 函数的参数也可以使用解构赋值
  • 函数参数的解构也可以使用默认值
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]

6. 用途

  • 交换变量的值let x = 1; let y = 2; [x, y] = [y, x];
  • 提取 JSON 数据:解构赋值对提取 JSON 对象中的数据,尤其有用
    • 取出函数返回的多个值:函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便
    • 函数参数的定义:解构赋值可以方便地将一组参数与变量名对应起来
    • 函数参数的默认值:解构赋值时可以通过设置默认值的形式,避免在函数体内部再默认赋值语句 var foo = config.foo || 'default foo'
  • 遍历 Map 结构:迭代中方便的获取 key 和 value for (let [key, value] of map) {}
  • 输入模块的指定方法:加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰 const { SourceMapConsumer, SourceNode } = require("source-map")

7. 总结

  • 解构遵循匹配模式:只要等号两边的模式相同,左边的变量就会被赋予对应的值
  • 解构赋值规则:只要等号右边的值不是对象或数组,就先将其转为对象
  • 解构默认值生效条件:属性值严格等于undefined
  • 解构不成功时变量的值等于undefined
  • undefined 和 null 无法转为对象,因此无法进行解构

数值的扩展

1. Number的扩展

  • Number.isFinite(); Number.isNaN()
  • Number.parseInt(); Number.parseFloat()
  • Number.isInteger()用来判断一个数值是否为整数
  • Number.EPSILON 极小的常量
  • Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER这两个常量,用来表示安全整数范围的上下限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内

2. Math 对象的扩展

  • Math.trunc()用于去除一个数的小数部分,返回整数部分
  • Math.sign() 用来判断一个数到底是正数、负数、还是零
  • Math.fround() 返回一个数的32位单精度浮点数形式
  • ES6 新增了 4 个对数相关方法
  • ES6 新增了 6 个双曲函数方法

3. 新增了一个指数运算符 **

2 ** 3 // 8

4. BigInt 数据类型

字符串的扩展

1. 模板字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者通过(${变量})在字符串中嵌入变量。

2. 标签模板

标签模板其实不是模板,而是函数调用的一种特殊形式

  • 标签指的就是函数,紧跟在后面的模板字符串就是它的参数
  • 如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数
    • 第一个参数是一个数组,该数组的成员是模板字符串中那些没有被变量替换的部分
    • 函数的其他参数,都是模板字符串各个变量被替换后的值

3. 标签模板的应用

  • 过滤 HTML 字符串,防止用户输入恶意内容SaferHTML`<p>${sender} has sent you a message.</p>
  • 多语言转换(国际化处理)i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!

4. 字符串的新增方法

  • str.includes(searchString[, position]) 于判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false
  • str.startsWith(searchString[, position]); str.endsWith(searchString[, length]) 用来判断当前字符串是否以另外一个给定的子字符串开头结尾,并根据判断结果返回 true 或 false
  • let resultString = str.repeat(count) 构造并返回一个新字符串,该字符串包含被连接在一起的指定数量的字符串的副本
  • str.padStart(targetLength [, padString]); str.padEnd(targetLength [, padString]) 用另一个字符串填充当前字符串(重复,如果需要的话),以便产生的字符串达到给定的长度。填充从当前字符串的开始末尾 应用的

数组的扩展

1. 扩展运算符

扩展运算符(spread)是三个点(...),它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

  • 用于函数调用,替代函数的 apply 方法:与正常的函数参数可以结合使用,非常灵活Math.max.apply(null, [14, 3, 77]); Math.max(...[14, 3, 77])
  • 复制数组const a2 = [...a1]; const [...a2] = a1;
  • 合并数组[...arr1, ...arr2, ...arr3]
  • 与解构赋值结合生成数组const [first, ...rest] = [1, 2, 3, 4, 5];
  • 调用的数据的 Iterator 接口转为真正的数组
  • 表达式中使用const arr = [ ...(2 > 0 ? ['a'] : []), 'b']
  • 扩展运算符后面是一个空数组,则不产生任何效果[...[], 1]

2. 数组新增方法

  • Array.from(arrayLike[, mapFn[, thisArg]]) 用于将类似数组的对象(即有length属性)和可遍历(iterable)的对象转为真正的数组
  • Array.of(element0[, element1[, ...[, elementN]]]) 用于将一组值,转换为数组
  • arr.includes(valueToFind[, fromIndex]) 判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false
  • arr.find(callback[, thisArg]); arr.findIndex(callback[, thisArg]) 返回数组中满足提供的测试函数的第一个元素的值;第一个元素索引
  • arr.fill(value[, start[, end]]) 用一个固定值填充一个数组中从起始索引到终止索引内的全部元素
  • arr.copyWithin(target, start = 0, end = this.length) 浅复制数组的一部分到同一数组中的另一个位置
  • arr.entries(); arr.keys(); arr.values() 返回一个新的Array Iterator对象,该对象包含数组中每个索引的键值对键值
  • arr.flat([depth]); arr.flatMap()

对象的扩展

1. 对象的扩展运算符

对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

  • 数组:由于数组是特殊的对象,所以对象的扩展运算符也可以用于数组
  • 字符串:会自动转成一个类似数组的对象,所以对象的扩展运算符也可以用于字符串
  • 扩展运算符后面不是对象,则会自动将其转为对象 {...1} 等同于 {...Object(1)} 最后结果为 {}
  • 合并两个对象 let ab = { ...a, ...b }; 等同于 let ab = Object.assign({}, a, b);
  • 用于解构赋值的最后一个参数 let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };

2. 对象的新增方法

  • Object.is(value1, value2) 与严格比较运算符(===)的行为基本一致。不同之处只有两个:一是+0不等于-0,二是NaN等于自身
  • Object.assign(target, ...sources) 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象
  • Object.getOwnPropertyDescriptors(obj) 获取一个对象的所有自身属性的描述符
  • __proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()
  • Object.keys(obj)、Object.values(obj)、Object.entries(obj) 分别是返回一个给定对象自身可枚举属性的属性、属性值、键值对数组
  • Object.fromEntries(iterable)Object.entries() 的逆操作,用于将一个键值对数组转为对象
  • __proto__:返回或设置对象的原型对象

3. 对象的扩展

  • 属性的简洁表示法const foo = 'bar'; const baz = {foo};
  • 属性名表达式const a = {[lastWord]: 'world'};
  • 方法的 name 属性:函数的name属性,返回函数名。对象方法也是函数,因此也有name属性
  • super 关键字: this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象

4. 链判断运算符 ?.

如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。链判断运算符有三种用法:

  • obj?.prop // 对象属性
  • obj?.[expr] // 同上
  • func?.(...args) // 函数或对象方法的调用
const user = (message && message.body && message.body.user) || 'default';
const user = message?.body?.user || 'default';

5. Null 判断运算符 ??

读取对象属性的时候,如果某个属性的值是null或undefined,有时候需要为它们指定默认值。常见做法是通过||运算符指定默认值。

const headerText = response.settings.headerText ?? 'Hello, world!'
const headerText = response.settings.headerText || 'Hello, world!'
const animationDuration = response.settings?.animationDuration ?? 300

6. 属性遍历 自身、可继承、可枚举、非枚举、Symbol

  • for-in:遍历对象自身可继承可枚举属性
  • Object.keys():返回对象自身可枚举属性的键组成的数组
  • Object.getOwnPropertyNames():返回对象自身可继承可枚举非枚举属性的键组成的数组
  • Object.getOwnPropertySymbols():返回对象Symbol属性的键组成的数组
  • Reflect.ownKeys():返回对象自身可继承可枚举非枚举Symbol属性的键组成的数组 规则
  • 首先遍历所有数值键,按照数值升序排列
  • 其次遍历所有字符串键,按照加入时间升序排列
  • 最后遍历所有Symbol键,按照加入时间升序排列

正则的扩展

  • RegExp 构造函数: 第一个参数是正则对象时,第二个参数可以指定修饰符 new RegExp(/abc/ig, 'i').flags
  • u 修饰符:含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符
  • y 修饰符:叫做“粘连”(sticky)修饰符,与g修饰符类似,也是全局匹配,后一次匹配必须从剩余的第一个位置开始
  • s 修饰符:dotAll 模式:使得.可以匹配任意单个字符。

函数的扩展

  • 函数参数的默认值
    • ES6 允许为函数的参数设置默认值与解构赋值默认值结合 function Func({ x = 1, y = 2 } = {}) {}
    • 参数赋值:惰性求值(函数调用后才求值)
    • 声明方式:默认声明,不能用const或let再次声明
    • length:返回没有指定默认值的参数个数但不包括 rest 参数 function Func({ x = 1, y = 2 }, arr, arr2 = [1,2], ...res) {} // 2
  • rest 参数(...):形式为(...变量名),用于获取函数的多余参数
  • name 属性:函数的 name 属性,返回该函数的函数名
  • 箭头函数
    • 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象
    • 不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误
    • 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
    • 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
  • 尾调用优化:只保留内层函数的调用帧 *

Symbol

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值

  • 创建 Symbol 的时候,可以添加一个描述 const sym = Symbol('foo');
  • Symbol 值作为对象属性名
    • 不能用点运算符
    • 属性名的遍历
      • 该属性不会出现在 for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回
      • Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值
      • Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名
  • Symbol 方法:
    • Symbol():创建以参数作为描述的Symbol值(不登记在全局环境)
    • Symbol.for():创建以参数作为描述的 Symbol 值,如存在此参数则返回原有的 Symbol 值(先搜索后创建,登记在全局环境)
    • Symbol.keyFor():返回已登记的 Symbol 值的描述(只能返回Symbol.for()的key)
  • 内置的 Symbol 值
    • Symbol.hasInstancefoo instanceof Foo在语言内部,实际调用的是 Foo[Symbol.hasInstance](foo)
    • Symbol.iterator 对象的 Symbol.iterator属性,指向该对象的默认遍历器方法
    • Symbol.toPrimitive 对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值
  • 应用场景
    • 唯一化对象属性名:属性名属于Symbol类型,就都是独一无二的,可保证不会与其他属性名产生冲突
    • 消除魔术字符串:在代码中多次出现且与代码形成强耦合的某一个具体的字符串或数值
    • 启用模块的 Singleton 模式:调用一个类在任何时候返回同一个实例(window和global), 使用 Symbol.for() 来模拟全局的 Singleton 模式

Set Map 数据结构

1. Set

它类似于数组,但是成员的值都是唯一的,没有重复的值(两个NaN是相等的;两个对象总是不相等的)。Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。

  • mySet.has(value) 返回一个布尔值来指示对应的值value是否存在Set对象中
  • mySet.delete(value) 从一个 Set 对象中删除指定的元素
  • mySet.clear() 用来清空一个 Set 对象中的所有元素
  • mySet.forEach(callback[, thisArg]) 根据集合中元素的插入顺序,依次执行提供的回调函数
  • mySet.keys(); mySet.values(); mySet.entries(); 分别返回[value];[value];[value value]的遍历器。另Set 结构的键名就是键值(两者是同一个值)
  • mySet.add(value) 向一个 Set 对象的末尾添加一个指定的值

set 特点:

  • 遍历顺序:插入顺序
  • 添加多个NaN时,只会存在一个NaN
  • 添加相同的对象时,会认为是不同的对象
  • 添加值时不会发生类型转换(5 !== "5")
  • 没有键只有值,可认为键和值两值相等,keys() 和 values() 的行为完全一致,entries() 返回的遍历器同时包括键和值且两值相等

2. WeakSet

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

  • WeakSet 的成员只能是对象,而不能是其他类型的值
  • WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用
  • 成员不适合引用,它会随时消失,因此 ES6 规定 WeakSet 结构不可遍历

应用:

  • 储存DOM节点:DOM节点被移除时自动释放此成员,不用担心这些节点从文档移除时会引发内存泄漏
  • 临时存放一组对象或存放跟对象绑定的信息:只要这些对象在外部消失,它在 WeakSet 结构中的引用就会自动消

3. Map

它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

  • myMap.has(key) 返回一个bool值,用来表明map 中是否存在指定元素
  • myMap.delete(key) 移除 Map 对象中指定的元素
  • myMap.clear() 移除Map对象中的所有元素
  • myMap.forEach(callback[, thisArg]) 根据集合中元素的插入顺序,依次执行提供的回调函数
  • myMap.keys(); myMap.values(); myMap.entries(); 分别返回[key];[value];[key value]的遍历器
  • myMap.set(key, value) 为 Map 对象添加或更新一个指定了键(key)和值(value)的(新)键值对
  • myMap.get(key) 返回某个 Map 对象中的一个指定元素

Map 特点:

  • 遍历顺序:插入顺序
  • 对同一个键多次赋值,后面的值将覆盖前面的值
  • 键跟内存地址绑定,只要内存地址不一样就视为两个键
    • 对同一个对象的引用,被视为一个键
    • 对同样值的两个实例,被视为两个键
  • 添加多个以NaN作为键时,只会存在一个以NaN作为键的值
  • Object结构提供字符串—值的对应,Map结构提供值—值的对应

4. WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMap与Map的区别有两点。

  • WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名
  • 它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内
  • 成员键不适合引用,它会随时消失,因此 ES6 规定 WeakMap 结构不可遍历
  • 弱引用的只是键而不是值,值依然是正常引用

应用:

  • 储存DOM节点:DOM节点被移除时自动释放此成员,不用担心这些节点从文档移除时会引发内存泄漏
  • 部署私有属性:内部属性是实例的弱引用,删除实例时它们也随之消失,不会造成内存泄漏

5. Map 与其他数据结构的互相转换

  • Map 转为数组:使用扩展运算符(...)
  • 数组转为 Map:使用 new Map(array)
  • Map 转为对象:如果所有 Map 的键都是字符串,它可以无损地转为对象。如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
  • 对象转为 Map:对象转为 Map 可以通过 Object.entries()
  • Map 转为 JSON:Map 的键名都是字符串,这时可以选择转为对象 JSON。Map 的键名有非字符串,这时可以选择转为数组 JSON
  • JSON 转为 Map:JSON 转为 Map,正常情况下,所有键名都是字符串。整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组,可以一一对应地转为 Map

Proxy

1. 定义

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

Proxy 其功能非常类似于设计模式中的代理模式,返回一个在被劫持的对象之前加了一层拦截的代理对象,读取代理对象中的属性或者是修改属性值就会被劫持。

是可以交由它来处理一些非核心逻辑,从而可以让对象只需关注于核心逻辑,达到关注点分离,降低对象复杂度等目的

  • 读取或设置对象某些属性前记录日志
  • 设置对象的某些属性值前先验证
  • 某些属性的访问控制等

2. 基本用法 const proxy = new Proxy(target, handler)

  • target 是用 Proxy 包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
  • handler 是以函数作为参数的对象,各属性中的函数分别定义了在执行各种操作时代理 proxy 的行为
  • proxy 是代理后的对象,当外界每次对 proxy 进行操作时,就会执行 handler 对象上的一些方法

3. handler 代理的一些常用的方法

  • construct(target, argumentsList, newTarget)
    • 拦截 new 操作符,比如 new proxy(...args)
  • 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循环,返回一个数组

4. Proxy.revocable()

Proxy.revocable() 返回可取消的 Proxy 实例(返回{ proxy, revoke },通过revoke()取消代理)

5. 注意

  • 数组的变化触发 get 和 set 可能不止一次,如有需要,自行根据 key 值决定是否要进行处理,比如如果是数组长度的变化则返回 if(key === 'length') return true
  • 对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果
  • 对于复杂数据类型,监控的是引用地址,而不是值,如果引用地址没有改变,那么不会触发 set
  • Proxy this 问题,在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理
const _name = new WeakMap();
class Person {
  constructor(name) {
    _name.set(this, name);
  }
  get name() {
    return _name.get(this);
  }
}
const jane = new Person('Jane');
jane.name // 'Jane'
const proxy = new Proxy(jane, {});
proxy.name // undefined

Object.defineProperty

1. 定义

Object.defineProperty(obj, prop, descriptor) 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

  • obj 要定义属性的对象
  • props 要定义或修改的属性的名称或 Symbol
  • descriptor 要定义或修改的属性描述符,是对象(数据描述符 + 存取描述符)

2. 描述符

数据描述符和存取描述符共同拥有的键

  • Configurable:属性表示对象的属性是否可以被删除,以及除 value 和 writable 特性外的其他特性是否可以被修改
  • Enumerable:属性定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举,而使用扩展运算符只能复制对象的可枚举属性
var o = {};
Object.defineProperty(o, "a", { value : 1, enumerable: true });
Object.defineProperty(o, "b", { value : 2, enumerable: false });
var p = { ...o }
Object.keys(p) // [a]

数据描述符拥有的键

  • value:该属性对应的值,可以是任何有效的 JavaScript 值(数值,对象,函数等)
  • writable:属性设置为 false 时,它不能被重新赋值(试图写入非可写属性,严格模式下会报错,非严格模式下不会引发错误也不会改变它)
var o = {};
Object.defineProperty(o, "a", {
  value : 37,
  writable : true,
  enumerable : true,
  configurable : true
});

存取描述符拥有的键

  • get 属性的 getter 函数:如果没有 getter,则为 undefined。当访问该属性时会调用此函数,该函数的返回值会被用作属性的值。执行时不传入任何参数,但是会传入 this 对象
  • set 属性的 setter 函数:如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象
var o = {};
var bValue = 1;
Object.defineProperty(o, "b", {
  // 下面两个缩写等价于:
  // get : function() { return bValue; },
  // set : function(newValue) { bValue = newValue; },
  get() { return bValue; },
  set(newValue) { bValue = newValue; },
  enumerable : true,
  configurable : true
});

3. 描述符的默认值

通常,使用点运算符和 Object.defineProperty() 为对象的属性赋值时,数据描述符中的属性默认值是不同的,如下例所示

var o = {};
o.a = 1;
// 等同于:
Object.defineProperty(o, "a", {
  value: 1,
  writable: true,
  configurable: true,
  enumerable: true
});

Object.defineProperty(o, "a", { value : 1 });
// 等同于:
Object.defineProperty(o, "a", {
  value: 1,
  writable: false,
  configurable: false,
  enumerable: false
});

4. 缺点

  • 只能劫持对象的属性,属性值也是对象那么需要深度遍历
  • Vue2 使用 Object.defineProperty 实现数据双向绑定,V3.0 则使用了 Proxy

5. Vue

  1. 可以将数组的索引作为属性进行劫持,但是仅支持直接对 arry[i] 进行操作,不支持数组的 API。Vue 经过内部 hack 处理,可以对一下几个 API 进行处理,其他的属性还是检测不到,具有局限性 push()、pop()、shift()、unshift()、splice()、sort()、reverse()
  2. 只能劫持对象的属性,属性值也是对象那么需要深度遍历。Vue 是通过(递归 + 遍历)对象来实现对数据的监控,如果属性值也是对象那么需要深度遍历
  3. Proxy作为es6的新属性在vue2.x之前就有了,为什么vue2.x不使用Proxy呢?
    • Proxy 是 es6 提供的新特性,兼容性不好,最主要的是这个属性无法用 polyfill 来兼容

Reflect

Reflect 对象的设计目的有这样几个:

  • 将 Object 对象的一些明显属于语言内部的方法,放到 Reflect 对象上。Object.defineProperty
  • 将某些Object方法报错情况改成返回false。Object.defineProperty(obj, name, desc) 在无法定义属性时会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false
  • 让 Object 操作都变成函数行为,某些 Object 操作是命令式。 name in obj; delete obj[name],而Reflect.has(obj, name);Reflect.deleteProperty(obj, name)让它们变成了函数行为
  • Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。

Iterator for...of

ES6 引入 for ... of 作为遍历所有数据结构的统一方法。只有实现了 Iterator 接口的对象才能使用 for of 来进行遍历。

1. Iterator 介绍

  • 迭代器是一种特殊的对象(类似接口)
  • 具有 next() 方法,返回一个对象,包含 value 和 done 两个属性
  • 迭代器保存一个内部指针,用来指向集合中当前值的位置,每次调用一次 next() 方法,都会返回下一个可用的值
  • Symbol.iterator 方法的最简单实现是 Generator 函数

2. 可迭代对象

  • 对象具有 Symbol.iterator 属性,此属性指向一个返回迭代器(Iterator)的方法
  • 默认部署迭代器的对象
    • Array、String、Map、Set、arguments、DOM data structures

3. 自动调用对象的 Iterator 方法

  • for...of 运行机制
    • 自动调用对象上的迭代器方法,依次执行迭代器的 next(),将 value 赋值给 for...of 内的变量
    • for...of 中断
      • 会自动调用迭代器的 return 方法
      • return() 方法必须有返回值,且值为object
  • 解构赋值
    • 对数据进行解构赋值时,会默认调用此数据的 Symbol.iterator 方法
    • var [a,b] = '12345'
  • 扩展运算符
    • 扩展运算符(...)会将任何部署了 Iterator 接口的数据结构,转为数组
    • [...'123']
  • yield* 关键字
    • yield* 后面跟的是一个可遍历的结构,执行时也会调用迭代器函数
    • let foo = function* () {yield 1; yield* [2,3,4]; yield 5;}
  • 作为数据源
    • 作为一些数据的数据源,比如某些 api 方法的参数是接收一个数组都会默认的调用自身迭代器
    • var arr = [100, 200, 300] arr[Symbol.iterator] = function () {}
    • var set = new Set(arr)
    • Array.from(arr)

4. 几种遍历数组语法的比较

  • for 循环
  • forEach
    • 无法中途跳出循环,break 命令或 return 命令都不能奏效
  • for...in
    • 数组的键名是数字,但是 for...in 循环是以字符串作为键名“0”、“1”、“2”等
    • for...in 循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键
    • 某些情况下,for...in 循环会以任意顺序遍历键名
  • for...of
    • 可以与 break、continue 和 return 配合使用
    • 提供了遍历所有数据结构的统一操作接口

Promise

1. 介绍

Promise 是异步编程的一种解决方案,比传统的解决方案回调函数和事件更合理和更强大。

  • 概念
    • 一个 Promise 有以下几种状态:pending、fulfilled、rejected
    • 对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态
      • 调用 resolve 使状态从 pending 变为 fulfilled
      • 调用 reject 使状态从 pending 变为 rejected
    • 一旦状态改变,就不会再变,任何时候都可以得到这个结果
  • 优点
    • 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
    • Promise 对象提供统一的接口,使得控制异步操作更加容易
  • 缺点
    • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消
    • 无法设置超时
    • 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部
    • 当处于 pending 状态时,无法得知目前进展到哪一个阶段

2. 方法

  1. new Promise(function(resolve, reject) {})
    • resolve
      • 将 Promise 对象的状态从 pending 变 resolved
      • 在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
      • resolve 函数的参数
        • 正常的值以外
        • 另一个 Promise 实例,那么 Promise 对象的最终状态取决于另一个异步操作的状态
    • reject
      • 将 Promise 对象的状态从 pending 变 rejected
      • 在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
    • 注意
      • Promise 新建后就会立即执行
      • then 方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行
      • 调用 resolve 或 reject 并不会终结 Promise 的参数函数的执行,后面的代码让会同步执行
  2. p.then(onFulfilled[, onRejected])
  3. p.catch(onRejected)
    • 如果 Promise 状态已经变成 resolved,再抛出错误是无效的
    • 建议总是使用 catch 方法,而不使用 then 方法的第二个参数
    • 使用 catch 回调捕获错误
      • Promise 内部 reject
      • Promise 内部同步抛出的错误
      • Promise 内部异步抛出的错误, 使用 try...catch 捕获然后在 catch 中使用 reject
      • then 方法指定的回调函数运行中抛出的错误,使用 try...catch 捕获然后在 catch 中使用 reject
    • 没有使用 catch 方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应
    • window.onunhandledRejection 可以捕获 promise 对象抛出的错误
  • p.finally(onFinally)
  • Promise.all(iterable) 所有 Promise 实例都变成 fulfilled,包装实例的状态就会变成 fulfilled
  • Promise.race() 只要一个 Promise 实例率先改变状态,包装实例的状态就会变成就跟着改变
  • Promise.allSettled() 等到所有 Promise 实例都返回结果,不管是 fulfilled 还是 rejected
  • Promise.any() 只要一个 Promise 实例 fulfilled状态,包装实例的状态就会变成 fulfilled
  • Promise.resolve(value/promise/thenable)
  • Promise.reject(reason)

Generator

1. 介绍

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。正常函数只能返回一个值,因为只能执行一次 return。Generator 函数可以返回一系列的值,因为可以有任意多个 yield。

  • Generator 函数是一个状态机,封装了多个内部状态
  • 执行 Generator 函数会返回一个遍历器对象。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态
  • Generator 函数是一个普通函数,但是有两个特征
    • function 关键字与函数名之间有一个星号
    • 函数体内部使用yield表达式,定义不同的内部状态
function* helloworldGenerator() {
    yield 'hello';
    yield 'world';
    return 'ending';
}

2. gen.next(value)

yield 表达式本身没有返回值,或者说总是返回 undefined。next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。

由于 next 方法的参数表示上一个 yield 表达式的返回值,所以在第一次使用 next 方法时,传递参数是无效的。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

3. gen.return(value)

Generator 函数返回的遍历器对象,还有一个 return 方法,可以返回给定的值,并且终结遍历 Generator 函数。

调用 return() 方法后,就开始执行 finally 代码块,不执行 try 里面剩下的代码了,然后等到 finally 代码块执行完,再返回 return() 方法指定的返回值。

function* numbers () {
  try {
    yield 2;
    yield 3;
  } finally {
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 2, done: false }
g.return(7) // { value: 5, done: false }
g.next() // { value: 7, done: true }

4. gen.throw(exception)

Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

这种函数体内捕获错误的机制,大大方便了对错误的处理。多个 yield 表达式,可以只用一个 try...catch 代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次 catch 语句就可以了。

Generator 函数体外抛出的错误,可以在函数体内捕获;反过来 Generator 函数体内抛出的错误,也可以被函数体外的 catch 捕获。

5. next()、throw()、return() 的共同点

next()、throw()、return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

  • next() 是将yield表达式替换成一个值。
  • throw() 是将yield表达式替换成一个throw语句。
  • return() 是将yield表达式替换成一个return语句。
const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;

gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));

gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;

async await

1. async

  • async 函数是 Generator 函数的语法糖:将 Generator 函数的星号(*)替换成 async;将 yield 替换成 await
  • async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数
  • async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态,抛出的错误对象会被 catch 方法回调函数接收到
  • async 函数可以保留运行堆栈,当函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句

2. await

  • await 命令后面是一个 Promise 对象,返回该对象的结果,如果不是 Promise 对象,就直接返回对应的值
  • await 命令后面是一个 thenable 对象(即定义then方法的对象),那么await会将其等同于 Promise 对象
  • await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到
  • 任何一个 await 语句后面的 Promise 对象变为 reject 状态,那么整个 async 函数都会中断执行。如果不要中断后面的异步操作,可以将 await 放在 try...catch 结构里面
  • 多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发

几种异步处理方法的比较

假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

Promise

function chainAnimationsPromise(elem, animations) {
  // 变量ret用来保存上一个动画的返回值
  let ret = null;
  // 新建一个空的Promise
  let p = Promise.resolve();
  // 使用then方法,添加所有动画
  for(let anim of animations) {
    p = p.then(function(val) {
      ret = val;
      return anim(elem);
    });
  }
  // 返回一个部署了错误捕捉机制的Promise
  return p.catch(function(e) {
    /* 忽略错误,继续执行 */
  }).then(function() {
    return ret;
  });
}

Generator

function chainAnimationsGenerator(elem, animations) {
  return spawn(function*() {
    let ret = null;
    try {
      for(let anim of animations) {
        ret = yield anim(elem);
      }
    } catch(e) {
      /* 忽略错误,继续执行 */
    }
    return ret;
  });

}

Async

async function chainAnimationsAsync(elem, animations) {
  let ret = null;
  try {
    for(let anim of animations) {
      ret = await anim(elem);
    }
  } catch(e) {
    /* 忽略错误,继续执行 */
  }
  return ret;
}