前端面试题手册

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

前端阅读 392024年6月24日 16:43

JS 如何去除 url 中的#号?

如果我们要在JavaScript中去除URL中的 # 号以及后面的部分,我们可以使用 window.location 对象,具体是 window.location.href 属性,再结合 String 对象的 split 方法。请看以下例子: // 假设当前URL为: https://www.example.com/page.html#section1// 使用 JavaScript 获取当前URL并去除 # 及之后的部分function removeHashFromUrl() { var currentUrl = window.location.href; var urlWithoutHash = currentUrl.split('#')[0]; window.location.href = urlWithoutHash; // 如果需要导航到去除hash的URL return urlWithoutHash; // 如果只是需要获取新的URL而不导航}var newUrl = removeHashFromUrl();console.log(newUrl); // 输出:https://www.example.com/page.html如果我们是在后端处理URL字符串,比如在Node.js环境或者其他不涉及浏览器的上下文中,我们可以简单地使用字符串处理方法。这里是用Node.js中的JavaScript例子:// 假设有一个URL字符串var url = "https://www.example.com/page.html#section1";// 去除URL中的 # 及之后的部分function removeHashFromUrl(url) { return url.split('#')[0];}var newUrl = removeHashFromUrl(url);console.log(newUrl); // 输出:https://www.example.com/page.html在Python中处理URL也很简单,我们可以使用内置的 urlparse库,这样可以更加优雅地处理复杂的URL。这是一个Python例子:from urllib.parse import urlparse# 假设有一个URL字符串url = "https://www.example.com/page.html#section1"# 去除URL中的 # 及之后的部分parsed_url = urlparse(url)new_url = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.pathprint(new_url) # 输出:https://www.example.com/page.html以上提供了去除URL中 # 号的几种方法,具体使用哪种取决于具体的应用场景以及开发环境。在前端JavaScript开发中,我们通常可能会涉及到浏览器的 window.location 对象,而在服务器端或者其他一些脚本处理中,则可能会使用字符串处理函数或者URL解析库。
前端阅读 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

cros 的简单请求和复杂请求的区别是什么?

CORS,即跨源资源共享(Cross-Origin Resource Sharing),是一种允许在一个源(origin)上的网页获取访问另一个源上资源的机制。它是一种安全特性,可以让网站的前端代码安全地进行跨域请求,而不会暴露用户数据。CORS 请求分为两类:简单请求(simple requests)和复杂请求(preflighted requests)。它们之间的区别主要体现在请求的方式和所发送内容上。简单请求简单请求满足以下条件:请求方法是以下三种方法之一:GETPOSTHEADHTTP的头信息不超出以下几种字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)简单请求的例子:GET /some/resource HTTP/1.1Host: api.example.comAccept-Language: en-USContent-Type: text/plain当浏览器判断一个请求为简单请求时,它会直接发起跨域请求,并在请求中携带Origin头部信息。服务端会检查这个Origin,决定是否允许这个跨域请求。复杂请求复杂请求通常指不满足以上简单请求条件的所有其他请求,例如:使用了 PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH 等 HTTP 方法。发送的 HTTP 头部信息超出了简单请求允许的范围。Content-Type 的值不属于简单请求中允许的三个值。在发送复杂请求之前,浏览器会先发起一个 OPTIONS 请求,这被称为“预检”请求(preflight request),用来确认真正的请求是否安全可被服务器接受。预检请求的例子:OPTIONS /data/resource HTTP/1.1Host: api.example.comOrigin: http://example.comAccess-Control-Request-Method: POSTAccess-Control-Request-Headers: X-Custom-Header如果服务器允许这样的请求,它会在响应的 HTTP 头部中包含Access-Control-Allow-Origin、Access-Control-Allow-Methods和Access-Control-Allow-Headers等字段,明确告知客户端是否可以进行实际的请求。之后,浏览器才会发送实际的请求。总结简单来说,简单请求是对CORS更宽容的请求,直接发起并通过Origin头部判断是否允许跨域;而复杂请求需要先进行一次额外的预检通信以确认安全性,只有在预检通过后,实际的请求才会发起。这个机制确保了敏感操作(如对数据的修改)在跨域时能够得到恰当的安全检查。
前端阅读 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 可以。
前端阅读 92024年6月24日 16:43

