javascript面试题
JavaScript
js有哪些内置对象?
JavaScript有许多内置对象,包括但不限于:
- 基本对象: Object、Boolean、Symbol、Number、String
- 符合数据结构:Array、Set、Map、WeakSet、WeakMap
- 日期和时间对象:Date
- 数学计算对象:Math
- 正则表达式对象:RegExp
- 函数对象:Function
- 错误对象:Error、TypeError、RangeError等
- 其他对象:Global、JSON等
什么是闭包?
闭包是指一个函数可以访问另一个函数作用域内的变量。当一个函数嵌套在另一个函数中时,内部函数可以访问外部函数的变量,即使外部函数已经返回了。这种情况下,内部函数形成了一个闭包,它保留了外部函数的作用域链并可以继续访问这些变量。闭包常常用于实现函数的封装和私有化,以及在回调和事件处理等场景下的数据共享与传递。
如何理解作用域、作用域链和执行上下文?
在JavaScript中,作用域、作用域链和执行上下文是密切相关的概念,它们与变量和函数的查找、访问以及生命周期有关。
-
作用域(Scope):作用域是一个变量或函数的可访问范围。JavaScript中有三种作用域:全局作用域、局部(函数)作用域和块级作用域。全局作用域中声明的变量和函数可以在整个代码中访问,局部作用域中声明的变量和函数只能在特定的函数内部访问,块级作用域在一对花括号内定义,对
let
和const
关键字声明的变量有效。变量的生命周期受其作用域的限制。全局作用域中的变量在整个程序执行过程中持续存在,局部作用域中的变量在函数执行结束时销毁,块级作用域在代码块执行结束时,块级作用域中的变量将被销毁。 -
作用域链(Scope Chain):当代码执行过程中访问一个变量或函数时,JavaScript引擎会沿着作用域链查找该标识符。作用域链是由当前执行上下文的作用域和其所有父级作用域组成的链表。查找过程从当前作用域开始,然后逐级向上查找,直到找到目标标识符或到达全局作用域。如果在全局作用域中仍未找到目标标识符,则返回
undefined
。 -
执行上下文(Execution Context):执行上下文是JavaScript代码执行过程中的环境。每当进入一个新的函数执行或全局代码执行时,都会创建一个新的执行上下文。执行上下文包含了当前执行的代码所需的所有信息,如变量、函数、作用域链等。JavaScript引擎使用执行上下文栈(Execution Context Stack)来管理执行上下文。栈顶的执行上下文为当前执行的代码环境。当一个函数被调用时,一个新的执行上下文被压入栈顶;当函数执行结束时,执行上下文从栈顶弹出,返回到调用者的上下文环境。
总结起来,作用域是变量和函数的可访问范围;作用域链是由当前执行上下文的作用域和其父级作用域组成的链表,用于在代码执行过程中查找变量和函数;执行上下文是代码执行过程中的环境,包含了当前执行的代码所需的所有信息。这三者共同决定了代码执行过程中变量和函数的查找、访问以及生命周期。
如何创建一个没有原型的对象?
可以使用 Object.create(null) 方法创建一个没有原型的对象。这个方法创建一个全新的对象并将其原型设置为 null,因此它没有继承任何属性或方法。例如:
const obj = Object.create(null);
console.log(obj.toString); // undefined
如何理解原型链?
原型链是 JavaScript 中实现继承的一种机制,它通过让一个对象的原型指向另一个对象,从而使得一个对象可以访问另一个对象中定义的属性和方法。当我们试图访问一个对象中不存在的属性或方法时,JavaScript 引擎会沿着原型链一直向上查找,直到找到该属性或方法为止,或者最终抵达 Object.prototype(所有对象的祖先)上停止查找。
let const var比较
- 作用域:
var
:声明的变量具有函数作用域。这意味着在函数内部声明的变量只能在该函数内部访问,而在函数外部声明的变量具有全局作用域。let
和const
:声明的变量具有块级作用域。这意味着变量仅在声明它们的代码块(例如:if
语句、for
循环、while
循环等)内部可访问。
- 变量提升(Hoisting):
var
:声明的变量会被提升到所在作用域的顶部。这意味着在声明之前访问变量不会导致引用错误,但变量的值将是undefined
。let
和const
:声明的变量不会被提升。在声明之前访问变量会导致引用错误。
- 重复声明:
var
:允许在同一作用域内多次声明同名变量,后续声明将被忽略。let
和const
:在同一作用域内不允许重复声明同名变量。尝试这样做会导致语法错误。
- 变量的可变性:
var
和let
:声明的变量可被重新赋值。const
:声明的变量是不可变的,即一旦赋值,无法更改。这对于声明常量或确保某个变量在整个程序执行过程中保持不变的情况非常有用。
谈谈你对变量提升的理解
变量提升(Hoisting)是 JavaScript 的一个核心概念,理解它对于编写和理解代码非常重要。以下是我对变量提升的理解:
- 什么是变量提升:在 JavaScript 中,变量和函数声明(使用var和function)在内部会被“提升”到它们所在作用域(全局或函数)的顶部。这意味着在代码执行之前,JavaScript 引擎已经知道这些变量和函数的存在,即使它们在源代码中的位置可能在后面。
- 声明与赋值:需要注意的是,提升只作用于声明,不作用于赋值或初始化。如果一个变量在后面被赋值,那么它在提升时仍被认为是
undefined
。只有当执行到赋值语句时,它才会被赋予特定的值。 - var, let, const:只有用
var
声明的变量会被提升。用let
和const
声明的变量也有类似的提升行为,但由于它们存在“暂时性死区”(Temporal Dead Zone,TDZ),在声明前对它们的访问会导致错误。 - 函数提升:函数声明也会被提升,并且优先级高于变量。如果一个函数和一个变量同名,且变量未被赋值,那么该名称指向函数。
JSON.stringify有什么缺点?
JSON.stringify()
是一个将JavaScript对象转换为JSON字符串的方法。尽管它在许多情况下非常有用,但它确实存在一些限制和缺点:
- 循环引用:
JSON.stringify()
无法处理具有循环引用的对象。如果一个对象的属性直接或间接引用了自身,JSON.stringify()
将抛出一个错误,表示存在循环引用。 undefined
、函数和Symbol忽略:JSON.stringify()
不会序列化对象中的undefined
、函数和Symbol类型的属性。这些属性将被忽略,不会出现在生成的JSON字符串中,单独转换则会返回undefined
。- 丢失原型链:在对象序列化后,原型链上的属性和方法将丢失。只有对象自身的可枚举属性会被序列化。因此,在反序列化(使用
JSON.parse()
)后,原始对象的原型链信息将不复存在。 - 日期对象处理:当使用
JSON.stringify()
序列化日期对象时,日期对象会被转换为它们的ISO字符串表示形式。在反序列化时,这些日期将被视为普通字符串,而不是日期对象。 - 非数组和非对象的值:对于不是数组或对象的顶层值(例如:字符串、数字、布尔值等),
JSON.stringify()
会直接返回其对应的JSON表示,而不会将其包装在对象或数组中。
for…in 和 for…of的区别?
for…in循环用于遍历对象的可枚举属性,返回的是属性名称;for…of循环用于遍历可迭代对象(如数组、字符串、Map、Set等),返回的是元素值。
new操作符都做了什么
- 创建一个新对象
- 对象的
__proto__
指向构造函数的prototype
- 构造函数将对象绑定到
this
并调用 - 如果构造函数返回对象或函数则直接返回,否则返回这个新对象
类数组和数组的区别,dom 的类数组如何转换成数组
类数组(Array-like)和数组(Array)都是用于存储多个值的数据结构,但它们之间存在一些关键区别:
- 类型:数组是JavaScript的内置对象类型,继承自
Array.prototype
,具有一系列数组方法(如push()
、pop()
、map()
等)。类数组是普通的对象,其属性名为索引(如0
、1
、2
等),具有一个length
属性,但不具备数组的方法。 - 原型:数组的原型为
Array.prototype
,因此具有数组的所有方法。类数组的原型通常为Object.prototype
,并不包含数组的方法。
要将DOM的类数组(例如,通过document.getElementsByClassName()
或document.querySelectorAll()
获取的元素集合)转换为数组,可以使用以下方法之一:
-
使用
Array.from()
方法:let nodeList = document.querySelectorAll(‘div’);
let array = Array.from(nodeList);
Array.from()
方法会创建一个新数组,并将类数组的元素逐个复制到新数组中。
-
使用扩展运算符(Spread Operator):
let nodeList = document.querySelectorAll(‘div’);
let array = […nodeList];
扩展运算符...
可以将类数组直接转换为数组。
-
使用
Array.prototype.slice.call()
:let nodeList = document.querySelectorAll(‘div’);
let array = Array.prototype.slice.call(nodeList);
Array.prototype.slice.call()
方法会将类数组作为上下文,并创建一个新数组,将类数组的元素逐个复制到新数组中。
这些方法可以将类数组转换为数组,这样就可以在转换后的数组上使用数组的方法了。注意,这些方法不仅适用于DOM类数组,还适用于其他类数组对象。
offsetWidth/offsetHeight,clientWidth/clientHeight 与 scrollWidth/scrollHeight 的区别
offsetWidth/offsetHeight是元素的可见宽度/高度加上padding、border和滚动条(如果存在)的宽度/高度。
clientWidth/clientHeight是元素的可见宽度/高度,不包括padding和滚动条。
scrollWidth/scrollHeight是元素内容的完整宽度/高度,包括溢出部分。如果元素没有溢出,则scrollWidth/scrollHeight等于clientWidth/clientHeight。如果有溢出,则scrollWidth/scrollHeight大于clientWidth/clientHeight。
mouseover/mouseout 与 mouseenter/mouseleave 的区别与联系
mouseover和mouseout是HTML DOM事件,它们会在鼠标移入或移出元素时触发。它们也会在鼠标指针进入或离开子元素时触发。这也就是说,如果在父元素上有mouseover事件,并且鼠标指针进入子元素,则该元素上仍然会触发mouseover事件。mouseout同理。
mouseenter和mouseleave事件也是在鼠标进入或离开元素时触发。与mouseover和mouseout不同的是,mouseenter和mouseleave事件不会传播到子元素。因此,如果鼠标指针进入或离开元素的子元素,则不会触发mouseenter和mouseleave事件。
event.stopPropagation()与event.stopImmediatePropagation的区别
event.stopPropagation()可以阻止事件冒泡到父元素,但不阻止其他事件处理程序的执行。而event.stopImmediatePropagation()可以立即阻止事件冒泡并取消同一元素上其他事件处理程序的执行。
说一下事件循环机制Event Loop
事件循环(Event Loop)是 JavaScript 运行时环境中的一个核心概念,它负责协调异步操作和同步代码的执行。JavaScript 是单线程的,这意味着它一次只能执行一个任务。事件循环使 JavaScript 能够在执行同步代码的同时,处理异步操作(如定时器、用户交互和网络请求)的回调。
事件循环的工作原理大致如下:
- 首先,JavaScript 引擎执行全局同步代码(例如来自
<script>
标签或 Node.js 文件的代码)。 - 当遇到异步操作(如
setTimeout
、setInterval
、Promise
、fetch
等),它们的回调函数会被放入相应的任务队列中(微任务队列或宏任务队列)。 - 同步代码执行完成后,事件循环开始检查微任务队列。如果队列中有任务,事件循环将依次执行它们,直到队列为空。
- 接下来,事件循环检查宏任务队列。如果队列中有任务,事件循环将执行第一个任务,然后返回到微任务队列,检查是否有新的微任务需要执行。
- 事件循环在微任务队列和宏任务队列之间循环,依次执行队列中的任务。当两个队列都为空时,事件循环将等待新的任务(如用户交互或网络请求回调)。
- 当新任务出现时,事件循环将其添加到相应的队列中,并继续循环执行任务。
事件循环的目标是在处理同步代码和异步回调之间保持平衡,确保 JavaScript 代码的执行效率和响应能力。通过这种方式,事件循环允许 JavaScript 在单线程环境中有效地处理并发操作。
esm和commonjs的区别
ESM(ECMAScript Modules)和 CommonJS 是 JavaScript 中两种不同的模块系统。它们都允许将代码拆分成可重用的模块,并在需要时导入这些模块。尽管它们都实现了相似的功能,但它们之间存在一些关键差异:
- 语法:ESM 和 CommonJS 使用不同的语法来导入和导出模块。
- ESM 使用
import
和export
关键字 - CommonJS 使用
require
和module.exports
关键字
- 运行时加载与静态加载:
- CommonJS 是运行时加载,这意味着模块在运行时解析和加载。因此,在运行时可以动态修改模块和依赖关系。
- ESM 是静态加载,这意味着模块在编译时解析和加载。这允许更好的优化,如代码消除和更快的加载速度,但不允许在运行时动态修改模块。
- 作用域:ESM 和 CommonJS 在处理变量作用域方面有所不同。
- ESM 使用模块作用域,每个模块具有自己的顶级作用域。在模块内声明的变量不会污染全局作用域。
- CommonJS 使用文件作用域,但与 ESM 不同,CommonJS 模块可以通过
global
对象访问全局作用域。
- 循环依赖:ESM 和 CommonJS 处理循环依赖的方式不同。
- ESM 可以更好地处理循环依赖,因为模块是静态加载的。在循环依赖中,导入的值可能是不完整的,但不会导致错误。
- CommonJS 在处理循环依赖时可能会遇到问题,因为模块是运行时加载的。这可能导致在循环依赖中的模块中获得一个不完整的对象。
- 兼容性和使用场景:
- CommonJS 主要用于 Node.js 环境,因为它是 Node.js 的原生模块系统。虽然现代 Node.js 版本也支持 ESM,但很多旧的 Node.js 代码仍使用 CommonJS。然而,许多新的 Node.js 项目逐渐采用 ESM。
- ESM 通常用于现代 Web 开发,因为大多数现代浏览器原生支持 ESM。在使用构建工具(如 Webpack、Rollup 或 Parcel)时,ESM 也提供了更好的优化和打包能力。
- 实时绑定与值拷贝:
- ESM 使用实时绑定,当导入的值发生更改时,导入模块的值也会跟着更改。这意味着导入的值始终保持最新。
- CommonJS 使用值拷贝,当模块被导入时,值被复制到导入模块。这意味着在导入模块中,值的更改不会反映到原始模块,导入的值在导入时是固定的。
- 导出值:
- ESM 导出值是映射关系,可读,不可修改,但可通过导出的函数修改导出的值。
- CoomonJS 导出值的拷贝,可以修改导出的值。
- export使用:
- ESM export和export default支持一起使用。
- CoomonJS module.exports和exports不支持一起使用,会被覆盖。
总结一下,ESM 和 CommonJS 的主要区别在于它们的语法、加载机制、作用域、循环依赖处理、兼容性和使用场景以及实时绑定与值拷贝。尽管它们在某些方面有所不同,它们都是为了解决 JavaScript 模块化编程的问题。
解释下JavaScript栈内存和堆内存?
在 JavaScript 中,栈内存(Stack Memory)和堆内存(Heap Memory)扮演着不同的角色,它们分别负责存储不同类型的数据。以下是它们在 JavaScript 中的简要说明:
- 栈内存(Stack Memory):
- 栈内存主要用于存储基本类型(原始类型)的值,如
number
、string
、boolean
、null
和undefined
。这些类型的值通常较小且固定大小。 - 栈内存还负责存储函数调用的执行上下文、局部变量和临时数据。
- 栈内存遵循后进先出(LIFO)的原则进行分配和释放空间。当函数被调用时,函数的执行上下文、局部变量和相关信息会被压入栈中;当函数返回时,这些数据会从栈中弹出。
- 栈内存的分配和回收速度较快,因为内存管理由 JavaScript 引擎自动完成。
- 由于栈内存有限,如果递归调用过深或者分配大量的局部变量,可能导致栈溢出。
- 堆内存(Heap Memory):
- 堆内存主要用于存储引用类型的值,如对象(
object
)、数组(array
)和函数(function
)。这些类型的值通常较大,大小不固定。 - JavaScript 引擎使用垃圾回收机制自动管理堆内存中的对象。当对象不再被引用时,它们会被标记为垃圾,并在下一次垃圾回收时释放内存。
- 与栈内存相比,堆内存分配和回收速度较慢,因为需要管理更复杂的数据结构和垃圾回收机制。
- 堆内存可以动态分配,因此可以存储更多数据。
- 如果没有正确处理引用关系,可能导致内存泄漏。
总结一下,在 JavaScript 中,栈内存用于存储基本类型的值、函数调用的执行上下文和局部变量,堆内存用于存储引用类型的值。理解栈内存和堆内存的差异有助于编写高效且内存友好的 JavaScript 程序。
箭头函数与普通函数区别?
箭头函数(Arrow Functions)与普通函数(常被称为函数声明或函数表达式)在 JavaScript 中有一些重要的区别。这些区别包括语法、this
关键字的绑定、arguments 对象的使用、构造函数行为以及原型链。以下是箭头函数和普通函数之间的主要区别:
this
关键字绑定:
- 箭头函数没有自己的
this
,它从包围它的普通函数或全局作用域继承this
。这使得在事件处理器或回调函数中使用箭头函数非常方便,因为它们自动捕获外部的this
。 - 普通函数有自己的
this
,它的值在函数调用时确定。根据函数调用的方式(如通过对象方法调用、直接调用、构造函数调用等),this
的值可能会有所不同。
- arguments 对象:
- 箭头函数没有自己的
arguments
对象。它们可以访问包围它们的普通函数的arguments
对象。 - 普通函数有自己的
arguments
对象,这是一个类数组对象,包含了传递给函数的参数。
- 构造函数行为:
- 箭头函数不能作为构造函数使用,因此不能使用
new
关键字调用。它们也没有prototype
属性。 - 普通函数可以作为构造函数使用,通过
new
关键字创建新的对象实例。
- 原型链:
- 由于箭头函数没有
prototype
属性,它们不能作为其他对象的原型。 - 普通函数有
prototype
属性,可以作为其他对象的原型。
- 生成器:
- 箭头函数不能使用
yield
关键字。 - 普通函数可以使用
yield
关键字变成生成器函数。
箭头函数的this是声明时确定还是调用时确定?
箭头函数的 this
是根据其被声明的位置来确定的,而不是它被调用的位置。这个特性称为"词法作用域"或者"静态作用域"。箭头函数不会创建自己的 this
,它会从自己的作用域链上一层继承 this
。
isNaN与Number.isNaN的区别
isNaN函数用于检查一个值是否是NaN,它会将传入的参数先转换为数字类型再进行判断。如果传入的参数无法转换为数字类型,则会返回true。
而Number.isNaN用于检查一个值是否为NaN,但它不会将参数转换为数字类型,只有在参数本身就是NaN时才返回true。否则,返回false。
谈谈你对this的理解
在 JavaScript 中,this
是一个特殊的关键字,它在函数调用时动态地引用了一个对象。this
的值取决于函数的调用方式,不同的调用方式会导致 this
指向不同的对象。以下是一些关于 this
的不同用法和场景:
-
全局上下文:当在全局作用域中使用
this
时,它指向全局对象。在浏览器环境中,全局对象是window
;在 Node.js 环境中,全局对象是global
。 -
函数调用:当在函数内部使用
this
且函数作为普通函数调用时(非对象方法调用),this
通常指向全局对象。但在严格模式下(使用"use strict"
),this
会被设置为undefined
。 -
对象方法调用:当在对象的方法内部使用
this
时,this
指向调用该方法的对象。这也适用于原型链中的方法。 -
构造函数调用:当在构造函数内部使用
this
且使用new
关键字调用构造函数时,this
指向新创建的对象实例。 -
显式绑定:使用
call
、apply
或bind
方法调用函数时,可以显式地将this
绑定到一个指定的对象。 -
箭头函数:箭头函数没有自己的
this
,它从包围它的普通函数或全局作用域继承this
。这使得在事件处理器或回调函数中使用箭头函数非常方便,因为它们自动捕获外部的this
。
总之,this
是 JavaScript 中一个动态上下文的关键字,它的值取决于函数调用的方式。
谈谈你对严格模式的理解
在JavaScript中,严格模式(strict mode)和非严格模式(sloppy mode)主要有以下几个区别:
- 变量声明: 在严格模式下,必须明确地声明变量(使用
let
、const
或var
关键字)。否则,将会抛出一个引用错误(ReferenceError)。在非严格模式下,如果没有声明变量,JavaScript会自动将其声明为全局变量,这可能会导致意外的全局污染。 - this指针: 在严格模式下,全局作用域中的
this
值为undefined
。在非严格模式下,全局作用域中的this
值为全局对象(浏览器环境中为window
对象,Node.js环境中为global
对象)。此外,在严格模式下,不允许使用call
、apply
或bind
将this
值设置为null
或undefined
。 - 禁止使用未来保留字: 严格模式中,不能将一些未来保留字(如
implements
、interface
、let
、package
、private
、protected
、public
、static
和yield
)用作变量名或函数名。 - 禁止使用八进制字面量: 在严格模式下,不允许使用八进制字面量(如
0123
)。非严格模式下,八进制字面量是允许的。 - 禁止删除变量、函数和函数参数: 严格模式中,使用
delete
操作符删除变量、函数和函数参数会引发语法错误(SyntaxError)。在非严格模式下,这样的操作是允许的,但实际上不会删除这些对象。 - 限制函数参数的重复声明: 在严格模式下,如果一个函数具有多个相同名称的参数,将会抛出一个语法错误。非严格模式下允许这种重复声明,但只有最后一个参数值会生效。
- 错误处理: 严格模式相较于非严格模式,更严格地处理某些类型的错误。例如,当试图修改只读属性、给不可扩展的对象添加属性或删除不可配置的属性时,严格模式会抛出类型错误(TypeError),而非严格模式下则会静默失败。
要启用严格模式,可以在脚本或函数开头添加"use strict";
指令。这将对整个脚本或函数体中的代码启用严格模式。推荐使用严格模式编写代码,因为它可以帮助发现潜在的错误并避免一些不良的编程实践。
谈谈你对Promise的理解
Promise是一种在JavaScript中用于处理异步操作的编程模式。它表示一个尚未完成但预计在未来某个时刻完成的操作的结果。Promise允许我们以更简洁、易读的方式处理异步操作,避免了传统的回调地狱(callback hell)问题。
Promise有三种状态:
- pending(待定):初始状态,既不是fulfilled,也不是rejected。
- fulfilled(已实现):表示异步操作已成功完成。
- rejected(已拒绝):表示异步操作失败。
Promise具有以下特点:
- Promise对象是不可变的,一旦创建,其状态就不能再被改变。
- Promise状态只能从pending变为fulfilled或rejected,不能逆向改变,且只能改变一次。
- Promise允许我们将成功和失败的处理函数分开,增加代码的可读性。
缺点:
- 无法取消:一旦创建了 Promise,就无法取消它。这可能导致在某些情况下,不再需要结果的异步操作仍然在执行。
- 总是异步:Promise 的回调总是异步执行,即使操作已经完成。这可能会导致一些意外的行为,特别是在执行顺序敏感的情况下。
- 调试困难:由于 Promise 的链式调用和异步特性,调试 Promise 可能比调试同步代码更具挑战性。错误堆栈可能不够清晰,难以确定问题出在哪里。
Promise基本用法包括:
- 创建Promise对象:通过
new Promise(executor)
创建一个Promise对象,其中executor是一个执行器函数,接受两个参数:resolve和reject。成功时调用resolve函数并传递结果,失败时调用reject函数并传递原因。 - 链式调用:通过
.then()
方法处理fulfilled状态,接受一个回调函数作为参数,当Promise状态变为fulfilled时调用。.catch()
方法处理rejected状态,接受一个回调函数作为参数,当Promise状态变为rejected时调用。 - Promise.all:接受一个Promise数组作为参数,当所有Promise都变为fulfilled状态时返回一个新的Promise,其值为所有Promise结果的数组。如果有任意一个Promise变为rejected状态,则返回的Promise也变为rejected,且返回原因是第一个rejected的Promise的原因。
- Promise.race:接受一个Promise数组作为参数,返回一个新的Promise,其状态和结果与第一个完成(无论是fulfilled还是rejected)的Promise相同。
通过使用Promise,我们可以更有效地处理异步操作,降低代码复杂性,提高可维护性。在现代JavaScript开发中,Promise已成为处理异步操作的重要基石。
为什么0.1+0.2不等于0.3
在JavaScript(以及许多其他编程语言)中,0.1 + 0.2 不等于 0.3 的原因是浮点数精度问题。JavaScript使用IEEE 754标准中规定的双精度浮点数(double-precision floating point)来表示数字。这种表示方法在大多数情况下都很有效,但有时会导致精度损失。
双精度浮点数只有有限的位数(64位)来表示数字,其中1位表示符号位,11位表示指数,以及52位表示尾数。当尝试表示某些数字(特别是十进制小数)时,它们的二进制表示可能是无限循环的,因此需要截断以适应有限的位数。这可能导致浮点数的近似值与实际值之间存在微小差异。
在本例中,0.1和0.2的二进制表示都是无限循环的,需要截断。当它们被截断并以双精度浮点数存储时,这两个数字的实际值与理论值略有不同。因此,当执行0.1 + 0.2时,结果也会有微小误差,与0.3的理论值不完全相等。
为了解决这个问题,可以将结果四舍五入到所需的精度。例如,如果要比较两个数字是否相等,可以将它们四舍五入到一个合理的精度,然后再进行比较:
function areNumbersAlmostEqual(num1, num2, epsilon = 1e-10) {
return Math.abs(num1 - num2) < epsilon;
}
console.log(areNumbersAlmostEqual(0.1 + 0.2, 0.3)); // 输出 true
在这个示例中,我们使用一个称为"epsilon"的小数值来表示可接受的误差范围。我们计算两个数字之差的绝对值,如果它小于epsilon,我们认为这两个数字几乎相等。在实践中,需要根据具体问题选择合适的epsilon值。
Map与WeakMap的区别
Map 和 WeakMap 在 JavaScript 中都提供了键值对的存储,但它们的工作方式和底层实现有一些重要的区别。
-
键的引用:在 Map 中,键的引用是强引用,也就是说只要 Map 存在,那么它的键值对就会保留在内存中,不会被垃圾收集器回收。而在 WeakMap 中,键的引用是弱引用,也就是说如果没有其他地方引用该键,那么该键就会被垃圾收集器回收,不论该 WeakMap 是否还存在。
-
键的类型:在 Map 中,键可以是任何类型,包括原始类型(比如字符串、数字、布尔值)和对象类型。而在 WeakMap 中,键必须是对象。
-
迭代器和清除方法:Map 具有诸如
size
、clear
、keys
、values
和entries
等方法,允许开发者获取大小,清除所有键值对,或者迭代所有的键或值。然而,由于 WeakMap 的键是弱引用,为了防止在垃圾回收过程中可能引发的并发问题,WeakMap 没有这些方法。 -
底层实现:JavaScript 本身是高级语言,其具体实现取决于底层的 JavaScript 引擎,如 V8 或 SpiderMonkey。在一般情况下,Map 可以使用简单的哈希表来实现。对于 WeakMap,由于其键是弱引用,因此在内存管理方面需要更加复杂的处理。这些处理通常在引擎级别完成,而不是在 JavaScript 代码级别。
以上这些区别使得 Map 和 WeakMap 有各自适用的情况。比如,当你需要存储的键值对在某个时间点之后不再需要时,使用 WeakMap 可以防止内存泄漏。而当你需要完全控制何时删除键值对时,使用 Map 更为合适。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!