ES2015~ES2019

一、ES6(简要介绍,具体请参考ECMAScript 6 入门

1. 变量声明

a. let: 声明变量,声明的变量具有块级作用域

    经典面试题: 
        //例1
        var arr = [];
        for(var i=0;i<2;i++) {
            arr[i] = function() {
                console.log(i);
            };
        }
        arr[0]();
        arr[1]();

        //例2
        var arr1 = [];
        for(let i=0;i<2;i++) {
            arr[i] = function() {
                console.log(i);
            };
        }
        arr1[0]();
        arr1[1]();

b. const: 声明变量,声明的变量具有块级作用域

c. 区别

2.结构

a. 解构赋值

1) 数组解构
    let [a,b,c] = [1,2,3];
    console.log(a);
    console.log(b);
    console.log(c);
2)对象解构
    //例1
    let person = {name: 'zhangsan', age: 20};
    let {name, age} = person;
    console.log(name);
    console.log(age);

    //例2
    let person1 = {name: 'zhangsan', age: 20};
    let {name: myName, age: myAge} = person1;
    console.log(myName);
    console.log(myAge);
3)参数解构,参数默认值
    //例1
    const consoleLogAttributes = ({ name, attractiveness }) => {
        console.log(name, attractiveness)
    }
    //当不传参是会报错,so
    const consoleLogAttributes = ({ name, attractiveness } = {}) => {
        console.log(name, attractiveness)
    }
    OR
    const consoleLogAttributes = ({ 
        name = 'Default name', 
        attractiveness = '10' 
    } = {}) => {
        console.log(name, attractiveness)
    }

b. 箭头函数

    () => {}
    (a, b) => {return a + b;} /*缩写*/ (a, b) => a + b;
    (a) => {return a;} /*缩写*/ a => a
    //this指向函数定义位置上下文

c. 扩展运算符

    //可利用扩展运算符将维数组转化成真正的数组
    var oDivs = document.getElementsByTagName('div');
    console.log(oDivs);
    var arr = [...oDivs];
    arr.push('a');
    console.log(arr);

d. map,set,promise,Symbol

3. 功能

a. 标记的模板文字

    //例1
    const num = 10;
    const str = `I am the ${number} person to recommend this article.`;

    //例2
    const consoleLogStr = (str) => {
        console.log(str);
    }
    consoleLogStr`I am the string to be logged!`;

    //例3
    function logOutValues(strings, value1, value2) {
        console.log(strings, value1, value2)
    }
    logOutValues`Here is one value: ${1} and two: ${2}. Wow!`;

    //例4
    const person = {
        name: "Scott",
        age: 25
    }
    function experience(strings, name, age) {
        const str0 = strings[0]; // "that "
        const str1 = strings[1]; // " is a "
        let ageStr = 'youngster'; 
        if (age > 99){
            ageStr = 'centenarian';
        }
        return str0 + name + str1 + ageStr;
    }
    const output = experience`that ${ person.name } is a ${ person.age }`;
    console.log(output);

b. 属性值简写

    const name = "Scott";
    const sayName = function() { console.log(this.name) };
    const person = { name, sayName };
    OR
    const person = { name, sayName() { console.log(this.name) } };

c.二进制和八进制字面量

    let oValue = 0o10;
    console.log(oValue); // 8
    
    let bValue = 0b10; // 二进制使用 `0b` 或者 `0B`
    console.log(bValue); // 2

d.for…of 和 for…in

    //例1
    let letters = ['a', 'b', 'c'];
    letters.size = 3;
    for (let letter of letters) {
        console.log(letter);
    }
    //例2
    let stus = ["Sam", "22", "男"];
    for (let stu in stus) {
        console.log(stus[stu]);
    }

e. class 语法、对象超类

    class Phone {
        constructor() {
            console.log("I'm a phone.");
        }
    }
    class MI extends Phone {
        constructor() {
            super();
            console.log("I'm a phone designed by xiaomi");
        }
    }
    let mi8 = new MI();

二、ES2016

1.数组包含性检测方法:Array.prototype.includes

    // 返回true | false,表示包含不包含
    // 可选参数fromIndex表示从该index开始找
    // 默认是`0`,负数表示倒数第几个(array.length + fromIndex)
    array.includes(item, [fromIndex])
    //与array.indexOf(item) !== -1区别
  • NaN:[NaN].includes(NaN) === true而[NaN].indexOf(NaN) === -1
  • 稀疏数组:[1, , 3].includes(undefined) === true而[1, , 3].indexOf(undefined) === -1
