本文目录

[[toc]]

类型转换

运算过程自动类型转换

转换规则

操作类型转换规则示例
string + any非字符串转为字符串console.log('3' + true + 1) // "3true1"
- / * / / / %所有操作数转为数字console.log(' 012 ' / 3) // 4
boolean 参与运算时true 转为数字是 1, false 转为数字是 0console.log(true + false) // 1
console.log(1 + true + false) // 2
null 参与运算时null 转为数字是 0 ,转为字符串是 nullconsole.log(null + 1) // 1
console.log(null + "1") // "null1"
undefined 参与运算时undefined 转为数字是 NaN, 转为字符串是 "undefined"console.log(undefined + 1) // NaN
console.log(undefined + "1") // "undefined1"
object / arraySymbol.toPrimitive > 内置的 ToPrimitive
内置 toPrimitive 优先调用 valueOf 尝试获取基本类型,如果返回非基本类型则调用 toString, toString 也不返回基本类型,抛出错误
参见下方示例
Date内置的 ToPrimitive 行为中,优先调用的是 toString 而不是 valueOfconsole.log(new Date() + 1) // Thu Feb 27 2025 11:05:01 GMT+0800 (中国标准时间)1
if / while / ! / && / ||将值转为 boolean 进行比较转换规则见 真值表
in / parseInt将值转为 string 再解析特殊场景
const obj = {
  /** 自定义隐式转换,会覆盖内置的,也就是后续不会继续调用 valueOf 与 toString ,但是还是会抛出错误 */
  [Symbol.toPrimitive]() {
    console.log('自定义转换')
    return {}
  },
  /** 覆盖内置的 valueOf 转换函数 */
  valueOf() {
    console.log('调用 valueOf')
    return {}
  },
  /** 覆盖内置的 toString 转换函数 */
  toString() {
    console.log('调用 toString')
    return {}
  }
}
console.log(1 + obj) // Uncaught TypeError: Cannot convert object to primitive value

常见的 toPrimitive

