目录
4、Vue 在遍历列表时会加上一个 key,这个 key 有些什么样的作用?
9、Vue 的 diff 算法和 React 的 diff 算法有什么区别?
3、如何实现一个两列布局,左边宽度为200px,右边撑满剩下空间。
5、你是哪个城市的?为什么想到xx城市发展?家里人对此的看法是?
8、大概什么时候开始投递简历的?到现在总共有几家面试?手里边有 offer 吗?方便介绍一下吗?
Vue
1、React 和 Vue 各自的优缺点。
- 共同点:
- 都采用数据驱动视图的设计理念,当数据发生变化时,视图会自动更新;
- 都使用虚拟 DOM 来提高性能,通过最小化 DOM 操作来优化渲染效率;
- 都采用了组件化开发的新模式,具有高度的可重用性和可维护性;
- 核心库与相关库分离,只有框架的骨架,其他的功能如路由、状态管理等是框架分离的组件。
- 区别:
- React 使用 JSX 语法,允许将 HTML 和 JavaScript 混合使用,而 Vue 使用单文件组件,将 HTML、CSS、JavaScript 集成在一个文件;
- React 强调函数式编程和单向数据流,而 Vue 更加关注渐进式框架和双向数据绑定;
- React 依赖于外部状态管理库,如 Redux、MobX 等,而 Vue 内置了 Vuex 进行状态管理,支持集中式的状态管理;
- React 使用 memo、useCallback 等手动优化性能,而 Vue 自动追踪依赖,优化性能。
- 各自优缺点:
- Vue:
- 优点:
- 易于学习和上手:简单直观的 API 和模板语法,使得初学者能够快速入门,降低了学习成本,适合快速迭代的小型项目;
- 轻量高效:体积小,性能优化好;
- 双向绑定:内置 v-model 指令,简化表单处理逻辑。
- 缺点:
- 生态系统相对较小:与 React 相比,Vue 的第三方库和工具数量相对较少;
- 灵活性较低:相比 React,Vue 的结构化设计可能限制复杂项目的灵活性。
- 优点:
- React:
- 优点:
- 灵活性和可扩展性高:React 是一个视图层库,可以根据项目需求选择不同的工具和库来搭配使用,适合大型复杂项目的开发;
- 强大的生态系统:拥有丰富的第三方库和工具,能够快速解决各种开发问题,加快项目开发进度;
- 适合函数式编程:React 推崇函数式编程思想,函数式组件的使用使得代码更加简洁、可预测和易于测试,有助于提高代码质量和可维护性。
- 缺点:
- 学习成本高:复杂的概念和语法,如 JSX 和虚拟 DOM,对于初学者来说可能比较困难,需要花费较多时间学习和实践;
- 代码复杂度高:在大型复杂的 React 应用中,如果没有良好的代码组织和架构设计,代码可能会变得非常复杂;
- SEO 优化难度高:基于 JavaScript 动态生成 DOM,搜索引擎爬虫可能无法很好地解析和理解页面内容。
- 优点:
- Vue:
2、路由守卫的权限控制是如何实现的?
router.beforeEach((to, from, next) => {
// 获取本地存储的认证信息
const isAuthenticated = !!localStorage.getItem('admin');
// 路由需要登录但用户未认证
if(to.meta.requireAuth && !isAuthenticated) {
next({ name: 'Login' }); // 跳转到登录页面
} else {
next(); // 不需要认证的情况下放行
}
});
3、Vue 中组件通信的方式。
- Props:父组件传值给子组件;
-
// 父组件 <template> <!-- 使用子组件 --> <Child :message="message" /> </template> <script setup> import Child from './components/Child.vue'; // 引入子组件 let message = 'we are one!'; </script> // 子组件 <template> <div>{{ message }}</div> </template> <script setup> const props = defineProps(["message"]); // 在 js 中需要使用 props.xxx 的方式调用,在 html 中不需要 props. console.log(props.message); </script>
-
- emits:子组件通知父组件触发一个事件,并且可以传值给父组件;
-
// 父组件 <template> <div>{{ message }}</div> <!-- 自定义 changeMessage 事件 --> <Child @changeMessage="changeMessage" /> </template> <script setup> import { ref } from 'vue'; import Child from './component/Child.vue'; const message = ref('we are one!'); // 更改 message 的值,data 是从子组件传过来的 function changeMessage(data) { message.value = data; } </script> // 子组件 <template> <div> <button @click="handleClick">子组件按钮</button> </div> </template> <script setup> // 注册一个自定义事件名,向上传递时告诉父组件要触发的事件 const emit = defineEmits(['changeMessage']); function handleClick() { // 参数:1.事件名 2.传给父组件的值 emit('changeMessage', '哈哈哈哈哈'); } </script>
-
- expose/ref:子组件通过 expose 暴露自身的方法和数据,父组件通过 ref 获取到子组件并调用其方法或访问数据;
-
// 父组件 <template> <div>{{ message }}</div> <button @click="callChildFn">调用子组件的方法</button> <Child ref="children" /> </template> <script setup> import { ref, onMounted } from 'vue'; import Child from './components/Child.vue'; const children = ref(null); // 通过 ref 绑定子组件 const message = ref(''); onMounted(() => { // 在加载完成之后,将子组件的 message 赋值给父组件的 message message.value = children.value.message; }) function callChildFn() { // 调用子组件的 changeMessage 方法 children.value.changeMessage('we are one!'); // 重新将子组件的 message 赋值给父组件的 message message.value = children.value.message; } </script> // 子组件 <template> <div>{{ message }}</div> </template> <script setup> import { ref } from 'vue'; const message = ref('哈哈哈哈哈'); function changeMessage(data) { message.value = data; } // 使用 defineExpose 向外暴露指定的数据和方法 defineExpose({ message, changeMessage }) </script>
-
- v-model
- 单值情况
-
// 父组件 <template> <Child v-model="message" /> </template> <script setup> import { ref } from 'vue'; import Child from './components/Child.vue' const message = ref('we are one!'); </script> // 子组件 <template> <!-- 必须用 update:modelValue 这个名字来通知父组件 --> <div @click="$emit('update:modelValue', '哈哈哈哈哈')">{{ modelValue }}</div> </template> <script setup> import { ref } from 'vue'; // 接收父组件使用 v-model 传进来的值,必须用 modelValue 这个名字来接收 const props = defineProps(['modelValue']); </script> - 多值情况
-
// 父组件 <template> <Child v-model:message1="message1" v-model:message2="message2" /> </template> <script setup> import { ref } from 'vue'; import Child from './components/Child.vue'; const message1 = ref('we are one!'); const message2 = ref('哈哈哈哈哈'); </script> // 子组件 <template> <div> <button @click="changeMessage1">修改 message1</button> {{ message1 }} </div> <div> <button @click="changeMessage2">修改 message2</button> {{ message2 }} </div> </template> <script setup> import { ref } from 'vue'; // 接收 const props = defineProps({ message1: String, message2: String }); const emit = defineEmits(['update:message1', 'update:message2']); function changeMessage1() { emit('update:message1', '啦啦啦啦啦'); emit('update:message2', 'call me'); } </script>
- 插槽 slot:插槽可以理解为传一段 HTML 片段给子组件,子组件将<slot>元素作为承载分发内容的出口;
- 默认插槽
-
// 父组件 <template> <Child> <div>哈哈哈哈哈</div> </Child> </template> <script setup> import Child from './components/Child.vue'; </script> // 子组件 <template> <div> <slot></slot> </div> </template> - 具名插槽
-
// 父组件 <template> <Child> <template v-slot:message> <div>哈哈哈哈哈哈</div> </template> <button>we are one!</button> </Child> </template> <script setup> import Child from './components/Child.vue'; </script> // 子组件 <template> <div> <!-- 默认插槽 --> <slot></slot> <!-- 具名插槽 --> <slot name="message"></slot> </template> - 作用域插槽
-
// 父组件 <template> <!-- v-slot="{scope}"获取子组件传上来的数据 --> <!-- :list="list"把 list 传给子组件 --> <Child v-slot="{scope}" :list="list"> <div>课程:{{ scope.name }}</div> <div>成绩:{{ scope.grade }}</div> </Child> </template> <script setup> import Child from './components/Child.vue'; import { ref } from 'vue'; const list = ref([ { name: '语文', grade: '55' }, { name: '数学', grade: '66' }, { name: '英语', grade: '77' } ]); </script> // 子组件 <template> <div> <!-- 用:scope="item"返回每一项 --> <slot v-for="item in list" :scope="item" /> </div> </template> <script setup> const props = defineProps({ list: { type: Array, default: () => []} }) </script>
- provide/inject:遇到多层传值时,可以用 provide 和 inject,无论组件层次结构有多深,父组件都可以作为其所有子组件的以来提供者
-
// 父组件 <template> <Child /> </template> <script setup> import Child from './components/Child.vue'; import { ref, provide, readonly } from 'vue'; const message1 = ref('哈哈哈哈哈'); const message2 = ref('we are one!'); // 使用 readonly 可以让子组件无法直接修改,需要调用 provide 往下传的方法来修改 provide('message1', readonly(message1)); provide('message2', message2); provide('changeMessage1', (value) => { message1.value = value; }) </script> // 子组件 <template> <div>{{ message1 }}</div> <div>{{ message2 }}</div> <button @click="handleClick">修改</button> </template> <script setup> import { inject } from 'vue'; const message1 = inject('message1', '啦啦啦啦啦'); // 看看有没有值,没有值的话使用默认值“啦啦啦啦啦” const message2 = inject('message2'); const changeMessage1 = inject('changeMessage1'); function handleClick() { changeMessage1('call me baby'); // 因为 message2 没被 readonly 过,所以可以直接修改 message2.value = 'baby don't cry'; } </script>
-
- mitt.js
-
// 安装 npm i mitt // Bus.js import mitt from 'mitt'; export default mitt(); // 父组件 <template> <Child /> </template> <script setup> import Child from './components/Child.vue'; import Bus from './Bus.js'; Bus.on('sayHello', () => console.log('we are one!')); </script> // 子组件 <template> <button @lick="handleClick">打招呼</button> </template> <script setup> import Bus from './Bus.js'; function handleClick() { Bus.emit('sayHello'); } </script>
-
- Vuex:主要解决跨组件通信的问题
-
// 安装 npm install vuex@next --save // 使用 // src/store/index.js import { createStore } from 'vuex'; export default createStore({ state: { // 数据仓库,用来存数据的 message: 'we are one!' }, getters: { // 获取数据的 getMessage(state) { return state.message + "哈哈哈"; } }, mutations: { // 更改 state 数据的方法都要写在 mutations 里 changeMessage(state, data) { state.message = data; } }, actions: { // 异步,异步的方法都写在这里,但最后还是需要通过 mutations 来修改 state 的数据 fetchMessage(context) { // 模拟请求 setTimeout(() => { context.commit('changeMessage', '啦啦啦'); }, 1000); } }, modules: { // 分包,如果项目比较大,可以将业务拆散成独立模块,然后分文件管理和存放 } }) // 引入 // src/main.js import { createApp } from 'vue'; import App from './App.vue'; import store from './store'; const app = createApp(App); app.use(store).mount('#app'); // 组件 // xxx.vue <script setup> import { useStore } from 'vuex'; const store = useStore(); console.log(store.state.message); // we are one! console.log(store.getters.getMessage); // we are one!哈哈哈 store.commit('changeMessage', '啦啦啦'); console.log(store.state.message); // 啦啦啦 store.dispatch('fetchMessage'); </script>
-
4、Vue 在遍历列表时会加上一个 key,这个 key 有些什么样的作用?
- 优化虚拟 DOM 的更新效率:Vue 使用虚拟 DOM 来高效地更新真实 DOM。当列表数据发生变化时(如新增、删除或者重新排序),Vue 需要对比新旧虚拟 DOM 的差异,以确定如何最小化地更新真实 DOM。如果没有 key,Vue 会默认使用“就地复用”策略,即直接更新现有元素的内容,而不是重新排序或移动元素。通过 key,Vue 可以准确识别每个元素的唯一性,从而在更新时正确地移动、添加或删除元素,而不是简单地复用现有元素。
- 保持元素状态:当列表中的元素包含内部状态时(如输入框的值、动画状态等),通过 key,Vue 可以正确匹配新旧元素,确保状态与正确的元素绑定。
- 避免不必要的 DOM 操作:在列表更新时,key 可以帮助 Vue 识别哪些元素是需要更新的,仅对它们进行操作,减少不必要的 DOM 操作,从而提高性能。
5、状态共享。
Vuex 是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间数据的共享。
- 优势:
- 能够在 Vuex 中集中管理共享的数据,易于开发和后期维护;
- 能够高效地实现组件之间的数据共享,提高开发效率;
- 存储在 Vuex 中的数据都是响应式的,能够实时保持数据与页面的同步。
- 注意点:
- 不要在组件中直接使用 store.state.xxx 去更改 state 中的数据,如果直接更改,调试工具检测不到数据更改;
- 只有 mutations 中定义的函数,才有权力修改 state 中的数据,不要在 mutations 函数中,执行异步操作,调试工具检测不到数据更改;
- 在 actions 中,不能直接修改 state 中的数据,必须通过 context.commit() 触发某个 mutation 才行。
6、HTTP 拦截器有哪些?
// request.js
import axios from 'axios';
// 创建 axios 实例
const services = axios.create({
// 配置 baseURL 等配置项
baseURL: "http://localhost:8080",
timeout: 6000
});
// 请求拦截器
services.interceptors.request.use(config => {
// 在这里可以添加一些请求头的逻辑,如配置 token
const token = localStorage.getItem('token'); // 从 localStorage 或任何其他地方获取 token
if (token) {
// 每次发送请求前将 token 作为凭证添加到请求头的 Authorization 字段中
config.headers.Authorization = 'Bearer ' + token;
}
return config;
}, error => {
return Promiese.reject(error);
});
// 响应拦截器
services.interceptors.response.use(response => {
// 在这里可以根据实际的响应值去进行判断,如登录失效跳转到登录页等
// 收到响应时,客户端检查响应头中是否有新的 token,如果有,客户端将新 token 保存在本地以便下次请求时使用
const newToken = response.headers['new-token']; // 检查响应头是否有新 token
// 如果存在新 token,保存在本地
if (newToken) {
localStorage.setItem('token', token);
}
}, error => {
return Promise.reject(error);
});
export default services;
7、说说路由守卫有哪些特色。
路由守卫主要通过跳转或取消的方式守卫导航,分为:
- 全局路由守卫:包括全局前置守卫和全局后置守卫,钩子执行顺序包括 beforeEach、beforeResolve、afterEach;
- 组件路由守卫:组件内执行的函数,钩子执行顺序为 beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave;
- 路由独享守卫:仅一个钩子函数 beforeEnter。
路由守卫钩子的三个参数:
- to:目标路由对象;
- from:即将要离开的路由对象;
- next:调用该方法后,进入下一个钩子函数;
- next():进 to 所指路由;
- next(false):中断当前路由;
- next('route'):跳转指定路由;
- next('error'):跳转错误路由。
8、Vue的响应式是怎么实现的?
- Vue2:
- 数据观测:当 Vue 实例创建时,它会遍历 data 对象的所有属性,并使用 Object.defineProperty 将每个属性转换为响应式,这个过程由 Observer 类完成;
- 依赖收集:当模板渲染或计算属性时,Vue 会追踪哪些数据被访问了,这通过 Dep 类和 Watcher 类完成,Watcher 会在读取数据时将自身添加到数据的依赖列表里;
- 数据变更通知:当数据被修改时,对应的 Watcher 会收到通知,并触发视图更新。
- Vue3:
- 数据包装:在 Vue3 中,响应式数据不再是直接修改的原生对象,而是通过 reactive 函数包装后的代理对象,这个代理对象由 Proxy 创建,可以拦截所有的读取和写入操作;
- 读取操作的追踪:当访问响应式数据的属性时,Proxy 的 get 方法会被调用,Vue 的响应式系统会记录下这次读取操作,并将其与当前的副作用函数 effect 关联起来;
- 写入操作的追踪:当修改响应式数据的属性时,Proxy 的 set 方法会被调用,Vue 的响应式系统会检查哪些副作用函数依赖于这个属性,并将它们标记为需要更新;
- 触发更新:当执行到被标记为需要更新的副作用函数时,Vue 的调度器会确保它们重新执行,从而触发视图的更新,这个过程通常是异步的,以提高性能。
9、Vue 的 diff 算法和 React 的 diff 算法有什么区别?
-
对比节点:当节点元素相同,但 classname 不同,Vue 会认为是不同类型的元素,删除重建,而 React 认为是同类型节点,只是修改节点属性;
-
对比列表:Vue 采用的是两端到中间比对的方式,而 React 采用的是从左到右依次对比的方式,当一个集合只是把最后一个节点移到了第一个,React 会把前面的节点依次移动,而 Vue 只会把最后一个节点移到第一个,总体上,Vue 的方式比较高效;
-
React 在 diff 遍历时,只对需要修改的节点进行了记录,形成了 effect list,最后才会根据 effect list 进行真实 DOM 的修改,修改时先删除,然后更新与移动,最后插入,而 Vue 在遍历时就用真实 DOM 的 insertBefore 方法修改了真实 DOM,最后做的删除操作。
10、聊一聊渐进式框架。
- 灵活性和可扩展性:渐进式框架允许开发者根据项目需求逐步引入和使用框架的不同特性和功能,可以根据需求进行定制化,并根据项目发展需要来扩展框架的功能;
- 学习曲线降低:通过将整体框架划分为多个可选的模块或特性,使得开发者可以逐步学习和应用不同的功能,降低了学习和掌握框架的难度;
- 性能优化:由于只引入所需的功能模块,渐进式框架可以避免不必要的复杂性和性能开销,可以更加精确地控制和优化框架的使用,提供更高效的开发和执行速度;
- 代码重用:渐进式框架通常会提供一些可复用的组件和工具,帮助开发者提高开发效率,减少开发过程中的重复劳动,同时还能保持代码的一致性和可维护性。
11、如何实现图片懒加载?
// 方式1:监听滚动事件
const images = [...document.querySelectorAll('img')];
window.addEventListener('scroll', lazyLoad);
function lazyLoad(e) {
const clientHeight = document.documentElement.clientHeight;
const scrollTop = document.documentElment.scrollTop;
for (let i = 0;i < images.length;i ++) {
if (images[i].offsetTop < clientHeight + scrollTop) {
images[i].setAttribute('src', images[i].getAttribute('data-src'));
}
}
}
// 方式2:通过 getBoundingClientRect() 判断元素是否出现在视口内
function lazyLoad() {
let viewHeight = document.documentElement.cilentHeight || document.body.clientHeight;
for (let i = count;i < num;i ++) {
// getBoundingClientRect() 方法返回元素相对视口左上角的偏移量以及元素本身长宽
if (img[i].getBoundingClientRect().top < viewHeight) {
if (img[i].getAttribute("src") !== "default.jpg") continue;
img[i].src = img[i].getAttribute("data-src");
count ++;
} else {
break;
}
}
}
// 方式3:通过 element-ui 的 <el-image> 组件的 lazy 属性开启懒加载功能
<el-image v-for="url in urls" :key="url" :src="url" lazy></el-image>
// 方式4:通过 vue-lazyLoad 插件
// main.js
import VueLazyload from 'vue-lazyload'
app.use(VueLazyload, {
preload: 1.3, // 预加载宽高比 loading
loadingimage: "", // 加载状态下显示的图片
error: "", // 加载失败状态下显示的图片
attempt: 1 // 加载失败后最大尝试次数
})
// xxx.vue
<img v-lazy="item" width="600" height="200" alt="" />
12、watch 和 computed 的区别。
- computed 是计算属性,watch 是监听数据变化;
- computed 支持缓存,当其依赖的属性的值发生变化时,计算属性会重新计算,反之则使用缓存中的属性值,watch 不支持缓存,当对应属性发生变化时相应执行;
- computed 不支持异步,有异步操作时无法监听数据变化,watch 支持异步操作;
- computed 第一次加载时就监听,watch 默认第一次加载时不监听;
- computed 中的函数必须调用 return,watch 不是。
使用场景:
- computed:一个数据受多个数据影响,如购物车商品结算;
- watch:一个数据影响多个数据,如搜索数据、数据变化响应、执行异步操作或高性能消耗的操作。
13、对组件缓存有什么了解?
<keep-alive>包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们,与<transition>相似,是一个抽象组件,自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
对于被<keep-alive>包裹的组件,生命周期:
- 第一次进入:created -> mounted -> activated
- 离开:deactivated
- 再一次进入:activated
如何使用:
<keep-alive>
<loading></loading>
</keep-alive>
属性:
- include:只要名称匹配的组件会被缓存;
- exclude:任何名称匹配的组件都不会被缓存;
- max:控制最多可以缓存多少组件实例,一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉;
- meta:
-
export default[ { path: '/', name: 'home', components: Home, meta: { keepAlive: true } // 需要被缓存的组件 }, { path: '/book', name: 'book', components: Book, meta: { keepAlive: false } // 不需要被缓存的组件 } ]
缓存后如何获取数据:
- beforeRouteEnter:每次组件渲染时,都会执行 beforeRouteEnter,每次进入路由执行都会获取数据;
- activated:在 keep-alive 缓存的组件被激活时,都会执行 activated 钩子进行获取数据。
14、Vue 的生命周期有哪些?
- beforeCreate:组件实例被创建之初,执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务;
- created:组件实例已经完全创建,初始化完毕,各种数据可以使用,常用于异步数据获取;
- beforeMount:组件挂载之前,未执行渲染、更新,dom 未创建;
- mounted:组件挂载到实例上去之后,初始化结束,dom 已创建,可用于获取访问数据和 dom 元素;
- beforeUpdate:组件数据发生变化,更新之前,可用于获取更新前各种状态;
- updated:组件数据更新之后,所有状态已是最新;
- beforeDestroy:组件实例销毁之前,可用于一些定时器或订阅的取消;
- destroyed:组件实例销毁之后,作用同上;
- activated:keep-alive 缓存的组件激活时调用;
- deactivated:keep-alive 缓存的组件停用时调用;
- errorCaptured:捕获一个来自子孙组件的错误时被调用。
15、v-if 和 v-for 可以放一起用吗?
对于 Vue2 而言,v-if 和 v-for 同时放在同一个元素上,会带来性能方面的浪费(每次渲染都会先循环再进行条件判断),因为 v-for 的优先级高于 v-if。
对于 Vue3 而言,v-if 和 v-for 是可以一起用的,因为更新了优先级,v-if 优先级高于 v-for。
16、v-if 和 v-show 的区别。
- v-if 通过控制 dom 节点的存在与否来控制元素显影,而 v-show 通过设置 dom 元素的 display 样式来控制元素显影;
- v-if 切换先销毁再重建内部监听事件和子组件,而 v-show 只是简单的 css 切换;
- v-if 在切换方面耗能高,v-show 在初始渲染方面耗能高。
17、有用过 nextTick() 吗?
nextTick() 即在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,获取更新后的 DOM。
原理:
- 把回调函数放入 callbacks 等待执行;
- 将执行函数放到微任务或宏任务中;
- 事件循环到了微任务或宏任务,执行函数依次执行 callbacks 中的回调。
CSS
1、实现居中布局有多少种方式?
- 使用 Flexbox:
-
// center 是 div 盒子的 class 类名 .center { display: flex; justify-content: center; align-items: center; }
-
- 使用 position 和 transform:
-
.center { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); }
-
- 使用 line-height(将其值等于 height 属性值)和 text-align:
-
.center { line-height: 200px; height: 200px; // 可以不设置 text-align: center; }
-
-
使用 padding 和 text-align:
-
.center { padding: 50px 0; text-align: center; }
-
2、伪类和伪元素有什么区别?
- 伪类本质上是给元素创建多一个类名,多个类名多个效果,而伪元素本质上是创建了一个有内容的虚拟容器,也就是多了一个 DOM 节点。
- CSS3 中伪类和伪元素的语法不同,伪元素一般是::,而伪类是:。
- 可以同时使用多个伪类(中间不要有空格),但只能同时使用一个伪元素。
3、如何实现一个两列布局,左边宽度为200px,右边撑满剩下空间。
<div class="container">
<div class="left">左侧内容</div>
<div class="right">右侧内容</div>
</div>
- Flex 布局:
.container {
display: flex;
}
.left {
width: 200px;
}
.right {
flex: 1;
// 右侧内容超出的话,会导致左侧固定的宽度大小不起作用了,需要加上 min-width: 0
min-width: 0;
}
- Grid 布局:
.container {
display: grid;
grid-template-columns: 200px 1fr;
}
- Float 布局:
.left {
float: left;
width: 200px;
}
.right {
margin-left: 200px;
width: auto;
}
4、关于 Grid 布局。
同时在两个维度上把元素按行和列排列整齐,属性如下:
- grid-template-colums/grid-template-rows:设置列宽/行高,可以使用 repeat(重复次数, 重复的值) 函数简写重复的值;
- grid-row-gap/grid-column-gap/grid-gap:设置行/列间距;
- grid-template-areas:定义区域,一个区域由一个或多个单元格组成;
- grid-auto-flow:决定元素顺序;
- justify-items/align-items/place-items:设置单元格内容水平/垂直位置;
- justify-content/align-content/place-content:指定隐式网格的宽高;
- grid-auto-rows/grid-auto-columns:指定网格项目所在的四个边框;
- grid-area:指定项目放在哪一个区域;
- justify-self/align-self/place-self:设置单个单元格内容的水平/垂直位置。
5、常用单位有哪些?
- 绝对单位:
- px:逻辑像素,px 是图像显示的基本单元,是 viewport 像素,是相对单位(同样都是1px,在不同设备上显示有可能是不一样的,这里认为是绝对单位在于 px 的大小和元素与其他属性无关)。
- 相对单位:
- em:相对于父元素的 font-size 来计算的,在没有任何 CSS 规则的前提下,1em 的长度是16px,当 em 单位设置了 font-size 属性后,会逐级向上相乘;
- rem:相对于根元素<html>的 font-size 来计算的,不用像 em 一样使用级联的方式来计算尺寸;
- vw:视窗宽度的百分比(1vw 代表视窗的宽度为 1%);
- vh:视窗高度的百分比;
- vmin:当前 vm 和 vh 中较小的一个值;
- vmax:当前 vm 和 vh 中较大的一个值;
- %:相对于父元素尺寸来计算的。
6、了解过回流与重绘吗?
- 回流:布局引擎会根据各种样式计算每个盒子在页面上的大小与位置;
- 触发回流的情况:
- 添加或删除可见的 DOM 元素;
- 元素的尺寸发生变化(包括 margin、padding、border、height 和 width 等);
- 元素的位置发生变化;
- 元素的内容发生变化,如文本变化、图片被另一个不同尺寸的图片所替代等;
- 页面一开始渲染的时候(这个无可避免);
- 浏览器的窗口尺寸变化(因为回流是根据视口大小来计算元素的位置和大小的);
- 获取一些特定属性,如 offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle计算得到的值等。
- 如何减少回流:
- 通过改变元素的 class 类名设定元素样式;
- 避免设置多项内联样式;
- 避免使用 table 布局,table 中每个元素的大小以及内容的改动都会导致整个 table 重新计算;
- 对于复杂的动画设置 postion: fixed/absolute,尽可能地使元素脱离文档流,从而减少对其它元素的影响;
- 使用 CSS3 硬件加速,可以让 transform、opacity、filters 这些动画不会引起回流重绘;
- 避免使用 CSS 的 JavaScript 表达式。
- 重绘:当计算好盒模型的位置、大小及其它属性后,浏览器会根据每个盒子特性进行绘制。
- 触发重绘的情况:
- 触发回流;
- 颜色的修改;
- 文本方向的修改;
- 阴影的修改。
7、了解盒模型吗?
- W3C 标准的盒子模型(标准盒模型)
- width 为 content 的宽度,height 为 content 的高度;
- 盒子大小 = content + padding + border + margin。
- IE 标准的盒子模型(怪异盒模型)
- width 为 content、border、padding 的总宽度,height 为 content、border、padding 的总高度;
- 盒子大小 = content + margin。
/*
* 用 box-sizing 属性去设置如何计算一个元素的总宽度和总高度
* content-box:默认值,与标准盒子模型表现一致
* border-box:与怪异盒子模型表现一致
* inherit:从父元素继承
*/
box-sizing: content-box | border-box | inherit
JavaScript
1、ES6新特性有哪些?
- 类型修饰符方面:
- let:不存在变量提升,声明前该变量不能用(会报错),只在块级作用域内生效,不允许在同一作用域内重复声明;
- const:拥有 let 所有特点,声明的是常量(必须初始化且值不可变,声明的是对象的话可以修改对象的属性值)。
- 基本数据类型方面:
- symbol:独一无二且不可变,解决全局变量冲突、内部变量覆盖问题;
- bigInt:任意精度整数,安全存储和操作大数。
- 函数方面:
- 函数默认值:若函数没有传参,参数(必须是尾参数)为默认值;
- 箭头函数:更加简洁,内部没有 arguments 和 prototype,所以不能用 new 调用,且内部 this 永远指向父作用域(定义时所在的对象)的 this,不能使用 yield 命令所以不能作为 Generator 函数;
- length:返回没有指定默认值的参数个数,rest 参数也不会计入 length 属性;
-
(function (a) {}).length; // 1 (function (a = 1) {}).length; // 0 (function (...arg) {}).length; // 0 (function (a, b, c = 1) {}).length; // 2 (function (a, b = 1, c) {}).length; // 1 (function (a = 1, b, c) {}).length; // 0
-
- name:返回函数名,Function 构造函数返回的函数实例 name 属性值为 anonymous,bind 返回的函数,name 属性会加上 bound 前缀;
- 严格模式:只要函数设置了默认值、解构赋值、扩展运算符,那么函数内部就不能显示设定为严格模式,会报错。
- 数组方面:
- fill():用给定的值填充数组;
- flat():将嵌套数组拉平,默认拉平一次,想全拉平传入 Infinity;
- flatMap():map() 和 flat() 的结合,先通过 map() 返回新数组,再 flat() 拉平一次;
- Array.from():将一个类数组/可遍历对象转换成真正的数组;
- 必须要有 length 属性指定数组长度,若没有转换后为空数组;
- 伪数组的对象属性名必须为数值型或字符串型数字。
- includes():判断数组是否包含某值,返回布尔值;
- find():遍历数组,在数组中找到第一个符合条件的值并返回,不存在返回 undefined;
- findIndex():和 find() 相同,只不过返回的是下标,不存在返回 -1;
- copyWithin():将指定位置的成员复制到其他位置(会覆盖原有成员);
-
// copyWithin(targer, start, end) // 1.target:必选,从该位置开始替换数据,如果为负数表示倒数 // 2.start:可选,从该位置开始读取数据,默认为0,如果为负数,从末尾开始计算 // 3.end:可选,从该位置前停止读取数据,默认为数组长度 [1, 2, 3, 4, 5].copyWithin(0, 3); // [4, 5, 3, 4, 5]
-
- Array.of():将一组数值转化为数组,没有参数时返回空数组,参数只有一个时,实际上是指定数组长度。
-
Array.of(); // [] Array.of(3); // [, , ,] Array.of(3, 11, 8); // [3, 11, 8]
-
- 对象方面:
- Map:存储键可以是任意值,以[键,值]形式存储;
- size:返回 Map 结构的成员总数;
- set():设置键名 key 对应的键值为 value,如果 key 已经有值,值会被更新,否则就新生成该键,返回整个 Map 结构;
- get():读取 key 的键值,找不到 key 返回 undefined;
- has():判断某个键是否存在,返回布尔值;
- delete():判断是否删除成功,返回布尔值;
- clear():清除所有成员,无返回值。
- Set:存储不重复的值,以[值,值]形式存储;
- add():添加某个值(不会处理重复添加),返回 Set 结构本身;
- delete():删除某个值,返回布尔值;
- has():判断某个值是否存在,返回布尔值;
- clear():清除所有成员,无返回值。
- WeakSet:和 Set 的区别是没有遍历操作 API 和 size 属性,而且成员只能是引用类型;
- WeakMap:和 Map 的区别是没有遍历操作 API 和 clear 情况方法,只接受对象作为键名,WeakMap 的键名所指向的对象,一旦不再需要,里面的键名对象和所对应的键值对会自动消失,不需要手动删除引用;
- Object.is():判断两个值是否相等;
- Object:assign():合并两个对象,返回目的对象(相同的属性覆盖);
- Object.keys():返回对象的所有属性;
- Object.values():返回对象的所有属性值;
- Object.entried():返回对象的所有属性和属性值;
- Object.getOwnPropertyNames(obj):返回一个数组,包括对象自身的所有属性(除了 Symbol 属性,但包含不可枚举属性)的键名;
- Object.getOwnPropertySymbols(obj):返回一个数组,包括对象自身的所有 Symbol 属性的键名;
- Reflect.ownKeys(obj):返回一个数组,包括对象自身(不含继承的)的所有(包括 Symbol、字符串、不可枚举的)键名;
- 属性简写:
-
const baz = { foo: foo }; // 等同于 const baz = { foo };
-
- 函数简写:
-
const o = { method: function() { return "hello"; } } // 等同于 const o = { method() { return "hello"; } } new o.method(); // 报错,不能用作构造函数
-
- 属性名表达式:
-
let lastWord = 'last word'; const a = { 'first word': 'hello', [lastWord]: 'world' }; let obj = { ['h' + 'ello']() { return 'hi'; } }; console.log(a['first word']); // hello console.log(a[lastWord]); // world console.log(a['lastWord']); // world console.log(obj.hello()); // hi
-
- Map:存储键可以是任意值,以[键,值]形式存储;
- 解构表达式:
-
// 数组解构 let [a, b, c] = [1, 2, 3]; // a = 1, b = 2, c = 3 let [a, b, c] = [1, 2]; // a = 1, b = undefined, c = 2 let [a, , b] = [1, 2, 3]; // a = 1, b = 3 let [a, ...b] = [1, 2, 3]; // a = 1, b = [2, 3] // 对象解构 let obj = { name: 'xfs', age: 100, sex: 'female' }; let { name, age, sex } = obj; // name = 'xfs', age = 100, sex = 'female' let { name: myName, age: myAge, sex: mySex } = obj; // myName = 'xfs', myAge = 100, mySex = 'female'
-
- Proxy:用于创建一个对象的代理,从而实现基本的拦截和自定义(如属性查找、赋值、枚举、函数调用等)操作;
- 方法:
- Proxy.revocable():创建一个可撤销的 Proxy 对象。
- 语法:const proxy = new Proxy(target, handler)。
- target:要使用 Proxy 包装的目的对象(可以是任何类型的对象,包括原生数组、函数、甚至另一个代理);
- handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 proxy 的行为。
- get(target, propKey, receiver):拦截对象属性的读取;
- set(target, propKey, value, receiver):拦截对象属性的设置;
- has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值;
- deleteProperty(target, propKey):拦截 delete proxy[propKey]的操作,返回一个布尔值;
- ownKeys(target):拦截 Object.keys(proxy)、for...in 等循环,返回一个数组;
- getOwnPropertyDescriptor(target, propKey):拦截 Object.defineProperty(proxy, propKey, propDesc),返回一个布尔值;
- defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc),返回一个布尔值;
- preventExtensions(target):拦截 Object.preventExtensions(proxy),返回一个布尔值;
- getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个布尔值;
- isExtensible(target):拦截 Object.isExtensible(proxy),返回一个布尔值;
- setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值;
- apply(target, object, args):拦截 Proxy 实例作为函数调用的操作;
- construct(target, args):拦截 Proxy 实例作为构造函数调用的操作。
- 方法:
- Promise:异步编程的一种解决方法,构造函数是同步执行的,then 方法是异步执行的;
- 字符串方面:
- 模板字符串:通过“${}”来界定,“``”来完成;
- 方法:
- includes():判断字符串是否包含参数字符,返回布尔值;
- startsWith():判断字符串是否以参数字符串开头,返回布尔值;
- endsWith():判断字符串是否以参数字符串结尾,返回布尔值;
- repeat():按指定次数返回一个新字符串;
- padStart():按给定长度从前补全字符串(没有参数默认为空格),返回新字符串;
- padEnd():按给定长度从后补全字符串。
- class:创建类和继承(子类必须在构造函数 constructor 里调用 super());
- 模块化:防止命名冲突、复用性强;
- 通过 import 关键字导入模块;
- 通过 export 和 export default 关键字导出模块。
- 运算符:
- ...:扩展运算符,常用于拷贝和合并,用于数组赋值时,只能放在参数的最后一位,否则会报错;
- ?.:可选运算符;
- :::函数绑定运算符。
2、介绍一下 Promise。
- 状态(一旦改变就不能再变):pending(初始状态)、fullfilled(成功状态)、rejected(失败状态);
- 只能由 pending -> fulfilled 或 pending -> rejected。
- 方法:
- then():函数回调执行,用于接收请求返回的数据,有两个参数,一个用于处理 Promise 成功时的回调函数,一个用于处理 Promise 拒绝时的回调函数,返回值也是 Promise 对象;
- catch():处理在 Promise 拒绝时的回调函数,返回值为 Promise 对象,同时也用来捕获代码异常和出错;
- finally():与 Promise 状态无关,不接受任何参数,也没有返回值,无论如何都会执行;
- resolve()(静态方法):用于创建一个成功状态的 Promise 对象;
- reject()(静态方法):用于创建一个失败状态的 Promise 对象;
- all()(静态方法):接收一个 Promise 数组,当数组内全部 Promise 对象没有出现 rejected 状态,则会在全部 resolve 成功后执行回调,一旦存在一个是 rejected 状态的,会立即执行异常函数;
- allSetteld()(静态方法):接收一个 Promise 数组,当数组内全部 Promise 对象状态都确定后(不管成功还是拒绝),执行回调并返回一个对象数组{[ state: xx, value/reson(成功状态是 value,失败状态是 reson): xx ]};
- race()(静态方法):接收一个 Promise 数组,当任何一个 Promise 对象状态被确定时,立即执行回调函数;
- any()(静态方法):接收一个 Promise 数组,只要数组内有一个 Promise 对象执行成功就会返回已经成功执行的 Promise 结果,若全部为 rejected 状态,则会在最后的 Promise 对象执行完成后,全部的 Promise 返回到异常函数中。
3、如何自己实现 Promise.all() 方法?
const myPromiseAll = (promiseArray) => {
return new Promise((resolve, reject) => {
const resArray = []; // 保存返回值
promiseArray.forEach((promiseItem, promiseIndex) => {
promiseItem.then(res => {
resArray.push(res);
if (promiseIndex === promiseArray.length - 1) {
resolve(resArray); // 全部状态都为成功才执行 resolve
}
}, error => {
reject(error);
});
});
});
};
4、防抖在项目中的实现。
- 搜索输入框:用户在输入时,连续触发 keyup 事件,只有在输入结束后才发送请求;
-
<template> <el-input placeholder="在此搜索" v-model="searchJobType" @input="handleSearchJobType" /> </template> <script setup> import { ref } from 'vue'; import { debounce } from 'lodash/function'; // 借助 lodash 库 import { recruiterApi } from '../../api/app.js'; // 封装请求的 api const searchJobType = ref(''); // 搜索框输入的职业类型 const jobTypes = ref([]); // 职位类型信息 // 处理搜索职业类型 const handleSearchJobType = debounce((val) => { if (val.length > 0) { // 输入框有输入 recruiterApi.searchJobTypeByName({ size: 99, page: 1, name: val }).then(res => { job_types.value = response.data; }).catch(error => { console.error(error); }); } }, 500); </script>
-
- 窗口调整:用户调整浏览器窗口大小时,频繁触发 resize 事件,防抖可以确保调整结束后再执行相应操作;
-
import { debounce } from 'lodash/function'; // 借助 lodash 库 // 窗口调整时的处理函数 const handleResize = () => { // 在这里添加调整结束后执行的操作 }; // 使用防抖函数包装处理函数 const resizeDebounce = debounce(handleResize, 500); // 监听窗口的 resize 事件 window.addEventListener('resize', resizeDebounce); // 在销毁钩子移除监听事件 onUnmounted(() => { window.removeEventListener('resize', resizeDebounce); })
-
- 表单验证:用户输入表单数据时,可以用防抖来减少频繁的验证请求。
-
<template> <div class="loginBox"> <!-- 登录表单 --> <el-input type="text" placeholder="手机号" v-model="loginUser.phone" /> <el-input type="password" placeholder="密码" v-model="loginUser.password" /> <el-button type="primary" @click="toLogin">登录</el-button> </div> </template> <script setup> import { debounce } from 'lodash/function'; // 借助 lodash 库 import { ref } from 'vue'; import { recruiterApi } from '../../api/app.js'; // 封装请求的 api import { useRouter } from 'vue-router'; // 登录信息 const loginUser = ref({ phone: '', password: '' }) const router = useRouter(); // 登录操作 const toLogin = debounce(() => { recruiterApi.login({ phone: loginUser.phone, password: loginUser.password }).then(res => { ElMessage({ message: '登录成功!', type: 'success' }); localStorage.setItem('recruiter', JSON.stringify(res.data)); // 存为全局变量 router.push('/recruiterMain'); // 跳转至主页 }).catch(error => { ElMessage({ message: '登录失败!', type: 'error' }); }); }); </script>
-
5、如何自己实现防抖?
function myDebounce(func, wait = 0) {
let timeId;
return function(...args) {
const _this = this;
clearTimeout();
timeId = setTimeout(() => {
func.apply(_this, args);
}, wait);
}
};
6、本地存储的方式。
- cookie:小型文本,一般大小不超过4KB,由一个名称、一个值和其他几个用于控制 cookie 有效期、安全性、适用范围的可选属性组成,解决 HTTP 无状态导致的问题;
- Expires:设置 cookie 过期时间;
- Max-Age:设置 cookie 失效前经过的秒数(优先级高于 Expires);
- Domain:指定 cookie 可以送达的主机名;
- Path:指定一个URL 路径,此路径必须出现在要请求的资源的路径中才可以发送 cookie 首部;
- Secure:标记为 Secure 的 cookie 只能通过 HTTPS协议加密过的请求发送给服务端。
- localStorage:
- 特点:
- 持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的;
- 存储的信息在同一域中是共享的;
- 当本页操作了 localStorage 的时候,本页面不会触发 storage 事件,但是别的页面会触发 storage 事件;
- 大小:5M(跟浏览器厂商有关)
- localStorage 本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,导致页面变卡;
- 受同源策略的限制。
- 缺点:
- 无法像 cookie 一样设置过期时间;
- 只能存入字符串,无法直接存对象。
- 特点:
- sessionStorage:类似于 localStorage,不同在生命周期,一旦页面关闭,sessionStorage 会删除数据;
- indexedDB:低级 API,用于客户端存储大量数据,该 API 使用索引来实现对数据的高性能搜索。
- 优点:
- 存储量理论上没有上限;
- 所有操作都是异步的,相比 localStorage 同步操作性能更高,尤其是数据量较大时;
- 原生支持存储 JS 的对象;
- 是个正经的数据库,意味着数据库能干的事它都能干。
- 缺点:
- 操作非常繁琐;
- 本身有一定门槛。
- 优点:
7、每种 token 存储方式的优缺点。
- Cookie:
- 优点:浏览器关闭后,cookie 仍然存在;
- 缺点:容易受到 CSRF(跨站请求伪造)攻击。
- SessionStorage:
- 优点:只在当前会话中存在,当用户关闭浏览器后,sessionStorage 中的数据会被清除;
- 缺点:如果用户在浏览器中打开新的标签页或窗口,那么新的页面将无法访问 sessionStorage 中的数据。
- LocalStorage:
- 优点:浏览器关闭后,数据仍然存在,且能在同一浏览器所有标签页和窗口中共享;
- 缺点:容易受到 XSS(跨站脚本)攻击。
8、了解 SEO 吗?
SEO 的全称是搜索引擎优化,或者说快照优化。
- 优势:相对于竞价来说,点击不收费,全天候展现,不存在同行之间的恶意点击或者无效点击,可以很好地帮助中小型企业摆脱高额的竞价推广成本,给了中小型企业一个与行业龙头同台竞争的机会,还能够进行常规的免费 SEO 诊断,确保网站 SEO 优化的方方面面都比较有利于排名;
- 劣势:上线周期比较长,如果关键词热度过高,可能过了行业旺季排名效果还没有出来;
- 优化方面:TDK、URL 链接、robots 文件、404自定义、301跳转、网站布局、常用标签、导航链接、网站地图、死链筛查、核心关键词及长尾词库建立等。
9、JS 的跨域问题。
如果两个页面的协议、域名或端口号有任何一个不同,就会发生跨域,解决方式如下:
- jsonp 请求:jsonp 的原理是利用<script>标签的跨域特性,可以不受限制地从其它域中加载资源,类似的标签还有<img>;
- document.domain:这种方式用在主域名相同子域名不同的跨域访问中;
- window.name:window 的 name 属性有一个特征——在一个窗口的生命周期内,窗口载入的所有页面都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因为新页面的载入而进行重置;
- window.postMessage:window.postMessage 是 HTML5 中实现跨域访问的一种新方式,可以使用它来向其它的 window 对象发送消息,无论这个 window 对象是属于同源或不同源;
- CORS:CORS 背后的基本思想是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是应该失败;
- Web Sockets:Web Scockets 原理——在 JS 创建了 Web Sockets 之后,会有一个 HTTP 请求发送到浏览器以发起连接,取得服务器响应后,建立的连接会使用 HTTP 升级从 HTTP 协议交换为 Web Sockets 协议。
10、介绍一下 JS 的数据类型有哪些以及它们的区别。
-
基本数据类型:存储在栈中,占用的内存较小,可以直接操作它们的值,而且是按值传递的,包括 String、Number、Boolean、Null、Undefined、Symbol 等;
-
引用数据类型:存储在堆中,占用的内存较大,不能直接操作它们的值,而是需要通过引用访问它们的属性和方法,它们的赋值和比较也是按引用进行的,包括 Object、Array、Function等。
11、怎么区分引用数据类型?
- instanceof:在跨 iframe 的场景中会出现问题;
- Object.prototype.toString.call();
- constructor:容易受到原型篡改的影响;
- _proto_和 prototype。
12、讲解一下原型。
函数(或构造函数)身上才有 prototype(原型,是一个对象),而其它任何通过构造函数实例化出来的对象(不包括 null、undefined)身上都有_proto_,实例化对象的_proto_就是构造函数的 prototype(===关系),undefined 和 null 既没有 prototype 也没有_proto_,因为它们俩既不是函数也不是函数实例化出来的对象。
13、介绍一下引用对象深拷贝、浅拷贝、赋值的区别。
- 深拷贝:复制并创建一个一模一样的对象,不共享内存,修改新对象,旧对象保持不变;
- 浅拷贝:创建一个对象,如果是基础类型,那么就复制基础类型的值,如果是引用类型,那么就赋值指向某个对象的指针,改变新的对象原来的对象也会改变;
- 赋值:将一个对象赋值给一个新的对象时,赋的其实是对象的地址而不是值,两者的改变会互相影响,不管改变的是基础类型还是引用类型的值。
14、==和===的区别是什么?
==会对不同类型的值做类型转换,再进行比较,而===不会做类型转换(所以,除了比较对象属性为 null 或 undefined 的情况下,可以使用==,其他情况建议用===)。
15、(手撕)合并两个数组。
const a = [
{
id: 1,
// 其它属性省略
},
{ id: 2 },
{ id: 3 }
];
const b = [
{ id: 1 },
{ id: 2 },
{ id: 4 },
{ id: 5 }
];
const combinedArray = [...a, ...b].reduce((previous, current) => {
const exist = previous.find(item => item.id === current.id);
// 判断 id 是否存在,存在合并,不存在添加
if (exist) {
Object.assign(exist, current);
} else {
previous.push(current);
}
return previous;
}, []);
16、(手撕)实现选择排序。
function selectionSort(arr) {
for (let i = 0;i < arr.length - 1;i ++) {
for (let j = i + 1;j < arr.length;j ++) {
if (arr[i] > arr[j]) {
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
17、了解闭包吗?
闭包,即有权访问另一个作用域的变量的函数;闭包函数,即声明在函数中的函数。
特点:
- 局部变量会常驻在缓存中;
- 让外部访问函数内部变量成为可能;
- 可以避免使用全局变量,防止全局变量污染;
- 会造成内存泄漏(有一块内存长期被占用而不被释放)。
作用:
- 封装私有变量;
-
function addFunc() { let count = 1; return function() { count ++; console.log(count); } } var acc = addFunc(); acc(); // 2 acc(); // 3 - 做缓存;
- 模块化编程;
- 防抖、节流、函数柯里化。
HTML
1、介绍 HTML 标签的分类。
- 块级元素:独占一行,可以直接控制宽度、高度及盒子模型的相关 CSS 属性,在不设置宽度的情况下,块级元素的宽度是它父级元素内容的宽度,在不设置高度的情况下,块级元素的高度是它本身内容的高度,常见的块级元素有 div、h1~h6、hr、menu、ol、ul、li、dl、dt、dd、tabel、p、form 等;
- 内联元素:和其它内联元素从左到右在一行显示,不能直接控制宽度、高度以及盒子模型的相关 CSS 属性,但是直接设置内外边距的左右值是可以的,宽度由本身内容大小决定,只能容纳文本或者其它内联元素,常见的内联元素有 span、a、b、strong、i、em、s、strike、del、br、u、textarea、input、select、label、img、sub、sup、big、small 等。
2、HTML5 新特性。
- 用于绘画的 canvas 元素;
- 用于媒介回放的 viedo 和 audio 元素;
- 对本地离线存储的更好的支持(localStorage、sessionStorage);
- 新的特殊内容元素(语义化标签),如 article、footer、header、nav、section;
- 新的表单控件,如 calendar、date、time、email、url、search。
HTTP
1、介绍以下 HTTP 常见状态码。
- 1xx(信息性状态码):表示接收的请求正在处理;
- 100(Continue):继续,客户端应继续其请求;
- 101(Switching Protocols):切换协议,服务器根据客户端的请求切换协议,只能切换到更高级的协议,例如切换到 HTTP 的新版本协议。
- 2xx(成功状态码):表示请求正常处理完毕;
- 200(OK):请求成功,一般用于 GET 与 POST 请求;
- 201(Created):已创建,成功请求并创建了新的资源;
- 202(Accepted):已接受,已经接受请求,但未处理完成;
- 203(Non-Authoritative Information):非授权信息,请求成功,但返回的 meta 信息不在原始的服务器,而是一个副本;
- 204(No Content):无内容,服务器成功处理,但未返回内容,在未更新网页的情况下,可确保浏览器继续显示当前文档;
- 205(Reset Content):重置内容,服务器处理成功,用户终端(例如浏览器)应重置文档视图,可通过此状态码清除浏览器的表单域;
- 206(Partial Content):部分内容,服务器成功处理了部分 GET 请求。
- 3xx(重定向状态码):需要后续操作才能完成这一请求;
- 300(Multiple Choices):多种选择,请求的资源可包括多个位置,相应可返回一个资源特征与列表用于用户终端选择;
- 301(Moved Permanently):永久移动,请求的资源已被永久的移动到新 URI,返回信息会包括新的 URL,浏览器会自动定向到新的URI,今后是任何新的请求都应使用新的 URI 代替;
- 302(Found):临时移动,与301类似,但资源只是临时被移动,客户端应继续使用原有 URI;
- 303(See Other):查看其他地址,与301类似,使用 GET 和 POST 请求查看;
- 304(Not Modified):未修改,服务器返回此状态码时,不会返回任何资源,客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源;
- 305(Use Proxy):使用代理,资源必须通过代理访问;
- 307(Temporary Redirect):临时重定向,与302类似,使用 GET 请求重定向。
- 4xx(客户端错误状态码):表示请求包含语法错误或无法完成;
- 400(Bad Request):客户端请求的语法错误,服务器无法理解;
- 401(Unauthorized):请求要求用户的身份认证;
- 402(Payment Required):保留,将来使用;
- 403(Forbidden):服务器理解客户端的请求,但是拒绝执行此请求;
- 404(Not Found):服务器无法根据客户端的请求找到资源,通过此码,网站设计人员可设置“您所请求的资源无法找的”的个性页面;
- 405(Method Not Allowed):客户端请求中的方法被禁止;
- 406(Not Acceptable):服务器无法根据客户端请求的内容特性完成请求;
- 407(Proxy Authentication Required):请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权;
- 408(Request Time-out):服务器等待客户端发送请求事件过长,超时;
- 409(Confilct):服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突;
- 410(Gone):客户端请求的资源已经不存在,410不同于404,如果资源以前有现在被永久删除了可使用410,网站设计人员可通过301代码指定资源的新位置;
- 411(Length Required):服务器无法处理客户端发送的不带 Content-Length 的请求信息;
- 412(Precondition Failed):客户端请求信息的先决条件错误;
- 413(Request Entity Too Large):由于请求的实体过大,服务器无法处理,因此拒绝请求,为防止客户端的连续请求,服务器可能会关闭连接,如果只是服务器暂时无法处理,则会包含一个 Retry-After 的响应信息;
- 414(Request-URI Too Large):请求的 URI 过程,服务器无法处理;
- 415(Unsupported Media Type):服务器无法处理请求附带的媒体格式;
- 416(Requested range not satisfiable):客户端请求的范围无效;
- 417(Expectation Failed):服务器无法满足请求头中的 Expect 字段指定的预期行为。
- 500(服务器错误状态码):服务器在处理请求的过程中发生了错误。
- 500(Internal Server Error):服务器内部错误,无法完成请求;
- 501(Not Implemented):服务器不支持请求的功能,无法完成请求;
- 502(Bad Gateway):作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应;
- 503(Service Unavailable):由于超载或系统维护,服务器暂时无法处理客户端的请求,延时的长度可包含在服务器的 Retry-After 头信息中;
- 504(Gateway Time-out):充当网关或代理的服务器,未及时从远端服务器获取请求。
- 505(HTTP Version not supported):服务器不支持请求的 HTTP 协议的版本,无法完成处理。
2、HTTP 和 HTTPS 的区别是什么?
-
HTTP 明文传输,数据都是未加密的,安全性较差,HTTPS 数据传输过程是加密的,安全性较好;
-
使用 HTTPS 需要到 CA(数字证书认证机构)申请证书,一般免费证书较少,因而需要一定费用;
-
HTTP 页面响应速度比 HTTPS 快,主要因为 HTTP 使用 TCP 三次握手建立连接,客户端和服务器需要交换3个包,而 HTTPS 除了 TCP 的三个包,还要加上 SSL 握手需要的9个包,所以一共是12个包;
-
HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,HTTP 是80,HTTPS 是443;
-
HTTPS 其实就是构建在 SSL/TLS 上的 HTTP 协议,所以要比 HTTP 更耗费服务器资源。
3、GET 和 POST 的区别是什么?
- GET 参数通过 URL 传递,POST 放在 request body 中;
- GET 在 URL 中传递的参数是有长度限制的,而 POST 没有;
- GET 比 POST 更不安全,因为参数直接暴露在 URL 中,所以不能用来传递敏感信息;
- GET 只能进行 URL 编码,而 POST 支持多种编码方式;
- GET 浏览器会主动 cache,而 POST 不会;
- GET 请求参数会被完整保留在浏览历史记录里,而 POST 中的参数不会被保留;
- GET 和 POST 本质上就是 TCP 连接,并无差别,但由于 HTTP 的规定和浏览器/服务器的限制,导致它们在应用过程中体现出一些不同;
- GET 产生1个 TCP 数据包,POST 产生2个,对于 GET,浏览器会把 http header 和 data 一并发送出去,服务器响应200,而对于 POST,浏览器先发送 header,服务器响应100,浏览器再发送 data,服务器响应200。
4、了解过前端的缓存策略吗?
- HTTP 缓存:当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有请求资源的副本,就可以直接从浏览器缓存中提取而不是从原始服务器中提取;
- 强缓存:浏览器会直接从本地缓存中获取资源,并不会发送请求到服务器,当缓存过期才会执行协商缓存机制,可以有效减少网络请求,提升网页加载速度和用户体验,如果设置的缓存时间过长,资源的更新可能不会立即生效,导致用户无法获取最新的资源;
- 协商缓存:强缓存失效后,浏览器携带特定缓存标识(HTTP 请求头中的某些字段)向服务器发送请求,由服务器根据缓存标识决定是否使用缓存的过程,如果缓存可用则返回304以及在响应头中设置对应字段,否则返回新的资源以及200,可以在资源发生变化时及时更新缓存,减少不必要的网络传输和服务器负载,与强缓存一起使用可以提供更灵活和高效的缓存策略,以提高应用的性能和用户体验。
- 浏览器缓存
- 按缓存位置划分:
- Service Worker Cache:一种在浏览器后台运行的 JS 脚本,可以拦截和处理网页发出的网络请求,以及管理缓存和离线数据,可以让网页在离线状态下仍能正常访问,并且可以提高网页的性能和响应速度,它由开发者编写的额外脚本控制,且缓存位置独立;
- Memory Cache:存储在浏览器内存中,获取速度快、优先级高,从内存中获取资源耗时为0ms,但生命周期短,当网页关闭后内存就会释放,也受限制于计算机内存的大小;
- Disk Cache:存储在计算机硬盘中,生命周期长,不触发删除操作则一直存在,但获取资源速度相对 Memory Cache 较慢,会根据保存下来的资源的 HTTP 首部字段来判断是否需要重新请求,如果重新请求那便是强缓存的失效流程,否则是生效流程。
- 按缓存存储划分(前文有介绍这里不再赘述)
- Cookie
- Web Storage
- IndexedDB
- 按缓存位置划分:
5、浏览器从输入网址到回车,会发生什么?
- URL 解析:判断输入是否合法;
- DNS 查询:获取域名对应的目标服务器 IP 地址;
- TCP 连接:经历三次握手建立 TCP 连接,确保服务器和客户端之间可以进行通信;
- HTTP 请求:浏览器发送 HTTP 请求(包括请求行、请求头和请求主体)到目标服务器;
- 响应请求:服务器接收到浏览器的请求后,进行逻辑操作,处理完成后返回一个 HTTP 响应消息(包括状态行、响应头和响应正文);
- 页面渲染:浏览器接收到服务器响应的资源后,对资源进行解析并渲染,渲染过程如下:
- 解析 HTML,构建 DOM 树;
- 解析 CSS,生成 CSS 规则树;
- 合并 DOM 树和 CSS 规则树,生成 render 树;
- 布局 render 树,负责各元素尺寸、位置的计算;
- 绘制 render 树,绘制页面像素信息;
- 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成,显示在屏幕上。
6、TCP和UDP的区别。
| TCP | UDP | |
|---|---|---|
| 可靠性 | 可靠 | 不可靠 |
| 连接性 | 面向连接 | 无连接 |
| 报文 | 面向字节流 | 面向报文 |
| 效率 | 传输效率低 | 传输效率高 |
| 双共性 | 全双工 | 一对一、一对多、多对一、多对多 |
| 流量控制 | 滑动窗口 | 无 |
| 拥塞控制 | 慢开始、拥塞避免、快重传、快恢复 | 无 |
| 传输效率 | 慢 | 快 |
7、OIS 七层协议。
- 应用层:通过应用程序间的交互来完成特定的网络应用【报文】;
- 表示层:使通信的应用程序能够解释交换数据的含义;
- 会话层:建立、管理和终止表示层实体之间的通信会话;
- 传输层:为两台主机进程之间的通信提供服务,主要的传输层协议是 TCP 和 UDP【报文段】;
- 网络层:选择合适的网间路由和交换节点,确保数据按时成功传送【IP 数据报】;
- 数据链路层:将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传输帧【帧】;
- 物理层:实现计算机节点之间比特流的透明传输【比特流】。
8、HTTPS 是如何实现加密的?
HTTPS 的安全性是由 SSL 来保障的,SSL 实现加密主要依赖于以下手段:
- 对称加密:采用协商的密钥对数据加密;
- 加密和解密的密钥是同一个。
- 非对称加密:实现身份认证和密钥协商;
- 存在两个密钥,公钥(公开的)和私钥(保密的);
- 使用公钥加密必须由私钥解密,反之,使用私钥加密必须由公钥解密。
- 摘要算法:验证信息的完整性;
- 一种特殊的压缩算法,它能够把任意长度的数据“压缩”成固定长度且独一无二的“摘要”字符串;
- 如散列函数、哈希函数。
- 数字签名:身份验证。
- 用私钥加密,公钥解密。
算法
1、知道哪些排序算法?
- 冒泡排序:重复地走访要排序的数列,一次比较两个元素,如果它们顺序错误就把它们交换过来;
- 选择排序:在未排序序列中找到最小(大)元素,放在已排序序列的末尾;
- 插入排序:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入;
- 归并排序:将已有序的子序列合并,得到完全有序的序列;
- 快速排序:对冒泡排序的一种改进,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据要比另一部分的所有数据小,再对这两部分数据分别进行快速排序。
2、二分查找的理念是什么?时间复杂度是多少?
二分查找的搜索过程是从数组的中间元素开始的,如果中间元素正好是要查找的元素,则搜索结束,如果某一特定元素大于或小于中间元素,则在数组(有序的)大于或小于中间元素的那一半查找,同样从中间元素开始比较,如果某一步数组为空,则代表找不到。
因为每次比较都使搜索范围缩小一半,所以时间复杂度为 O(logn)。
function binarySearch(arr, target) {
let left = 0; // 数组左边界下标
let right = arr.length - 1; // 数组右边界下标
while (left <= right) {
let middle = Math.floor((left + right) / 2); // 中间元素下标
if (arr[middle] > target) {
right = middle - 1;
} else if (arr[middle] < target) {
left = middle + 1;
} else {
// 判断是否是重复项的第一个,是则返回,不是则继续往前查找
if (middle === 0 || arr[middle - 1] < target) return middle;
right = middle - 1;
}
}
return -1;
}
前端模块化
1、项目为什么要打包?
- 性能优化:通过合并文件,减少了 HTTP 请求的数量,从而加快页面加载速度;
- 代码优化:打包工具可以压缩和混淆代码,进一步减少文件的大小;
- 资源优化:能够处理各种资源(如图片、字体、样式)并将它们包含在最终的输出中;
- 转换&兼容性:可以将高级的 JavaScript(如 ES6+)或其它编程语言(如 TypeScript)转换为广泛支持的 ES5 代码;
- 依赖管理:确保代码按正确的顺序执行,满足模块间的依赖关系。
设计模式
1、了解过哪些设计模式?
- 发布订阅模式:消息的发布者不会将消息直接发送给特定的订阅者,而是将发布的消息分为不同的类别,通过消息代理和订阅者进行通信,无需了解哪些订阅者可能存在;
- 观察者模式:定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知并自动更新;
- 单例模式:保证一个类只有一个实例,并提供一个访问它的全局访问点,实现的方法先判断实例是否存在,如果存在则直接返回,否则创建了再返回;
- 工厂模式:通常分为三个角色,工厂负责实现创建所有实例的内部逻辑,抽象产品是所创建的所有对象的父类,负责描述所有实例所共有的公共接口,具体产品是创建目标,所有创建的对象都充当这个角色的某个具体类的实例;
- 装饰器模式:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法;
其他问题
1064

被折叠的 条评论
为什么被折叠?



