基础系列-ECMAScript规范

ECMAScript

ECMA是一个将信息标准化的组织。JavaScript被提交到ECMA进行标准化,由此诞生了一个新的语言标准,也就是我们所知道的ECMAScript。JavaScript是该标准(最流行)的一个实现。

检查各个浏览器中哪些ES6/ES6+特性可用:http://kangax.github.io/compat-table/es6/

can i use: https://caniuse.com/?search=rem

兼容ES6+的插件:babelpolyfillbabel-cli

使用babel编译ES6+代码

npm i babel-cli babel-preset-env -D

babel 编译插件,babel-runtime babel-plugin-transform-runtime 的区别是,相当一前者是手动而后者是自动,每当要转译一个api时都要手动加上 require('babel-runtime')。其中 babel-plugin-transform-runtime 是在 babel-runtime 的基础上运行

npm i -S babel-plugin-transform-runtime babel-runtime

安装后,新建.babelrc配置文件,进行配置

{
    "presets": [
        [
            "env",
            {
                "targets": {
                    "node": "current"
                }
            }
        ]
    ],
    "plugins": [
        [
            "transform-runtime",
            {
                "polyfill": false,
            	"regenerator": true
            } 
        ]
    ]
}

配置当前node环境后,在package.json中新增命令行使用,在此之前需要进行安装nodemonrimraf

"scripts": {
    "dev": "nodemon -w src --exec \"bable-node src --presets env\"",
    "build": "rimraf dist && babel src -s -D -d dist --presets env"
}

npm run dev 自动运行 src 目录下的编译后的index.js文件。npm run build自动将dist文件删除之后,重新编译项目放到dist文件中。

npm install --save-dev babel-core babel-preset-es2015 babel-preset-latest
npm install --save-dev webpack babel-loader

ES6

参考链接:https://262.ecma-international.org/6.0/

ES2015

let与const

  • 在for循环中,如for(let i = 0; i < length; i++)中let的作用域在for循环中,for循环里和循环外都不影响。
  • ES6 允许块级作用域的任意嵌套。外层作用域无法读取内层作用域的变量。内层作用域可以定义外层作用域的同名变量。
  • 块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。
  • 块级作用域与函数声明:允许在块级作用域内声明函数;函数声明类似于var,即会提升到全局作用域或函数作用域的头部;同时,函数声明还会提升到所在的块级作用域的头部。考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立。

变量提升:一个普通块内部的函数声明通常会被提升到所在作用域的顶部

函数声明会被首先提升,然后才是变量。

为什么会变量提升?

如 var a = 2,var a 发生在编译阶段,a = 2 发生在执行阶段。意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。

声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。

变量的解构赋值

  • 数组解构,let [a, b, c] = [1, 2, 3];常规用法,分完全解构不完全解构。等号右边的值必须是可遍历结构。转为对象以后或本身不具备 Iterator 接口都会出错。转为对象以后不具备 Iterator 接口如1, false, NaN, undefind, null,本身不具备 Iterator 接口如{}

  • 默认值,ES6 内部使用严格相等运算符(===),判断一个位置是否有值,因此只有当一个数组成员严格等于undefined,默认值才会生效。如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

    // 默认值为常量
    let [x = 1] = [undefined] // x = 1
    let [x = 1] = [2] // x = 2
    let {x, y = 5} = {x: 1} // x = 1, y = 5
    
    // 默认值为表达式
    function f() {
        console.log('run f()')
    }
    let [x = f()] = [1] // x会等于1,不会进行默认值计算
    console.log(x) // 1
    [x = f()] = [undefined] // run f() 会进行默认值计算
    
    // 嵌套赋值
    let obj = {};
    let arr = [];
    
    ({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true }); // 注意使用() js会将{}认为是代码块所以要用()包裹
    obj // {prop:123}
    arr // [true]
    
    let { p, p: [x, { y }] } = { p: [ 'Hello', { y: 'World' } ] };
    // p = [ 'Hello', { y: 'World' } ], x = 'hello' y = 'World'
  • 可以取到继承的属性,类似于in

  • 其他类型解构。如果等号右边是数值和布尔值,会先转为对象。