分别介绍事件冒泡、事件代理、事件捕获,以及它们的关系?

事件冒泡 (Event Bubbling)事件冒泡是一种事件传播机制,在这种机制下,当一个元素上的事件被触发时,这个事件会从触发元素开始,逐级向上传播至最外层的父元素。这种传播方式允许在父元素上监听并处理来自子元素的事件。事件冒泡通常用于减少事件处理器的数量,并且简化事件管理。例子: 假设我们有一个按钮(<button>)位于一个段落(<p>)元素内,该段落又位于一个容器(<div>)元素内。如果用户点击了按钮,那么点击事件会首先在按钮元素上触发,然后依次向上冒泡至段落元素,最终到达容器元素。事件代理 (Event Delegation)事件代理是一种借助事件冒泡机制实现的事件处理模式。它通过在父元素上设置一个事件监听器来管理所有子元素的同类事件。这样可以避免在每个子元素上单独设置事件监听器,从而提高效率和性能,尤其是在动态添加或删除子元素的情况下。例子: 假如我们有一个任务列表,列表中的每一项任务都需要一个点击事件监听器。使用事件代理,我们可以在任务列表的容器元素上设置一个点击事件监听器,而不是在每个任务项上单独设置。当点击事件发生并冒泡到容器元素时,我们可以检查事件的目标元素(event.target)来确定是哪个任务项被点击,并进行相应的处理。事件捕获 (Event Capturing)事件捕获是DOM事件流的另一部分,与事件冒泡相对应。在事件捕获阶段,事件从最外层的父元素开始传递,一直向下直到触发元素。主要的区别在于事件的传播方向:事件捕获是从外到内,而事件冒泡是从内到外。例子: 再次使用上面的按钮、段落和容器元素的场景,当用户点击按钮时,在事件捕获阶段,点击事件会首先到达最外层的容器元素,然后到达段落元素,最后到达按钮元素。它们的关系事件捕获和事件冒泡是DOM事件流的两个阶段。在实际的事件处理中,浏览器首先经过捕获阶段,从最外层的父元素向下传递到目标元素,然后是目标元素上的事件处理,接着是冒泡阶段,事件从目标元素开始向上逐级传播。事件代理则是利用了事件冒泡原理来简化事件管理。通过在父元素上监听事件,可以管理所有子元素的事件,而无需在每个子元素上单独绑定事件监听器,使得代码更加简洁高效。在使用 addEventListener 方法时,我们可以指定第三个参数为 true 或 false 来明确选择是在捕获阶段还是冒泡阶段处理事件,默认值为 false,即在冒泡阶段处理。总之,事件捕获和事件冒泡共同构成了事件传播的完整过程,而事件代理则是一种利用这种传播机制的高效事件处理策略。
前端阅读 102024年6月24日 16:43

闭包的核心是什么

