跳至正文
返回博客

JavaScript 底层机制

JS 中的奇奇怪怪行为总结

更新于

类型转换

运算过程自动类型转换

转换规则

操作类型 转换规则 示例
string + any 非字符串转为字符串 console.log('3' + true + 1) // "3true1"
- / * / / / % 所有操作数转为数字 console.log(' 012 ' / 3) // 4
boolean 参与运算时 true 转为数字是 1, false 转为数字是 0 console.log(true + false) // 1
console.log(1 + true + false) // 2
null 参与运算时 null 转为数字是 0 ,转为字符串是 null console.log(null + 1) // 1
console.log(null + "1") // "null1"
undefined 参与运算时 undefined 转为数字是 NaN, 转为字符串是 "undefined" console.log(undefined + 1) // NaN
console.log(undefined + "1") // "undefined1"
object / array Symbol.toPrimitive > 内置的 ToPrimitive
内置 toPrimitive 优先调用 valueOf 尝试获取基本类型,如果返回非基本类型则调用 toString, toString 也不返回基本类型,抛出错误
参见下方示例
Date 内置的 ToPrimitive 行为中,优先调用的是 toString 而不是 valueOf console.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

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

真值表

  • false: 0 、 NaN 、 '' 、 null 、 undefined
  • true: 非零数字、非空字符串、 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 + 1       → 3(优先调用 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 in 与 for 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 对象(除了 Error 、 EvalError 、 RangeError 、 ReferenceError 、 SyntaxError 、 TypeError 、 URIError 之外的都不能被拷贝)
  • 属性描述,如只读、不可枚举等描述信息
  • 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或者 undefined , call 方法将把全局对象(浏览器上是 window 对象)作为 this 的值。所以,不管传入 null 还是 undefined ,其 this 都是全局对象 window 。

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

但是在严格模式中, null 就是 null , undefined 就是 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 对象,状态为 resolved , Promise.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()

返回值

如果在 then 中 return Promise,则 return 会创建一个新的 Promise(PromiseResolveThenableJob) 如果将 then 返回的 Promise 称为 promise1 , return 接受的 Promise 称为 promise2 ,则 PromiseResolveThenableJob 的任务就是将 promise1 的 resolve 、 reject 与 promise2 的 resolve 、 reject 关联起来,此时会多一个 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 对象。

除此之外, return 非 Promise.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.all 与 Promise.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 是一个预留字段,默认指向函数名
  • function.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 位,但是数字所能表示的上下限以及精度会被尾数挤占,导致上下限降低,以及精度丢失。