简言之,includes能够处理NaN,并且不会跳过稀疏数组中的空位(也就是说不判断数组中的元素是否存在) 具体的,includes比较相等性时采用SameValueZero算法:
  • 对象仅比较引用
  • 基本值比较类型和值
  • 值比较中存在2个特例,+0与-0相等,NaN与NaN相等

2.幂运算符

    // 底数 ** 指数  完全等价于Math.pow(base, exponent)
    base ** exponent

    // 运算并赋值
    let n = 2;
    n **= 4;
    n; // => 16

    NaN ** 0;       // => 1
    Infinity ** 0;  // => 1
    -5 ** NaN;      // => NaN
    NaN ** NaN;     // => NaN

三、ES2017

1. async

2. Shared memory and atomics

算是在多线程并行能力方面的基础建设, 分为2部分:

  • SharedArrayBuffer允许主线程、及WebWorkers之间共享数据
  • Atomic operations(原子操作)用来解决数据同步的问题,如加锁、事务
    // 主线程
    var w = new Worker("myworker.js");
    var sab = new SharedArrayBuffer(1024);  // 1KiB shared memory
    // 同样通过postMessage给worker线程丢过去
    w.postMessage(sab);// worker线程(myworker.js)
    var sab;
    onmessage = function (ev) {
        sab = ev.data;  // 1KiB shared memory, the same memory as in the parent
    }

之前线程之间传递的是值copy,而不是共享引用,现在可以通过SharedArrayBuffer共享同一份数据,并且在worker线程里也可以创建共享数据;
另外,SharedArrayBuffer可以作为ArrayBuffer使用,所以也可以共享TypedArray:

    var sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000); // 100000 primes
    var ia = new Int32Array(sab);  // ia.length == 100000
    var primes = new PrimeGenerator();
    for ( let i=0 ; i < ia.length ; i++ )
    ia[i] = primes.next();
    w.postMessage(ia);

由于数据是多线程共享的,势必面临数据同步的问题,通过Atomics全局对象提供的一些方法来解决:

    // 读
    Atomics.load(typedArray, index)
    // 写
    Atomics.store(typedArray, index, value)
    // 写,返回旧值
    Atomics.exchange(array, index, value)
    // 条件写,仅当旧值等于oldval时才写,返回旧值
    compareExchange(array, index, oldval, newval)
    // 带读写锁的运算(加、减、与、或、异或)
    Atomics.add(array, index, value)
    Atomics.sub(array, index, value)
    Atomics.and(array, index, value)
    Atomics.or(array, index, value)
    Atomics.xor(array, index, value)

这些原子操作不会被打断(not interruptible),在此基础上可以实现:

  • 保证连续读写操作的顺序
  • 避免写操作“丢失”(比如写到脏数据上了)

此外,还允许挂起/唤醒(更友好的线程等待方式,不多占资源):

    Atomics.wait(typedArray, index, value[, timeout])
    Atomics.wake(typedArray, index, count)
    例如:
    // A线程写
    console.log(ia[37]);  // Prints 163
    Atomics.store(ia, 37, 123456);
    Atomics.wake(ia, 37, 1);// B线程等着读
    Atomics.wait(ia, 37, 163);
    console.log(ia[37]);  // Prints 123456

而不需要靠死循环来实现阻塞式等待:

    while (Atomics.load(ia, 37) == 163);
    console.log(ia[37]);  // Prints 123456

3. Object.values/Object.entries

a. Object.values

    // 返回 (1)自身的 (2)可枚举的 (3)非Symbol类型的 属性的值
    Object.values(obj)
    // 实现大致如下
    function values(obj) {
        var vals = [];
        for (var key in obj) {
            if (obj.hasOwnProperty(key) && obj.propertyIsEnumerable(key)) {
                vals.push(obj[key]);
            }
        }
        return vals;
    }

与Object.keys()一致,对属性都有3个限定条件(own && enumerable && non-Symbol-only)。因此,不考虑性能的话,可以更简单的实现:

    function values(obj) {
        return Object.keys(obj).map(key => obj[key]);
    }

b. Object.entries

    // 返回 (1)自身的 (2)可枚举的 (3)非Symbol类型的 属性的键值对儿
    Object.entries(obj)
    // 实现大致如下
    function entries(obj) {
        var entrys = [];
        for (var key in obj) {
            if (obj.hasOwnProperty(obj, key) && obj.propertyIsEnumerable(obj, key)) {
                entrys.push([key, obj[key]]);
            }
        }
        return entrys;
    }; 

除了返回值形式不同以外,与Object.values(obj)一毛一样