闭包(closure)的核心是能够捕获并包含自己创建时所在作用域中的变量,并在这个函数在其原始作用域之外被调用时仍然可以访问那些变量。闭包是函数和声明该函数时的词法环境的组合。在编程中,闭包常常用于以下几种场合:数据封装:闭包可以用来模拟私有变量,因为它的内部状态对外部代码是不可见的,只能通过闭包暴露的方法来访问。示例:function createCounter() { let count = 0; // `count` 是一个闭包中的私有变量 return { increment: function() { count++; }, getValue: function() { return count; } };}const counter = createCounter();counter.increment();console.log(counter.getValue()); // 输出 1在这个例子中,createCounter 返回了一个拥有两个方法的对象,而这两个方法都有权访问私有变量 count。count 变量是无法从外部直接访问的,只能通过闭包提供的 increment 和 getValue 方法来操作。回调函数和异步执行:闭包常用于回调函数,尤其是在异步操作中,它能够记住并访问其创建时的环境,即使在主执行流程已经完成之后。示例:function asyncGreeting(name) { setTimeout(function() { // 这个匿名函数就是一个闭包 console.log('Hello, ' + name); }, 1000);}asyncGreeting('World'); // 1秒后输出 "Hello, World"setTimeout 的回调是一个闭包,它记住了变量 name,即使 asyncGreeting 函数已经执行完毕。模块化代码:通过闭包,可以创建模块,这些模块拥有公开的方法和私有的数据,这是一种设计模式(即模块模式),可用于组织和管理 JavaScript 代码。在这些场景中,闭包的核心特性是记忆作用域中的变量,这使得函数在定义它的那个作用域之外执行时依然可以访问那些变量。这对于JavaScript等语言来说是一个极为强大的特性,因为它可以创造出在其他编程范式中难以实现的某些功能。
前端阅读 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。
前端阅读 172024年6月24日 16:43

JavaScript 怎么判断是一个空对象

JavaScript 中判断一个对象是否为空对象,通常可以通过检查对象是否有自身的属性。最常见的方法是使用 Object.keys() 函数,它会返回一个由给定对象的所有可枚举自身属性的属性名组成的数组。如果这个数组的长度为0,那么可以认定该对象为一个空对象。以下是一个函数示例,它可以用来判断一个对象是否为空:function isEmptyObject(obj) { return Object.keys(obj).length === 0 && obj.constructor === Object;}这个函数首先检查 Object.keys(obj) 返回的数组长度是否为0,确保没有枚举的自身属性。然后,通过 obj.constructor === Object 确保 obj 是由 Object 构造函数创建的,避免错误地将具有自身属性但不是普通对象的实例(比如 new Date())判断为“空对象”。我们可以用以下例子来测试这个函数:// 空对象let obj1 = {};// 非空对象let obj2 = { prop: 'value' };// 空对象的另一种形式(通过 Object.create(null) 创建)let obj3 = Object.create(null);console.log(isEmptyObject(obj1)); // trueconsole.log(isEmptyObject(obj2)); // falseconsole.log(isEmptyObject(obj3)); // true, 但请注意这种对象没有原型链在这些例子中,obj1 是一个标准的空对象,所以 isEmptyObject(obj1) 返回 true。obj2 有一个自身属性 prop,所以 isEmptyObject(obj2) 返回 false。obj3 虽然是一个空对象,但它通过 Object.create(null) 创建,这意味着它没有原型链,而 isEmptyObject(obj3) 也返回 true。需要注意的是,这个函数不能检测那些没有可枚举属性但实际上不是空的对象,比如:let obj4 = Object.create(Object.prototype, { prop: { value: 'value', enumerable: false }});console.log(isEmptyObject(obj4)); // true,但实际上 obj4 有一个不可枚举的属性 prop在这种情况下,prop 属性是不可枚举的,因此 Object.keys(obj4) 不会包含 prop,导致 isEmptyObject(obj4) 错误地返回 true。如果需要考虑不可枚举的属性,可以使用 Object.getOwnPropertyNames() 替换 Object.keys(),它会返回所有自身属性的数组,无论它们是否可枚举。
前端阅读 172024年6月24日 16:43

ajax/axios/fetch 的区别是什么?

Ajax (Asynchronous JavaScript and XML)、Axios 和 Fetch API 是前端开发中用于与服务器进行异步通信的不同技术。下面,我将详细介绍它们之间的主要区别:Ajax概念: Ajax 并不是一种独立的技术,而是一系列技术的集合,包括 HTML、CSS、JavaScript、DOM、XMLHttpRequest 等。它允许网页在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页内容。XMLHttpRequest 对象: Ajax 通常依赖于 XMLHttpRequest 对象来进行通信。这是一个老旧的 API,可以用来执行异步请求。例子: var xhr = new XMLHttpRequest();xhr.open('GET', 'server.php', true);xhr.send();xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { console.log(xhr.responseText); }};兼容性: XMLHttpRequest 在老版本的浏览器中有很好的兼容性。缺点: 编写代码较繁琐,不支持 Promise,错误处理和编写异步代码可能比较复杂。Axios概念: Axios 是一个基于 Promise 的 HTTP 客户端,用于浏览器和 node.js。它提供了一个易于使用的 API,可以在客户端执行异步请求。特点: 支持 Promise,可以很容易地使用 .then() 和 .catch() 方法进行链式调用。Axios 还提供了请求取消、HTTP 自动转换 JSON 数据、客户端支持防御 XSRF 等功能。例子: axios.get('/user?ID=12345') .then(function (response) { console.log(response); }) .catch(function (error) { console.log(error); });兼容性: Axios 可以用在浏览器和 node.js 中,并且基本上支持所有现代浏览器。缺点: 相对于 Fetch API,Axios 是一个额外的库,需要单独安装和配置。Fetch API概念: Fetch API 提供了一个 JavaScript 接口,用于访问和操纵 HTTP 管道的部分,例如发起请求。它提供了一个全局 fetch() 方法,具有比 XMLHttpRequest 更加简洁和强大的功能。特点: Fetch API 基于 Promise 设计,使得异步请求代码更加简洁。它不是以形式参数的形式,而是以配置对象的形式接收请求信息,使得请求更加灵活。例子: fetch('https://api.example.com/data', { method: 'GET', headers: { 'Content-Type': 'application/json' }}).then(response => response.json()).then(data => console.log(data)).catch(error => console.error(error));兼容性: Fetch API 在现代浏览器中得到了良好的支持,但在一些老旧浏览器中可能不可用。缺点: Fetch API 默认不发送或接收 cookies,如果站点依赖于用户会话,则需要额外的配置。此外,即使请求失败,fetch 也不会拒绝 Promise,只有在网络故障时才会被拒绝。总结来说,Ajax、Axios 和 Fetch API 提供了实现前端 HTTP 通信的不同方法。Ajax 是最传统的方式,依赖于 XMLHttpRequest。Axios 是一个现代的库,提供了丰富的 API 和易于使用的 Promise 支持。
前端阅读 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 类型。
前端阅读 122024年6月24日 16:43

