商汤科技前端面试题及参考答案
有没有配置过 webpack,讲一下 webpack 热更新原理,能否自己实现一些插件?
Webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。在实际项目中,经常会对其进行配置,以满足项目的各种需求,比如处理不同类型的文件、优化代码、配置开发服务器等。
Webpack 热更新(HMR,Hot Module Replacement)的原理如下:
- 首先,在 webpack-dev-server 启动时,会创建一个 WebSocket 服务器,用于与浏览器建立连接。
- 当代码发生变化时,webpack 会监听到文件的更改,然后对更改的模块进行重新编译。
- 编译完成后,webpack 会通过 WebSocket 将更新的模块信息发送到浏览器。
- 浏览器接收到更新信息后,会根据这些信息来决定如何更新页面。对于样式模块,通常会直接替换样式表;对于 JavaScript 模块,会尝试在不刷新页面的情况下,用新的模块替换旧的模块,并执行相应的更新逻辑。
自己可以实现一些 webpack 插件。webpack 插件是基于事件流机制的,通过在 webpack 的不同生命周期钩子上注册回调函数来实现各种功能。例如,要实现一个简单的插件来在打包结束后打印出打包时间,可以这样做:
class MyPlugin {apply(compiler) {compiler.hooks.done.tap('MyPlugin', (stats) => {const endTime = new Date().getTime();const startTime = stats.startTime;const duration = (endTime - startTime) / 1000;console.log(`打包完成,耗时 ${duration} 秒`);});}
}
module.exports = MyPlugin;
然后在 webpack 配置中引入这个插件:
const MyPlugin = require('./MyPlugin');
module.exports = {// 其他配置plugins: [new MyPlugin()]
};
vue 双向数据绑定实现,用 Object.defineProperty () 实现的缺点,有什么场景是不能用它实现的。那么其他场景如何实现。不用 Object.defineProperty () 如何实现?
Vue 双向数据绑定是通过 Object.defineProperty () 方法来实现的。它的基本原理是:利用 Object.defineProperty () 方法将数据对象的属性转换为访问器属性(getter 和 setter),当数据被读取时,会调用 getter 函数,当数据被修改时,会调用 setter 函数。在 setter 函数中,可以触发视图更新的逻辑,从而实现数据和视图的双向绑定。
使用 Object.defineProperty () 实现的缺点主要有:
- 深度监听问题:它只能监听对象的属性,如果对象的属性是一个嵌套的对象,那么需要递归地对嵌套对象的属性进行监听,这增加了实现的复杂性。
- 无法监听数组的变化:对于数组的一些方法,如 push、pop 等,它无法直接监听。虽然 Vue 通过重写数组的方法来实现了对数组变化的监听,但这并不是 Object.defineProperty () 本身的能力。
一些不能用它实现的场景包括:
- 动态添加属性:在 Vue 实例创建之后,如果动态地给数据对象添加新的属性,Object.defineProperty () 无法对新属性进行监听。
- 访问器属性的限制:对于一些本身就是访问器属性的对象,使用 Object.defineProperty () 可能会导致冲突。
对于动态添加属性的场景,可以使用 Vue 的\(set方法来解决。\)set 方法会手动触发对新属性的监听。
不用 Object.defineProperty () 也可以实现数据绑定,例如在 Vue3 中采用了 Proxy 来实现。Proxy 是 ES6 中新增的特性,它可以对对象进行代理,拦截对对象的各种操作,包括读取、写入、删除等。与 Object.defineProperty () 相比,Proxy 具有以下优势:
- 可以直接监听对象的变化,包括动态添加和删除属性,不需要像 Object.defineProperty () 那样进行额外的处理。
- 可以监听数组的变化,不需要重写数组的方法。
- 它是对整个对象进行代理,而不是对每个属性进行单独的监听,性能上可能更优。
vue2 和 vue3 区别
Vue2 和 Vue3 存在多方面的区别:
- 性能方面
- Vue3 使用 Proxy 代替了 Vue2 中的 Object.defineProperty () 来实现数据响应式,这使得 Vue3 能够更高效地监听数据变化,尤其是对于复杂数据结构的监听更加优化。
- Vue3 的虚拟 DOM 进行了重写,采用了更高效的 Diff 算法,能够更快地找出需要更新的节点,减少了不必要的 DOM 操作,提高了更新性能。
- Vue3 支持静态提升,将不参与更新的静态节点提升到渲染函数之外,避免了重复创建,进一步提升了性能。
- API 方面
- Vue3 引入了 Composition API,它提供了一种更灵活、更高效的方式来组织组件逻辑。相比之下,Vue2 主要使用 Options API,在处理复杂组件逻辑时可能会显得比较混乱。Composition API 允许开发者将相关的逻辑代码组合在一起,提高了代码的可维护性和复用性。
- Vue3 中的 setup 函数是组件的入口点,在 setup 函数中可以使用 Composition API 来定义组件的响应式数据、方法等。而 Vue2 中没有 setup 函数,组件的逻辑是通过各种选项(如 data、methods、computed 等)来定义的。
- 语法方面
- Vue3 对模板语法进行了一些优化,例如支持片段(Fragments),可以在模板中直接返回多个根节点,而不需要像 Vue2 那样必须有一个根节点包裹。
- Vue3 中的 v-model 指令更加灵活,支持在组件上使用多个 v-model,并且可以自定义 v-model 的修饰符。
- 其他方面
- Vue3 的打包体积更小,通过 tree-shaking 等优化手段,能够去除未使用的代码,使得最终的打包文件大小更小,加载速度更快。
- Vue3 对 TypeScript 的支持更好,提供了更完善的类型定义和类型推断,使得使用 TypeScript 开发 Vue 应用更加方便和高效。
v-for 中,为什么使用 key,在生命周期中起什么作用?
在 v-for 中使用 key 主要有以下几个原因:
- 提高渲染效率:当列表中的数据发生变化时,Vue 会通过 Diff 算法来更新 DOM。如果没有 key,Vue 会采用 “就地复用” 的策略,可能会导致一些不必要的 DOM 更新。而通过给每个列表项设置一个唯一的 key,Vue 可以更准确地识别哪些列表项发生了变化、新增或删除,从而只更新真正需要更新的 DOM 节点,提高渲染效率。
- 保持列表项的状态:在列表渲染中,每个列表项可能有自己的状态,比如输入框的内容、复选框的选中状态等。使用 key 可以让 Vue 在更新列表时,正确地保留这些状态。如果不使用 key,当列表项的顺序发生变化时,可能会导致状态混乱,因为 Vue 无法准确地识别哪个列表项对应哪个状态。
在生命周期中,key 的作用主要体现在更新阶段。当数据更新触发重新渲染时,Vue 会根据 key 来判断列表项的变化情况。如果 key 相同,且对应的组件实例也相同,那么 Vue 会尝试复用该组件实例,只更新其数据。如果 key 不同,或者没有找到对应的组件实例,那么 Vue 会创建新的组件实例来渲染新的列表项。同时,在卸载阶段,具有相同 key 的组件实例会被正确地卸载和销毁,避免内存泄漏等问题。通过 key 的正确使用,可以确保列表在整个生命周期中,无论是初始化渲染、更新还是卸载,都能以高效、正确的方式进行处理。
data 为什么是一个函数
在 Vue 组件中,data 之所以是一个函数,是为了保证每个组件实例都有自己独立的数据源。
如果 data 是一个对象,那么当多个组件实例共享同一个 data 对象时,其中一个组件对 data 的修改会影响到其他组件,因为它们指向的是同一个内存地址。这会导致数据的混乱和不可预测性,不符合组件化开发中每个组件应该具有独立性和封装性的原则。
而当 data 是一个函数时,每个组件实例在创建时都会调用这个函数,返回一个新的对象作为该组件实例的数据源。这样,每个组件实例都有自己独立的 data 对象,它们之间的数据相互隔离,不会相互影响。例如:
Vue.component('my-component', {data() {return {message: 'Hello'};},template: '<div>{{ message }}</div>'
});
在上面的代码中,当创建多个my - component
组件实例时,每个实例都会有自己独立的message
属性,修改一个实例的message
不会影响到其他实例。
这种方式使得组件在复用性和数据管理上更加灵活和可靠,符合 Vue 的组件化架构设计理念,能够更好地实现数据的封装和隔离,提高应用的可维护性和可扩展性。
v-if 和 v-show,v-show 设置组件不可见的时候,DOM 还可以查询到么,或则使用其他方法可以查询到这个不显示的节点
v-if 和 v-show 都是 Vue 中用于控制元素显示与隐藏的指令,但它们的实现原理和使用场景有所不同。
v-if 是一种条件渲染指令,当表达式的值为 false 时,对应的元素及其子元素会被完全从 DOM 中移除;当表达式的值为 true 时,才会将元素插入到 DOM 中。也就是说,v-if 控制的元素在 DOM 中存在与否取决于表达式的值。
v-show 则是通过修改元素的 CSS 的 display
属性来控制元素的显示与隐藏。当表达式的值为 false 时,元素的 display
属性会被设置为 none
,元素不可见;当表达式的值为 true 时,元素的 display
属性会恢复为原来的值(如 block
、inline
等),元素可见。
当使用 v-show 设置组件不可见时,DOM 是可以查询到的。因为 v-show 只是改变了元素的 display
属性为 none
,元素仍然存在于 DOM 树中。例如在 JavaScript 中,可以使用 document.querySelector
等方法来获取到这个不显示的节点。下面是一个简单的示例:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>v-show示例</title><script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body><div id="app"><div v-show="isShow">这是一个使用v-show控制显示隐藏的元素</div></div><script>new Vue({el: '#app',data: {isShow: false},mounted() {const element = document.querySelector('div');console.log(element); // 即使isShow为false,这里也能获取到该元素}});</script>
</body>
</html>
从上述示例可以看出,即便使用 v-show 使元素不可见,依然可以通过常规的 DOM 查询方法获取到该元素。
在实际应用中,v-if 适用于在运行时很少改变条件,不需要频繁切换显示状态的场景,因为它有较高的切换开销(创建和销毁 DOM 元素)。而 v-show 适用于需要频繁切换显示状态的场景,因为它只是简单地修改 CSS 属性,开销较小。
前端开发中,有哪些因素导致页面卡顿,前端性能优化方法有哪些?
在前端开发中,导致页面卡顿的因素有很多,以下是一些常见的情况:
- 大量的 DOM 操作:频繁地创建、删除、修改 DOM 元素,尤其是在循环中进行这些操作,会导致浏览器频繁地进行重排和重绘,消耗大量的性能。例如,在一个循环中每次都向页面中添加一个新的 DOM 元素,就可能导致页面卡顿。
- 复杂的样式计算:当页面中存在大量复杂的 CSS 样式,尤其是涉及到大量的层级嵌套、动画效果、滤镜等,浏览器在计算样式时会花费较多的时间,影响页面的流畅度。比如使用了大量的
box-shadow
、transform
等属性的组合。 - ** JavaScript 执行时间过长 **:如果在主线程中执行的 JavaScript 代码过于复杂、计算量过大,或者存在死循环等情况,会阻塞主线程,导致页面无法及时响应和更新,从而出现卡顿现象。
- 资源加载缓慢:页面中需要加载的图片、脚本、样式表等资源过多,或者网络环境不佳,导致资源加载时间过长,页面无法及时渲染,也会给用户造成卡顿的感觉。例如,加载一张非常大的图片时,可能会使页面长时间处于等待状态。
- 内存泄漏:在 JavaScript 中,如果没有正确地管理内存,导致对象无法被垃圾回收机制回收,随着时间的推移,内存占用会越来越高,最终可能导致页面卡顿甚至崩溃。
针对以上这些导致页面卡顿的因素,前端性能优化可以采取以下方法:
- 减少 DOM 操作:尽量合并 DOM 操作,比如可以先将需要添加的元素创建好,然后一次性添加到页面中;使用
DocumentFragment
来批量操作 DOM,减少重排和重绘的次数。 - 优化 CSS 样式:避免使用过于复杂的选择器和样式属性,尽量使用简洁的 CSS 代码;对于动画效果,可以考虑使用
requestAnimationFrame
来实现,以提高动画的流畅度。 - 优化 JavaScript 代码:将耗时较长的任务分解为多个小任务,使用
setTimeout
或requestIdleCallback
等方法在空闲时间执行;避免在主线程中执行大量的计算任务,可以考虑使用 Web Workers 将计算任务放到后台线程中执行。 - 优化资源加载:对图片进行压缩,减小文件大小;使用 CDN 来加速资源的加载;对资源进行懒加载,只在需要时才加载相关资源。
- 避免内存泄漏:及时释放不再使用的对象,比如在组件销毁时,清除定时器、事件监听器等;注意闭包的使用,避免因闭包导致的内存泄漏。
echarts 数据量太大,造成图标渲染卡顿,如何优化?
当 Echarts 处理的数据量太大导致图表渲染卡顿时,可以从以下几个方面进行优化:
- 数据采样与简化:
- 对于时间序列数据等,可以根据时间间隔进行采样。例如,如果数据点非常密集,每秒钟有多个数据点,但在图表上并不需要如此精细的展示,可以每隔一定时间取一个数据点,减少数据量的同时不影响整体趋势的呈现。
- 对于数值型数据,可以考虑进行数据聚合。比如将相近的数据进行分组,计算每组的平均值、最大值、最小值等,以较少的数据点来代表一组数据。
- 使用异步加载与分页:
- 将数据分成多个部分,采用异步加载的方式。当图表初始化时,先加载一部分数据进行渲染,在用户操作(如滚动、翻页)时再加载后续数据。这样可以避免一次性加载大量数据导致的卡顿。
- 实现分页功能,类似于数据表格的分页,只展示当前页的数据,用户可以通过分页按钮查看其他页的数据。在 Echarts 中,可以结合数据的索引和展示范围来实现这一功能。
- 优化图表配置:
- 减少不必要的图表元素和样式。例如,如果图表中有过多的装饰性线条、阴影、渐变色等,可以适当简化,以降低渲染的复杂度。
- 合理设置图表的动画效果。如果动画效果过于复杂或频繁,可能会导致卡顿。可以选择简单的动画或者关闭一些不必要的动画。
- 使用虚拟列表技术:对于列表形式的图表(如柱状图、折线图等),可以借鉴虚拟列表的思想。只渲染当前可见区域内的数据项,当用户滚动图表时,动态地更新可见区域内的数据项,而不是渲染所有的数据项。这样可以大大减少渲染的工作量,提高性能。
- 优化 Echarts 本身的设置:
- 调整 Echarts 的渲染策略。例如,在一些情况下,可以尝试将
renderer
设置为'canvas'
,因为在处理大量数据时,canvas
的渲染性能可能会优于'svg'
(但canvas
在交互性和可访问性方面可能不如'svg'
,需要根据具体需求权衡)。 - 缓存图表的计算结果。如果图表的某些计算是重复的,可以将计算结果缓存起来,避免每次渲染时都重新计算。
- 调整 Echarts 的渲染策略。例如,在一些情况下,可以尝试将
通过以上这些优化方法,可以在一定程度上缓解 Echarts 因数据量过大而导致的渲染卡顿问题,提升用户体验。
重绘重排以及解决方式
重排(也叫回流)和重绘是浏览器渲染过程中的两个重要概念。
重排是指当 DOM 的变化影响了元素的几何属性(如宽、高、位置、边距等),浏览器需要重新计算元素的几何信息,并重新构建渲染树,然后将渲染树绘制到屏幕上。例如,当我们改变元素的 width
、height
属性,或者添加、删除元素,都会导致重排。重排是一个比较昂贵的操作,因为它涉及到对整个渲染树的重新计算和构建。
重绘是指当元素的外观发生变化,但不影响其几何属性时,浏览器只需要更新元素的外观,而不需要重新计算元素的几何信息。例如,当我们改变元素的 color
、background-color
等样式属性时,就会触发重绘。重绘的开销比重排小,但如果频繁发生重绘,也会影响页面性能。
以下是一些解决重排和重绘问题,提升页面性能的方式:
- 批量修改 DOM 和样式:避免频繁地单独修改 DOM 元素和样式属性。可以先将要修改的元素的
display
属性设置为none
,此时页面会进行一次重排,然后批量修改元素的属性,最后再将display
属性改回原来的值,这样只需要进行两次重排,而不是每次修改都触发重排。例如:
const element = document.getElementById('myElement');
element.style.display = 'none';
element.style.width = '200px';
element.style.height = '200px';
element.style.backgroundColor ='red';
element.style.display = 'block';
- 使用
transform
代替top
、left
等属性来移动元素:当使用top
、left
等属性改变元素位置时,会触发重排;而使用transform
属性改变元素的位置时,只会触发重绘,因为它不会影响元素的几何属性。例如:
.element {position: relative;transform: translateX(100px); /* 移动元素 */
}
- 将动画效果应用到
position: fixed
或position: absolute
的元素上:这样的元素脱离了文档流,其动画不会影响其他元素,从而减少重排的发生。 - 避免在布局中使用
table
元素:table
元素的渲染比较复杂,当其中的内容发生变化时,可能会导致整个表格及其相关元素的重排。尽量使用div
等其他元素来实现布局。 - 使用
requestAnimationFrame
来执行动画:requestAnimationFrame
会在浏览器下次重绘之前执行回调函数,能够保证动画的流畅性,并且可以避免不必要的重绘和重排。例如:
function animate() {// 动画逻辑requestAnimationFrame(animate);
}
animate();
通过合理运用这些方法,可以有效地减少重排和重绘的发生,提高页面的性能和响应速度。
前端存的 Token 怎么通过 axios 拦截器发给后端?
在前端开发中,Token 通常用于身份验证,为了在每次请求后端接口时将 Token 发送给后端,可以利用 axios 的拦截器来实现这一功能。以下是具体的实现步骤:
- 存储 Token:首先,需要在前端将获取到的 Token 存储起来。常见的存储方式有
localStorage
、sessionStorage
或者 Vuex(如果使用 Vue 框架)等。以localStorage
为例,假设获取到的 Token 是一个字符串类型的token
值,可以这样存储:
localStorage.setItem('token', token);
- 设置 axios 请求拦截器:axios 提供了请求拦截器和响应拦截器,我们可以使用请求拦截器在每次请求发送之前,将 Token 添加到请求头中。以下是一个简单的示例:
import axios from 'axios';// 创建一个axios实例
const instance = axios.create({baseURL: 'https://your-api-domain.com/api', // 后端接口的基础URLtimeout: 5000 // 设置请求超时时间
});// 设置请求拦截器
instance.interceptors.request.use(config => {const token = localStorage.getItem('token');if (token) {// 将Token添加到请求头中,常见的键名是Authorizationconfig.headers.Authorization = `Bearer ${token}`;}return config;
}, error => {// 处理请求错误return Promise.reject(error);
});export default instance;
在上述代码中,首先创建了一个 axios 实例 instance
,然后通过 interceptors.request.use
方法设置了请求拦截器。在拦截器的回调函数中,从 localStorage
中获取 Token,如果存在 Token,则将其添加到请求头的 Authorization
字段中(这里使用了常见的 Bearer
认证方式)。最后,将修改后的配置对象 config
返回,这样请求就会带着 Token 发送到后端。
- 使用 axios 实例发送请求:在项目的其他地方,就可以使用这个配置好的 axios 实例来发送请求,Token 会自动被添加到请求头中。例如:
import instance from './axiosInstance'; // 假设上述代码保存在axiosInstance.js文件中// 发送GET请求
instance.get('/some-endpoint').then(response => {console.log(response.data);}).catch(error => {console.error(error);});
通过以上步骤,就可以在前端利用 axios 拦截器将存储的 Token 发送给后端,实现身份验证等功能。同时,在后端接收到请求后,也需要对请求头中的 Token 进行验证和处理。
ajax axios 二者区别
AJAX 是一种在网页上实现异步数据传输的技术,它不是一个具体的库或框架,而是一种概念。Axios 则是基于 Promise 用于浏览器和 node.js 的 HTTP 客户端,以下是它们的一些区别:
- 本质:AJAX 是一种技术统称,通过 XMLHttpRequest 对象来实现异步数据交互。Axios 是一个具体的库,对 XMLHttpRequest 进行了封装,提供了更简洁的 API。
- 使用方式:AJAX 使用原生的 XMLHttpRequest 对象时,代码相对繁琐,需要处理各种事件回调来处理请求的不同阶段。Axios 使用起来更加简洁直观,基于 Promise 实现,通过 then 和 catch 方法来处理成功和失败的情况,代码结构更清晰。
- 功能特性:Axios 具有一些 AJAX 原生实现不具备的特性,例如自动转换请求和响应数据、支持请求和响应拦截器、能轻松地处理并发请求等。Axios 还可以在服务器端使用,而原生 AJAX 主要用于浏览器环境。
- 兼容性:原生 AJAX 在一些旧浏览器上可能存在兼容性问题,需要编写额外的代码来处理。Axios 在兼容性方面做了较好的处理,能在大多数现代浏览器以及 node.js 环境中良好运行。
ajax 与 axios 中间还有一个类似异步操作的东西,是 promise 吗?如果是,讲一下 promise 相关知识
是的,Promise 是一种用于处理异步操作的技术,在 AJAX 和 Axios 中都有广泛应用。
Promise 是一个代表异步操作最终完成或失败的对象。它有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。一旦状态从 pending 转变为 fulfilled 或 rejected ,就不会再改变。
Promise 提供了一种更优雅的方式来处理异步操作的回调,避免了回调地狱的问题。通过 then 方法可以指定在 Promise 成功时执行的回调函数,通过 catch 方法可以捕获 Promise 被 rejected 时的错误。还可以使用 finally 方法指定无论 Promise 是成功还是失败都会执行的代码。
例如,使用 Promise 来处理一个简单的 AJAX 请求:
const promise = new Promise((resolve, reject) => {const xhr = new XMLHttpRequest();xhr.open('GET', 'https://example.com/api/data');xhr.onload = () => {if (xhr.status === 200) {resolve(xhr.responseText);} else {reject(new Error('请求失败'));}};xhr.send();
});promise.then(result => console.log(result)).catch(error => console.error(error));
Promise 还支持链式调用,即一个 Promise 的 then 方法返回的也是一个 Promise ,可以继续在后面调用 then 方法,这样可以方便地按顺序处理多个异步操作。
promise 结合 settimeout 实现 delay 函数
可以使用 Promise 结合 setTimeout 来实现一个 delay 函数,该函数可以延迟一定时间后再执行后续操作。以下是实现代码:
function delay(ms) {return new Promise(resolve => setTimeout(resolve, ms));
}async function main() {console.log('开始');await delay(2000);console.log('延迟2秒后执行');
}main();
在上述代码中,delay 函数接受一个参数 ms ,表示延迟的毫秒数。它返回一个 Promise ,在 Promise 的构造函数中使用 setTimeout ,当指定的时间到达后,调用 resolve 函数来 resolve 这个 Promise 。在 main 函数中,使用 async 和 await 来配合 Promise ,先输出 ' 开始 ' ,然后通过 await delay (2000) 暂停执行,等待 2 秒后再继续执行后面的代码,输出 ' 延迟 2 秒后执行 ' 。
Promise.all () 实现原理
Promise.all () 用于将多个 Promise 实例包装成一个新的 Promise 实例。它的实现原理大致如下:
- 接受一个 Promise 数组作为参数,返回一个新的 Promise 。
- 遍历传入的 Promise 数组,为每个 Promise 添加成功和失败的回调函数。
- 当所有的 Promise 都成功时,新的 Promise 才会成功,并且将所有 Promise 的结果组成一个数组作为新 Promise 的 resolved 值。
- 如果有任何一个 Promise 被 rejected ,新的 Promise 就会立即被 rejected ,并将第一个被 rejected 的 Promise 的 reason 作为新 Promise 的 rejected 原因。
以下是一个简单的模拟实现:
Promise.myAll = function(promises) {return new Promise((resolve, reject) => {const results = [];let count = 0;promises.forEach((promise, index) => {Promise.resolve(promise).then(result => {results[index] = result;count++;if (count === promises.length) {resolve(results);}}).catch(error => {reject(error);});});});
};
在这个实现中,创建了一个新的 Promise ,通过遍历传入的 Promise 数组,将每个 Promise 转换为已 resolved 的状态(如果本身不是 Promise 则直接 resolved ),然后在成功回调中将结果存入 results 数组,并记录完成的数量。当所有 Promise 都完成时,调用 resolve 并传入 results 数组。如果有 Promise 失败,则立即调用 reject 并传入错误信息。
从页面输入 url 之后到显示页面 发生了什么,包括网络通信和页面渲染相关过程
当在浏览器中输入 URL 后到页面显示,主要经历以下过程:
- DNS 解析:浏览器首先需要将输入的域名解析为对应的 IP 地址。它会先检查浏览器缓存中是否有该域名的 IP 地址记录,如果没有则会向本地 DNS 服务器发送请求,本地 DNS 服务器再递归查询或迭代查询其他 DNS 服务器,直到找到对应的 IP 地址并返回给浏览器。
- 建立连接:浏览器与目标服务器通过 TCP 协议建立连接,经历三次握手过程,确保双方能可靠地传输数据。
- 发送请求:连接建立后,浏览器根据输入的 URL 构建 HTTP 请求报文,包括请求方法(如 GET 、POST 等)、请求头(包含浏览器信息、缓存信息等)和请求体(如果是 POST 请求等可能包含数据),然后将请求发送给服务器。
- 服务器处理请求:服务器接收到请求后,根据请求的 URL 和方法等进行处理,可能涉及到查询数据库、执行服务器端脚本等操作,生成响应数据。
- 发送响应:服务器将生成的响应数据构建成 HTTP 响应报文,包括响应状态码(如 200 表示成功、404 表示未找到等)、响应头(包含数据类型、缓存信息等)和响应体(实际的页面数据、资源等),并发送给浏览器。
- 浏览器接收响应:浏览器接收响应数据,根据响应状态码判断请求是否成功。如果是 200 ,则开始解析和渲染页面。
- 页面渲染:浏览器首先解析 HTML 代码构建 DOM 树,然后解析 CSS 样式构建 CSSOM 树,将两者结合形成渲染树。渲染树确定了页面上每个元素的样式和位置等信息。接着浏览器根据渲染树进行布局计算,确定每个元素在页面上的具体位置和大小。最后进行绘制操作,将渲染树中的元素绘制到屏幕上,形成可见的页面。在渲染过程中,如果遇到 JavaScript 脚本,会暂停渲染来执行脚本,因为脚本可能会修改 DOM 等。同时,浏览器还会根据需要进行资源的加载,如图片、脚本、样式表等,这些资源的加载可能会阻塞页面的渲染,也可能会并行进行,取决于资源的类型和加载方式。
TCP 拥塞控制相关内容
TCP 拥塞控制是 TCP 协议中至关重要的一部分,其核心目的在于防止网络出现拥塞,确保网络的高效与稳定运行。当网络中的流量超出其承载能力时,就会引发拥塞,导致数据包丢失、延迟增加等问题,而 TCP 拥塞控制机制能够有效应对这些情况。
TCP 拥塞控制主要通过以下几种算法实现:
慢启动:在建立 TCP 连接之初,发送方会以较小的拥塞窗口(cwnd)开始发送数据,通常初始拥塞窗口大小为 1 个 MSS(最大段大小)。每收到一个确认(ACK),拥塞窗口就会增加 1 个 MSS。这样,随着时间的推移,发送方发送的数据量会呈指数级增长。不过,为了避免拥塞窗口增长过快,还设置了慢启动阈值(ssthresh)。当拥塞窗口达到该阈值时,就会进入拥塞避免阶段。
拥塞避免:在这个阶段,拥塞窗口不再以指数级增长,而是线性增长。每收到一个 ACK,拥塞窗口只增加 1/cwnd 个 MSS。这种方式能让发送方更谨慎地增加发送数据的速率,降低拥塞发生的可能性。
快速重传:当发送方连续收到 3 个重复的 ACK 时,就意味着有数据包可能丢失,但网络尚未完全拥塞。此时,发送方会立即重传丢失的数据包,而无需等待超时重传,这样可以减少不必要的等待时间,提高传输效率。
快速恢复:在快速重传之后,进入快速恢复阶段。发送方将慢启动阈值(ssthresh)设置为当前拥塞窗口的一半,然后将拥塞窗口设置为 ssthresh 加上 3 倍的 MSS(因为收到了 3 个重复的 ACK)。接着,每收到一个重复的 ACK,拥塞窗口就增加 1 个 MSS。当收到新的 ACK 时,将拥塞窗口设置为 ssthresh,然后进入拥塞避免阶段。
这些算法相互配合,使得 TCP 能够根据网络的实际状况动态调整发送数据的速率,从而有效地避免网络拥塞,保障数据的可靠传输。
跨域以及解决方式
跨域指的是浏览器从一个域名的网页去请求另一个域名的资源时,由于浏览器的同源策略,会受到限制。同源策略要求协议、域名和端口都相同,只有满足这一条件,浏览器才允许页面之间进行资源共享和交互。
以下是一些常见的跨域解决方式:
JSONP(JSON with Padding):这是一种比较古老的跨域解决方案。其原理是利用 <script>
标签的 src 属性不受同源策略限制的特点。客户端在请求时,会在 URL 中添加一个回调函数名作为参数,服务器收到请求后,会将数据包装在这个回调函数中返回给客户端。客户端的 <script>
标签会执行这个回调函数,从而获取到服务器返回的数据。不过,JSONP 只支持 GET 请求,且安全性较低,容易受到 XSS 攻击。
CORS(Cross-Origin Resource Sharing):这是一种现代的跨域解决方案,由 W3C 提出。它通过在服务器端设置响应头来允许跨域请求。服务器可以在响应头中添加 Access-Control-Allow-Origin
字段,指定允许访问的域名。此外,还可以设置其他相关的响应头,如 Access-Control-Allow-Methods
、Access-Control-Allow-Headers
等,来控制允许的请求方法和请求头。CORS 支持所有类型的 HTTP 请求,是目前比较推荐的跨域解决方案。
代理服务器:在同源的服务器上设置一个代理,客户端将请求发送到同源的代理服务器,代理服务器再将请求转发到目标服务器,并将目标服务器的响应返回给客户端。这样,客户端与代理服务器之间是同源的,不会受到跨域限制。这种方式适用于各种场景,但需要额外搭建和维护代理服务器。
Nginx 反向代理:Nginx 是一款高性能的 Web 服务器和反向代理服务器。可以通过配置 Nginx 来实现跨域请求的转发。在 Nginx 的配置文件中,设置反向代理规则,将客户端的请求转发到目标服务器,并在响应中添加必要的响应头,以允许跨域访问。这种方式配置相对简单,性能也比较高。
js 数组方法知道的有哪些?map 和 forEach 的区别
JavaScript 数组提供了丰富的方法,以下是一些常见的数组方法:
push()
:用于在数组的末尾添加一个或多个元素,并返回新的数组长度。
pop()
:用于移除数组的最后一个元素,并返回该元素。
shift()
:用于移除数组的第一个元素,并返回该元素。
unshift()
:用于在数组的开头添加一个或多个元素,并返回新的数组长度。
splice()
:可以用于删除、插入或替换数组中的元素。它接受多个参数,第一个参数是开始位置,第二个参数是要删除的元素个数,后面的参数是要插入的元素。
slice()
:用于截取数组的一部分,并返回一个新的数组。它接受两个参数,分别是开始位置和结束位置(不包含结束位置的元素)。
concat()
:用于合并两个或多个数组,并返回一个新的数组。
join()
:用于将数组中的所有元素转换为一个字符串,元素之间可以用指定的分隔符分隔。
reverse()
:用于反转数组中元素的顺序。
sort()
:用于对数组中的元素进行排序。默认情况下,它会将元素转换为字符串并按字典序排序。也可以传入一个比较函数来指定排序规则。
map()
和 forEach()
是两个常用的迭代方法,它们的区别如下:
返回值:map()
方法会返回一个新的数组,新数组中的元素是原数组中每个元素经过某种处理后的结果。而 forEach()
方法没有返回值,它只是对数组中的每个元素执行一次提供的函数。
使用场景:map()
常用于需要对数组中的每个元素进行转换并得到一个新数组的场景。例如,将数组中的每个元素乘以 2 :
const numbers = [1, 2, 3];
const newNumbers = numbers.map(num => num * 2);
console.log(newNumbers); // [2, 4, 6]
forEach()
则更适合用于对数组中的元素进行一些副作用操作,如打印元素、修改全局变量等。例如:
const numbers = [1, 2, 3];
numbers.forEach(num => console.log(num)); // 依次打印 1, 2, 3
讲一下对象继承和原型链
在 JavaScript 中,对象继承是一种让一个对象可以使用另一个对象的属性和方法的机制,而原型链是实现对象继承的一种重要方式。
每个 JavaScript 对象都有一个内部属性 [[Prototype]]
,也称为原型。当访问一个对象的属性或方法时,JavaScript 引擎首先会在该对象本身查找,如果找不到,就会沿着该对象的原型链向上查找,直到找到该属性或方法,或者到达原型链的末尾(即 Object.prototype
)。
以下是一个简单的示例来说明原型链的工作原理:
// 创建一个对象 person
const person = {name: 'John',sayHello: function() {console.log(`Hello, my name is ${this.name}`);}
};// 创建一个对象 student,并将其原型设置为 person
const student = Object.create(person);
student.studentId = 123;// 访问 student 对象的属性和方法
console.log(student.name); // John
student.sayHello(); // Hello, my name is John
在这个示例中,student
对象的原型被设置为 person
对象。当访问 student.name
时,由于 student
对象本身没有 name
属性,JavaScript 引擎会沿着原型链找到 person
对象的 name
属性。同样,当调用 student.sayHello()
时,也会在 person
对象上找到该方法并执行。
除了使用 Object.create()
方法来设置原型外,还可以通过构造函数和 prototype
属性来实现原型链继承。例如:
// 定义一个构造函数 Person
function Person(name) {this.name = name;
}// 为 Person 的原型添加方法
Person.prototype.sayHello = function() {console.log(`Hello, my name is ${this.name}`);
};// 定义一个构造函数 Student,并继承自 Person
function Student(name, studentId) {Person.call(this, name);this.studentId = studentId;
}// 设置 Student 的原型为 Person 的实例
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;// 创建一个 Student 对象
const student = new Student('Jane', 456);
student.sayHello(); // Hello, my name is Jane
在这个示例中,Student
构造函数通过 call()
方法调用 Person
构造函数,继承了 Person
的属性。同时,通过 Object.create()
方法将 Student
的原型设置为 Person
的实例,从而继承了 Person
的方法。
js 作用域(闭包)相关知识
JavaScript 中的作用域定义了变量和函数的可访问范围,它控制着变量和函数的可见性和生命周期。主要有全局作用域、函数作用域和块级作用域(ES6 引入)。
全局作用域:在代码的最外层定义的变量和函数具有全局作用域,它们可以在代码的任何地方被访问。例如:
var globalVariable = 'I am global';
function globalFunction() {console.log('This is a global function');
}
函数作用域:在函数内部定义的变量和函数只能在该函数内部访问,外部无法直接访问。例如:
function myFunction() {var localVariable = 'I am local';function localFunction() {console.log('This is a local function');}console.log(localVariable); // 可以访问localFunction(); // 可以调用
}
// console.log(localVariable); // 报错,无法访问
// localFunction(); // 报错,无法调用
块级作用域:ES6 引入了 let
和 const
关键字,它们可以创建块级作用域。块级作用域是指用 {}
包裹的代码块,在块级作用域中定义的变量和常量只能在该块内访问。例如:
if (true) {let blockVariable = 'I am in block scope';console.log(blockVariable); // 可以访问
}
// console.log(blockVariable); // 报错,无法访问
闭包是 JavaScript 中一个非常重要的概念,它是指有权访问另一个函数作用域中变量的函数。即使该函数已经执行完毕,其作用域内的变量也不会被销毁,而是会被闭包所引用。例如:
function outerFunction() {var outerVariable = 'I am from outer function';function innerFunction() {console.log(outerVariable);}return innerFunction;
}var closure = outerFunction();
closure(); // 输出: I am from outer function
在这个示例中,innerFunction
就是一个闭包,它可以访问 outerFunction
作用域内的 outerVariable
变量。当 outerFunction
执行完毕后,其作用域内的 outerVariable
变量不会被销毁,因为 innerFunction
仍然引用着它。
闭包的应用场景非常广泛,例如实现私有变量和方法、函数柯里化、事件处理等。但需要注意的是,闭包会占用更多的内存,因为它会保留对外部函数作用域的引用,所以在使用闭包时要注意避免内存泄漏。
let 和 const 暂时性死区相关内容
在 ES6 引入 let
和 const
之前,JavaScript 只有函数作用域和全局作用域,使用 var
声明的变量会存在变量提升的现象。而 let
和 const
声明的变量具有块级作用域,并且会产生暂时性死区(Temporal Dead Zone,TDZ)。
暂时性死区是指在代码块内,使用 let
或 const
声明变量之前,该变量处于不可用状态。从代码块开始到变量声明语句之间的区域就是暂时性死区。在这个区域内访问变量会抛出 ReferenceError
错误。
例如:
{// 这里是暂时性死区,访问tmp会报错console.log(tmp); let tmp = 1;
}
在上述代码中,在 let tmp = 1
语句之前访问 tmp
变量,由于处于暂时性死区,会抛出 ReferenceError
错误。
const
同样存在暂时性死区,并且 const
声明的常量一旦声明就必须赋值,之后不能再重新赋值。例如:
{// 这里是暂时性死区,访问PI会报错console.log(PI); const PI = 3.14;
}
暂时性死区的存在有助于开发者养成良好的编程习惯,避免变量在未声明时就被使用,从而减少潜在的错误。它使得变量的声明和使用更加清晰,提高了代码的可读性和可维护性。同时,这也是 let
和 const
与 var
的重要区别之一,进一步完善了 JavaScript 的变量作用域和声明机制。
es6 的新特性有哪些?
ES6(ECMAScript 2015)为 JavaScript 带来了许多新特性,极大地提升了语言的表达能力和开发效率。
块级作用域
引入 let
和 const
关键字,用于声明块级作用域的变量和常量。与 var
不同,let
和 const
声明的变量不会发生变量提升,并且具有块级作用域,避免了在循环等场景中出现的变量泄漏问题。
箭头函数
提供了一种更简洁的函数定义方式,语法为 (参数) => { 函数体 }
。箭头函数没有自己的 this
、arguments
、super
或 new.target
,它的 this
值继承自外层函数。例如:
const numbers = [1, 2, 3];
const squared = numbers.map(num => num * num);
模板字符串
使用反引号()来定义字符串,可以嵌入表达式,使用
${}` 语法。模板字符串还支持多行字符串,增强了字符串的拼接和处理能力。例如:
const name = 'John';
const message = `Hello, ${name}!`;
解构赋值
允许从数组或对象中提取值并赋值给变量。可以简化变量的赋值操作,提高代码的可读性。例如:
const [a, b] = [1, 2];
const { x, y } = { x: 10, y: 20 };
默认参数
函数参数可以设置默认值,当调用函数时没有传递该参数,会使用默认值。例如:
function greet(name = 'Guest') {console.log(`Hello, ${name}!`);
}
扩展运算符
使用 ...
语法,可以将数组或对象展开。在函数调用、数组拼接、对象合并等场景中非常有用。例如:
const arr1 = [1, 2];
const arr2 = [3, 4];
const combined = [...arr1, ...arr2];
类和继承
引入了 class
关键字,用于定义类,以及 extends
关键字实现类的继承。这使得 JavaScript 具有了更清晰的面向对象编程语法。例如:
class Animal {constructor(name) {this.name = name;}speak() {console.log(`${this.name} makes a noise.`);}
}class Dog extends Animal {speak() {console.log(`${this.name} barks.`);}
}
Promise 对象
用于处理异步操作,避免了回调地狱的问题。Promise 有三种状态:pending
(进行中)、fulfilled
(已成功)和 rejected
(已失败)。可以通过 then
方法处理成功结果,通过 catch
方法处理失败结果。例如:
const promise = new Promise((resolve, reject) => {setTimeout(() => {resolve('Success');}, 1000);
});
promise.then(result => console.log(result)).catch(error => console.error(error));
模块化
ES6 引入了 import
和 export
关键字,用于实现模块的导入和导出,使 JavaScript 代码的组织和管理更加方便。例如:
// 导出模块
export const add = (a, b) => a + b;// 导入模块
import { add } from './math.js';
其他特性
还包括 Symbol
类型、Proxy
和 Reflect
对象、async/await
异步编程模式等,这些特性进一步丰富了 JavaScript 的功能。
symbol 的应用场景和作用分别是什么?
Symbol
是 ES6 引入的一种新的原始数据类型,它表示独一无二的值。以下是 Symbol
的应用场景和作用:
作为对象属性名
由于 Symbol
值是独一无二的,使用 Symbol
作为对象的属性名可以避免属性名冲突。例如,在一个对象中可能有多个模块添加属性,使用 Symbol
可以确保每个模块添加的属性名不会相互覆盖。
const sym1 = Symbol('prop1');
const sym2 = Symbol('prop2');
const obj = {[sym1]: 'value1',[sym2]: 'value2'
};
这样,即使不同的开发者使用相同的描述创建 Symbol
,它们也是不同的值,不会导致属性名冲突。
模拟私有属性和方法
在 JavaScript 中,没有真正的私有属性和方法。但可以使用 Symbol
来模拟私有性。由于 Symbol
不能通过常规的方式被访问,外部代码很难直接获取到使用 Symbol
作为属性名的属性。
const privateProp = Symbol('private');
class MyClass {constructor() {this[privateProp] = 'This is private';}getPrivate() {return this[privateProp];}
}
const instance = new MyClass();
// 外部无法直接访问 privateProp
console.log(instance[privateProp]); // undefined
console.log(instance.getPrivate()); // This is private
定义常量
使用 Symbol
定义常量可以避免常量值被意外修改。因为 Symbol
是不可变的,且每个 Symbol
都是独一无二的。
const COLOR_RED = Symbol('red');
const COLOR_GREEN = Symbol('green');
function getColorName(color) {switch (color) {case COLOR_RED:return 'Red';case COLOR_GREEN:return 'Green';default:return 'Unknown';}
}
实现迭代器和生成器
Symbol.iterator
是一个内置的 Symbol
,用于定义对象的迭代器方法。通过实现 Symbol.iterator
方法,对象可以被 for...of
循环遍历。
const myIterable = {[Symbol.iterator]() {let index = 0;return {next() {if (index < 3) {return { value: index++, done: false };}return { done: true };}};}
};
for (const value of myIterable) {console.log(value);
}
扩展内置对象的功能
可以使用 Symbol
来扩展内置对象的功能,而不会影响原有的属性和方法。例如,为 Array
对象添加一个自定义的方法:
const customMethod = Symbol('customMethod');
Array.prototype[customMethod] = function() {return this.map(item => item * 2);
};
const arr = [1, 2, 3];
console.log(arr[customMethod]());
判断数组的多种方式有哪些?
在 JavaScript 中,有多种方式可以判断一个值是否为数组。
使用 Array.isArray()
方法
这是 ES5 引入的方法,用于判断一个值是否为数组。它是最直接和推荐的方式,具有较好的兼容性。例如:
const arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true
使用 instanceof
运算符
instanceof
运算符用于判断一个对象是否是某个构造函数的实例。对于数组来说,可以使用 instanceof Array
来判断。例如:
const arr = [1, 2, 3];
console.log(arr instanceof Array); // true
但需要注意的是,在多个 iframe 环境中,由于每个 iframe 都有自己的全局对象和构造函数,使用 instanceof
可能会出现问题。
使用 Object.prototype.toString.call()
方法
通过调用 Object.prototype.toString.call()
方法可以返回一个表示对象类型的字符串。对于数组,返回的字符串是 [object Array]
。例如:
const arr = [1, 2, 3];
console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true
这种方法可以准确判断各种类型的对象,不受 iframe 环境的影响。
使用 constructor
属性
每个对象都有一个 constructor
属性,它指向创建该对象的构造函数。可以通过检查 constructor
属性是否为 Array
来判断一个值是否为数组。例如:
const arr = [1, 2, 3];
console.log(arr.constructor === Array); // true
但这种方法也有局限性,因为 constructor
属性可以被修改,可能会导致判断结果不准确。
使用 Array.prototype.isPrototypeOf()
方法
Array.prototype.isPrototypeOf()
方法用于判断一个对象是否存在于另一个对象的原型链中。可以使用该方法判断一个值是否是数组。例如:
const arr = [1, 2, 3];
console.log(Array.prototype.isPrototypeOf(arr)); // true
不过,这种方法在某些复杂的原型链情况下可能会有意外的结果。
综上所述,Array.isArray()
是最简单、最可靠的判断数组的方式,在大多数情况下都能满足需求。而 Object.prototype.toString.call()
方法则适用于更复杂的场景,尤其是需要准确判断对象类型的情况。
数组扁平化方法,除了用递归还有什么方法?
数组扁平化是指将一个多维数组转换为一维数组的过程。除了递归方法,还可以使用以下几种方法实现数组扁平化。
使用 flat()
方法
flat()
是 ES2019 引入的数组方法,用于扁平化数组。它接受一个可选的参数,表示要扁平化的深度,默认值为 1。例如:
const arr = [1, [2, [3, 4]]];
const flattened = arr.flat(2);
console.log(flattened); // [1, 2, 3, 4]
如果要扁平化任意深度的数组,可以传入 Infinity
作为参数:
const arr = [1, [2, [3, [4]]]];
const flattened = arr.flat(Infinity);
console.log(flattened); // [1, 2, 3, 4]
使用 reduce()
和 concat()
方法
reduce()
方法可以对数组中的每个元素执行一个回调函数,并将结果累积为一个单一的值。结合 concat()
方法可以实现数组扁平化。例如:
const arr = [1, [2, [3, 4]]];
const flattened = arr.reduce((acc, val) => acc.concat(Array.isArray(val)? val.flat() : val), []);
console.log(flattened); // [1, 2, 3, 4]
这里的 reduce()
方法会遍历数组中的每个元素,如果元素是数组,则使用 flat()
方法将其扁平化后再与累积结果合并;如果元素不是数组,则直接与累积结果合并。
使用 toString()
和 split()
方法
这种方法适用于数组元素都是基本数据类型的情况。首先使用 toString()
方法将数组转换为字符串,然后使用 split()
方法将字符串分割为数组。例如:
const arr = [1, [2, [3, 4]]];
const flattened = arr.toString().split(',').map(Number);
console.log(flattened); // [1, 2, 3, 4]
需要注意的是,这种方法会将数组元素转换为字符串,然后再转换为数字,可能会导致数据类型的丢失,并且不适用于包含对象或其他复杂类型元素的数组。
使用 Generator
函数
Generator
函数是 ES6 引入的一种特殊函数,可以暂停和恢复执行。可以使用 Generator
函数来实现数组扁平化。例如:
function* flatten(arr) {for (const item of arr) {if (Array.isArray(item)) {yield* flatten(item);} else {yield item;}}
}
const arr = [1, [2, [3, 4]]];
const flattened = [...flatten(arr)];
console.log(flattened); // [1, 2, 3, 4]
这里的 flatten
函数是一个 Generator
函数,它会递归地遍历数组中的每个元素,如果元素是数组,则继续调用 flatten
函数;如果元素不是数组,则使用 yield
关键字返回该元素。最后使用扩展运算符 ...
将 Generator
对象转换为数组。
这些方法各有优缺点,可以根据具体的需求和场景选择合适的方法来实现数组扁平化。
js 基本数据类型和引用数据类型分别有哪些?
在 JavaScript 里,数据类型可分为基本数据类型和引用数据类型。
基本数据类型属于简单的数据值,在内存里直接存储于栈内存中。主要包含以下几种:
- Number:用于表示数字,既可以是整数,也能是浮点数。像
1
、3.14
等都是Number
类型。在 JavaScript 中,Number
类型采用 IEEE 754 双精度 64 位浮点数格式存储。 - String:用于表示文本,由零个或多个 Unicode 字符组成。可以用单引号、双引号或者反引号来定义,例如
'hello'
、"world"
、JavaScript。 - Boolean:只有两个值,即
true
和false
,常用于条件判断。 - Null:只有一个值
null
,表示一个空对象指针。通常用于显式地表示某个变量不指向任何对象。 - Undefined:当变量已声明但未赋值,或者函数没有返回值时,变量的值就是
undefined
。 - Symbol:这是 ES6 引入的新类型,代表独一无二的值。可以用
Symbol()
函数创建,例如const sym = Symbol('description')
。 - BigInt:ES2020 引入,用于表示任意大的整数。通过在整数后面加
n
来创建,如123n
。
引用数据类型则是对象,在内存中存储于堆内存,变量存储的是对象在堆内存中的引用地址。常见的引用数据类型有:
- Object:是最基础的引用数据类型,可用于存储键值对。可以用对象字面量
{}
或者new Object()
来创建。 - Array:用于存储有序的数据集合,元素可以是不同类型。可以用数组字面量
[]
或者new Array()
创建。 - Function:用于封装可重复使用的代码块,能接收参数并返回值。可以用函数声明、函数表达式或者箭头函数来定义。
- Date:用于处理日期和时间,通过
new Date()
创建。 - RegExp:用于处理正则表达式,可进行字符串匹配和替换等操作,通过
new RegExp()
或者正则表达式字面量/pattern/flags
创建。
js 判断一个数据类型的方法,以及他们分别能判断出哪些类型(回答 typeof instanceof 等相关方法)
在 JavaScript 中,有多种方法可以判断数据类型,以下是常见的几种:
- typeof 运算符:这是最常用的方法之一,它返回一个表示数据类型的字符串。
typeof
能判断的类型有:number
:如typeof 1
返回'number'
。string
:如typeof 'hello'
返回'string'
。boolean
:如typeof true
返回'boolean'
。object
:不过对于null
,typeof null
返回'object'
,这是 JavaScript 的一个历史遗留问题。对于数组、对象等引用类型,也返回'object'
,所以不能精确区分它们。function
:如typeof function() {}
返回'function'
。undefined
:如typeof undefined
返回'undefined'
。symbol
:ES6 引入的Symbol
类型,typeof Symbol()
返回'symbol'
。bigint
:ES2020 引入的BigInt
类型,typeof 123n
返回'bigint'
。
- instanceof 运算符:用于判断一个对象是否是某个构造函数的实例。它检查对象的原型链上是否有该构造函数的
prototype
属性。例如:
const arr = [];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
instanceof
可以判断对象是否为特定引用类型的实例,但对于基本数据类型无效。
- Object.prototype.toString.call () 方法:这是一种较为准确的判断数据类型的方法。它返回一个格式为
[object Type]
的字符串,其中Type
表示具体的数据类型。例如:
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
这种方法可以区分出 null
、undefined
、数组、对象、函数等多种类型。
- Array.isArray () 方法:专门用于判断一个值是否为数组。例如:
const arr = [];
console.log(Array.isArray(arr)); // true
- 使用 constructor 属性:每个对象都有一个
constructor
属性,指向创建该对象的构造函数。可以通过检查constructor
属性来判断对象的类型。例如:
const num = 1;
console.log(num.constructor === Number); // true
不过,constructor
属性可以被修改,所以这种方法不太可靠。
那如何进一步判断出 object 和 function?
在 JavaScript 里,虽然 typeof
能判断出 function
类型,但对于 object
类型,它不能准确区分普通对象、数组、null
等。以下是进一步判断 object
和 function
的方法:
判断 function
- 使用
typeof
运算符:这是最直接的方法。function
类型在使用typeof
时会返回'function'
。例如:
function myFunction() {}
console.log(typeof myFunction === 'function'); // true
- 使用
instanceof
运算符:Function
是所有函数的构造函数,所以可以用instanceof
来判断一个值是否为函数。例如:
const func = function() {};
console.log(func instanceof Function); // true
- 使用
Object.prototype.toString.call()
方法:该方法会返回[object Function]
来表示函数类型。例如:
const myFunc = () => {};
console.log(Object.prototype.toString.call(myFunc) === '[object Function]'); // true
判断 object
- 排除
null
和function
后使用typeof
:因为typeof null
返回'object'
,所以要先排除null
。同时,函数也会被typeof
判断为'object'
,所以也要排除。例如:
const obj = {};
if (typeof obj === 'object' && obj!== null && typeof obj!== 'function') {console.log('这是一个普通对象');
}
- 使用
Object.prototype.toString.call()
方法:对于普通对象,该方法会返回[object Object]
。例如:
const myObj = { key: 'value' };
console.log(Object.prototype.toString.call(myObj) === '[object Object]'); // true
- 使用
Object.getPrototypeOf()
方法:普通对象的原型是Object.prototype
。可以通过检查对象的原型是否为Object.prototype
来判断是否为普通对象。例如:
const obj = {};
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
js 如何实现一个类?
在 ES6 之前,JavaScript 没有类的概念,而是通过构造函数和原型链来模拟类的行为。ES6 引入了 class
关键字,让类的实现更加直观和简洁。
使用构造函数和原型链实现类
// 定义构造函数
function Person(name, age) {this.name = name;this.age = age;
}// 在原型上添加方法
Person.prototype.sayHello = function() {console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
};// 创建实例
const person1 = new Person('John', 25);
person1.sayHello(); // Hello, my name is John and I'm 25 years old.
在这个例子中,Person
是构造函数,相当于类的构造器。通过 new
关键字创建 Person
的实例,实例会继承构造函数的属性。而方法则添加到原型上,所有实例都可以共享这些方法。
使用 ES6 的 class
关键字实现类
// 定义类
class Person {constructor(name, age) {this.name = name;this.age = age;}sayHello() {console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);}
}// 创建实例
const person2 = new Person('Jane', 30);
person2.sayHello(); // Hello, my name is Jane and I'm 30 years old.
ES6 的 class
语法更加简洁和清晰。constructor
方法是类的构造函数,用于初始化实例的属性。类的方法直接定义在类的内部,不需要使用 prototype
。
类的继承
在 ES6 中,可以使用 extends
关键字实现类的继承。
class Student extends Person {constructor(name, age, grade) {super(name, age);this.grade = grade;}study() {console.log(`${this.name} is studying in grade ${this.grade}.`);}
}const student = new Student('Tom', 18, '12th');
student.sayHello(); // Hello, my name is Tom and I'm 18 years old.
student.study(); // Tom is studying in grade 12th.
在这个例子中,Student
类继承自 Person
类。super
关键字用于调用父类的构造函数。子类可以继承父类的属性和方法,还可以添加自己的属性和方法。
深拷贝和浅拷贝的概念,浅拷贝的常用方法(比如 slice,concat 等,对比 = 和 object.assign () 的区别)
深拷贝和浅拷贝的概念
在 JavaScript 中,浅拷贝和深拷贝是处理对象和数组等引用数据类型时的重要概念。
浅拷贝会创建一个新对象或数组,新对象或数组的顶层属性是原对象或数组的副本,但对于引用类型的属性,新对象和原对象会共享同一个引用,也就是说,修改其中一个对象的引用类型属性,会影响到另一个对象。
深拷贝则会递归地复制对象或数组的所有属性,包括嵌套的对象和数组,创建一个完全独立的副本。修改深拷贝后的对象不会影响原对象,反之亦然。
浅拷贝的常用方法
- 使用
slice()
方法(针对数组):slice()
方法返回一个新的数组对象,包含从原数组中指定开始到结束位置的元素。例如:
const originalArray = [1, [2, 3]];
const shallowCopy = originalArray.slice();
shallowCopy[0] = 10; // 不会影响原数组
shallowCopy[1][0] = 20; // 会影响原数组
console.log(originalArray); // [1, [20, 3]]
- 使用
concat()
方法(针对数组):concat()
方法用于合并两个或多个数组,返回一个新的数组。例如:
const arr1 = [1, [2, 3]];
const arr2 = [4];
const shallowCopy = arr1.concat(arr2);
shallowCopy[1][1] = 30; // 会影响原数组
console.log(arr1); // [1, [2, 30]]
- 使用
Object.assign()
方法(针对对象):Object.assign()
方法用于将一个或多个源对象的所有可枚举属性复制到目标对象,返回目标对象。例如:
const originalObject = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, originalObject);
shallowCopy.a = 10; // 不会影响原对象
shallowCopy.b.c = 20; // 会影响原对象
console.log(originalObject); // { a: 1, b: { c: 20 } }
=
和 Object.assign()
的区别
=
:使用=
进行赋值操作只是将一个变量的引用指向另一个变量,它们指向同一个对象或数组。修改其中一个变量会直接影响另一个变量。例如:
const obj1 = { x: 1 };
const obj2 = obj1;
obj2.x = 2;
console.log(obj1.x); // 2
Object.assign()
:Object.assign()
会创建一个新的对象,将原对象的属性复制到新对象中。对于顶层属性,是复制操作,但对于引用类型的属性,只是复制引用。例如上面的Object.assign()
示例,修改新对象的引用类型属性会影响原对象。
综上所述,=
只是引用赋值,而 Object.assign()
是浅拷贝,它们在处理对象和数组时的行为有所不同。
数组去重,数组里面有重复的函数对象如何处理?
在 JavaScript 中对数组进行去重是常见需求,当数组中包含重复的函数对象时,由于函数对象在内存中是引用类型,不能简单地通过值比较来去重。下面介绍几种可行的方法。
利用 Map 对象
Map
对象可以存储键值对,并且键是唯一的。可以将函数对象作为键存储在 Map
中,从而实现去重。
function removeDuplicateFunctions(arr) {const map = new Map();for (const func of arr) {map.set(func, true);}return Array.from(map.keys());
}const functions = [function() {},function() {},function() {}
];
const uniqueFunctions = removeDuplicateFunctions(functions);
console.log(uniqueFunctions);
这里通过 Map
的特性,每个函数对象只会被存储一次,最后将 Map
的键转换为数组返回。
利用 WeakMap 对象
WeakMap
对象的键必须是对象,并且对键是弱引用。如果数组中的函数对象没有其他地方引用,当被 WeakMap
存储后,仍然可以被垃圾回收。
function removeDuplicateFunctionsWithWeakMap(arr) {const weakMap = new WeakMap();const result = [];for (const func of arr) {if (!weakMap.has(func)) {weakMap.set(func, true);result.push(func);}}return result;
}const functions2 = [function() {},function() {},function() {}
];
const uniqueFunctions2 = removeDuplicateFunctionsWithWeakMap(functions2);
console.log(uniqueFunctions2);
在这个方法中,通过检查 WeakMap
中是否已经存在该函数对象来决定是否添加到结果数组中。
比较函数的字符串表示
将函数转换为字符串,然后比较字符串是否相同。但这种方法有局限性,因为不同的函数实现可能字符串表示相同,而且函数内部的闭包等信息无法通过字符串表示体现。
function removeDuplicateFunctionsByString(arr) {const uniqueStrings = new Set();const result = [];for (const func of arr) {const funcString = func.toString();if (!uniqueStrings.has(funcString)) {uniqueStrings.add(funcString);result.push(func);}}return result;
}const functions3 = [function() {},function() {},function() {}
];
const uniqueFunctions3 = removeDuplicateFunctionsByString(functions3);
console.log(uniqueFunctions3);
通过 Set
存储函数的字符串表示,确保每个字符串只出现一次,从而实现去重。
实现左定宽右自适应的方法有哪些?
在前端开发中,实现左定宽右自适应的布局是常见需求,以下介绍几种不同的实现方法。
浮动布局
使用 float
属性将左侧元素浮动到左边,右侧元素通过设置 margin-left
为左侧元素的宽度来实现自适应。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>.left {float: left;width: 200px;background-color: lightblue;}.right {margin-left: 200px;background-color: lightgreen;}</style>
</head><body><div class="left">左定宽</div><div class="right">右自适应</div>
</body></html>
这种方法的优点是兼容性好,但需要注意清除浮动,避免布局混乱。
绝对定位布局
将左侧元素设置为绝对定位,右侧元素通过设置 margin-left
为左侧元素的宽度来实现自适应。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>.container {position: relative;}.left {position: absolute;left: 0;width: 200px;background-color: lightblue;}.right {margin-left: 200px;background-color: lightgreen;}</style>
</head><body><div class="container"><div class="left">左定宽</div><div class="right">右自适应</div></div>
</body></html>
这种方法的优点是布局简单,但父元素需要设置 position: relative
,并且可能会影响其他元素的布局。
Flexbox 布局
使用 Flexbox
布局可以很方便地实现左定宽右自适应。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>.container {display: flex;}.left {width: 200px;background-color: lightblue;}.right {flex: 1;background-color: lightgreen;}</style>
</head><body><div class="container"><div class="left">左定宽</div><div class="right">右自适应</div></div>
</body></html>
Flexbox
布局的优点是代码简洁,易于维护,并且具有良好的响应式特性。
Grid 布局
使用 Grid
布局也可以实现左定宽右自适应。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>.container {display: grid;grid-template-columns: 200px 1fr;}.left {background-color: lightblue;}.right {background-color: lightgreen;}</style>
</head><body><div class="container"><div class="left">左定宽</div><div class="right">右自适应</div></div>
</body></html>
Grid
布局提供了强大的二维布局能力,适用于复杂的布局场景。
flex:1,会不会在一些移动端(如安卓机等)出问题,如何解决?
flex: 1
是 Flexbox
布局中常用的属性,它是 flex-grow: 1
、flex-shrink: 1
和 flex-basis: 0
的简写,表示元素在主轴上会根据可用空间进行伸缩。在一些移动端(如安卓机)可能会出现问题,主要原因及解决方法如下。
浏览器兼容性问题
一些旧版本的安卓浏览器可能对 Flexbox
布局的支持不完全,导致 flex: 1
无法正常工作。可以通过添加浏览器前缀来解决兼容性问题。
.container {display: -webkit-flex; /* Safari */display: flex;
}.item {-webkit-flex: 1; /* Safari */flex: 1;
}
使用 -webkit-
前缀可以兼容一些旧版本的 Safari 和安卓浏览器。
盒子模型问题
如果元素的 box-sizing
属性设置不正确,可能会导致 flex: 1
布局出现问题。建议将 box-sizing
设置为 border-box
,这样元素的内边距和边框不会影响元素的宽度和高度计算。
* {box-sizing: border-box;
}.container {display: flex;
}.item {flex: 1;
}
父元素高度问题
如果父元素没有明确的高度,子元素使用 flex: 1
可能无法正常伸缩。可以为父元素设置明确的高度,或者使用 min-height
来确保父元素有足够的空间。
.container {display: flex;min-height: 100vh;
}.item {flex: 1;
}
嵌套 Flexbox
布局问题
在嵌套的 Flexbox
布局中,可能会出现一些意外的布局问题。可以通过调整嵌套元素的 flex
属性或者使用其他布局方式来解决。例如,确保每个嵌套的 Flexbox
容器都有正确的 display: flex
设置。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>.outer-container {display: flex;min-height: 100vh;}.inner-container {display: flex;flex: 1;}.item {flex: 1;}</style>
</head><body><div class="outer-container"><div class="inner-container"><div class="item">内容1</div><div class="item">内容2</div></div></div>
</body></html>
CSS3 如何实现一个 3D 图形?
CSS3 提供了强大的 3D 变换和透视效果,通过这些特性可以实现各种 3D 图形。以下是实现一个简单 3D 正方体的步骤和代码示例。
设置透视效果
透视效果可以让元素在 3D 空间中产生近大远小的视觉效果。可以通过 perspective
属性为父元素设置透视距离。
.container {perspective: 1000px;
}
perspective
的值表示观察者与元素的距离,值越小,透视效果越明显。
创建正方体的面
使用 div
元素创建正方体的六个面,并设置它们的位置和样式。
<div class="container"><div class="cube"><div class="face front">前面</div><div class="face back">后面</div><div class="face right">右面</div><div class="face left">左面</div><div class="face top">顶面</div><div class="face bottom">底面</div></div>
</div>
对正方体的面进行 3D 变换
使用 transform
属性对每个面进行旋转和平移,使其组成一个正方体。
.cube {position: relative;width: 200px;height: 200px;transform-style: preserve-3d;animation: rotate 5s infinite linear;
}.face {position: absolute;width: 200px;height: 200px;background-color: rgba(0, 0, 255, 0.5);border: 1px solid blue;display: flex;justify-content: center;align-items: center;font-size: 24px;
}.front {transform: translateZ(100px);
}.back {transform: rotateY(180deg) translateZ(100px);
}.right {transform: rotateY(90deg) translateZ(100px);
}.left {transform: rotateY(-90deg) translateZ(100px);
}.top {transform: rotateX(90deg) translateZ(100px);
}.bottom {transform: rotateX(-90deg) translateZ(100px);
}@keyframes rotate {from {transform: rotateX(0deg) rotateY(0deg);}to {transform: rotateX(360deg) rotateY(360deg);}
}
在这个代码中,transform-style: preserve-3d
表示子元素会保留 3D 变换效果。通过 translateZ
和 rotate
方法对每个面进行定位和旋转,使其组成一个正方体。animation
属性为正方体添加了旋转动画,使其在 3D 空间中旋转。
完整代码示例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>.container {perspective: 1000px;}.cube {position: relative;width: 200px;height: 200px;transform-style: preserve-3d;animation: rotate 5s infinite linear;}.face {position: absolute;width: 200px;height: 200px;background-color: rgba(0, 0, 255, 0.5);border: 1px solid blue;display: flex;justify-content: center;align-items: center;font-size: 24px;}.front {transform: translateZ(100px);}.back {transform: rotateY(180deg) translateZ(100px);}.right {transform: rotateY(90deg) translateZ(100px);}.left {transform: rotateY(-90deg) translateZ(100px);}.top {transform: rotateX(90deg) translateZ(100px);}.bottom {transform: rotateX(-90deg) translateZ(100px);}@keyframes rotate {from {transform: rotateX(0deg) rotateY(0deg);}to {transform: rotateX(360deg) rotateY(360deg);}}</style>
</head><body><div class="container"><div class="cube"><div class="face front">前面</div><div class="face back">后面</div><div class="face right">右面</div><div class="face left">左面</div><div class="face top">顶面</div><div class="face bottom">底面</div></div></div>
</body></html>
通过上述步骤和代码,就可以使用 CSS3 实现一个简单的 3D 正方体。
flex 布局,常见属性有哪些?
Flexbox(弹性盒子布局模型)是一种一维的布局模型,它提供了强大的空间分布和对齐能力。以下是 Flexbox 布局中常见的属性。
容器属性
- display:定义一个元素为 Flex 容器,其值可以是
flex
或inline-flex
。flex
使容器成为块级元素,inline-flex
使容器成为行内元素。
.container {display: flex;
}
- flex-direction:定义主轴的方向,决定子元素的排列方向。取值有
row
(水平从左到右,默认值)、row-reverse
(水平从右到左)、column
(垂直从上到下)、column-reverse
(垂直从下到上)。
.container {flex-direction: column;
}
- flex-wrap:定义子元素是否换行。取值有
nowrap
(不换行,默认值)、wrap
(换行)、wrap-reverse
(换行且顺序反转)。
.container {flex-wrap: wrap;
}
- flex-flow:是
flex-direction
和flex-wrap
的简写形式。
.container {flex-flow: row wrap;
}
- justify-content:定义子元素在主轴上的对齐方式。取值有
flex-start
(起始端对齐,默认值)、flex-end
(末尾端对齐)、center
(居中对齐)、space-between
(两端对齐,中间平均分布)、space-around
(每个元素两侧间隔相等)、space-evenly
(元素之间和两端的间隔都相等)。
.container {justify-content: center;
}
- align-items:定义子元素在交叉轴上的对齐方式。取值有
stretch
(拉伸填充容器,默认值)、flex-start
(起始端对齐)、flex-end
(末尾端对齐)、center
(居中对齐)、baseline
(基线对齐)。
.container {align-items: center;
}
- align-content:定义多行子元素在交叉轴上的对齐方式,当
flex-wrap
为wrap
或wrap-reverse
时有效。取值有stretch
(拉伸填充容器,默认值)、flex-start
(起始端对齐)、flex-end
(末尾端对齐)、center
(居中对齐)、space-between
(两端对齐,中间平均分布)、space-around
(每行两侧间隔相等)。
.container {align-content: space-between;
}
子元素属性
- order:定义子元素的排列顺序,数值越小越靠前,默认值为 0。
.item {order: 1;
}
- flex-grow:定义子元素的放大比例,默认值为 0,表示不放大。如果所有子元素的
flex-grow
都为 1,则它们会平均分配剩余空间。
.item {flex-grow: 1;
}
- flex-shrink:定义子元素的缩小比例,默认值为 1,表示当空间不足时会缩小。如果为 0,则表示不缩小。
.item {flex-shrink: 0;
}
- flex-basis:定义子元素在主轴上的初始大小,默认值为
auto
,表示根据内容自动确定大小。也可以设置具体的长度值,如200px
。
.item {flex-basis: 200px;
}
- flex:是
flex-grow
、flex-shrink
和flex-basis
的简写形式。常用值有auto
(相当于1 1 auto
)、none
(相当于0 0 auto
)、1
(相当于1 1 0
)。
.item {flex: 1;
}
- align-self:定义单个子元素在交叉轴上的对齐方式,会覆盖容器的
align-items
属性。取值与align-items
相同。
.item {align-self: flex-end;
}
通过合理使用这些属性,可以实现各种复杂的 Flexbox 布局。
垂直水平居中实现方式有哪些?
在前端开发中,实现元素的垂直水平居中是一个常见需求,以下是多种不同场景下的实现方式。
对于行内元素
- 单行文本:若要使单行文本在其父元素内垂直水平居中,可借助
text-align: center
实现水平居中,使用line-height
等于父元素的height
实现垂直居中。
.parent {height: 100px;text-align: center;line-height: 100px;
}
- 多行文本:对于多行文本,可采用
flexbox
布局。给父元素设置display: flex
或display: inline-flex
,再结合justify-content: center
和align-items: center
。
.parent {display: flex;justify-content: center;align-items: center;
}
对于块级元素
- 已知宽高的块级元素:可使用绝对定位与负边距的方法。将元素的
top
和left
设置为50%
,再通过负边距将其向上和向左移动自身宽高的一半。
.parent {position: relative;
}
.child {position: absolute;top: 50%;left: 50%;width: 200px;height: 200px;margin-top: -100px;margin-left: -100px;
}
- 未知宽高的块级元素:同样使用绝对定位,不过结合
transform: translate(-50%, -50%)
来实现垂直水平居中。
.parent {position: relative;
}
.child {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);
}
使用 Flexbox 布局
对于父元素和子元素,只需给父元素设置 display: flex
或 display: inline-flex
,再结合 justify-content: center
和 align-items: center
即可实现子元素的垂直水平居中。
.parent {display: flex;justify-content: center;align-items: center;
}
使用 Grid 布局
给父元素设置 display: grid
或 display: inline-grid
,然后使用 place-items: center
来实现子元素的垂直水平居中。
.parent {display: grid;place-items: center;
}
手写:div 在页面中间垂直居中
以下是几种不同的实现方式,能让 div
在页面中间垂直居中。
使用 Flexbox 布局
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>html, body {height: 100%;margin: 0;}.parent {display: flex;justify-content: center;align-items: center;height: 100%;}.child {width: 200px;height: 200px;background-color: lightblue;}</style>
</head><body><div class="parent"><div class="child"></div></div>
</body></html>
在这个例子里,先把 html
和 body
的高度设为 100%
,再给父元素 parent
应用 flexbox
布局,通过 justify-content: center
和 align-items: center
实现子元素 child
的垂直水平居中。
使用绝对定位和 transform
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>html, body {height: 100%;margin: 0;position: relative;}.child {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);width: 200px;height: 200px;background-color: lightblue;}</style>
</head><body><div class="child"></div>
</body></html>
此方法将子元素 child
绝对定位,top
和 left
设为 50%
,再用 transform: translate(-50%, -50%)
把元素向上和向左移动自身宽高的一半,从而实现垂直水平居中。
使用 Grid 布局
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>html, body {height: 100%;margin: 0;display: grid;place-items: center;}.child {width: 200px;height: 200px;background-color: lightblue;}</style>
</head><body><div class="child"></div>
</body></html>
这里给 html
和 body
应用 grid
布局,使用 place-items: center
让子元素 child
垂直水平居中。
手写:三栏布局,三个 div 均分
以下几种方法可以实现三个 div
均分的三栏布局。
使用 Flexbox 布局
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>.container {display: flex;}.column {flex: 1;background-color: lightblue;margin: 5px;padding: 20px;}</style>
</head><body><div class="container"><div class="column">Column 1</div><div class="column">Column 2</div><div class="column">Column 3</div></div>
</body></html>
在这个例子中,父元素 container
使用 flexbox
布局,子元素 column
的 flex
属性设为 1
,这意味着它们会平均分配父元素的剩余空间,从而实现三栏均分。
使用 Grid 布局
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>.container {display: grid;grid-template-columns: repeat(3, 1fr);}.column {background-color: lightblue;margin: 5px;padding: 20px;}</style>
</head><body><div class="container"><div class="column">Column 1</div><div class="column">Column 2</div><div class="column">Column 3</div></div>
</body></html>
这里父元素 container
使用 grid
布局,grid-template-columns: repeat(3, 1fr)
表示将父元素的宽度平均分成三份,每个子元素 column
占据一份,实现三栏均分。
使用浮动布局
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>.container {overflow: auto;}.column {float: left;width: calc(33.33% - 10px);background-color: lightblue;margin: 5px;padding: 20px;}</style>
</head><body><div class="container"><div class="column">Column 1</div><div class="column">Column 2</div><div class="column">Column 3</div></div>
</body></html>
此方法利用浮动布局,每个子元素 column
的宽度设为 calc(33.33% - 10px)
,其中减去 10px
是为了给 margin
留出空间,通过 float: left
让元素向左浮动,实现三栏均分。但需要注意的是,使用浮动布局时,父元素需要清除浮动,这里通过 overflow: auto
来实现。
讲一下第三方登录(SSO,OAuth) 的原理和流程
单点登录(SSO)
- 原理:单点登录允许用户使用一组凭证(如用户名和密码)访问多个相关但独立的应用系统。其核心原理是在一个统一的身份验证中心(认证服务器)进行用户身份验证,当用户在一个应用系统登录后,认证服务器会颁发一个令牌(通常是一个会话 ID),用户访问其他关联的应用系统时,只需携带该令牌,应用系统会将令牌发送到认证服务器进行验证,验证通过后用户即可访问该应用系统,无需再次输入凭证。
- 流程:
- 用户访问应用系统 A,发现未登录,被重定向到认证服务器。
- 用户在认证服务器输入用户名和密码进行登录。
- 认证服务器验证用户信息,若验证通过,生成一个令牌并将其存储在自身的会话中,同时将令牌返回给应用系统 A。
- 应用系统 A 接收到令牌后,将其存储在本地(如 cookie 中),并允许用户访问。
- 当用户访问应用系统 B 时,应用系统 B 发现用户未登录,检查本地是否有令牌。若有,将令牌发送到认证服务器进行验证。
- 认证服务器验证令牌,确认其有效性后,通知应用系统 B 用户已认证,应用系统 B 允许用户访问。
OAuth(开放授权)
- 原理:OAuth 是一种开放标准的授权协议,允许用户授权第三方应用访问其在另一个服务提供商上的资源,而无需将自己的用户名和密码提供给第三方应用。它通过令牌机制来实现授权,用户在服务提供商处授权第三方应用后,服务提供商颁发一个访问令牌给第三方应用,第三方应用使用该令牌访问用户的资源。
- 流程(以三方应用请求访问用户在服务提供商的资源为例):
- 第三方应用向服务提供商请求授权,携带自身的客户端 ID 和重定向 URI。
- 服务提供商显示授权页面,提示用户授权第三方应用访问其资源。
- 用户同意授权后,服务提供商将用户重定向到第三方应用提供的重定向 URI,并附带一个授权码。
- 第三方应用使用授权码和自身的客户端密钥向服务提供商请求访问令牌。
- 服务提供商验证授权码和客户端密钥,若验证通过,颁发访问令牌给第三方应用。
- 第三方应用使用访问令牌向服务提供商请求用户的资源,服务提供商验证访问令牌,若有效则返回用户资源给第三方应用。
memory *** 与 disk *** 区别,除此之外还有别的浏览器缓存么
memory cache 与 disk cache 的区别
- 存储位置:
- memory cache:即内存缓存,数据存储在浏览器的内存中。内存是计算机的临时存储区域,读写速度非常快,但容量相对较小,并且当浏览器关闭或页面刷新时,内存中的数据会被清除。
- disk cache:即磁盘缓存,数据存储在计算机的硬盘或固态硬盘上。磁盘的存储容量较大,但读写速度相对较慢。磁盘缓存的数据在浏览器关闭后仍然存在,除非达到缓存的过期时间或被手动清除。
- 读写速度:
- memory cache:由于数据存储在内存中,读写速度极快,几乎可以瞬间完成数据的读取和写入操作。这使得浏览器在需要快速加载资源时,优先从内存缓存中获取数据。
- disk cache:虽然磁盘的读写速度也在不断提高,但相较于内存缓存,仍然有明显的差距。当内存缓存中没有所需资源时,浏览器会从磁盘缓存中查找。
- 缓存策略:
- memory cache:通常用于缓存当前页面正在使用的资源,如当前页面的脚本、样式表等。浏览器会根据资源的使用频率和重要性来决定是否将其缓存到内存中。
- disk cache:会缓存更多类型的资源,包括一些不常用的资源。磁盘缓存的过期策略通常由服务器通过响应头信息(如
Cache-Control
、Expires
等)来控制。
其他浏览器缓存
- Service Worker Cache:Service Worker 是一种在浏览器后台运行的脚本,它可以拦截网络请求并缓存资源。Service Worker Cache 可以实现离线缓存,即使在没有网络连接的情况下,用户也可以访问之前缓存的页面。开发人员可以通过编写 Service Worker 脚本来精确控制缓存的资源和缓存策略。
- Application Cache:这是 HTML5 引入的一种缓存机制,允许网页开发者指定哪些文件需要缓存,以便在离线时可以访问。不过,由于其使用复杂且存在一些问题,已经逐渐被 Service Worker 取代。
- IndexedDB:是一种在浏览器中存储大量结构化数据的数据库。它可以用于缓存数据,特别是对于需要频繁访问和更新的数据。IndexedDB 支持事务操作和数据版本控制,提供了比
localStorage
和sessionStorage
更强大的存储功能。 - localStorage 和 sessionStorage:
- localStorage:用于长期存储数据,除非手动清除,否则数据不会过期。它可以存储字符串类型的数据,存储容量一般为 5MB 左右。
- sessionStorage:数据仅在当前会话期间有效,当页面关闭时,数据会被清除。它的使用场景与
localStorage
类似,但数据的生命周期更短。
说下事件冒泡,事件捕获
事件冒泡和事件捕获是 DOM 事件流中的两种不同机制,它们描述了事件在 DOM 树中传播的顺序。
事件捕获是事件流的第一个阶段。当一个事件发生时,事件从文档的根节点(通常是 document
对象)开始,逐级向下查找,直到找到事件发生的目标元素。在这个过程中,事件会依次触发目标元素的祖先元素上绑定的捕获阶段的事件处理程序。例如,当我们点击一个页面中的按钮时,事件会先从 document
开始,然后依次经过 html
、body
等元素,直到到达按钮元素。在这个过程中,如果这些祖先元素上绑定了捕获阶段的事件处理程序,它们会依次被触发。
事件冒泡则是事件流的第二个阶段。在事件到达目标元素后,事件会从目标元素开始,逐级向上传播,直到到达文档的根节点。在这个过程中,事件会依次触发目标元素的祖先元素上绑定的冒泡阶段的事件处理程序。继续以上面点击按钮的例子来说,当事件到达按钮后,它会从按钮开始,依次经过按钮的父元素、父元素的父元素等,直到到达 document
。在这个过程中,如果这些祖先元素上绑定了冒泡阶段的事件处理程序,它们会依次被触发。
在 HTML 中,默认情况下,事件处理程序是在冒泡阶段触发的。不过,我们可以通过 addEventListener
方法的第三个参数来指定事件处理程序是在捕获阶段还是冒泡阶段触发。如果第三个参数为 true
,则事件处理程序会在捕获阶段触发;如果为 false
或者省略,则事件处理程序会在冒泡阶段触发。例如:
const parent = document.getElementById('parent');
const child = document.getElementById('child');parent.addEventListener('click', function() {console.log('Parent clicked - 冒泡阶段');
}, false);parent.addEventListener('click', function() {console.log('Parent clicked - 捕获阶段');
}, true);child.addEventListener('click', function() {console.log('Child clicked - 冒泡阶段');
}, false);child.addEventListener('click', function() {console.log('Child clicked - 捕获阶段');
}, true);
当我们点击 child
元素时,事件的传播顺序会是先捕获阶段,从 document
开始,依次触发 parent
和 child
上的捕获阶段的事件处理程序,然后是冒泡阶段,从 child
开始,依次触发 child
和 parent
上的冒泡阶段的事件处理程序。
事件冒泡和事件捕获为我们处理 DOM 事件提供了不同的方式,我们可以根据具体的需求选择合适的事件处理阶段。
botton 怎么拿到 input 框,事件委托还是 ref
在前端开发中,让 button
元素获取 input
框的方式有多种,事件委托和使用 ref
是比较常见的两种方法,它们各有适用场景。
事件委托是一种利用事件冒泡原理的技术。当一个 button
需要与 input
框交互时,我们可以将事件处理程序绑定到它们的共同祖先元素上。当 button
被点击时,事件会冒泡到祖先元素,在祖先元素的事件处理程序中,我们可以通过事件对象来获取 input
框的值。例如:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
</head><body><div id="container"><input type="text" id="myInput"><button>获取输入框的值</button></div><script>const container = document.getElementById('container');container.addEventListener('click', function(event) {if (event.target.tagName === 'BUTTON') {const input = document.getElementById('myInput');console.log(input.value);}});</script>
</body></html>
在这个例子中,我们将点击事件处理程序绑定到 container
元素上。当 button
被点击时,事件会冒泡到 container
,在事件处理程序中,我们通过判断 event.target
的标签名是否为 BUTTON
来确定是 button
被点击,然后获取 input
框的值。事件委托的优点是可以减少事件处理程序的数量,提高性能,特别是在动态添加或删除元素的场景中。
使用 ref
则是直接引用 input
框。在不同的前端框架中,ref
的使用方式有所不同。以 React 为例:
import React, { useRef } from 'react';function App() {const inputRef = useRef(null);const handleClick = () => {console.log(inputRef.current.value);};return (<div><input type="text" ref={inputRef} /><button onClick={handleClick}>获取输入框的值</button></div>);
}export default App;
在这个 React 组件中,我们使用 useRef
创建了一个 ref
对象 inputRef
,并将其绑定到 input
框上。当 button
被点击时,在 handleClick
函数中,我们可以通过 inputRef.current
直接访问 input
框的 DOM 元素,从而获取其值。使用 ref
的优点是可以直接、方便地操作 DOM 元素,代码逻辑更清晰。
如果需要处理大量动态元素的事件,事件委托可能更合适;如果只是简单地获取特定 input
框的值,使用 ref
会更简洁明了。
场景题:input 框上传文件怎么做,想在上面覆盖一个 button
要实现 input
框上传文件并且在上面覆盖一个 button
,可以按照以下步骤进行。
首先,我们需要创建一个 input
元素,其 type
属性设置为 file
,用于选择文件。然后创建一个 button
元素,将其覆盖在 input
元素上。通过 CSS 样式设置 input
元素的透明度为 0,使其不可见,同时将 button
元素的样式设置为我们想要的外观。最后,通过 JavaScript 监听 button
的点击事件,当点击 button
时,触发 input
元素的点击事件,从而弹出文件选择对话框。
以下是一个示例代码:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>.upload-container {position: relative;width: 200px;}.upload-button {display: inline-block;background-color: #007BFF;color: white;padding: 10px 20px;border: none;cursor: pointer;}.upload-input {position: absolute;top: 0;left: 0;width: 100%;height: 100%;opacity: 0;cursor: pointer;}</style>
</head><body><div class="upload-container"><button class="upload-button">选择文件</button><input type="file" class="upload-input" id="fileInput"></div><script>const button = document.querySelector('.upload-button');const input = document.getElementById('fileInput');button.addEventListener('click', function() {input.click();});input.addEventListener('change', function() {const files = this.files;if (files.length > 0) {console.log('选择的文件:', files[0].name);// 这里可以添加上传文件的逻辑}});</script>
</body></html>
在这个示例中,我们创建了一个 upload-container
容器,将 button
和 input
元素放在其中。通过 CSS 样式将 button
元素显示出来,将 input
元素设置为透明并覆盖在 button
上。在 JavaScript 部分,我们监听 button
的点击事件,当点击 button
时,触发 input
元素的点击事件,从而弹出文件选择对话框。同时,我们监听 input
元素的 change
事件,当用户选择了文件后,会在控制台输出选择的文件名,你可以在这个事件处理程序中添加实际的文件上传逻辑。
你能简单描述一下登录的流程吗,如何获取 token,如何鉴权?
登录流程通常涉及用户输入凭证、验证凭证、获取访问令牌(token)以及后续的鉴权过程。
用户发起登录请求时,会在登录页面输入用户名和密码等凭证信息。这些信息会被发送到服务器端进行验证。服务器接收到请求后,会对用户输入的凭证进行验证,通常是将用户名和密码与数据库中存储的信息进行比对。如果验证通过,服务器会生成一个唯一的 token
,并将其返回给客户端。
获取 token
的方式有多种。常见的是使用 OAuth 2.0 协议,在用户授权后,客户端向授权服务器发送请求,包含客户端 ID、客户端密钥、授权码等信息,授权服务器验证这些信息后,会返回一个 token
,包括访问令牌(access token)和刷新令牌(refresh token)。访问令牌用于访问受保护的资源,而刷新令牌用于在访问令牌过期后获取新的访问令牌。在简单的登录系统中,服务器也可以直接在用户登录成功后生成一个 token
并返回给客户端。
拿到 token
后,客户端需要在后续的请求中携带该 token
进行鉴权。鉴权是指服务器验证客户端请求的合法性,确保请求是由合法用户发起的。客户端通常会将 token
放在请求头中,常见的做法是在请求头的 Authorization
字段中添加 Bearer
前缀,后面跟上 token
,例如 Authorization: Bearer <token>
。服务器接收到请求后,会从请求头中提取 token
,并对其进行验证。验证过程可能包括检查 token
的格式是否正确、是否过期、签名是否有效等。如果 token
验证通过,服务器会认为请求是合法的,允许客户端访问受保护的资源;如果验证失败,服务器会返回相应的错误信息,如 401 Unauthorized。
为了提高安全性,token
通常会设置一个过期时间。当 token
过期后,客户端可以使用刷新令牌向服务器请求新的访问令牌,避免用户重新登录。此外,服务器还可以采用一些其他的安全措施,如对 token
进行加密、使用多因素认证等,以增强系统的安全性。
token 放在请求头哪个字段里?
在前端开发中,当使用 token
进行身份验证时,通常会将 token
放在请求头的 Authorization
字段中。这种做法遵循了 HTTP 协议中关于身份验证的规范,并且被广泛应用于各种 API 接口的鉴权过程。
Authorization
字段的格式通常有多种,常见的是使用 Bearer
方案。Bearer
方案是 OAuth 2.0 协议中定义的一种用于传递访问令牌的方式。在这种方案中,Authorization
字段的值由 Bearer
关键字和实际的 token
组成,中间用空格分隔。例如:
Authorization: Bearer <token>
这里的 <token>
就是服务器颁发给客户端的访问令牌。客户端在向服务器发送请求时,将这个 Authorization
字段添加到请求头中,服务器在接收到请求后,会从该字段中提取 token
并进行验证。
除了 Bearer
方案,还有其他一些身份验证方案也可以使用 Authorization
字段,比如 Basic
方案。在 Basic
方案中,Authorization
字段的值是 Basic
关键字加上经过 Base64 编码的用户名和密码,格式为 Authorization: Basic <base64-encoded-username-password>
。不过,Basic
方案通常用于简单的身份验证,安全性相对较低,而 Bearer
方案更适合用于传递 token
进行身份验证。
将 token
放在 Authorization
字段中是一种标准且安全的做法,它可以确保 token
在传输过程中的安全性,同时也方便服务器进行统一的身份验证和鉴权操作。在实际开发中,我们可以使用各种前端框架或库来设置请求头,例如在使用 axios
发送请求时,可以通过设置 headers
对象来添加 Authorization
字段:
import axios from 'axios';const token = 'your_token_here';
axios.get('https://api.example.com/data', {headers: {'Authorization': `Bearer ${token}`}
}).then(response => {console.log(response.data);
}).catch(error => {console.error(error);
});
这样,在发送请求时,token
就会被正确地添加到请求头的 Authorization
字段中,服务器可以根据该字段进行身份验证。