应用场景上,Object.entries(obj)可以用来完成mapObject转Map的工作:

    new Map(Object.entries({
        one: 1,
        two: 2,
    }))
    // 输出 Map(2) {"one" => 1, "two" => 2}

枚举性,原型属性与Symbol

  • 枚举性:通过obj.propertyIsEnumerable(key)来检查,下面用enumerable表示可枚举
  • 是不是原型属性:通过obj.hasOwnProperty(key)来检查,下面用own表示仅针对非原型属性
  • 是不是Symbol:通过typeof key === 'symbol'来检查,下面用non-Symbol-only表示仅针对非Symbol类型属性,用Symbol-only表示仅针对Symbol类型属性

JS里围绕对象属性的这3个特点提供了很多工具方法,除了上面提到的Object.keys()、Object.values()、Object.entries()外,还有

  • Object.getOwnPropertyNames(obj):own && non-Symbol-only
  • Object.getOwnPropertySymbols():own && Symbol-only
  • Reflect.ownKeys(obj):own。等价于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

4. Object.getOwnPropertyDescriptors

    // 以对象字典形式返回 (1)自身的 所有属性的描述符
    Object.getOwnPropertyDescriptors(obj)

包括Symbol类型属性与不可枚举属性,例如:

    const obj = {
        [Symbol('foo')]: 123
    };
    Object.defineProperty(obj, 'bar', {
        value: 42,
        enumerable: false
    });
    console.log(Object.getOwnPropertyDescriptors(obj));
    // 输出
    // {
    //   bar: {value: 42, writable: false, enumerable: false, configurable: false},
    //   Symbol(foo): {value: 123, writable: true, enumerable: true, configurable: true}
    // }
    // 而 Object.keys(obj).length === 0

可以通过Reflect.ownKeys(obj)实现

    function getOwnPropertyDescriptors(obj) {
        const result = {};
        for (let key of Reflect.ownKeys(obj)) {
            result[key] = Object.getOwnPropertyDescriptor(obj, key);
        }
        return result;
    }

应用场景上,主要用来完成精细的对象拷贝工作:

    // 连带属性描述符原样搬过去
    function clone(obj) {
        return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
    }
    // 会丢失不可枚举属性以及原描述符
    function copy(obj) {
        return Object.assign({}, obj);
    }

区别如下:

    const obj = {};
    Object.defineProperty(obj, 'bar', {
        value: 42,
        enumerable: false
    });
    Object.defineProperty(obj, 'foo', {
        value: 24,
        enumerable: true,
        writable: false
    });
    Object.getOwnPropertyDescriptors(clone(obj));
    // 属性保持原状
    // bar: {value: 42, writable: false, enumerable: false, configurable: false}
    // foo: {value: 24, writable: false, enumerable: true, configurable: false}
    
    Object.getOwnPropertyDescriptors(copy(obj));
    // 不可枚举的bar丢了,foo的属性描述符被重置回默认了
    // foo: {value: 24, writable: true, enumerable: true, configurable: true}

5. String padding

    // 默认补空格(U+0020)
    '1'.padStart(4) === '1'.padStart(4, ' ')

    //  也可以填充指定串
    '1'.padEnd(4, 0) === '1000'

    // 填充串的长度不限于一个字符,太长会被裁剪掉
    '1'.padEnd(4, 'abcde') === '1abc'

    // 不用补就不补
    '1345'.padStart(2) === '1345'

    '12'.padStart(10, 'YYYY-MM-DD');
    // <- 'YYYY-MM-12'

    '09-12'.padStart(10, 'YYYY-MM-DD');
    // <- 'YYYY-09-12'

6. 形参/实参列表允许有多余逗号

    function foo(
        param1,
        param2, // 形参列表允许有多余逗号
    ) {
        foo(
            'abc',
            'def',  // 实参列表允许有多余逗号
        );
    }

实际上,类似的变动在ES5.1也发生过: 对象字面量键值对儿列表允许有多余逗号

还有语言最初的语法规则:数组字面量允许有多余逗号

特殊的:

    const arr = [1, 2, 3,,,];
    arr.length; // 5

四、ES2018

1. 支持异步数据源迭代

    //同步迭代
    let arr = [1, 2, 3];
    let iter = arr[Symbol.iterator]();
    // 手动遍历
    while (true) {
        let step = iter.next();
        if (step.done) break;
        console.log(step.value);  // 1, 2, 3
    }
    OR
    for (let value of arr) {
        console.log(value); // 1, 2, 3
    }

数据源是异步的,for…of循环就只能拿到一堆Promise

    // 异步数据源
    let arr = [1, 2, 3].map(n => Promise.resolve(n));
    for (let value of arr) {
        console.log(value); // Promise.{<resolved>: 1}...
    }

