Skip to content
Back to blog

JavaScript Underlying Mechanisms

A summary of all kinds of quirky behaviors in JS

Updated

Type Conversion

Automatic Type Conversion During Operations

Conversion Rules

Operation type Conversion rule Example
string + any Non-strings are converted to strings console.log('3' + true + 1) // "3true1"
- / * / / / % All operands are converted to numbers console.log(' 012 ' / 3) // 4
When a boolean participates true converts to 1, false converts to 0 console.log(true + false) // 1
console.log(1 + true + false) // 2
When null participates null converts to 0 as a number, and to null as a string console.log(null + 1) // 1
console.log(null + "1") // "null1"
When undefined participates undefined converts to NaN as a number, and to "undefined" as a string console.log(undefined + 1) // NaN
console.log(undefined + "1") // "undefined1"
object / array Symbol.toPrimitive > the built-in ToPrimitive
The built-in toPrimitive first calls valueOf to try to get a primitive; if a non-primitive is returned, it calls toString; if toString also returns a non-primitive, an error is thrown
See the example below
Date In the built-in ToPrimitive behavior, toString is called first instead of valueOf console.log(new Date() + 1) // Thu Feb 27 2025 11:05:01 GMT+0800 (China Standard Time)1
if / while / ! / && / || The value is converted to boolean for comparison See the truth table for conversion rules
in / parseInt The value is converted to string first, then parsed See special cases
const obj = {
  /** Custom implicit conversion — overrides the built-in one, so valueOf and toString are not called afterwards, but an error is still thrown */
  [Symbol.toPrimitive]() {
    console.log('custom conversion')
    return {}
  },
  /** Override the built-in valueOf conversion function */
  valueOf() {
    console.log('valueOf called')
    return {}
  },
  /** Override the built-in toString conversion function */
  toString() {
    console.log('toString called')
    return {}
  }
}
console.log(1 + obj) // Uncaught TypeError: Cannot convert object to primitive value

Common toPrimitive Behaviors

Type valueOf toString
Primitive types Returns itself Converted to a string
Number / String / Boolean Returns the corresponding primitive value The string for the primitive value
Symbol Returns itself Returned in the form Symbol(description)
Arrays Returns the array object itself Same as calling join
Date Timestamp (milliseconds since 1970) Time string
RegExp Returns the object itself Returns the regex's raw string version (escaped — e.g. \\ becomes \\\\)
Math Returns the object itself [object Math]
Objects Returns the object itself [object Object]
Function Returns the function itself, i.e. its reference The function's source code

Truth Table

  • false: 0, NaN, '', null, undefined
  • true: non-zero numbers, non-empty strings, Symbol, all object types

Special Cases

// Array conversion
[] + {}       // "[object Object]" ([] → "", {} → "[object Object]")
{} + []       // 0 ({} is parsed as an empty code block, +[] → 0)
console.log({} + []) // "[object Object]"

// Chrome : `[object Object][object Object]`
// Firefox: the first {} is an empty scope, so this is equivalent to +{}, i.e. converted to a number, i.e. NaN
// If not printed directly in the console but passed to console.log, there should be no dispute — it is Chrome's output
{} + {}

+[] // Equivalent to (+"") = 0

// Object conversion
const obj = {
  a: 123,
  valueOf: () => 2,
  toString: () => "3"
};
obj + 1       → 3 (valueOf() is called first)
console.log(parseInt(obj)) // 3 — toString is called directly to get '3', then parseInt
console.log(['a'] in obj) // true — ['a'] has toString called, becoming `'a' in obj`, which is true

// Date objects
new Date() + 1 → "Mon Jul 18 2022 12:00:00 GMT+08001" (toString() is called first)

Equality Comparison

===

  • No type conversion — both type and value are compared
  • Objects are compared by reference address; different addresses give false
  • NaN compared with any value (including NaN) is false
  • +0 and -0 are considered equal

Object.is

A stricter, more precise comparison than ===

  • 0 === -0; // true

  • NaN === NaN; // false

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

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

== Equality Comparison

Principle: prefer converting to number for comparison, then string, and finally boolean; when a non-primitive participates in the comparison, it is first converted via toPrimitive

