JavaScript面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

前端阅读 1602024年8月5日 12:43

javascript 如何实现高效的字符串前缀匹配

在JavaScript中实现高效的字符串前缀匹配通常可以通过以下几种方式:1. 原生字符串方法使用字符串的startsWith()方法,这是最简单直接的方法,性能也相当好。function isPrefix(str, prefix) { return str.startsWith(prefix);}// 使用示例console.log(isPrefix('javascript', 'java')); // 输出: trueconsole.log(isPrefix('javascript', 'script')); // 输出: false2. 正则表达式利用正则表达式的^锚点来匹配字符串的开头。function isPrefix(str, prefix) { let regex = new RegExp('^' + escapeRegExp(prefix)); return regex.test(str);}// 为了安全性,对特殊字符进行转义,防止注入攻击function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');}// 使用示例console.log(isPrefix('javascript', 'java')); // 输出: trueconsole.log(isPrefix('javascript', 'script')); // 输出: false3. 字符串切片比较通过截取原字符串前N个字符,然后与前缀进行比较。function isPrefix(str, prefix) { return str.slice(0, prefix.length) === prefix;}// 使用示例console.log(isPrefix('javascript', 'java')); // 输出: trueconsole.log(isPrefix('javascript', 'script')); // 输出: false4. 循环比较逐个字符比较,这通常不是最高效的方法,但在某些特定情况下可能是必要的。function isPrefix(str, prefix) { if (str.length < prefix.length) return false; for (let i = 0; i < prefix.length; i++) { if (str[i] !== prefix[i]) { return false; } } return true;}// 使用示例console.log(isPrefix('javascript', 'java')); // 输出: trueconsole.log(isPrefix('javascript', 'script')); // 输出: false5. 使用内置方法 indexOf检查前缀是否在字符串的开头位置。function isPrefix(str, prefix) { return str.indexOf(prefix) === 0;}// 使用示例console.log(isPrefix('javascript', 'java')); // 输出: trueconsole.log(isPrefix('javascript', 'script')); // 输出: false每种方法都有其适用场景,一般而言,如果只需要简单的前缀匹配,推荐使用startsWith()方法,因为它简单且意图明确。如果需要对匹配进行更复杂的控制,可能会选择正则表达式。在处理大量数据或性能至关重要的情况下,可以进行基准测试以确定哪种方法最有效。
前端阅读 1242024年8月5日 12:43

javascript 的类型以及如何检测

JavaScript 是一种动态类型语言,这意味着在声明变量时不需要指定数据类型,数据类型会在脚本执行时自动确定。JavaScript 的数据类型主要分为两大类:原始数据类型和对象类型。原始数据类型undefined:表示变量未定义,即声明了变量但未初始化。null:表示一个空值。boolean:布尔类型,有两个值:true 和 false。string:表示文本数据,例如 "Hello, World!"。number:可以是整数或浮点数,例如 42 或 3.14159。bigint:表示大于2^53 - 1的整数。symbol:表示唯一的、不可变的数据值。对象类型Object:JavaScript 中的对象是键值对的集合,几乎所有的 JavaScript 值都是对象类型的,包括数组、函数以及其他内置对象。类型检测的方法在 JavaScript 中,检测变量的类型常用的有几种方法:typeof 运算符:用来检测一个变量的类型,对于原始数据类型非常有效,但对于对象类型和 null,会有一些局限性。let num = 42;console.log(typeof num); // "number"let str = "Hello";console.log(typeof str); // "string"let flag = true;console.log(typeof flag); // "boolean"let bigIntNumber = 1234567890123456789012345678901234567890n;console.log(typeof bigIntNumber); // "bigint"let sym = Symbol('foo');console.log(typeof sym); // "symbol"let und;console.log(typeof und); // "undefined"let obj = { key: 'value' };console.log(typeof obj); // "object"let arr = [1, 2, 3];console.log(typeof arr); // "object", 尽管它是数组let func = function() {};console.log(typeof func); // "function", 函数是对象的一种特殊类型let nul = null;console.log(typeof nul); // "object", 这是一个历史上的错误instanceof 运算符:用来检测一个对象是否是另一个对象的实例。let arr = [1, 2, 3];console.log(arr instanceof Array); // trueconsole.log(arr instanceof Object); // truelet d = new Date();console.log(d instanceof Date); // true// 注意,instanceof 无法检测原始数据类型Array.isArray():用来确定一个值是否是数组。let arr = [1, 2, 3];console.log(Array.isArray(arr)); // true对象的 constructor 属性:可以用来判断对象的构造函数。let arr = [1, 2, 3];console.log(arr.constructor === Array); // truelet obj = {};console.log(obj.constructor === Object); // trueObject.prototype.toString.call():这是一个通用的类型检测方法,可以准确判断各种类型的值。let d = new Date();console.log(Object.prototype.toString.call(d)); // "[object Date]"let num = 42;console.log(Object.prototype.toString.call(num)); // "[object Number]"let str = "Hello";console.log(Object.prototype.toString.call(str)); // "[object String]"注意,当使用类型检测方法时,应当根据具体情况选择最适合的方法,因为每种方法都有其适用场景和限制。
前端阅读 1152024年8月5日 12:43