SO 新增了3个东西:

a. 异步迭代器接口:AsyncIterator

    //同步Iterator接口
    const { value, done } = syncIterator.next();
    //异步AsyncIterator接口 要求next()返回携带着{ value, done }的Promsie
    asyncIterator.next().then(({ value, done }) => /* ... */);

    //例如
    let myObj = {/* ... */};
    // 实现了Symbol.asyncIterator就说明我是可被异步迭代的(async iterable)
    myObj[Symbol.asyncIterator] = () => {
        return {
            next() {
                return Promise.resolve({ value: "more and more...", done: false });
            }
        }
    };
    let asyncIter = myObj[Symbol.asyncIterator]();
    (async () => {
        while (true) {
            let step = await asyncIter.next();
            if (step.done) break;
            console.log(step.value);  // more and more...死循环,无限序列嘛
        }
    })();

b. 异步迭代语句:for-await-of

实现了AsyncIterator接口的,就叫async iterable,就有能通过for-await-of遍历的特权

    // 异步数据源
    let arr = [1, 2, 3].map(n => Promise.resolve(n));
    // 实现AsyncIterator接口
    arr[Symbol.asyncIterator] = () => {
        let i = 0;
        return {
            next() {
                let done = i === arr.length;
                return !done ?
                arr[i++].then(value => ({ value, done })) :
                Promise.resolve({ value: void 0, done: true });
            }
        }
    };(async ()=> {
        for await (const n of arr) {
            console.log(n); // 1, 2, 3
        }
    })();

用起来与同步for…of没太大区别,只是实现AsyncIterator接口有些麻烦,迫切需要一种更方便的方式

P.S.同样,await关键字只能出现在async function里,for-await-of的await也不例外

c. 异步(迭代器的)生成器:async generator functions

就是我们迫切想要的异步迭代器的生成器

    // 异步数据源
    let arr = [1, 2, 3].map(n => Promise.resolve(n));
    // 实现AsyncIterator接口
    arr[Symbol.asyncIterator] = async function*() {
        for (let value of arr) {
            yield value;
        }
    }

async generator返回值本来就是async iterable(隐式实现了AsyncIterator接口),没必要手动实现该接口

    let asyncIterable = async function*() {
        let arr = [1, 2, 3].map(n => Promise.resolve(n));
        for (let value of arr) {
            yield value;
        }
    }();
    //类似于同步版本
    let iterable = function*() {
        let arr = [1, 2, 3];
        for (let value of arr) {
            yield value;
        }
    }();

就具体语法而言,async generator有3个特点

  • 返回async iterable对象,其next、throw、return方法都返回Promise,而不直接返回{ value, done },并且会默认实现Symbol.asyncIterator方法(因此async generator返回async iterable)
  • 函数体中允许出现await、for-await-of语句
  • 同样支持yield\*拼接迭代器
    let asyncIterable = async function*() {
        let arr = [1, 2, 3].map(n => Promise.resolve(n));
        for (let value of arr) {
            yield value;
        }
        // yield*拼接异步迭代器
        yield* (async function*() {
            for (let v of [4, 5, 6]) {
                yield v;
            }
        }());
        // 允许出现await
        let seven = await Promise.resolve(7);
        yield seven;
        // 允许出现for-await-of
        for await (let x of [8, 9]) {
            yield x;
        }
    }();// test
    (async ()=> {
        for await (const n of asyncIterable) {
            console.log(n); // 1, 2, 3...9
        }
    })();

注意一个细节,类似于await nonPromise,for-wait-of也能接受非Promise值(同步值)

P.S.另外,async generator里的yield等价于yield await,具体见Suggestion: Make yield Promise.reject(…)

d. 实现原理

asyncIterator内部维持了一个请求队列,以此保证遍历次序,例如:

    const sleep = (ts) => new Promise((resolve) => setTimeout(resolve, ts));
    let asyncIterable = async function*() {
        yield sleep(3000);
        yield sleep(1000);
    }();
    const now = Date.now();
    const time = () => Date.now() - now;
    asyncIterable.next().then(() => console.log('first then fired at ' + time()));
    asyncIterable.next().then(() => console.log('second then fired at ' + time()));
    // first then fired at 3002
    // second then fired at 4005

第一个next()结果还没完成,立即发起的第二个next(),会被记到队列里,等到前置next()都完成以后,才实际去做

上例相当于:

    let iterable = function*() {
        let first;
        yield first = sleep(3000);
        // 排队,等到前置yield promise都完成以后,才开始
        yield first.then(() => sleep(1000));
    }();
    iterable.next().value.then(() => console.log('first then fired at ' + time()));
    iterable.next().value.then(() => console.log('second then fired at ' + time()));

