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 属性类
对象的扩展
新增数据类型
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
.