什么是"use strict";?使用它有什么优缺点?​

什么是"use strict"?"use strict"; 是一个JavaScript中的指令,也称作严格模式(strict mode),它用于将整个脚本或单个函数置于一个更加严格的操作环境中。当在代码的开始处使用该指令时,它有助于捕获一些常见的编程错误,同时防止或抛出错误,以及在某些情况下提高编译器的优化水平。由于这些原因,它会改善代码的运行速度和效率。使用它有什么优点?提前捕获错误: 严格模式会在代码执行前就发现一些错误,这些在非严格模式下可能不会被检测到。例如,对不可写的属性赋值,或对只读属性(如undefined,NaN)赋值。避免意外的全局变量: 在严格模式下,如果不使用var、let或const来声明变量,将会抛出错误,这样可以避免全局变量的隐式声明,减少代码中的潜在错误。消除this的混乱: 在严格模式下,如果没有指定上下文对象,函数内的this值将是undefined,这比默认指向全局对象要安全。更安全的eval: 严格模式下,eval函数内部声明的变量不会影响到外部作用域,这使得eval的使用更加安全。提高编译器优化: 代码在执行之前可以进行更多的检查,这为JavaScript引擎的优化打下基础,可能会提高执行速度。使用它有什么缺点?兼容性问题: 在老旧的浏览器或JavaScript环境中,可能不支持严格模式,或者其行为与新版的解释器不一致。代码修改成本: 如果要在已有项目中引入严格模式,可能需要对现有代码进行较大范围的修改,以确保兼容性和正确性。学习曲线: 对于初学者来说,严格模式下的某些限制可能会增加学习难度,因为它们需要更好地理解JavaScript的工作原理。可能隐藏的问题: 在非严格模式写的代码中可能含有在严格模式中会失败的部分,如果不进行彻底的测试,这些隐藏的问题在切换到严格模式后可能会导致运行时错误。示例:以下是一个简单的例子,展示了在使用严格模式时变量必须声明,否则会抛出错误:"use strict";function myFunction() { undeclaredVariable = 123; // 这里会抛出错误,因为变量没有声明}myFunction();如果没有"use strict"; 指令,上面的代码中的undeclaredVariable会被创建为一个全局变量,这可能是一个潜在的问题。使用严格模式,我们可以避免这种情况。
前端阅读 682024年7月7日 14:40

如何实现一个JSON.stringify

在实现一个JSON.stringify功能时,我们需要考虑几个关键点:正确处理不同类型的数据(如字符串、数字、对象、数组等),处理循环引用和其他边界情况,以及保证转换后的字符串格式正确。下面我们一步步来实现简化版的JSON.stringify。第一步:基础类型处理对于基础数据类型,处理相对简单:数字:直接转换为其字符串形式。字符串:需要加上引号,并处理特殊字符,如转义字符。布尔值:转换为"true"或"false"。null:直接返回"null"。第二步:数组和对象对于数组和对象,需要递归处理其内部元素:数组:遍历数组的每个元素,对每个元素应用stringify,然后将结果用逗号连接,最后加上[]。对象:遍历对象的每个可枚举属性,对键和值应用stringify,然后将结果用冒号和逗号连接,注意键名需要加引号,最后加上{}。第三步:特殊情况处理undefined、函数和symbol:在JSON中这些类型是不被允许的,通常应该返回undefined或者在数组中被转换为null。循环引用:需要维护一个已访问对象的记录,如果检测到循环引用,应抛出错误或者别的处理方式。示例实现这里是一个简化的示例实现,只包含基础的功能:function jsonStringify(obj) { const type = typeof obj; if (type !== 'object' || obj === null) { // 非对象或null直接返回文本 if (type === 'string') obj = '"' + obj + '"'; return String(obj); } else { const json = []; const isArray = Array.isArray(obj); for (const key in obj) { let value = obj[key]; const valueType = typeof value; if (valueType === 'string') { value = '"' + value + '"'; } else if (valueType === 'object' && value !== null) { value = jsonStringify(value); } json.push((isArray ? "" : '"' + key + '":') + String(value)); } return (isArray ? "[" : "{") + String(json) + (isArray ? "]" : "}"); }}测试示例console.log(jsonStringify({ x: 5, y: 6 })); // 输出:{"x":5,"y":6}console.log(jsonStringify([1, "false", false])); // 输出:[1,"false",false]console.log(jsonStringify({ x: [10, undefined, function(){}, Symbol('')] })); // 输出:{"x":[10,null,null,null]}这个实现非常基础,许多复杂情况如日期对象、正则对象、BigInt、循环引用等情况都没有处理。在实际开发中,需要根据具体需求添加更多的处理逻辑和优化。
前端阅读 2642024年6月24日 16:43

JavaScript有几种类型的值

JavaScript中有8种基本类型的值:Undefined:一个未给定值的变量的类型是undefined。例如: let x;Null:表示缺少或者空值的类型。例如:let x = null;Boolean:有两个boolean操作符:true 和 false。例如:let x = true;String:用于显示文本数据的类型。例如:let x = 'hello world';Number:用于表示整数和浮点数。例如:let x = 3.14;BigInt:一种用于存储和操作任意大小整数的类型。例如:let x = 9007199254740991n;Symbol:一种唯一并且不可变的数据类型。例如:let x = Symbol('hi');Object:对象数据类型用于存储更复杂的数据集。例如: let x = {firstName:"John", lastName:"Doe"};以上8种类型可大致分为两类:原始值(Undefined, Null, Boolean, Number, String, Symbol, BigInt对象值(Object)。
前端阅读 482024年6月24日 16:43

列举 3 种强制类型转换和 2 种隐式类型转换

强制类型转换强制类型转换 是指开发者显式地将一种数据类型转换为另一种数据类型。强制类型转换的例子:Number转换为String:使用 toString()方法来转换数字为字符串。例如:(123).toString(),结果为 "123"。String转换为Number:使用 Number()函数将字符串转换为数字。例如:Number("123"),结果为 123。非布尔值转换为布尔值:使用 Boolean()函数将非布尔值转换为布尔值。例如:Boolean(1),结果为 true。 隐式类型转换隐式类型转换,又被称为隐式类型强制转换,是指JavaScript引擎在处理表达式时自动完成的类型转换。隐式类型转换的两个例子:加法运算符:当通过加法运算符加入字符串和非字符串(数字,布尔值等)时,非字符串将被转换为字符串。例如:"5" + 3,结果为 "53"。相等性比较:如果比较的值具有不同的类型,JavaScript会尝试通过诸如转换字符串为数字,或转换布尔值为数字等方式,来进行比较。例如:"5" == 5,结果为 true。
前端阅读 682024年6月24日 16:43

Form 表单可以执行跨域请求吗?

HTML表单(form)是可以执行跨域请求的。在Web开发中,跨域请求指的是从一个源(domain、协议、端口)发起的请求试图获取另一个源上的资源。通常,出于安全考虑,浏览器会实施同源策略(Same-Origin Policy),这意味着如果使用XMLHttpRequest或Fetch API来发起Ajax请求,那么请求通常会受到限制,除非目标服务器明确允许跨源资源共享(CORS,Cross-Origin Resource Sharing)。但是,HTML表单不受同源策略的限制,因此可以向任何URL发起POST或GET请求,即使这个URL指向的是另一个域。当表单提交时,浏览器会将用户输入的数据作为请求参数发送到表单的 action属性所指定的URL。不过,使用表单进行跨域提交时,浏览器会导航到响应页面,也就是说用户的当前页面会被新页面替换。下面是一个简单的表单跨域请求的例子:<form action="https://example.com/api/submit" method="POST"> <input type="text" name="username" value="User"> <input type="text" name="password" value="Pass"> <input type="submit" value="Submit"></form>在此示例中,表单数据将提交给位于 example.com域的服务器,即使表单所在的HTML页面可能托管在不同的域上。当用户点击提交按钮时,浏览器会将表单数据作为POST请求的一部分发送到 example.com。需要注意的是,即使表单可以跨域提交,服务端仍然需要处理来自不同源的请求。此外,跨域表单提交不会提供Ajax那样的客户端JavaScript接口来访问响应内容,除非服务器在响应中包含适当的CORS头部信息。
前端阅读 292024年6月24日 16:43

JavaScript 执行过程分为哪些阶段?

JavaScript 的执行过程大致可以分为以下几个阶段:1. 解析(Parsing)在这一阶段,JavaScript 引擎会读取源代码,并将其解析成抽象语法树(AST)。抽象语法树是一种深层次的、结构化的代码表达方式,能够以树形结构表现代码中的每个语句、表达式等元素。解析过程中,如果遇到语法错误,会抛出错误,停止进一步执行。2. 编译(Compilation)JavaScript 引擎(如V8)通常会将解析后的代码进行即时编译(JIT)。编译器会先生成字节码,这是一种低级的、比源代码更接近机器语言的代码。随后根据程序的执行情况,编译器可能会把热点代码(经常执行的代码)编译成优化的机器码,提高执行效率。3. 执行(Execution)编译后得到的字节码或机器码被送到 JavaScript 引擎的执行环境中执行。在执行过程中,会进入下面的子阶段:创建执行上下文(Execution Context):首先会创建全局执行上下文,随后每当调用一个函数时,就会为该函数创建一个新的执行上下文。执行上下文包括变量对象、作用域链和 this 引用等信息。变量提升(Hoisting):在执行代码前,函数声明和变量(声明)会被提升到它们各自的执行上下文的顶部。变量会初始化为 undefined,而函数则会完整地提升。执行代码(Running Code):按照执行上下文中的代码逐行执行,进行变量赋值、函数调用等。垃圾回收(Garbage Collection):在执行过程中,引擎会进行内存管理,自动释放那些不再被需要的内存空间。4. 优化(Optimization)在代码执行的过程中,某些代码可能会被执行多次,引擎会尝试对这些频繁执行的代码进行优化。例如,在V8引擎中,有一个称为“TurboFan”的优化编译器,它可以根据代码执行的特点对代码进行优化,提高性能。如果优化假设失败了(即出现了“去优化” deoptimization),引擎还可以将代码回退到一个较少优化的版本。5. 回收(Deoptimization & Garbage Collection)对于那些不再需要的数据和优化,JavaScript 引擎会进行去优化和垃圾回收,以保证内存的高效使用。例子:假设我们有这样一个简单的 JavaScript 函数:function sum(a, b) { return a + b;}let result = sum(5, 3);首先,该函数会被解析成 AST。然后,它可能会被编译成字节码,当我们调用 sum(5, 3) 时,会创建一个新的执行上下文,包含 a 和 b 的参数以及任何局部变量。在这个上下文中,a 和 b 被赋予了值 5 和 3,函数执行,并返回结果 8。这个过程中可能还包括了对 sum 函数的优化,如果函数被频繁调用。最后,当执行上下文离开作用域,如果没有其他引用指向其中的数据,垃圾回收器最终会清理掉这些对象。
前端阅读 432024年6月24日 16:43

JavaScript 为什么要区分微任务和宏任务?

JavaScript 区分微任务(microtasks)和宏任务(macrotasks)主要是为了有效地管理异步操作的执行时机和顺序。这两种类型的任务允许 JavaScript 引擎维持一个控制异步操作何时何地执行的精细化调度机制。宏任务 (Macrotasks)宏任务通常是浏览器的主要任务,包括但不限于:setTimeoutsetIntervalI/O 操作UI 渲染事件处理(如点击、滚动事件)每次执行栈为空时,事件循环都会从任务队列中取出一个宏任务执行。微任务 (Microtasks)微任务通常是需要快速响应的任务,执行时机在每个宏任务之后,以及JavaScript执行环境准备好以后。微任务包括:Promise 回调(例如.then、.catch和.finally)MutationObserver 的回调queueMicrotask函数执行顺序在每个宏任务执行完毕后,在执行下一个宏任务之前,JavaScript 引擎会处理所有队列中的微任务。这意味着微任务总是在当前宏任务结束和下一个宏任务开始之间执行,并在新的UI渲染前完成。这样可以确保异步操作的快速响应,同时因为微任务的延迟更小,所以适合高优先级的任务。为什么区分区分微任务和宏任务的原因包括:性能优化: 通过微任务,JavaScript 可以在不影响用户界面渲染的情况下,快速执行简单的操作,如承诺的解决。这提高了应用程序的响应速度和性能。控制异步操作顺序: 微任务和宏任务的区分允许开发者控制异步操作的执行顺序。例如,一个由Promise产生的微任务可以确保其在下次UI渲染之前解决,而setTimeout可能会推迟到下一个宏任务。避免阻塞: 对于需要快速执行的代码,使用微任务可以避免阻塞宏任务队列,这有助于避免长时间运行的任务阻塞UI更新。示例假设我们有以下代码:console.log('宏任务开始');setTimeout(() => { console.log('宏任务');}, 0);Promise.resolve().then(() => { console.log('微任务');});console.log('宏任务结束');执行顺序会是这样的:打印"宏任务开始"宏任务结束时,设置了一个setTimeout打印"宏任务结束"当前宏任务已结束,开始执行微任务队列中所有任务打印"微任务"微任务队列为空,开始下一个宏任务打印"宏任务"通过这个例子,我们可以看到微任务总是在当前执行栈清空后立即执行,而宏任务的执行则可能会因为任务队列中的其他宏任务而延迟。这种机制允许JavaScript有效地处理异步事件,同时保持对执行顺序的细粒度控制。
前端阅读 262024年6月24日 16:43

jsonp 为什么不支持 post 方法?

JSONP(JSON with Padding)是一种利用<script>标签不受同源策略限制的特性来实现跨源请求的技术。因为<script>标签的初衷是加载静态的JavaScript文件,所以<script>标签仅支持GET方法来请求资源,它并不支持POST方法。这就是为什么JSONP不支持POST方法的根本原因。当使用JSONP进行通信时,您会将请求参数包含在URL中,并通过动态创建<script>标签的方式将其发出。服务器接收到GET请求后,将数据包裹在一个函数调用中,并将其作为响应返回。客户端定义好回调函数后,这段包裹着JSON数据的JavaScript被执行,回调函数便会被调用并处理返回的数据。例如,假设您的页面需要从http://example.com获取一些用户数据,您可能会发送如下的JSONP请求:<script type="text/javascript"> // 定义回调函数 function handleResponse(data) { console.log('Received data:', data); }</script><script type="text/javascript" src="http://example.com/data?callback=handleResponse"></script>服务器端需要接收到callback参数后,把数据包装在该函数调用中:// 服务器端响应handleResponse({ "user": "Alice", "age": 25 });如上所述,JSONP请求的本质是一个GET请求,它是通过<script>标签的src属性来发起的。因此,它不能使用POST方法,后者通常用于传输大量数据或者发送需要安全传输的数据。如果您需要进行跨域的POST请求,可以考虑使用更现代的技术,如CORS(跨源资源共享),它允许在各种HTTP方法中使用跨源请求,同时提供了更好的安全性。
前端阅读 352024年6月24日 16:43

Ajax 如何处理请求跨域问题?CORS 如何设置?

Ajax 处理请求跨域问题Ajax(Asynchronous JavaScript and XML)本身是不支持跨源请求的,这是因为浏览器出于安全考虑实施的同源策略(Same-Origin Policy)。同源策略限制了来自不同源的文档或脚本对当前文档读取或设置某些属性的能力。不过,有几种方法可以绕过这个限制来实现跨源请求:JSONP(JSON with Padding):这是一种老的技巧,它利用 <script>标签不受同源策略限制的特点来进行跨域请求。服务器返回的响应拼接一个函数调用作为JavaScript代码。这种方法的缺点是它只能用于GET请求,并且存在一定的安全隐患。CORS(Cross-Origin Resource Sharing):这是现在推荐的方法,它允许服务器显式地指定哪些源可以访问该服务器上的资源。CORS是一个 W3C 标准,它通过在服务器上设置特定的HTTP头来工作。代理服务器:使用一个服务器充当中间人的角色,接受来自客户端的跨源请求,然后转发请求到目标服务器。代理服务器接收到响应后,再将数据返回给客户端。这种方法需要额外的服务器端配置。WebSockets: WebSockets提供了一个全双工的通信渠道,可以在浏览器和服务器之间建立持久连接。虽然WebSocket协议与HTTP不同,但它可以绕过同源策略,从而实现跨域通信。CORS 设置当你控制服务器时,可以通过设置HTTP响应头来启用CORS。这些头部主要包括:Access-Control-Allow-Origin: 指定了哪些网站可以参与跨域资源共享。例如,设置为 *代表允许所有域进行跨域请求,但出于安全考虑通常会指定明确的域名。Access-Control-Allow-Methods: 指定了允许的HTTP请求方法,如 GET, POST, PUT, DELETE, OPTIONS等。Access-Control-Allow-Headers: 在实际请求中允许自定义的HTTP头部字段列表。Access-Control-Allow-Credentials: 表示是否允许发送Cookie。如果服务器端设置了这个值为 true,那么前端请求的时候也必须将 withCredentials属性设置为 true。Access-Control-Expose-Headers: 允许客户端访问的服务器白名单头部字段。Access-Control-Max-Age: 表明在多少秒内,不需要再发送预检验请求(对于某一资源的预检请求结果的有效期)。下面是一个简单的例子,展示了如何在Node.js的Express框架中设置CORS响应头:const express = require('express');const app = express();app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', 'https://example.com'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); res.header('Access-Control-Allow-Credentials', true); if (req.method === 'OPTIONS') { res.header('Access-Control-Max-Age', '86400'); return res.status(200).send(); } next();});// 你的其他路由和逻辑app.listen(3000, () => { console.log('Server running on port 3000');});
前端阅读 752024年6月24日 16:43

base64 为什么能提升性能?

Base64编码通常不是直接用来提升性能的,而是用来确保二进制数据可以通过仅支持文本格式的传输层安全地传输。Base64将二进制数据编码为ASCII字符串,这使得它可以在不支持二进制数据的系统(如电子邮件)中传输。虽然Base64编码的数据比原始二进制数据大约增加了33%,但在某些情况下,它可以间接地提升性能:减少HTTP请求:在Web开发中,Base64编码通常用于将小的图片或其他文件直接嵌入到HTML或CSS中。这样做的好处是可以减少浏览器发起的HTTP请求的数量,因为所有的资源都包含在了主文档中。少了额外的请求,网页加载时间就会缩短,间接提高了用户体验和性能。举个例子,如果一个网页中有多个小图标,通常的做法可能是每个图标一个HTTP请求来获取图像文件。如果将这些图标的图片用Base64编码后嵌入到CSS中,就可以将多个HTTP请求合并为一个请求,从而减少了服务器的请求负载和网络延迟,提高了页面的加载速度。数据URI方案:Base64编码可以使用数据URI方案在Web页面中直接嵌入图像数据,这样可以避免服务器配置对小文件的较慢响应时间。服务器对小文件的处理往往不如大文件高效,因为涉及到磁盘I/O等开销。通过避免这些小文件请求,可以在服务器端节省资源,从而提升性能。安全和兼容性:有些系统不支持二进制数据的传输,或者在传输过程中可能会因为某些字符(如NUL byte)的存在而出现问题。在这种情况下,Base64编码提供了一种可靠的方法来处理和传输数据,避免了潜在的数据损坏和传输错误,从而确保系统的顺畅运行和性能。总之,Base64编码本身增加了数据量,理论上会降低传输效率,但通过减少HTTP请求的数量、充分利用缓存、避免小文件请求开销以及提高数据的安全性和兼容性,它可以在特定场景下间接提升系统的整体性能。
前端阅读 372024年6月24日 16:43

addEventListener 的第三个参数的作用是什么?

addEventListener 方法是 JavaScript 中常用来为元素添加事件监听器的方法。这个方法可以让开发者指定当某个事件在目标元素上触发时,应该调用的回调函数。addEventListener 方法通常接收三个参数:type: 字符串,表示监听事件类型的名称,比如 click, mouseover 等。listener: 函数,事件触发时浏览器调用的函数。options or useCapture: (可选)布尔值或者是一个对象。这是第三个参数,它指定了事件处理的更多选项。当第三个参数是布尔值时,它指的是 useCapture。如果 useCapture 设置为 true,则表示在捕获阶段触发事件处理函数;如果设置为 false 或者省略,则表示在冒泡阶段触发事件处理函数。在 DOM 事件处理中,事件传播分为三个阶段:捕获阶段、目标阶段和冒泡阶段。默认情况下,事件监听器只在冒泡阶段被调用。如果第三个参数是一个对象,它可以包含多个属性,如下所示:capture: 布尔值,和直接提供 useCapture 作为布尔值的效果一样。once: 布尔值,如果为 true,监听器会在添加之后第一次触发时自动移除。passive: 布尔值,如果为 true,表明监听器永远不会调用 preventDefault()。如果监听器确实调用了这个函数,客户端将会忽略它并且可能给出一个警告。例如,如果我们想要在用户第一次点击按钮时做出反应,并且希望在捕获阶段而不是冒泡阶段处理事件,我们可以这样写代码:const button = document.querySelector('#myButton');button.addEventListener('click', (event) => { // 处理点击事件 console.log('Button clicked!');}, { capture: true, once: true });在这个例子中,{ capture: true, once: true } 作为第三个参数传递,确保了监听器在捕获阶段执行,并且只执行一次。
前端阅读 292024年6月24日 16:43

JavaScript 的遍历方法中,在 map 和 for 中调用异步函数的区别是什么?

在JavaScript中,map和for循环是遍历数组的两种常见方法,但在处理异步函数时,它们的行为有显著差异。使用map调用异步函数map函数是Array原型上的一个方法,它对数组中的每个元素执行一个由你提供的函数,并返回一个新的数组,该数组是由原数组中每个元素调用处理函数得到的结果组成的。当你在map内使用异步函数时,每次迭代都会立即发起异步操作,但不会等待上一个完成,这意味着所有异步操作几乎是同时发起的。map不会等待异步函数的解决,它会立即继续到下一次迭代。例如,如果你使用map遍历数组,并在每个元素上调用一个返回Promise的异步函数:let promises = [1, 2, 3].map(async (num) => { let result = await someAsyncFunction(num); return result;});这里,promises数组将包含三个Promise对象,这些Promise对象是someAsyncFunction返回的,并且他们将并行执行。使用for循环调用异步函数使用传统的for循环,你可以更容易地控制异步函数的执行顺序。如果在循环内部使用await,你可以确保每次迭代都等待上一个异步操作完成再继续。例如,使用for循环顺序执行异步操作:let results = [];for (let num of [1, 2, 3]) { let result = await someAsyncFunction(num); results.push(result);}在这段代码中,someAsyncFunction会为数组中的每个元素顺序执行。第二次迭代会等待第一次迭代中的异步操作完成,以此类推。这意味着异步操作是串行执行的。总结使用map调用异步函数时,所有异步操作几乎同时开始,它们是并行的,最后你得到一个Promise对象的数组。使用for循环(或其他类型的循环,如for...of、for...in、while等)并结合await调用异步函数时,操作将按顺序一个接一个地执行,即串行执行。因此,选择哪种方法取决于你是否需要并行或串行执行异步操作。如果操作之间没有依赖,并且你想最大限度地提高效率,可以使用map。如果操作必须按照一定的顺序执行,或者一个操作的输出是另一个操作的输入,那么使用for循环会更合适。
前端阅读 222024年6月24日 16:43

for..of 和 for...in 是否可以直接遍历对象?

for...of 循环是在ES6中引入的,它专门用于遍历可迭代对象的元素,如数组、字符串、Map、Set 等这些实现了迭代器接口的数据结构。所谓的可迭代对象就是那些具有 Symbol.iterator 属性的对象。例如,数组是可迭代对象,可以使用 for...of 遍历其元素:let array = [10, 20, 30];for (let value of array) { console.log(value);}// 输出:// 10// 20// 30然而,普通对象不是可迭代的,没有实现 Symbol.iterator 方法,因此不能直接使用 for...of 遍历它的属性。尝试使用 for...of 直接遍历一个对象会导致一个错误:let obj = {a: 1, b: 2, c: 3};for (let value of obj) { console.log(value);}// TypeError: obj is not iterable另一方面,for...in 循环是用来遍历一个对象的所有可枚举属性的键,包括继承的可枚举属性。它不仅可以遍历普通对象的属性,还可以遍历数组(虽然通常不推荐这样做,因为它会返回数组索引,而且可能会遍历到原型链上的属性)。使用 for...in 遍历对象的例子:let obj = {a: 1, b: 2, c: 3};for (let key in obj) { console.log(key + ': ' + obj[key]);}// 输出:// a: 1// b: 2// c: 3总结一下,for...of 用于遍历可迭代对象的元素,而 for...in 用于遍历对象的所有可枚举属性的键。因此,for...of 不能直接遍历普通对象,而 for...in 可以。
前端阅读 302024年6月24日 16:43

setTimeout 与 setInterval 的区别是什么?

setTimeout 和 setInterval 都是 JavaScript 中用于控制时间和执行定时任务的函数,但它们的工作方式和用途有所不同。setTimeoutsetTimeout 函数用于设置一个定时器,该定时器将在指定的毫秒数后执行一次您指定的函数或代码块。一旦定时器完成任务(即执行了指定的函数或代码),它就会停止。用法示例:function sayHello() { console.log('Hello!');}// 调用 sayHello 函数,但是会在 2000 毫秒(2 秒)后执行setTimeout(sayHello, 2000);在这个例子中,sayHello 函数会在约 2 秒后执行一次,然后 setTimeout 就完成了它的任务。setInterval与 setTimeout 不同,setInterval 函数用于设置一个定时器,该定时器会无限次地以指定的时间间隔重复执行您指定的函数或代码块,除非您明确停止它。用法示例:function sayHelloRepeatedly() { console.log('Hello again!');}// 每隔 2000 毫秒(2 秒),调用一次 sayHelloRepeatedly 函数const intervalId = setInterval(sayHelloRepeatedly, 2000);// 当你想停止定时器时,可以调用 clearInterval// clearInterval(intervalId);在这个例子中,sayHelloRepeatedly 函数会每隔 2 秒执行一次,这将一直持续下去,直到调用 clearInterval(intervalId) 才会停止这个定时器。总结差异setTimeout 是执行一次延迟操作的函数。setInterval 是重复执行操作的函数,直到清除定时器。setTimeout 定时器执行完毕后自动清除。setInterval 定时器会持续运行,直到你调用 clearInterval。实际应用中,选择哪一个函数取决于你的具体需求:如果你需要延迟执行一次操作,使用 setTimeout;如果你需要以固定的时间间隔重复执行操作,使用 setInterval。
前端阅读 292024年6月24日 16:43

JavaScript 继承都有哪些方法?

在JavaScript中,继承是一个用来使一个类(子类)能够获取另一个类(父类)的属性和方法的机制。以下是在JavaScript中实现继承的几种方法:1. 原型链继承原型链继承是将子类的原型对象设置为父类的一个实例,从而实现继承。function Parent() { this.parentProperty = true;}Parent.prototype.getParentProperty = function() { return this.parentProperty;};function Child() { this.childProperty = false;}// 继承ParentChild.prototype = new Parent();var child = new Child();console.log(child.getParentProperty()); // true2. 构造函数继承构造函数继承通过在子类的构造函数中调用父类构造函数实现继承,并使用 .call()或 .apply()方法将子类的 this绑定到父类上。function Parent(name) { this.name = name;}function Child(name) { Parent.call(this, name);}var child = new Child('Alice');console.log(child.name); // Alice3. 组合继承(原型链 + 构造函数继承)组合继承结合了原型链继承和构造函数继承的优点,即子类的原型被设置为父类的一个实例,并且父类构造函数被用来增强子类实例。function Parent(name) { this.name = name; this.colors = ['red', 'blue', 'green'];}Parent.prototype.sayName = function() { console.log(this.name);};function Child(name, age) { Parent.call(this, name); this.age = age;}Child.prototype = new Parent();Child.prototype.constructor = Child;Child.prototype.sayAge = function() { console.log(this.age);};var child1 = new Child('Alice', 10);child1.colors.push('yellow');console.log(child1.name); // Aliceconsole.log(child1.age); // 10console.log(child1.colors); // ['red', 'blue', 'green', 'yellow']4. 原型式继承原型式继承是基于已有的对象创建新对象,使用 Object.create方法实现。var parent = { name: "Bob", getName: function() { return this.name; }};var child = Object.create(parent);child.name = "Alice";console.log(child.getName()); // Alice5. 寄生式继承寄生式继承创建一个封装继承过程的函数,这个函数在内部以某种方式增强对象然后返回。function createAnother(original) { var clone = Object.create(original); clone.sayHi = function() { console.log('Hi'); }; return clone;}var person = { name: 'Bob', getName: function() { return this.name; }};var anotherPerson = createAnother(person);anotherPerson.sayHi(); // Hi6. 寄生组合式继承寄生组合式继承通过使用寄生式继承来继承父类的原型,并将结果指定给子类的原型。function inheritPrototype(childObj, parentObj) { var prototype = Object.create(parentObj.prototype); prototype.constructor = childObj; childObj.prototype = prototype;}function Parent(name) { this.name = name;}Parent.prototype.sayName = function() { console.log(this.name);};function Child(name, age) { Parent.call(this, name); this.age = age;}inheritPrototype(Child, Parent);Child.prototype.sayAge = function() { console.log(this.age);};var child = new Child('Alice', 10);child.sayName(); // Alicechild.sayAge(); // 10
前端阅读 342024年6月24日 16:43

JavaScript 中 number 为什么会出现精度损失?应该怎样避免number的精度损失问题?

JavaScript 中的 number 类型是基于 IEEE 754 标准的双精度64位浮点数表示。这种表示方式导致了两类主要的精度问题:有限的位数: 64位中,有1位用于符号,11位用于表示指数,剩下的52位用于表示尾数(或分数)。这限制了可以精确表示的数字的范围和精度。当数字超出这个精确范围时,就会出现舍入误差。二进制浮点数的局限性: 并非所有的十进制小数都能被二进制系统精确地表示。例如,十进制的0.1在二进制中是一个无限循环的分数,就像十进制中的1/3不能精确表示一样。在二进制浮点数中,这样的十进制数会被近似为一个有限位数的二进制数,因此会有精度损失。例子:在 JavaScript 中计算 0.1 加 0.2 时,预期结果是 0.3,但实际结果往往是 0.30000000000000004,这展示了精度损失的问题。为了避免这种精度损失,可以使用以下策略:整数运算: 将浮点数转换为整数,进行运算后再转换回去。这适用于简单的加减乘除运算。 // 例子:使用整数运算来避免精度损失 let result = (0.1 * 10 + 0.2 * 10) / 10; // 结果为0.3使用第三方库: 为了处理更复杂的数学运算和避免精度损失,可以使用如 BigNumber.js 或 decimal.js 等第三方库,这些库提供了更为精确的数值计算能力。 // 使用 BigNumber.js 示例 BigNumber.config({ DECIMAL_PLACES: 10 }) let a = new BigNumber(0.1); let b = new BigNumber(0.2); let result = a.plus(b); // '0.3'内置 BigInt 类型: 对于整数运算,ES2020 引入了 BigInt 类型,它支持任意精度的整数。使用 BigInt 可以避免大整数计算中的精度损失,但它不适用于浮点数。 // 例子:使用 BigInt 进行大整数计算 let bigInt1 = BigInt("9007199254740993"); let bigInt2 = BigInt("1"); let result = bigInt1 + bigInt2; // 9007199254740994n总而言之,为了解决 JavaScript 中的 number 类型的精度问题,开发者需要根据实际情况选取适合的方法来保证数值的精确度。对于常规的小数点精度问题,转换为整数运算通常是最简单的解决办法;对于更复杂的场景,则可能需要使用第三方库或者 BigInt 类型。
前端阅读 292024年6月24日 16:43

什么是 base64 编码方式?它有什么作用?

Base64是一种基于64个可打印字符来表示二进制数据的编码方法。这种编码方式设计用来确保二进制数据在编码过程中能够通过不同的媒介,特别是那些只支持ASCII文本的媒介,不会因为字符解读错误而破坏。Base64编码方式的作用包括:数据编码:将二进制数据转换成ASCII字符串,这样数据就可以在文本环境下安全传输,比如通过电子邮件或者XML文件。提升兼容性:某些系统不支持所有的二进制数据或特殊字符,Base64编码后的数据可以在这些系统中无障碍传输。打印友好:Base64编码后的字符串包含的是可打印字符,方便打印和查看。Base64编码规则非常简单,基本过程如下:将原始二进制数据的每个字节分成6位一组,如果最后一组不足6位,则用0填充。对照Base64索引表将这些6位的组合转换成相应的字符。Base64索引表包含了大小写英文字母各26个,加上10个数字和+、/两个符号,共64个字符。如果编码后的字符数不是4的倍数,则用=字符填充,以确保最终的输出字符数是4的倍数。举个例子,如果我们要编码单词"Man"为Base64:原始ASCII码是"M"=77, "a"=97, "n"=110二进制表示为:01001101 01100001 01101110划分成6位一组:010011 010110 000101 101110对照Base64索引表转换:T W F u因此,"Man"这个单词用Base64编码后是"TWFu"。
前端阅读 572024年6月24日 16:43

setTimeout 有什么缺点?setTimeout 和 requestAnimationFrame 之间有什么区别?

setTimeout 的缺点setTimeout 函数是 Web API 的一部分,它可以在指定的毫秒数后执行一个函数或指定的代码。然而,setTimeout 有几个缺点:不精确的时间控制:setTimeout 并不能保证在指定时间后立即执行,因为它受到 JavaScript 事件循环的影响。如果事件队列中有其他任务,setTimeout 的回调可能会延迟执行。性能问题:使用 setTimeout 进行重复的或高频的任务(例如动画)可能会导致性能问题。因为它不会考虑浏览器的绘制帧。这可能会导致动画不流畅或者页面重绘。多个定时器的管理:如果页面上有多个 setTimeout 定时器,管理和清除这些定时器可能会变得复杂。资源消耗:即使浏览器窗口或页面不在前台时,setTimeout 也会继续执行,这可能会导致不必要的 CPU 和电力消耗。setTimeout 与 requestAnimationFrame 的区别setTimeout 和 requestAnimationFrame(简称 rAF)都可以用于延迟执行代码,但它们的用途和行为有显著的区别:目的:setTimeout 用于在设定的时间后执行一次回调函数。requestAnimationFrame 主要用于动画,它告诉浏览器在下次重绘之前执行一个函数,以便动画可以平滑地按照屏幕的刷新率运行。执行时机:setTimeout 的回调执行时间不一定与浏览器的绘制帧同步。requestAnimationFrame 的回调会在浏览器绘制下一帧之前执行,这通常意味着回调以 60 次/秒的频率执行(或者与显示器的刷新率相匹配)。性能:setTimeout 可能会导致掉帧,因为它不考虑浏览器的帧率。requestAnimationFrame 会与浏览器的帧率同步,减少掉帧的情况,因此动画更平滑,性能也更优。节能:setTimeout 在后台标签页或隐藏的 iframe 中仍然会运行,可能导致不必要的资源消耗。requestAnimationFrame 在页面不可见时会自动暂停,从而节省资源。使用场景:setTimeout 适用于不需要与帧率同步的一次性或非频繁的延迟任务。requestAnimationFrame 适用于需要高性能动画的场景,例如游戏或界面动效。