2. Rest/Spread(…)新增剩余属性和展开属性

a. 剩余属性

    let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
    z;  // { a: 3, b: 4 }

    let complex = {
        x: { a: 1, b: 2, c: 3 }
    };
    let {
        x: { a: xa, ...xbc }
    } = complex;

    // 浅拷贝(不带原型属性)
    let { ...aClone } = a;// 扩展选项参数
    function baseFunction({ a, b }) {
        // ...
    }
    function wrapperFunction({ x, y, ...restConfig }) {
        // do something with x and y
        // pass the rest to the base function
        return baseFunction(restConfig);
    }

解构赋值与剩余属性的差异,看似等价,实则不然

    let { x, y, ...z } = a;
    // is not equivalent to
    let { x, ...n } = a;
    let { y, ...z } = n;

    let a = Object.create({x: 1, y: 2});
    a.z = 3;
    void (() => {
        let { x, y, ...z } = a;
        console.log(x, y, z); // 1 2 {z: 3}
    })();
    void (() => {
        let { x, ...n } = a;
        let { y, ...z } = n;
        console.log(x, y, z); // 1 undefined {z: 3}
    })();

关键区别在于剩余属性只取自身属性,而解构赋值会取自身及原型链上的属性,所以对照组中的y变成undefined了

b. 展开属性

    let n = { x, y, ...z };
    n;  // { x: 1, y: 2, a: 3, b: 4 }

    // 浅拷贝(不带原型属性)
    let aClone = { ...a };
    // 等价于
    let aClone = Object.assign({}, a);// merge多个对象
    let ab = { ...a, ...b };
    // 等价于
    let ab = Object.assign({}, a, b);// 重写属性
    let aWithOverrides = { ...a, x: 1, y: 2 };
    // 或者
    let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
    // 等价于
    let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });// 默认属性
    let aWithDefaults = { x: 1, y: 2, ...a };
    // 等价于
    let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);// 打包-还原
    let assembled = { x: 1, y: 2, a: 3, b: 4 };
    let { x, y, ...z } = assembled;
    let reassembled = { x, y, ...z };
  • 展开属性只触发(待展开对象的)getter,不触发(目标对象的)setter
  • 尝试展开null, undefined不会引发报错,而是忽略掉
    // 拷贝x时会触发getter
    let runtimeError = { ...{a: 1}, ...{ get x() { throw new Error('报错') } } };
    // 重写x时候不触发setter
    let z = { set x(v) { throw new Error('不报错'); }, ...{ x: 1 } }; // No error

3. 正则表达式增强(建议熟悉正则表达式的选手了解)

ES2015增强过一波

  • Unicode mode (the u flag):实际应用见JavaScript emoji utils | 正则表达式中的Unicode
  • sticky mode (the y flag):严格从lastIndex指定的位置开始匹配
  • the RegExp.prototype.flags getter:获取正则表达式对象所开启的模式标识(gimuy按字母序排列,分别表示全局匹配、忽略大小写、多行匹配、Unicode支持与严格模式)
ES2018进一步增强

a. s (dotAll) flag for regular expressions:点号通配模式,在此模式下,点号可以匹配任意字符(默认点号只能匹配除换行符外的任意字符)

不开s模式的话

    /a.c/.test('abc') === true
    /a.c/.test('a\nc') === false
    /a.c/.test('a\rc') === false
    /a.c/.test('a\u2028c') === false
    /a.c/.test('a\u2029c}') === false

要想匹配任意字符的话,只能通过一些技巧绕过,如:

    // [^]匹配一个字符,什么都不排除
    /a[^]c/s.test('a\nc') === true
    // [\s\S]匹配一个字符,任意空白字符和非空白字符
    /a[^]c/s.test('a\nc') === true

开s模式后

    const regex = /a.c/s;
    regex.test('a\nc') === true

另外,还有两个属性用来获取该模式是否已开启:

    regex.dotAll === true
    regex.flags === 's'

注意,点号通配模式(s)并不影响多行匹配模式(m),二者是完全独立的:

s:只影响.(点号)的匹配行为

m:只影响^$的匹配行为

可以一起用,也互不干扰:

    // 不开m时,$匹配串尾
    /^c$/.test('a\nc') === false
    // 开m之后,$能够匹配行尾
    /^c$/m.test('a\nc') === true
    // 同时开sm,各司其职
    /^b./sm.test('a\nb\nc') === true