Comparison type Conversion rule
boolean == anything else Booleans convert to numbers (true → 1, false → 0)
string == number The string converts to a number ("5" == 5 → true)
object / array == primitive Converted to a primitive (via ToPrimitive) before comparing
null == undefined A special case in the spec — the two are equal (null == undefined → true)
null == 0 A special case in the spec — null does not convert to 0, so they are not equal

Common Conversion Examples in Comparisons

[] == ![] → true

Conversion process:

  1. Evaluate ![]: all object types are truthy, so ![] is false
  2. Evaluate [] == false: [] goes through toPrimitive, giving ''
  3. Evaluate '' == false: both sides convert to numbers, i.e. 0 == 0

[0] == false

Conversion process:

  1. Evaluate [0]: [0] goes through toPrimitive, giving '0'
  2. Evaluate '0' == false: both sides convert to numbers, i.e. 0 == 0

> / < / >= / <= Comparisons

  • If either side is a number, both sides convert to numbers: '21' < 4 → false
  • If both sides are strings, compare in lexicographic order: '21' < '4' → true

Numeric Computation

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

Operators

Implicit Conversion + Operator Default Values

let a = 1
console.log(a++ + a) // Equivalent to (a++) + a, equivalent to a + (a + 1) = 3

console.log(1 + 2) // 3
console.log(1 + +2) // Equivalent to (1 + 0) + 2 = 3
console.log(1 + + +2) // Equivalent to ((1 + 0) + 0) + 2 = 3

console.log(`${1}2`) // "12"
console.log(1 + +'2') // Equivalent to 1 + (+"2"), equivalent to 1 + 2 = 3
console.log(`1${2}`) // "12"
console.log(`1${+2}`) // Equivalent to "1" + (+2), equivalent to "1" + 2 = "12"

console.log(1 + true) // Equivalent to 1 + 1 = 2
console.log(1 + +true) // Equivalent to 1 + (+true), equivalent to 1 + 1 = 2
console.log(`1${true}`) // "1true"
console.log(`1${+true}`) // Equivalent to "1" + (+true), equivalent to "1" + 1 = "11"

console.log(1 + null) // Equivalent to 1 + 0 = 1
console.log(1 + +null) // Equivalent to 1 + (+null), equivalent to 1 + 0 = 1
console.log(`1${null}`) // "1null"
console.log(`1${+null}`) // Equivalent to "1" + (+null), equivalent to "1" + 0 = "10"

console.log(1 + undefined) // Equivalent to 1 + NaN = NaN
console.log(1 + +undefined) // Equivalent to 1 + (+undefined), equivalent to 1 + NaN = NaN
console.log(`1${undefined}`) // "1undefined"
console.log(`1${+undefined}`) // Equivalent to "1" + (+undefined), equivalent to "1" + NaN = "1NaN"
console.log(`1${+ +undefined}`) // Equivalent to "1" + (+(+undefined)), equivalent to "1" + (+NaN), equivalent to "1" + NaN = "1NaN"

Chained Comparison Operators

console.log(0 == 1 == 2) // Equivalent to `console.log(false == 2)`, so it is false
console.log(2 == 1 == 0) // Equivalent to `console.log(false == 0)`, so it is true

for in vs for of

If Elements Are Added Dynamically During the Loop

  • for in: new elements are not added to the loop
  • for of: new elements are added to the loop and executed
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)

Iterating Over Prototype Fields

  • for in iterates over fields on the prototype
  • for of only iterates over the current object's own fields and does not look up the prototype
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

Modifying arguments affects the parameter values

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

  // Only a is modified; d is not, because d itself is undefined and no reference was recorded
  console.log(a, b, c, d)
}

log(1, 2, 3)

Named Function Accessibility

func1() // Error — during hoisting, func1 is undefined; the function itself is not hoisted
func2() // First, to run this you must comment out the previous line — otherwise the error prevents reaching here; if reached, this also errors

// After a named function is assigned, the outside of the function cannot access it by its function name, but the inside can — and this name cannot be changed
var func1 = function func2() {
  console.log(2)
}
var a = 1
function a() {  // Hoisted above the var variable definition
}

console.log(typeof a) // A named function can be overwritten from the outside; this returns "number"

