JS 基础语法

2025-12-22

本文参考内容 https://www.bilibili.com/video/BV1UR4y1m7SB/open in new window

行末分号

以下字符开头的代码,前一行行末不能省略分号,可以在行首加上

  • [ (方括号)
  • ( (圆括号)
  • ` (反引号)
  • / (斜杠)
  • +, - 等一元或二元运算符

以下是部分常见的行首增加分号的情况

  • 使用 ES6 解构对变量进行交换
    // 数组乱序
    const arr = [1, 2, 3, 4, 5, 6, 7]
    for(let i = arr.length - 1; i > 0 ; i--) {
        const j = Math.floor(Math.random() * (i+1))  // 0-i 的一个索引值,实现可能交换或不交换
        ;[arr[j], arr[i]] = [arr[i], arr[j]]  // 数组解包,类似 py - 此处由于开头满足条件,在行首使用分号
    } 
    
  • 自执行函数
    ;(function(){
        console.log(1)
    })()
    ;(function(){
        console.log(1)
    }())
    
  • 非数组对象调用数组方法
    let res = ''
    const str = 'abc'
    ;[].forEach.call(  // `[` 开头,需要分号
        str, c => {res += "\\x" + c.charCodeAt(0).toString(16)}
    )
    console.log(res)
    

基本类型

部分基本类型相关用法介绍

  • bigInt
    let a = 99999999999999999999999n  // 防止整数过大时精度丢失
    let b = 99999999999999999999999
    console.log(a,b)
    
  • 长字符串换行
    console.log('aaaaaaaa\
    bbbbbbbbaaaaaaaa\
    ccccccccbbbbbbbbaaaaaaaa')  // 与python的三引号多行字符串类似,前方不能有空格,否则字符串中也会包含空格
    

逻辑符

使用 || && 控制代码执行

  • func1() || func2(): 当第一个函数返回真时,第二个函数不执行
  • func1() && func2(): 当第一个函数返回假时,第二个函数不执行

在此基础上,无括号优先级的连续逻辑符,则按从左至右的顺序,依次判断执行到哪个函数停止

E.g. func1() || func2() && func3() || func4()

  • 根据 func1 返回是否为真判断后续函数是否执行
  • 再根据 func2 返回为假判断后续函数是否执行
  • ...

每个函数的结果只影响身后的逻辑符

数组

  • 声明
    • 隐式创建 / 字面量声明 var arr = []
    • 构造函数声明,可传入数字表明声明数组的长度
      var arr = new Array()  
      // 若使用 new Array(5), 此时是默认填充 5个 undefined 的数组
      
    • 省略 new 的构造函数声明 var arr = Array()
  • 下标
    arr.length = 99  // 修改数组长度,此时会填充 undefined
    console.log(arr[0])
    console.log(arr.at(-1))  // at 与方括号类似,但多了负索引的支持
    // 越界,不会报错,返回 undefined
    console.log(arr[101])
    
  • 数组内容: 元素内容可执行时,读取时会直接返回执行结果
    var arr = [1+2, function(){return 1}()]
    console.log(arr[0], arr[1])
    // 3 1
    

常用函数

  • 元素增删
    • push/pop 组尾操作 推入/弹出
    • shift/unshift 组首操作 拿出/放入
  • 拼接
    var res = arr.concat([1,2,3])
    // arr 不会被修改,需要返回值接收
    
  • 搜索. 参数1 目标值, 参数2 从哪个索引开始找
    arr.indexOf(1)  // 不存在返回 -1 
    arr.lastIndexOf(1)  // 从右往左
    
  • 是否包含 arr.includes(1)
  • 拼接成字符串 arr.join() 无参数默认是逗号
  • 截取
    // 参数 [index1, index2]   [) 左闭右开 , 不会修改 arr,返回值需要接收
    var res = arr.slice(1, 3)
    var res = arr.slice()  // 都不传默认是浅copy原数组
    
  • 修改 splice 参数1 索引 参数2 删除个数 参数345... 添加元素内容
    // 从索引1开始,删除2个元素。返回被删除的元素
    var res = arr.splice(1, 2)
    // 从索引1开始,插入元素2,元素3 (插入后的数组内顺序与此处顺序一致)
    var res = arr.splice(1, 0, 2, 3)
    // 将索引 1 开始的 2 个元素替换为元素 2,3,4 共 3 个元素
    var res = arr.splice(1, 2, 2,3,4)
    
  • 反转 arr.reverse()
  • toString 每个元素调用toString转字符串后,使用逗号拼接
    // 可使用隐式转换,与以下写法等效 [1,2,3] + ''
    [1,2,3].toString()
    // '1,2,3'
    
  • 降维(只拆解一个维度)
    ;[[1,2,3],[4,5,6],[7,8,9]].flat()
    // 得到[1,2,3,4,5,6,7,8,9]
    ;[[1,[2,3]], [4,5,6]].flat()
    // 得到[1,[2,3],4,5,6]
    
  • 遍历
    • for + 索引
      for(let i=0; i < arr.length; i++){
          arr[i]
      }
      
    • for in 获取下标/属性名(obj keys)
      for(const i in arr) {
          arr[i]
      }
      
    • for of 可遍历实现 iterable 接口的对象
      for(const item of arr) {item}
      

遍历相关高阶函数

以下高阶函数的参数包含函数,数组中每个元素都会传入参数中的函数内

需要注意的是,传入函数的函数体若为代码块,需要明确返回值,不然将默认返回 undefined ! 此处以第一个 some 为例详细说明,后续省略

  • some 返回是否存在满足条件的数组元素
    ;[1,2,3].some(item => item == 0) // 假
    ;[1,2,3].some(item => item == 1)  // 真 ,条件不是代码块,省略 return
    ;[1,2,3].some(item => {return item == 1} ) // 代码块,需要写 return
    ;[1,2,3].some(item => {item == 1} )  // 错误示范! 代码块没有 return 默认无返回值,结果恒定为 undefined
    
  • every 返回是否数组内每个元素都满足条件 ;[1,1,1].every(item => item == 1) -> true
  • find 返回数组中满足传入函数的第一个元素的值 , 没有则返回 undefined
    var a = [{'id': 1, 'data': 1}, {'id': 2, 'data': 2}, {'id': 3, 'data': 3}]
    a.find(item => item.id == 1)
    // {'id': 1, 'data': 1}
    a.find(item => item.id == 4)
    // undefined
    
  • filter 数组中传入函数返回值为真的所有元素,组成一个新数组并返回 ;[1, 2, 1, 3, 1, 4].filter(item => item == 1) -> [1,1,1]
  • map 已有数组元素进行相同处理,返回值创建新数组 ;[1,2,3].map(item => item*2) -> [2, 4, 6]
  • reduce 归纳(数学归纳法) 第一轮值由前两项确定,后续结果由下一项与上一轮的值计算
    // 将第一个元素与第二个元素作为参数,得到返回值。下一轮中将上一轮的返回值作为参数1,当前元素作为参数2
    // 出入值 // 第一轮 1,2 -> 3  // 第二轮 3(1+2) 3 -> 6 // 第三轮 6(3+3) 4 -> 10
    ;[1, 2, 3, 4].reduce((item1, item2) => item1 + item2)
    // 输出: 10
    
  • sort 排序,默认按编码值排序
    ;[1, 3, 200].sort()
    // 编码值排序导致 2 开头的 200,排在 3 前面。最终输出 [1, 200, 3]
    ;[1, 3, 200].sort((item1, item2) => item1 - item2)
    // 比较两个元素谁大的规则,回传一个 number 值,根据 >0 =0 <0 判断两个值的大小
    // item1 是后面的项,item2 是前面的项。若返回值 > 0,保持后项在后,前项在前
    
  • flatMap 先处理元素,再降维
    ;[[1,[2,3]], [4,5,6], 7].flatMap(item => item instanceof Number ? item * 2 : item)
    // 共三项,仅最后一项 7 是数字,需要 `*2`
    // [1,[2,3],4,5,6,14]
    

Tips: 字符串不能直接调用以上数组的方法。如何使用详见#类数组使用数组方法

展开符

var a = [3,4,5]
var b = [1,2, ...a, 6,7]
console.log(b)
var c = [...b]  // 快速复制
console.log(c)

类数组

什么是类数组?访问元素时可使用方括号+下标访问

var obj = {0: 1, 1: null, 2: undefined}
var str = 'abc'

类数组使用数组方法

可使用以下几个方法,让类数组可以像数组一样操作

  • Array.from 类数组的 map 方法。将类数组转换为数组,从而方便后续可以调用数组其他方法
    // 遍历
    Array.from('aaa', c => c.charCodeAt())
    // [97, 97, 97]
    // 与 ['a', 'a', 'a'].map(c => c.charCodeAt()) 的输出相同
    
  • <方法>.call/apply(调用该方法的对象) 通过call/apply,修改this指向,让指定对象调用 <方法>。关于 call/apply 方法,详见 # call&apply
    // 通过 Array 原型定位数组方法
    console.log(Array.prototype.join.call('abc'))
    // 通过一个空数组定位数组方法
    console.log([].join.apply('abc'))
    // a,b,c
    
  • 展开符(必须非常类似 Array 的对象才能使用, str 与 intObj 不能使用)
    function func(){
        // arguments.forEach(i => console.log(i))  // 报错,不是Array
        [...arguments].forEach(i => console.log(i))
    }
    func(1,2,3)
    

解构

  • 通过下标一对一赋值
    var [a, b, c] = [1, 2, 3, 4]
    console.log(a, b, c)
    // 1 2 3 - 下标 2 后的元素没有对应变量读取
    var [a, b, c] = [1, 2]
    console.log(a, b, c)
    // 1 2 undefined  - 下标 2 的内容在数组中不存在,得到 undefined
    
  • 默认值
    var [a,b,c,d=4] = [1,2,3]
    console.log(a,b,c,d)
    
  • 将其余元素打包为数组,都赋值给某一个变量(使用展开符)
    var [a, ...b] = [1,2,3]
    console.log(a, b)
    // 1  (2)[2, 3]
    
  • 函数参数解构
    function func1([a, b, ...c]) {
        console.log(a, b, c) 
        // 1 2  (5)[3, 4, 5, 6, 7]
    }
    func1([1,2,3,4,5,6,7])
    
  • 交换两数
    var a = 1, b = 2
    ;[a, b] = [b, a]
    console.log(a, b)
    

数组作为函数参数

此时函数传入的为数组的引用。即函数内修改数组元素,在函数调用结束后会对该数组真实生效

作用域

JS 中的作用域说明

代码块

在介绍作用域之前,先了解代码块。以下是三种不同类型的代码块

  • 普通代码块
    • 可以在任何位置使用代码块。以下代码可以正常运行
      {
          console.log(1)
      }
      
    • 可以随意嵌套代码块。以下代码也可正常运行
      {
          {
              {
                  {
                      {
                          console.log(2)
                      }
                  }
              }
          }
      }
      
  • 语句代码块
    for(i = 0; i < 10; i++) {  // for语句的代码块
        console.log(i)
    }
    
  • 函数代码块: 函数在声明时也使用了 {}。函数的块是块的特例(特殊性在其内部定义函数的作用域中体现)
    // 函数每次调用时,会开辟新的空间。因此 **函数内声明的变量** 每次都是全新的。多次调用函数时,变量互不影响
    function test(input) {
        const a = input
        console.log(a)
    }
    test(1)  // 创建 input,创建了 a 接收 input 的值,调用结束后销毁
    test(2)  // 创建了新的 input 与 a
    

变量作用域(声明关键字)

  • constlet 声明的变量作用域在其所在的代码块中(代码块中定义,代码块结束时销毁)。如果没有在代码块中,则为全局作用域
    • 例1: 普通代码块
      {
          const a = 1; // 出了该定义语句所在块,则该声明失效
          {
              const b = 2 ;  // 内层块内有效
          }  // 此时销毁 b
          console.log(a)
          // console.log(b)  // 这里会报错
      }  // 此时销毁 a
      // 本例中 由于该块内的子块也属于该块,所以内层块中 a 也有效
      
    • 例2: 语句代码块
      //   i 在 for 语句内,且该语句含有代码, i 的作用域就是 for 语句的代码块
      for(let i = 0; i < 10; i++) {  //与别的 for 语句代码块中的 i 不冲突
          console.log(i)  // let 具有块级作用域。每次循环,都会在内存里产生一个全新的 i
      }  // for 循环结束时销毁 i
      // console.log(i)  // 这里会报错
      
    • 例3: 代码块内不可重复定义
      // 重复声明报错
      let a = 1
      let a = 2  // 报错:Identifier 'a' has already been declared
      
  • var 定义变量
    • 特性

      1. 作用域至少为函数语句块
      2. 升格到作用域头部
      3. 可重复声明同一变量,会覆盖值
    • 例1: 函数代码块

      function testVar() {
          console.log(a)
          {
              var a = 1
              console.log(a)
          }
          console.log(a)
      }
      
      // 由于 var 的作用域是 函数,所以 var 声明会跳出非函数的块。变成如下样子
      /*
      function testVar() {
          var a  // 1. 升格到函数作用域开始处
          console.log(a)  // 3. 还没赋值,输出 undefined
          {
              a = 1  // 2. 赋值保留原始位置
              console.log(a)  // 正常输出
          }
          console.log(a)  // a已经被赋值 正常输出
      }
      */
      // 最终输出 undefined 1 1
      
    • 例2: 普通块中的 var

      console.log(a)  // 1. 升格后,此处输出 undefined
      {
          console.log(a)  // 2. 升格后,此处输出 undedefined
          var a = 1  // 0. 由于升格提升,该 var 为全局作用域。赋值仍然在此处
      }
      console.log(a)  // 3. 已赋值,此处输出 1
      // 2.3 重复声明
      var a = 1
      var a = 'a'  // 会覆盖之前的变量
      // 即解释器会解析成如下内容
      /*
      var a
      a = 1  // 此时该语句变成多余的赋值
      a = 'a'
      */
      
  • 代码块中无关键字的变量声明都是 全局变量
    {
        g1 = 1
        {
            g2 = 2
        }
    }
    console.log(g1, g2)  // 普通代码块内声明的全局变量
    // 1 2
    for (i = 0; i<10; i++){
        ;
    }
    console.log(i)  // 语句代码块内声明的全局变量
    // 10
    function globalVar() {
        g3 = 3
    }
    globalVar()  // 需要执行一下函数以保证该全局变量的声明被执行
    console.log(g3)  // 函数代码块内声明的全局变量
    // 3
    

关键字选择的Tips: 推荐默认使用 const 来声明所有变量,只有当您确定需要对变量进行重新赋值时(E.g. 在循环中,同一个变量 i 被多次赋值),才使用 let

后续的所有操作都是对这个对象内部结构的增、删、改,不改变其引用的,都可以使用 const

  • 代码意图更清晰: 使用 const 能明确地告诉其他开发者,这个变量的引用不会被改变,有助于提高代码的可读性和可维护性
  • 减少意外错误: 它可以防止意外地对变量进行重新赋值,从而减少潜在的 bug

函数作用域

  • 变量关键字形式声明的函数,其作用域符合变量作用域的逻辑

    // E.g. var 定义函数,就是 var 变量,升格,但赋值位置不变
    function func() {
        console.log(test) // 由于升格会输出 undefined
        var test = function () {}
    }
    func()
    
  • function 关键字定义的函数,作用域类似 var,至少为函数代码块,且作用域升格到作用域头部。函数内容与 var 不同,内容升格根据情况而定

    • function 定义的函数在 普通代码块 和 语句代码块 中: 效果完全同 var

      // E.g.1 不执行的语句代码块
      console.log(func)  // 升格效果同 var,未赋值,此处输出 undefined
      if (false) {
          // func 会升格,但该函数声明不被执行,因此 func 无法完成赋值
          function func() {const b = 2}
      }
      console.log(func)  // 同 var 变量,输出 undefined
      
      // E.g.2 执行的语句代码块
      console.log(func1)  // undefined
      if (true) {
          // func 会升格,该函数声明被执行了, if 的语句代码块升格效果同 var
          // 此处改用普通代码块使声明执行,也是相同升格效果,保持与 var 效果一致
          function func1() {const b = 2}
      }
      console.log(func1)  // 同 var 变量,输出 undefined
      
    • function 定义的函数在 函数代码块 中: 函数定义(赋值)也会升格

      console.log(func)  // 由于 func 在普通代码块,此处的升格会输出 undefined
      {
          function func() {
              console.log(test) // 由于 test 的定义在函数代码块,此时 test 的定义也会升格
              // 正常输出 test 的定义
              console.log(test())  // 由于定义也升格了,此处可在代码中定义的前方正常调用 test ,执行其中的 console.log(a)
              // 正常执行 test 内的代码,输出 1
              function test() {
                  const a = 1
                  console.log(a)
              }
          }
      }
      func()
      
    • Tips: 在函数代码块这种特殊块中,function 定义的函数会完全升格,即 test把定义也提前。而一般的块中,升格效果同 var 一样,作用域升格但赋值不会升格,func 在真正声明行之前是 undefined

作用域链

观察以下示例代码

const x = 1
{  // block 1
    const a = 1
    {  // block 2
        const a = 2
        {  // block 3
            const b = 3
            for (i = 1; i < 10; i++){  // block 4
                const c = 4
                console.log(i, a, b, c, d, x, y)
            }
        }
    }
}

说明: 当代码执行到 console.log(a, b, c, d, x, y) 时,解释器会先在其所在的作用域,即 block 4 中寻找对应变量。如果没有,则往父层作用域中寻找,依次向上,直至全局作用域,寻找目标变量。找到则停止该变量的查找,找不到则报错“未定义”

本例中几个特殊变量

  • y: 一直往上无法找到。则报错“未定义”
  • a: 由于依次往上,先找到值为 2 ,因此 a 会是 2。即使 a = 1 对子层来说也是作用域,但 a = 2 会被先找到

此处依次向上查找的过程 block 4 -> block 3 -> block 2 -> block 1 -> 全局 就是作用域链

代码执行到任何位置都会有作用域链,此时可访问作用域链上的所有变量(同名时,内层会覆盖外层,例如本例中指定到最里边时的 a 变量)

函数

  • arguments 函数的参数列表(类数组对象),仅非箭头函数可用
    function func() {
        console.log(arguments)
        // 不是真数组,会报错
        // arguments.forEach(element => console.log(element))
        // Array.from 将类数组迭代
        Array.from(arguments, ele => console.log(ele))
        // 使用 <func>.call 让指定对象调用 <func> 函数
        ;[].forEach.call(arguments, element => console.log(element))
    }
    func(1,2,3)
    // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
    // 1 // 2 // 3 // 1 // 2 // 3
    
  • 可变参数(需配合展开符使用)
    function func1(...arr) {  // ... 展开符
        console.log(arr)  // arr 可变参数
        console.log(arguments)
    }
    func1(1,2,3)
    // (3) [1, 2, 3]
    // Arguments(3) [1, 2, 3, callee: <accessor>, Symbol(Symbol.iterator): ƒ]
    
    // 与普通参数一起使用
    function func2(p1, p2, ...parr) {
        console.log(parr)  // 可变参数为 [3,4,5,6,7]
        console.log(arguments)
        // Arguments(7) [1, 2, 3, 4, 5, 6, 7, callee: (...), Symbol(Symbol.iterator): ƒ]
    }
    func2(1,2,3,4,5,6,7)
    

call&apply

观察以下代码

name = 'Alice'

function greet(msg1, msg2) {
    console.log(this.name, "say: ", msg1, msg2)
}
const person = {
    name: 'Bob'
}
// 直接调用时,greet 中的 this 指向全局作用域,读取到的 name 就是全局变量 name
greet('Hi.', 'Hello.')
// Alice say:  Hi. Hello.

Function 原型中存在两个函数 Function.prototype.callFunction.prototype.apply,用于进行动态调用函数(调用对象或参数是需要动态变化的)

使用方法为 <func>.call/apply(参数),以下是参数说明

  • 第一个参数 thisArg: 为 this 指向,即调用目标函数的对象。 <func>.call/apply(obj) 即为 obj.<func>() 如果函数不处于严格模式,当第一个参数为 nullundefined 时,会被替换为全局对象,原始值会被转换为对象

  • 后续参数有所不同

    • call 目标函数的参数作为 call 的后续参数 call(thisArg, arg1, arg2, /* …, */ argN)
      greet.call(person, 'Hi.', 'Hello.')
      // Bob say:  Hi. Hello.
      
    • apply 目标函数的参数以数组方式传入 apply(thisArg, argsArray)
      greet.apply(person, ['Hi.', 'Hello.'])
      // Bob say:  Hi. Hello.
      
    • call 配合数组展开符
      greet.call(person, ...['Hi.', 'Hello.'])
      // Bob say:  Hi. Hello.
      

高阶函数

如果一个函数的参数或返回值是一个函数。则该函数称为高阶函数

例如:

  • 数组的多个遍历相关函数 map , some, every ... 其参数包含一个函数 #数组遍历相关高阶函数

  • 闭包时,函数的返回值就是高阶函数

匿名函数

没有函数名的函数表达式

形如 function(){}() => {}。下面对匿名函数常见用法进行说明

赋值给变量

通过 const func1 = function() {} 将函数赋值给变量

赋值给对象的属性(this指向说明)

此时该匿名函数就是对象的方法

const obj1 = {name: 'obj1'}
obj1.getName = function(){return this.name}

Tips: 普通对象的方法最好不要使用箭头函数(构造函数实例化时不会存在问题),会出现 this 指向问题。箭头函数中的 this 指向箭头函数声明时所在的作用域

对象本身不会创建作用域名,即使对象嵌套,依然指向最外层对象所在作用域(对象的属性是一个对象,该内层对象的方法使用箭头函数读取 this 时,依然指向外层函数的作用域)

obj1.getName = () => {return this.name}
obj1 = {
    name: 'obj1',
    getName: () => {return this.name}
}
// 以上两种对象属性是箭头函数的写法,等效为以下两行代码
const getName = () => this.name;  // 此时由于先定义了函数,this 指向该箭头函数声明行所在作用域
obj1.getName = getName;  // 再将该函数绑定到对象上。此代码中,函数内的 this 依然保持指向全局

以下是更为复杂的情况(箭头函数绑定的对象的作用域不是全局)。首先定位箭头函数的声明

var name = 'glb'
function func1() {
    var name = 'func1'
    var obj1 = {name: 'obj1'}
    function func2() { 
        var name = 'func2'  // 1. 局部变量,不会挂载到 func2 的 this 上。除非编写 this.name
        obj1.getName = () => {return this.name}  // 0. 此时 this 是 func2 的 this
    }
    func2()  // 2. 当一个普通函数被独立调用(没有上下文)时,它内部的 this 默认指向全局对象。因此 func2 的 this 就是全局
    console.log(obj1.getName())
    // 输出 glb
}
func1()

// () => {} 箭头函数声明所处作用域的 this 就是箭头函数的 this

关于 this:

  • 一般情况(非严格模式)下,this 始终指向全局。如函数的独立调用。(严格模式,this 指向 undefined
  • 隐式绑定: 当非箭头函数有上下文对象时,如 obj.func() ,函数内的 this 指向该对象(箭头函数除外,其始终指向所在行的作用域)
  • 隐式丢失
    • 使用另一个变量来给函数取别名
      var func1 = obj.func
      // func1 会丢失 this 指向。使用 `func1()` 时,内部的this恢复指向全局(因为调用时,调用者是全局)
      // func 作为 obj 的方法,`obj.func()` 依然保持 this 指向 obj
      obj1.func = obj.func  // 别名,发生隐式丢失
      obj1.func()
      // 发生隐式丢失,此时调用者是 obj1,因此 this 指向 obj1
      
    • 将函数作用参数传递时会被隐式赋值,回调函数丢失 this 绑定,转移到指向全局
      function callback(func) {
          func()
      }
      obj = {num: 0, func(){
          console.log(this.num)
      }}
      num = 1
      obj.func()
      // 0   // 根据上下文,this 指向为 obj
      callback(obj.func)
      // 作为参数,发生隐式丢失
      
  • 显示绑定: 使用 call apply bind(箭头函数无法使用这些方法修改 this 指向)
  • new 绑定。通过 new 创建的对象,构造时绑定的方法内的 this 指向该对象
    num = 0
    obj = {
        num: 1,
        func() {
            console.log(this.num)
            return ()=>{console.log(this.num)}
        }
    }
    obj.func()
    // 1  // func 中的 this 指向 obj
    obj.func()()
    // 1  // 先执行 func 中的打印,输出 obj 的 num
    // 1  // 返回的箭头函数被执行,其 this 等同于定义行作用域的 this。
    // 由于定义在 func 中,且 func 中的 this 指向 obj。故 箭头函数的 this 也指向 obj
    

作为函数的参数(类似回调)

setTimeout(() => {
    console.log(1)
}, 1000)

作为函数返回值(闭包)

在 js 中,某些变量需要全局访问,但又不想被轻易修改。

因此采用函数嵌套,使变量的作用域在一个函数中,内层函数对这个变量进行引用与修改。再通过一个变量接收内层函数,当需要此变量时,直接调用内层函数

function createCounter() {
    let count = 0; // 局部变量

    // 返回一个内部函数(匿名函数)
    return function() {
        count++; 
        console.log(count);
    };
}

const myCounter = createCounter(); // 使用一个变量接收内层的匿名函数

myCounter(); // 输出 1
myCounter(); // 输出 2
myCounter(); // 输出 3

在 python 中,也能实现相同效果。但 python 的内层函数不能直接通过作用域链访问外层函数的变量与全局变量,需要使用关键字 nonlocalglobal

自执行函数(分隔命名空间)

多个人员同时编辑一个文件时,在 ES6 前由于作用域仅为 全局作用域 和 函数作用域

为了防止全局作用域下的标识符重名,使用匿名函数使自定义标识符作用域仅在自定义函数内

以下以 变量 a 在三种命名空间为例,说明自执行函数的三种写法

!function(input){
    var a = input
}(1)  // 使用 `!` `+` `-` `~`

;(function(input){  // 括号开头使用 ;
    var a = input
}('a'))  // 参数在括号内

;(function(input){
    var a = input
})({})  // 参数在括号外

ES6 可使用普通代码块 与 const / let 关键字进行标识符隔离

对象

key 只能为字符串或符号

字面量声明

const prop = 'name'
const prop1 = 'a'
const prop2 = 'ge'
const obj2 = {[prop]: 'obj2', [prop1 + prop2]: 10}
console.log(obj2.name, obj2.age)

属性

对象的属性可以是 Symbol,关于 Symbol 详见 #内置对象 Symbol

  • 属性遍历
    const readPropsObj = {a: 1, b: 2, c: 3, [Symbol()]: 4}
    // 方法1: 获取属性数组,Symbol属性 和 描述符为不可枚举的 无法获取
    console.log(Object.keys(readPropsObj))
    // 方法2: for key in obj, Symbol属性 和 描述符为不可枚举的 无法获取
    for(let key in readPropsObj) {
        console.log(key, readPropsObj[key])
    }
    // 方法3: 获取 Symbol 属性的数组 - 此方法只输出对象的 Symbol,不包含普通属性
    console.log(Object.getOwnPropertySymbols(readPropsObj))  // [Symbol()]
    
  • 查询是否含有指定属性
    const obj1 = {name: 1}  // 字面量方式声明对象  Object() 构造函数声明对象
    console.log('name' in obj1, 'name1' in obj1)
    

封包与解构

ES6 语法

  • 封包
    // 根据变量快速创建对象
    a = 1
    b = 2
    c = 3
    d = {a, b, c}
    console.log(d) // {a:1, b:2, c:3}
    
  • 解构
    //根据对象快速声明变量 - 对象快速解构
    e = {f:1}
    const {f} = e  // `f = e.f = 1` 将对象属性 f 声明为一个同名变量
    console.log(f)  // 1
    const {f:ff} = e  // `ff = e.f = 1`  将对象属性 f 的值赋值给变量 ff
    console.log(ff)  // 1
    // 声明与解构不在同一行时需要括号
    let g
    ({g} = {g:1})  // 先声明再解构时,需要使用小括号,将解构处括起。防止解释器解释成对象赋值给代码块
    console.log(g)  // 1
    // 解构默认值
    const {aa: aaa = 100} = {a: 1}  // 不存在时使用默认值
    console.log(aaa)  // 100
    

对象方法

const obj3 = {}
// 写法1 - 函数对象
function test (){}
obj3.test = test
// 写法2 - 匿名函数
obj3.test = function() {}
// 写法3 - 箭头函数
//(注意此写法会产生之前在匿名函数处提到的this指向问题)
obj3.test = () => {}
// 写法4 - 声明时的匿名函数简写
const obj4 = {
    test() {  // 等效为 test: function() {}
        console.log(1)
    }
}
console.log(obj4)
// 写法5 - 使用封包
const obj5 = {test}  // 等效为 {test: test}
console.log(obj5)

展开符

var a = {a:1}
var b = {...a, b:2}
console.log(b)
var c = {...b}  // 快速复制
console.log(c)

Object部分方法

obj = {a: 1, b: 2}
Object.entries(obj)  // 键值对二维数组
// [['a', 1], ['b', 2]]
Object.keys(obj)  // 键数组
// ['a', 'b']
Object.values(obj)  // 值数组
// [1, 2]

arr = Object.entries(obj)
Object.fromEntries(arr)  // 二维数组/Map转对象
// {a: 1, b: 2}

作为函数参数

与数组类似,此时函数传入的为对象的引用。即函数内修改键值对,在函数调用结束后会对该对象真实生效

在 ES6 中新增了通过 class 关键字创建类的方式

class ES6Class {
    // class 关键代码块内的代码,默认都是严格模式

    // 实例属性,只能通过实例访问,每个实例独立拥有
    attr  // 无初始值
    attrInt = 10  // 有初始值

    // 静态属性/类属性,只能通过类访问
    static clsAttr  // 无初始值
    static clsAttrInt = 20  // 有初始值

    // 实例方法 - this 指向对象
    func = function(){console.log(this.attrInt)}
    // 可简写为
    // func() {console.log(this.attrInt)}
    // 使用箭头函数
    // func = () => {console.log(this.attrInt)}  // 此处 this 指向不会混乱
    // JS 引擎底层,箭头函数被等效转换到 constructor(构造函数)里面去执行的。constructor 内部的 this 就是正在被 new 出来的那个实例对象本身

    // 类方法 - this 指向类
    static clsFunc = function(){console.log(this.clsAttrInt)}
}

obj1 = new ES6Class()

// 实例属性
console.log(obj1.attrInt)
console.log(obj1.clsAttr)  // 类属性,对象无法访问 undefined

// 访问 与 修改 类属性
ES6Class.clsAttr = 15  // 无初始值的属性需要手动赋值
console.log(ES6Class.clsAttr)
console.log(ES6Class.attrInt)  // 实例属性,类无法访问 undefined

// 实例方法
obj1.func()
// ES6Class.func()  // 实例方法,类无法访问 TypeError: ES6Class.func is not a function

// 类方法
ES6Class.clsFunc()

Tips: 实例方法声明时,普通方法声明与箭头函数声明有不同点。假设类存在普通方式声明的实例方法 normalFunc 与箭头函数声明的 arrowFunc

  • 箭头函数的优势: 当需要把方法提取出来单独调用(比如作为事件回调)
    • const testNormal = obj.normalFunc 普通函数的 this 指向取决于执行。独立调用时 this 本该是 window,但 class 内部默认严格模式,this 变成了 undefined。如果其中存在this调用,会出现混乱
    • const testArrow = obj.arrowFunc 箭头函数的 this 指向取决于定义位置。它诞生在 constructor 里,this 早就永久绑定为 obj 实例了
  • 箭头函数的缺点
    • 使用普通方法 normalFunc 是挂载在类的原型(prototype)上的。如果 new 了 1000 个实例,内存里其实只有 1 个 normalFunc 函数,大家共享,非常节省内存。
    • 箭头函数 arrowFunc = () => {}:它是作为属性直接挂载在实例上的。因为每次 new 实例化时,constructor 都会重新运行一次,所以如果 new 了 1000 个实例,内存里就会创建 1000 个长得一模一样的箭头函数,比较占用内存

构造函数

在初始化时自动调用,可以给属性赋值

class ClassConstru {
    // 由于构造方法进行了属性绑定,此处的实例属性声明可以省略
    attr1
    attr2
    
    // 构造函数 - this指向对象
    constructor(p1, p2) {  // 实例化时的形参
        this.attr1 = p1
        this.attr2 = p2

        // 构造函数不应有返回值
        // 返回一般值无效,例如 int
        // 返回对象时,构造出来的对象就是返回的对象,导致类的声明无意义
    }
}
obj2 = new ClassConstru('1')  // 在实例化时会自动执行 class 内的 constructor
console.log(obj2)
// ClassConstru {attr1: '1', attr2: undefined}

封装

class PrivAttrCls {
    // 私有属性使用 # 声明
    #privAttr
    // 构造时修改私有属性
    constructor(p) {this.#privAttr = p}
    // 获取方法 - 私有属性只能通过定义实例方法进行获取
    getPrivAttr() {return this.#privAttr}
    // 修改方法 - 私有属性只能通过定义 实例方法 或 构造方法 进行修改
    setPrivAttr(p) {this.#privAttr = p} 
}
obj3 = new PrivAttrCls('p')  // 私有属性赋值
console.log(obj3)  // PrivAttrCls {#privAttr: 'p'}
// console.log(obj3.#privAttr)  // 报错  Private field '#privAttr' must be declared in an enclosing class
obj3.setPrivAttr('priv')  // 通过 实例方法 或 构造方法 才能修改私有属性
console.log(obj3.getPrivAttr())  // 通过实例方法才能访问私有属性
// 可以不提供 set 方法,来实现属性是只读的效果

// 为了使私有属性可以通过 <class>.<attr> 方式访问与修改,在声明时将私有属性与 `get` `set` 关键字一起使用
class PrivAttr {
    #privAttr
    get privAttr() {return this.#privAttr}
    set privAttr(p) {this.#privAttr = p}
}
obj4 = new PrivAttr()
obj4.privAttr = 25  // 调用 `set privAttr` 方法
console.log(obj4.privAttr)  // 调用 `get privAttr` 方法
// 25

继承与重写

class Parent {
    #attr
    get attr() {return this.#attr}
    set attr(p) {this.#attr = p}
    constructor(p) {this.attr = p}
}
class Child extends Parent {
    // 重写构造方法
    constructor(p){
        // 重写构造方法,需要保持参数一致,并调用父类的构造方法
        super(p)
        this.attr = p + '1'
    }
}
const child = new Child(1)
console.log(child)  // 继承并重写父类构造方法
// Child {#attr: '11'}
child instanceof Parent  // 对象是否为目标类或其子类
// true

可选链

判断内容是否存在

class Test {
    func() {console.log('call func()')}
}
const test = new Test()
test && test.func && test.func()  // 判断对象是否存在,对象的对应属性是否存在
test?.func?.()  // 方法的可选链

拷贝

  • 同一指向,不是复制
    var arr1 = [1,2,3,4,5]
    // 都是引用,指向同一个对象
    var arr2 = arr1  
    arr1[0] = 6
    // 因为指向同一个对象,因此通过一个引用修改,另一个也会被影响
    console.log(arr2[0]) // 6 
    

浅拷贝

得到两个不同的数组/对象,但数组/对象内的非基本类型元素还是同一引用

  • 使用展开符进行浅 copy
    • 数组
      var arr1 = [0, {x:1}]
      var arr2 = [...arr1]
      arr1[0] = 1
      arr1[1]['x'] = 2
      // arr2 的第 0 项不是引用,不受影响
      console.log(arr2)  // [0, {x: 2}]
      
    • 对象
      var obj1 = {name: 'Jacky', age: 35, data: {x:1}}
      var obj2 = {...obj1}
      obj1.age = 36
      obj1.data.x = 2
      console.log(obj2.age, obj2.data)  // 35 {x: 2}
      
  • 循环赋值
    • 数组
      var arr1 = [{x:1}]
      var arr2 = []
      arr1.forEach(i => arr2.push(i))
      arr1[0]['x'] = 2
      console.log(arr2[0])  // {x: 2}
      
    • 对象
      var obj1 = {name: 'Jacky', age: 35, data: {x:1}}
      var obj2 = {}
      for (const k in obj1) {
          obj2[k] = obj1[k]
      }
      obj1.data.x = 2
      console.log(obj2.data)  // {x: 2}
      
  • 使用对应的修改方法
    • 数组 slice
      var arr1 = [{x:1}]
      var arr2 = arr1.slice()
      arr1[0]['x'] = 2
      console.log(arr2[0])
      
    • 对象 assign
      var obj1 = {name: 'Jacky', age: 35, data: {x:1}}
      var obj2 = Object.assign({}, obj1)
      obj1.data.x = 2
      console.log(obj2.data)
      

深拷贝

  • 浏览器方法 structuredClone(node环境没有,会报错)
    var obj1 = {name: 'Jacky', age: 35, data: {x:1}}
    var obj2 = structuredClone(obj1)
    obj1.data.x = 2
    console.log(obj2.data)  // {x: 1}
    
  • 使用 JSON,会丢失方法与一些特殊内容
    var obj1 = {name: 'Jacky', age: 35, data: {x:1}}
    var obj2 = JSON.parse(JSON.stringify(obj1))
    obj1.data.x = 2
    console.log(obj2.data)  // {x: 1}
    

内置对象

解释器自带对象分为两种

  • 内置对象: ES标准定义的对象,如 Date Math Array Object 等
  • 宿主对象: 浏览器提供的对象,如 window navigator document 等

以下是部分 JavaScript 标准内置对象

Symbol

用于创建唯一标识

每次调用,都会产生唯一标识

const s = Symbol()
console.log(s)  // Symbol()
console.log(typeof s)  // symbol

作为对象属性名,可以防止重名

且使用 symbol 作为属性名,该属性无法通过 for 遍历(无法得知和使用该属性)

const obj1 = {}  // 字面量方式创建一个对象
const s1 = Symbol()
obj1[s1] = 's1'
console.log(obj1, obj1[s1])
// 如果没有创建时返回的变量 s1,则无法访问
// 例如浏览器内置对象类似的预先生成对象,无法获取属性名的symbol,则无法访问该属性的值
const obj2 = Object()  // Object方法返回一个新对象
obj2[Symbol()] = 'cannot access'  // 预先定义的symbol
// 此时无法获取之前 symbol 变量,而新创建的 symbol 是全新值,因此输出 undefined
console.log(obj2, obj2[Symbol()])  // 此时无法获取之前 symbol 变量,而新创建的 symbol 是全新值,因此输出 undefined
console.log(Object.keys(obj2))  // Symbol属性无法正常遍历,此处返回空数组
// 可通过获取对象 symbol 获取到当时声明的 symbol对象,从而获取属性值
obj2[Object.getOwnPropertySymbols(obj2)[0]]  // 'cannot access'

Date

用于对时间进行获取与处理。以下是获取时间戳的几种写法

new Date().getTime()
;(new Date).getTime()
Date.now()
;+new Date()  // 隐式转换

Map

普通对象(key 字符串 或 符号)的扩展, key 可以是任意值

var mapping = new Map()
// 必须通过 set 增加键值对
var temp = {}
mapping.set(temp, 'xxx')  // 此处键是一个空对象。直接写空对象,取值时无法得到该空对象的引用
mapping.set(undefined, 1)
console.log(mapping)
var temp1 = temp
// 获取必须通过 get
console.log(mapping.get(temp1)) // 当 key 为对象/数组等复合类型时,需保证传入引用指向相同地址
// 删除必须通过 delete
console.log(mapping.delete(temp1))
// 清空键值对
mapping.clear()
// 键值对个数
console.log(mapping.size)
// 是否包含指定键值对
mapping.has(undefined)
console.log(mapping)
// 转二维数组
var mappingArr = Array.from(mapping)
var mappingArr = [...mapping]
// 使用二维数组在构建 Map 时给予初始值
var mapping = new Map([[1,2], [true, false]])
console.log(mapping)
// 遍历
// 1. for - of  Map 自身已经实现了 iterator 接口
for(const ele of mapping) {
    console.log(ele)  // 键值对数组
}
// 2. for of mapping.entries()键值对 / mapping.values()值 / mapping.keys()键
// 3. forEach
mapping.forEach((value, key) => console.log(key, value))

Set

集合。一种特殊的 Map,使用 Map 实现的不能重复

var set = new Set()
// 添加元素
set.add(1)
// 删除
set.delete(1)
// 是否包含
set.has(1)
// 数量
set.size
// 清空
set.clear()
// set转数组
var arr = [...set]
// 数组转set
var set = new Set(arr)
// 数组去重
var arr = [...new Set(arr)]
// 遍历
for(const ele of set) {
    console.log(ele)
}
for(const ele of set.entries()) {  // Set 是特殊的 Map,键值相同,利用 Map 键不能重复实现去重
    console.log(ele)
}

String

部分方法同 Array。由于字符串是基本类型(不可变),修改内容时不会修改原始字符串对象,而是返回一个新字符串

Tips: Class.prototype.func 表示该方法调用时需要对象,不是静态方法

var str = new String('111')  // 字符串对象
var str = 'Hello'  // 原始类型字符串  ,与字符串对象等效
str.length  // 长度
str.charAt(2) === str[2] // 获取指定索引的字符
// 拼接,不修改原始字符串
console.log(str.concat(' ', 'world!'), str)
// 开头结尾
console.log(str.startsWith('H'), str.endsWith('o'))
// 搜索
console.log(str.indexOf('l'), str.lastIndexOf('l'))
// 是否包含字符
console.log(str.includes('z'))
// 填充 - X => 00X - 不修改原始
console.log('1'.padStart(3, '0'), '1'.padEnd(3, '0'))
// 重复
console.log('='.repeat(10))
// 切分
console.log(str.slice(1, 3))  // 开始索引与结束索引
console.log(str.substring(1, 3))  // 开始索引与结束索引
console.log(str.substr(1, 2))  // 开始索引与切分长度
// 修改
console.log(str.splice(1, 0))  // 指定索引处,移除指定数量的字符
// 大小写
console.log(str.toLowerCase(), str.toUpperCase())
// 移除前后空 - python中的 strip()
console.log(str.trim(), str.trimStart(), str.trimEnd())
// 编码
// String.prototype.charCodeAt() - 获取字符的 ascii 码
console.log(Array.from('abc', c => c.charCodeAt(c)))
// String.fromCharCode() - ascii码转字符
console.log(String.fromCharCode(65,66,67,68))

RegExp

Regular expression,检索或替换指定规则的文本

// 构造表达式声明正则时,匹配内容为字符串,因此需要编写转义字符,参数 2 为模式, i 表示不区分大小写  - 字符串可进行混淆与加密
var reg = new RegExp('\\d+', 'i')
// 隐式创建,字面量创建
// 此时正则字符串不需要转义,用两个 / 包裹,后跟模式 - 不能进行混淆加密
var reg = /\d+/i
console.log(reg.test('11aa22bb33cc'))
// 是否匹配到目标格式字符串,是否包含
console.log(reg.exec('11aa22bb33cc'))
// 匹配出符合规则的内容(只返回第一项) 
// ['11', index: 0, input: '11aa22bb33cc', groups: undefined]
console.log(reg.exec('aabbcc'))
// 匹配不到符合规则的内容返回 null

匹配模式/修饰符

  • i 忽略大小写
  • g 全局匹配(多次匹配),正则对象会初始化一个计数器,每次匹配时会增加次数
  • m 多行匹配
  • s dotAll(点匹配全部)模式,. 可以匹配换行符
var reg = /\d+/g
console.log(reg.test('11aa22bb33cc'))
// 匹配出第 1 项符合条件的 '11' - 返回 true
console.log(reg.exec('11aa22bb33cc'))
// 匹配出第 2 项符合条件的 '22' - 返回 ['22' ....]
console.log(reg.exec('11aa22bb33cc'))
// 匹配出第 3 项符合条件的 '33' - 返回 ['33' ....]
console.log(reg.exec('11aa22bb33cc'))
// 匹配不到第 4 项符合条件的 - 返回 null
// 一般通过循环,条件为返回值是否为null,得到所有满足条件的子字符串 
// while(const res = reg.exec(str)) {}
console.log(reg.exec('11aa22bb33cc'))
// 循环到第 1 项符合条件的 '11' - 返回 ['11' ....]

量词

  • 符号量词

    • + 1次到多次
    • * 0次到多次
    • ? 0次或1次
  • 指定次数

    • {0} 指定字符出现指定次数(此处表示被修饰的内容没有出现)
    • {0,1} 指定字符出现0-1次(等效为'?')
    • {1,} 指定次数出现1次异常(等效为'+')
  • 惰性匹配,尽量少匹配字符,在 量词后增加 ?

console.log(/a.*b/.exec('a1b2c3b'))
// 默认是贪婪匹配 - 整个字符串匹配后,一个个往外吐出字符
console.log(/a.*?b/.exec('a1b2c3b'))
// ^ 只匹配指定规则开头的字符串   $ 只匹配行末
var str = 'ab1ab2ab3b'
let res = null
var reg = /ab/g
var reg = /^ab/g
while (res = reg.exec(str)) {
    // 不要把正则直接写在while中,不然每次会生成新的正则对象,导致正则对象的计数器也是新的,从而死循环
    console.log(res)
}

元字符

  • . 除换行符外的任意字符(s模式可以匹配全部)
  • [a-d] 字符范围 a-d, [xyz] 字符范围 x/y/z , [^xyz] 反选,不是x/y/z的字符串(中括号内表示反选,中括号外表示行首)
  • \w 字母数字下划线(等效[a-zA-Z0-9_])
  • \W 不包含字母数字下划线(\w的反选)
  • \d 数字(等效[0-9])
  • \D 非数字(\d的反选,等效[^0-9])
  • \s 空白字符
  • \S 非空字符
  • [\w\W] [\s\S] ... 等表示匹配所有字符。类似 s 模式下的 .
    console.log(/[a-c]+/.exec('1acc122bcd33asd11aaa'))
    // ['acc', index: 1, input: '1acc122bcd33asd11aaa', groups: undefined]
    
  • \b 单词边界
  • \B 非边界
    console.log(/h.+o\b/.exec('hxoob hi hello ooo aluoha'))
    // 匹配出 'hxoob hi hello ooo'。 h 开头 o 结尾,o后方是边界 的 贪婪匹配
    console.log(/h.+?o\b/.exec('hxoob hi hello ooo aluoha'))
    // 匹配出 'hxoob hi hello'   h 开头 o 结尾,o后方是边界 的 惰性匹配
    console.log(/h.+?o\B/.exec('hxoob hi hello ooo aluoha'))
    // 匹配出 'hxo'。 h 开头 o 结尾,o后方不是边界
    
  • \ 匹配特殊字符的转义符
    console.log(/\.+/.exec('xxx...xxx...xxx'))
    // 将 \ 修饰的 '.' 作为一个字符进行匹配
    

分组

可用于匹配出子表达式

  • | 或,匹配某一种规则
    console.log(/Hello|world/.exec('Hello JS!'))
    // `|` 前方都是条件1,后方都是条件2
    console.log(/Hello|world/.exec('Hi world!'))
    // 使用括号限制条件长度 - 同时返回值多了分组内容(第二项)
    console.log(/a(b|c)z/.exec('zzzabzzz'))
    // ['abz', 'b', index: 3, input: 'zzzabzzz', groups: undefined] - 子表达式
    console.log(/a(b|c)z/.exec('zzzaczzz')) 
    
  • 是否捕获。使用分组时默认捕获返回值,通过 ?: 使分组不捕获返回值
    console.log(/a(b|c)z/.exec('zzzaczzz'))
    // 返回值第二项由index变为分组内容
    // ['acz', 'c', index: 3, input: 'zzzaczzz', groups: undefined]
    console.log(/a(?:b|c)z/.exec('zzzaczzz'))
    // 在分组前增加 ?: ,使分组内容不进行返回,此时返回内容的第二项依然是index
    // ['acz', index: 3, input: 'zzzaczzz', groups: undefined]
    
  • 反向引用: 引用前一个子表达式捕获到的结果
    // 匹配前两个字符与后两个字符相同,中间为任意字符的长度为5的字符(形如AAxAA)
    console.log(/(.{2}).\1/igs.exec('aacaabaabbcacc'))  // 此处的 `\1` 为占位符,值为第一个子表达的结果
    // ['aacaa', 'aa', index: 0, input: 'aacaabaabbcacc', groups: undefined]
    
  • 零宽断言: 匹配指定条件开头的字符串(条件不占用宽度,不作为最后的匹配结果),使用?开头
    • 分类
      • 先行/后行:决定了匹配内容在前还是后(有<表示后行)。
      • 正向/反向:决定了期望的结果(必须有还是必须没有)( =/! )。
    • <= 正向后行 E.g. 获取数字开头的 3 个连续字母(正向: 需要符合条件, 后行: 要匹配的目标在条件后面)
      var str = 'axyz1abcb0xZy2xyz'
      var reg = /(?<=\d)[a-z]{3}/igs  // ? + <=
      var ret = null
      while(ret = reg.exec(str)) {
          console.log(ret)
      }
      // ['abc', index: 5, input: 'axyz1abcb0xZy2xyz', groups: undefined]
      // ['xZy', index: 10, input: 'axyz1abcb0xZy2xyz', groups: undefined]
      // ['xyz', index: 14, input: 'axyz1abcb0xZy2xyz', groups: undefined]
      
    • <! 反向后行 E.g. 获取非数字开头的 3 个连续字母
      console.log(/(?<!\d)[a-z]{3}/igs.exec('1abc2abcyabc3abc'))
      // ['bcy', index: 5, input: 'axyz1abcb0xZy2xyz', groups: undefined]
      
    • = 正向先行 E.g. 获取数字结尾的 3 个连续字母
      console.log(/[a-z]{3}(?=\d)/igs.exec('1abcyabc2abczabc'))
      // ['abc', index: 5, input: 'axyz1abcb0xZy2xyz', groups: undefined]
      
    • ! 反向先行 E.g. 获取非数字结尾的 3 个连续字母
      console.log(/[a-z]{3}(?!\d)/igs.exec('1abc1abc2abc#abc'))
      // ['abc', index: 9, input: '1abc1abc2abc#abc', groups: undefined]
      
  • ?<> 分组命名(?<分组名>匹配符)。将捕获到的内容放入 groups 中,方便获取
    console.log(/<a href="(?<url>.*?)">(?<text>.*?)<\/a>/.exec('<a href="http://www.google.com">Google</a>').groups)
    // groups: {url: 'http://www.google.com', text: 'Google'}
    

匹配html

var htmlStr = `
    <p>
    <div>
        <a href="http://www.google.com">Google</a>
        <a href="http://www.bing.com">Bing</a>
        <a href="http://www.baidu.com">百度</a>
    </div>
    xxx
    <span>
`
// 构造目标格式的第一项,记得全局模式,不然后续会死循环
var reg = /<a href="http:\/\/www.google.com">Google<\/a>/g
// 将 url 内容替换为 \w 和 \.(注意'.'的转义)  标签内容可能是中文,使用 '.' 进行匹配(.*? - 常用的万能匹配,注意s模式)
var reg = /<a href="http:\/\/\w+\.\w+\.\w+">.+?<\/a>/g
// 匹配标签的属性时,常常使用 [^"]*? 排除引号的惰性匹配获取引号内的属性内容。[\s\S]*? 用于在非s模式下,出现换行符等内容的万能匹配
var reg = /<a href="http:\/\/[^"]*?">[\s\S]*?<\/a>/g
// 通过分组获取具体内容
var reg = /<a href="(http:\/\/[^"]*?)">([\s\S]*?)<\/a>/g
var retval = null
while(retval = reg.exec(htmlStr)) {
    console.log(retval)
}
// (3) ['<a href="http://www.google.com">Google</a>', 'http://www.google.com', 'Google', index: 27, input: '\n    <p>\n    <div>\n        <a href="http://www.goo….baidu.com">百度</a>\n    </div>\n    xxx\n    <span>\n', groups: undefined]
// (3) ['<a href="http://www.bing.com">Bing</a>', 'http://www.bing.com', 'Bing', index: 78, input: '原始字符串,同上,此处省略', groups: undefined]
// (3) ['<a href="http://www.baidu.com">百度</a>', 'http://www.baidu.com', '百度', index: 125, input:'原始字符串,同上,此处省略', groups: undefined]

正则替换

  • replace 支持字符串与正则
    console.log('13911112222'.replace('1111', '****'))
    console.log('13911112222'.replace(/(?<=1[3-9]\d+)\d{4}/, '****'))
    // 139****2222
    
  • replaceAll
    // replace + 全局模式下,会将所有匹配项都进行替换
    console.log('aaxxccx'.replace(/x/g, '*'))
    // replaceAll 等效为 replace + 全局模式
    console.log('aaxxccx'.replaceAll('x', '*'))
    // aa**cc*  
    
  • match 获取匹配结果
    // 非全局模式,返回内容等效为 RegExp.prototype.exec 第一次的结果
    console.log('aaxxaacc'.match(/aa/))
    // ['aa', index: 0, input: 'aaxxaacc', groups: undefined]
    // 全局模式,直接返回匹配到的所有字符串组成的的数组(无 index,groups 等信息)
    console.log('aaxxaacc'.match(/aa/g))
    // ['aa', 'aa']
    
  • matchAll 返回可迭代对象(后面的正则必须全局模式
    console.log('aaxxaacc'.matchAll(/aa/g))
    // RegExpStringIterator {}
    // 使用数组解包将迭代对象解包
    // 得到 RegExp.prototype.exec 全部结果组成的数组(因为exec结果本身为数组,最终是二维数组)
    console.log([...'aaxxaacc'.matchAll(/aa/g)]) 
    // [
    //   ['aa', index: 0, input: 'aaxxaacc', groups: undefined],
    //   ['aa', index: 4, input: 'aaxxaacc', groups: undefined]
    // ]
    
  • search 获取匹配内容的索引
    console.log('abc123'.search(/\d/))
    // 3
    
  • split 字符串切分,支持字符串与正则
    console.log('123a321b123c321'.split('b'))
    // ['123a321', '123c321']
    console.log('123a321b123c321'.split(/[a-z]/))
    // ['123', '321', '123', '321']
    

包装类

对象才能调用方法与属性,但以下原始类型也能调用方法与属性

String,Number,BigInt,Boolean 等,在需要的时候,解释器会对这些原始类型包装成对象,从而调用对应方法

该过程是解释器自己完成,无需手动操作

''.length
// 0
;''.charCodeAt()
// NaN

Tips: nullundefined 没有包装类,没有方法可以调用

Function

函数的构造方法。自定义函数所属的类 func.__proto__ === Function.prototype

  • 使用 Function 创建函数
    func = new Function('a', 'return a')  // 参数为入参与函数体
    // 等效为以下内容
    // function func(a) {return a}
    
  • 构造器过debugger: 已知 func.constructor = func.__proto__.constructor === Function.prototype.constructor === Function
    // 将构造器置空
    Function.prototype.constructor = function(){}
    // 再此之后声明的函数
    function  func(params) {return params}
    // 其构造器就是空函数
    func.constructor  
    // 输出 ƒ (){}
    
    • Function 构造 debugger: 以下是单次的 debugger。通过与定时器配合实现反复触发 debugger 但不影响主进程执行业务逻辑
      // 通过自定义函数构造器,调用 Function 构造 debugger 并调用
      func.constructor('debugger').call() 
      // 通过匿名函数
      (function(){}.constructor('debugger').call())
      
    • 方法
      • 重写定时器
        _setInterval = setInterval
        setInterval = function(a, b){
            // 判断无 debugger 关键字,再正常设置定时器
            if (a.toSting().indexOf('debugger') === -1) {
                return _setInterval(a, b)
            }
        }
        
      • 重写构造器
        _constructor = Function.prototype.constructor
        Function.prototype.constructor = function() {
            if(arguments[0] === 'debugger') {
                // 什么都不做
            } else {
                return _constructor.apply(this, arguments)
            }
        }
        
      • 重写代码: 在发生debugger时查看堆栈,定位触发debugger的代码(函数),在执行代码块前下断点。刷新后重写该代码,再放行

全局函数

  • url编解码 (注意,根据所用函数不同,编码时仅对query参数内容进行编码)
    • 编码
      // encodeURIComponent - 此函数只能对参数内容进行编码
      console.log(encodeURIComponent('params?a=[1]&b=测试'))  // 此写法 `?` `=` `&` 也会被编码
      // params%3Fa%3D%5B1%5D%26b%3D%E6%B5%8B%E8%AF%95
      console.log(`params?a=${}&b=${encodeURIComponent('测试')}`)  // 正确写法encodeURIComponent('[1]')
      // params?a=%5B1%5D&b=%E6%B5%8B%E8%AF%95
      
      // encodeURI - 此函数可以只对参数进行编码
      console.log(encodeURI('params?a=[1]&b=测试'))
      // params?a=%5B1%5D&b=%E6%B5%8B%E8%AF%95
      
    • 解码
      console.log(decodeURIComponent(decodeURI('params?a=%5B1%5D&b=%E6%B5%8B%E8%AF%95')))
      console.log(decodeURI('params?a=%5B1%5D&b=%E6%B5%8B%E8%AF%95'))
      
  • unicode编解码
    console.log(escape('测试'))
    // %u6D4B%u8BD5
    console.log(unescape('%21'))
    
  • str2int(也可通过 Number 类进行调用)
    console.log(parseInt('1'))
    console.log(Number.parseInt('1'))
    
  • str2float
    console.log(parseFloat('1.1'))
    console.log(Number.parseFloat('1.1'))
    
  • 是否为 NaN
    console.log(isNaN(NaN))
    console.log(Number.isNaN(NaN))
    
  • 是否有限
    console.log(isFinite(1))
    console.log(Number.isFinite(1))
    
  • 定时器
    • setTimeout 指定时间后执行一次 clearTimeout 清除定时器
    • setInterval 指定时间间隔反复执行 clearInterval 清除定时器

其他常用内置对象

  • Promise
  • async
  • await
  • 迭代器
  • 生成器 yield
  • eval
  • Proxy
  • Reflect

事件循环

在 JavaScript 的事件循环(Event Loop)模型中,最核心的是同步执行栈、宏任务队列、微任务队列

setTimeout(() => console.log(1), 0)
Promise.resolve().then(() => console.log(2))
console.log(3)
// 输出顺序:3 2 1

setTimeout 将一个回调函数放入 宏任务队列(macro task queue),等待主线程执行完后再执行。即:定时器的回调会稍后执行。

.then() 回调属于 微任务(micro task)。 微任务会在当前宏任务(也就是主线程执行完同步代码)之后、下一个宏任务之前执行。

同步代码,立即执行。

最终执行顺序

  • 执行主线程同步代码 输出 3

  • 执行所有微任务(microtasks) 输出 2

  • 执行下一个宏任务(macrotask) 输出 1

async/await

async/await 是 Promise 的语法糖。当你在代码中使用 async 函数时,它的执行分为两部分:

await 之前(包括 await 紧挨着的表达式)的代码:是同步执行的。

await 之后的代码:会被放入微任务队列中,它的行为和 Promise.then() 完全一样。

因此await内的代码也是微任务

更多类型

根据 运行环境(浏览器 vs Node.js) 的不同,还有一些“特殊通道”或“其他队列”:

  • 在浏览器环境中:除了宏任务和微任务,还有与 渲染(Rendering) 高度相关的队列:
    • requestAnimationFrame (rAF):不属于严格意义上的宏任务或微任务。浏览器在下一次重绘(Paint)之前执行的专门队列。通常用来做流畅的 JavaScript 动画。
      • 执行时机:在微任务执行完毕后,浏览器决定要渲染UI之前。
    • requestIdleCallback (rIC):顾名思义,在浏览器主线程空闲时才会执行的任务。属于优先级极低的后台任务。如果浏览器很忙,可能一直不执行。
  • 在 Node.js 环境中(非常特殊):Node.js 的事件循环比浏览器更复杂(基于 libuv),除了标准的宏任务(setTimeout 等)和微任务(Promise.then),还有两个专属的机制:
    • process.nextTick(VIP 微任务):属于微任务的范畴,但是!拥有最高优先级。
      • 在 Node.js 中,每次同步代码执行完准备去清空微任务队列时,会优先清空 process.nextTick 队列,然后再去清空 Promise.then 队列。
    • setImmediate(特定的宏任务):属于宏任务,但 Node.js 会安排在事件循环的 Check 阶段执行。一般情况下,如果和 setTimeout(fn, 0) 一起执行,本类型任务的顺序是不确定的(受机器性能影响),但在 I/O 回调中,setImmediate 总是优先于 setTimeout 执行。

总结一张全局执行顺序表(以最常见的场景为例):

  1. 执行整体代码(全局同步代码)
  2. 遇到 process.nextTick 加入 Tick 队列(仅 Node.js)
  3. 遇到 Promise.then/await 后续代码 加入 微任务队列
  4. 遇到 setTimeout/setInterval 加入 宏任务队列
  5. 同步代码执行完毕
  6. 清空 Tick 队列(仅 Node.js)
  7. 清空 微任务队列(输出示例代码中的 2
  8. 执行 UI 渲染(仅浏览器,如 requestAnimationFrame)
  9. 取出一个 宏任务 执行(输出示例代码中的 1
  10. 重复步骤 6~9...

导入导出

将一些函数、变量等内容暴露出来,供其他 js 文件导入与引用

ES6 Modules 规范

JavaScript 语言官方(ECMAScript 6 标准)在 2015 年正式推出的模块化方案。是静态的语法(在编译阶段就能确定依赖关系)

使用关键字 export export default import

浏览器环境 逐渐支持了这种写法(配合 <script type="module">

nodejs环境 要在 package.json 中配置 "type": "module",以便告诉 Node:“请用官方的 ES6 规范来解析我”

  • export 声明暴露: 在声明语句的最开始加上 export 关键字直接进行暴露

    • 暴露
      export var testStr = 'testStr'
      export function testFunc(){console.log('test')}
      
    • 导入
      • 全部导入 需要起一个别名作为接收对象,此时导入内容是一个对象
        import * as test from ‘./common-js.js’  // 别名 test
        // 调用 通过别名对象点出来
        test.testFunc()
        
      • 解构导入 通过 {} 将导入的对象解构,可实现 按需导入
        import {testStr, testFunc} from './myModule.js'
        import {testStr as str} from './myModule.js'  // 解构别名
        
  • export 统一暴露 (Named Export),将多个内容使用一个对象进行暴露。一个文件可拥有多个独立的 export(包括声明暴露和统一暴露),地位平等。

    • 暴露
      // 正常编写内容
      var testStr = 'testStr'
      function testFunc(){console.log('test')}
      // 统一暴露 - es6 封包,等效于 {testStr: testStr, testFunc: testFunc}
      export {testStr, testFunc}
      
    • 导入。可使用之前相同的写法
      // 与之前类似,用大括号,且按需引入
      import { testStr } from './myModule.js'; // 正确
      import { testStr as myStr } from './myModule.js'; // 需要将暴露的对象改名时,只能使用 `as`
      
  • default 暴露 (Default Export),一个文件只能存在一个 export default。它是这个模块的“主要输出

    • 暴露
      // 正常编写内容
      var testStr = 'testStr'
      function testFunc(){console.log('test')}
      // default 暴露对象
      export default {
          testStr: testStr,
          testFunc: testFunc
      }
      
    • 导入: 导入与之前类似,导入一个对象 或 使用结构进行按需导入
      • 全部导入。由于已经指明了默认导出的对象,因此名称可自定义
        import A from './myModule.js'; // A 就是那个对象
        import B from './myModule.js'; // B 也是那个对象,完全合法
        
      • 解构导入 与之前一样,通过 {} 将导入的对象解构,可实现 按需导入
  • 动态导入: 在代码执行过程中进行导入

    var x = true
    if (x) {
        import('./myModule.js').then(module => {
            module.testFunc()
        })
    }
    

Tips:

  • 按需加载 (Tree-shaking) 的现代前端项目中,不推荐使用 default 暴露,在导入时推荐使用解构导入

    • 因为这样打包工具(如 Webpack/Vite)能清楚地知道只引入了哪些内容,于是会把没用到的内容直接删掉不打包

    • 而如果用的是 default 暴露一个大对象,打包工具往往很难拆解这个对象,只能把整个对象全打包进去,造成代码体积变大

  • 如果一个文件使用了多个 export 声明暴露和 export 统一暴露

    • 可以通过{}的方式一行导入多个暴露里的内容。

    • 如果使用 import * as test from './myModule.js' 的方式导入,会将多个暴露都放到 test 对象里

    // ====== 暴露 ======
    // 1. 声明暴露了变量和函数
    export const name = 'Alice';
    export function sayHi() { console.log('Hi!'); }
    
    // 2. 正常编写,然后统一暴露
    const age = 18;
    const gender = 'female';
    export { age, gender };
    
    // ====== 导入 ======
    import { name, sayHi, age } from './myModule.js';
    
    console.log(name); // 'Alice'
    console.log(age);  // 18
    sayHi();           // 输出 'Hi!'
    
  • 如果一个文件使用了多个 export 声明暴露和 export 统一暴露 和一个 default 暴露。使用对象进入导入,对象的 default 属性指向 default 暴露的内容

    // ====== 暴露 ======
    export const name = 'Alice';       // 声明暴露
    export { age: 18 };                // 统一暴露
    export default function() {        // default 暴露
        console.log('我是默认导出的函数');
    }
    
    // ====== 导入 ======
    import * as test from './myModule.js';
    
    console.log(test.name);    // 'Alice'
    // 怎么调用 default 暴露的内容?
    test.default();            // 输出 '我是默认导出的函数'
    

CommonJS 规范

Node.js 诞生之初为了解决服务器端模块化问题,自己采用的一套社区规范

关键字:module.exports exports require()

导入的是动态的(在代码运行到 require 那一行时才会去同步加载文件)

  • 暴露 使用module.exports进行统一暴露
    • 暴露单个函数
      function decrypt(code) {
          return code
      }  
      module.exports = decrypt  // 暴露了一个函数对象
      
    • 暴露多个内容: 与 ES6 规范的 统一暴露与 default 暴露类似,将多个内容封包成一个对象
      function a() {}
      function b() {}
      module.exports = {a, b}
      
  • 导入: 使用 require 接收暴露的对象
    // 当暴露的是函数对象时
    const func = require('./decrypt.js');  // 接收到的就是一个函数对象,可自定义名称
    func()  // 此处的 func 函数就是 decrypt 函数
    
    const utils = require('./utils');
    utils.a();
    utils.b();
    

Tips: module.exports 到底是什么?

  • Node 在每个文件背后偷偷包了一层
    (function (exports, require, module, __filename, __dirname) {
        // 文件内的代码在这里
    });
    
  • 这是 Node.js 在运行时为每一个 CommonJS 文件自动加上的“函数外壳”。其中:
    • module.exports:最终导出的东西
    • exports:只是 module.exports 的一个引用
  • webpack 在浏览器环境中模拟了 node 中的 common js 以实现隔离
    // 分发器中代码
    return __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    
    • 浏览器环境 原生没有 CommonJS, 但 Node 模块设计太成功了
    • webpack 就在 浏览器环境 里 复刻了一套 Node 模块运行时
    • 用函数包裹模块实现浏览器模块化