P.S.m模式术语叫增强的行锚点模式:增强的行锚点模式,把段落分割成逻辑行,使得^和$可以匹配每一行的相应位置,而不是整个串的开始和结束位置

b. RegExp Lookbehind Assertions:肯定逆序环视,支持向后看

正则环视(lookaround)相关的一个特性,环视的特点是不匹配任何字符,只匹配文本中的特定位置

(? <= ...):肯定逆序环视(Positive lookbehind assertions),子表达式能够匹配左侧文本时才成功匹配
(? <! ...):否定逆序环视(Negative lookbehind assertions),子表达式不能匹配左侧文本时才成功匹配

一种向后看的能力,典型应用场景如下:

    // 从'$10.53'提取10.53,即捕获左侧是$符的数值
    '$10.53'.match(/(?<=\$)\d+(\.\d*)?/)[0] === '10.53'
    // 从'$-10.53 $-10 $0.53'提取正值0.53,即捕获左侧不是负号的数值
    '$-10.53 $-10 $0.53'.match(/(?<=\$)(?<!-)\d+(\.\d*)?/g)[0] === '0.53'

向前看的能力一直都有,例如:

    // (?=…) 肯定顺序环视,    子表达式能够匹配右侧文本
    'baaabac'.match(/(?=(a+))a*b\1/)[0] === 'aba'
    // (?!…) 否定顺序环视,子表达式不能匹配右侧文本
    'testRegexp test-feature tesla'.match(/(?<=\s)(?!test-?)\w+/g)[0] === 'tesla'
逆序环视与反向引用

实现上,含逆序环视的正则表达式的匹配顺序是从右向左的,例如:

    // 逆序环视,从右向左扫描输入串,所以$2贪婪匹配到了053
    '1053'.replace(/(?<=(\d+))(\d+)$/, '[$1,$2]') === '1[1,053]'
    // 一般情况,从左向右扫描输入串,贪婪匹配$1为105
    '1053'.replace(/^(\d+)(\d+)/, '[$1,$2]') === '[105,3]'
    //从上例能够发现另一个细节:虽然扫描顺序相反,但捕获分组排序都是从左向右的

此外,逆序环视场景下反向扫描对反向引用有影响,毕竟只能引用已匹配过的内容,所以要想匹配叠词的话,应该这样做

    /(?<=\1(.))/.test('哈哈') === true
    OR NOT
    /(?<=(.)\1)/.test('哈8') === true

实际上,这里的\1什么都匹配不到,永远是空串(因为从右向左扫,还没捕获哪来的引用),删掉它也没关系(/(?<=(.))/)

c. RegExp named capture groups:命名捕获分组

常见的日期格式转换场景:

    '2017-01-25'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1/$2/$3') === '2017/01/25'

我们通过$n来引用对应的捕获到的内容,存在两个问题

  • 可读性:$n仅表示第几个捕获分组,不含其它语义
  • 灵活性:一旦正则表达式中括号顺序发生变化,replacement($1/$2/$3)要跟着变

命名捕获分组能够很好的解决这两个问题:

    const reDate = /(?<yyyy>\d{4})-(?<mm>\d{2})-(?<dd>\d{2})/;
    '2017-01-25'.replace(reDate, '$<yyyy>/$<mm>/$<dd>') === '2017/01/25'

正则表达式中的捕获分组与replacement中的引用都有了额外语义

另外,匹配结果对象身上也有一份命名捕获内容:

    let result = reDate.exec('2017-01-25');
    const { yyyy, mm, dd } = result.groups;
    OR
    // const { groups: {yyyy, mm, dd} } = result;
    `${yyyy}/${mm}/${dd}` === '2017/01/25'

从语法上看,引入了3个新东西:

(?<name>...):命名捕获型括号
k<name>:命名反向引用
$<name>:命名replacement引用,函数形式的replacement把groups作为最后一个参数,具体见Replacement targets

d. RegExp Unicode Property Escapes:Unicode(序列)属性转义

Unicode字符有一些属性,比如π是希腊文字,在Unicode中对应的属性是Script=Greek

为了支持根据Unicode属性特征匹配字符的场景,提供了两种语法:

  • \p{UnicodePropertyName=UnicodePropertyValue}:匹配一个Unicode属性名等于指定属性值的字符
  • \p{LoneUnicodePropertyNameOrValue}:匹配一个该Unicode属性值为true的字符
P.S.对应的\P表示补集

注意,都要开u模式,不开不认

