基础系列-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+的插件:babel
、polyfill
、babel-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中新增命令行使用,在此之前需要进行安装nodemon
和rimraf
包
"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/
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
WeakSet
和 WeakMap
与 Set
和 Map
的区别:
WeakSet
或WeakMap
类没有entries
、keys
和values
等方法;- 只能用对象作为键。
WeakSet
和 WeakMap
是弱化版本,使用这两个类主要是为了性能,使用对象作为键,没有强引用的键。这使得 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
Array.prototype.includes()
指数操作符(**)
ES8
Async 函数
对象扩展
字符串填充
函数参数列表结尾允许逗号
SharedArrayBuffer对象
Atomics对象
ES9
Promise.prototype.finally()
展开运算符
正则表达式
异步遍历器
ES10
字符串扩展
数组扩展
Symbol扩展
对象扩展
函数扩展
修改catch绑定
ES11
基本数据类型 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 对象)