ECMAScript新语法、特性总结

前言

从2015年的ES6开始,JavaScript的语言标准每年都在更新,其中尤其以ES6的力度之大,到现在ES10已经发布,这里总结一下新语法。

参考:阮一峰 ECMAScript 6 教程 、ECMAScript 6入门 、1.5万字概括ES6全部特性

声明变量

  • const   块级作用域,变量被const声明后不允许改变,通常在声明时定义
  • let  块级作用域

注意点:

变量提升:
var存在变量提升,const、let不存在变量提升,意思是:var声明的变量在声明之前可以访问,访问到的值为undefined;const、let声明的变量在声明之前不可以访问,如果访问直接报错。
暂时性死区:
var tmp = 123;
if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}
看上面这段代码,if内的第一行会报错。ES6明确规定,如果区块中存在let和const命令,这个区块对这些命声明的变量,从一开始就形成了封闭的作用域。凡是在声明之前就是用这些变量,就会报错。在语法上称为“暂时性死区”

解构赋值

ES6中只能对数组进行解构赋值,ES7增加了对象解构赋值的支持(其他类型的不知什么时候支持的)

字符串解构赋值
let [ a, b, c ] = "hello"
// a: "h", b: "e", c: "l"

数组解构赋值
let array= ["one", "two", "three", "four"];
let [a, b, c] = array;    // 同时定义了三个变量,并按照顺序为其赋值

对象解构赋值
let array = { foo: "aaa", bar: "bbb" };
let { foo, bar } = abc;    // 按照 键的名称 赋值给新的变量
let { foo: oof } = abc;    // 将abc中foo的值拿出来,赋给新的变量oof
let { foo, bar, def = "haha" } = abc; // 为变量def设置默认值

函数参数解构赋值
function func([ x = 0, y = 1 ]) {}
function func({ x = 0, y = 1 }) {}

运算符

延展操作符

延展操作是将数组/对象内的所有成员依次拿出来,如果所有成员是值类型,可以看做是深拷贝。

// 数组
let arr1 = [ 'a', 'b', 'c'];
let arr2 = [ 1, 2, 3 ];
let result = [ ...arr1, ...arr2 ]; 
// 对象(注意:如果对象中的属性名相同,会被覆盖)
let smallDog = { name:'小煤球', age: 1 };
let bigDog = { name: 'Python', age: 2 };
let dog = { ...smallDog, ...bigDog };

求幂运算符

ES7 新增,与 Math.pow() 作用相同

const result = x ** 5

可选链操作符

ES2020 新增,当使用某些对象的时候,为了更安全地访问它们的属性,我们使用可选链操作符。

obj.prop?.prop2

在 ES2020 之前,我们通常通过三目运算符或、if 判断 或 && 运算符,现在我们可以使用语法更为简便的可选链操作符

const street = user && user.address && user.address.street;
const address = user ? user.address : null;

const street = user?.address?.street;
const address = user?.address;

如果这个访问链中的某个值不存在,将不会继续访问下去,返回 undefined,否则将是我们期望访问的属性的值,再也不用看到  "Cannot read property 'xxx' of undefined" 这种 error 了!

空位合并操作符

ES2020 新增,由于 JavaScript 是动态类型的,在分配变量时常常需要对 真/假 值的处理,比如:

let person = {
  profile: {
    name: "",
    age: 0
  }
};
console.log(person.profile.name || "Anonymous"); // Anonymous
console.log(person.profile.age || 18); // 18

console.log(person.profile.name ?? "Anonymous"); // ""
console.log(person.profile.age ?? 18); // 0

这时双管道符会将我们认为的有效值覆盖成默认值,这令人不爽。我们可以用 ?? 空位合并操作符来代替它,仅当值为 null 或 undefined 时才允许使用默认值。?? 与 ? 的区别是:前者是对有效值的严格处理,后者是对空缺值的友好处理。

字符串扩展

1.模板字符串

let age = 20;
let str = `我今年${age}岁了`;
let str2 = `想要换行
直接换就可以了
无需添加那么多加号
`;