前者适用于非布尔值(non-binary)属性,后者用于布尔值(binary)属性,例如:

    const regexGreekSymbol = /\p{Script=Greek}/u;
    regexGreekSymbol.test('π') === true
    // Unicode数字
    /\p{Number}{2}/u.test('罗马数字和带圈数字Ⅵ㉜') === true
    // Unicode版\d
    /^\p{Decimal_Number}+$/u.test('') === true

4. Promise.prototype.finally

Pending的Promise要么Resolved要么Rejected,而有些时候需要的是Resolved || Rejected,比如只想等到异步操作结束,不论成功失败,此时Promise.prototype.finally就是最合适的解决方案

    fetch('http://www.example.com').finally(() => {
        // 请求回来了(不论成功失败),隐藏loading
        document.querySelector('#loading').classList.add('hide');
    });

可以在finally块里做一些清理工作(类似于try-catch-finally的finally),比如隐藏loading、关闭文件描述符、log记录操作已完成

之前类似的场景一般通过then(f, f)来解决,但finally的特点在于:

  • 没有参数(专职清理,不关心参数)
  • 没有参数(专职清理,不关心参数)
  • 不影响Promise链的状态及结果(而then(() => {}, () => {})会得到Resolved undefined),除非finally块里的throw或者return rejectedPromise会让Promise链变为Rejected error
    //例如
    Promise.resolve(1)
    .finally(() => 2)
    .finally((x) => new Promise((resolve) => {
    setTimeout(() => {
        resolve(x+1)
    }, 3000);
    }))
    .then(
    // 3秒后,log 1
    res => console.log(res)
    )

Resolved 1始终没被改变,因为从设计上不希望finally影响返回值

其中,returning a value early指的是返回Rejected Promise,例如:

    Promise.resolve(1)
    // returning a value early
    .finally(() => Promise.reject(2))
    .catch(ex => console.log(ex))
    .finally(() => {
        // throwing an exception
        throw 3;
    })
    .catch(ex => console.log(ex))

5. 对标签模板可以解析不合法的转义序列

模板字符串默认识别(尝试去匹配解释)其中的转义字符

  • \u:Unicode字符序列,如\u00FF或\u{42}
  • \x:十六进制数值,如\xFF
  • \0:八进制,如\101,具体见Octal escape sequences

P.S.实际上,八进制转义序列在模板字面量和严格模式下的字符串字面量都是不合法的

对于不合法的转义序列,会报错:

    // Uncaught SyntaxError: Invalid Unicode escape sequence
    `\uZZZ`
    // Uncaught SyntaxError: Invalid hexadecimal escape sequence
    `\xxyz`
    // Uncaught SyntaxError: Octal escape sequences are not allowed in template strings.
    `\0999`
    // 更容易出现的巧合
    `windowsPath = c:\usrs\xxx\projects`

但是,模板字符串作为ES2015最开放的特性:

标签模板以开放的姿态欢迎库设计者们来创建强有力领域特定语言。这些语言可能看起来不像JS,但是它们仍可以无缝嵌入到JS中并与JS的其它语言特性智能交互。我不知道这一特性将会带领们走向何方,但它蕴藏着无限的可能性,这令我感到异常兴奋!

这种粗暴的默认解析实际上限制了模板字符串的包容能力,例如latex:

    let latexDocument = `
    \newcommand{\fun}{\textbf{Fun!}}  // works just fine
    \newcommand{\unicode}{\textbf{Unicode!}} // Illegal token!
    \newcommand{\xerxes}{\textbf{King!}} // Illegal token!Breve over the h goes \u{h}ere // Illegal token!
    `

这是一段合法的latex源码,但其中的\unicode、\xerxes和\u{h}ere会引发报错

针对这个问题,ES2018决定对标签模板去掉这层默认解析,把处理非法转义序列的工作抛到上层

    //例如:
    function tag(strs) {
    // 解析过的,存在非法转义序列就是undefined
    strs[0] === undefined
    // 裸的,与输入完全一致
    strs.raw[0] === "\\unicode and \\u{55}";
    }
    tag`\unicode and \u{55}`

注意,这个特性仅针对标签模板,普通模板字符串仍然保留之前的行为(遇到非法转义序列会报错):

    let bad = `bad escape sequence: \unicode`; // throws early error

五、ES2019

1. 可选的 catch 绑定

可选的 catch 绑定提案是为了能够选择性地移除使用不到的 catch 绑定。

    try {} catch(err) {}
    //现在可以删除使用不到的绑定。
    try {  ...} catch {  ...}   

2. JSON 超集

这个提案的目的是让 JSON 字符串可以包含未转义的 U+2028 LINE SEPARATOR 和 U+2029 PARAGRAPH SEPARATOR 字符,而 ECMAScript 字符串是不能包含这些字符的。在 ES2019 生效之前,这样做会出现“SyntaxError: Invalid or unexpected token”错误。

    const LS = eval('"\u2028"');
    const PS = eval("'\u2029'");