应用:
  • 交换变量的值。

    let x = 1, y = 2;
    [x, y] = [y, x]; // x = 2, y = 1
  • 从函数返回多个值。

  • 函数参数的定义。

  • 提取JSON的值。

  • 函数参数的默认值。

  • 遍历 Map 结构,便利取出key, value

  • 输入模块的指定方法

字符串扩展

  • 字符的Unicode表示法,如 '\u0061' 等价于 'a'。但是仅限于码点在 \u0000 ~ \uFFFF 之间的字符。超出这个范围的字符,必须用双字节的形式表示。

    // 表示一个字符
    '\z' === 'z'  // true
    '\172' === 'z' // true
    '\x7A' === 'z' // true
    '\u007A' === 'z' // true
    '\u{7A}' === 'z' // true
  • 字符串的遍历器接口。可以使用 for...of 进行遍历,可以识别大于 0xFFFF 的码点,而传统的 for 循环不能识别。

  • 模板字符串。

    // 1. 基础使用
    const foo = 'xxxx'
    const printTxt = `print: ${foo}`
    const printBool = `print bool: ${ 1 > 0 ? 'yes' : 'no'}`
    
    // 2. 标签模板字符串:可以通过模板字符串的方式对一个函数进行调用
    function test(...args) {
        console.log(args)
    }
    test`aaaa`
    test`print: ${foo}`
    
    let a = 5;
    let b = 10;
    
    tag`Hello ${ a + b } world ${ a * b }`;
    // 等同于
    tag(['Hello ', ' world ', ''], 15, 50);
  • String.raw() 是模板字符串的标签函数,返回给定模板字符串的元素字符串,即返回一个斜杠都被转义的字符串。

    String.raw`Hi\n${2+3}!`;
    // 'Hi\n5!',Hi 后面的字符不是换行符,\ 和 n 是两个不同的字符
    
    String.raw `Hi\u000A!`;
    // "Hi\u000A!",同上,这里得到的会是 \、u、0、0、0、A 6个字符,
    // 任何类型的转义形式都会失效,保留原样输出,不信你试试.length
    
    let name = "Bob";
    String.raw `Hi\n${name}!`;
    // "Hi\nBob!",内插表达式还可以正常运行
    
    
    // 正常情况下,你也许不需要将 String.raw() 当作函数调用。
    // 但是为了模拟 `t${0}e${1}s${2}t` 你可以这样做:
    String.raw({ raw: 'test' }, 0, 1, 2); // 't0e1s2t'
    // 注意这个测试, 传入一个 string, 和一个类似数组的对象
    // 下面这个函数和 `foo${2 + 3}bar${'Java' + 'Script'}baz` 是相等的.
    String.raw({
      raw: ['foo', 'bar', 'baz']
    }, 2 + 3, 'Java' + 'Script'); // 'foo5barJavaScriptbaz'
    
    // Polyfill
    String.raw = function (strings, ...values) {
      let output = '';
      let index;
      for (index = 0; index < values.length; index++) {
        output += strings.raw[index] + values[index];
      }
    
      output += strings.raw[index]
      return output;
    }
  • String.fromCharPoint(),将字符的 Unicode 码点返回对应字符。

    与之相似功能的有ES5的 String.fromCharCode() ,但是不能识别大于 0xFFFF ,ES6刚好弥补了这点。

    String.fromCodePoint(0x20BB7)
    // "𠮷" 
    String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y' // 多个参数合并成一个字符串
    // true
  • String.prototype.codePointAt(pos) ,返回一个 Unicode 码点的非负整数,用于处理码点大于 0xFFFF 的字符,能够正确处理返回一个字符的码点。

    与之相似功能的有ES5的 String.prototype.charCodeAt(pos) 返回给定索引处的 UTF-16 代码单元整数。

    JavaScript 内部,字符以 UTF-16 的格式存储,每个字符固定为 2 个字节,对于需要 4个字节存储的字符,JavaScript会认为是两个字符。

  • String.prototype.normalize([form]) 用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。

  • String.prototype.includes(searchString[, pos]) 返回布尔值,表示是否找到了参数字符串。

  • String.prototype.startsWith(searchString[, pos]) 返回布尔值,表示参数字符串是否在原字符串的头部或从目标位置开始查询。

  • String.prototype.endsWith(searchString[, pos]) 返回布尔值,表示参数字符串是否在原字符串的尾部或从目标位置开始查询。

  • String.prototype.repeat(count) 返回一个新字符串,表示将原字符串重复n次。