注意:1.必须是反引号``,不是单引号。 2.大括号内可以进行简单的数值运算(如加减乘除)、逻辑运算(与或非)、函数调用、三目运算。

2.字符串遍历:for - of

let str = "ES6不香吗?";
for (let i of str) {
  console.log(i);  
}

3.标签模板

标签模板是模板字符串与函数调用的结合,来看例子:

let  name = '小白', age = 20;
let message = myTag`我是${name},今年${age}岁了`; // message 最终内容是: "哈哈哈"

function myTag(stringArr, value1, value2) {
    console.log(stringArr);  // ["我是", ",今年", "岁了", raw] 注意:该数组有一个raw属性,保存的是转义后的原字符串
    console.log(value1);     // 小白
    console.log(value2);     // 20
    return "哈哈哈";     // 需要有返回值
}

其实就是调用了字符串处理函数,返回处理后的字符串,这里要注意函数的传参:第一个参数是数组,存放着分割后的字符串片段;后面的参数是模板字符串中的变量。

4.Unicode表示法

大括号包含表示Unicode字符

"\u{20BB7}" // 吉
"\u{41}\u{42}\u{43}" // ABC

5.codePointAt()、fromCodePoint()

在JavaScript内部,字符以UTF-16的形式存储,每个字符固定为2个字节,对于那些需要4个字节存储的字符并不支持,ES6中使用 codePointAt() 方法来存储四字节的字符。

let s = '猿';
console.log(s.codePointAt(0)); // 29503
console.log(String.fromCodePoint(29503)); // 猿

6.其它新增方法:

repeat()        把字符串重复n次,返回新字符串
macthAll()     返回正则表达式在字符串的所有匹配
includes()      检查字符串中是否存在指定的字符(子字符串),返回布尔值
startsWith()   字符串头部是否以 ... 开头
endsWith()     字符串尾部是否以 ... 开头
padStart() / padEnd()  头部补全/尾部补全
    "ab".padStart(5,"dc") 接收两个参数,第一个指定字符串最小长度,第二个用来补全长度的字符串,默认使用空格补全

数字类型的扩展

Number.MAX_SAFE_INTEGER 最大安全数值 和 Number.MIN_SAFE_INTEGER 最小安全数值:

MAX_SAFE_INTEGER 是一个值为 9007199254740991的常量,因为 JavaScript 的 number 类型使用了双精度浮点数数据类型,这一数据类型能够安全存储 -(2^53 - 1) 到 2^53 - 1 之间的数值(包含边界值)

二进制表示法:        0b或0B开头表示二进制(0bXX或0BXX)
八进制表示法:        0o或0O开头表示二进制(0oXX或0OXX)
 Number.EPSILON:        数值最小精度
 Number.MIN_SAFE_INTEGER:最小安全数值(-2^53)
 Number.MAX_SAFE_INTEGER:最大安全数值(2^53)
 Number.parseInt():        返回转换值的整数部分
 Number.parseFloat():    返回转换值的浮点数部分
 Number.isFinite():        是否为有限数值
 Number.isNaN():        是否为NaN
 Number.isInteger():    是否为整数
 Number.isSafeInteger():是否在数值安全范围内
 Math.trunc():        返回数值整数部分
 Math.sign():        返回数值类型(正数1、负数-1、零0)
 Math.cbrt():        返回数值立方根
 Math.clz32():        返回数值的32位无符号整数形式
 Math.imul():        返回两个数值相乘
 Math.fround():    返回数值的32位单精度浮点数形式
 Math.hypot():    返回所有数值平方和的平方根
 Math.expm1():    返回e^n - 1
 Math.log1p():    返回1 + n的自然对数(Math.log(1 + n))
 Math.log10():    返回以10为底的n的对数
 Math.log2():    返回以2为底的n的对数
 Math.sinh():    返回n的双曲正弦
 Math.cosh():    返回n的双曲余弦
 Math.tanh():    返回n的双曲正切
 Math.asinh():    返回n的反双曲正弦
 Math.acosh():    返回n的反双曲余弦
 Math.atanh():    返回n的反双曲正切

