JavaScript 高级进阶 ES6~ES13 详解

文章目录

        • [1. ES6 - 对象字面量增强](#1. ES6 - 对象字面量增强)
        • [2. ES6 - 对象和数组的解构](#2. ES6 - 对象和数组的解构)
        • [3. ES6 - let / const 的基本使用](#3. ES6 - let / const 的基本使用)
        • [4. ES6 - let / const 的作用域提升](#4. ES6 - let / const 的作用域提升)
        • [5. ES6 - let / const 和 window 的关系](#5. ES6 - let / const 和 window 的关系)
        • [6. ES6 - 新增的块级作用域](#6. ES6 - 新增的块级作用域)
        • [7. ES6 - 模板字符串的使用](#7. ES6 - 模板字符串的使用)
        • [8. ES6 - 标签模板字符串](#8. ES6 - 标签模板字符串)
        • [9. ES6 - 函数参数的默认值](#9. ES6 - 函数参数的默认值)
        • [10. ES6 - 函数的剩余参数](#10. ES6 - 函数的剩余参数)
        • [11. ES6 - 箭头函数的补充](#11. ES6 - 箭头函数的补充)
        • [12. ES6 - 展开语法的使用](#12. ES6 - 展开语法的使用)
        • [13. ES6 - 二进制和八进制连接符](#13. ES6 - 二进制和八进制连接符)
        • [14. ES6 - Symbol 数据类型的使用](#14. ES6 - Symbol 数据类型的使用)
        • [15. ES6 - 数据结构 Set(元素不可重复)](#15. ES6 - 数据结构 Set(元素不可重复))
        • [16. ES6 - 数据结构 WeakSet(存放对象)](#16. ES6 - 数据结构 WeakSet(存放对象))
        • [17. ES6 - 数据结构 Map(支持对象属性 key)](#17. ES6 - 数据结构 Map(支持对象属性 key))
        • [18. ES6 - 数据结构 WeakMap(key只能是对象)](#18. ES6 - 数据结构 WeakMap(key只能是对象))
        • [19. ES7 - Array.includes 方法和指数运算符](#19. ES7 - Array.includes 方法和指数运算符)
        • [20. ES8 - Object.values、Object.entries 方法](#20. ES8 - Object.values、Object.entries 方法)
        • [21. ES8 - String.padStart、String.padEnd 方法](#21. ES8 - String.padStart、String.padEnd 方法)
        • [22. ES10 - Array.flat、Array.flatMap 方法](#22. ES10 - Array.flat、Array.flatMap 方法)
        • [23. ES10 - Object.fromEntries、String.trimStart 方法](#23. ES10 - Object.fromEntries、String.trimStart 方法)
        • [24. ES11 - 大整数类型 Bigint](#24. ES11 - 大整数类型 Bigint)
        • [25. ES11 - 空值合并运算符 ??](#25. ES11 - 空值合并运算符 ??)
        • [26. ES11 - 可选链操作符 ?.](#26. ES11 - 可选链操作符 ?.)
        • [27. ES11 - globalThis 和 for...in 标准化](#27. ES11 - globalThis 和 for...in 标准化)
        • [28. ES12 - FinalizationRegistry 执行清理回调](#28. ES12 - FinalizationRegistry 执行清理回调)
        • [29. ES12 - WeakRef 创建对象的弱引用](#29. ES12 - WeakRef 创建对象的弱引用)
        • [30. ES12 - 逻辑赋值运算符、String.replaceAll()](#30. ES12 - 逻辑赋值运算符、String.replaceAll())
        • [31. ES13 - Array/String.at()、Object.hasOwn()](#31. ES13 - Array/String.at()、Object.hasOwn())
        • [32. ES13 - 类中新增的成员(私有化成员)](#32. ES13 - 类中新增的成员(私有化成员))
1. ES6 - 对象字面量增强

对象字面量的增强写法主要体现在以下几个方面,以提高代码的可读性、可维护性和功能性:

  • 属性简写:当属性名和变量名相同时,可以省略属性名,直接写变量名
  • 方法简写:可以省略 function 关键字,直接写方法名
  • 计算属性名:使用表达式的结果作为对象属性的键名

当前有多个变量,想要把它们放入到对象中,并且在对象中定义一个方法,在 ES6 之前是这样写的:

javascript 复制代码
const name = 'liang'
const age = 20
const object = {
    name: name,
    age: age,
    foo: function () { }
}

属性简写和方法简写的使用示例:

javascript 复制代码
const name = 'liang'
const age = 20
const object = {
    // property shorthand (属性简写)
    name,
    age,
    // method shorthand (属性简写)
    foo() {

    }
}

可以使用箭头函数作为方法,这样可以更简洁地定义函数,但是要注意箭头函数中的 this 指向

javascript 复制代码
const object = {
    foo() {
        console.log(this) // { foo: ƒ, bar: ƒ }
    },
    bar: () => {
        console.log(this) // Window { }
    }
}

计算属性名:[] 是计算属性名的语法,使用表达式的结果作为对象属性的键名

javascript 复制代码
const name = 'liang'
const object = {
    // ES6 计算属性名,等价于 liang456
    // 它告诉 JavaScript 引擎,属性名不是直接写死的标识符或字符串,而是要计算 [] 内部表达式的结果
    [name + 456]() {
        console.log('ES6 的计算属性写法');
    }
}
object[name + '123'] = function () {
    console.log('ES6 之前的计算属性写法');
}
object.liang123()
object.liang456()
2. ES6 - 对象和数组的解构

ES6 解构赋值是一种强大的语法特性,它允许你从数组或对象中提取数据,并将其赋值给变量

这种语法使得从复杂的数据结构中提取数据变得更加简洁和直观,这个语法非常重要,请移步:解构赋值用法详解

javascript 复制代码
// 现有一个数组或对象,要将里面的多个值取出来
const arr = ['html', 'css', 'js']

// 在 ES6 之前,是这样获取的
const item1 = arr[0]
const item2 = arr[1]
const item3 = arr[2]

// ES6 新增解构赋值,可以这样获取
const [value1, value2, value3] = arr

// 对象的解构赋值
const { name, age } = { name: 'liang', age: 20 }
3. ES6 - let / const 的基本使用

在 ES5 中声明变量都是使用的 var 关键字,从 ES6 开始新增了两个关键字可以声明变量:letconst

javascript 复制代码
// variable 变量
var a = 1
let b = 2
// constant 常量
const c = 3

通过 letconst 声明的变量不可以重复定义

javascript 复制代码
var a = 1
var a = 1

let b = 2
// let b = 2 // Uncaught SyntaxError: Identifier 'b' has already been declared

const c = 3
// const c = 3 // Uncaught SyntaxError: Identifier 'c' has already been declared

const 本质上是传递的值不可以修改,如果传递的是引用类型,可以通过引用找到对象,去修改对象内部的属性

javascript 复制代码
// const 本质上是传递的值不可以修改
// 但是,如果传递的是一个引用类型(内存地址),可以通过引用找到对应的对象,去修改对象内部的属性
const obj = { name: 'liang' }
obj.name = 'hello'
4. ES6 - let / const 的作用域提升

let、const 和 var 的另一个重要区别是作用域提升:

  • var 声明的变量是会进行作用域提升的,在声明之前使用该变量不会报错,只是没有值
  • 使用 let、const 声明的变量,在声明之前使用该变量会报错
javascript 复制代码
console.log(a) // undefined
var a = 1
console.log(b) // Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 1

那么是不是意味着 letconst 声明的变量只有在代码执行阶段才会创建呢 ?(很多人以为是没有被创建的)

  • 事实上并不是这样的,可以看一下 ECMA262 对 let 和 const 的描述:
  • 这些变量会被创建在包含它们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值

对作用域提升的理解:

  • 在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么可以称之为作用域提升
  • 在这里,它虽然被创建出来了,但是不能被访问,不能称之为作用域提升

暂时性死区:

  • 它表达的意思是在一个代码中,使用 let/const 声明的变量,在声明之前变量都是不可访问的
  • 我们将这种现象称之为暂时性死区
5. ES6 - let / const 和 window 的关系

Window 对象添加属性:

  • 全局通过 var 声明的变量,会被添加到 window 对象上,作为 window 对象的属性
  • let、const 是不会给 window 上添加任何属性的
javascript 复制代码
var name = 'liang'
console.log(window.name) // liang
let title = 'vue'
console.log(window.title) // undefined
6. ES6 - 新增的块级作用域

在 ES5 中只有两个东西会形成作用域:全局作用域、函数作用域

javascript 复制代码
// 全局作用域
var name = 'liang'
// 函数作用域
function foo() {
    var bar = 'function'
}

块代码的结构:

javascript 复制代码
// {} 块代码(block code)
{
    var a = 1
}
// 这里的 {} 不是块代码,这是对象字面量
var obj = { name: 'liang' }

在 ES6 中新增了块级作用域,并且通过 let、const、function、class 声明的标识符是具备块级作用域限制的

我们会发现函数拥有块级作用域,但是在外面依然是可以访问的

  • 这是因为引擎会对函数的声明进行特殊的处理,允许像 var 那样进行提升
javascript 复制代码
{
    var a = 1
    let foo = 'liang'
    function demo() {
        console.log('demo')
    }
}
console.log(a) // 1(对 var 声明的变量是无效的,所以这里也能正常输出)
// console.log(foo) // Uncaught ReferenceError: foo is not defined

// 不同的浏览器有不同的实现(大部分浏览器为了兼容以前的代码,让 function 没有块级作用域)
demo() // 此时可以正常调用(如果使用只支持 ES6 的浏览器,是无法访问的)

ifswitchfor 语句都是块级作用域

javascript 复制代码
// if 语句的代码就是块级作用域
if (true) {
    let foo = 'foo'
}
console.log(foo) // Uncaught ReferenceError: foo is not defined

// switch 语句的代码也是块级作用域
let color = 'red'
switch (color) {
    case 'red':
        var bar = 'bar'
        let bgColor = '#000'
}
console.log(bar)
console.log(bgColor) // Uncaught ReferenceError: bgColor is not defined

// for 语句的代码也是块级作用域(这个场景用的最多,强烈推荐使用 let)
for (var i = 0; i < 5; i++) { }
console.log(i) // 5
for (let j = 0; j < 5; j++) { }
console.log(j) // Uncaught ReferenceError: j is not defined

我们来看一下 for 循环语句中,起始变量使用 var 声明的导致的问题:无论点击哪个按钮输出结果都是一样的

html 复制代码
<button>按钮</button>
<button>按钮</button>
<button>按钮</button>
javascript 复制代码
const btns = document.getElementsByTagName('button')
// 特别注意:这里是 var 声明的起始变量 i
for (var i = 0; i < btns.length; i++) {
    btns[i].onclick = function () {
        console.log('第' + i + '个按钮被点击') // 第3个按钮被点击
    }
}

这是因为 var 声明的变量 i 是全局作用域,函数中没有变量 i 就会往上级作用域找,循环结束后 i 就已经变为 3

学习过块级作用域后,只需要将 i 的声明从 var 改为 let 即可,这就是块级作用域给我们带来的好处

javascript 复制代码
// 使用 let 声明起始变量 i,使其形成块级作用域
for (let i = 0; i < btns.length; i++) {
    btns[i].onclick = function () {
        console.log('第' + i + '个按钮被点击')
    }
}
7. ES6 - 模板字符串的使用

ES6 模板字符串是一种增强型的字符串表示方式,使用反引号包裹内容,支持在字符串中嵌入变量、表达式以及多行文本,极大的简化了字符串拼接和格式化操作。这是一个非常实用的特性,提升了的字符串处理的灵活性和代码的可读性

javascript 复制代码
// ES6 之前拼接字符串和其他标识符
const name = 'liang'
const age = 20
const height = 180
console.log('my name is ' + name + ', age is ' + age + ', height is ' + height);

基本语法:模板字符串通过反引号定义,可以包含变量和表达式,使用 ${} 语法进行插入

javascript 复制代码
const string = `my name is ${name}, age is ${age + 5}, height is ${height}`

多行文本:模板字符串天然支持多行文本,无需使用 \n 和字符串拼接,可以直接换行书写

plaintext 复制代码
const html = `<div>
    <button>按钮</button>
    <button>按钮</button>
    <button>按钮</button>
</div>`
8. ES6 - 标签模板字符串

标签模板字符串是 ES6 中的一个高级特性,它允许你通过一个函数来处理模板字符串

这个函数接收模板字符串的各个部分作为参数,并且可以根据需要对这些部分进行处理,从而实现自定义的字符串处理逻辑

  • 我们自己很少这样编写函数,但是有些框架会让我们这样调用它编写的函数(比如: React 框架)
javascript 复制代码
// 当使用标签模板字符串调用函数时
// 第一个参数:依然是模版字符串中的整个字符串,只是被切成了多块,放到了一个数组中
// 第二个参数:模板字符串中的第一个 ${}
// 第三个参数:模板字符串中的第二个 ${}
function foo(m, n, x) {
    console.log({ m, n, x });
}
// 普通调用
// foo('hello', 'world', '!')
// 标签模板字符串调用
const name = 'liang'
const age = 18
foo`hello${name}world${age}!`
9. ES6 - 函数参数的默认值

在 ES6 中,函数参数的默认值功能极大的简化了函数定义和调用过程,相比 ES5 时代的写法更加直观和安全

javascript 复制代码
// ES5 及之前给参数默认值,写起来很麻烦,并且代码的阅读性比较差,还有 bug(比如:传入一个 0)
function foo(m) {
    // 没有传递参数 m 默认是 undefined
    m = m || '默认值'
}
foo()

// ES6 可以给函数的参数设置默认值
function foo(m = '默认值') {
    // ...
}

// 对象参数和默认值以及解构
function foo({ name, age, gender = '男' } = { name: 'liang', age: 20 }) {
    // ...
}

函数的 length 属性值为其参数数量,对于有参数默认值的函数,从有默认值的参数开始,后续参数不做数量统计

javascript 复制代码
// 函数的 length 属性值为其参数数量
function foo(a, b) { }
console.log(foo.length) // 2

// 对于有参数默认值的函数,从有默认值的参数开始,后续参数不做数量统计
function bar(a, b, c = 3) { }
console.log(bar.length) // 2

// 实际上有默认值的参数一般都是放到最后,下面这种写法不推荐
function info(a, b = 2, c) { }
console.log(info.length) // 1 (有默认值的参数后续其他参数不做数量统计)
10. ES6 - 函数的剩余参数

ES6 中的剩余参数(Rest Parameter)是一种强大特性,可以将不定数量的参数放入到一个数组

javascript 复制代码
// 如果最后一个参数是以 ... 为前缀的,那么它会将剩余参数放到该参数中,并且作为一个数组
// 并且剩余参数只能放在最后一个参数(放在前面会报错)
function foo(a, b, ...args) {
    console.log(a) // 1
    console.log(b) // 2
    console.log(args) //  [3, 4, 5]
}

foo(1, 2, 3, 4, 5)

那么,剩余参数和 arguments 有什么区别呢 ?

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参
  • arguments 对象并不是一个真正的数组,而剩余参数是一个真正的数组,可以进行数组的所有操作
  • arguments 是早期 ECMAScript 中为了方便去获取所有的参数提供的一个数据结构,而剩余参数是 ES6 中提供的,并且希望以此来替代 arguments 的
11. ES6 - 箭头函数的补充

箭头函数是没有显式原型的,所以不能作为构造函数,无法通过 new 来创建对象

javascript 复制代码
const foo = () => { }
console.log(foo.__proto__) // undefined
new foo(); // Uncaught TypeError: foo is not a constructor
12. ES6 - 展开语法的使用

展开语法(Spread Syntax)最初在 ES6 中引入,主要用于数组操作,到了 ES9,展开语法被扩展到了对象处理

展开语法允许我们将数组或对象的元素展开为独立的元素,它使用三个 ... 表示:

  • 可以在函数调用、数组构造时,将数组表达式或 string 在语法层面展开
  • 还可以在构造字面量对象时,将对象表达式按 key-value 的方式展开

展开语法的场景:

  • 在函数调用时使用
  • 在数组构造时使用
  • 在构建对象字面量时,也可以使用展开运算符,这个是在 ES9 中添加的新特性
javascript 复制代码
// 1. 在函数调用时使用展开语法
function foo(a, b, c) {
    console.log({ a, b, c })
}
foo(...['html', 'css', 'js'])
// foo(...'html') // 也可以展开一个字符串

// 2. 构造数组时
const name = 'liang'
const hobby = ['html', 'css']
const newArr = [...name, ...hobby]
console.log(newArr)

// 3. 构建对象字面量 ES9(ES2018)
const info = { name: 'liang', age: 20 }
const object = { title: 'profile', ...info, gender: 1 }
console.log(object)

展开运算符进行的是一个浅拷贝

javascript 复制代码
const info = { name: 'liang', profile: { age: 18 } }
const user = { ...info }
user.profile.age = 20
console.log(info.profile.age) // 20
13. ES6 - 二进制和八进制连接符

javascript 复制代码
const num1 = 100 // 十进制

// b -> binary
const num2 = 0b100 // 二进制
// o -> octonary
const num3 = 0o100 // 八进制
// x -> hex
const num4 = 0x100 // 十六进制

console.log(num1) // 100
console.log(num2) // 4
console.log(num3) // 64
console.log(num4) // 256

// ES12(ES2011)对于大的数组,可以使用连接符 _
// 这种写法常用于提高大数字的可读性,划线 _ 作为数字分隔符,方便识别位数
const num = 10_000_000_000
console.log(num) // 10000000000
14. ES6 - Symbol 数据类型的使用

Symbol 是 ES6 中新增的一个基本数据类型,翻译为:符号

那么,Symbol 有什么用,为什么需要 Symbol 呢 ?

  • 在 ES6 之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突
  • 比如: 原来有一个对象,想在其中添加一个属性和值,但是在我们不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖它内部的某个属性
javascript 复制代码
// ES6 之前,对象的属性名 key 都是字符串形式
var obj = { name: 'liang' }
// 如果给 obj.name 赋值,会覆盖原来的值
obj.name = 'hello'
console.log(obj.name) // hello

Symbol 就是为了解决上面的问题,用来生成一个独一无二的值

  • Symbol 值是通过 Symbol() 函数来生成的,生成后可以作为属性名
  • 也就是在 ES6 中,对象的属性名可以使用字符串,也可以使用 Symbol 值

Symbol 即使多次创建值,它们也是不同的:Symbol 函数执行后,每次创建出来的值都是独一无二的

我们也可以在创建 Symbol 值的时候传入一个描述 description:这个是 ES10(ES2019)新增的特性

javascript 复制代码
// Symbol() 可以生成一个独一无二的值,可以让它作为对象的 key
const s1 = Symbol()
const s2 = Symbol()
console.log(s1 === s2) // false

// 创建 Symbol 值的时候传入一个描述
const s3 = Symbol('aaa')
console.log(s3.description) // aaa

Symbol 值作为对象的属性 key 的多种写法:

javascript 复制代码
const s1 = Symbol()
const s2 = Symbol()
const s3 = Symbol()
const s4 = Symbol()
// 1. 在定义对象字面量时使用
const obj = {
    [s1]: '111',
    [s2]: '222',
}
// 2. 新增属性
obj[s3] = '333'
// 3. Object.defineProperty 方式
Object.defineProperty(obj, s4, {
    enumerable: true,
    configurable: true,
    writable: true,
    value: '444',
})
// 注意:不能通过.语法获取(obj.s4 是获取不到的,字符串 key 才能这样用)
// 获取属性值的正确写法: 对象[Symblod值]()
console.log(obj[s1]) // 111

使用 Symbol 作为 key,在遍历或 Object.keys 等中是获取不到这些 Symbol 值的,有专门的方法去获取

javascript 复制代码
const s1 = Symbol()
const s2 = Symbol()
const obj = { [s1]: '111', [s2]: '222' }
// 使用 Symbol 作为 key,无法获取不到这些 Symbol 值
console.log(Object.keys(obj)) // []
console.log(Object.getOwnPropertyNames(obj)) // []
// 但是,可以使用 Object.getOwnPropertySymbols() 来获取所有 Symbol 的 key
const symbolKeys = Object.getOwnPropertySymbols(obj)
for (const item of symbolKeys) {
    console.log(obj[item]);
}

前面我们说过,Symbol() 返回的值都是独一无二的,那么有没有办法让它返回相同的值呢 ?

Symbol.for(key) 是 ES6 中的一个静态方法,用于创建或查找全局注册表中的 Symbol

  • 如果全局注册表中已经存在一个与给定键 key 关联的 symbol, 则返回该 symbol
  • 如果不存在,会创建一个新的 symbol,并将其与该键关联后放入全局注册表中
javascript 复制代码
// Symbol.for(key)
const s1 = Symbol.for('aaa')
const s2 = Symbol.for('aaa')
console.log(s1 === s2) // true
// Symbol.keyFor() 用于获取全局注册表中 Symbol 对应的键名
const key = Symbol.keyFor(s1)
console.log(key) // aaa
15. ES6 - 数据结构 Set(元素不可重复)

在 ES6 之前,存储数据的结构主要有两种:数组、对象

  • 在 ES6 中新增了另外两种数据结构:Set、Map,以及它们的另外形式:WeakSet、WeakMap

Set 是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是 Set 的元素不能重复

  • 创建 Set 我们需要通过 Set 构造函数(暂时没有字面量创建的方式)
  • 因为 Set 中存放的元素是不会重复的,那么它有一个很常用功能,就是数组去重
javascript 复制代码
// 通过构造函数创建 Set 结构
const set = new Set()

// set.add 向 Set 结构中添加元素
set.add(10)
set.add(20)
set.add(30)
set.add(30) // 不会被重复添加,因为结构中已有该元素

// 添加对象时,要注意,下面这样可以被重复添加,因为它们引用地址是不同的
set.add({})
set.add({})

console.log(set);

因为 Set 结构具有元素不会重复的特性,所以我们可以用于 数组去重

javascript 复制代码
// 数组去重
const arr = [32, 10, 25, 10, 32, 15]

// 传统方式
const newArr = []
arr.forEach(item => {
    if (newArr.indexOf(item) === -1) newArr.push(item);
})
console.log(newArr)

// 利用 set 的特性去重
const arrSet = new Set(arr)
// const newArr2 = Array.from(arrSet)
const newArr3 = [...arrSet]

Set 的常见属性和方法:

javascript 复制代码
const set = new Set([32, 10, 25, 10, 32, 15])
console.log(set) // Set(4) { 32, 10, 25, 15 }
// size 属性:获取元素数量
console.log(set.size) // 4
// 添加和删除元素
set.add(18)
set.delete(10)
// 判断元素是否在结构中
console.log(set.has(25)) // true
// 清空 set 中的元素
// set.clear()
console.log(set) // Set(4) { 32, 25, 15, 18 }

对 Set 结构进行遍历:

javascript 复制代码
const set = new Set([32, 10, 25, 10, 32, 15])
console.log(set) // Set(4) { 32, 10, 25, 15 }
set.forEach(item => {
    console.log(item)
})
console.log('- - - - - -');
for (const item of set) {
    console.log(item)
}
16. ES6 - 数据结构 WeakSet(存放对象)

和 Set 类似的另外一个数据结构称之为 WeakSet,也是内部元素不能重复的数据结构

那么和 Set 有什么区别呢 ?

  • WeakSet 中只能存放对象类型,不能存放基本数据类型
  • WeakSet 对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么 GC 可以对该对象进行回收

WeakSet 常见的方法:

  • add(value):添加某个元素,返回 WeakSet 对象本身
  • delete(value):从 WeakSet 中删除和这个值相等的元素,返回 boolean 类型
  • has(value):判断 WeakSet 中是否存在某个元素,返回 boolean 类型
javascript 复制代码
const weakSet = new WeakSet()

// 区别一:只能存放对象类型,无法添加基本类型数据
// weakSet.add(10) // Uncaught TypeError: Invalid value used in weak set

// 区别二:WeakSet 对数据是一个弱引用,而 Set 对数据是一个强引用
let obj = { name: 'liang' }
weakSet.add(obj)

对强引用和弱引用的理解:

javascript 复制代码
// 因为 Set 是强引用,所以当 user 修改为 null 时,user 不会被 GC 回收
let user = { name: 'liang' }
const set = new Set(user)
user = null

// 因为 WeakSet 是弱引用,所以当 profile 修改为 null 时,profile 就被 GC 回收掉了
let profile = { name: 'liang' }
const weakSet = new WeakSet(profile)
profile = null
17. ES6 - 数据结构 Map(支持对象属性 key)

Map 是一种新的数据结构,用于存储键值对,它与传统对象(Object)相比,具有更灵活的键类型和更好的性能特性

Map 用于存储映射关系,之前我们可以使用对象来存储映射关系,它们有什么区别呢 ?

  • 事实上对象存储映射关系只能用字符串(ES6 新增了 Symbol)作为属性名(key)
  • 某些情况下,可能希望使用其他数据类型作为 key。比如: 对象,这个时候会自动将对象转为字符串来作为 key
javascript 复制代码
// 传统的对象属性名 key,只能是字符串或Symbol值
const user = { name: 'liang' }
const profile = { age: 20 }
const object = { [user]: 'aaa', [profile]: 'bbb' }
// [] 是计算属性名的写法,将表达式的结果作为属性名
// 因为会将属性名转为字符串,所以都变为了 [object Object],后者覆盖前面,然后输出以下结果
console.log(object) // { [object Object]: 'bbb' }

// Map 就是允许对象类型作为 key 的
const map = new Map()
map.set(user, 'aaa')
map.set(profile, 'bbb')
map.set('name', 'liang') // 也可以传普通字符串
console.log(map)

可以通过构造函数来创建一个空的 Map,也可以传入一个包含键值对数组的参数来初始化 Map

javascript 复制代码
// 创建一个空的 Map
let map = new Map()
// 创建一个带有初始值的 Map
let map2 = new Map([['name', 'liang'], ['music', '天空之外']])

Map 常见属性和方法:

  • size 属性:获取 Map 中键值对的数量
  • set() 方法:添加或更新键值对,返回整个 Map 对象,支持链式调用
  • get() 方法:根据键获取对应的值
  • has() 方法:检测 Map 中是否存在指定的键
  • delete() 方法:删除指定键对应的键值对
  • clear() 方法:清空键值对
javascript 复制代码
const map = new Map([['name', 'liang'], ['music', '天空之外']])
console.log(map.size) // 2
// map.set() 添加或更新键值对
map.set('age', 20).set('height', 190)
// map.get() 根据键获取对应的值
console.log(map.get('name')) // liang
// map.has() 检测是否存在指定的键
console.log(map.has('age')) // true
// map.delete() 删除指定键对应的键值对
map.delete('height')
// map.clear() 清空键值对
map.clear()
console.log(map)

遍历 Map 也有多种方式:

javascript 复制代码
const map = new Map([['name', 'liang'], ['music', '天空之外']])
map.forEach((value, key) => {
    console.log(key, value)
})
for (const [key, value] of map) {
    console.log(key, value)
}
18. ES6 - 数据结构 WeakMap(key只能是对象)

和 Map 相似的另外一个数据结构称之为 WeakMap,也是以键值对的形式存在的

WeakMap 和 Map 有什么区别呢 ?

  • WeakMap 的 key 只能使用对象,不接受其他的类型作为 key
  • WeakMap 的 key 对对象的引用是弱引用,如果没有其他引用引用这个对象,那么 GC 可以回收该对象
javascript 复制代码
// Map 是强引用,所以当 obj 修改为 null 时,obj 不会被 GC 回收
let obj = { name: 'liang' }
const map = new Map()
map.set(obj, 'aaa')
obj = null

// WeakMap 是弱引用,所以当 obj2 修改为 null 时,obj2 就被 GC 回收掉了
let obj2 = { name: 'liang' }
const map2 = new WeakMap()
map2.set(obj2, 'bbb')
obj2 = null
19. ES7 - Array.includes 方法和指数运算符

之前判断数组中是否存在某个元素,通常是判断索引值,ES7 新增了 includes 方法,可以直接判断数据是否在数组中

javascript 复制代码
const arr = ['html', 'css', 'js']
// 判断索引值
if (arr.indexOf('css') !== -1) {
    console.log('在数组中')
}
// 支持传入第二个参数指定从哪个位置判断 arr.includes('css', 1)
if (arr.includes('css')) {
    console.log('在数组中')
}

那么 indexOfincludes 有没有什么区别呢 ?当然是有的

javascript 复制代码
const arr = ['html', 'css', NaN]
// indexOf 无法正确判断 NaN
console.log(arr.indexOf(NaN)) // -1
// includes 可以正确判断 NaN
console.log(arr.includes(NaN)) // true

ES7 新增的指数运算符:

javascript 复制代码
// 2 的 5 次方(之前指数运算的写法)
console.log(Math.pow(2, 5))
// ES7 增加了一个运算符 **
console.log(2 ** 5)
20. ES8 - Object.values、Object.entries 方法

之前我们可以通过 Object.keys() 获取一个对象的所有 key,在 ES8 中提供了 Object.values() 来获取所有的 value 值

javascript 复制代码
// 常见用法
const user = { name: 'liang', age: 20, height: 180 }
console.log(Object.keys(user)) //  ['name', 'age', 'height']
console.log(Object.values(user)) // ['liang', 20, 180]
// 用于数组和字符串(很少用)
console.log(Object.values(['html', 'css', 'js'])) // ['html', 'css', 'js'] (原数组本身)
console.log(Object.values('hello')) // ['h', 'e', 'l', 'l', 'o']

通过 Object.entries() 可以获取到一个数组,数组中会存放可以枚举属性的键值对数组

javascript 复制代码
// 一帮情况下,使用它是为了让我们方便进行遍历操作的
const user = { name: 'liang', age: 20, height: 180 }
console.log(Object.entries(user))
console.log(Object.entries('html'))
console.log(Object.entries(['html', 'css', 'js']))
21. ES8 - String.padStart、String.padEnd 方法

在 ES8 中,引入了两个字符串方法:padStart、padEnd,用于在字符串的开头或结尾填充指定字符,使其达到指定长度

语法格式:

  • targetLength:目标字符串长度
  • padString:用于填充的字符串,默认为空格
  • 返回值为填充后的字符串,不会修改原字符串
plaintext 复制代码
string.padStart(targetLength[, padString])
string.padEnd(targetLength[, padString])

使用示例:

javascript 复制代码
const string = 'hello'
console.log(string)
// 目标长度8,默认填充空格
console.log(string.padStart(8))
// 目标长度8,填充指定符号
console.log(string.padStart(8, '*'))
// 从结尾填充,目标长度8,填充指定符号
console.log(string.padEnd(8, '*'))

那么,有哪些场景可以用到字符串填充呢 ?

javascript 复制代码
// 时间格式化
function formatTime(time) {
    const date = new Date(time);
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    const hours = String(date.getHours()).padStart(2, '0');
    const minutes = String(date.getMinutes()).padStart(2, '0');
    const seconds = String(date.getSeconds()).padStart(2, '0');
    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 传入一个11位的手机号,将中间四位修改为 '*'
function formatMobile(mobile) {
    return mobile.slice(0, 3).padEnd(7, '*') + mobile.slice(-4)
}

尾逗号(Trailing Commas)是指在数组、对象字面量或函数参数列表的最后一个元素后添加的额外逗号

这一特性自 ES5 起已被标准正式支持,并在 ES2017(ES8)中扩展至函数参数

尽管看似冗余,但它显著提升了代码的可维护性,尤其是在多行结构中增删项时,可以避免因遗漏逗号而引发的语法错误

javascript 复制代码
// ES8 才开始支持函数接收参数时或调用时最后面还有逗号
function foo(m, n,) {
    console.log({ m, n });
}
foo(111, 222,)
22. ES10 - Array.flat、Array.flatMap 方法

flat()flatMap() 是两个用于处理数组扁平化的常用方法,它们各有不同的用途和特点

  • flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历的子数组中的元素合并为一个新数组返回
  • flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩为一个新数组
    • flatMap 是先进行 map 操作,再做 flat 的操作
    • flatMap 中的 flat 相当于深度为 1

语法格式:

javascript 复制代码
// depth 是可选参数,表示要展开的嵌套层级,默认为 1。如果传入 Infinity,则会完全展开所有层级的嵌套数据
const newArray = array.flat([depth = 1])

使用示例:

javascript 复制代码
const nums = [1, [2], [[3]], [[[4]]]]
console.log(nums)
console.log(nums.flat(1)) // [1, 2, Array(1), Array(1)]
console.log(nums.flat(2)) // [1, 2, 3, Array(1)]
console.log(nums.flat(Infinity)) // [1, 2, 3, 4]

flatMap() 方法的使用方法,通过以下代码示例学习:

javascript 复制代码
// 需求:将数组元素中的每个单词抽离出来
const slogan = ['what are you doing', 'how old are you']

// 刚学习完 flat() 方法,你可能会这么做
const words = slogan.map(item => item.split(' ')).flat()
console.log(words) // ['what', 'are', 'you', 'doing', 'how', 'old', 'are', 'you']

// 现在告诉你,可以使用 flatMap() 简化代码,它会先执行 map() ,然后再进行 flat()
const words2 = slogan.flatMap(item => item.split(' '))
console.log(words2) // ['what', 'are', 'you', 'doing', 'how', 'old', 'are', 'you']
23. ES10 - Object.fromEntries、String.trimStart 方法

Object.fromEntries() 方法用于将键值对列表转换为一个普通对象

javascript 复制代码
const user = { name: 'liang', age: 20 }
console.log(user) // { name: 'liang', age: 20 }

// 如何根据 entries 结果,再将数据处理成对象
const entries = Object.entries(user)
console.log(entries) // [ Array(2), Array(2) ]

// 传统方式是这样做的
const newObj = {}
for (const [key, value] of entries) {
    newObj[key] = value
}
console.log(newObj) // { name: 'liang', age: 20 }

// 还可以通过 Object.fromEntries() 方法实现
console.log(Object.fromEntries(entries)) // { name: 'liang', age: 20 }

Object.fromEntries() 方法的应用场景,配置 URLSearchParams() 处理查询字符串

javascript 复制代码
const queryStrig = 'name=liang&age=20&height=180'
const queryParams = new URLSearchParams(queryStrig)
// 它是一个可迭代的对象
for (const item of queryParams) {
    console.log(item)
}
// 将它转为一个普通对象
const params = Object.fromEntries(queryParams)
console.log(params) // { name: 'liang', age: '20', height: '180' }

清除字符串中的空格

javascript 复制代码
const string = '  hello world  '
// 清除前后两边的空格(ES5)
console.log({ trim: string.trim() })
// 清除前面的空格(ES10)
console.log({ trimStart: string.trimStart() })
// 清除后面的空格(ES10)
console.log({ trimEnd: string.trimEnd() })
24. ES11 - 大整数类型 Bigint

在早期的 JavaScript 中,我们不能正确的表示过大的数字

  • 大于 Number.MAX_SAFE_INTEGER 的数值,表示的值可能是不正确的
javascript 复制代码
// ES11 之前 max_safe_integer
const maxInt = Number.MAX_SAFE_INTEGER
console.log(maxInt) // 9007199254740991
// 可以发现 +1 和 +2 的值竟然是相同的
console.log(maxInt + 1) // 9007199254740992
console.log(maxInt + 2) // 9007199254740992

// ES11 之后:Bigint( 后面加一个 n )
const bigInt = 900719925474099100n
console.log(bigInt) // 900719925474099100n

// 直接进行运算是会报错的,需要在 10 后面加个 n
// Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
// console.log(bigInt + 10)
console.log(bigInt + 10n) // 900719925474099110n

// 如果是一个变量,可以先使用 BigInt() 转为 bigint,再进行运算
const num = 20
console.log(bigInt + BigInt(20)) // 900719925474099120n
25. ES11 - 空值合并运算符 ??

空值合并运算符(Nullish Coalescing Operator),当左侧操作数为 null/undefined 时,返回右侧操作数

  • 只有 nullundefined 被视为空值,其他假值不会触发 fallback(右侧的后备方案)
javascript 复制代码
// 之前获取值时,设置默认值大多使用逻辑或实现,这种是存在弊端的(比如:对于 0、false)
let foo = 0
console.log(foo || 'hello') // hello
// 空值合并运算符
let bar = 0
console.log(bar ?? 'hello') // 0
26. ES11 - 可选链操作符 ?.

ES11 进入了一个非常实用的特性:可选链操作符(Optional Chaining Operator)

它的核心作用是:安全的访问嵌套对象的属性或方法,避免因为中间某一层为 nullundefined 而抛出错误

javascript 复制代码
const profile = { name: 'liang' }

// 以下代码会抛出错误,这是因为 profile.info 是 undefined,又访问了 undefined 的 age 属性
// Uncaught TypeError: Cannot read properties of undefined (reading 'age')
console.log(profile.info.age)

// 使用可选链操作符,属性不存在也不会报错
console.log(profile.info?.age)

当对接后端接口时,接口中的字段值可能出现意料之外的情况,导致前端代码报错,我们应该怎么避免这种问题

javascript 复制代码
// 比如:当前查询某个列表,data 应该始终为一个数组,但是后端接口不规范
// 后端提供的接口,当没有数据时,data 给的是一个 null,导致前端代码报错
// 我对接Java 后端接口时,这个问题出现很多次,而且对方还不想修改,说是改起来比较麻烦/框架问题
const res = { code: 200, msg: 'success', data: null }

// 报错原因:res.data 为 null,调用了 null.forEach 就报错了
// Uncaught TypeError: Cannot read properties of null (reading 'forEach')
// res.data.forEach(item => { })

// 使用 ES11 的可选链,可以很简单的处理掉这个问题
res.data?.forEach(item => { })
27. ES11 - globalThis 和 for...in 标准化

在 ES11 之前,我们想要获取当前环境的全局对象,不同的环境获取方式是不一样的

  • 比如:在浏览器中可以通过 this、window 来获取
  • 在 node 中我们需要通过 global 来获取

globalThis 是 ES11 引入的一个标准化的全局对象引用,用于在任何 JavaScript 环境中统一访问全局对象

javascript 复制代码
// 浏览器环境(使用浏览器打开)
console.log(this)
console.log(window)

// node 环境(终端运行命令: "node 文件名" 可查看)
console.log(global)

// ES11(在任何环境中都可以访问全局对象)
console.log(globalThis)

ES11 对 for...in 语句进行了标准化规范

javascript 复制代码
const user = { name: 'liang', age: 20 }
// ES11 之前,ECMA 对 for...in 没有制定标准,部分浏览器中 item 指向的可能是 value,而不是 key
// 在 ES11 中,ECMA 对 for...in 进行标准化规范,至此 item 统一指向了 key
for (const item in user) {
    console.log(item)
}
28. ES12 - FinalizationRegistry 执行清理回调

FinalizationRegistry 是 ES12 引入的一个高级特性,用于在对象被垃圾回收(GC)后执行清理回调。

FinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调,它提供了这样的一种方法:

  • 当一个在注册表中注册的对象被回收时,请求在某个时间点上调用一个清理回调(finalizer)
  • 你可以通过调用 register 方法,注册任何你想要清理回调的对象,传入该对象和所含的值
javascript 复制代码
// 创建一个目标对象
let user = { name: 'liang' }
// ES12:FinalizationRegistry 类,参数是一个回调函数,对象被垃圾回收时执行
const finalRegistry = new FinalizationRegistry(() => {
    // 刷新页面并不会立即执行
    // 因为 GC 垃圾回收机制是不定时来检测一下有没有垃圾,多等一会就行
    console.log('注册到 finalRegistry 的某个对象被销毁');
})
// 将对象注册到 finalRegistry 上
finalRegistry.register(user)
// 当 user 不再被任何强引用持有时,未来某刻回调可能被调用(我测试是 30秒左右)
user = null // 移除强引用,使其可被 GC

可以注册多个对象到注册表,并且可以传入第二个参数(用于在回调中传递额外信息)

javascript 复制代码
// 目标对象
let user = { name: 'liang' }
let info = { name: 'james' }
// 通过打印时间,可发现目标对象被移除强引用之后的30秒左右执行的清理回调
console.log(new Date().getTime() / 1000)
const finalRegistry = new FinalizationRegistry(value => {
    console.log(new Date().getTime() / 1000)
    console.log('注册到 finalRegistry 的某个对象被销毁 ' + value)
})
// 注册多个对象到注册表,并且可以传入第二个参数
finalRegistry.register(user, 'aaa')
finalRegistry.register(info, 'bbb')
// 移除强引用,使其可被 GC
user = null
info = null
29. ES12 - WeakRef 创建对象的弱引用

WeakRef 是 ES12 引入的一个特性,用于创建对对象的弱引用,它就是提供弱引用能力的内置类

它允许你引用一个对象,但不会阻止该对象被垃圾回收(GC)

  • 强引用(Strong Reference):常规的对象引用。只要存在强引用,对象就不会被垃圾回收
  • 弱引用(Weak Reference):不会阻止对象被回收。当对象仅被弱引用持有时,它可能在任意时间被 GC 回收
javascript 复制代码
// 因为 user 的引用复制给了 info,所以当 user 设置为 null,也不会触发清理回调(引用还存在)
let user = { name: 'liang' }
let info = user // 强引用
console.log(new Date().getTime() / 100)
const finalRegistry = new FinalizationRegistry(value => {
    console.log('注册到 finalRegistry 的某个对象被销毁 ' + value)
})
finalRegistry.register(user, 'aaa')
user = null

那么,我们如何将 info 变为 user 的弱引用呢 ?

javascript 复制代码
let user = { name: 'liang' }

// 1. 强引用
// let info = user
// 2. 可以改为使用 WeakSet,使 info 变为弱引用,这种方式能实现
// let info = new WeakSet()
// info.add(user)
// 3. ES12 推出 WeakRef,让我们更方便的创建弱引用
let info = new WeakRef(user)
console.log(info.deref()) // 获取到对象数据

const finalRegistry = new FinalizationRegistry(value => {
    console.log('注册到 finalRegistry 的某个对象被销毁 ' + value)
})
finalRegistry.register(user, 'aaa')
user = null
30. ES12 - 逻辑赋值运算符、String.replaceAll()

JavaScript 中的三种逻辑赋值运算符是 ES12 引入的语法糖,用于在满足特定条件时对变量或属性进行条件性赋值

javascript 复制代码
// 逻辑或赋值运算 ||=
let message = ''
message ||= 'default' // 相当于 message = message || 'default'

// 逻辑与赋值运算 &&=
let a = 1
let b = 2
a &&= b // 相当于 a = a && b(当 a 有值,取 b 的值返回)

// 空值合并赋值 ??=
let c = ''
let d = 2
c ??= d // c = c ?? d

String.prototype.replaceAll() 是 ES12 引入的一个字符串方法,用于将字符串中所有匹配的子串替换为新值

  • 它解决了长期以来 String.prototype.replace() 默认只替换第一个匹配项的问题

为什么需要 replaceAll() ? 在它出现之前,要全局替换字符串,通常需要:

javascript 复制代码
// 方法1:使用正则表达式 + 全局标志 g
str.replace(/old/g, 'new')
// 方法2:手动处理(不推荐)
str.split('old').join('new')

replaceAll() 提供了更简单、直观、安全的全局替换方式,语法格式:

  • searchValue:可以是字符串或者正则表达式
  • replaceValue:新的字符串
javascript 复制代码
str.replaceAll(searchValue, replaceValue)

使用示例:

javascript 复制代码
const string = 'good idea good'
// 第一个参数:使用字符串
console.log(string.replaceAll('good', 'aaa'))
// 第一个参数:正则表达式。必须带有全局标志 g , 否则会抛出错误
console.log(string.replaceAll(/good/g, 'bbb'))
31. ES13 - Array/String.at()、Object.hasOwn()

Array.prototype.at()String.prototype.at() 是 ES13 引入的新方法,支持负数索引取值

  • 它解决了长期以来 JavaScript 无法直接用负索引取值的问题
javascript 复制代码
// index:整数,可以是正数(从 0 开始),也可以是负数(从 -1 开始,表示最后一个)
// 如果超出索引范围,返回 undefined,不会报错
array.at(index)
string.at(index)

在 ES13 之前,要访问数组最后一个元素,通常这样写:

javascript 复制代码
const arr = ['html', 'css', 'js']
arr[arr.length - 1] // js

从 ES13 开始,可以使用 at() 直接获取指定负索引位置的元素,同样它也支持正索引

javascript 复制代码
const arr = ['html', 'css', 'js', 'vue']
console.log(arr.at(0)) // vue(第一个)
console.log(arr.at(2)) // js(索引为2)
console.log(arr.at(-1)) // vue(最后一个)

ES13 中 Object 新增了一个静态方法(类方法):Object.hasOwn(obj, key)

javascript 复制代码
const user = {
    name: 'liang',
    // 2. 在原型上添加属性也可以写在这里
    __proto__: { height: 180 }
}

// 1. 在原型上添加属性
Object.prototype.age = 20

console.log(user.name) // liang
console.log(user.age) // 20
console.log(user.height) // 180

// ES13 之前
// 判断属性是否在对象自身上,在自身上返回 true(在原型上会返回 false)
console.log(user.hasOwnProperty('name')) // true
console.log(user.hasOwnProperty('age')) // false

// ES13 推出 Object.hasOwn(obj, key) 用于替代 Object.prototype.hasOwnProperty(key)
console.log(Object.hasOwn(user, 'name')) // true
console.log(Object.hasOwn(user, 'age')) // false

那么 Object.hasOwn(obj, key)Object.prototype.hasOwnProperty(key) 有什么区别呢 ?

  • 因为 hasOwnProperty 是一个实例方法,那么这个方法可能会 被"污染"或覆盖
  • hasOwn 对无原型对象的数据也支持,而 hasOwnProperty() 不行
javascript 复制代码
// 区别1: 因为 hasOwnProperty 是一个实例方法,那么这个方法可能会 被"污染"或覆盖
const user = {
    name: 'liang',
    // 重写该方法,那么原型上的 hasOwnProperty 就被覆盖了
    hasOwnProperty() {
        return 'aaa'
    }
}
console.log(user.hasOwnProperty('name')) // aaa(显然是不对的)

// 区别2: hasOwn 对无原型对象也支持,而 hasOwnProperty() 不行
// Object.create() 用于创建一个新对象,并指定其原型
const info = Object.create(null)
info.name = 'liang'
// 此时不能使用 hasOwnProperty()
// Uncaught TypeError: info.hasOwnProperty is not a function
// console.log(info.hasOwnProperty('name'))
console.log(Object.hasOwn(info, 'name')) // true
32. ES13 - 类中新增的成员(私有化成员)

从 ES13 开始,正式支持在类中定义真正的私有化属性(private)

javascript 复制代码
class Person {
    // 2. 对象属性:public 公共的
    // 在类的内部和外部都可以访问(并且所有类的实例都能访问到这个值)
    height = 180
    //(ES13 之前)对象属性:private 私有的,只是开发者之间的约定,在外面其实还是能访问的
    _intro = '个人介绍'
    //(ES13 开始)对象属性:private 私有的
    #info = 'real private info'
    constructor(name, age) {
        // 1. 对象中的属性:在 constructor 通过 this 设置
        this.name = name
        this.age = age
    }
    running() {
        console.log(this.#info) // 类内部可读取
        this.#showName() // 类内部可调用
    }
    // 私有方法
    #showName() {
        console.log('name: ' + this.name);
    }
}
const p = new Person('liang', 20)
console.log(p._intro) // 可以访问(不是真的私有属性)
p.running()
const p2 = new Person('james', 25)
console.log(p.height) // 180
console.log(p2.height) // 180

类的静态属性、静态方法使用示例:

javascript 复制代码
class Person {
    // 静态的公共属性(默认情况是 public)
    static name = 'liang'
    // 静态的私有属性 private(使用 # 表示)
    static #age = 20

    // 静态方法
    static showName() {
        console.log(this.#age);
    }

    // 静态代码块(在类初始化时立即执行的代码块)
    static {
        console.log('hello world');
    }
}
console.log(Person.name)
Person.showName()
相关推荐
一拳不是超人2 小时前
Electron主窗口弹框被WebContentView遮挡?独立WebContentView弹框方案详解!
前端·javascript·electron
wuhen_n3 小时前
代码生成:从AST到render函数
前端·javascript·vue.js
Lee川3 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
wuhen_n3 小时前
AST转换:静态提升与补丁标志
前端·javascript·vue.js
destinying3 小时前
性能优化之实战指南:让你的 Vue 应⽤跑得飞起
前端·javascript·vue.js
晴殇i5 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
BER_c5 小时前
前端权限校验最佳实践:一个健壮的柯里化工具函数
前端·javascript
敲敲敲敲暴你脑袋5 小时前
写个添加注释的vscode插件
javascript·typescript·visual studio code
SuperEugene6 小时前
后台权限与菜单渲染:基于路由和后端返回的几种实现方式
前端·javascript·vue.js
csdn飘逸飘逸6 小时前
Autojs基础-全局函数与变量(globals)
javascript