一年前,系统学习过一次ES6语法,随着时间的推移,记忆有些模糊,加之一年的工作产出了一些沉淀,因此,准备温习一遍,记录一下新的体会,参考书籍为阮一峰老师的ECMAScript 6 入门,感谢🙏
-
在ES5中,var命令会发生”变量提升“现象,即变量可以在声明之前使用,值为undefined,为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错
// var 的情况 console.log(foo); // 输出undefined var foo = 2; // let 的情况 console.log(bar); // 报错ReferenceError let bar = 2;
-
const命令只是限制变量对应的地址不可改变,但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了,如果真的想将对象冻结,应该使用Object.freeze方法
const foo = {}; // 为 foo 添加一个属性,可以成功 foo.prop = 123; foo.prop // 123 // 将 foo 指向另一个对象,就会报错 foo = {}; // TypeError: "foo" is read-only var constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach( (key, i) => { if ( typeof obj[key] === 'object' ) { constantize( obj[key] ); } }); };
-
解构赋值的默认值,只有在原本赋值不存在或者===undefined的情况下,才会使用默认赋值,例如,原本赋值为null,解构赋值也不会采用默认值
let [x, y = 'b'] = ['a']; // x = 'a', y = 'b' let [x, y = 'b'] = ['a', undefined]; // x = 'a', y = 'b' let [x, y = 'b'] = ['a', null]; // x = 'a', y = null
-
js中,可以用’\u0061’表示Unicode码形式,但是,这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示,ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符
"\u0061" // "a" "\uD842\uDFB7" // "𠮷" "\u20BB7" // " 7" "\u{20BB7}" // "𠮷"
-
ES5中提供charCodeAt和String.fromCodeAt来进行码点和对应字符的转换,但是这两个api不能识别 32 位的 UTF-16 字符,ES6新增了codePointAt和String.fromPointAt两个API,需要注意的是,两个API分别是挂在字符串对象和String上的。codePointAt方法的参数,仍然是不正确的。比如,上面代码中,字符a在字符串s的正确位置序号应该是 1,但是必须向codePointAt方法传入 2。解决这个问题的一个办法是使用for…of循环,因为它会正确识别 32 位的 UTF-16 字符
var s = "𠮷a"; s.charCodeAt(0) // 55362 s.charCodeAt(1) // 57271 s.codePointAt(0) // 134071 s.codePointAt(1) // 57271 s.codePointAt(2) // 97 String.fromCharCode(0x20BB7) // "ஷ" String.fromCodePoint(0x20BB7) // "𠮷" let s = '𠮷a'; for (let ch of s) { console.log(ch.codePointAt(0).toString(16)); } // 20bb7 // 61
-
ES6字符串提供一个repeat()方法,将原字符串重复n次
'x'.repeat(3) // "xxx" 'hello'.repeat(2) // "hellohello" 'na'.repeat(0) // "" 'na'.repeat(2.9) // "nana"
-
ES2017引入了字符串补全长度的功能,我个人觉得这个功能非常有用,如果某个字符串不够指定长度,会在头部或尾部补全,padStart()用于头部补全,padEnd()用于尾部补全
'x'.padStart(5, 'ab') // 'ababx' 'x'.padStart(4, 'ab') // 'abax' 'x'.padEnd(5, 'ab') // 'xabab' 'x'.padEnd(4, 'ab') // 'xaba'
-
ES6新增了识别大于\uFFFF字符的方法,对应的,在正则表达式中新增了一个u的flags,含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符,也就是说,会正确处理四个字节的 UTF-16 编码
/^\uD83D/u.test('\uD83D\uDC2A') // false /^\uD83D/.test('\uD83D\uDC2A') // true
-
ES6 还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。 y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义
var s = 'aaa_aa_a'; var r1 = /a+/g; var r2 = /a+/y; r1.exec(s) // ["aaa"] r2.exec(s) // ["aaa"] r1.exec(s) // ["aa"] r2.exec(s) // null
-
正则表达式中,点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符(line terminator character),
ES2018
引入s修饰符,使得.可以匹配任意单个字符 -
正则表达式具名组匹配,在ES5中,正则表达式使用圆括号进行组匹配,组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号(比如matchObj[1])引用,要是组的顺序变了,引用的时候就必须修改序号,
ES2018
引入了具名组匹配,允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用,如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>的写法,类似于\1的写法,这两种写法可以兼容一起使用组名>const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/; const matchObj = RE_DATE.exec('1999-12-31'); const year = matchObj[1]; // 1999 const month = matchObj[2]; // 12 const day = matchObj[3]; // 31 const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; const matchObj = RE_DATE.exec('1999-12-31'); const year = matchObj.groups.year; // 1999 const month = matchObj.groups.month; // 12 const day = matchObj.groups.day; // 31 const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/; RE_TWICE.test('abc!abc!abc') // true RE_TWICE.test('abc!abc!ab') // false
-
ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示,如果要将0b和0o前缀的字符串数值转为十进制,要使用Number方法
0b111110111 === 503 // true 0o767 === 503 // true Number('0b111') // 7 Number('0o10') // 8
-
ES6在Number上挂载了一些全局方法,区别如下
-
Number.isFinite() vs isFinite()
isFinite()会先调用Number(),再判断,Number.isFinite()直接进行判断
isFinite('1') // true Number.isFinite('1') // false
-
Number.isNaN() vs isNaN()
和isFinite()一样,isNaN()会先调用Number(),Number.isNaN()直接进行判断
isNaN('NaN') // true Number.isNaN('NaN') // false
- Number.parseInt() 和 Number.parseFloat()和全局方法完全一样
-
Number.isInteger() 判断是否为整数,数字大于Number.MAX_SAFE_INTEGER或者数字小于Number.MIN_VALUE,会出现精度缺失,由于js中整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值
Number.isInteger(25) // true Number.isInteger(25.0) // true Number.isInteger() // false Number.isInteger(null) // false Number.isInteger('15') // false Number.isInteger(true) // false
- Number.EPSILON 可以认为是js的最小精度
- Number.MAX_SAFE_INTEGER/Number.MIN_SAFE_INGETER代表js的精度范围内的最大整数和最小整数,js的安全范围是-2^53到2^53,不包含两个端点
- Number.isSafeInteger() 数字是否存在于js的整数安全范围内
-
-
ES5中,如果要给函数参数设置默认值,一般是 param ’’ 的方式,这样当传入bool值为false的参数时,都会采用默认值,因此需要先进行undefined的判断,不是非常方便,ES6中可以直接在参数上写默认值,毕竟方便 -
ES6函数默认值与解构赋值结合使用,避免出错
function foo({x, y = 5}) { console.log(x, y); } foo() // TypeError: Cannot read property 'x' of undefined function foo({x, y = 5} = {}) { console.log(x, y); } foo() // undefined 5
-
ES6中为函数的非尾部参数设定默认值的时候,若要使用默认值,需要显示的传入undefined,不然会报错
function f(x = 1, y) { return [x, y]; } f(, 1) // 报错 f(undefined, 1) // [1, 1]
-
尾调用
简单来说,尾调用就是一个函数的最后一步是调用另一个函数,下方对函数m和n的调用都属于尾调用
function f(x) { if (x > 0) { return m(x) } return n(x); }
-
尾调用优化
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
function f() { let m = 1; let n = 2; return g(m + n); } f(); // 等同于 function f() { return g(3); } f(); // 等同于 g(3);
上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧
-
ES6中规定,函数循环调用自己,必须写成尾循环形式,这样只需要保存一个调用帧,否则需要保存非常多的调用帧,容易发生stack overflow尾递归优化
-
ES2017
允许函数的最后一个参数后面加上逗号 -
和上面一样,ES6增加了对4字节unicode的支持,但是ES5中,字符串length和reverse()都不能正确识别4字节unicode,ES6可以通过…运算符将字符串转为数组来正确识别
'x\uD83D\uDE80y'.length // 4 [...'x\uD83D\uDE80y'].length // 3 let str = 'x\uD83D\uDE80y'; str.split('').reverse().join('') // 'y\uDE80\uD83Dx' [...str].reverse().join('') // 'y\uD83D\uDE80x'
-
ES6中新增了Array.from()方法,用于将类数组转为数组,第二个参数类似map方法,操作数组中的元素并且返回,这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法
let spans = document.querySelectorAll('span.name'); // map() let names1 = Array.prototype.map.call(spans, s => s.textContent); // Array.from() let names2 = Array.from(spans, s => s.textContent) Array.from({ length: 2 }, () => 'jack') // ['jack', 'jack']
-
ES6中新增了find()和findIndex()方法,均接受一个函数,返回第一个为true的数组成员和成员index,ES5中的indexOf()方法不能识别NaN,但是findIndex可以借助Object.is()进行操作
[1, 5, 10, 15].find(function(value, index, arr) { return value > 9; }) // 10 [1, 5, 10, 15].findIndex(function(value, index, arr) { return value > 9; }) // 2 [NaN].indexOf(NaN) // -1 [NaN].findIndex(y => Object.is(NaN, y)) // 0
-
ES5中new Array(3)会得到[empty * 3],ES6中数组多了一个fill方法,可以用于初始化空数组,也可以用于抹去数组中现有的元素,fill(num, beginIndex, endIndex)
new Array(3).fill(3) // [3, 3, 3] [1, 2, 3].fill(1, 1, 2) // [1, 1, 3]
-
ES5中在{}中对对象进行赋值只能使用标识符,在ES6中,可以通过表达式来对对象进行赋值,注意,属性名表达式与简洁表示法,不能同时使用,会报错
let propKey = 'foo'; let obj = { [propKey]: true, ['a' + 'bc']: 123 }; // 报错 const foo = 'bar'; const bar = 'abc'; const baz = { [foo] }; // 正确 const foo = 'bar'; const baz = { [foo]: 'abc'};
-
ES5中严格模式NaN === NaN为false,+0 === -0为true,为了区分这种现象,ES6新增了Object.is()方法,用于判断两个参数是否相等
-
ES2018
中将数组中…运算符引入了对象,扩展运算符的解构赋值,不能复制继承自原型对象的属性let o1 = { a: 1 }; let o2 = { b: 2 }; o2.__proto__ = o1; let { ...o3 } = o2; o3 // { b: 2 } o3.a // undefined
-
ES6中的set使用的是Object.is()判断是否相同来去重的,所以5和”5”是不同的,NaN和NaN是相同的,对象都是不同的,毕竟地址不同
-
ES6的set的keys,values返回结果是一样的,其实set自带Iterator属性,可以直接for…of…遍历
-
ES6的map和set一样,对象是不一样的,两个不同地址的对象作为key取到的是不一样的value
-
ES6新增了Symbol这种基本类型,作为唯一标识符,两个Symbol()是不相等的,两个Symbol(‘value’)也是不相等的,如果想要两个Symbol相等,可以使用Symbol.for()方法,这个方法会在全局注册,再次赋值的时候会去全局查找,Symbol.keyFor()方法可以知道Symbol.for()方法的参数
let s = Symbol() typeof s // symbol Symbol() === Symbol() // false Symbol('a') === Symbol('a') // false Symbol.for('a') === Symbol.for('a') // true Symbol.keyFor(Symbol()) // undefined Symbol.keyFor(Symbol.for('s')) // "s"
-
在对象中使用Symbol作为唯一key,要用[]赋值,因为点运算符会被认为是字符串
-
四个操作会忽略enumerable为false的属性
- for…in循环:只遍历对象自身的和继承的可枚举的属性
- Object.keys():返回对象自身的所有可枚举的属性的键名
- JSON.stringify():只串行化对象自身的可枚举的属性
- Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性
-
ES6 一共有 5 种方法可以遍历对象的属性
- for…in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)
- Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名
- Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名
- Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名
- Reflect.ownKeys返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举
- 以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则,首先遍历所有数值键,按照数值升序排列;其次遍历所有字符串键,按照加入时间升序排列;最后遍历所有 Symbol 键,按照加入时间升序排列
-
ES6中Reflect对象上的静态方法
-
Reflect.apply(target, thisArg, args),绑定this对象后执行给定函数
const ages = [11, 33, 12, 54, 18, 96]; // 旧写法 const youngest = Math.min.apply(Math, ages); const oldest = Math.max.apply(Math, ages); const type = Object.prototype.toString.call(youngest); // 新写法 const youngest = Reflect.apply(Math.min, Math, ages); const oldest = Reflect.apply(Math.max, Math, ages); const type = Reflect.apply(Object.prototype.toString, youngest, []);
-
Reflect.construct(target, args),等同于new target(…args),这提供了一种不使用new,来调用构造函数的方法
function Greeting(name) { this.name = name; } // new 的写法 const instance = new Greeting('张三'); // Reflect.construct 的写法 const instance = Reflect.construct(Greeting, ['张三']);
-
Reflect.get(target, name, receiver),查找并返回target对象的name属性,如果没有该属性,则返回undefined,如果name属性部署了读取函数(getter),则读取函数的this绑定receiver
var myObject = { foo: 1, bar: 2, } Reflect.get(myObject, 'foo') // 1 Reflect.get(myObject, 'bar') // 2 var myObject = { foo: 1, bar: 2, get baz() { return this.foo + this.bar; }, }; var myReceiverObject = { foo: 4, bar: 4, }; Reflect.get(myObject, 'baz', myReceiverObject) // 8
-
Reflect.set(target, name, value, receiver),设置target对象的name属性等于value,注意,如果 Proxy 对象和 Reflect 对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver,那么Reflect.set会触发Proxy.defineProperty拦截
let p = { a: 'a' }; let handler = { set(target, key, value, receiver) { console.log('set'); Reflect.set(target, key, value, receiver) }, defineProperty(target, key, attribute) { console.log('defineProperty'); Reflect.defineProperty(target, key, attribute); } }; let obj = new Proxy(p, handler); obj.a = 'A'; // set // defineProperty
- Reflect.defineProperty(target, name, desc),和Object.definePropert用法一样
-
Reflect.deleteProperty(target, name),删除对象属性,类似与delete obj.param
let a = { name: 'snowwinter', } Reflect.deleteProperty(a, 'name')
-
Reflect.has(target, name),判断属性是否存在于对象之中
let a = { name: 'snowwinter', } 'name' in a // true Reflect.has(a, 'name') // true
-
Reflect.ownKeys(target),返回对象所有的键名,包括不可枚举的以及Symbol对象
var myObject = { foo: 1, bar: 2, [Symbol.for('baz')]: 3, [Symbol.for('bing')]: 4, }; // 旧写法 Object.getOwnPropertyNames(myObject) // ['foo', 'bar'] Object.getOwnPropertySymbols(myObject) //[Symbol(baz), Symbol(bing)] // 新写法 Reflect.ownKeys(myObject) // ['foo', 'bar', Symbol(baz), Symbol(bing)]
-
Reflect.isExtensible(target),对应Object.isExtensible,返回一个布尔值,表示当前对象是否可扩展
const myObject = {}; // 旧写法 Object.isExtensible(myObject) // true // 新写法 Reflect.isExtensible(myObject) // true Object.isExtensible(1) // false Reflect.isExtensible(1) // 报错
- Reflect.preventExtensions(target),对应Object.preventExtensions方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功
- Reflect.getOwnPropertyDescriptor(target, name),和Object.getOwnPropertyDescriptor一样
- Reflect.getPrototypeOf(target),和Object.getPrototypeOf一样,用于获取对象的__proto__
- Reflect.setPrototypeOf(target, prototype),和Object.setPrototypeOf一样,同于设置对象的__proto__
-
-
ES2018
中给Promise增加了finally方法,用于指定不管 Promise 对象最后状态如何,都会执行的操作promise .then(result => {···}) .catch(error => {···}) .finally(() => {···});
-
同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 的方法,鉴于这是一个很常见的需求,所以现在有一个提案,提供Promise.try方法
const f = () => console.log('now'); (async () => f())(); console.log('next'); // now // next const f = () => console.log('now'); ( () => new Promise( resolve => resolve(f()) ) )(); console.log('next'); // now // next const f = () => console.log('now'); Promise.try(f); console.log('next'); // now // next
-
ES5和ES6的类方法的可遍历性是不一样的,ES5中,挂在prototype上的方法是可以遍历的,而ES6中写在class中的方法是不可遍历的
class Point { constructor(x, y) { // ... } toString() { // ... } } Object.keys(Point.prototype) // [] Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"] var Point = function (x, y) { // ... }; Point.prototype.toString = function() { // ... }; Object.keys(Point.prototype) // ["toString"] Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"]
-
Object.getPrototypeOf(instance)可以获得实例对应的类的原型,Object.getPrototypeOf(Child)可以获得子类继承的父类
class Parent {} class Child extends Parent {} Child.__proto__ == Parent // true Child.prototype.__proto__ == Parent.prototype // true
-
子类中super用作对象的时候在静态方法之中指向父类,在普通方法之中指向父类的原型对象
-
编程风格的规范
- 在let和const之间,优先使用const
- 函数要返回多个数值的时候,使用对象而不是数组,这样便于解构赋值,不用严格按照顺序
- 对象字面量定义的时候,尽量把所有的key都定义好,就算当前没有使用,也建议先赋null
- 区分Object和Map,只有模拟现实世界的实体对象的时候,才用Object,如果只是需要key:value的数据结构,使用map结构,因为map有内建的遍历机制
-
ES6规定,一个数据结构只要具有Symbol.iterator属性,就可以认为是”可遍历的”(iterable)
const obj = { [Symbol.iterator]: function() { return { next: function () { value: 1, done: true } } } } let arr = ['a', 'b', 'c']; let iter = arr[Symbol.iterator](); iter.next() // { value: 'a', done: false } iter.next() // { value: 'b', done: false } iter.next() // { value: 'c', done: false } iter.next() // { value: undefined, done: true }
-
ES中,对象没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定
let obj = { data: [ 'hello', 'world' ], [Symbol.iterator]() { const self = this; let index = 0; return { next() { if (index < self.data.length) { return { value: self.data[index++], done: false }; } else { return { value: undefined, done: true }; } } }; } };