BinInt 任意精度整数

JavaScript 第7个原始类型(ES2020),是个任意精度的整数。变量可以表示超过 MAX_SAFE_INTEGER 的数字,并且不失精度。

数组的扩展

1.扩展方法

 Array.from():将类数组、可遍历对象转化为真正的数组。
    类数组对象:包含length的对象、Arguments对象、NodeList对象
    可遍历对象:String、Set结构、Map结构、Generator函数
 Array.of():     转换一组值为真正数组,返回新数组
 includes():  判断数组中是否有某成员
 copyWithin():把指定位置的成员复制到其他位置,返回原数组
 find():           返回第一个符合条件的成员
 findIndex():   返回第一个符合条件的成员索引值
 fill():             根据指定值填充整个数组,返回原数组
 keys():          返回以索引值为遍历器的对象
 values():       返回以属性值为遍历器的对象
 entries():      返回以索引值和属性值为遍历器的对象
 数组空位:       ES6明确将数组空位转为undefined

2.扩展应用

克隆数组:const arr = [...arr1]
合并数组:const arr = [...arr1, ...arr2]
拼接数组:arr.push(...arr1)
代替apply:Math.max.apply(null, [x, y]) => Math.max(...[x, y])
转换字符串为数组:[..."hello"]
转换类数组对象为数组:[...Arguments, ...NodeList]
转换可遍历对象为数组:[...String, ...Set, ...Map, ...Generator]
与数组解构赋值结合:const [x, ...rest/spread] = [1, 2, 3]
计算Unicode字符长度:Array.from("hello").length => [..."hello"].length

函数扩展

1.箭头函数

() => {} 与function关键字声明的函数相比,除了语法上的简洁,this的指向也不同。箭头函数与包围它的代码共享一个this,因为其内部根本没有this。

// 只有一个参数时,括号可以省略
(a) => { console.log(a) }   可以简写为:  a => { console.log(a) }
// 有返回值时,花括号和return可以省略
(a) => { return a }   可以简写为:  a => a
// 返回值为键值对时,外面是圆括号,里面是花括号
(a) => { return { a:"a", b:"b" } }   可以简写为:  a => ({a:"a",b:"b"})

箭头函数注意事项:

函数体内的this是定义时所在的对象而不是使用时所在的对象
不能修改this
不可当作构造函数,因此箭头函数不可使用new命令
不可使用yield命令,因此箭头函数不能用作Generator函数
不可使用Arguments对象,此对象在函数体内不存在(可用rest/spread参数代替)

2.函数参数

默认参数:
function ( a = 1, b = "hello" ) {}
function ( a, b = getValue() ) {}

剩余参数:
let arg1 = "宋小宝", arg2 = "赵薇", arg3 = "王菲", arg4 = "那英";
let logName = ( arg1, ...arg2 ) => {
    console.log(arg1, arg2);
}
logName(arg1, arg2, arg3, arg4)  // 宋小宝 ["赵薇", "王菲", "那英"]

延展参数:
延展参数其实就是在函数调用时,利用延展操作符
myFunc( a, ...b ); // 一次性传了很多参数

3.函数的name属性

返回函数的名称

// 将匿名函数赋值给变量:空字符串(ES5)、变量名(ES6)
const a = () => {};
const b = function(){};
console.log( a.name, b.name );  // 谷歌浏览器上测试(支持ES6语法):"a" "b" ;IE11测试(只支持ES5):undefined

// 将具名函数赋值给变量:函数名(ES5和ES6)
function myFunc(){}
const a = myFunc;
console.log( myFunc.name, a.name );  //  "myFunc"  "myFunc"

// bind返回的函数:bound 函数名(ES5和ES6)
... 不不举例

// Function构造函数返回的函数实例:anonymous(ES5和ES6)
... 不举例

4.尾调用优化

5.块级函数