Redefining a Named Function Inside Itself

In JavaScript, when a function is declared with the function keyword, the function is hoisted.

So when console.log(typeof a) executes at the top, it recognizes a as a function and returns "function".

After entering the a function, typeof a is printed first — at this point a is still the function itself, so it returns "function". Then a = 'a' executes, which reassigns the original function a to a string, so the later console.log(typeof a) returns "string".

In the global scope, a was initially treated as a function declaration; reassigning it inside the function changed the function object a originally pointed to into a string. This series of operations changed the type of the global variable 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 is a named function expression. The name d is local to the function body (scope) only, so outside the function body, typeof d returns undefined.
console.log(typeof d) // "undefined"
c()

A Function Definition as an if Condition

In the example below, foo is treated like an IIFE-style function block and is not hoisted to the global scope, so foo is inaccessible both inside and outside

if (function foo() {
  console.log('BFE')
}) {
  console.log('dev') // This prints
  foo() // Error — foo is not defined
}
foo() // Error — foo is not defined

bind

When bind is called multiple times, only the first bind takes effect — subsequent calls cannot modify this, just like with arrow functions

Deep Copying

Data Types That structuredClone Cannot Copy

See the MDN algorithm introduction

  • Function
  • symbol
  • DOM nodes
  • Prototype chains
  • Some Error objects (anything other than Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError cannot be copied)
  • Property descriptors, such as read-only, non-enumerable, and other descriptor information
  • getter / setter

Variable Hoisting

Function Hoisting

A function defined with the function keyword, if defined inside a conditional block, is hoisted unless the condition is permanently false

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

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

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

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

(() => {
  if (false) {
    function fn3() {
      console.log('5')
    }
  }
  fn3() // Undefined — throws an Error
})()

This

Unspecified Scope

According to the ECMAScript262 specification: if the object passed as the first argument is null or undefined, the call method takes the global object (the window object in browsers) as the value of this. So whether you pass null or undefined, this is the global object window.

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

But in strict mode, null is null and undefined is undefined

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

Object Literals {}

Functions inside {} have no this of their own at definition time and point to the this one level up; but when called, they do have a this pointing to the {} object itself

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

Function

In Javascript, this points to the caller at the time the function executes.

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

Arrow Functions