JS 改变 this 指向的方式都有哪些?

在JavaScript中改变 this指向的常见方式主要有以下几种:使用函数的 .bind()方法.bind()方法会创建一个新的函数,你可以传入一个对象来指定原函数中的 this。新函数的 this将被永久绑定到 .bind()的第一个参数上。 function greeting() { return `Hello, I'm ${this.name}`; } const person = { name: 'Alice' }; const boundGreeting = greeting.bind(person); console.log(boundGreeting()); // "Hello, I'm Alice"使用函数的 .call()和 .apply()方法.call()和 .apply()方法都是在特定的 this上调用函数,即可以直接指定 this的值。两者的区别在于如何传递函数的参数:.call()方法接受参数列表,而 .apply()方法接受一个包含多个参数的数组。 function introduction(name, profession) { console.log(`My name is ${name} and I am a ${profession}.`); } introduction.call(person, 'Alice', 'Engineer'); // "My name is Alice and I am a Engineer." introduction.apply(person, ['Alice', 'Engineer']); // "My name is Alice and I am a Engineer."箭头函数箭头函数不会创建自己的 this上下文,因此它的 this值继承自上一层作用域链。这是编写回调函数或闭包时常见的使用场景。 function Team(name) { this.name = name; this.members = []; } Team.prototype.addMember = function(name) { this.members.push(name); setTimeout(() => { console.log(`${name} has been added to the ${this.name} team.`); }, 1000); }; const team = new Team('Super Squad'); team.addMember('Hero'); // "Hero has been added to the Super Squad team." after 1 second在这个例子中,setTimeout中的箭头函数继承了 addMember方法的 this上下文。在回调函数中使用局部变量保存 this在 ES6 之前,由于函数的 this值在运行时确定,一个常见的模式是在闭包中用变量(通常是 self或 that)保存对外层 this的引用。 function Team(name) { this.name = name; this.members = []; var that = this; this.addMember = function(name) { that.members.push(name); setTimeout(function() { console.log(name + ' has been added to ' + that.name + ' team.'); }, 1000); }; } var team = new Team('Super Squad'); team.addMember('Hero'); // "Hero has been added to Super Squad team." after 1 second使用库或框架提供的功能一些JavaScript库和框架提供了自己的方法来绑定或者定义 this的上下文,比如在React组件的事件处理中,你可能会使用类似 .bind()的方法。在类中使用箭头函数定义方法在ES6类中,你可以使用箭头函数定义类的方法,这样就能确保方法内部的 this绑定到类的实例。 class Button { constructor(label) { this.label = label; } handleClick = () => { console.log(`Clicked on: ${this.label}`); } } const button = new Button('Save'); const btnElement = document.createElement('button'); btnElement.textContent = button.label; btnElement.addEventListener('click', button.handleClick); // 点击按钮时,会正确打印 "Clicked on: Save"以上就是JavaScript中改变 this指向的主要方式。
前端阅读 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 适用于需要高性能动画的场景,例如游戏或界面动效。
前端阅读 332024年6月24日 16:43

