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,undefinedtrue: 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 NaNcompared with any value (includingNaN) isfalse+0and-0are considered equal
Object.is
A stricter, more precise comparison than ===
0 === -0; // trueNaN === NaN; // falseObject.is(0, -0); // falseObject.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:
- Evaluate
![]: all object types are truthy, so![]is false - Evaluate
[] == false:[]goes throughtoPrimitive, giving'' - Evaluate
'' == false: both sides convert to numbers, i.e.0 == 0
[0] == false
Conversion process:
- Evaluate
[0]:[0]goes throughtoPrimitive, giving'0' - 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 loopfor 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 initerates over fields on the prototypefor ofonly 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
FunctionsymbolDOMnodes- Prototype chains
- Some
Errorobjects (anything other thanError,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIErrorcannot 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 ... inand similar methods do not affect it —mapcannot change aholevalue, andforEachcannot read aholevaluefor ... ofcan readholevalues, printing them asundefined- When the whole array is printed,
holevalues are filled in asundefined
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
lengthaffects array behavior — appendingholes or deleting existing elements until the array length matcheslength - The value of
lengthcannot be overridden through inheritance to keep it fixed;lengthis 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) // -1includes: usesSame-value-zerocomparison — can detectNaNand treats-0as 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 returnsfalse. - 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.nameis a special field — whatever you assign to it is converted to astringfunction.nameis a reserved field that points to the function name by defaultfunction.lengthis 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.