正则扩展

  • 构造函数 RegExp()。var regex = new RegExp(/xyz/, 'i'),第二个参数会重置第一个参数设置过的flags。

  • u 修饰符,用来正确处理大于 \uFFFF的 Unicode 字符。

    ES6 中新增了使用大括号表示 Unicode 字符

  • RegExp.prototype.unicode 属性,表示是否设置了 u 修饰符。

  • y 修饰符,全局匹配。每次匹配都是从剩余字符串的第一位开始。

    可以指定 lastIndex 属性,表示开始匹配位。

    // 使用
    var s = 'aaa_aa_a';
    var r1 = /a+/g; // g 修饰符只要剩余位置中存在匹配就可
    var r2 = /a+/y; // y 修饰符确保匹配必须从剩余的第一个位置开始
    
    r1.exec(s) // ["aaa"]
    r2.exec(s) // ["aaa"]
    
    r1.exec(s) // ["aa"]
    r2.exec(s) // null
    
    
    //  y 粘性模式和 matchAll() 互相替换
    // y粘性模式
    const matchYWay = (str) => {
      const reg = /\[[\u4e00-\u9fa5|a-zA-Z]*\]/y
      let match = null
      while (match = reg.exec(str)) {
         console.log(match)
      }
    }
    // matchAll方式
    const matchAllWay = (str) => {
      const reg = /\[[\u4e00-\u9fa5|a-zA-Z]*\]/g
      for (let match of str.matchAll(reg)) {
        console.log(match)
      }
    }

数值扩展

  • 构造函数

函数扩展

箭头函数

箭头函数中的this指向定义箭头函数的作用域,即外层作用域。

箭头函数没有 constructor函数 和 arguments属性。

// 原函数
const arrow = function (param) {}
// 箭头函数
const arrow = (param) => {}
// 只有一个参数
const arrow = param => {}
// 有多个参数
const arraw = (param1, param2) => {}
// 只有一行代码处理传参
const arrow = param => console.log(param)
// 返回一个对象
const arrow = param => ({ param: param })
箭头函数注意点
  • 箭头函数没有自己的 this 对象。this 就是定义时上层作用域中的 this。
  • 不可以当作构造函数,即不可以对箭头函数使用 new 命令,否则会抛出错误。
  • 不可以使用 arguments 对象,该对象在函数体内不存在。如果要使用 rest 参数(形式为...变量名),用于获取函数的多余参数。
  • 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

箭头函数让 this 指向固定化,绑定 this 使得它不再可变。

数组扩展

对象扩展

Symbol

Set & Map

WeakSetWeakMapSetMap 的区别:

  1. WeakSetWeakMap 类没有 entrieskeysvalues等方法;
  2. 只能用对象作为键。

WeakSetWeakMap 是弱化版本,使用这两个类主要是为了性能,使用对象作为键,没有强引用的键。这使得 JavaScript 的垃圾回收器可以从中清除整个入口。必须用键才能取出值。

let x = { id: 1 }
let y = { id: 2 }
let m = new WeakMap()
m.set(x, y)
console.log(m.get(x)) // { id: 2 }
x = null
y = null
console.log(m.get(x)) // undefined
Map vs Object