let 块作用域是怎么实现的?

let 关键字在JavaScript中被引入是为了提供块作用域(block scope)的功能。块作用域意味着由 let 声明的变量仅在声明它们的代码块内部是可见的。代码块是被花括号 {} 包围的一段代码,例如在 if 语句、for 和 while 循环以及函数定义中都会用到代码块。在ES6之前,JavaScript主要依赖的是函数作用域(function scope),由 var 关键字声明的变量要么是全局的,要么是在函数内部局部的。这种设计有时会导致意料之外的问题,特别是在循环中。下面是一个使用 let 的例子来说明块作用域是如何工作的:function runLoop() { for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 100 * i); }}runLoop();在这个例子中,变量 i 是用 let 在 for 循环的块中声明的。这意味着每次循环迭代时,变量 i 都是一个新的变量,并且它被限制在这个循环的块作用域中。所以当 setTimeout 的回调函数执行时,它能够访问到循环迭代时对应的 i 的值。如果我们用 var 替换掉 let,结果将会不同:function runLoop() { for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 100 * i); }}runLoop();在这个例子中,由于 var 声明的变量 i 是函数作用域的,当 setTimeout 的回调函数执行时,它会打印出循环结束后变量 i 的最终值,即5,会打印五次5,而不是0到4。总结来说,let 关键字允许开发者在更细粒度的级别控制变量的作用域。这样做提高了代码的可读性和可维护性,并且减少了由于作用域导致的常见错误。
前端阅读 442024年6月24日 16:43

讲一下 import 的原理,与 require 有什么不同?

