ES6语法特性精华

96
科研者
2017.09.01 19:02* 字数 6976

以下内容是我在学习和研究ES6时,对ES6的特性、重点和注意事项的提取、精练和总结,可以做为ES6特性的字典;在本文中,不但详细严谨地罗列了ES6各特性的具体规则,对于一些较难理解的特性也给出了其内部的运行机制,方便大家深入理解;


1. let和const的特性

  1. let用来声明变量;const用来声明常量;
  2. 块级作用域;
  3. 不会变量提升;
  4. 不允许重复声明;
  5. 在全局作用域内用let、const、class声明的量不会成为全局对象的属性;
  6. 在块级作用内任何地方用let或const声明的变量,在该块级作用域开始处就已经存在,但不可获取,只能在声明之后才能获取;
    备注:
    这个特性说明:在实现机制上,let和const在块级作用域内还是存在变量提升的,编译器只是从语法规则上模仿了没有变量提升的特性;

2. 能声明变量或常量的关键字

var、function、let、const、import、class


3. 代码块

代码块不存在于表达式中;
应用示例:

let x;
{x} = {x:34};   // 提示语法错误:  SyntaxError: syntax error

这段代码会被提示语法错误;因为解析器认为{x}是个代码块;
如果想让解析器认为{x} = {x:34}是变量解构表达式,则只需要在{x} = {x:34}外加个圆括号即可,因为({x} = {x:34})会被认为是表达式,而代码块是不会存在于表达式中的;如下:

let x;
({x} = {x:34});

4. Null传导运算符

Null传导运算符为标识符?.操作
表示:如果标识符的值为null或undefined,则返回undefined,并且不执行后面的操作;
(此特性应该是借鉴的swift语言中的可选链特性;)

Null传导运算符有四种用法,如下:

obj?.prop // 读取对象属性
obj?.[expr] // 读取对象属性
func?.(...args) // 函数或对象方法的调用
new C?.(...args) // 构造函数的调用

5. 属性特性enumerable影响的几个操作

目前,有四个操作会忽略属性特性enumerable为false的属性:

  1. for...in循环:只遍历对象自身的和继承的可枚举的属性;
  2. Object.keys():返回对象自身的所有可枚举的属性的键名;
  3. JSON.stringify():只串行化对象自身的可枚举的属性;
  4. Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性;