An arrow function's this is fixed at definition time and cannot be modified via call / bind / apply (no error is thrown — it just has no effect).

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',
  /**
   * Defines a function-typed property
   */
  a() {
    return this.dev
  },
  /**
   * Equivalent to function a — the syntactic sugar form
   */
  b() {
    return this.dev
  },
  /**
   * An arrow function in an object creates a new scope, whose this is the same as the this at the time the object was defined — window in this example
   */
  c: () => {
    return this.dev
  },
  d() {
    return (() => {
      // The scope is the same as the scope when d executes
      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) // Unquestionably 1

obj.b() // 1
;(obj.b)() // 1 — parentheses do not change the scope
const b = obj.b
b() // Assignment changes this, so this is undefined
obj.b.apply({ a: 2 }) // apply changes this, so this is 2

obj.c() // 1
obj.d() // undefined
;(obj.d)() // undefined
obj.d.apply({ a: 2 }) // undefined — arrow functions also have call/apply/bind; calling them does not throw, it just has no effect

obj.e() // undefined — IIFE + arrow function: when the IIFE executes, this is still window, not obj
;(obj.e)() // undefined — parentheses do not change the scope, so no difference
obj.e.call({ a: 2 }) // undefined — the scope is locked by the arrow function; even call cannot modify this
obj.f()() // 1 — this is only bound by the arrow function when f is called
;(obj.f())() // 1 — parentheses do not change the scope, this stays the same
obj.f().call({ a: 2 }) // 1 — after f executes, this is locked by the arrow function, so call cannot modify this either

Promise

Promise.resolve

Parameters

If the argument to Promise.resolve is a primitive value, or an object without a then method, Promise.resolve returns a new Promise object in the resolved state, and the argument is passed on to the callback.

The parameter then accepts is a function; if what is passed is not a function, it is effectively interpreted as then(null), which causes the previous Promise's result to be passed down.

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

Return Value

When Promise.resolve executes, if the argument itself is a Promise, it returns that promise itself

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

Return Value

Unlike Promise.resolve, the Promise.reject method does not reuse an existing Promise instance. It always returns a new Promise instance that wraps the rejection reason (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()

Return Value

If you return Promise inside then, the return creates a new Promise (PromiseResolveThenableJob) If we call the Promise returned by then promise1, and the Promise accepted by return promise2, then the job of PromiseResolveThenableJob is to wire up promise1's resolve and reject with promise2's resolve and reject. This adds an extra micro task cycle — that is, the following then only fires after two micro task cycles.

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()

See the MDN documentation

Parameters

The callback does not receive any arguments

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

Return Value

finally does not change the original Promise's result. So by default it returns the value of the previous Promise (regardless of resolve or reject).

If an exception is thrown, or Promise.reject is returned, an exceptional Promise object is returned.

Otherwise, returning any value that is not Promise.reject does not change the argument of .then() (which points to the previous Promise's value).

Promise.resolve('1')
  .then((res) => {
    console.log(res)
  })
  .finally(() => {
    console.log('finally')
  })
Promise.resolve('2')
  .finally(() => {
    console.log('finally2')
    return 'the value returned by finally2'
  })
  .then((res) => {
    console.log('the then after finally2', res)
  })
Promise.resolve('1')
  .finally(() => {
    console.log('finally1')
    throw new Error('the exception thrown in finally')
  })
  .then((res) => {
    console.log('the then after finally', res)
  })
  .catch((err) => {
    console.log('caught error', err)
  })

Promise.all and Promise.race

Error Handling Flow

If the array passed to all or race contains async tasks that throw, only the first error thrown is caught — by then's second parameter or a later catch; this does not affect the execution of the other async tasks in the array.

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))

Summary

Only Promise.resolve may reuse a Promise; all the others create a new Promise.

Array

array hole

A hole is a gap — the uninitialized part of an array, generally shown as empty. It prints as undefined and is serialized to null in JSON.

Arrays created with new Array() are all holes by default

A hole has the following characteristics:

  • map / forEach / Object.keys / for ... in and similar methods do not affect it — map cannot change a hole value, and forEach cannot read a hole value
  • for ... of can read hole values, printing them as undefined
  • When the whole array is printed, hole values are filled in as undefined
const a = [0]
console.log(a.length) // 1

a[3] = 3
console.log(a.length) // 4 — the array actually expands to 4 members, shown as [0, empty, empty, 3]

for (const item of a) {
  console.log(item) // All 4 members are iterated by of; empty is treated as an unexpected empty value, i.e. undefined — prints four times: 0, undefined, undefined, 3
}

a.map((item) => {
  console.log(item)
}) // map filters out empty internally — only 0 and 3 are printed here

console.log(a.map((item) => {
  console.log(item)
})) // [0,empty,empty,3] — holes are not passed to the callback, but they are still there when printed as a whole

a.forEach((item) => {
  console.log(item)
}) // forEach filters out empty internally — only 0 and 3 are printed here

console.log([...a]) // [0,undefined,undefined,3] — in a shallow copy, empty is filled with undefined

console.log(Object.keys(a)) // Keys whose values are empty are filtered out; numeric keys are converted to strings — prints ["0", "3"]

delete a[3]
console.log(a.length) // The value was deleted, but deleting a member does not affect the array's capacity — it remains [0, empty, empty, empty]

a[2] = 2
a.length = 1
console.log(a[0], a[1], a[2]) // Manually shrinking the array capacity — accessing beyond the new length yields empty values, printing 0, undefined, undefined

length

length is a special property of arrays with the following characteristics:

  • Changing length affects array behavior — appending holes or deleting existing elements until the array length matches length
  • The value of length cannot be overridden through inheritance to keep it fixed; length is non-configurable — it can only be assigned and read

The maximum array index is 2 ** 32 - 2; assigning beyond that has no effect, and new Array(2 ** 32) throws

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

Comparison

  • indexOf: uses === comparison — [NaN].indexOf(NaN) // -1
  • includes: uses Same-value-zero comparison — can detect NaN and treats -0 as equal, [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

Abnormal Computations

console.log(1 / 0) // Infinity PS: normally 0 cannot be a divisor, but JS has a built-in rule: when 0 is the divisor and the dividend is not 0, the result is infinity, with the sign computed as usual
console.log(-1 / 0) // -Infinity
console.log(0 / 0) // NaN PS: meaningless

console.log(0 === -0) // true. In JS, strict equality cannot tell the sign of 0 apart, and neither can Math.sign
console.log(Object.is(0, -0)) // true. Patched — this function strictly distinguishes +0 from -0

console.log(Object.is(0, Math.round(-0.5))) // Equivalent to Object.is(0, -0) = false
console.log(Object.is(0, Math.round(0.5))) // Equivalent to Object.is(0, 1) = false

console.log(0 * Infinity) // Meaningless — NaN
console.log(Infinity / Infinity) // Meaningless — NaN

console.log(Object.is(0, Math.sign(0))) // Equivalent to Object.is(0, 0) = true
console.log(Object.is(0, Math.sign(-0))) // Equivalent to Object.is(0, -0) = false

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

console.log(1n / 0n) // Throws — BigInt has fixed the unreasonable behavior above

Default Values

console.log(Math.min()) // Compares against Infinity by default, so this prints Infinity
console.log(Math.max()) // Compares against -Infinity by default, so this prints -Infinity
console.log(Math.min(1)) // 1
console.log(Math.max(1, 2)) // 2
console.log(Math.min([1, 2, 3])) // An array cannot be properly converted to a number — NaN here

Special Values

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"]

The regular expression /^[a-z]$/gi has the global flag g enabled, which causes the regExp.test() method to update the regex object's lastIndex property after each match (recording where the last match ended).

  • First iteration: elem = 'a'; test() returns true and lastIndex becomes 1 (because the first character matched).
  • Second iteration: elem = 'b'; test() now starts matching from lastIndex = 1, but b is a single-character string and cannot match from position 1, so it returns false, and lastIndex resets to 0.
  • Third iteration: elem = 'c'; test() starts matching from lastIndex = 0, returns true, and lastIndex becomes 1.
  • Fourth iteration: elem = '1'; the match fails and returns false.
  • In the end, only 'a' and 'c' pass the filter.

You can fix this by manually resetting lastIndex on each filter call

const chars = arr.filter((elem) => {
  regExp.lastIndex = 0 // Forcibly reset the match position
  return regExp.test(elem)
})

string

JS strings are immutable

const a = 'bfe.dev'
a[0] = 'c' // Assignment has no effect — a string's value cannot be changed
console.log(a)
const b = new String('bfe.dev')
b[0] = 'c' // new String does not support changing a string's value by assignment either
console.log(b)

Reserved Fields

  • window.name is a special field — whatever you assign to it is converted to a string
  • function.name is a reserved field that points to the function name by default
  • function.length is a reserved field whose default is the number of declared parameters at function definition time

Floating-Point Numbers

Floating-point numbers come from scientific notation, representing numbers in the form mantissa * exponent. Per the IEEE specification, the mantissa converted to binary has the form 1.xxx.

To convert the fractional part to binary, use the multiply-by-two-and-take-the-integer method: multiply the fraction by 2, take the integer digit from each step, and arrange them in order — that is the corresponding binary

For example, the decimal number 0.15625 converts to binary as 0.00101. To make the first digit 1, perform a logical right shift of 3 bits, making the mantissa 1.01; since it was shifted right by 3 bits, the exponent part is -3.

According to the IEEE standard:

  • float: 1 sign bit + 8 exponent bits + 23 mantissa bits
    • Range: -3.40×10³⁸ to 3.40×10³⁸
    • Precision: 6–7 digits after the decimal point in decimal terms
  • double: 1 sign bit + 11 exponent bits + 52 mantissa bits
    • Range: -1.79×10³⁰⁸ to +1.79×10³⁰⁸
    • Precision: 15–16 digits after the decimal point in decimal terms

From the underlying implementation's perspective, the number of bits a floating-point number occupies does not increase because of the extra mantissa storage — it is still 32 bits and 64 bits — but the representable upper/lower limits and precision are squeezed by the mantissa, lowering the limits and losing precision.