在代码块中能够声明函数,函数也被称之为块级函数,在严格模式下,块级函数会提升到当前所处代码块的顶部,在整个代码块中能够被访问,在代码块之外的地方就不能被访问。在非严格模式下,块级函数会被提升到全局作用域。

正则扩展

1.构造函数的参数使用规则变更

2.正则方法调用变更

字符串对象的 match() 、replace() 、search() 、split()均可使用正则表达式,在语言内部调用都调用RegExp的实例方法,从而做到所有与正则相关的操作,全部定义在RegExp对象上

String.prototype.match 调用 RegExp.prototype[Symbol.match]
String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
String.prototype.search 调用 RegExp.prototype[Symbol.search]
String.prototype.split 调用 RegExp.prototype[Symbol.split]

3.其它

u 修饰符
y 修饰符
s 修饰符
sticky 属性
flags 属性
后行断言
Unicode 属性类

对象的扩展

详情见 谈论JavaScript对象——个人总结

新增数据类型

1.Symbol

指的是独一无二的值。

const set = Symbol(str) // 创建一个独一无二的值,参数可选
// 原型方法:
Symbol():创建以参数作为描述的Symbol值(不登记在全局环境)
Symbol.for():创建以参数作为描述的Symbol值,如存在此参数则返回原有的Symbol值(先搜索后创建,登记在全局环境)
Symbol.keyFor():返回已登记的Symbol值的描述(只能返回Symbol.for()的key)
Object.getOwnPropertySymbols():返回对象中所有用作属性名的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()的行为
Symbol.split:指向一个函数,当实例对象被String.prototype.split()调用时会重新定义split()的行为
Symbol.iterator:指向一个默认遍历器方法,当实例对象执行for-of时会调用指定的默认遍历器
Symbol.toPrimitive:指向一个函数,当实例对象被转为原始类型的值时会返回此对象对应的原始类型值
Symbol.toStringTag:指向一个函数,当实例对象被Object.prototype.toString()调用时其返回值会出现在toString()返回的字符串之中表示对象的类型
Symbol.unscopables:指向一个对象,指定使用with时哪些属性会被with环境排除

应用场景:

唯一化对象属性名:使用 symbol 可以保证不会与其他属性名产生冲突

遍历属性名:无法通过for - in, for - of, Object.keys()等返回,只能通过 Object.getOwnPropertySymbols() 返回

2.Set

Set

集合,每个成员都唯一且没有重复的值,是一个无序的数据集合。

cosnt set = new Set(array); // 声明一个set变量
属性:size,相当于数组的 length
方法:
add():添加值,返回实例
delete():删除值,返回布尔值
has():检查值,返回布尔值
clear():清除所有成员
keys():返回以属性值为遍历器的对象
values():返回以属性值为遍历器的对象
entries():返回以属性值和属性值为遍历器的对象
forEach():使用回调函数遍历每个成员

应用:

字符串去重:[...new Set(str)],join("")
数组去重:[...new Set(arr)]  或  Array.from(new Set(arr))

注意:

遍历顺序:插入顺序
添加相同对象时,会认为是不同的对象

WeakSet

和Set结构类似,与Set的不同在于:(1) 成员值只能是对象,确切地说是只能存放对象引用,不能存放值类型 (2) WeakSet中存放的值都是弱引用的,即WeakSet通过弱引用存储元素。如果没有其他对象引用该对象,则会从WeakSet中清除该对象。因此,你不能遍历WeakSet对象,也不能获取它的大小。

const a = new WeakSet(arr);
方法:
add():添加值,返回实例
delete():删除值,返回布尔值
has():检查值,返回布尔值

应用场景:

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

3.Map

Map

类似于Object的数据结构,区别是:object的键只能是字符串或数字,map的键可以是任何类型的值。

const map = new Map(); // 创建Map实例
属性:size 返回成员总数
方法:
get():返回键值对
set():添加键值对,返回实例
delete():删除键值对,返回布尔值
has():检查键值对,返回布尔值
clear():清除所有成员
keys():返回以键为遍历器的对象
values():返回以值为遍历器的对象
entries():返回以键和值为遍历器的对象
forEach():使用回调函数遍历每个成员