import 的原理在JavaScript中,import语句用于从模块中导入绑定(即函数、对象、原始类型等)。这是ES6规范(即ECMAScript 2015)引入的模块化特性的一部分。import的工作原理基于ECMAScript模块(ESM)系统。当你使用 import语句时,JavaScript引擎执行以下步骤:解析模块标识符:确定要导入的模块的位置及其文件路径。模块加载:如果模块尚未加载,JavaScript引擎会加载模块文件。编译模块:引擎会对模块代码进行编译,检查语法并进行优化。执行模块代码:在私有的模块作用域内执行模块代码,以初始化导出的绑定。缓存模块:模块的导出会被缓存,这意味着每个模块只会被执行一次,之后的导入会重用同一份导出的实例,保持状态的一致性。import 与 require 的不同import和 require都是JavaScript中用于加载模块的语句,但它们之间存在几个关键差异:语法规范:import是ES6中引入的模块化语法,而 require则来自于CommonJS规范,后者主要用于Node.js环境中。模块类型:import用于加载ESM模块,而 require用于加载CommonJS模块。加载方式:import声明是静态的,意味着它必须位于模块的顶部,不能动态运行或按条件导入模块。require是动态的,可以在代码的任何地方调用,支持条件加载和运行时动态计算路径。异步与同步:import可以支持异步模块的导入,通过 import()函数进行动态导入,返回一个Promise对象。require的加载是同步的,当调用 require时,代码会停止执行,直到模块被加载和返回。性能优化:由于 import是静态的,它允许JavaScript引擎进行更强大的性能优化,比如死代码消除和模块的静态分析。导出绑定的可变性:使用 import导入的绑定是活动的,也就是说如果导出的模块变量值发生变化,导入的绑定也会更新。使用 require导入的值是导出值的拷贝,一旦导入,无论源模块如何变化,导入的值都不会改变。例子使用 import:// ES6模块导入语法import { myFunction, myVariable } from './myModule.js';// 使用导入的函数和变量myFunction();console.log(myVariable);使用 require:// CommonJS模块导入语法const myModule = require('./myModule.js');// 使用模块的属性和方法myModule.myFunction();console.log(myModule.myVariable);在处理前端项目时,我们可能更倾向于使用 import,因为它与现代JavaScript模块化标准一致,而在Node.js环境中,尽管现在已经支持ESM,require依然被广泛使用,特别是在老项目中。
前端阅读 102024年6月24日 16:43

事件的触发过程是怎么样的?什么是事件代理?