6. 模板字符串

  1. 模板字符串是增强版的字符串,用反引号`标识;可以通过${表达式}在模板字符串中嵌入变量或者表达式;
  2. 模板字符中的所有空格和换行都会被保留;

7. 标签模板

标签模板是函数结合模板字符串的一种调用形式;即标签模板是一种函数的调用;标签指代函数名字,模板指代模板字符串;
标签模板的格式如下:

函数名 模板字符串

标签模板会自动把模板字符串拆分,然后把各部分以参数的形式传给函数,具体的拆分规则和传参规则如下:
模板字符串的拆分规则:

  1. 如果模板字符串中有n个模板插值,则会以模板插值${表达式}为节点把模板字符串拆分成n+1个不含插值的字符串;
  2. 如果模板插值在模板字符串中的最前端或者最后端,则拆分后的 第1个 或者 最后一个 字符串为不包含任何字符的空字符串;
  3. 如果模板字符串不包含模板插值,则不对模板字符串进行拆分';

模板字符串的传参规则:

  1. 把被拆分后的n+1个字符串(不包含模板插值)按照在模板字符中的次序装进数组中,然后把该数组传为第1个参数传给标签函数;
  2. 模板中第m个插值的值会作为标签函数的第m+1个参数传给标签函数;

8. 数组

  1. 支持扩展运算符...
    格式为:...数组
    作用:把数组展开为用逗号分隔的该数组中所有元素的序列;它就像rest参数操作符的逆运算;如果数组为空,则不产生任何内容;
  2. ES6规定,对数组进行的任何的操作,都不能将空位忽略,必须将数组中的空位将会被转为undefined;

9. 对象

  1. 对象支持属性简写语法;
  2. 在对象字面量中支持用表达式定义属性名,格式为{[表达式]:属性值},属性的名为把表达式的值被转成字符串类型后的值;
  3. (本条是ES2017的新特性)支持扩展运算符...,格式为:{...对象};规则如下:
    假设:x是个对象;
    则有:表达式{...x}会取出对象x的所有可遍历属性,并将取出的所有属性及属性值拷贝到当前对象字面量中;
  4. 支持Null传导运算符;

10. 属性的简写语法规则

在ES6中,对象的属性支持简写,具体规则如下:
假设:x是 变量 或者 常量 或者 函数;
则有:{x}相当于{'x':x}


11. 变量的解构赋值

  1. 允许模式不完全匹配,即支持不完全解构;
  2. 对于没有匹配值的变量的值为undefined;
  3. 可以给模式中的变量设置默认值,默认值生效的条件是:该变量没有配置的值,或者配置的值全等于===undefind;默认值可以是表达式,且该表达式是惰性求值的,即:只有在需要设置该默认值时,才会求值该表达式;
  4. 数组解构赋值的机制:根据模式中变量的位置对变量赋值;
  5. 对象解构赋值的机制:根据模式中的属性名对该属性相对应的变量赋值;
  6. 对象的解构模式支持属性的简写语法;
  7. 可以对数组应用对象解构,因为数组本质上是对象;(详情请参考《JavaScript的发现与理解》)
  8. 当解构字符串时,字符串会被转换成类似数组的对象,该对象有length属性,所以可以对字符串应用数组解构;
  9. 当被解构的数据是非对象类型时,会先把该数据转换成相应的基本包装对象,然后再对其进行解构;
  10. 变量解析也可以应用于函数的参数;

12. 函数

  1. 参数可以设置默认值;
  2. 当函数的部分或者全部参数带有默认值时,函数的任何参数都不能有同名参数;
  3. 参数的默认值是惰性求值的,且每次需要默认值时都会重新计算;
  4. 当函数的参数是解构时,在调用该函数时,传入的参数必须使所有的解构参数成功解构,否则将报错;通过给解构参数设置能成功解构的默认值可以防止函数在调用时因传解构失败而报错;
  5. 函数对象有个length属性,该属性表示的准确意义是:函数预期传入的参数个数;所以,length属性的值为函数的不带默认值的参数的个数;
  6. 如果函数带有包含默认值的参数,那么函数在被调用时,会为所有的参数会形成一个单独的作用域;该作用域会在所有参数被初始化后消失;如果函数的任何参数都不包含默认值,则在函数在被调用时不会为参数生成单独的作用域;
  7. 如果函数的任何参数都不包含默认值,则函数的参数是在函数内默认用var声明的;如果函数带有包含默认值的参数,则函数的参数是在被用let声明的;(此结论是经过测试研究推理得出的,没有得到权威的承认);
  8. 支持rest参数(剩余参数),且rest参数必须是最后一个参数;
  9. 在ES2016中,只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错;
  10. 函数对象的name属性保存的是函数最初设置的名字;
    在ES6中:对于匿名函数,在其第1次赋值给的变量之前,其name属性的值为空字符串"",在其第1次赋值给的变量之后,其name属性的值为该匿名函数第1次赋值给的变量的名字;
    在ES5及之前中:对于匿名函数,其name属性的值一直为空字符串"";
    Function构造函数返回的函数实例的name属性的值为anonymous;
    bind()返回的函数的name属性值是;bound 原name;
  11. 支持尾调用优化;

13. 箭头函数

  1. 箭头函数会绑定其this的值为:该箭头函数被定义处所在作用域中的this的值;即:箭头函数中的this的值 是 定义该箭头函数处所在作用域中的this的值;
  2. 不能用作构造函数;
  3. 箭头函数中没有arguments对象;
  4. 箭头函数中不能使用yield关键字;

14. Set和Map

Set:类似数组,但成员值都是唯一的,不会重复,并且没有索引;
WeakSet:特殊的Set;成员只能是对象类型,并且对成员的引用是弱引用;当成员被回收时,该成员会自动被移除;
Map:键值集合,键可以是各种类型的值;
WeakSet:特殊的Map,只能用对象或者null作为键,键对对象的引用是弱用;当键引用的对象被回收时,该键及其对应的值会被自动移除;


15. Proxy代理

通过proxy对象调用的方法中的this的值为proxy自身;


16. Promise特点

  1. promise对象的状态不受处界影响;
  2. promise对象共有3种可能的状态:Pending、Fulfiled、Rejected;
  3. 但promise对象的状态变化只能是以下2种情况之一:
    1. 由Pending变为Fulfiled
    2. 由Pending变为Rejected
      promise对象的状态一旦改变,就不会再变,任何时候都可以得到这个结果;
  4. promise对象一旦创建就会立即执行创建该promise对象时传入的异步函数;

17. for...of遍历

for...of循环只能用于遍历实现了Iterator接口的数据结构;
只要数据结构部署了Symbol.iterator属性,就被视为实现了iterator接口;

for...of遍历的语法:

for (值变量 of 数据结构) {
   循环体
}

示例:

let array = ['我','是','成','员'];

for (let memberValue of array) {
   console.log(value);
}

/*输出结果是:
*我
*是
*成
*员
*/

for...of遍历的机制:

  1. for...of遍历开始;
  2. 调用数据结构的Symbol.iterator方法数据结构[Symbol.iterator]()获取数据结构的迭代器iterator;
  3. 调用迭代器的next()方法iterator.next()获取迭代器的结果iterationResult;
  4. 如果iterationResult的done属性的值为true,则结束for...of遍历;
  5. 如果iterationResult的done属性的值不为true,或者不存在,则将iterationResult.value的值赋给值变量memberValue;
  6. 执行循环体;
  7. 重复步骤3;

18. Iterator接口

由于JavaScript没有描述接口的语法,所以,就用TypeScript语法来描述遍历器接口Iterable、指针对象接口Iterator和next方法的返回值接口IterationResult 的定义,如下:

遍历器接口Iterable的定义:

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

指针对象接口Iterator的定义:


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

next方法的返回值接口IterationResult的定义:

interface IterationResult {
  value: any,
  done: boolean,
}

19. Generator函数

定义语法格式:

function* 函数名(参数){
yield 表达式;
...
yield 表达式;
return 表达式;
}

说明:

  1. 参数不是必须的;
  2. yield语句和return语句都不是必须的;
  3. yield关键字只能用在Generator函数内部,不能用在其它地方;
  4. 如果yield语句如果用在另一个表达式之中,则必须将yield语句用圆括号包住;
  5. 如果yield语句用作函数参数或放在赋值表达式的右边,则可以不用将yield语句用圆括号包住;
  6. 执行Generator函数会返回一个迭代器对象iterator,该迭代器对象iterator有个[Symbol.iterator]属性,该[Symbol.iterator]属性的值为该迭代器对象iterator自身;
  7. yield语句的返回值为调用迭代器的next()方法时传入的参数;如果没有给迭代器的next()方法传入的参数,则yield语句的返回值为undefined;
  8. 编译器会忽略第1次调用迭代器的next()方法时传入的参数;
  9. Generator函数可用于for...of循环;
  10. yield* 表达式会展开表达式的值;
  11. Generator函数总是返回一个遍历器,ES6规定这个遍历器是 Generator函数 的实例,也会继承 Generator函数 的prototype对象;

使用机制:

  1. 调用Generator函数函数名(参数),会返回一个迭代器对象iterator;
  2. 调用迭代器的next()方法iterator.next()执行函数体内未执行的代码;
  3. 如果遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回对象iterationResult(迭代器结果)的value属性值,并给迭代器结果iterationResult的done属性赋值为false;
  4. 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回对象iterationResult(迭代器结果)的value属性值,并给迭代器结果iterationResult的done属性赋值为true;
  5. 如果该函数没有return语句,则返回对象iterationResult(迭代器结果)的value属性值为undefined,并给迭代器结果iterationResult的done属性赋值为true;
  6. 当下一次手动调用next方法时,重复步骤2;

20. async函数

定义语法格式:

async function 函数名(参数){
var 结果 = await 异步表达式;
...
var 结果 = await 异步表达式;
return 表达式;
}

说明:

  1. async函数会返回一个Promise对象;
  2. async函数内部return语句返回的值,会成为then方法回调函数的参数;
  3. async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到;
  4. async函数返回的 Promise 对象,必须等到内部所有await命令后面的 异步表达式执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数;
  5. 正常情况下,await命令后面是一个 Promise 对象。如果不是,会被转成一个立即resolve的 Promise 对象;
  6. await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被传递到async函数会返回的Promise对象;
  7. 只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行;
  8. 若希望await失败后继续执行后面的代码,可以将await放在try...catch结构里面,这样不管这个异步操作是否成功,后面的代码都会执行;
  9. await命令只能用在async函数之中,如果用在普通函数,就会报错;
  10. 目前,@std/esm模块加载器支持顶层await,即await命令可以不放在 async 函数里面,直接使用;

21. class

定义语法格式:

class 类名 extends 父类名 {

}

说明:

  1. 基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已;
  2. 类的数据类型就是函数,类本身就是它的构造方法constructor;
  3. 类的方法都定义在其prototype对象上面;
  4. constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加;
  5. 类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行;
  6. 生成类的实例对象的写法,与 ES5 完全一样,也是使用new命令;
  7. 与函数一样,类也可以使用表达式的形式定义,格式为:var BClass = class AClass {};;注意:这个类的名字是BClass而不是AClass,AClass只在 Class 的内部可用,指代当前类;如果类的内部没用到AClass的话,可以省略AClass,也就是可以写成var BClass = class {};
  8. 因为子类必须在父类之后定义,类不存在声明提升;
  9. 类的name属性总是返回紧跟在class关键字后面的类名;
  10. 如果需要给类定义Generator方法,只需在Generator方法前加上星号*号;
  11. 通过在一个方法前加上static关键字可以定义类的静态方法,静态方案是定义在类上的,通过类来调用,不能在类的实例上访问到;
  12. ES6 明确规定,Class 内部允许定义静态方法,不允许定义静态属性;
  13. 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象;ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this;
  14. 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例;

22. super关键字

  1. super关键字,既可以当作函数,也可以当作对象;
  2. 当把super关键字作为函数调用时,代表父类的构造函数;ES6 要求,子类的构造函数必须执行一次super函数;
  3. 当把super关键字作为对象时,在实例方法中,super表示父类的原型对象;在静态方法中,super表示父类;
  4. ES6 规定,通过super调用父类的实例方法时,super会绑定this为super被调用处的作用域中的this值;
  5. 使用super时,必须显式表明super是作为函数用还是作为对象用,否则会报错;

23. Decorator装饰器

  1. 当装饰器装饰类时,装饰器对类的修改是在代码编译阶段发生的;
  2. 修饰器只能用于类和类的方法,不能直接用于函数(可通过其它方式作用于函数),因为存在函数提升;

24. 模块

模块的特性

  1. ES6的模块自动采用严格模式;
  2. 在ES6模块中,顶层的this的值是undefined,不应该在顶层代码使用this;
  3. export语句输出的接口与其对应的值是动态绑定关系,即通过该接口,可以获取到模块内部实时的值;这一点与CommonJS规范完全不同,CommonJS模块输出的是值的缓存,不存在动态更新;
  4. export 和 import 命令可以并且只能出现在模块顶层的任意位置;如果export 和 import 命令处于块级作用域内,就会报错;这是因为如果export 和 import 命令处于代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷;
  5. 因为:export default命令的本质是:将该命令后面的值,赋给default变量,然后输出一个叫做default的量;
    所以:
    1. 可以直接将一个值写在export default之后;
    2. export default之后不能跟变量声明语句;
  6. import后面的from指定模块文件的位置,可以是相对路径或者绝对路径,.js后缀也可以省略;如果from后面指定的不是路径,只是一个模块名字,那么必须有配置文件能使JavaScript引擎找到该模块的位置;
  7. import命令具有提升效果,会提升到整个模块的顶部,首先执行。这种行为的本质是:import命令是编译阶段执行的,所以它会在所有代码运行之前执行;
  8. 由于import语句是在编译阶段执行,所以import语句中不能包含表达式、变量等只能在运行时才能得到结果的语法结构;
  9. 如果有相同的多条import语句,那么只会执行一次同一条import语句;
  10. 由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错;
  11. 在 Node 环境中,使用import命令加载 CommonJS 模块,Node 会自动将module.exports属性,当作模块的默认输出,即等同于export default;
  12. 采用require命令加载 ES6 模块时,ES6 模块的所有输出接口,会成为输入对象的属性。
  13. ES6模块与CommonJS模块的区别:
  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块。
  1. import()提案:import(modulePath) 函数可以运行时加载内容,也就是说,只有当代码执行到 import() 语句时,才会加载指定的内容;该函数返回一个 Promise 对象;import() 函数可以用在任地方,不仅仅是模块,非模块的脚本也可以使用;与 import 语句不同的是: import() 函数所加载出的值 与 源模块中的值不具有动态绑定关系;注意: import()暂时还只是个提案;

模块导入和导出的各种写法

导出

有以下几种导出方法:

  1. 导出声明:
    直接在(变量、函数、类型、类型别名、接口)声明前添加export关键字;
    示例如下:
export const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
    }
}
  1. 导出语句:
    导出语句可以把需要导出的实例放在一对大括号中一块导出,也可以对导出的部分重新命名;(其实这种写法只是属性的简写语法,详见<属性的简写语法规则>;)
    示例如下:
export const numberRegexp = /^[0-9]+$/;

class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
            }
}

export { ZipCodeValidator,numberRegexp };
export { ZipCodeValidator as mainValidator };
  1. 重新导出:
    我们经常会去扩展其它模块,并且只导出那个模块的部分内容。 重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量;
    示例如下:
// 导出原先的类但做了重命名
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";
  1. 默认导出:
    每个模块都可以有一个default导出。 默认导出使用default关键字标记;并且一个模块只能够有一个default导出。 需要使用一种特殊的导入形式来导入default导出。标记为默认导出的实体可以省略名字;
    示例如下:
export default function (s: string) {
    return s.length === 5 && numberRegexp.test(s);
}

导入

模块的导入操作与导出一样简单。 可以使用以下import形式之一来导入其它模块中的导出内容:

  1. 导入一个模块中的某个导出内容,也可对于进行重命名:(其实这相当于对象的解构)
    格式如下:
import { 要导出的内容的名字 } from "模块路径";
import { 要导出的内容的名字 as 新名字 } from "模块路径";

示例如下:

import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();
  1. 将整个模块导入到一个变量,并通过它来访问模块的导出部分:
    格式如下:
import * as 新名字 from "模块路径";

示例如下:

import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();
  1. 导出默认的导出项:
    每个模块都可以有一个default导出。 默认导出使用default关键字标记;并且一个模块只能够有一个default导出。
    导出默认的导邮项目的格式如下:
import 自定义名字 from "模块路径";

示例如下:

import JQ from "JQuery";

JQ("button.continue").html( "Next Step..." );
  1. 具有副作用的导入模块:
    格式如下:
import "模块路径";

此种导入,相当于把相应模块的代码插入到了本模块;

  1. import()函数:
    注意: import()暂时还只是个提案;
    import() 函数可以运行时加载内容,也就是说,只有当代码执行到 import() 语句时,才会加载指定的内容;该函数返回一个 Promise 对象;import() 函数可以用在任地方,不仅仅是模块,非模块的脚本也可以使用;与 import 语句不同的是: import() 函数所加载出的值 与 源模块中的值不具有动态绑定关系;

语法:

import(路径):Promise

说明:
import() 函数会返回一个 Promis 对象,当模块被加载成功之后,会生成一个模块对象,该模块对象会作为参数传给 Promise 的成功的回调函数;所以也可以用对象解构赋值语法定义成功的回调函数的参数;

示例:

var modulePromise = import("./myModule.js");
modulePromise.then(function(moduleObj){
    moduleObj.export1 ...... ;
    moduleObj.export2 ...... ;
    moduleObj.default ...... ;
});

或者:

var modulePromise = import("./myModule.js");
modulePromise.then(function({export1,export2,default}){
    
});

25. # 严格模式的限制

严格模式主要有以下限制。

  1. 变量必须声明后再使用
  2. 函数的参数不能有同名属性,否则报错
  3. 不能使用with语句
  4. 不能对只读属性赋值,否则报错
  5. 不能使用前缀0表示八进制数,否则报错
  6. 不能删除不可删除的属性,否则报错
  7. 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  8. eval不会在它的外层作用域引入变量
  9. eval和arguments不能被重新赋值
  10. arguments不会自动反映函数参数的变化
  11. 不能使用arguments.callee
  12. 不能使用arguments.caller
  13. 禁止this指向全局对象
  14. 不能使用fn.caller和fn.arguments获取函数调用的堆栈
  15. 增加了保留字(比如protected、static和interface)
JavaScript