注意:

遍历顺序:插入顺序
键跟内存地址绑定,只要内存地址不一样就视为两个键
Object结构提供 字符串/数字 —— 值 的对应,Map结构提供 值 —— 值 的对应

WeakMap

与Map结构类似,不同在于:(1) 成员的键只能是对象,值可以是任意的 (2) 键是弱引用的,因此键不可枚举,导致整个结构不可枚举。

get():返回键值对
set():添加键值对,返回实例
delete():删除键值对,返回布尔值
has():检查键值对,返回布尔值

与WeakSet一样,不影响垃圾回收机制。

Class

终于,JavaScript有那么一点像传统的面向对象语言了,与Java类的使用方法甚是相似。class关键字很早就存在于JavaScript中,但一直没真正使用,直到ES6。需要着重强调的是:js中的class并不是类,class关键字的使用并不改变JavaScript基于原型这一本质,它只是构造函数的语法糖,可以看做是构造函数的另一写法。

ES6的class绝大部分功能,都可以通过ES5来实现,class写法能够让对象原型的写法更加清晰、更像面向对象变成的语法。类内的所有方法都定义在prototype属性上面,在类的实例上调用方法,实际上就是调用原型(链)上的方法。

1.constructor方法

构造方法,通过new操作符生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,会默认添加一个空的constructor方法。

该方法会默认返回实例对象(即this),当然你可以完全返回另外一个对象,如果是这样,产生的实例将完全不是class定义的类的实例。

2.getter和setter,取值函数和存值函数

我们可以在类的内部使用 get 和 set 关键字,为某个属性设置拦截器,拦截该属性的存取行为。

class MyClass {
  get hello() {
    return 'getter'; // get函数一定要有返回值
  }
  set hello(value) {  // set函数有参数
    console.log('setter: '+value);
  }
}

在这里,hello属性的取值和存值的行为都被自定义了。set和get函数是设置在 Descriptor 对象上的。

let descriptor = Object.getOwnPropertyDescriptor(MyClass.prototype, "hello");
"get" in descriptor  // true
"set" in descriptor  // true

3.属性表达式

类的属性名(方法名),可以采用表达式

let methodName = 'getArea';

class Square {
  constructor(props) {
    super(props);
  }

  [methodName]() {
    // ...
  }
}

4.class表达式

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

你可能以为上面这段代码没什么,不过是定义了一个类Me,然后为其增加了一个引用,名为MyClass,实则不然。

这个类的名字为Me不假,但是Me只能在类的内部使用,指代当前类。在外部,只能通过MyClass引用。

// 如果类的内部没有用到Me,可以将其省略,类似于匿名函数。
const MyClass = class { /* ... */ };
// 也可以写出立即执行的class,即只生成一个实例
let person = new class {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name);
  }
}('张三');

5.静态方法

通过static关键字修饰的方法都是静态方法。该方法不会被实例继承,而是直接通过类来调用。父类的静态方法可以被子类继承,静态方法也可以从super对象上调用。

如果静态方法内出现 this ,这个 this 指的是类,不是实例。

6.静态属性

静态属性指的是Class本身的属性,即Class.propName,不是定义在实例对象上的属性,也是被 static 关键字修饰的。

class Foo {
  static hello = "a";
}
Foo.hello2 = 1;

7.继承

extends关键字实现继承。需要说明的是,子类的构造函数内一般要使用super关键字,当做方法调用,代表父类的构造函数。另外,super()只能用在子类的构造函数中,用在其他地方就会报错。

注意点:

1.严格模式。类和模块内部,默认就是严格模式,所以不需要'use strict'
2.不存在变量提升。使用在前,定义在后这样的行为,会报错。
3.name属性。本质上,ES6类是对ES5构造函数的一层封装,因此函数许多特性被Class继承,包括name属性
4.Generator方法。在某个方法前面加上 * 号,就表示该方法是一个 Generator 函数
5.async方法,在方法名前面加上async关键字。
5.this的指向。默认指向类的实例,但是如果单数使用该方法,很可能报错。
class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
解决办法是使用箭头函数,或者在构造函数中使用 bind(this)