3. 符号描述

符号是在 ES2015 中引入的,具有非常独特的功能。在 ES2019 中可以提供给定的描述,目的是避免间接从 Symbol.prototype.toString 获取描述。

    const mySymbol = Symbol('myDescription');
    console.log(mySymbol);
    // Symbol(myDescription)

    console.log(mySymbol.toString()); 
    // Symbol(myDescription)

    console.log(mySymbol.description); 
    // myDescription

4. 修订版的 Function.prototype.toString

之前的函数原型已经有 toString 方法,但是在 ES2019 中,它经过了修订,可以包含函数内的注释,不过不适应于箭头函数。

    function foo (){} 
    /* Before */
    console.log(foo.toString()); 
    // function foo(){} 

    /* Now ES2019 */
    console.log(foo.toString()); 
    // function foo (){} 

    /* Arrow Syntaxconst */
    bar = () => {} 
    console.log(bar.toString()); 
    // () => {}

5. Object.fromEntries

Object.entries 方法的反向操作,可用于克隆对象。

    const obj = {    
        prop1: 1,    
        prop2: 2,
    }; 
    const entries = Object.entries(obj);
    console.log(entries); 
    // [ [ 'prop1', 1 ], [ 'prop2', 2 ] ]

    const fromEntries = Object.fromEntries(entries);
    console.log(fromEntries); 
    // Object { prop1: 1, prop2: 2 }

    console.log(obj === fromEntries); 
    // false

不过需要注意的是,嵌入式对象 / 数组都只是引用

    const obj = { 
        prop1: 1, 
        prop2: 2,    
        deepCopy: {        
            mutateMe: true    
        }
    }; 
    const entries = Object.entries(obj);
    const fromEntries = Object.fromEntries(entries);
    fromEntries.deepCopy.mutateMe = false;
    console.log(obj.deepCopy.mutateMe); 
    // false

6. 格式化的 JSON.stringify

使用 JSON 转义序列表示输出结果,而不是返回 UTF-16 代码单元。

    /* Before */
    console.log(JSON.stringify('\uD800')); 
    // "�" 

    /* Now ES2019 */
    console.log(JSON.stringify('\uD800')); 
    // "\ud800"

7. String.prototype 的 trimStart 和 trimEnd

用来移除字符串开头和结尾的空格

    /* Trim */
    const name = "   Codedam ";
    console.log(name.trim());       
    // "Codedam"

    /* Trim Start */
    const description = "   Unlocks Secret Codes ";
    console.log(description.trimStart());   
    // "Unlocks Secret Codes "

    /* Trim End */
    const category = "  JavaScript ";
    console.log(category.trimEnd());    
    // "  JavaScript"

8. Array.prototype 的 flat 和 flatMap

a. flat 方法

通过将所有子数组元素以递归方式连接到指定的深度来创建数组。默认深度为 1,使数组的第一层嵌套展平。

    var arr = [1,2,3,[4,5,6,[7,8,9,[1,2,3]]]];
    console.log(arr.flat());
    console.log(3,arr.flat(3));
    console.log(2,arr.flat(2));
    console.log(1,arr.flat(1))
    //(7) [1, 2, 3, 4, 5, 6, Array(4)]
    //3 (12) [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3]
    //2 (10) [1, 2, 3, 4, 5, 6, 7, 8, 9, Array(3)]
    //1 (7) [1, 2, 3, 4, 5, 6, Array(4)]

    // You can use Infinity to flatten all the nested arrays no matter how deep the array is 
    const arrExtreme = [1, [2, [3, [4, [5, 6, 7, [8, 9]]]]]];
    arrExtreme.flat(Infinity); 
    // [1, 2, 3, 4, 5, 6, 7, 8, 9]

b. flatMap 方法

类似于 flat,并且还与 map 相关,它会先映射数组然后将其展平。

    const arr = ['Codedam', 'is Awsome', '!']; 
    const mapResult = arr.map(item => item.split(' '));
    console.log(mapResult);
    // [ [ 'Codedam' ], [ 'is', 'Awsome' ], [ '!' ] ] 

    const flatMapResult = arr.flatMap(chunk => chunk.split(' '));
    console.log(flatMapResult);
    // ['Codedam', 'is',  'Awsome', '!'];

六、其他属于第三阶段的特性

1. globalThis

2. BigInt

3. import()

4. 遗留的 RegEx

5. 私有的实例方法

6. String.prototype.matchAll

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值