更多阅读:https://medium.com/front-end-weekly/es6-map-vs-object-what-and-when-b80621932373

  • 如果你知道所有的key,它们都为字符串或整数(或是Symbol类型),你需要一个简单的结构去存储这些数据,Object是一个非常好的选择。构建一个Object并通过知道的特定key获取元素的性能要优于Map(字面量 vs 构造函数,直接获取 vs get()方法)。
  • 如果需要在对象中保持自己独有的逻辑和属性,只能使用 Object 。
  • Map是一个纯哈希结构,而Object不是(它拥有自己的内部逻辑)。使用delete对Object的属性进行删除操作存在很多性能问题。所以,针对于存在大量增删操作的场景,使用Map更合适。
  • 不同于Object,Map会保留所有元素的顺序。Map结构是在基于可迭代的基础上构建的,所以如果考虑到元素迭代或顺序,使用Map更好,它能够确保在所有浏览器中的迭代性能。
  • Map在存储大量数据的场景下表现更好,尤其是在 key 为未知状态,key 值得范围不限于字符串,各种类型的值(包括对象)都可以当作 key。即 Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应。

Proxy

Promise

Iterator 和 for…of循环

for...of循环内部调用的是数据结构的Symbol.iterator方法。

数组原生具备iterator接口(即默认部署了Symbol.iterator属性),for...of循环本质上就是调用这个接口产生的遍历器。

生成器(Generator)

生成器是一类特殊的函数,可以一次或多次启动和停止,并不一定非要完成。

生成器函数是一个特殊的函数。

Class类

this指向: 静态方法中的this指向类,实例方法中的this指向类实例。静态方法不会被继承,因此静态方法中使用的this指向函数调用的是类的静态方法,无法调用类的实例方法。

Class和普通构造函数有何区别

class的本质就是函数,是特殊函数。JavaScript中只有对象。

类的 prototype 属性和__proto__属性
每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。
Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class Point {
    constructor() { // 在 new 时自动执行
        // ...
    }

    toString() {
        // ...
    }

    toValue() {
        // ...
    }
}

// 等同于

Point.prototype = {
    constructor() {},
    toString() {},
    toValue() {},
};

class B {}
const b = new B();

b.constructor === B.prototype.constructor // true

所有实例共享一个原型对象。

//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }

}

var point = new Point(2, 3);

point.toString() // (2, 3)

point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false toString()是原型对象的属性
point.__proto__.hasOwnProperty('toString') // true

var p1 = new Point(2,3);
var p2 = new Point(3,2);

p1.__proto__ === p2.__proto__ // true

__proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。

静态类与实例类

this指向不一样,静态类无法获取到this指向。如果类函数需要使用this,则不能命名为静态类。

类可以不用创建实例就使用静态类,但是实例类必须要类进行实例后才能使用。

class Root {
    constructor() {
        // 当前的this是指对象实例
        this.bar = 'bar'
    }

    set name(name) {
        console.log(name)
    }

    get name() {
        return this.bar
    }

    static foo() { // 静态方法,定义在 class 的对象上 this指对象
        // this.bar => undefined
        console.log('foo')
    }

    test() { // 实例方法,定义在 class 的原型链上
        console.log(this.bar)
    }
}

class Son extends Root {
    constructor() {
        super() // 调用父类的构造器
    }
}

const root = new Root()
root.name = 'root' // root
console.log(root.name) // bar
const son = new Son()
son.name = 'son' // son
son.test() // bar
Root.foo() // foo
Son.foo() // foo

子类继承父类时,想要拥有父类的属性和静态方法,需要在构造函数中调用 super 访问父类,在重写父类定义的函数时可以使用。

getter 和 setter 方法类似于 Object.defineProperty() 的 getter 和 setter。

类表达式
const foo = class {
    
}

const enhance = function () {
    return class extend parentClass { // 最终显示的是父类的名字(react)
        
    }
}

模块化

通过 export 命令显式指定输出的代码,再通过 import 命令输入。

编译时加载(静态加载)

export与import

export 用于规定模块的对外接口

import 用于输入其他模块提供的功能

export 默认导出,import 导入时不用与导出名相同,可以为默认导入指定任一名字

一个模块只能有一个默认导出