8.类的私有成员(ES2020)

在变量名前加一个 # 号表示私有成员,只可以在类/实例内部访问。

class Message {
  #message = "Howdy"
   message = "hello"

  greet() { console.log(this.#message) }
}

const greeting = new Message()

greeting.greet() // Howdy
greeting.message // hello
greeting.#message // Private field '#message' must be declared in an enclosing class

Module模块化

import

import { firstName, lastName } from './abc.js';  // 按需引入,逐一加载
import { firstName as name } from './abc.js';  // 将引入的变量重命名
import * as API from './api.js';   // 模块整体加载,命名为API,通过API.xxx使用。
import abc from 'def.js';  // 默认导入,当def.js文件中有export default默认导出时才可以。此时的导入名称 abc 可以任意拟定
import 'lodash';  // 全局引入

注意:

1.import命令引入的变量都是只读的,不能修改,我们也应该将其当成完全只读。

import { a } from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only; 不能修改
a.foo = "hello"; // 允许改写a的属性,但不建议这样做

2.import命令具有提升效果

foo();
import { foo } from 'my_module';

是因为import的执行早于foo的调用。这种行为的本质是,import命令时编译阶段执行的,在代码运行之前。

3.import是静态执行,不能使用表达式和变量

// 报错
import { 'f' + 'oo' } from 'my_module';

// 报错
let module = 'my_module';
import { foo } from module;

// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

export

export const firstName = "Jim";
export { firstName, lastName, year };
export { firstName as name };  // 导出时重命名
export default firstName;  // 默认导出

// 错误的写法:
export 1;
var a = 1;
export a;
// 正确的写法:
export var a = 1;
export {a};
export {b as a};

复合写法

export { foo, bar } from 'my_module';  // 导入的同时导出,在这里做转发
export { foo as myFoo } from 'abc';    // 改名之后导出
export * from 'my_module';   // 整体输出
export { default } from 'foo';  // 默认导出,做转发
export { abc as default } from "./someModule";  // 具名导出改为默认导出
export { default as abc } from './someModule';  // 默认导出改为具名导出

export * as Utils from './utils.js';    // ES2020 新增,以前不可以这样做
它等同于:
import * as utils from './utils.js'
export { utils }

动态导入 Dynamic import

有时候一个页面用到的依赖不多,我们没必要将所有的依赖全都加载在浏览器中,而是需要的时候再加载,可以使用动态 import(即没用到的依赖代码,根本不会被发送到浏览器端,需要用到的时候再通过网络请求获取)。动态导入在 ES2020 正式进入语法标准,在此之前我们需要安装插件/依赖库才能使用。上面的 import/export 与它不同的是,前者是静态编译期执行,在代码执行之前就已经确定,而后者是在程序运行中动态获取,会发送按需请求的代码,不会增加额外的开销。

if  (hello) {
    const module = await import('./dynamicmodule.js');
    module.addNumbers(3, 4, 5);
}

Promise、Generator、Async/await

详情看文章:JavaScript异步

Proxy

详情参考 谈论JavaScript对象——个人总结

Reflect

详情参考 谈论JavaScript对象——个人总结

globalThis

JavaScript可以运行在多个环境中:浏览器、NodeJS、Web Worker。他们各自的全局对象也有所不同,例如浏览器中是 window,NodeJS中是 global,Web Worker中是 self。如果有更多的环境,全局对象也将有所不同,所以在 ES2020 之前我们不得不自己实现检测环境,然后使用正确的全局对象。ES2020 的 globalThis 始终引用全局对象:

globalThis.setTimeout === window.setTimeout // true

.

posted @ 2020-01-04 20:44  学霸初养成  阅读(765)  评论(0编辑  收藏  举报