类型valueOftoString
原始类型返回自身转为字符串
Number / String / Boolean返回对应的原始值原始值对应的字符串
Symbol返回自身按照 Symbol(description) 返回
数组返回数组对象本身与调用 join 相同
Date时间戳 (1970 至今的毫秒数)时间字符串
RegExp返回对象本身返回正则的原始字符串版本(会进行转义,比如 \\ 会转为 \\\\
Math返回对象本身[object Math]
对象返回对象本身[object Object]
Function返回函数本身,即函数的引用函数源码

真值表

  • true: 0NaN''nullundefined
  • false: 非零数字、非空字符串、 Symbol 、 所有对象类型

特殊场景

// 数组转换
[] + {}       // "[object Object]" ([] → "",{} → "[object Object]")
{} + []       // 0 ({} 被解析为空代码块,+[] → 0)
console.log({} + []) // "[object Object]"

// Chrome : `[object Object][object Object]`
// Firefox: 第一个 {} 是空作用域,也就是说这里等价于 +{} ,也就是转数字,也就是 NaN
// 如果不是直接在控制台输出,而是放到 console 中,应该是没有异议的,就是 Chrome 的输出结果
{} + {}

+[] // 等价于 (+"") = 0

// 对象转换
const obj = {
  a: 123,
  valueOf: () => 2,
  toString: () => "3"
};
obj + 13优先调用 valueOf()
console.log(parseInt(obj)) // 3 , 会直接调用 toString 转为 '3' ,再 parseInt
console.log(['a'] in obj) // true , ['a'] 会被调用 toString , 转为 `'a' in obj` 也就是 true

// 日期对象
new Date() + 1"Mon Jul 18 2022 12:00:00 GMT+08001"优先调用 toString()

相等性比较

===

  • 不进行类型转换,同时比较类型与值
  • 对象比较引用地址,非相同地址为 false
  • NaN 与任何值(包括 NaN )相比都是 false
  • +0-0 视为相等

Object.is

=== 更加严格的比较,更加精准的比较

  • 0 === -0; // true

  • NaN === NaN; // false

  • Object.is(0, -0); // false

  • Object.is(NaN, NaN); // true

== 相等性比较

原则: 优先转为 number 比较,其次是 string 最后是 boolean ,有非基本类型参与比较,非基本类型先通过 toPrimitive 再比较

比较类型转换规则
boolean == 其他布尔值转数字(true → 1,false → 0)
string == number字符串转数字("5" == 5 → true)
object / array == 基本类型转基本类型(通过 ToPrimitive )再比较
null == undefined标准中的特例,二者相等(null == undefined → true)
null == 0标准中的特例, null 不会转为 0 , 所以不相等

常见比较时转换示例

[] == ![] → true

转换过程:

  1. 计算 ![]: 所有对象类型都是 true ,所以 ![] 就是 false
  2. 计算 [] == false: [] 进行 toPrimitive ,也就是 ''
  3. 计算 '' == false: 两边转数字,即 0 == 0

[0] == false

转换过程:

  1. 计算 [0]: [0] 进行 toPrimitive'0'
  2. 计算 '0' == false: 两边转数字,即 0 == 0

> / < / >= / <= 类型比较

  • 若其中之一是数字,两边转数字比较 '21' < 4 → false
  • 若两边都是字符串,按照字典序比较 '21' < '4' → true

数字计算

console.log(1 / 0) // Infinity
console.log(-1 / 0) // -Infinity
console.log(1 / -0) // -Infinity
console.log(1 / 0) // Infinity
console.log(1n / 0n) // throw Error

运算符

隐式转换 + 运算符缺省值

let a = 1
console.log(a++ + a) // 等价于 (a++) + a 等价于 a + (a + 1) = 3

console.log(1 + 2) // 3
console.log(1 + +2) // 等价于 (1 + 0) + 2 = 3
console.log(1 + + +2) // 等价于 ((1 + 0) + 0) + 2 = 3

console.log(`${1}2`) // "12"
console.log(1 + +'2') // 等价于 1 + (+"2") 等价于 1 + 2 = 3
console.log(`1${2}`) // "12"
console.log(`1${+2}`) // 等价于 "1" + (+2) 等价于 "1" + 2 = "12"

console.log(1 + true) // 等价于 1 + 1 = 2
console.log(1 + +true) // 等价于 1 + (+true) 等价于 1 + 1 = 2
console.log(`1${true}`) // "1true"
console.log(`1${+true}`) // 等价于 "1" + (+true) 等价于 "1" + 1 = "11"

console.log(1 + null) // 等价于 1 + 0 = 1
console.log(1 + +null) // 等价于 1 + (+null) 等价于 1 + 0 = 1
console.log(`1${null}`) // "1null"
console.log(`1${+null}`) // 等价于 "1" + (+null) 等价于 "1" + 0 = "10"

console.log(1 + undefined) // 等价于 1 + NaN = NaN
console.log(1 + +undefined) // 等价于 1 + (+undefined) 等价于 1 + NaN = NaN
console.log(`1${undefined}`) // "1undefined"
console.log(`1${+undefined}`) // 等价于 "1" + (+undefined) 等价于 "1" + NaN = "1NaN"
console.log(`1${+ +undefined}`) // 等价于 "1" + (+(+undefined)) 等价于 "1" + (+NaN) 等价于 "1" + NaN = "1NaN"

比较运算符连用

console.log(0 == 1 == 2) // 等价于 `console.log(false == 2)` 所以是 false
console.log(2 == 1 == 0) // 等价于 `console.log(false == 0)` 所以是 true

for infor of

如果循环期间动态添加元素

  • for in: 新元素不会被加入循环中
  • for of: 会将新元素也加入循环中执行
const list = Array.from({ length: 3 }).fill(1).map((num, idx) => num + idx)

for (const key in list) {
  if (list[key] === 2) {
    list.push(4)
  }
  console.log(list[key])
}

for (const item of list) {
  if (item === 2) {
    list.push(5)
  }
  console.log(item)
}

console.log(list)

遍历原型字段

  • for in 会遍历出原型上的字段
  • for of 只能遍历当前对象的字段,不会查找原型
const list = Array.from({ length: 3 }).fill(1).map((num, idx) => num + idx)

list.__proto__.val = 123

for (const key in list) {
  console.log(`for in ${list[key]}`)
}

for (const item of list) {
  console.log(`for of ${item}`)
}

console.log(list)

Function

arguments

修改 arguments 会影响参数值

function log(a, b, c, d) {
  console.log(a, b, c, d)
  arguments[0] = 'bfe'
  arguments[3] = 'dev'

  // 只有 a 会被修改, d 不会被修改,因为 d 本身就是 undefined ,没有被记录引用
  console.log(a, b, c, d)
}

log(1, 2, 3)

具名函数可访问性

func1() // 报错,变量提升的时候, func1 是 undefined ,并不能把函数也提升
func2() // 首先如果要执行必须注释掉上一行,否则报错了不会执行到这里,如果执行到了,这里也是报错

// 具名函数被复制后,函数外部无法通过函数名访问此函数,但是函数内部可以,并且这个命名不可被改变
var func1 = function func2() {
  console.log(2)
}
var a = 1
function a() {  // 提升到 var 变量定义之前
}

console.log(typeof a) // 具名函数在外部是可以被覆盖的,这里返回 "number"

具名函数内部重定义

在 JavaScript 中,使用 function 关键字进行函数声明时,函数会存在变量提升的情况。

所以在代码开头执行 console.log(typeof a) 时,它能够识别到 a 是一个函数,所以返回 "function"

当进入 a 函数内部,先是打印 typeof a ,此时 a 仍然是函数本身,所以返回 "function" 。接着执行 a = 'a' ,这里相当于把原本的函数 a 重新赋值为一个字符串,所以后续再执行 console.log(typeof a) 就返回 "string" 了。

在全局作用域下,最开始 a 是被当作函数声明处理的,在函数内部重新赋值改变了 a 原本指向的函数对象,使其变成了一个字符串,这一系列操作改变了全局变量 a 的类型。

function a() {
  console.log(typeof a) // "function"
  a = 'a'
  console.log(typeof a) // "string"
}

console.log(typeof a) // "function"
a()
function a(){
}
const b = function() {
}
const c = function d() {
  console.log(typeof d) // "function"
  d = 'e'
  console.log(typeof d) // "function"
}
console.log(typeof a) // "function"
console.log(typeof b) // "function"
console.log(typeof c) // "function"

// d 是一个 命名函数表达式。这个名称 d 仅在函数体(作用域)内是局部的,因此在函数体外部,typeof d 返回 undefined。
console.log(typeof d) // "undefined"
c()

函数定义作为 if 条件

如下示例, foo 会被视为 IIFE 一样的函数块,不会提升到全局,所以内外部访问 foo 都是访问不到的

if (function foo() {
  console.log('BFE')
}) {
  console.log('dev') // 会输出
  foo() // Error , foo 未定义
}
foo() // Error , foo 未定义

bind

多次 bind 时,只有第一次 bind 会生效,后续会像箭头函数一样无法修改 this

深拷贝

structuredClone 无法拷贝的数据类型

参见 MDN 算法介绍

  • Function
  • symbol
  • DOM 节点
  • 原型链
  • 部分 Error 对象(除了 ErrorEvalErrorRangeErrorReferenceErrorSyntaxErrorTypeErrorURIError 之外的都不能被拷贝)
  • 属性描述,如只读、不可枚举等描述信息
  • getter / setter

变量提升

Function 提升

function 关键字定义的函数,如果在条件块中定义,除非条件永假,否则就会被提升

(() => {
  if (!fn) {
    function fn() {
      console.log('2')
    }
  }
  fn() // 输出 "2"
})()

function fn() {
  console.log('1')
}

// another one
function fn1() {
  console.log('3')
}

(() => {
  if (!fn1) {
    function fn1() {
      console.log('4')
    }
  }
  fn1() // 输出 "4"
})()

(() => {
  if (false) {
    function fn3() {
      console.log('5')
    }
  }
  fn3() // 未定义,抛 Error
})()

This

未指定作用域

根据 ECMAScript262 规范规定:如果第一个参数传入的对象调用者是null或者 undefinedcall 方法将把全局对象(浏览器上是 window 对象)作为 this 的值。所以,不管传入 null 还是 undefined ,其 this 都是全局对象 window

function a() {
  console.log(this)
}
a.call(null) // window
a.call(undefined) // window

但是在严格模式中, null 就是 nullundefined 就是 undefined

'use strict'
function a() {
  console.log(this)
}
a.call(null) // null
a.call(undefined) // undefined

对象字面量 {}

{} 内的函数,在定义时没有单独的 this ,会指向更上一层的 this ,但是在调用时有 this ,会指向 {} 对象本身

const obj = {
  func1() {
    console.log(this)
  },
  func2: () => {
    console.log(this)
  }
}
obj.func1() // obj
obj.func2() // window

Function

Javascript 中,this 指向函数执行时的调用者。

function foo() {
  console.log(this.a)
}
function doFoo() {
  console.log(this.a)
  foo()
}
const obj = {
  a: 1,
  doFoo
}
const a = 2
obj.doFoo() // 1 2
const a = 10
const obj = {
  a: 20,
  say() {
    console.log(this.a)
  }
}
obj.say() // 20
const anotherObj = { a: 30 }
obj.say.apply(anotherObj) // 30

箭头函数

箭头函数的 this 在函数定义时就已经确定了,并且不能通过 call / bind / apply 等方式修改(不会报错,只是没效果)。

const a = 10
const obj = {
  a: 20,
  say: () => {
    console.log(this.a)
  }
}
obj.say() // 10
const anotherObj = { a: 30 }
obj.say.apply(anotherObj) // 10
const obj = {
  dev: 'bfe',
  /**
   * 定义函数类型的属性
   */
  a() {
    return this.dev
  },
  /**
   * 与 a 函数等价,是语法糖形式
   */
  b() {
    return this.dev
  },
  /**
   * 对象中出现箭头函数,会创建一个新的作用域,该作用域与对象定义时的 this 相同,在本例子中为 window
   */
  c: () => {
    return this.dev
  },
  d() {
    return (() => {
      // 作用域与 d 函数执行时的作用域相同
      return this.dev
    })()
  },
  e() {
    return this.b()
  },
  f() {
    return this.b
  },
  g() {
    return this.c()
  },
  h() {
    return this.c
  },
  i() {
    return () => {
      return this.dev
    }
  }
}

console.log(obj.a()) // "bfe"
console.log(obj.b()) // "bfe"
console.log(obj.c()) // undefined
console.log(obj.d()) // "bfe"
console.log(obj.e()) // "bfe"
console.log(obj.f()()) // undefined
console.log(obj.g()) // undefined
console.log(obj.h()()) // undefined
console.log(obj.i()()) // "bfe"
const obj = {
  a: 1,
  b() {
    console.log(this.a)
  },
  c() {
    console.log(this.a)
  },
  d: () => {
    console.log(this.a)
  },
  e: (function () {
    return () => {
      console.log(this.a)
    }
  })(),
  f() {
    return () => {
      console.log(this.a)
    }
  }
}

console.log(obj.a) // 没有疑问就是 1

obj.b() // 1
;(obj.b)() // 1 ,括号不会改变作用域
const b = obj.b
b() // 赋值会改变 this ,所以这里是 undefined
obj.b.apply({ a: 2 }) // apply 会改变 this ,所以这里是 2

obj.c() // 1
obj.d() // undefined
;(obj.d)() // undefined
obj.d.apply({ a: 2 }) // undefined , 箭头函数也有 call/apply/bind , 调用后不会报错,只是没有效果

obj.e() // undefined , IIFE + 箭头函数,在 IIFE 执行时, this 还是 window 而不是 obj
;(obj.e)() // undefined , 括号不改变作用域,所以没区别
obj.e.call({ a: 2 }) // undefined , 作用域已经被箭头函数锁死,通过 call 也无法修改 this
obj.f()() // 1 , this 只有 f 被调用时才会被箭头函数绑定
;(obj.f())() // 1 , 括号不改变作用域, this 不变
obj.f().call({ a: 2 }) // 1 , f 被执行后, this 就被箭头函数锁死,所以 call 也无法修改 this

Promise

Promise.resolve

参数

Promise.resolve 方法的参数如果是一个原始值,或者是一个不具有 then 方法的对象,则 Promise.resolve 方法返回一个新的 Promise 对象,状态为 resolvedPromise.resolve 方法的参数,会同时传给回调函数。

then方法接受的参数是函数,而如果传递的并非是一个函数,它实际上会将其解释为then(null), 这就会导致前一个Promise的结果会传递下面。

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

返回值

Promise.resolve 执行时,如果参数本身为 Promise,则返回其本身

const p = new Promise((res, rej) => {
  res(1)
})

async function asyncReturn() {
  return p
}

function basicReturn() {
  return Promise.resolve(p)
}

console.log(p === basicReturn()) // true
console.log(p === asyncReturn()) // false

Promise.reject

返回值

与 Promise.resolve 不同, Promise.reject 方法不会重用已存在的 Promise 实例。它始终返回一个新的 Promise 实例,该实例封装了拒绝的原因(reason)。

const p = Promise.resolve(1)
const rejected = Promise.reject(p)
console.log(rejected === p) // false
rejected.catch((v) => {
  console.log(v === p) // true
})

.then()

返回值

如果在 thenreturn Promise,则 return 会创建一个新的 Promise(PromiseResolveThenableJob 如果将 then 返回的 Promise 称为 promise1return 接受的 Promise 称为 promise2 ,则 PromiseResolveThenableJob 的任务就是将 promise1resolverejectpromise2resolvereject 关联起来,此时会多一个 micro task 周期,也就是说,这个后面的 then 需要在两个 micro task 周期后才会触发。

Promise.resolve()
  .then(() => {
    console.log(0)
    return Promise.resolve(4)
  })
  .then((res) => {
    console.log(res)
  })

Promise.resolve()
  .then(() => {
    console.log(1)
  })
  .then(() => {
    console.log(2)
  })
  .then(() => {
    console.log(3)
  })
  .then(() => {
    console.log(5)
  })
  .then(() => {
    console.log(6)
  })

.finally()

mdn 文档

参数

回调的参数不会接受任何参数

Promise.resolve(1)
  .finally(res => console.log(res))

返回值

finally 不会改变原 Promise 的结果。所以返回的默认是一个上一层 Promise 值(不分 resolve 或者 reject )。

如果抛出的是一个异常,或者返回 Promise.reject 则会返回异常的 Promise 对象。

除此之外, returnPromise.reject 类型的值都不会改变 .then() 的参数(指向上一层 Promise 的值)。

Promise.resolve('1')
  .then((res) => {
    console.log(res)
  })
  .finally(() => {
    console.log('finally')
  })
Promise.resolve('2')
  .finally(() => {
    console.log('finally2')
    return '我是finally2返回的值'
  })
  .then((res) => {
    console.log('finally2后面的then函数', res)
  })
Promise.resolve('1')
  .finally(() => {
    console.log('finally1')
    throw new Error('我是finally中抛出的异常')
  })
  .then((res) => {
    console.log('finally后面的then函数', res)
  })
  .catch((err) => {
    console.log('捕获错误', err)
  })

Promise.allPromise.race

错误处理流程

all 和 race 传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被then的第二个参数或者后面的catch捕获;但并不会影响数组中其它的异步任务的执行。

function runAsync(x) {
  const p = new Promise(r =>
    setTimeout(() => r(x, console.log(x)), 1000)
  )
  return p
}
function runReject(x) {
  const p = new Promise((res, rej) =>
    setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
  )
  return p
}
Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log('result: ', res))
  .catch(err => console.log(err))

总结

只有 Promise.resolve 才可能会复用 Promise ,其他的都会生成新的 Promise 。

Array

array hole

hole 指的是洞,即数组中未被初始化的部分,一般表示为 empty ,在打印时会输出 undefined ,在 JSON 序列化的时候会转为 null

使用 new Array() 创建的数组默认都是 hole

hole 具有以下特性:

  • map / forEach / Object.keys / for ... in 等方式,不会对其施加影响,也就是通过 map 无法改变 hole 值, forEach 也无法读取 hole
  • for ... of 可以读到 hole 值,输出位 undefined
  • 打印完整数组的时候, hole 值会被填充为 undefined
const a = [0]
console.log(a.length) // 1

a[3] = 3
console.log(a.length) // 4 ,数组实际会扩展为 4 个成员,表现为 [0, empty, empty, 3]

for (const item of a) {
  console.log(item) // 4 个成员都会被 of 遍历到, empty 会被视为意料之外的空值,也就是 undefined ,最终打印四次,分别为 0, undefined, undefined, 3
}

a.map((item) => {
  console.log(item)
}) // map 内部会过滤掉 empty ,这里只会打印 0、3

console.log(a.map((item) => {
  console.log(item)
})) // [0,empty,empty,3] 虽然 hole 不会应用到回调里面,但是整体打印的时候还在

a.forEach((item) => {
  console.log(item)
}) // forEach 内部会过滤掉 empty ,这里只会打印 0、3

console.log([...a]) // [0,undefined,undefined,3], 浅拷贝的时候, empty 使用 undefined 填充

console.log(Object.keys(a)) // 这里会过滤掉值为 empty 的 key ,数字的 key 会被转为字符串,也就数打印 ["0", "3"]

delete a[3]
console.log(a.length) // 虽然值被删除了,但是删除成员不会影响数组容量,这里保存了 [0, empty, empty, empty]

a[2] = 2
a.length = 1
console.log(a[0], a[1], a[2]) // 手动修正数组容量,超出的部分访问时会是空值,也就是打印 0, undefined, undefined

length

length 是数组的特殊属性,具有以下特性:

  • 改变 length 会影响数组行为,如追加 hole 或者删除已有的部分元素,直到数组长度与 length 匹配为止
  • 无法通过继承覆盖 length 的值,使其保持不变, length 是不可配置的,只能赋值与取值

数组最大下标为 2 ** 32 - 2 ,超出的部分值赋值无效,如果 new Array(2 ** 32) 会报错

class MyArray extends Array {
  get length() {
    return 3 // this will not override parent's length
  }
}
const arr1 = new MyArray(10)
console.log(arr1.length) // 10
const arr2 = Array.from({ length: 10 })
console.log(arr2.length) // 10

比较

  • indexOf: 使用 === 比较, [NaN].indexOf(NaN) // -1
  • includes: 使用 Same-value-zero 比较,能检查 NaN-0 等值, [NaN].includes(NaN) // true
function sameValueZero(x, y) {
  if (typeof x === 'number' && typeof y === 'number') {
    // x and y are equal (may be -0 and 0) or they are both NaN
    return x === y || (x !== x && y !== y)
  }
  return x === y
}

Math

异常计算

console.log(1 / 0) // Infinity PS: 正常来说 0 不能是除数,但是 JS 内置了当 0 为除数,并且被除数不是 0 的时候,就是无穷大,符号还是照样算
console.log(-1 / 0) // -Infinity
console.log(0 / 0) // NaN PS: 无意义

console.log(0 === -0) // true 。 在 JS 中通过全等无法判断 0 的符号,通过 Math.sign 也无法判断
console.log(Object.is(0, -0)) // true 。 打了补丁,这个函数可以严格区分 +0 与 -0

console.log(Object.is(0, Math.round(-0.5))) // 等价于 Object.is(0, -0) = false
console.log(Object.is(0, Math.round(0.5))) // 等价于 Object.is(0, 1) = false

console.log(0 * Infinity) // 无意义,为 NaN
console.log(Infinity / Infinity) // 无意义, 为 NaN

console.log(Object.is(0, Math.sign(0))) // 等价于 Object.is(0, 0) = true
console.log(Object.is(0, Math.sign(-0))) // 等价于 Object.is(0, -0) = false

console.log(1 / -0) // -Infinity
console.log(1 / 0) // Infinity

console.log(1n / 0n) // 抛异常, BigInt 已经修复了上述的不合理行为

缺省值

console.log(Math.min()) // 默认会与 Infinity 比较,所以这里打印 Infinity
console.log(Math.max()) // 默认会与 -Infinity 比较,所以这里打印 -Infinity
console.log(Math.min(1)) // 1
console.log(Math.max(1, 2)) // 2
console.log(Math.min([1, 2, 3])) // 数组无法正常转为数字,这里是 NaN

特殊值

console.log(Math.sign(-10)) // -1
console.log(Math.sign(0.1)) // 1

console.log(Math.sign(0)) // 0
console.log(Math.sign(-0)) // -0

RegExp

const arr = ['a', 'b', 'c', '1']
const regExp = /^[a-z]$/gi
const chars = arr.filter(elem => regExp.test(elem))
console.log(chars) // ["a","c"]

正则表达式 /^[a-z]$/gi 启用了全局匹配标志 g, 这导致 regExp.test() 方法在每次匹配后会更新正则对象的 lastIndex 属性(记录上次匹配的结束位置)。

  • ​第一次迭代: elem = 'a', test() 返回 true, lastIndex 变为 1 (因为匹配到了第 1 个字符)。
  • ​第二次迭代: elem = 'b', 此时 test() 从 lastIndex = 1 开始匹配,但 b 是单字符字符串,无法从位置 1 开始匹配,因此返回 false, 随后 lastIndex 重置为 0。
  • ​第三次迭代: elem = 'c', test() 从 lastIndex = 0 开始匹配,返回 true, lastIndex 变为 1。
  • ​第四次迭代: elem = '1', 匹配失败,返回 false
  • 最终只有 ‘a’ 和 ‘c’ 通过了过滤条件。

可以通过每次 filter 手动重置 lastIndex 解决

const chars = arr.filter((elem) => {
  regExp.lastIndex = 0 // 强制重置匹配位置
  return regExp.test(elem)
})

string

JS 字符串不可改变

const a = 'bfe.dev'
a[0] = 'c' // 赋值无效,不能改变字符串值
console.log(a)
const b = new String('bfe.dev')
b[0] = 'c' // new String 也不支持赋值改变字符串值
console.log(b)

预留字段

  • window.name 是一个特殊的字段,不管赋值为什么,都会被转为 string
  • function.name 是一个预留字段,默认指向函数名
  • functin.length 是一个预留字段,默认为函数定义时,声明的参数个数

浮点数

浮点数取自科学计数法,将数字使用 尾数 * 指数 的形式表示,根据 IEEE 规范,尾数的形式转为二进制后会是 1.xxx 的形式。

小数部分转二进制,采用 乘二取整法 ,将小数 * 2,取出每次计算出来的整数位,顺序排列,即为对应的二进制

比如十进制数 0.15625 ,转为二进制是 0.00101 。为了让第 1 位为 1 ,执行逻辑右移 3 位,尾数部分成为 1.01,因为右移了 3 位,所以指数部分是 -3 。

按照 IEEE 标准来看:

  • float: 1 符号位 + 8 指数位 + 23 尾数位
    • 范围: -3.40×10³⁸ 至 3.40×10³⁸
    • 精度: 十进制来说是小数点后 6-7 位
  • double: 1 符号位 + 11 指数位 + 52 尾数位
    • 范围: -1.79×10³⁰⁸ 至 +1.79×10³⁰⁸
    • 精度: 十进制来说是小数点后 15-16 位

底层实现来看,浮点数占用的数位并没有因为多存储了 尾数 而有所增加,依旧是 32 位 与 64 位,但是数字所能表示的上下限以及精度会被尾数挤占,导致上下限降低,以及精度丢失。