前端面试题手册
前端如何解决移动端H5点击有300ms延迟?
在移动端H5开发中,300ms点击延迟是一个历史问题,它最初是由于早期智能手机的浏览器为了区分单击与双击(放大操作)而故意设置的。用户在触摸屏幕的时候,浏览器会等待大约300毫秒来判断用户是否要进行双击操作。这在现代的Web开发中通常是不必要的,并且会导致用户体验下降。以下是几种常见的解决方法:使用viewport meta标签:通过在HTML中添加viewport meta标签,可以禁用用户缩放,从而消除延迟。例如: <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">这段代码能禁止用户缩放页面,因此浏览器不需要等待判断用户是否要双击缩放,从而消除了300ms延迟。CSS touch-action属性:CSS的touch-action属性可以用来指定某个元素的一些触摸行为,例如: button { touch-action: manipulation; }这个属性设置后可以关闭某些默认的触摸操作,比如缩放和双击滚动,从而消除延迟。快速点击库(如FastClick):FastClick是一个流行的JavaScript库,用于在不支持Pointer Events的浏览器上消除点击延迟。它通过监听触摸事件来避开300ms延迟。直接在touchend事件触发后立即发出click事件,从而绕过浏览器自身的300ms延迟。使用方法很简单,只需要在页面加载完毕后实例化FastClick对象即可: if ('addEventListener' in document) { document.addEventListener('DOMContentLoaded', function() { FastClick.attach(document.body); }, false); }利用Pointer Events:Pointer Events是一个浏览器的新标准,它可以合并触摸、鼠标、笔和其他类型的输入事件。通过使用pointerevent,你可以避免300ms延迟。例如: element.addEventListener('pointerdown', function(event) { // 处理点击事件 });然而,并非所有浏览器都支持Pointer Events,因此需要检查兼容性或使用polyfill。使用touchstart或touchend代替click事件:你也可以直接监听touchstart或者touchend事件来代替click事件,这样可以避免等待300ms。但这种方法需要注意的是,touchstart和touchend可能会在用户没有意图点击时触发(例如滚动屏幕时),所以需要额外的逻辑来判断实际的用户意图。以上就是几种常用的解决移动端H5点击300ms延迟的方法。在实际开发中,可能需要根据具体情况和浏览器的兼容性选择最适合的方案。
为什么 React 元素有一个 $$typeof 属性
React 元素有一个 $$typeof 属性的原因主要是出于安全考虑,它是用来防止一种名为 XSSI(跨站脚本包含)的攻击。$$typeof 属性可以帮助确保在 React 应用中引入的数据是有效的 React 元素,而非恶意对象。在过去,攻击者可能会尝试利用 JSONP 等技术将恶意脚本插入到网站中。JSONP 是一种通过 <script> 标签载入跨域的 JSON 数据的方法。在 JSONP 中,由于 <script> 标签会执行载入的内容,如果返回的数据不是纯粹的 JSON 而是可执行的 JavaScript 代码,那么它就可能被用于执行恶意脚本。为了防止这种攻击,React 开发团队引入了 $$typeof 属性。每个通过 React.createElement 创建的元素都会自动添加这个属性。这个属性的值是一个特殊的符号(在支持 Symbol 的 JavaScript 环境中)或者一个特定的数字(在不支持 Symbol 的环境中)。这样,在 React 对元素进行处理之前,它可以检查元素是否具有正确的 $$typeof 值,确保该元素是由 React 创建的,从而防止可能的 XSSI 攻击。例如,如果我们通过网络请求获取了一个对象,并且直接将其作为子元素传递给 React 来渲染,React 会检查这个对象是否具有正确的 $$typeof 属性。如果没有,React 将不会将其作为 React 元素进行处理,这样就能防止恶意对象被当作 React 元素来渲染,从而避免潜在的安全风险。总之,$$typeof 属性是 React 为了增强应用安全而引入的一个机制,用于验证被处理的元素确实是通过 React 的正确方式创建的,从而避免了某些类型的安全漏洞。
什么是深拷贝?什么是浅拷贝?
深拷贝和浅拷贝是编程中用于复制数据结构的两种不同方法,它们在复制复杂对象(如包含其他对象的对象或数组等)时的行为上有所区别。浅拷贝浅拷贝(Shallow Copy)只复制对象的顶层结构,如果对象中包含了对其他对象的引用,浅拷贝不会复制被引用的对象本身,而是复制引用。因此,原始对象和浅拷贝后的对象会共享相同的引用类型数据。例子:假设我们有一个对象A,它包含一个指向另一个对象B的引用。如果我们对A进行浅拷贝得到A',那么A'中会有一个指向B的引用,而不是B的一个拷贝。如果我们修改B,那么这个修改会通过A和A'的引用被反映出来。import copyoriginal_list = [1, 2, [3, 4]]shallow_copied_list = copy.copy(original_list)# 修改原始列表中的嵌套列表元素original_list[2][0] = "changed"# 输出被浅拷贝的列表print(shallow_copied_list) # 输出将会是 [1, 2, ["changed", 4]]在这个例子中,修改原始列表中嵌套列表的元素也会影响到浅拷贝的列表,因为它们共享同一个嵌套列表对象的引用。深拷贝深拷贝(Deep Copy)不仅仅复制对象的顶层结构,还会递归地复制所有对象的成员,包括对象中引用的对象。这样,原始对象和深拷贝后的对象不会共享任何引用类型的数据。例子:使用相同的对象A和B,如果我们对A进行深拷贝得到A'',那么A''将会包含一个B的完整拷贝B'。此时,如果我们修改B,A''中的B'不会受到影响,因为B'是一个独立的对象。import copyoriginal_list = [1, 2, [3, 4]]deep_copied_list = copy.deepcopy(original_list)# 修改原始列表中的嵌套列表元素original_list[2][0] = "changed"# 输出被深拷贝的列表print(deep_copied_list) # 输出将会是 [1, 2, [3, 4]]在这个例子中,深拷贝的列表保持原有结构,不受原始列表中嵌套列表元素修改的影响。总结来说,浅拷贝只复制了最顶层的对象,深拷贝则是复制了所有层级的对象,确保了复制体与原始数据结构之间完全独立。在实际应用中,选择浅拷贝还是深拷贝取决于具体的需求和情况。
阅读 9·2024年6月24日 16:43
什么是弹性盒布局模型?
弹性盒布局模型,通常称为Flexbox,是CSS3中的一种先进布局方式,旨在提供一种更有效的方式来布置、对齐和分配容器内项目的空间,即使它们的尺寸是未知或者是动态变化的。Flexbox布局给予了容器能够扩展项目以填充可用空间,或者收缩它们以防止溢出的能力。Flexbox的主要特点:灵活性:Flexbox允许项目的宽度和高度可以根据容器的空间动态伸缩。方向无关性:与传统的布局(如块布局,基于行,或内联布局,基于列)不同,Flexbox的方向可以是水平的或垂直的,这意味着它不受页面流的限制。容器和项目的区别:在Flexbox中存在两种类型的元素:容器(flex容器)和项目(flex项目)。容器用来定义一个flex环境并包含一些布局属性,项目则是容器的子元素,可以使用不同的属性来控制其自身的排版。对齐:可以轻松地在水平或垂直方向上对齐项目,这在以前的布局模型(如浮动或定位)中通常需要额外的工作。空间分配:当项目占用的空间小于容器的时候,可以控制多余空间的分配方式,或者当空间不足时,控制如何收缩项目。例子:假设我们有一个导航菜单,我们希望菜单项在容器内均匀分布,并且在不同屏幕尺寸上都能保持良好的布局。使用Flexbox,我们可以这样来定义容器和项目:/* Flex Container */.nav-menu { display: flex; justify-content: space-between; /* 分散对齐子项 */}/* Flex Items */.nav-item { flex-grow: 1; /* 允许子项根据需要占据空间 */ text-align: center; /* 文本居中对齐 */}在HTML中,我们的导航菜单可能是这样的:<nav class="nav-menu"> <a class="nav-item" href="#">首页</a> <a class="nav-item" href="#">关于我们</a> <a class="nav-item" href="#">服务</a> <a class="nav-item" href="#">联系方式</a></nav>在上述示例中,.nav-menu是一个flex容器,.nav-item是其子项,也就是flex项目。通过对.nav-menu使用display: flex;属性,我们定义了一个弹性容器,并且使用justify-content: space-between;来确保.nav-item在容器内均匀分布。每个.nav-item都被设置了flex-grow: 1;属性,这意味着所有的子项都会根据可用空间进行伸缩,来占据容器的空间。这样,不管屏幕大小如何变化,菜单项都能保持均匀的布局。Flexbox的这种灵活性和易用性,使得它成为创建响应式布局和现代网页设计必不可少的工具之一。
阅读 15·2024年6月24日 16:43
什么是双向绑定?Vue 是如何实现双向绑定功能的?
双向绑定是一种编程模式,用于简化用户界面与应用状态之间的同步。在传统的单向绑定中,用户界面(UI)只是从应用状态中读取数据并显示出来;而在双向绑定模式中,UI不仅可以显示出应用状态,还可以修改它,反过来也一样,应用状态的改变也会立即反映在UI上。在Vue.js中,双向绑定主要通过v-model指令实现。v-model指令在内部使用了Vue的响应式系统,这个系统基于Object.defineProperty或Proxy(在Vue 3中)实现。下面是Vue实现双向绑定的两个主要步骤:响应式数据的建立:在Vue 2.x版本中,Vue通过Object.defineProperty方法拦截对data对象属性的访问和修改。Vue将data对象中的每个属性都转换为getter/setter,并且在内部追踪这些属性的依赖(即哪些组件或计算属性依赖于这个数据属性)。在Vue 3.x版本中,Vue使用了ES6的Proxy特性来实现响应式。Proxy可以更灵活地拦截和定义对象属性的行为,包括属性的读取、写入以及枚举等,并且它是以更精细的方式工作,不再需要递归地遍历每个属性。依赖收集与派发更新:当组件进行渲染时,会访问与之相关的响应式数据属性,这时Vue会进行依赖收集,即记录下当前组件依赖了哪些数据。当响应式数据发生变化时,Vue会通知所有依赖于这个数据的组件进行更新。如果是通过v-model绑定的输入元素(如<input>, <select>, <textarea>等)发生了用户输入,v-model会监听这些输入事件,将新的值赋值给绑定的数据属性。数据的更新又会触发组件的重新渲染,从而将更新反映在UI上。例如,考虑下面的Vue模板代码:<input v-model="message">这里的v-model指令绑定了一个名为message的数据属性。当用户在输入框中输入文字时,message的值会被更新,同时,如果其他地方的代码改变了message的值,输入框中显示的内容也会相应更新。这种机制的好处是,开发者不需要手动监听输入事件然后更新数据,也不需要观察数据的变化再去更新UI,Vue的双向绑定机制会自动处理这一切。
vue3 中 setup 中如何获取组件实例
在 Vue 3 中,setup 函数是组件选项 API 的替代,它允许您在组件创建之前使用组合式 API 设置组件的响应式状态和函数。通常情况下,setup 函数内部并不直接提供组件实例,因为它在组件实例创建之前就被调用了。但是,您可以通过 Vue 提供的 getCurrentInstance 方法来获取当前组件实例。请注意,getCurrentInstance 主要用于库和框架的开发者,而不是推荐给普通应用程序开发中使用,因为它暴露了一些内部的 API,可能会导致与 Vue 的未来版本不兼容。下面是如何在 setup 函数中获取组件实例的示例:import { getCurrentInstance } from 'vue';export default { setup() { // 获取当前组件实例 const instance = getCurrentInstance(); // 确保 instance 不为 null if (instance) { // 现在你可以访问组件实例的属性和方法 console.log(instance.proxy); // 输出 Vue 代理对象,包含 data、methods 等 } // 定义 setup 函数返回的响应式数据和方法 return { // ... }; },};在上述代码中,我们首先从 vue 导入了 getCurrentInstance 函数。在 setup 函数内部,我们调用了这个函数以获取当前组件的实例。getCurrentInstance 返回的是一个包含组件实例的内部数据的对象,其中 instance.proxy 属性代表了组件的代理对象,它包含组件的所有响应式数据、计算属性以及方法等。使用 getCurrentInstance 时,请确保您的代码不过度依赖于 Vue 的内部实现,以免在未来的 Vue 版本升级中产生兼容问题。
css 选择器的权重计算方式
在 CSS (层叠样式表) 中,选择器的权重由其特定性(specificity)确定。具有更高特定性的样式规则将覆盖较低特定期的规则。以下就是计算特定性的方法:内联样式(Inline Styles):在 HTML 元素内部使用 style 属性定义的样式。例如 <div style="color: red;"> 的权重是 1000。ID 选择器:如 #content(权重为 0100)。类选择器、属性选择器、伪类选择器:如 .class, [type="text"], :hover(权重为 0010)。元素选择器、伪元素选择器:如 div, p, ::before (权重为 0001)。特殊选择器:*、+、>、~ 这些组合选择器不增加权重。对于 !important 属性权重最高,出现在样式属性值后,如 color: red !important; 。计算权重时,各类选择器的数量是独立计算的,比较权重时,会按照四个层级(四个数字)进行逐级比较,权重高的样式优先生效。例如, ID 选择器的权重更高,所以 #id p(一个 ID 和一个元素,权重为 0101)会覆盖 .class .class .class(三个类,权重为 0030)的样式。可理解各部分权重为 四个 "十进制位",高位权重大于低位。 请注意:权重只是决定 CSS 样式冲突的一个因素,还应考虑源代码的顺序和继承。
阅读 47·2024年6月24日 16:43
JavaScript 的继承方式有哪些?
JavaScript的继承方式主要包括以下几种:1. 原型链继承 (Prototype Chain Inheritance)原型链继承是JavaScript最基本的继承方式。每个JavaScript对象都有一个原型(prototype),对象从其原型继承属性和方法。创建子类时,将子类的原型设置为父类的实例,从而实现继承。function Parent() { this.parentProperty = true;}Parent.prototype.getParentProperty = function() { return this.parentProperty;};function Child() { this.childProperty = false;}Child.prototype = new Parent(); // 设置Child的原型为Parent的实例Child.prototype.constructor = Child; // 修正constructor指向var childInstance = new Child();console.log(childInstance.getParentProperty()); // true原型链继承存在的问题包括:创建子类实例时不能向父类构造函数传参,而且所有实例共享父类构造函数中的引用属性。2. 构造函数继承 (Constructor Inheritance)构造函数继承使用父类构造函数来增强子类实例,通过 call或 apply方法在子类构造函数中调用父类构造函数。function Parent(name) { this.name = name; this.colors = ['red', 'blue', 'green'];}function Child(name) { Parent.call(this, name); // 子类构造函数中调用父类构造函数}var childInstance = new Child('ChildName');console.log(childInstance.name); // ChildName这种方式允许不同的子类实例有不同的属性。不过,它无法继承父类原型上定义的方法,因此不是真正意义上的继承。3. 组合继承 (Combination Inheritance)组合继承结合了原型链和构造函数的技术,通过使用原型链继承原型上的属性和方法,使用构造函数继承实例属性。function Parent(name) { this.name = name; this.colors = ['red', 'blue', 'green'];}Parent.prototype.sayName = function() { return this.name;};function Child(name, age) { Parent.call(this, name); // 借用构造函数继承属性 this.age = age;}Child.prototype = new Parent(); // 原型链继承方法Child.prototype.constructor = Child;var childInstance = new Child('ChildName', 18);console.log(childInstance.sayName()); // ChildNameconsole.log(childInstance.age); // 18组合继承弥补了原型链和构造函数继承的不足,然而它也使得父类构造函数被调用两次,造成了一些不必要的性能开销。4. 原型式继承 (Prototypal Inheritance)原型式继承是指使用一个已有对象作为新创建对象的原型。ES5引入了 Object.create方法来实现原型式继承。var parent = { name: 'Parent', sayName: function() { return this.name; }};var child = Object.create(parent);child.name = 'Child';console.log(child.sayName()); // Child这种方式非常简洁,但同样,所有实例会共享引用属性。5. 寄生式继承 (Parasitic Inheritance)寄生式继承类似于原型式继承,但是在此基础上可以增加更多属性或方法。var parent = { name: 'Parent', sayName: function() { return this.name; }};function createAnother(original) { var clone = Object.create(original); clone.sayHi = function() { console.log('Hi'); }; return clone;}var child = createAnother(parent);child.sayHi(); // Hi
阅读 21·2024年6月24日 16:43
JavaScript 如何实现自定义事件吗?
在JavaScript中实现自定义事件主要有以下几个步骤:创建自定义事件:您可以使用Event构造器或者更专门化的构造器如CustomEvent来创建一个事件。CustomEvent构造器还允许您传递附加的信息(称为“详情”或detail属性)。触发自定义事件:使用dispatchEvent方法可以在特定的元素上触发自定义事件。调用此方法时,您需要传递您创建的事件对象。监听自定义事件:使用addEventListener方法可以在一个元素上添加一个事件监听器,当特定的事件类型被触发时执行回调函数。以下是一个简单的例子:// 第一步:创建一个自定义事件// 这里我们创建一个名为 "userLogin" 的事件,并携带一些用户信息var loginEvent = new CustomEvent("userLogin", { detail: { username: "JohnDoe" }});// 第二步:监听这个自定义事件// 假设我们希望在文档的某个元素上监听这个事件document.addEventListener("userLogin", function(e) { console.log("用户登录事件被触发,登录用户名为:" + e.detail.username);});// 第三步:触发自定义事件// 当需要的时候,我们可以在任何时候触发这个自定义事件document.dispatchEvent(loginEvent);在这个例子中,我们定义了一个名为"userLogin"的自定义事件,它在用户登录时被触发。当这个事件被触发时,我们添加了一个监听器来处理这个事件,并输出了用户的名字。这样,我们就可以在应用程序的任何部分触发"用户登录"事件,并且相关的处理逻辑会被执行。这种机制非常有用,特别是在需要在不相关的组件之间传递信息或者在特定的行为之后触发一系列的行为时。使用自定义事件可以使我们的应用程序更加模块化,事件的生产者与消费者可以松耦合地交互。
阅读 22·2024年6月24日 16:43
什么是柯里化函数?JavaScript 中有哪些使用场景?
柯里化(Currying)是一种在函数式编程中常见的技巧。它是指将一个多参数的函数转换成多个单参数(或较少参数)函数的序列。这样做的主要目的是参数复用、提前确认和延迟执行等。定义以 JavaScript 为例,柯里化的一个基本定义可以是这样的:function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn.apply(this, args); } else { return function(...args2) { return curried.apply(this, args.concat(args2)); } } };}这个 curry 函数接收一个函数 fn 作为参数,并返回一个新的函数 curried。新函数检查接收的参数数量,如果足够执行原函数,则直接执行;如果不够,则返回一个新的函数,等待接收更多的参数。使用场景参数复用:当你有一个函数需要多次调用,但是某些参数在这些调用中是不变的时候,可以通过柯里化创建特定的函数,只传入剩下的参数。例如,如果你有一个 add 函数,可以创建一个 addFive 函数,它将一个数与5相加。 function add(a, b) { return a + b; } const addFive = curry(add)(5); addFive(10); // 返回 15延迟计算/执行:柯里化允许你将多个参数的获取分散到多个步骤中。只有当所有需要的参数都被提供后,函数才会被执行。这在需要等待某些数据才能执行操作的情况下非常有用。例如,如果一个函数需要从几个不同的数据源获取数据,可以将每个数据源的结果逐一传入柯里化函数中。动态生成函数:可以动态地生成需要的函数。在处理事件监听或者回调函数的时候,如果某些参数是已知的,可以通过柯里化生成一个新的函数,而无需重新定义函数体。 const on = curry(function(eventType, element, callback) { element.addEventListener(eventType, callback); }); const onClick = on('click'); onClick(document.getElementById('myButton'), () => console.log('Button clicked!'));函数组合:在函数式编程中,柯里化可以帮助实现函数组合。通过柯里化,可以轻松地将一个函数的输出作为另一个函数的输入。这在创建数据流和中间件等链式操作时非常有用。 const compose = (f, g) => (a) => f(g(a)); const multiplyBy2 = (n) => n * 2; const addTen = (n) => n + 10; const multiplyBy2AndAddTen = compose(addTen, multiplyBy2); multiplyBy2AndAddTen(5); // 返回 20总之,柯里化是一种强大的函数式编程技术,它在 JavaScript 中可以用于创建更灵活和可重用的代码。通过柯里化,可以更容易地实现参数复用、延迟执行和高阶函数构建等高级编程模式。
阅读 34·2024年6月24日 16:43