前端面试题手册
什么是defineProperty方法?什么时候需要用到defineProperty?
Object.defineProperty 方法是 JavaScript 中的一个非常重要的方法,它允许精确地添加或修改对象的属性。通过这个方法,我们可以控制属性的各种特性,如它们是否可写、可枚举或可配置,甚至定义存取器属性(getter 和 setter)。Object.defineProperty 方法接受三个参数:要在其上定义属性的对象。将被定义或修改的属性的名称。描述符对象,它定义了属性的具体行为和特征。属性描述符对象可以包含以下键:value:属性的值。writable:如果为 true,则属性的值可以被重写。configurable:如果为 true,则该属性的描述符可以被改变,也可以从对象上删除该属性。enumerable:如果为 true,则该属性会出现在对象的枚举属性中。get:作为属性的 getter 函数,当访问属性时触发。set:作为属性的 setter 函数,当属性值被修改时触发。以下是一个使用 Object.defineProperty 的例子:const person = {};Object.defineProperty(person, 'name', { value: '张三', writable: false, enumerable: true, configurable: true});console.log(person.name); // 输出 '张三'person.name = '李四'; // 由于 writable 是 false,这里的赋值操作不会成功console.log(person.name); // 仍然输出 '张三'在上面的例子中,我们定义了一个对象 person 并使用 Object.defineProperty 给它添加了一个名为 name 的属性。这个属性被设置为不可写,所以尝试更改它的值将不会有任何效果。需要用到 Object.defineProperty 的情况包括:控制属性特性:当你需要精细地控制一个对象属性的可枚举性、可配置性、可写性等。创建只读属性:当你希望对象的某些属性是只读的,不允许修改。定义存取器属性:如果你想通过 getter 和 setter 控制属性的访问和赋值。保护对象结构:在对象创建后保护其结构不被意外改变,比如防止属性被删除。这些特性使得 Object.defineProperty 成为在创建具有特定行为的对象属性时非常有用的工具,特别是在模块和库的开发中,我们经常需要确保对象的接口行为符合预期,防止意外的修改。
阅读 15·2024年6月24日 16:43
javascript 定时器为什么是不精确的?
JavaScript 中的定时器包括 setTimeout 和 setInterval,这两个函数通常用于延迟执行或定期执行某些代码。但是,它们的执行时间并不精确,原因主要有以下几点:单线程和事件循环:JavaScript 是单线程执行的,它依赖于事件循环来处理异步事件。当你设置一个定时器时,你实际上是告诉事件循环在未来的某个时间点上执行一段代码。但是,如果事件循环在处理其他任务时被阻塞,例如执行一个耗时的同步操作,那么定时器的代码执行将会被推迟直到主线程空闲为止。Web 浏览器的定时器分辨率:出于性能和节能的考虑,Web 浏览器的定时器分辨率不会非常高。这意味着定时器的回调函数可能不会在预期的精确毫秒数后立即执行。不同的浏览器和不同的环境(比如浏览器后台运行或系统睡眠)可能会对定时器的精度有不同的影响。最小延迟时间:在 HTML5 标准中,规定了 setTimeout 和 setInterval 的最小延迟时间通常不得低于4ms(若之前调用了5次以上的定时器,则最小延迟10ms)。这意味着即使你请求更短的时间间隔,执行也会被推迟到这个最小值。浏览器标签页的背景化:当用户将包含JavaScript定时器的浏览器标签页置于后台时,大多数现代浏览器会减少定时器的执行频率以节约资源和电池寿命。这可能导致后台标签页中的定时器变得非常不精确。垃圾回收:JavaScript的垃圾回收机制可能会暂时阻塞主线程,从而延迟定时器的执行。例如,假设你有一个使用 setTimeout 指定在10ms后执行的回调函数,但是此时主线程正在处理一个复杂的计算任务,这可能需要100ms才能完成。在这种情况下,即使定时器的时间到了,回调函数也必须等到计算任务完成后,事件循环才能调用它。所以实际上回调函数的执行时间将晚于预期的10ms。综上所述,由于单线程的特性、事件循环的机制、Web浏览器的实现细节和环境限制,JavaScript定时器的执行时间就不能保证绝对精确。开发者在使用定时器时,应该考虑到这些因素,并且不应该依赖定时器来进行精确的时间控制。
阅读 23·2024年6月24日 16:43
当添加原生事件不移除时,为什么会出现内存泄露?
内存泄露通常是指在程序运行过程中,分配的内存未能及时释放,导致随着时间的推移,系统可用内存逐渐减少。当我们在JavaScript中处理原生DOM事件时,如果不正确移除这些事件监听器,很容易导致内存泄露。在详细解释原因之前,先来了解一下事件监听器和内存泄露的基本概念。事件监听器是我们绑定到DOM元素上的函数,用来响应特定的事件,比如点击或者键盘按键。而内存泄露则是指已经不再需要的内存,由于各种原因,没有被及时回收,使得应用程序占用的内存越来越多。现在,让我说明为什么不移除原生事件监听器会导致内存泄露:事件监听器与DOM元素的引用关系:当你给一个DOM元素添加事件监听器时,浏览器会创建一个引用,指向该监听器的函数。即使你从DOM树中移除了这个元素,如果事件监听器没有被移除,那么浏览器的事件处理系统仍然会保留一个对该函数的引用,这导致了DOM元素和监听器函数无法被垃圾回收机制回收,因为从技术上讲,它们依旧是可达的。闭包:在JavaScript中,闭包是一个常见的功能,它允许函数访问并操纵函数外部的变量。如果事件监听器是一个闭包,它可能会引用其他外部变量或对象。只要这个监听器存在,所有它引用的对象也都无法被回收,即使这些对象已经没有其他用途。复杂的引用链:在大型的web应用程序中,DOM元素、事件监听器、以及其他相关对象可能会构成复杂的引用链。这些引用链使得垃圾回收变得复杂,如果链中的某一部分没有被正确管理和解除引用,就可能导致整个链条上的对象都不能被回收。让我举一个例子来说明这个问题:假设我们有一个简单的网页应用,它有一个按钮,当用户点击时会弹出一个警告框:function setup() { var button = document.getElementById('myButton'); button.addEventListener('click', function handleClick() { alert('Button clicked!'); });}setup();如果我们某个时刻决定移除这个按钮:var button = document.getElementById('myButton');button.remove();虽然DOM元素被移除了,但是我们没有移除与之绑定的handleClick事件监听器。这意味着handleClick依然保留着对button元素的引用,因此即使我们从DOM中移除了这个元素,它也不会被垃圾回收。如果我们的应用中有很多这样的操作,随着时间的推移,内存消耗将不断增加,这就是内存泄露。解决这个问题的一个方法是在移除DOM元素之前,显式地移除事件监听器:var button = document.getElementById('myButton');button.removeEventListener('click', handleClick);button.remove();这样,我们就手动断开了事件监听器与DOM元素之间的引用关系,使得这些对象可以被垃圾回收。这是为什么在处理原生DOM事件时,开发者需要注意正确地添加和移除事件监听器,以避免不必要的内存
阅读 16·2024年6月24日 16:43
localstorage 对象有哪些 API?
LocalStorage 是 Web Storage API 的一部分,它允许网页在用户浏览器本地存储数据。下面是 LocalStorage 对象常用的 API 方法:setItem(key, value): 该方法用于在 LocalStorage 中存储数据。它接收两个参数,key(键)表示存储数据的名称,而 value(值)则是实际存储的数据。例如:localStorage.setItem('username', 'JohnDoe');getItem(key): 通过 key(键)检索存储在 LocalStorage 中的值。如果指定的键不存在,它将返回 null。例如:let userName = localStorage.getItem('username');console.log(userName); // 输出: JohnDoeremoveItem(key): 删除 LocalStorage 中的指定 key(键)的数据。例如:localStorage.removeItem('username');clear(): 清除 LocalStorage 中的所有数据。例如:localStorage.clear();length: 一个只读属性,返回 LocalStorage 中的数据项数。例如:let numberOfItems = localStorage.length;console.log(numberOfItems);key(index): 根据索引来获取 LocalStorage 中的键。索引从 0 开始。如果索引超出了范围,将返回 null。例如:let firstKeyName = localStorage.key(0);console.log(firstKeyName);LocalStorage 的数据会以字符串的形式存储,即使你存储的是数字或是对象。如果需要存储对象,通常会使用 JSON.stringify 方法将对象转换为字符串格式存储,检索时再用 JSON.parse 方法将字符串转换回对象。例如:let user = { name: 'John', age: 30 };localStorage.setItem('user', JSON.stringify(user));let retrievedUser = JSON.parse(localStorage.getItem('user'));console.log(retrievedUser); // 输出: { name: 'John', age: 30 }以上就是 LocalStorage 对象的主要 API。需要注意的是,虽然 LocalStorage 提供了方便的本地存储机制,但它并不适合存储敏感信息,因为这些信息是以明文形式存储的,且没有到期时间,容易受到跨站脚本(XSS)攻击的影响。
阅读 54·2024年6月24日 16:43
什么是 webp 图片?它的优势和缺点是有哪些?
WebP 图片简介:WebP 是一种现代图像格式,由Google在2010年发布。它是一种旨在为网页图像压缩提供优异的压缩比的格式,其目标是减小图像的文件大小,而在视觉上保持高质量。WebP 支持无损压缩和有损压缩,同时还支持透明度(Alpha通道)和动画。WebP 的优势:更小的文件大小: 相较于传统的JPEG和PNG格式,WebP图像通常可以在不牺牲视觉质量的情况下提供更小的文件大小。这意味着更快的加载时间和更低的带宽消耗,这对于移动设备用户和带宽有限的环境尤其重要。支持有损和无损压缩: WebP既可以提供有损压缩以减少文件大小,也可以提供无损压缩来保持图像质量。这使得它比只能提供一种压缩方式的格式更为灵活。支持动画: WebP可以替代传统的GIF格式,提供更优的动画压缩效果,同时文件尺寸比GIF更小。支持透明度: 除了对颜色的压缩外,WebP还支持8位的透明通道,即使是在有损压缩的情况下也能处理透明度,这对于网页设计中的图标和图形元素非常有用。色彩表现: WebP支持广色域,这意味着它能够展现更丰富的颜色和更平滑的色彩过渡。WebP 的缺点:兼容性问题: 尽管WebP的支持正在增加,但一些旧的浏览器和图像编辑软件还不支持WebP格式。这可能导致必须为不同的用户提供不同格式的图片。编辑软件支持有限: 相较于JPEG和PNG,WebP格式在图像编辑软件中的支持较少,可能需要使用特定的工具来编辑或转换这些图像。用户认知: 普通用户可能不熟悉WebP格式,对于需要用户上传图片的网站来说,可能需要额外的工作来确保用户上传的图片是支持的格式。转换成本: 对于已经有大量存量图片的网站,将图片转换为WebP格式可能需要消耗一定的时间和资源。例子:为了说明WebP格式的优势,我们可以考虑一个在线商城。该商城的页面包含了大量的产品图片,如果这些图片使用WebP格式而不是JPEG或PNG,那么页面加载速度可能会显著提高,从而改善用户体验和提升转化率。在一个实际测试中,转换JPEG图片到WebP格式,可能会看到文件大小减少25%以上,而视觉上的差异几乎察觉不到。这对于用户来说意味着更快的加载时间,对于商城运营者来说,则意味着更低的数据传输费用。然而,为了确保所有用户都能正常浏览图片,商城可能需要保留JPEG或PNG格式的备份,以便不支持WebP的浏览器也能显示图片。
阅读 126·2024年6月24日 16:43
移动设备安卓与-ios-的软键盘弹出的处理方式有什么不同
移动设备的Android和iOS平台在软键盘弹出的处理方式上确实存在一些差异,主要反映在以下几个方面:1. 视图调整Android:Android平台上,当软键盘弹出时,默认情况下会重新调整活动(Activity)的大小,以确保当前获得焦点的输入字段可见。开发者可以通过设置AndroidManifest.xml中Activity的windowSoftInputMode属性来调整这一行为。例如,adjustResize会调整Activity的大小,而adjustPan会上移整个视图。例子:<activity android:name=".MainActivity" android:windowSoftInputMode="adjustResize"></activity>iOS:在iOS上,软键盘的弹出默认不会引起视图的调整,除非开发者主动编写代码来处理键盘事件。开发者通常需要监听键盘的UIKeyboardWillShow和UIKeyboardWillHide通知,并据此调整视图,比如通过更改底部视图的约束或者滚动视图的contentInset。例子:NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)@objc func keyboardWillShow(notification: NSNotification) { // 调整视图以适应键盘}@objc func keyboardWillHide(notification: NSNotification) { // 恢复视图}2. 第三方键盘支持Android:Android系统在较早时候就开放了对第三方键盘应用的支持,因此Android上的应用需要能够适应多样的键盘布局和行为。iOS:iOS开始对第三方键盘支持是从iOS 8开始的,iOS平台上的应用也需要适应不同的键盘行为,虽然Apple提供了一套相对统一的界面风格。3. 输入法切换Android:Android设备通常会在软键盘上提供一个方便的输入法切换按钮,允许用户快速切换不同的键盘。iOS:iOS设备通常需要用户通过点击全局的键盘切换键或者长按地球标志键来切换输入法。4. API和框架支持Android:Android提供了InputMethodManager等API来管理软键盘的显示和隐藏。iOS:iOS通过UIKit框架中的UITextField、UITextView等组件与键盘进行交互,同时提供了相关的API来控制键盘的行为。5. 软键盘定制Android:Android允许开发者相对更深层次地定制软键盘,例如通过创建自定义输入法编辑器(IME)。iOS:iOS对于键盘的定制性较低,对于标准的键盘行为有着较严格的界面指南。总结:Android和iOS在软键盘弹出的处理方式上体现出的差异主要是由于两个平台的设计哲学和开发者工具的不同。Android提供了更多的自动调整视图的选项和对第三方键盘的支持,而iOS则要求开发者更加主动地管理键盘事件和视图调整。开发者在开发跨平台应用时,需要了解这些差异,并确保提供适合各自平台的用户体验。
阅读 44·2024年6月24日 16:43
介绍防抖节流原理区别以及应用
防抖(Debouncing)原理:防抖是一种控制方法,用于减少函数调用的频次。具体而言,当一个动作频繁触发时,防抖会确保该函数在指定的时间内只被执行一次。如果在这个延迟时间内再次触发了该动作,计时器会被重置,直到延迟时间结束后,才会执行函数。应用:举个例子,假设你在一个搜索框中键入文本,为了减少请求服务器的次数,你可以利用防抖技术。这意味着用户停止键入一段时间(比如500毫秒)之后,才会发送搜索请求。如果用户在500毫秒内继续输入,计时器会重置,直到用户停止输入后过了500毫秒,才会执行搜索。节流(Throttling)原理:节流是另一种控制方法,它同样用于减少函数调用的频次。与防抖不同的是,节流会强制函数以固定的时间间隔执行。即便动作频繁触发,函数也只会按照这个间隔执行,不会像防抖那样完全等待动作停止。应用:以滚动事件为例,如果我们需要在用户滚动页面时进行一些计算或更新界面元素,不加控制的话可能会导致性能问题。此时我们可以应用节流技术,比如设置每100毫秒执行一次更新操作,无论在这100毫秒内滚动事件被触发了多少次,更新函数都只会执行一次。区别触发频率:防抖函数会在动作结束后才执行,而节流函数会按照预定的频率执行,不管动作是否结束。目的:防抖通常用于确保某些计算密集型或高延迟的任务不会因为用户的高频操作而频繁执行,节流则用于控制函数的执行频率,保持性能的平稳。使用场景:防抖适用于诸如输入框内容自动完成、窗口大小调整(resize)、提交按钮(防止多次提交)等场景。节流适用于滚动加载、用户滚动监听、动画的持续触发等场景。总结来说,防抖和节流都是优化高频事件触发后的回调执行频率的技术。防抖是通过延迟执行来合并频繁的触发,而节流是通过减少执行的频率来降低触发的次数。根据具体的应用场景和需求选择合适的优化策略,可以显著提升应用的性能和用户体验。
阅读 10·2024年6月24日 16:43
在 Vue 中,watch和watchEffect的区别是什么?
在Vue中,watch和watchEffect是两种响应式侦听器,都能够对响应式状态的变化作出反应,但是它们的工作方式和使用场景有所不同。watchwatch API 允许我们侦听特定的数据源,并在数据源改变时执行回调函数。它是响应式系统的一部分,非常适合于执行异步操作或比较昂贵的操作,因为你可以精确控制何时以及如何响应状态的变化。精确性: watch允许你指定确切的响应式引用或计算属性来侦听。懒执行: watch默认情况下不会立即执行回调,它只会在侦听的响应式源发生变化时才执行。深度监听: watch可以配置为深度监听,侦听对象内部属性的变化。旧值和新值: watch回调提供新旧值,便于比较。停止监听: watch返回一个停止观察函数,你可以用它来停止监听。例子:<template> <div>{{ count }}</div></template><script>export default { data() { return { count: 0 } }, watch: { count(newVal, oldVal) { // 当 count 改变时,这个函数将被调用 console.log(`Count changed from ${oldVal} to ${newVal}`); } }}</script>watchEffectwatchEffect是一个响应式侦听器,它自动追踪它的函数内部使用的任何响应式状态,当这些状态改变时会重新执行该函数。自动追踪: watchEffect会自动侦听函数内部所有的响应式引用,并在引用更新时重新运行。立即执行: watchEffect在创建时会立即执行一次,确保响应式效果与当前的状态同步。无需指定侦听源: 不需要像watch那样指定侦听的具体状态,它会自动收集依赖。无旧值: watchEffect不提供旧值,因为它不侦听特定的数据源。停止监听: watchEffect同样返回一个停止监听的函数。例子:<template> <div>{{ count }}</div></template><script>import { ref, watchEffect } from 'vue';export default { setup() { const count = ref(0); watchEffect(() => { // 这个函数会在 setup() 时立即执行一次,并在 count 改变时再次执行 console.log(`Count is now: ${count.value}`); }); return { count }; }}</script>总结一下,watch更适用于当你需要侦听特定数据并在变化时进行比较或执行复杂逻辑时,而watchEffect则更适用于当你希望自动追踪并响应响应式状态变化,而不需要过多地控制侦听源和执行时机时。
阅读 79·2024年6月24日 16:43
Vite 为什么启动非常快
Vite 之所以能快速启动,主要得益于它的两个核心设计理念:使用原生 ES 模块(ESM)和按需编译。原生 ES 模块加载(ESM):Vite 利用现代浏览器支持的原生 ES 模块特性来服务模块。在开发模式下,Vite 作为一个由浏览器原生支持的模块服务器,它不需要对代码进行打包和构建,而是通过 <script type="module"> 直接加载模块。这样可以快速启动服务器,并且在请求代码时,浏览器只需加载需要的模块,而不是整个应用程序的打包文件。按需编译:当使用传统的打包工具时,比如 Webpack,整个应用的所有依赖在启动时都需要先被打包,这包括了构建第三方库和应用代码。这个过程需要时间,特别是当应用规模庞大时。与此相反,Vite 在开发模式下采用了按需编译的策略。这意味着只有当请求某个文件时,Vite 才会对这个文件进行编译和处理。这种方式显著减少了启动时间,并且能够快速响应代码的热更新。缓存优化:Vite 对依赖的预构建过程进行了优化。它会在第一次运行时将所有的依赖项编译成 ESM,并将结果缓存起来。在随后的启动中,如果没有依赖变化,它会直接使用缓存的结果而不是重新编译,这进一步提高了启动速度。快速的 HMR(热模块替换):Vite 的热模块替换是模块级别的,当文件发生变化时,只有那个文件和依赖它的链会被重新加载和编译。这比 Webpack 这样的打包器进行整体刷新要快得多。高效的依赖预打包(Pre-bundling):对于某些复杂的依赖(如 CommonJS 或者依赖其他模块的库),Vite 使用 esbuild 预打包这些依赖。esbuild 是用 Go 语言编写的,它的打包速度远远超过基于 JavaScript 的打包器,如 webpack 和 Rollup。例如,假设你有一个具有数百个模块的大型应用程序。在传统的打包器中,即使你只修改了一个模块,也需要重新打包整个应用程序,这可能需要几十秒甚至更长时间。而在 Vite 中,如果你修改了一个模块,只有这个模块会被重新编译和返回,因此你几乎可以立即看到变化,从而极大提高了开发效率。总的来说,Vite 之所以能够实现快速的启动,是因为它摈弃了在开发阶段不必要的打包步骤,采用了更现代的模块加载方式,并通过智能的缓存和预编译策略来提高效率。这些设计理念使得 Vite 在开发过程中能够提供极致的快速响应体验。
阅读 8·2024年6月24日 16:43
如何广度优先遍历DOM树?
广度优先遍历(Breadth-First Traversal, BFT)是一种遍历或搜索树或图结构的算法,它从根节点开始,然后遍历所有邻近的节点,再对每个邻近节点做同样的处理,依此类推,直到遍历完所有可达的节点。在DOM树中应用广度优先遍历同样遵循这个原则,其中DOM树的根节点通常是document对象的documentElement属性,通常指向HTML文档的<html>元素。在JavaScript中执行DOM树的广度优先遍历,我们可以使用队列(Queue)这一数据结构来辅助实现。以下是一个简单的例子:function breadthFirstTraversal(root) { // 创建一个队列,并将根节点入队 let queue = [root]; // 当队列不为空时,循环执行 while (queue.length > 0) { // 出队一个节点并访问 let currentNode = queue.shift(); console.log(currentNode.tagName); // 打印当前节点的标签名 // 将当前节点的所有子节点入队 [].slice.call(currentNode.children).forEach(child => { queue.push(child); }); }}// 调用函数,传入document.documentElement作为遍历的起点breadthFirstTraversal(document.documentElement);在这个例子中,我定义了一个名为breadthFirstTraversal的函数,它接收一个DOM节点作为遍历的起点。然后,我使用一个数组作为队列来存放待访问的节点。在while循环中,我不断地从队列中取出节点,访问它,并将它的子节点加入队列末尾。通过这种方式,我能够按照广度优先的顺序访问整个DOM树的每一个节点。这个例子中,console.log(currentNode.tagName)是对当前节点的访问方式,实际应用中可以替换为其他操作,比如获取或修改节点信息等。此外,.slice.call(currentNode.children)是一种常见的技巧,用来将HTMLCollection或NodeList对象转换为数组,以便使用数组的forEach方法。在现代的JavaScript中,你也可以直接使用Array.from(currentNode.children)来进行转换。
阅读 14·2024年6月24日 16:43