事件的触发过程在Web开发中,事件的触发过程通常遵循以下几个步骤:捕获阶段:事件开始由最外层的document对象向事件目标节点传播的阶段。这个阶段不是所有事件都会有。目标阶段:事件到达目标元素,即实际触发事件的元素。冒泡阶段:事件从目标元素向外传播到document对象的阶段,事件可以在这个阶段的任意元素上被监听和处理。例如,假设我们有一个按钮(<button>元素),它位于一个段落(<p>元素)内,该段落又位于一个页面(document)。如果用户点击了按钮,那么在捕获阶段,事件会从document开始,经过<p>,直到达到<button>。此时,事件进入目标阶段,通常是在这里触发任何与按钮直接相关的事件监听器。之后,事件会进入冒泡阶段,途径<p>元素,最后到达document。在这个过程中,开发者可以选择在捕获阶段或冒泡阶段的任何点上处理事件。事件代理事件代理(Event Delegation)是一种常用的事件处理模式,它利用了JavaScript中事件的冒泡机制。在这种模式下,我们不是直接在目标元素(例如一个按钮)上设置事件监听器,而是在其父元素上设置一个事件监听器,监听其所有子元素的事件。当子元素上的事件被触发并冒泡到父元素时,父元素上的监听器会捕捉到这些事件,并根据事件的来源执行相应的事件处理函数。事件代理的优势在于:减少内存消耗:不需要为每个子元素都添加事件监听器,只需要在父元素上添加一个监听器即可。动态内容的事件管理:对于动态添加到页面中的元素,不需要重新绑定事件监听器,已有的事件代理依然有效。简化事件管理:通过在一个中心位置管理事件,使事件的添加、删除和修改变得更加容易。例子:假设我们有一个任务列表,每个任务项都有一个删除按钮,我们要给这些按钮添加点击事件来删除对应的任务项。如果使用事件代理,我们会在任务列表的容器上添加一个点击事件监听器:document.getElementById('taskList').addEventListener('click', function(event) { if (event.target.className === 'delete-btn') { // 如果点击的是删除按钮,则删除对应的任务项 event.target.closest('.task-item').remove(); }});在这个例子中,无论何时新增任务项,其删除按钮点击事件都会被容器上的事件监听器捕获和处理,而不需要单独给每个删除按钮绑定事件监听器。这就是事件代理的概念。
前端阅读 222024年6月24日 16:43

事件的触发过程是怎么样的?知道什么是事件代理吗?

事件的触发过程事件的触发过程,通常指的是在Web浏览器中,当用户与网页上的元素交互时(如点击按钮、移动鼠标等),将会触发相应的事件(如click, mousemove等)。此过程遵循一个特定的模式,称为“事件流”,它描述了从浏览器到DOM元素再回到浏览器的过程。事件流有两种模型:事件冒泡和事件捕获。事件捕获(Capturing): 事件开始于window对象,然后向下传递到目标元素的父元素,最终到达目标元素自身。这个过程是从外向内逐层捕获事件的过程。目标阶段(Targeting): 事件到达目标元素,执行绑定在该元素上的事件处理器。事件冒泡(Bubbling): 在目标阶段完成后,事件又会从目标元素开始,逐层向上冒泡,直到window对象。举个例子,假设我们有一个按钮元素,它位于一个表单内,该表单又位于HTML页面的body元素内。当用户点击按钮时,如果所有这些元素都对点击事件定义了处理函数:在捕获阶段:首先window对象检查是否有onclick事件处理器,然后是body元素,接着是form元素,最后是按钮本身。目标阶段:事件到达按钮元素,触发绑定在按钮上的点击事件处理器。在冒泡阶段:事件从按钮开始向上冒泡,先到form元素,然后是body元素,最后是window对象。开发者可以通过JavaScript控制事件监听器是在捕获阶段还是冒泡阶段触发。事件代理事件代理是一种常用于减少内存使用并避免为多个子元素绑定监听器的技术。事件代理的基本原理是利用了事件冒泡的特性。而不是在每个子元素上单独设置事件监听器,我们在其父元素上设置单个监听器,以监控所有子元素上的事件。在这个监听器中,我们可以使用 event.target属性来获取实际触发事件的元素,并据此执行相应的事件处理逻辑。事件代理的主要优势在于:内存效率:不必为每个子元素创建和维护独立的事件监听器,减少了内存的占用。动态元素:对于在运行时动态添加到DOM中的元素,我们不需要再单独为它们添加监听器,因为父元素上的代理监听器已经能够处理。简化管理:当有许多子元素需要相同的事件处理逻辑时,通过在父元素上设置单一监听器,简化了事件管理。举个例子,假设我们有一个待办事项列表(<ul>元素),它下面有多个列表项(<li>元素)。如果我们想要为每个列表项添加点击事件,使用事件代理的方式如下:// 假设ul元素有id="todo-list"var todoList = document.getElementById('todo-list');// 为ul元素添加点击事件监听器todoList.addEventListener('click', function(event) { if (event.target.tagName.toLowerCase() === 'li') { // 这里可以处理点击事件 console.log('你点击了列表项:' + event.target.textContent); }});
前端阅读 1162024年6月24日 16:43

在 CSS 中,::before 和 :after 中双冒号和单冒号有什么区别

在 CSS 中,::before 和 :before,::after 和 :after是伪元素,它们被用来向选择的元素添加一些内容。其中,::before 和 ::after 是在 CSS3 才出现的,并且是推荐的用法。双冒号 :: 是用来区分伪元素和伪类的。伪元素用于向元素的特定部分添加样式。例如,::first-line、::first-letter、::before、::after 等。单冒号 : 是 CSS1 和 CSS2 中的写法,在 CSS3 中仍然维持了对它的支持,主要是为了维持向后兼容。单冒号 : 用来表示伪类,例如 :hover、:active、:focus 等。举例来说的话,如果你写的样式需要兼容一些较为老旧的浏览器,那么可能需要使用 :before 和 :after。在现代浏览器中,推荐使用 ::before 和 ::after。总结如下:双冒号 :: 用于 CSS3 的伪元素。单冒号 : 用于 CSS2 的伪元素以及所有的伪类,同时保持向后的兼容。目前很多浏览器都已经支持了双冒号,但如果你在开发时需要兼容老版本的浏览器,那么还是建议使用单冒号。