默认导出最终导出的是名为 default 的变量

// 导出
const age = 19
export default age
// 导入
import Person from '../xxx'
console.log(Person.age) // 19

export 导出多个数据或对象,导入时可以使用花括号,并且导入名需要和导出名相同,或者使用as进行重命名。

export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

export 语句输出的接口与其对应的值时动态绑定关系,即通过该接口,可以取到模块内部实时的值。

import 命令输入的变量都是只读的,因为它的本质是输入接口。如果导入引用类型,不要轻易改写,容易出现未知问题。

import 命令具有提升效果。

// 导出
export const name = 'luck'
export const getName = () => {
    return name
}
// 或
const name = 'luck'
const getName2 = () => {
    return name
}
export {
	name,
    getName2 as getName // 导出时重命名
}
// 对应的导入方式
import { 
    name, 
    getName as getName2 // 导入时重命名
} from '../xxx'
console.log(name) // luck
// 或 使用整体导入
import * from '../xxx'
console.log(name) // luck
import * as Person from '../xxx'
console.log(Person.name) // luck

export 默认导出和多个数据导出的复合写法

// 导出
export const name = 'luck'
export const getName2 = () => {
    return name
}
export default {
    age: 18
}
// 导入
import Person, { name, getName } from '../xxx'
console.log(Person.age, name) // 18, luck

export 和 import 的复合写法

// 导入 导出 用于做中转(代码规范)
export * from '../xxx' // 整体输出

export { name } from '../xxx' 
// 等同于
import { name } from '../xxx'
export { name }

export { name as firstName } from '../xxx'

// (ES2020)
export * as ns from "mod";

// 等同于
import * as ns from "mod";
export {ns};

一个模块中多个复合写法,最终会合并成一个模块导出

ES6模块化如何使用,开发环境如何打包

ES7

es2016

Array.prototype.includes()

指数操作符(**)

ES8

es2017

Async 函数

对象扩展

字符串填充

函数参数列表结尾允许逗号

SharedArrayBuffer对象

Atomics对象

ES9

es2018

Promise.prototype.finally()

展开运算符

正则表达式

异步遍历器

ES10

es2019

字符串扩展

数组扩展

Symbol扩展

对象扩展

函数扩展

修改catch绑定

ES11

es2020

基本数据类型 BigInt

空置处理 (??)

空值合并操作符(**??**)是一个逻辑操作符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

逻辑或操作符(||不同,逻辑或操作符会在左侧操作数为假值时返回右侧操作数。也就是说,如果使用 || 来为某些变量设置默认值,可能会遇到意料之外的行为。比如为假值(例如,''0)时。

const foo = null ?? 'default string';
console.log(foo); // default string

const baz = 0 ?? 42;
console.log(baz); // 0

不能与 and 或 or 共用,需要用括号进行优先级处理。

可选链(?.)

可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空(nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined

const adventurer = {
    name: 'Alice',
    cat: {
        name: 'Dinah'
    },
	arr: []
};

const dogName = adventurer.dog?.name;
console.log(dogName); // undefined
console.log(adventurer.someNonExistentMethod?.()); // undefined
console.log(adventurer.arr?.[0]) // undefined

Promise.allSettled()

import() 动态导入

动态导入模块,将返回一个promise

静态导入的方式会进行提升,加入动态导入方式,可以实现条件导入

import(`../xxx.js`)
	.then(module => {
        console.log(module)
    })
	.catch(err => {
        console.log(err)
    })

非模块脚本也可以使用,运行时执行,即表示运行到当前位置是导入另一段脚本执行,执行后接着当前内容继续执行

使用场景:

  • 按需加载
  • 条件加载
  • 动态的模块路径

与 require 的区别:require是异步加载,import() 是同步加载(promise 对象)

globalThis

ES12

es2021

字符串扩展

Promise.any()

WeakRefs对象

逻辑运算符和赋值表达式

数字分隔符

参考链接

阮一峰:https://es6.ruanyifeng.com/

掘金:https://juejin.cn/post/6844903811622912014