在JavaScript中,==和===操作符有什么区别?在你的项目中你通常选择使用哪一个,为什么?
这两个操作符都用于比较,但==会进行类型转换,而===不会。在我的项目中,我通常会使用===,因为这可以避免由于类型转换带来的潜在问题。
说说原型和原型链
原型和原型链的关系可以想象成一颗倒置的树,树根是null,树根的子节点就是 Object.prototype,然后依次通过 Prototype 属性链接下去,形成了一条原型链。这条原型链就是 JavaScript 在查找属性和方法时的查找路径。
JavaScript 中的所有对象都是由其原型继承而来,这个原型就像一个模板,提供一些公共的属性和方法,对象可以共享这些属性和方法。比如,我们创建一个数组,那么这个数组就会有一些方法,如 push,pop 等,这些方法就是从 Array 的原型上继承过来的。那么原型又是怎么来的呢?
我们访问一个对象的某个属性或方法,JavaScript 解释器首先在该对象自身属性中寻找,如果没有找到,那么它就会去它的原型对象([[Prototype]])中寻找,如果还没有找到,就继续去原型的原型中寻找,如此循环,直到找到该属性或方法,或者到达原型链的末端(null)。这样形成的链状结构就是所谓的原型链。
为了更直观地理解,你可以想象原型链像一条向上延伸的链条,链条的起点是你创建的对象,链条的终点是 null。当你访问对象的一个属性或者方法时,JavaScript 解释器就像沿着这条链条向上爬行,寻找你要的属性或方法。
说说 vue 的响应式原理
数据劫持
Vue 创建实例时,会把 data 中的属性挂在 Vue 实例上
然后通过 Object.defineProperty 方法定义 getter 和 setter,将这些属性转为 getter/setter,实现数据劫持。
依赖收集
当渲染 Vue 实例时,会访问其中的各个属性,从而触发 getter。
在 getter 中会进行依赖收集,也就是收集哪些地方用到了这个属性,形成一个依赖的列表。例如,在模板中使用了某个属性,在 getter 中会收集这个属性在模板中的依赖关系。
监听数据变化
当通过 Vue 实例修改属性的值时,会触发 setter。在 setter 中会通知之前在 getter 中收集的依赖进行更新,比如重新渲染模板等。这样就完成了数据的响应式。
在 Vue 3 中,这一响应式系统的实现被改进并优化,由 Object.defineProperty 改为了使用 Proxy,但大体原理仍然相似。
Vue3 和 Vue2 有什么区别?
- Composition API
- 性能优化
- 更好的TS支持
- 根不在唯一
Composition API
这可能是最大的区别
Vue 2 主要使用的是 Options API,我们需要按照 options 的类型(如 data、methods、computed 等)来组织代码。
而在 Vue 3 的 Composition API 中,我们可以按照逻辑关系来组织代码,这对于理解和复用代码非常有帮助。
性能优化
包括启用 tree-shaking 支持,减少框架本身的大小和更快的虚拟 DOM。
Vue 3.0 的更新速度和内存使用量都得到了显著的提升。
更好的TS支持
vue3.0 是用TS写的,所以对TS支持更好
多个根
2中只能有一个根,3可以有多个根,编写组件变得更加灵活
如何创建一个对象?
- 字面量创建
- new 关键字和构造函数
- Object.create()
字面量创建
只需要使用花括号即可,缺点就是代码不能复用,创建多个相同对象时比较繁琐
使用 new 关键字和构造函数
先定义一个构造函数,然后使用new关键字创建对象实例
缺点就是不够高效,构造函数的语法复杂一些,要清除理解 this 关键字和每个新建对象机制
Object.create()
创建一个新对象并指定新对象的原型,新对象的原型被设置为我们传入该方法的第一个参数。
let prototypeObj = {
sayHello: function() {
console.log("Hello!");
}
};
let myObj = Object.create(prototypeObj);
myObj.sayHello(); // 打印出 "Hello!"
这样做的一个优点是,所有通过 Object.create(prototypeObj) 创建的对象都会共享相同的 sayHello 方法,而不是每个对象都有自己独立的 sayHello 方法。这可以帮助我们节省内存,并允许我们在原型对象上添加、修改或删除方法,这些更改将影响所有继承自该原型的对象。
当然旧版本浏览器中可能不支持这种写法
前端项目上线后白屏了怎么解决
1、查看控制台输出,看看有没有资源加载失败或者错误信息
2、检查网络请求是否出现问题
3、检查构建过程是否出现异常
4、检查生产环境版本及依赖是否和本地有较大差异
如果我们开发的是小程序或者app,没有浏览器那样的一些工具,我们可能要考虑其他方式
比如在关键代码处增加日志,app运行后,通过查看日志来定位问题
我们要检查模拟器和真机的差异,因为有的情况下模拟器和真机效果是不一致的
我们也可以尝试逐步注释代码,或者删减功能,逐步定位问题
uniapp 我们还可以使用比如 weex 或者 vconsole 来进行控制台输出日志查看问题
new 关键字做了什么?
当我们使用 new 关键字创建一个对象实例时
它会有以下过程:
1、JavaScript 会创建一个新的空对象
2、新对象的 proto 属性会被链接到指定的构造函数原型对象 prototype 上,这样就能访问构造函数原型上的所有属性和方法了
3、绑定 this,构造函数内部,this 会绑定到新建的对象上
4、返回新对象
合并一个数组有哪些方法?
- concat
- push(修改原数组!!)
- flat
- 展开运算符
concat
Array.prototype.concat 方法最常用也比较简单
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let merged = arr1.concat(arr2);
展开运算符
这是es6的新语法
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let merged = [...arr1, ...arr2];
flat
看起来有些复杂,但是很好用
let arr1 = [[1, 2, 3], [4, 5, 6]];
let arr2 = arr1.flat();
console.log(arr2); // 输出:[1, 2, 3, 4, 5, 6]
push
将第二组直接推入第一组
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
arr1.push(...arr2);
// arr1 现在是 [1, 2, 3, 4, 5, 6]
为什么 Vuex 的 mutation 中不能做异步操作?
假设你正在玩一个角色扮演游戏,你的角色现在有一项任务,那就是从村庄走到城堡。这个过程可以看作是一个“mutation”,因为你的角色的位置(我们可以看作是“状态”)将会改变。
现在想象,如果这个过程(即你的角色从村庄到城堡)是立即完成的(同步的),那么你可以很清楚地知道什么时候任务开始(你在村庄),什么时候任务结束(你在城堡),如果有其他任务(其他状态的改变)依赖于这个任务的完成,你也能够很好地安排他们的顺序。
但如果这个过程是异步的,就像你告诉你的角色去城堡,但是它可能立即走,也可能等一会儿再走,甚至中途还停下来做别的事情,那么你就无法确切地知道你的角色什么时候能到城堡,也就无法准确地安排其他依赖于这个任务的任务。并且,如果你的角色突然出现在城堡,你可能会感到困惑,因为你无法追踪它的行动,也无法理解它为什么会突然出现在那里。
这就是为什么 Vuex 的 mutation 需要是同步的原因。通过让 mutation 是同步的,我们可以确保我们的状态(角色的位置)始终是可预测和可追踪的,我们可以清晰地知道什么时候和为什么状态会改变,这对于理解和维护复杂的状态变化非常重要。
对于那些需要一些时间才能完成的任务(异步操作),例如需要去打个怪,或者需要去收集一些资源,这种情况下我们就可以使用 Vuex 的 action。你可以告诉你的角色去打怪(发出 action),你的角色会去打怪,然后当打怪完成后,通过 mutation 来更新你的角色的状态(例如,增加经验值)。这样,即使打怪的过程是异步的,你也可以通过 mutation 确保状态更新是同步和可追踪的。
说说你对虚拟 DOM 的了解,以及它的优缺点?
一个“虚拟”的节点树用JavaScript对象的形式存在,然后这个虚拟节点树将同步到“真实”的DOM节点树。它是一种抽象的概念,真正的数据解析、渲染、维护都是通过JavaScript来完成的,是JavaScript和真实DOM之间的一层桥梁。
它能够提高性能,状态变化导致视图变化的时候,我们不需要直接操作DOM,而是操作JS对象,大大提升了效率。
而且使得代码更简洁,易于维护。
它的缺点也很明显:
内存消耗会很大,因为要在内存中维护一颗完整的DOM树,频繁的生成和比较DOM要消耗一部分CPU资源
在第一次渲染时要构建完整的DOM树,比直接操作DOM要慢一些
依赖DOM的第三方库可能无法正常工作。
Git 常用命令
git init:初始化一个新的 Git 仓库git clone <url>:克隆一个远程仓库到本地git status:查看当前仓库的状态git add <file>:将一个文件添加到暂存区,准备提交git commit -m "commit message":提交暂存区的改动到仓库git push origin <branch>:将本地的分支推送到远程仓库git pull origin <branch>:将远程仓库的更新拉取到本地git checkout -b <new-branch>:创建一个新的分支并切换到新分支git checkout <branch>:切换到一个已存在的分支git branch -d <branch>:删除一个分支git merge <branch>:合并指定分支到当前分_branch
组件之间的通信方式有哪些?
- 父子通信
- 兄弟通信
- 跨级通信
- 无关系通信
- Refs 通信
父子通信
父组件向子组件传递数据通过 props,子组件向父组件传递数据通过 $emit 触发父组件的事件。
兄弟通信
通过共同的父组件作为桥梁。
子组件A通过 $emit 将数据传给父组件,然后父组件再通过 props 传给子组件B。
父组件在这里起到了中介的作用。可以想象成你的两个孩子想要相互传递东西,他们不能直接传,而需要你(父组件)作为中间人。
// Parent.vue
<template>
<ChildA @child-msg="handleMsg" />
<ChildB :msg="msg" />
</template>
// ChildA.vue
this.$emit('child-msg', 'Hello from ChildA')
// ChildB.vue
props: ['msg']
跨级通信
Vue 为了方便跨级组件之间的通信,提供了 provide 和 inject 的方式。父组件通过 provide 提供数据,然后在任何子孙组件中,都可以通过 inject 来接收数据。
这就像是你的孩子和你的孙子想要相互传递东西,他们需要一个更复杂的方式来传递,比如邮寄(provide/inject)。
缺点就是组件之间的依赖同样变得难以理解和维护。
// Parent.vue
provide() {
return {
parentMsg: 'Hello from Parent'
}
}
// GrandChild.vue
inject: ['parentMsg']
无关系通信
使用事件总线(Event Bus)或者状态管理库如 Vuex。
比如你和你的邻居(无关系的组件)要相互传递东西,你们可能需要用到邮局(Event Bus)或者快递服务(Vuex)。
但是缺点也比较明显,大型应用中,难以追踪和维护。
Refs
在父组件中直接操作子组件,子组件需要被标记为 ref,然后在父组件中可以通过 this.$refs.refName 来访问到。
这是一种非常直接的方式,你可以直接进入你的孩子的房间(子组件)并操作他的物品(数据、方法)。
// Parent.vue
<template>
<Child ref="childRef" />
</template>
this.$refs.childRef.someMethod()
我们通常不鼓励子组件直接操作父组件。Vue 组件的设计哲学是“单向数据流”,即数据从父组件流向子组件。
这种方式可以让组件之间的数据流更清晰、易于理解,也减少了数据在多个地方被修改带来的风险。
然而,子组件可以通过触发事件来通知父组件进行相应的操作。这是通过 this.$emit('event', payload) 在子组件中发射一个事件,然后在父组件中监听这个事件,并响应事件处理函数来实现的。
refs 这种直接操作的方式,非常灵活简便,但是增加了组件之间的耦合度,使得组件重用和测试变得困难。
了解动态路由吗?
插槽是什么?什么情况下需要使用插槽?
什么情况下会导致内存泄露?
如何解决内存泄露?
前端如何优化图片大小?
- 选择正确的格式
- CDN加速
- 懒加载
- 使用现代图片格式
- 响应式图片
- 图片精灵
正确的格式
PG格式适合于具有许多颜色和细节的图片,而PNG适合于具有固定颜色和简单形状的图片。
另外,如果可能的话,SVG是一个很好的选择,因为它是向量的,可以无损地缩放,并且文件大小通常较小。
压缩图像
使用图像压缩工具(如TinyPNG,ImageOptim等)或服务,可以有效地减小图像文件的大小,而不会显著降低其质量。
懒加载
一些在页面滚动过程中才会出现的图片,可以采用懒加载(lazy loading)的方式,即只有当图片进入视口时,才开始加载图片。这种方法可以显著提高页面的初始加载速度。
使用 CDN
使用内容分发网络(CDN)可以减少服务器的负载,并减小图片加载的延迟。因为CDN会将内容存储在距离用户更近的服务器上,从而减少了请求往返的时间。
使用现代图片格式
像WebP和JPEG XR这样的现代图像格式,提供了比传统的PNG和JPEG更好的压缩率,可以使图像在保持相同质量的情况下,大小更小。
响应式图片
根据设备的屏幕大小和分辨率提供不同大小的图片。这样,小设备就无需下载大图像,从而节省了带宽。
图片精灵
如果网站上有许多小图标和背景图片,可以考虑使用图片精灵。图片精灵将多个图片合并为一个图片,通过CSS来定位显示需要显示的部分,这样可以减少HTTP请求的数量。
你通常是怎么优化vue项目的 ?
- 路由懒加载
- 组件级别的懒加载
- keep-alive
- 使用官方的性能分析工具
- 优化 v-for
- 静态资源压缩
- 使用 SSR 预渲染
- 开启 gzip 压缩
- vuex 数据持久化
路由懒加载
通过把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,可以实现路由级别的代码分割。这种技术可以显著地提高大型应用的加载速度。
组件级别的懒加载
如果在某个路由页面有大量的组件,但并不是所有组件都会立刻用到,那么可以采用组件懒加载,即在需要的时候再加载组件。
使用 keep-alive
当组件切换频繁时,可以使用<keep-alive>来缓存组件的状态,这样用户在切换组件时可以保持原来的状态,同时也减少了渲染的次数,从而提高性能。
数据持久化
当有大量的状态需要管理时,可以使用vuex持久化插件,将状态保存到sessionStorage或localStorage中,这样用户在刷新页面时,状态不会丢失。
官方性能分析工具
Vue Devtools 包含了一个性能分析工具,可以帮助你发现可能的性能问题。
使用 SSR 或者预渲染
如果项目允许,可以考虑使用服务端渲染(SSR)或预渲染,这样可以提高首屏渲染速度,对SEO也更友好。
为什么避免同时使用 v-for 和 v-if ?
在 Vue.js 中,v-for 指令有比 v-if 更高的优先级!!!
这意味着 v-if 将会无论如何都在每次循环中执行。如果你的目标是有条件地跳过循环的执行,这样做将会浪费性能。因为无论条件最后是否得到满足,都会首先执行 v-for 循环。
例如,你可能想要渲染一个列表,但只在某个复杂计算的结果为真时才这样做。如果你在 v-for 和 v-if 同时使用,无论计算结果如何,v-for 循环都会运行。换句话说,你会先渲染列表,然后再根据 v-if 的结果去删除它们。这无疑会导致性能的浪费。
为了解决这个问题,你可以改为在一个计算属性中完成你的复杂计算,然后在 v-for 循环中使用这个计算属性,或者你可以把 v-if 放在一个包裹元素或者 <template> 元素上。
Proxy 与 Object.defineProperty 优劣对比
Vue 2.x 使用 Object.defineProperty 进行数据的双向绑定,Vue 3.x 则采用了 Proxy。
Object.defineProperty
在 ES5 及其以上版本都可以使用,兼容性较好。
缺点就是:
- 无法监控到数组长度的变化,导致通过数组长度改变数组元素无法实现响应式;
- 无法监听到对象属性的新增和删除;
- 深度监听需要递归到底,一次性计算量大。
Proxy
- Proxy 可以直接监听对象而非属性;
- Proxy 可以直接监听数组的变化;
- Proxy 有 13 种拦截方法,比如
has、ownKeys、deleteProperty、enumerate等等是 Object.defineProperty 不具备的; - Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,原对象完全不需要变动。
let target = { message: 'hello' };
let handler = {
get: function (target, prop, receiver) {
return 'world';
},
};
let proxy = new Proxy(target, handler);
console.log(proxy.message); // 'world'
console.log(target.message); // 'hello'
缺点也很明显,就是兼容性问题
Proxy 是 ES6 的特性,如果要兼容 IE11 以下的浏览器,就需要使用 Object.defineProperty。
HTML5新特性
引入了一些新的语义化标签,例如 <header>, <footer>, <nav>, <article>, <section>,和 <aside>。这些标签让我们能更好地描述网页的结构和内容,也更有利于 SEO 和网页的可访问性。
新增了 <audio> 和 <video> 标签,让我们能在网页上直接播放音频和视频,而无需依赖 Flash 或其他插件。
表单方面做了许多增强,例如新的输入类型(如 email, date, range, color)和新的表单元素(如 <datalist>, <output>, <progress>, <meter>)。
HTML5 引入了 <canvas> 标签,我们可以使用 JavaScript 在 <canvas> 上绘制图形和动画。此外,HTML5 还引入了 SVG,让我们能使用矢量图形来创建更高质量的图像和动画。
提供了本地存储,Web Storage(包括 localStorage 和 sessionStorage)和 IndexedDB,让我们能在用户的浏览器上存储和检索数据,提高应用的性能和用户体验。
还有一些性能优化方面,比如地理位置,文件API,web socket等等
熟悉vue的源码吗?
他们让我们编写更高效的 CSS
常用的功能包括:
- 嵌套规则
- 混合
- 变量
- 继承
- 运算
他们有什么区别呢?虽然他们都是CSS的超集,但是语法不同
SCSS更接近CSS原生语法,使用 ; 和 {}
而 Less 语法更接近于 JavaScript ,会使用()
社区规模上来说,Less社区比SCSS社区小很多,在寻找资源方面会差一些
Less 提供了一些方便的函数,比如 color 和 round,这些在 SCSS中需要自定义
谈谈你对 import 和 export 的理解
这是 ES6 模块系统的一部分
export 用于将变量,函数,对象或类从模块中导出,以便其他模块可以通过 import 语句来使用它们。
例如,如果我有一个模块,我想共享其中的一个函数,我可以这样做:
// math.js
export function add(x, y) {
return x + y;
}
// 在另一个模块就可以导入
// app.js
import { add } from './math.js';
console.log(add(2, 3)); // 输出 5
也可以使用 export default 来导出模块的默认值。
这在一个模块只导出一个值,例如一个函数或类时很有用:
// greet.js
export default function greet(name) {
return `Hello, ${name}!`;
}
接下来我们看看高阶用法
重命名导入
如果你导入的模块中有命名冲突,或者你希望更改导入模块的名称,可以使用 as 关键字来重命名:
// app.js
import { add as addNumbers } from './math.js';
console.log(addNumbers(2, 3)); // 输出 5
全部导入
如果你需要从一个模块中导入所有导出的值,你可以使用 * 来实现这一点:
// app.js
import * as math from './math.js';
console.log(math.add(2, 3)); // 输出 5
console.log(math.subtract(2, 3)); // 输出 -1 (假设 math.js 中也有 subtract 函数)
动态导入
某些时候,我们可能希望在运行时动态导入模块,尝尝在代码分割和懒加载时使用
// app.js
button.addEventListener('click', async () => {
const math = await import('./math.js');
console.log(math.add(2, 3)); // 输出 5
});
重新导出
有时候,你可能希望从一个模块中导出另一个模块的值。这可以通过 export ... from ... 来实现:
// index.js
export { add } from './math.js';
默认导入和命名导入的混合使用
// app.js
import defaultExport, { namedExport } from './module.js';
vuex 有哪些模块
state
这是存储的基本对象,我们要共享的状态就存储在这个对象里。
比如我们要记录用户的登录状态,我们就可以定义一个 user 对象:
state: {
user: {
name: '',
isLoggedIn: false
}
}
Getters
这是对于 state 中的数据进行计算后获取的
当依赖的 state 发生变化,getters 就会重新计算,比如我们要计算购物车中商品总价:
getters: {
totalPrice: state => {
return state.cart.reduce((total, item) => total + item.price, 0);
}
}
Mutations
用来更改 state 中的数据,所有的数据修改都要通过 Mutations
这样可以确保数据的一致性
比如我们现在要修改用户的登录状态:
mutations: {
setUser(state, payload) {
state.user = payload.user;
}
}
Actions
这是处理异步的地方,比如我们从服务器获取数据,就可以放在这里操作
actions 通过 commit 来调用 Mutations
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
在这个示例中,incrementAsync 是 action 名,它通过 commit 方法触发 increment mutation。然后在组件中,我们可以使用 dispatch 来触发 action:
methods: {
increment() {
this.$store.dispatch('incrementAsync');
}
}
除了使用 dispatch ,我们还可以通过辅助函数来操作
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'incrementAsync', // 将 `this.incrementAsync()` 映射为 `this.$store.dispatch('incrementAsync')`
])
}
}
连续调三个接口,ab接口调完之后再调c接口,你会怎么做?
可以使用 Promise 或者 async/await
使用 Promise
let aPromise = fetch('a接口的url');
let bPromise = fetch('b接口的url');
Promise.all([aPromise, bPromise])
.then(responses => {
// a, b 接口都完成
// 这里处理 a, b 的响应,然后调用 c 接口
return fetch('c接口的url');
})
.then(response => {
// c 接口完成
// 这里处理 c 的响应
})
.catch(error => {
// 这里处理任何一个接口的错误
console.error(error);
});
使用 async/await
async function fetchABC() {
try {
let aPromise = fetch('a接口的url');
let bPromise = fetch('b接口的url');
let [aResponse, bResponse] = await Promise.all([aPromise, bPromise]);
// a, b 接口都完成
// 这里处理 a, b 的响应
let cResponse = await fetch('c接口的url');
// c 接口完成
// 这里处理 c 的响应
} catch (error) {
// 这里处理任何一个接口的错误
console.error(error);
}
}
fetchABC();
了解什么是单点登录吗?
这是一种身份验证服务
用户可以用一组凭证登录多个与这个服务器关联的服务或者应用
如果没有单点登录,用户需要为每个系统或者服务创建不同的账号密码
单点登录就是一组凭证访问多个系统,不仅方便用户,也能增加系统安全性
比如 Google 就是一个例子
使用Google账号的用户可以通过一次登录,就能访问Google旗下的所有服务,例如Gmail,Google Drive,Google Photos等。
sbustr、substring、slice 三个方法的区别?
substring(start, end):这个方法返回从start到end(不包括end)的字符串。如果start大于end,这两个参数会被互换;如果任一参数小于 0 或为 NaN,它会被当作 0。substr(start, length):这个方法返回从start开始且长度为length的字符串。如果start为负数,start会从字符串的尾部开始计数。如果length为 undefined,那么子串会延续到原字符串的结束。slice(start, end):这个方法返回从start到end(不包括end)的字符串。如果start或end为负数,那么它会被当作strLength+start或strLength+end,其中strLength是字符串的长度。
总结一下就是:
substring 不接受负参数,如果 end 小于 start,它就会交换这两个参数
substr 是基于长度的,需要一个 length
slice 就是切片,可以从头切,也可以从尾切
用 webpack 做过什么优化?
以 Uniapp 小程序为例
- Tree shaking
- 去除未使用的代码
- Code Splitting 代码拆分
- 把代码拆成小的 chunks,减少代码体积,降低首次加载时间
- 压缩和最小化 Minification
- 压缩和最小化代码
- Lazy Loading
- 异步模块加载,提高应用的加载速度
- loader 优化资源
- 把图片和文字转为 base64,减少网络请求次数
- 缓存优化
- 利用浏览器长缓存机制,减少不必要的文件下载
- Environment Variables
- DefinePlugin,创建在编译时可以配置的全局常量。这对于允许开发模式和生产模式之间的不同行为非常有用。
- Tree Shaking:首先,你需要在
webpack.config.js文件中将optimization.usedExports设置为true。其次,你需要在package.json文件中将sideEffects属性设置为false。这样,webpack 在打包过程中就会删除没有使用的代码。
2、代码拆分:在 webpack.config.js 文件中,你需要配置 optimization.splitChunks 选项。
3、 压缩和最小化:Webpack 默认在生产环境下开启压缩。你可以通过 optimization.minimize 和 optimization.minimizer 选项自定义压缩行为。这通常需要安装 terser-webpack-plugin 插件。
4、Lazy Loading:使用 import() 语法动态地加载模块。
-
资源优化:需要安装
url-loader和file-loader,然后在webpack.config.js文件中配置它们。 -
缓存优化:在
webpack.config.js文件中设置 output 的filename和chunkFilename属性,使用[contenthash]作为文件名的一部分。
封装组件的过程?你都封装过哪些组件?
比如我封装过一个评论区的评论功能组件,包括输入框以及需要带上emoji表情
首先我要分析组件功能,比如要能够输入文字,插入表情,点击发送按钮来触发事件
然后考虑一下这个组件的状态和属性,比如是否启用,是否显示
接下来设计组件的接口,比如用户点击发送按钮
实现组件的一些交互逻辑
最后我们可能考虑成熟的类库,比如 emoji-mart-vue 类库来完成表情包的插入
call 和 apply、bind 是干嘛的?
改变函数运行时的上下文,也就是 this 指向
call
第一个参数是要替代的 this 对象,然后是一个参数列表
function greet() {
console.log(this.name);
}
var person = {
name: 'John'
};
greet.call(person); // 输出 "John"
调用 greet 函数,并通过 call 方法把 this 绑定到 person 对象上,所以 this.name 会输出 "John"。
apply
第一个参数同样是要替代的 this 对象,但第二个参数是一个数组或类数组对象,其中包含了要传递给函数的参数。
function greet(greeting) {
console.log(greeting + ', ' + this.name);
}
var person = {
name: 'John'
};
greet.apply(person, ['Hello']); // 输出 "Hello, John"
bind
bind 不会立即执行函数,而是返回一个新的函数
call 和 apply都会立即执行
function greet() {
console.log(this.name);
}
var person = {
name: 'John'
};
var boundGreet = greet.bind(person);
boundGreet(); // 输出 "John"
什么是执行上下文?
当前 JavaScript 代码被评估和执行的环境或范围。每次函数被调用时,都会创建一个新的执行上下文。
总得来说就是:
- 变量环境
- 作用域链
- this 值
要理解掌握执行上下文,实际就是要理解以下的情况:
- 函数作用域和块级作用域
- 闭包
- 变量提升
- this 关键字
- 异步 js
我们需要理解js运行过程中能执行哪些变量,this 的指向又是哪里
做过大屏可视化吗?
最重要的就是图表,我们会用 echarts、chart.js等库来创建复杂图表
其次是数据,我们会考虑使用 websocket 来从服务端实时获取数据
然后是布局,我们可以考虑用 flex 来实现响应式布局
最后我们可以考虑一下优化
比如优化图片格式,用CND加速等等
Promise 什么时候会进入 catch ?
三种情况
- promise 对象被拒绝,状态变成了 rejected
- then 方法回调中抛出错误
- catch 方法之前的 catch抛出错误
要注意的是:
如果一个 Promise 对象已经进入了 resolved 状态(即已经成功),那么它就不会再进入catch状态。
同样,如果一个 Promise 对象已经进入了catch状态,那么它就不能再进入 resolved 状态。这就是所谓的 Promise 只能被 settle(解决或拒绝)一次。
说说SPA 和 SSR 的区别?
v-model 的底层原理
它就是一个语法糖,实际是 v-bind 和 v-on 的结合体
对于一个 Input 元素,v-model 的底层逻辑是这样的
<!-- v-model的使用 -->
<input v-model="message" />
<!-- 等价于下面的v-bind和v-on的组合使用 -->
<input v-bind:value="message" v-on:input="message = $event.target.value" />
也就是说,我们在输入框中输入值时
v-on:input监听的input事件就会触发,然后更新message的值
当message的值发生变化时,v-bind:value就会将message的新值绑定到输入框的value属性上,从而更新视图。
动态组件如何实现?
Vue 的动态组件是指,我们可以使用 Vue 提供的 <component> 特殊标签来动态地绑定我们要渲染的组件,实现在运行时根据某种条件(如数据变化、用户交互等)动态地切换组件。
<component :is="currentComponent"></component>
在这个例子中,:is 是 Vue 中的特殊属性,它可以绑定一个变量 currentComponent。这个变量 currentComponent 可以是一个字符串,代表我们要渲染的组件名,也可以是一个对象,代表我们要渲染的组件选项对象。
new Vue({
el: '#app',
data: {
currentComponent: 'myComponent'
},
components: {
myComponent: { /* ... */ }
}
})
在这个例子中,如果我们更改 currentComponent 的值,那么在 <component> 标签处渲染的组件就会相应地更改。
动态组件的使用场景包括但不限于:根据用户交互显示不同的组件(例如模态窗口)、根据不同的路由显示不同的页面组件等。
路由权限的实现原理
1、用户登录
服务器返回用户信息,包括权限信息,比如角色等
这些信息可能存储在token中
2、前端收到这些权限信息,把他们存储到 vuex 和 localstorage 中
3、定义路由
给路由添加 meta
const routes = [
{
path: '/user',
component: User,
meta: {
requiresAuth: true,
roles: ['admin', 'user']
}
},
{
path: '/admin',
component: Admin,
meta: {
requiresAuth: true,
roles: ['admin']
}
}
]
/user 路由可以被 admin 角色和 user 角色访问,而 /admin 路由只能被 admin 角色访问。
4、导航守卫
每次路由改变时,我们检查目标路由是否有权限访问
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
const userRole = store.state.userRole // 假设你在 Vuex 存储了用户角色
if (to.meta.roles.includes(userRole)) {
next()
} else {
next('/403') // 或者其他你选择重定向的路由
}
} else {
next()
}
})
async 和 defer 的区别
这是HTML中两个用来改变脚本加载行为的属性,主要用在 script 标签中
当浏览器遇到一个 <script src="..."> 标签(没有 async 或 defer 属性)时,它会立即停止解析 HTML,开始下载脚本,然后执行下载完成的脚本,最后再继续解析 HTML。
这种方式的问题显而易见,如果脚本比较大或者网络较慢,就会导致页面渲染阻塞
async 异步下载脚本,无序执行
当一个脚本有 async 属性时,浏览器会异步下载这个脚本,也就是说它不会阻塞 HTML 的解析。
脚本下载完成后,会立即执行这个脚本,同时会暂停 HTML 的解析。
注意,由于 async 脚本并不保证按照指定的顺序执行,因此只适合于那些互相之间没有依赖关系的脚本。
defer 等HTML完了再执行脚本,按照导入顺序
当一个脚本有 defer 属性时,浏览器也会异步下载这个脚本。
但与 async 不同的是,defer 脚本会等到整个 HTML 都解析完成后再执行。如果有多个 defer 脚本,它们会按照在 HTML 文件中出现的顺序执行。
null 和 undefined 的区别
undefined 声明了但没有赋值
对于函数来说,没有明确的返回值,返回的也是 undefined
null
null 表示已经赋值了,只不过这个值是一个空值
所以我们要记住
undefined 是变量没有被初始化
null 是开发者主动赋予的值
null == undefined 返回 true
但 null === undefined 返回 false
浏览器输入 Url 后会发生什么?
- 解析 URL
- DNS 查询
- 建立 TCP 连接
- 发送 HTTP 请求
- 接收 HTTP 响应
- 页面渲染
- 关闭 TCP 连接
说说你对 TypeScript 的理解
这是JS的一个超集,优势在于提供了静态类型检查和高级的编程特性
- 类型声明
- 接口
- 泛型
- 类和模块
主要目标是提高开发大型应用的生产力
interface 和 type 有什么区别?
- interface 创建新的名字,可以合并
- type 没有那些,更注重操作
interface 创建了一个新的名字,它可以塑造你的值,检查对象是否符合某种形状
type 不创建新的名字,更关注于操作
举个例子:
interface Box {
width: number;
height: number;
}
let myBox: Box = { width: 5, height: 10 };
如果用type是什么样的?
type Box = {
width: number;
height: number;
};
let myBox: Box = { width: 5, height: 10 };
虽然和 interface 比较相似,但是如果有错误,信息只会指向具体的对象类型 { width: number; height: number; },而不会提到 Box。
interface 还有一点,可以合并,这个是type不具备的
interface Box {
width: number;
}
interface Box {
height: number;
}
let myBox: Box = { width: 5, height: 10 }; // 这是合法的
我们先后定义了两个 Box 接口,TypeScript 会将它们自动合并为一个接口,所以这个 myBox 对象是合法的。
如果 type 重复定义,会报错
什么是泛型?
定义函数、接口、或者类时
我们不预先确定具体类型,而是使用时再确定类型
比如 你可以创建一个 Array<T> 类型的数组,T 可以是任何类型,这样你就可以创建 Array<number>、Array<string> 等具体的类型
promise.all 的原理是什么
这个新的 Promise 的状态取决于传入的 Promise 数组中的每一个 Promise 的状态:
- 当所有的 Promise 都解决(resolve)了,
Promise.all返回的 Promise 将会解决,并且它的结果将是一个数组,这个数组包含了所有 Promise 的解决值,这些值的顺序和原始 Promise 数组中的顺序是一致的。 - 但如果其中有任何一个 Promise 被拒绝(reject),
Promise.all返回的 Promise 将会立即被拒绝,并且拒绝的理由将是第一个被拒绝的 Promise 的拒绝理由。
这是 Promise.all 的基本工作原理。它内部实际上就是通过 Promise.prototype.then 方法监听每一个 Promise 的状态变化,然后据此确定返回的 Promise 的状态和值。
这种机制使得 Promise.all 非常适合用于处理需要并行执行多个异步操作,并且需要等待所有操作都完成后才能进行下一步的情况。例如,一个网页可能需要同时从多个源获取数据,然后再进行下一步的渲染操作,这就是 Promise.all 能派上用场的场合。
Map 和 Object 两个数据结构有什么区别?
他们都可以用来存储键值对
但是 object 的键只能是字符串或者符号,map 的键可以是任意值,包括函数、对象等
- 键的类型:
Object的键只能是字符串或符号,而Map的键可以是任意类型的值,包括函数、对象、基本类型。 - 键的顺序: 在
Map中,键的顺序是按照插入的顺序进行排列的,而Object的键则没有特定的顺序。 - 性能: 在频繁的添加和删除键值对的操作中,
Map通常会有更好的性能。 - 内存占用: 对于同样的数据,
Map通常会比Object占用更多的内存,因为Map需要维护键的插入顺序。 - 内置方法:
Map有一些内置的方法,如size、has、delete、clear等,而Object则没有。 - 遍历:
Map是可迭代的,可以直接使用 for...of 循环进行遍历,而Object则需要使用Object.keys()或Object.entries()等方法来获取可遍历的键或键值对。
如果你需要保持键的插入顺序,或者你的键的类型不是字符串或符号,那么 Map 可能会是更好的选择。如果你对内存占用有严格的要求,或者你的数据结构非常简单,那么 Object 可能会是更好的选择。
介绍一下 webpack 的loader和plugin吧,你都用过哪些?
-
Loader: Loader 用于转换特定类型的模块。它们在加载模块时运行,并对原始内容应用转换操作。事实上,loader 是 webpack 中的一个预处理器,负责将源文件预处理为 webpack 可以处理的有效模块。
你可以把 loader 看作是一个翻译员,他们的工作是把不同类型的资源(如CSS、图片、TypeScript等)转换成 JavaScript,这样 webpack 就能理解和处理这些资源。
常用 Loader: 包括
babel-loader(将 ES6 代码转译为 ES5 代码),style-loader和css-loader(处理 CSS 文件),url-loader和file-loader(处理文件和图片)等。 -
Plugin: Plugin 是一个具有更大范围的功能,它们直接影响整个构建流程。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件在 webpack 的运行周期中的特定时刻执行,帮助你完成更复杂的任务。
插件就像是webpack的小助手,他们在构建过程中的特定时刻执行特定任务,比如优化和压缩代码、注入环境变量等。
常用 Plugin: 包括
HtmlWebpackPlugin(简化 HTML 文件的创建,服务于 webpack bundles),MiniCssExtractPlugin(将 CSS 从主应用程序中分离,用于生产环境的代码拆分),DefinePlugin(创建在编译时可以配置的全局常量)等。
记住它们的方法就是:loader 负责资源转换,plugin 负责完成更大范围的任务。
要记住:Loader 是"翻译员",Plugin 是"小助手"。以后当你看到这两个词时,就可以立刻联想到他们的职责和作用了。
为什么我们要把所有资源都转换成 JavaScript 呢?
Webpack 是一个模块打包工具,其核心理念就是一切都是模块,这些模块包括 JavaScript、CSS、图片、字体等等。Webpack 需要把所有类型的资源转换成 JavaScript:
- 首先,Webpack 的运行环境是 Node.js,Node.js 是一个 JavaScript 运行环境。因此,Webpack 需要把所有的资源都转换为 JavaScript 才能进行进一步的处理和打包。
- 其次,把所有资源转换为 JavaScript 模块,这样我们就可以在 JavaScript 代码中以模块的方式引入各种资源(如 CSS、图片等),并且可以利用 JavaScript 的模块系统(如 ES Modules 或 CommonJS)来处理这些资源的依赖关系。这就为我们提供了一种统一的方式来管理和使用所有类型的资源。
所以,这就是为什么我们需要 Loader 将各种类型的资源转换成 JavaScript。它就像是一个翻译员,将各种语言(各种类型的资源)翻译成 webpack 能理解和处理的语言(JavaScript)。
项目是如何鉴权的,就是登录以后会发生什么?
- 用户登录:用户通过提供用户名和密码进行登录。登录请求发送到服务器进行验证。
- 验证用户信息:服务器检查用户名和密码,验证用户是否合法。
- 创建并返回令牌:如果用户合法,服务器会创建一个表示用户身份的令牌,然后将这个令牌发送回客户端。这个令牌通常会包含用户的ID、令牌的发放时间、过期时间等信息,并且这个令牌会被加密。
- 客户端存储令牌:客户端收到令牌后,会将其存储在某个地方,如cookie或localStorage。
- 发送请求:之后,每当客户端向服务器发送请求时,都会在请求的Header中附上这个令牌。
- 验证令牌:服务器收到请求后,会检查Header中的令牌。如果令牌合法且未过期,服务器会处理这个请求;如果令牌不合法或已过期,服务器会拒绝这个请求。
- 用户登出:用户登出时,客户端会删除存储的令牌。
小程序大小超过限制怎么办
微信对小程序有限制,所有分包大小不能超过20M,单个主包或分包大小不能超过2M
我们通常可以选择以下的做法:
1、分包加载
把项目进行拆包,以此来降低主包大小,app.json 里进行指定根页面,用户打开小程序时只会下载主包内容,跳转到分包页面时才会下载对应分包
2、优化图片
把小图标转base64内嵌在代码中,使用webP格式的图片以减小图片体积
3、清理无用文件和代码
4、减少第三方库的使用
5、压缩代码和混淆
以此来减少代码体积,但这样会使代码难以阅读和维护
通常是通过一些构建工具(如 Webpack、UglifyJS 等)来完成的。
压缩主要是删除代码中的空格、换行、注释等无关字符,将长变量名替换为短变量名,以此来减少代码的体积。
混淆则是通过一些技巧(如重命名、控制流扁平化等)来改变代码的结构和外观,但不改变其执行逻辑,以此来防止他人理解和复制你的代码。
当需求不能按时完成时怎么办?
我要了解任务延期的原因,如果是技术方面,我会鼓励团队协作,一起讨论进行解决。
有时候,一个新思路或者视角就能够解决问题。
如果是因为工作量大导致延期,我会重新评估项目的时间线和优先级,这可能需要和项目管理者进行沟通,看看如何调整任务优先级,是否可以增加额外的资源,同时也要寻求办法优化工作流程,减少冗余工作提高效率。
如果是团队成员因为个人问题导致,我会尽量提供支援,寻找临时解决方案,比如把任务分配给其他成员等
vue-router 路由模式有几种?
两种模式,hash 和 history
Hash模式
点击链接时,只会改变#后面部分,对于页面和浏览器来说没有任何影响
History模式
这是HTML5的API,提供了 pushState 和 replaceState 管理浏览器历史记录堆栈
如果你的网站要支持IE9或者更老的浏览器,那么只能选择哈希模式,因为history模式是HTML5的API,老旧版本浏览器并不支持
如果服务器已经配置好了 history 模式的URL,那就选择 history,URL会看上去更美观,也符合我们的习惯
history 可能有个特殊问题
http://www.example.com/user 假如我们在浏览器输入了这个地址,但实际服务器没有配置 /user路径,那服务器找不到,会返回404错误
但是单页面应用中,/user 实际是前端路由,它对应的资源是有前端管理和提供的,而不是服务器
所以我们希望不论什么路径请求,都能返回到单页面上,然后由前端决定到底去哪个页面,所以我们要在服务器进行特殊配置。
相比之下,HASH模式就没有这个问题,因为 url 的 # 号后面部分本来就不会发送到服务器,服务器只会接收到根路径的请求,然后返回单页面,这样服务器就无需进行配置了
让一个元素水平垂直居中怎么写?
- flex
- Grid
- position 和 transform
- 定位和 margin:auto
flex
.container {
display: flex;
justify-content: center;
align-items: center;
}
justify-content: center; 将子元素水平居中,align-items: center; 则将子元素垂直居中。
Grid
.container {
display: grid;
justify-items: center;
align-items: center;
}
position 和 transform
.container {
position: relative;
}
.centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
定位和 margin:auto
.container {
position: relative;
}
.centered {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
webpack 做过哪些优化,开发效率方面、打包策略方面等等
Webapck在优化方面的工作可以分为两类:开发效率的优化和打包优化。
-
开发效率的优化:
- 热模块替换(HMR):通过
webpack-dev-server的hot参数启用。HMR可以在你修改完代码后,无需完全刷新页面就能看到更新后的效果。 - 使用
source-map:这是一种提供源代码到构建后代码映射技术,可以帮助我们在出现错误时准确地定位到源代码的位置。 - 通过
babel-loader进行ES6+语法的转译,让我们可以在开发中使用最新的语法特性,而不用担心兼容性问题。
- 热模块替换(HMR):通过
-
打包优化:
splitChunksPlugin:这个插件可以将公共代码分离出来,避免在每个页面或模块中都重复加载相同的代码,从而降低了打包后的体积,提高了代码的利用率。CleanWebpackPlugin:这个插件在每次构建前清理旧的构建文件,确保构建文件夹中只有当前打包生成的文件,避免了手动去清理旧文件的麻烦,提升了开发效率。MiniCssExtractPlugin:将 CSS 从 JavaScript 文件中抽离出来,不仅可以减小 JavaScript 文件的体积,提高页面加载速度,还使得 CSS 可以被浏览器单独缓存,提高了页面的渲染效率。terser-webpack-plugin:通过压缩 JavaScript 代码,可以显著减少打包后的文件体积,从而提高页面加载速度,优化用户体验。compression-webpack-plugin:通过对资源文件进行 gzip 压缩,可以进一步减小文件体积,加快文件的传输速度,缩短页面的加载时间。html-webpack-plugin:自动化生成 HTML 文件并引入所有 webpack 生成的 bundle,避免了手动创建 HTML 文件并引入资源的麻烦,提高了开发效率。
箭头函数与普通函数的区别
- 没有自己的 this
- 没有 arguments 对象
- 不能用作构造函数
- 没有 prototype
1、箭头函数没有自己的 this
被创建时由上下文自动绑定的。这种绑定方式的优点是,你可以在回调函数中安全地使用this,而不必担心this的值会在运行时改变。
function Person(){
this.age = 0;
setInterval(function growUp() {
this.age++; // 这里的this在非严格模式下指向的是全局对象,严格模式下是undefined
}, 1000);
}
var p = new Person(); // p的age并不会增加
但是如果使用箭头函数,问题就能解决
function Person(){
this.age = 0;
setInterval(() => {
this.age++; // this 正确地指向了p实例
}, 1000);
}
var p = new Person(); // p的age会每秒增加
2、箭头函数没有自己的 arguments 对象
如果你在箭头函数内部尝试访问 arguments,你实际上访问的是包含箭头函数的函数的 arguments 对象
let arrowFunc = () => console.log(arguments);
arrowFunc(1, 2, 3); // ReferenceError: arguments is not defined
function normalFunc() { console.log(arguments) };
normalFunc(1, 2, 3); // Arguments(3) [1, 2, 3, ...]
3、不能用作构造函数
你不能使用 new 关键字去实例化一个箭头函数。因为箭头函数没有自己的 this
const Foo = () => {};
const foo = new Foo(); // TypeError: Foo is not a constructor
function Foo() {}
const foo = new Foo(); // works fine
4、没有 prototype
箭头函数不能用作构造函数,所以没有 prototype 属性
const Foo = () => {};
console.log(Foo.prototype); // undefined
function Foo() {}
console.log(Foo.prototype); // {constructor: ƒ}
在Vue组件中,scoped样式是什么作用?
它可以让你的样式只作用于当前的组件,而不影响其他的组件。这对于防止样式冲突非常有用。
原理是在样式和模板中添加一个唯一的属性(如 data-v-21e5b78),然后用这个属性来限定CSS的作用范围。
需要注意的是,虽然 scoped 可以有效防止样式冲突,但它并不能完全替代CSS模块或BEM等其他的CSS管理方法。
比如,scoped 无法解决样式的复用问题,你仍然需要使用CSS预处理器的功能(如Mixin、Extend等)或者其他的CSS模块化方案来解决这个问题。
移动端适配的方案有哪些?
你常用 CSS3 的哪些新特性?
- flex 布局
- 媒体查询
- 渐变
- 阴影
- 圆角
- 过渡 transition
- 转换 transform
- 动画
css 作用域和名称冲突你是怎么解决的?
在大型项目中,确实可能存在多个开发者在不同文件或者模块进行开发
我们的方法可以分为:
1、BEM命名法
我会为每个块(Block)、元素(Element)和修饰符(Modifier)都定义明确的名称。这种策略帮助我清晰地表达样式的用途,并避免命名冲突。
2、使用 css 预处理器比如 Sass
这样使得样式作用域更加明确,减少冲突
3、使用 CSS 模块
4、组件化样式
浏览器重绘与重排的区别?
promise.catch 后是什么状态
Promise 对象有三种状态:
- Pending(进行中)
- Fulfilled(已成功)
- Rejected(已失败)
当你使用 .catch() 方法处理 Promise 对象时,如果原本的 Promise 对象是 Rejected 状态,那么 .catch() 方法将处理(也就是捕获)这个错误。此后,Promise 对象会变为 Fulfilled 状态。
换句话说,.catch() 方法实际上就是 .then(null, handler) 的别名。它的任务是处理 Promise 对象的错误,然后返回一个新的,Fulfilled 状态的 Promise 对象,以便后续的 .then() 方法可以继续执行。
这就是为什么你可以在 .catch() 之后链式调用 .then() 方法。实际上,你可以将 .catch() 看作是一种恢复 Promise 链的方式,它能够将 Promise 对象从 Rejected 状态恢复到 Fulfilled 状态。
说说你对前端模块化的理解?
关于前端模块化,其实觉得它就像把一个大型项目分解成小的拼图块。每个模块都是独立的,只做它该做的事情,就像一个工具箱里的工具一样。模块化帮我们避免了很多问题,比如防止命名冲突,让代码变得易于维护,还有就是可以复用。
如果我要用一句话来形容模块化,那就是“分而治之”。通过把一个大问题分解成许多小问题,我们可以更有效地解决问题。
在 JavaScript 中,有几种主要的模块化方案,例如 CommonJS,它是 Node.js 用的。还有 AMD,这是一个支持异步加载模块的规范。ES6 Module 是最新的,它在语言标准的层面实现了模块功能。
在实际的项目中,我通常会使用像 webpack 这样的工具来帮我管理和打包模块。
说说你对盒模型的理解
在 CSS 中,每个元素都被表示为一个矩形的盒子
这个模型被称为"盒模型"(Box Model)。
盒模型是用来定义元素在设计和布局时所占的空间。
一个盒模型由四个部分组成:
- Content(内容) :这是盒子的中心部分,即元素的实际内容,例如文字、图片等。
- Padding(内边距) :内容周围的空白区域,位于内容与边框之间。
- Border(边框) :围绕在内边距和内容外的边框。
- Margin(外边距) :边框外部的空白区域,用来分隔相邻的元素。
这四个部分从内到外按照上述顺序构成了一个完整的盒模型。
有一点需要注意的是,在标准的 CSS 盒模型中,width 和 height 指定的是内容区的宽和高,并不包括内边距、边框和外边距。
但是在 IE 的怪异盒模型中,width 和 height 是包括内容区、内边距和边框的。
如果你希望 width 和 height 包括内容、内边距和边框,而不包括外边距,可以通过设置 box-sizing 属性为 border-box 来实现。这种方式又被称为“边框盒模型”(border-box model)。
前端项目中请求的错误处理是怎么做的?
- 检测网络状态
- 请求超时
- 捕获错误
- 处理服务器错误状态码
- 全局错误处理
- 日志监控
检测网络状态
在发送请求之前,可以检测用户的网络状态。如果用户当前没有网络连接,那么就没必要发送网络请求了。
这可以通过监听浏览器的 online 和 offline 事件来实现。
在uniapp中,我们可以通过 uni.getNetworkType API 获取当前的网络状态,
也可以使用 uni.onNetworkStatusChange API 监听网络状态的改变。
请求超时
大多数网络请求库(如 Axios、Fetch 等)都允许你设置请求的超时时间。
如果请求在指定的时间内没有得到响应,那么请求库会自动终止请求并抛出一个错误。
捕获错误
当请求出错时(例如因为网络问题、服务器错误、请求超时等原因),网络请求库会抛出一个错误。你应该在请求的 Promise 链中添加一个 catch 处理程序来捕获并处理这个错误。
可以根据错误的类型和详细信息来决定如何处理错误,例如显示一个错误消息、重定向到错误页面、重试请求等。
处理服务器错误状态码
即使请求成功发送并得到了响应,服务器也可能返回一个表示错误的 HTTP 状态码(如 400、404、500 等)。
应该检查服务器返回的状态码,并根据状态码来决定如何处理错误。
日志监控和全局错误处理
在 Vue 中,可以使用 Vue.config.errorHandler 来捕获未被处理的 Vue 组件渲染错误。
同时,你也可以监听 window 对象的 error 和 unhandledrejection 事件来捕获其他未被处理的错误。
对于 Uni-app,你可以在 main.js 文件中使用 Vue.config.errorHandler 来捕获全局的 Vue 组件渲染错误。
对于 JavaScript 运行时错误,由于目前 Uni-app 还不支持监听 window 对象的 error 和 unhandledrejection 事件,你需要在可能产生错误的地方手动添加 try...catch。
前端的权限管理一般是怎么实现的?
路由级别的权限
常用于单页应用(SPA)。
通过设置路由元数据(例如,在路由配置中为每个路由添加一个 roles 数组),来定义每个路由需要的权限。
然后,我们可以使用 Vue Router 的导航守卫,来在跳转路由前检查用户的角色是否满足当前路由的权限需求。如果不满足,我们可以重定向到一个错误页面或者登录页面。
组件级别的权限
对于某些特定的组件,可能需要更细粒度的权限控制,例如,某个按钮只对管理员可见。这种时候,我们可以创建一个权限指令,然后在需要控制权限的组件上使用这个指令。
API 级别的权限
对于一些敏感的数据,我们可能希望只有拥有特定权限的用户才能访问。这种情况通常需要在后端进行权限检查。当用户登录时,后端会返回一个 JWT(Json Web Token)给前端。前端在发起请求时,将这个 JWT 添加到请求头中。后端收到请求后,会验证 JWT,并根据 JWT 中的信息检查用户的权限。
typeof 和 instanceof 的区别?
首先他们都可以用来检测数据类型,但是typeof更多是用来检测基础数据类型,undefined、boolean、number、string、symbol、object、function。但是对于null和数组,typeof会返回"object",所以在某些情况下,typeof可能无法提供准确的类型信息。
所以如果需要判断基本数据类型,可以使用typeof,如果需要判断一个对象是否属于某个类的实例,可以使用instanceof。
比如 null 和 array 在typeof 的检测下都是 object,这时候就适合使用 instanceof
项目中你是如何使用 WebSocket 进行实时通信的?
在我的一个小程序项目中,两人进行头脑风暴答题PK,这就需要实时通信的支持。
我会使用WebSocket来实现这个功能。当两个用户进入同一场游戏时,我会创建一个WebSocket连接,通过这个连接,服务器可以实时地将题目、对方的得分等信息推送给用户,用户也可以实时地将自己的答案发送到服务器。
这样,两个用户就可以实时地进行比赛了。
// 建立 WebSocket 连接
const socket = new WebSocket('ws://your-websocket-server-url');
// 连接打开时触发
socket.onopen = function(event) {
console.log('WebSocket 连接已打开');
socket.send('Hello Server!'); // 可以发送数据给服务器
};
// 接收到服务器数据时触发
socket.onmessage = function(event) {
console.log('来自服务器的数据: ', event.data);
};
// 连接关闭时触发
socket.onclose = function(event) {
console.log('WebSocket 连接已关闭');
};
// 发生错误时触发
socket.onerror = function(error) {
console.log('WebSocket 发生错误: ', error);
};
如果我们需要更多复杂场景,比如短线重连,那就要考虑使用第三方库比如:Socket.IO
平时项目中是如何排查前端线上问题的?
keep-alive 是怎么使用的?有哪些参数?
<keep-alive>
<your-component></your-component>
</keep-alive>
-
include: 可以是一个字符串或者是一个正则表达式,用来指定哪些组件应该被缓存。例如<keep-alive include="your-component">。 -
exclude: 与include相反,用来指定哪些组件不应该被缓存。例如<keep-alive exclude="another-component">。 -
max: 一个数字,用来指定最多可以缓存多少组件实例。
在你的工作中,你是否使用JavaScript的ES6+新特性,如箭头函数,解构,模板字符串,类,模块等?如果是,这些新特性是如何帮助你改进你的代码的?
我在我的项目中大量使用ES6的新特性。箭头函数使得函数定义更加简洁,模板字符串可以让我方便地插入变量到字符串中。解构可以让我方便地从对象或数组中提取值。类和模块使得我的代码更加组织化,更容易理解和维护。
为什么要使用打包工具对前端项目进行打包?
模块化
我们经常需要对代码进行拆分,项目中会存在很多导入导出,webpack能自动识别这些并打包到一起
代码优化
webpack 有很多内置功能,比如代码压缩,或者去除死代码等,来提升应用的性能,为用户节省下载时间
处理静态资源
我们会使用很多CSS、图片、字体等静态资源,webpack能通过各种loader让我们在JS中直接import这些资源,这样做的好处是,能够自动添加css前缀,优化图片大小,转换字体等等
开发工具
还有一些提升开发体验,比如热更新,让我们实时看到开发效果,不论我们替换了css还是图片
让你封装一个组件,你会怎么考虑?
一般在Vue的哪个生命周期进行接口调用?为什么?
如果需要等DOM渲染完后调用,那就需要放在 mounted 生命周期里
如果不需要等待DOM渲染完成,那就放在 created 生命周期调用,这样能更早的拿到数据进行展示。
当然如果用的是vue3的话
如果需要等待 DOM 渲染完成,那么我会选择 onMounted;如果不需要,那么我会选择 onBeforeMount。
rpx 适配的原理是什么?
rpx 是小程序开发中使用的一个长度单位,主要用于适配不同的屏幕。
在微信小程序中,规定屏幕宽度为 750rpx。所以,无论在什么屏幕下,750rpx都将占满屏幕的宽度。由此,我们可以根据设计稿的尺寸直接换算成对应的rpx值。
例如,如果设计稿的宽度是375像素,那么1px就对应2rpx,设计稿中的元素宽度是50px,那么在小程序中就应该设置宽度为100rpx。
rpx的核心原理是基于屏幕宽度的动态计算。
在运行时,设备会根据自身屏幕宽度来计算出1rpx对应的实际像素值,从而达到在不同屏幕尺寸下都能提供相同的视觉效果,实现响应式的效果。
因此,rpx是一个相对单位,可以根据设备的屏幕宽度进行适配,非常适合开发响应式布局。
vue怎么给对象添加属性
Vue2 中,初始化实例的时候会对数据做响应式转换
所以在这之后添加的属性,vue就无法使他们成为响应式
我们应该使用 Vue.set 或者 $set 的方法
this.$set(this.someObject, 'newProperty', 'newValue');
Vue.set(this.someObject, 'newProperty', 'newValue');
由于 Vue 3 使用了新的响应式系统(基于 ES6 的 Proxy),可以直接为响应式对象添加新的属性
无需再使用 Vue.set 或 Vue 对象的 $set 方法。
this.someObject.newProperty = 'newValue';
看过vue的哪些源码?
响应式系统
Observer 类是整个响应式系统的核心。
当我们传入一个对象给 Vue 实例作为 data 选项,Vue 会使用 Observer 类把这个对象转变成响应式对象。
export class Observer {
constructor(value) {
this.value = value
// 给对象定义一个 __ob__ 属性,值为 this
// 以后我们就可以通过一个对象的 __ob__ 属性获取到它的 Observer 实例
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 省略处理数组的逻辑...
} else {
this.walk(value)
}
}
// 遍历对象的每一个属性,把它们转化成响应式属性
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
这里的 defineReactive 函数就是用来把一个普通属性转化成响应式属性的。
它的基本思路是,利用 Object.defineProperty 把一个属性转化成 getter/setter,当我们读取这个属性的时候,就会触发 getter 函数,当我们修改这个属性的时候,就会触发 setter 函数。在 getter 函数和 setter 函数中,我们就可以做一些额外的工作,比如依赖收集和派发更新。
function defineReactive(data, key, val) {
// 省略了一些代码...
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 省略了一些代码...
return val
},
set: function reactiveSetter(newVal) {
// 省略了一些代码...
}
})
}
你对 vue 路由了解多少
单页面应用的核心
- 声明式路由映射
- 嵌套路由
- 路由参数、查询参数、通配符
- 视图命名
- 路由钩子函数
- 路由懒加载
- 历史模式和哈希模式
讲讲路由钩子函数吧
全局路由守卫、组件内守卫,这些都可以称为路由钩子函数
我常用的一般有:
1、全局前置守卫:检测登录状态,如果用户没有登录,那就跳转到登录页
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
2、全局解析守卫:比如我们在路由确认前需要做一些加载数据的工作,或者加载某些大型异步组件,就可以用这个
router.beforeResolve((to, from, next) => {
// 可以在这里进行数据预加载或者异步组件的加载
if (to.path === '/someAsyncPage') {
store.dispatch('fetchData').then(() => {
next();
});
} else {
next();
}
});
3、全局后置钩子:我们通常会进行一些操作比如修改页面标题,进行访问埋点等
router.afterEach((to, from) => {
// 更新页面标题
document.title = to.meta.title || 'Default Title';
// 进行页面访问的埋点
analytics.recordPageView(to.path);
});
4、组件内守卫,比如页面进入时要获取服务器数据,或者离开时保存一些状态
const Foo = {
data() {
return {
info: null,
};
},
beforeRouteEnter(to, from, next) {
fetchData(to.params.id).then((info) => {
next((vm) => {
vm.info = info;
});
});
},
beforeRouteLeave(to, from, next) {
if (this.isDataChanged) {
confirm('You have unsaved changes, are you sure to leave?') ? next() : next(false);
} else {
next();
}
},
};
axios封装一般都做了哪些东西?
1、设置基础url,这样就不用再每个请求中写基础url了
2、设置默认 headers,比如加上token
3、请求和响应拦截器
// 添加请求拦截器
axios.interceptors.request.use(config => {
// 在发送请求之前可以做些什么,例如添加loading
return config;
}, error => {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(response => {
// 对响应数据做点什么,例如隐藏loading
return response;
}, error => {
// 对响应错误做点什么
return Promise.reject(error);
});
4、封装API方法
function getUser(id) {
return axios.get(`/users/${id}`);
}
5、错误处理,例如如果返回的状态码表示错误,我们可以把错误信息展示给用户,或者根据错误的类型做出不同的处理。
Vue2和Vue3的生命周期有什么不同?
3 引入了 Composition API,其中的生命周期钩子也和 Vue 2 有所不同。
我们看一下差异:
beforeCreate->setup:setup是新的创建阶段的入口函数,在beforeCreate和created之间触发。created->setupbeforeMount->onBeforeMountmounted->onMountedbeforeUpdate->onBeforeUpdateupdated->onUpdatedbeforeDestroy->onBeforeUnmountdestroyed->onUnmounted- 新增了
onRenderTracked和onRenderTriggered,可以追踪到虚拟 DOM 渲染过程中的依赖项。
了解Vue中的$nextTick吗?
他会在下一次DOM更新循环结束后执行一段代码
这对于数据改变之后,你需要等待VUE完成DOM更新的场景非常有用
这是因为 Vue 异步地批量更新 DOM,这意味着当你修改了数据,这个修改并不会立即反映在 DOM 上,而是在下一个更新周期才会进行。
export default {
methods: {
updateMessage: function () {
this.message = 'updated';
console.log(this.$el.textContent) // => 'not updated'
this.$nextTick(function () {
console.log(this.$el.textContent) // => 'updated'
})
}
}
}
上面这个例子,我们修改了 message 数据,然后试图立即打印这个更改在 DOM 中的反映。
但是因为 DOM 更新是异步的,所以在 $nextTick 之前的 console.log 还是显示的旧数据。然后在 $nextTick 的回调中,我们再次打印,这时就能看到新的数据了。
注意,$nextTick 常用语vue2中,vue3引入了Composition API,改变了组件的写法。
在 setup 函数中,Vue 3 提供了 onMounted 和 onUpdated 钩子,他们分别在组件挂载完成和更新后被调用,这样我们就不需要再用 $nextTick 来等待 DOM 更新了。
Cookie、sessionStorage、localStorage 的区别
Cookie (最早的客户端存储技术
大小限制为4KB,每次同源请求时,浏览器都会携带相应的 Cookie,他在安全性和效率上存在问题
sessionStorage (HTML5引入的技术
数据的大小限制为5MB。当浏览器关闭或者标签页关闭后,sessionStorage 中的数据会被清除。它主要用于存储临时的会话数据,比如表单填写的内容。
localStorage (也是HTML5的技术
大小限制也是5MB,持久性存储,即便关了浏览器,数据也能保存,我们可以用它来存储用户的偏好设置
说说强缓存和协商缓存吧
强缓存
在缓存期间不需要请求服务器,浏览器可以直接使用缓存资源。当我设置了 Expires 或 Cache-Control 后,再次刷新页面,就算手动刷新,请求也不会发送到服务器,浏览器直接读取缓存文件。
协商缓存
是利用的是 Last-Modified/If-Modified-Since 和 Etag/If-None-Match 这两对首部字段控制的。
在再次请求服务器时,由服务器决定资源是否被缓存。如果协商缓存生效,返回 HTTP 304 和一个空的响应体,直接使用缓存数据,这样就节省了数据传输的时间。
typeof 是做什么用的?它的原理是?
用于检测数据类型
它会返回7种类型:
- undefined:变量未定义或者值为undefined
- boolean:布尔类型
- number:数字类型
- object:对象或者Null
- function:函数
- symbol:符号
JavaScript存储变量时会在内部为其添加一些标识,比如类型标识,typeof操作符就是读取这些内部标识来判断变量的类型。
基本类型和引用类型的区别?
基本类型
Number、String、Boolean、Undefined、Null 和 Symbol(es6新增)
他们在内存中占据着固定大小
引用类型
Object、Array 和 Function
操作引用类型时,其实操作的不是实际的值,而是引用
赋值、传参、拷贝等操作中,尤其要注意
let a = {value: 1};
let b = a;
a.value = 2;
console.log(b.value); // 输出 2,b 的值随 a 的改变而改变
JS如何判断一个对象为空?
JSON.stringify() 方法:将对象转换为JSON字符串,如果对象为空,那么结果将是字符串 "{}"。
function isEmpty(obj) {
return JSON.stringify(obj) === "{}";
}
var myObj = {};
console.log(isEmpty(myObj)); // 输出 true
通过 Object.keys() 方法来实现:
function isEmpty(obj) {
return Object.keys(obj).length === 0;
}
// 该方法返回一个数组,检查这个数组的长度即可
var myObj = {};
console.log(isEmpty(myObj)); // 输出 true
事件冒泡和事件捕获到底有何区别?
什么是“use strict”,有什么好处和坏处?
这是JS的严格模式,用来改变JS的默认语义,为了让JS在更严格的条件下运行,减少语言自身的一些问题
只要在JS文件头部添加 use strict 即可
这么做有什么好处?
能够减少一些怪异行为,提高编译效率,增加运行速度
举个例子:非严格模式下不会有任何问题,但是严格模式就会报错了
"use strict";
x = 3.14; // 抛出错误,因为 x 没有被声明
防抖和节流?有什么区别?如何实现?
他们都是用来控制函数执行频率的技术
防抖 (定时器
将多次高频化的操作只在最后一次执行
比如搜索框的输入
实现防抖的一个基本方法是设置一个定时器,在指定时间过去之后执行函数,如果在这个期间再次触发了事件,那么就会取消之前的定时器并设置一个新的定时器。
节流 (比较时间戳
使得函数在指定的时间间隔内最多执行一次,比如滚动加载
实现节流的一种基本方法是记录上一次执行的时间戳
当当前时间戳与上一次执行的时间戳的差值大于指定的时间间隔时,就执行函数。
防抖是将多次执行变为最后一次执行,节流则是在一定时间内只允许函数执行一次。
前端是如何解决跨域的?
- CORS (后端响应头
- JSONP (script 标签
- 反向代理
- websocket
1、CORS(跨域资源共享),需要后端在响应头中添加CORS头信息
如 Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers 等。前端发起请求时,浏览器会自动处理 CORS 相关的事情。
2、JSONP
这是一种老的方案,现在用的少利用 script 标签不受同源策略限制的特点,通过动态创建<script>标签发起请求,但它只能发起 GET 请求。
3、反向代理
本地开发时,Vue 的开发服务器中,都内置了代理功能,只要在配置文件中设置代理,就可以将请求转发到目标服务器
4、WebSocket
全双工通信协议,一旦建立连接,就不会受到同源策略限制
数组去重都可以使用哪些方法?
1、使用 set
Set 是 ES6 新增的数据结构,它的一个最大特点就是所有的元素都是唯一的,这个特性使得它非常适合做数组去重。
2、使用 filter
filter 方法接受一个函数作为参数,这个函数会对数组中的每个元素进行判断,只有返回 true 的元素才会留下。
const arr = [1, 2, 2, 3, 3, 4];
const uniqueArr = arr.filter((item, index, array) => array.indexOf(item) === index);
3、使用 reduce
let unique2 = arr.reduce((accumulator, item) => { return accumulator.includes(item) ? accumulator : [...accumulator, item]; }, []); console.log(unique2); // 输出 [1, 2, 3, 4, 5]
reduce() 方法会对数组中的每一个元素执行一个函数,然后把这个函数的返回值累积起来,最后返回这个累积值。在这种情况下,我们让函数的初始累积值为一个空数组,然后在函数体中,我们检查累积值是否包含当前元素,如果不包含,就把当前元素添加到累积值中。
4、对象键值对
对象的属性名是唯一的,所以我们可以把数组的每一个元素都当作对象的一个属性,然后获取这个对象的所有属性名,就得到了去重后的数组。
let obj = {};
for (let i of arr) {
obj[i] = true;
}
let unique3 = Object.keys(obj).map(item => Number(item));
console.log(unique3); // 输出 [1, 2, 3, 4, 5]
4、使用对象的键值对:这种方法利用了对象的键名是唯一的这个特性。
对象的常用方法有哪些?
- Object.assign 用来合并对象,或者拷贝一个对象
- Object.create 创建一个对象
- Object.keys(obj) 返回一个数组,包含对象所有可枚举属性的键名
- Object.values(obj) 返回一个数组,包含obj的属性的值
- Object.entries 返回一个数组,每一个元素都包含一个属性名和属性值
- Objectha.hasOwnProperty 检查自身是否有某种属性
假设我们有个对象:Person
let person = {
name: 'Tom',
age: 25,
job: 'Engineer'
};
object.keys 获取键名
let keys = Object.keys(person);
console.log(keys); // 输出:['name', 'age', 'job']
object.values 获取对象的值
let values = Object.values(person);
console.log(values); // 输出:['Tom', 25, 'Engineer']
Object.entries 获取键值对
let entries = Object.entries(person);
console.log(entries); // 输出:[['name', 'Tom'], ['age', 25], ['job', 'Engineer']]
Set 和 Map 的区别
都是ES6引入的新的数据结构,set用来处理非重复值的,map使用处理键值对的
set 最常用的就是存储无重复项的集合,比如数组去重
let arr = \[1, 2, 2, 3, 3, 3];
let set = new Set(arr); // Set {1, 2, 3}
关于 map 的使用
本来我们是可以通过普通对象存储键值对的,比如:
let obj = {};
obj['name'] = 'John';
但这种方式有一些限制。例如,对象的键只能是字符串或者 Symbols,如果你试图使用其他类型(如对象或者数组)作为键,它们会被转换为字符串。
键值的插入顺序是保留的,也就是遍历map对象时,键值对的顺序就是它们被添加的顺序,普通对象键值对顺序不固定
如果要频繁添加键值对,map有更好的性能
而且我们用map,可以很方便的知道它的大小,是用map.size,要获取普通对象大小就要麻烦很多
了解重绘和重排吗?
这是浏览器渲染页面时的两种操作,他们都会带来性能上的影响
重排
元素的尺寸规模、布局等都需要改变,消耗的性能最大,需要尽量避免
重绘
只改变了一些样式,对性能影响不如重排
我们要记住:
重排一定引起重绘,但重绘不一定引起重排
那如何减少重绘和重排以提升性能呢?
- 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上
- 避免在布局信息改变之后立即进行计算样式,否则会引起回流。
- 避免使用表格布局。
- 尽可能在 DOM 树的最末端改变样式。
- 不要一条一条地改变样式,而是通过改变 class,或者使用 cssText 或者 style 对象的 css 属性一次性地更改样式。
你对 websocket 了解多少
它是HTML5的一个全双工通信协议,在浏览器和服务器之间提供双向实时数据传输
比如在线聊天和游戏
传统的HTTP协议只能进行单向请求
websocket 建立连接后可以保持长连接
在使用 WebSocket 时,客户端会先向服务器发起一个 HTTP 请求,这个请求的头部信息中带有 Upgrade 字段,表示要将协议升级。如果服务器同意升级,那么协议就会从 HTTP 切换到 WebSocket,之后的通信就直接通过这个已经建立的 WebSocket 连接进行。
通常我们可以使用一些库如 socket.io,来简化 websocket 的使用,而且还有一些比如自动重连的功能。
socket.io 的兼容性也比较号,如果环境不支持 websocket ,会自动降级为HTTP长轮询方式,确保任何环节下都能实时通信。
注意,老旧浏览器是不支持websocket的,例如Internet Explorer 9及以下版本。
还有个公司或者项目的网络环境也可能导致。有些网络运营商也可能会阻止websocket流量,不过这种情况少见
如何判断一个数据的类型,以及它们的优缺点
如果是基础数据类型,它可以很好处理
但如果是原始类型或者对象类型,就不能很好处理了
比如 Null、数组、对象、日期等等,都会返回 object
这时候我们还得使用 instanceof 或者 Array.isArray() 或者 Object.prototype.toString.call()
instanceof的原理是用来检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
比如 [] instanceof Array 会返回 true
但是他有局限性,如果有多个全局环境,比如多个 iframe,环境各自都有上下文,那可能得到的结果会不一致
所以最好我们可以使用 Object.prototype.toString.call
看起来很繁琐,但确实是最准确的判断方式
大多数情况下,typeof 是够用了
如何判断 token 失效?如果失效了怎么处理?
失效一般是两种情况:
- 达到了预设的过期时间
- 用户登出或者更换了密码
如何判断 token 失效呢?
那么如何判断一个 token 是否失效呢?这个需要依赖服务器的反馈。通常来说,当我们的请求中带着一个失效的 token 时,服务器会返回一个特定的错误码或者错误信息,提示 token 已经失效。客户端收到这个反馈后就可以知道 token 失效了。
通常我们可以这么做:
- 自动刷新 token
- 提示用户重新登录
- token 续期
自动刷新
在 token 即将失效时(比如还剩5分钟),客户端自动向服务器发起一个更新 token 的请求,服务器验证旧 token 没有问题后返回一个新的 token。
这种方式的好处是用户无感知,但要求服务器提供刷新 token 的接口,并且在客户端要实现相应的逻辑比较复杂。
提示用户登录
当检测到 token 失效后,直接清除本地的 token,并跳转到登录页面让用户重新登录。这是最简单的处理方式,适用于大部分情况。
Token 续期
如果服务器端的 token 是可以续期的,那么在用户每次操作时都重新刷新一下 token 的有效期,这样只有长时间不操作的用户才会被登出。
常用的ES6都有哪些?
- let 和 const
- 箭头函数
- 类
- 模块化
- 模板字符串
- 解构赋值
- Promise
- Map 和 Set 两种数据结构
- Symbol
说说对前端缓存的理解
这是一个能够提高前端性能的手段,可以避免重复的网络请求,减少网络带宽的消耗
一般有以下几种:
- 浏览器缓存,如 Cache-Control
- 存储级缓存,比如 Cookie、LocalStorage、SessionStorage、IndexedDB
- 应用级缓存,比如 keep-alive
反向代理为什么能解决跨域问题,原理是什么 ?
跨域是因为浏览器的同源策略所限制:协议、域名、端口
反向代理是解决跨域的一种方法
例如,我在前端有一个 www.mywebsite.com 的页面,我想要访问 www.otherwebsite.com/api 的 API。
假如你在vue的本地服务(localhost:8080)上去请求一个服务器(www.otherwebsite.com/api)
我们可以在 vue.config.js 文件中进行配置:
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://www.otherwebsite.com',
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
}
}
}
}
'proxy'属性定义了代理规则,当你的应用程序在localhost:8080上发送到/api开头的请求时,请求将被代理到www.otherwebsite.com。'target'是你想要代理的目标服务器地址。'changeOrigin'设置为true时,代理服务器会改变请求头中的host信息为目标服务器的地址,从而让目标服务器认为这个请求是从它自身的服务器发出的,以此避免一些安全策略的问题。'pathRewrite'用于重写请求路径,'^/api': '/api'表示将路径中的/api替换为/api。因为你在本地请求的地址可能是localhost:8080/api/some/path,但实际上你希望请求的地址是www.otherwebsite.com/api/some/path,所以需要这样的重写。
这个解决方案只适用于在开发环境下的本地服务器,它并不能用于生产环境的服务器。生产环境的服务器通常需要通过配置 Nginx 或其他服务器软件来实现代理。
什么是同源策略?
这是浏览器的一种安全策略。
只有两个网页拥有相同的协议、域名、端口时,才被认为是同源的,才能进行如读写这样的交互,调用接口等。
这个策略的目的是为了保护用户的信息安全,防止恶意的网页利用用户在其他网站上的登录状态,去获取或操作用户的私人数据。
computed 和 watch 区别?
computed 计算属性
对依赖数据进行缓存,依赖的数据变化时,会重新计算
比如我们的购物车需要来计算总价
watch 监听
没有缓存,每次数据变化都会执行
比如搜索框的输入变化
总结一下区别就是:
- computed 可以依赖多个,watch 只能一个
- watch 可以深度监听,能监听对象内部或者数组内部变化
- watch 适合去处理异步操作,computed 必须是同步
- computed 侧重于计算,watch 更注重变化后的一些动作
了解 Symbol 吗
这是ES6新引入的一种基本数据类型,他可以用作对象的属性键
项目开发中,我们常用来作为对象的唯一属性名,防止属性名冲突
let sym1 = Symbol('key');
let obj = {
[sym1]: "value"
};
console.log(obj[sym1]); // "value"
let sym2 = Symbol("description");
let sym3 = Symbol("description");
console.log(sym2 === sym3); // 输出:false
但是要注意,常规的遍历方式是不能访问到 Symbol 属性
for (let key in obj) {
console.log(key, obj[key]); // 无输出,因为 Symbol 属性不会被遍历到
}
如果想要访问对象的 Symbol 属性,我们可以使用 Object.getOwnPropertySymbols() 方法:
let symbols = Object.getOwnPropertySymbols(obj);
console.log(symbols); // 输出:[ Symbol(mySymbol) ]
map 和 foreach 有什么区别?
他们都可以对数组进行遍历
map 会返回一个全新的数组,foreach 返回undefined,并且改变原数组
map 返回新数组,所以可以链式调用其他数组方法,比如:
array.map(...).filter(...).reduce(...)
在实际开发中,如果你需要根据现有数组生成新数组,通常使用map方法。如果你只是想对数组中的每个元素执行操作,且不关心返回值,那么forEach可能更适合。
说说 for in 和 for of 的区别吧
他们是两种循环,有各自使用场景
for...in 遍历对象的属性,包括原型链上的属性
"in" 就像 "index",就像你在查阅目录("index")一样,你在操作的是目录条目(即对象的属性名)。
let obj = {a: 1, b: 2, c: 3};
for (let prop in obj) {
console.log(prop); // 输出:a, b, c
}
for...of遍历可迭代的对象(数组、字符串、map、set),遍历的是值,不是属性
“of" 像 "off",就像你在一个列表中一个个地拿掉("off")东西,你在操作的是列表中的元素本身。
let arr = [1, 2, 3];
for (let value of arr) {
console.log(value); // 输出:1, 2, 3
}
解释一下JavaScript中map(), reduce(), filter()
map 映射
它处理数组中的每一个元素并返回新的数组
想象一张地图(map),你在每个位置上都做了一些操作,然后你得到了一个新的、修改过的地图,但位置还是那些位置。
let array = [1, 2, 3];
let newArray = array.map(x => x * 2); // newArray 就是 [2, 4, 6]
reduce 归约
将所有元素合并为一个单一结果
let array = [1, 2, 3];
let sum = array.reduce((accumulator, current) => accumulator + current, 0); // sum 就是 6
accumulator 是累加器,它是回调函数的第一个参数。
current 是当前元素,它是回调函数的第二个参数。在每一次迭代中,current 代表的是当前正在处理的元素。
- 在第一次迭代中,
accumulator的值是0(初始值),current的值是1(数组的第一个元素)。所以accumulator + current的结果是1,这个结果会被传给下一次迭代。 - 在第二次迭代中,
accumulator的值是1(上一次迭代的结果),current的值是2(数组的第二个元素)。所以accumulator + current的结果是3,这个结果会被传给下一次迭代。 - 在第三次迭代中,
accumulator的值是3(上一次迭代的结果),current的值是3(数组的第三个元素)。所以accumulator + current的结果是6,因为这已经是数组的最后一个元素,所以reduce()返回6,这就是sum的值。
filter 过滤
let array = [1, 2, 3];
let newArray = array.filter(x => x > 1); // newArray 就是 [2, 3]
一个过滤器(filter),只有满足条件的东西才能通过,否则就会被过滤掉。
你是否了解闭包?工作中有哪些实际的应用?
说说微信小程序的支付
主要的交互就是小程序和我们自己的后端服务器,以及我们后端与微信的支付系统。
用户下单并确认购买,我们发送请求到后端服务器,生成一个订单
后端服务器调用微信的支付系统,这要涉及微信的API,我们会提供一些必要信息,比如订单详情,支付金额,回调地址等等,这些会生成一个预支付交易会话ID,prepay_id
支付系统返回这个 prepay_id 后,我们后端将这个ID和一些必要参数返回给小程序
小程序收到这些参数,调用微信小程序的支付API,把参数传递进去,用户会看到支付确认弹窗
用户支付订单成功后,支付系统会给我们的回调地址发送一个通知。后端收到支付成功的通知后,更新订单状态,并通知小程序的支付结果。
Promise有几个方法以及它们之间的区别?
- resolve 返回一个解析后的 Promise 对象
- reject 返回一个带有拒绝原因的 Promise 对象
- all 所有Promise都成功后才解析,一个拒绝则立即拒绝
- race 任一Promise成功或失败就改变状态
- any 任一成功就会解析,全部失败才会拒绝
- allSettled 所有都解决或者失败,才会解析
HTTPS 和 HTTP 的区别
最明显的区别就是安全性。
HTTP 是不安全的,数据传输过程是明文的,容易被第三方截取和篡改。
而HTTPS是安全的,使用了SSL/TLS协议对数据进行了加密。
HTTP默认端口时80,HTTPS是443
HTTPS要进行加密解密,所以性能会不如HTTP
SEO:搜索引擎更倾向于HTTPS,因为更安全
HTTPS还需要申请和使用证书,而HTTP不需要
router 和 route 有什么区别?
我们直接看一个例子:
// 创建一个 router
const router = new VueRouter({
routes: [
// 这是一个 route
{ path: '/foo', component: Foo },
// 这是另一个 route
{ path: '/bar', component: Bar }
]
})
在这个例子中,router 是管理整个路由系统的对象,而 routes 数组中的每个对象都是一个 route,定义了一个路径及其对应的组件。
因此,简单来说,Router 是一个系统,管理整个路由的系统。
它负责维护路由表,处理导航(即 URL 更改),以及渲染相应的组件。在 Vue 中,你会创建一个 Router 实例,你在其中定义你的所有路由。
你可以理解 Router 是一个邮局,而 Route 则是邮局中的每一个邮递员的路线,他们知道每个地址应该去哪里,而 Router(邮局)则管理这些 Route(邮递员的路线),确保邮件能准确地投递到每一个地址。
说说ES6的循环和遍历的方式 ?
for...of
用于遍历可迭代对象(包括 Array、Map、Set、String、TypedArray、arguments 对象等等)。
map
创建一个新的数组,每个元素都是一个函数的返回结果
const array = [1, 2, 3];
const newArray = array.map(x => x * 2);
console.log(newArray); // 输出:[2, 4, 6]
filter
创建一个新的数组,其中包含所有通过测试的元素,过滤掉那些不满足条件的元素
const array = [1, 2, 3, 4, 5];
const newArray = array.filter(x => x > 3);
console.log(newArray); // 输出:[4, 5]
reduce
把数组变成一个单一的值
它接受两个参数:累加器和当前值
你不要以为它只能用来求和,它也可以去计算元素的乘积
const numbers = [1, 2, 3, 4];
const product = numbers.reduce((accumulator, current) => accumulator * current, 1);
console.log(product); // 输出:24
它还可以将二维数组扁平化
const arrays = [[1, 2], [3, 4], [5, 6]];
const flat = arrays.reduce((accumulator, current) => accumulator.concat(current), []);
console.log(flat); // 输出:[1, 2, 3, 4, 5, 6]
uniapp 开发小程序和原生小程序开发有什么不同?
- 编程语言和技术栈:原生小程序主要使用微信提供的专用框架以及 WXML、WXSS 和 JS。而 Uniapp 则使用 Vue.js 框架,并且支持使用 HTML、CSS 和 JavaScript。
- 跨平台能力:Uniapp 的最大优势在于它的跨平台能力。使用 Uniapp 开发的小程序可以同时编译为微信小程序、支付宝小程序、百度智能小程序等多个平台的小程序,还可以编译为 H5 和原生 App。原生小程序开发则只能用于微信小程序。
- 组件库和 API:Uniapp 为了能够实现跨平台的能力,提供了一套自己的组件库和 API,这些 API 和组件在不同的平台下会被编译为对应平台的代码。而原生小程序开发则需要直接使用微信提供的组件和 API。
- 开发工具:原生小程序开发通常使用微信官方提供的微信开发者工具,这个工具提供了丰富的调试功能和性能分析工具。而 Uniapp 则可以在任何支持 Vue.js 的开发环境下开发,比如常见的 WebStorm、VS Code 等。
- 性能和灵活性:原生小程序在某些复杂场景下可能会有更好的性能,因为不存在任何的中间层。同时,对于一些微信提供的新特性和 API,原生小程序可能会更早获得支持。而 Uniapp 则需要等待更新才能支持。
深拷贝和浅拷贝
我们知道,数据类型分为两类,基本数据类型和引用数据类型。
Object、Array、Function 就是引用类型,他们的值实际是一个指针,指向一个内存地址
深拷贝浅拷贝的区别就在这里了
浅拷贝实际就是拷贝了对象的指针,没有拷贝真正的对象,所以新对象和原对象还是引用了同一个地址,这就是浅拷贝
深拷贝就是完全拷贝了原对象,新旧对象的地址完全不同
如何实现一个浅拷贝?
浅拷贝是指创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
如果属性值是基础数据类型,拷贝的就是基础数据类型的值,如果属性值是引用数据类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个内存地址,就会影响到另一个对象。
- Object.assign()、
- 数组的 slice() 或 concat()、
- 展开运算符 (...)。
需要注意的是,这些方法都只能实现浅拷贝,不能拷贝对象的深层属性。当对象的属性也是一个对象时,就需要使用深拷贝。
赋值引用是浅拷贝吗?
赋值引用和浅拷贝在处理对象或数组等引用类型数据时,都是拷贝的内存地址,而不是真正创建一个新的完全相同的数据,所以在这一点上他们是相似的,对原数据的修改都会影响到新数据。
他们的主要区别是:
赋值引用其实没有创建一个新的数据,它只是创建了一个新的引用,指向同一个数据。在这种情况下,原始数据和新引用是完全等价的,任何一方的改变都会直接影响到另一方。
而浅拷贝则是创建了一个新的数据,但这个新数据的属性(对于对象)或元素(对于数组),如果是基础类型,则直接拷贝值;如果是引用类型,则拷贝其引用(内存地址)。所以浅拷贝的结果是,新数据的顶层是新创建的,可以独立修改,但如果修改的属性或元素是引用类型的话,那就会影响到原始数据,因为它们共享同一个内存地址。
// 赋值引用
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = obj1;
obj2.a = 10;
obj2.b.c = 20;
console.log(obj1); // { a: 10, b: { c: 20 } }
// 浅拷贝
let obj3 = { a: 1, b: { c: 2 } };
let obj4 = { ...obj3 };
obj4.a = 10;
obj4.b.c = 20;
console.log(obj3); // { a: 1, b: { c: 20 } }
赋值引用的例子中,修改 obj2 的任何属性都会影响到 obj1;而在浅拷贝的例子中,修改 obj4 的顶层属性 a 不会影响到 obj3,但修改引用类型的属性 b.c 则会影响到 obj3,因为他们指向的是同一个对象。
如何实现一个深拷贝?
深拷贝就是创建一个新对象,并把原对象的所有属性和方法都复制过来,而且新旧对象间没有任何引用关系,修改新对象不会影响到原对象。
那么,如何实现一个深拷贝呢?几种常见的方式:
使用 JSON 方法
这是最简单也是最直观的深拷贝方式。
先使用 JSON.stringify 把原对象转化为字符串,再使用 JSON.parse 把这个字符串解析为新的对象。
这样新旧对象间就没有任何引用关系了。但是,这种方式有一个明显的缺陷,那就是不能复制函数和 undefined。
let obj = { a: 1, b: { c: 2 } };
let deepCopyObj = JSON.parse(JSON.stringify(obj));
递归实现
对于每个需要复制的对象,遍历它的每一个属性,判断该属性值的类型,如果是对象或数组,就递归调用深拷贝函数,如果不是,就直接复制。
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
let cloneObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}
第三方库
使用 lodash 库,它是一个具有强大功能的 JavaScript 实用工具库,其中包括深拷贝功能。
你可以使用它的 _.cloneDeep 方法来实现深拷贝。
此外,还有其他一些库可以提供深拷贝功能,如 jQuery 的 $.extend 方法,如果你的项目没有使用 jQuery,那么 lodash 可能是更好的选择。
说说你对JS单线程的理解?
JavaScript 作为一门编程语言,其执行环境(例如浏览器)提供的运行模型是单线程的。这意味着 JavaScript 代码在任何特定时间只能执行一项任务,而其他任务则需要等待当前任务完成后才能执行。这种模型被称为单线程模型。
想象你在一家餐厅就餐,这家餐厅只有一个厨师(代表 JavaScript 的运行环境)。这位厨师只能一次烹饪一个菜品(执行一个任务)。如果厨师正在烹饪一道菜(执行一个任务),那么他就无法同时烹饪其他的菜品(执行其他任务)。他必须完成当前的菜品(任务),然后才能开始烹饪下一道菜(执行下一个任务)。这就是 JavaScript 的单线程模型。
这种模型的好处在于,我们不需要担心多线程编程带来的复杂性,比如数据同步,竞态条件等问题。
但是,它的缺点也很明显,如果一个任务需要长时间执行,那么其他任务就需要等待,这可能会导致用户体验不佳。
为了解决这个问题,JavaScript 引入了事件循环和异步回调的概念,它们可以让 JavaScript 在等待某些任务(例如 I/O 操作)完成时,去执行其他的任务。这使得 JavaScript 能够有效地处理并发任务,提高应用程序的性能和用户体验。
复制一个对象有哪些方式?都有什么优缺点?
- Object.assign()
- JSON.parse(JSON.stringify(obj))
- 递归复制
- 使用函数库
Object.assign
优点:简单
缺点:仅能浅拷贝
JSON
把对象转为字符串再解析成对象
优点:简单,深拷贝
缺点:不能复制函数及特殊对象
递归复制
优点:深拷贝,无论多复杂的数据都可以处理
缺点:实现比较负责,如果有引用循环会导致死循环
函数库
例如 lodash 的 _.cloneDeep()
优点:使用方便,兼容性好
缺点:要引入外部库,增加代码依赖
什么是解构赋值?
这是 ES6 的新特性,允许我们将数据从对象或者数组中提取到变量中
数组解构
我们可以用这个特性很容易地将数组中的值赋给变量
let [a, b, c] = [1, 2, 3];
console.log(a); // 输出:1
console.log(b); // 输出:2
console.log(c); // 输出:3
变量解构
类似于数组解构,对象解构允许我们将对象的属性赋值给变量:
let {name, age} = {name: 'John', age: 30};
console.log(name); // 输出:John
console.log(age); // 输出:30
默认值
我们还能为变量设置默认值
let {a = 10, b = 5} = {a: 3};
console.log(a); // 输出:3
console.log(b); // 输出:5
Vue的生命周期钩子有哪些?在你的项目中,你通常在哪些生命周期钩子中完成哪些类型的任务?
我经常用到的生命周期钩子有created,mounted,updated,和destroyed。在created阶段,我通常会初始化数据,而在mounted阶段,我会进行DOM操作,如添加事件监听器。在updated阶段,我会监视数据的变化,如需要,进行必要的DOM更新。最后,在destroyed阶段,我会进行清理工作,如移除事件监听器。
如何判断一个对象有某种属性?
- in 运算符
- hasOwnPropetry
1、in 运算符,无论属性来自原型还是原型链
let obj = { name: 'Alice' };
console.log('name' in obj); // 输出:true
console.log('toString' in obj); // 输出:true
2、hasOwnProperty,但不会检查原型链
let obj = { name: 'Alice' };
console.log(obj.hasOwnProperty('name')); // 输出:true
console.log(obj.hasOwnProperty('toString')); // 输出:false
Vue3 和 Vue2 的区别?
- 更好的性能:新的虚拟DOM引擎和优化编译,提升了运行速度和内存效率
- 更小的提及:Vue 3.x 采用了更好的 Tree-shaking 支持,可以更有效地移除无用代码
- Composition API:更灵活的组织组件逻辑
- 更好的TS支持
- 更多新特性:Fragments(多个根节点)
- 更好的自定义指令API
set 和数组有什么区别?
数组是有序的元素集合,所有有索引,也有Length。
set 是值的集合,它不支持索引,没有length,但是会有一些方法,比如add、delete、has等
在使用Vue开发项目时,你是如何进行性能优化的?
- 组件懒加载
- 使用 v-show,减少 v-if 的使用
- 使用 key 来提升列表渲染性能
- 使用计算属性
- 优化 vuex 的使用,尽量使用局部状态
- 使用 webpack 代码分割 Code Splitting
直接给一个数组项赋值,Vue 能检测到变化吗?
你直接使用索引设置一个数组项的值,例如 vm.items[index] = newValue,Vue 是无法检测到这种变化的。
原因是 Vue.js 在初始化数据时,会为数据添加 getter 和 setter 来实现响应式,而这个过程并没有涉及到数组索引的监听。
当然你可以使用 $set的方法。当然也可以使用 push或者splice方法。
上面这些说的都是vue2的情况,在3版本中,引入了proxy作为响应式系统,这个问题就解决了。
有了proxy,vue就能直接检测到这种变化。所以vue3版本中是不需要担心的。
Vue 项目时为什么要在列表组件中写 key,其作用是什么?
如果不使用key,如果列表顺序发生变化,或者新元素添加进来,Vue 就会尝试修复和重用已经存在的元素,而不是直接销毁旧的元素并创建新的元素。这是因为创建新元素通常比修复和重用现有元素要消耗更多的资源。
然而,这种修复和重用的行为可能会导致一些不预期的问题。比如说,如果这些元素是由 Vue 组件创建的,那么每个组件可能会有自己的状态(如数据、计算属性等)。在修复和重用的过程中,这些状态可能会被错误地保留下来,导致一些无法预测的行为。
如果我们给每个元素分配了一个唯一的 key。这样,Vue 就可以明确地跟踪每个元素的身份,当数据发生变化时,Vue 就可以准确地销毁旧的元素并创建新的元素,而不是尝试去修复和重用旧的元素。这样就可以避免之前提到的状态保留的问题。
key 的值通常应该是每项都有唯一标识的属性,比如每项的 id 或者其他唯一标识。
不建议使用元素的索引作为 key,尤其是在列表顺序可以改变的情况下,因为这可能会导致性能变差,或者导致状态错误地保留到了其他元素。
Vue3中引入了Composition API作为Options API的补充,你能描述一下这两种API各自的优点,以及为何会在项目中选择使用其中的一种???
Vue3的Composition API是一个新的API,它提供了一种基于函数的方式来组织和重用代码。与Options API相比,Composition API更容易管理和重用逻辑,因为它允许我们把相关的功能组合在一起,而不是按照options(如data、methods)来组织代码。然而,Options API在组织结构上更清晰,适合较小或较简单的应用。在我的项目中,我会根据需求和团队的熟悉程度来决定使用哪种API。
get 和 post 的区别
他们是HTTP协议中最常见的两种请求方式:
参数位置:
- get 把参数放在url后面,通过?分割,参数用&连接
- post 把参数放在请求体中。
安全性方面:
- get 请求把参数暴露在url中,有安全风险
- post 在请求体中,相对安全一些
数据大小方面:
- get 受到浏览器或者服务器限制,传递数据量比较小
- post 没有这样的限制
幂等性:
- 多次 get 请求,效果是一样的
- post 会导致每次的结果都有可能不同
历史记录和缓存:
- get 会记录在浏览器历史记录中,也会被浏览器缓存
- post 没有这方面问题
谈谈你对Vue中计算属性和侦听器的理解,以及你在实际开发中如何选择使用这两者的?
计算属性和侦听器都是用来处理响应式数据的。计算属性主要用于计算值,当依赖的数据变化时,计算属性会自动重新计算。侦听器主要用于监听数据变化,当数据变化时,侦听器会自动执行回调函数。在实际开发中,我通常优先选择使用计算属性,因为它可以使代码更清晰,更符合数据驱动的原则。只有当我需要在数据变化时执行异步操作或较复杂的操作时,我才会使用侦听器。
你了解哪些设计模式?
工厂模式
用于创建具有相同属性和方法的多个对象。
比如,我们可能需要创建多个 DOM 节点,它们都有相同的属性和方法,这时可以使用工厂模式。
这样减少了直接创建对象时的复杂性,尤其是当创建对象涉及到一些初始化操作的时候。此外,它也提供了更好的封装性和复用性。
单例模式
单例模式保证一个类只有一个实例,并提供一个全局访问点。
例如,我们在构建一个前端应用时,可能需要一个全局的配置对象,这个配置对象可以使用单例模式来创建,确保整个应用中只有一个全局配置对象。
确保全局只有一个实例存在,有利于进行资源的统一管理和控制,避免因多个实例而产生的资源浪费或者状态冲突。
观察者模式
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当主题对象发生变化时,它的所有依赖者都会收到通知并更新。Vue.js 的响应式系统就是观察者模式的一种实现。
实现了低耦合、非侵入式的通知与更新机制。
模块模式
JavaScript 中的模块模式被广泛用于创建封装特定功能的独立组件。
这种模式在现代前端开发中十分常见,比如 ES6 的模块系统就是这种模式的一种实现。
装饰器模式
装饰器模式可以在不改变对象自身的基础上,动态地添加功能。
在 React 的高阶组件(HOC)就是装饰器模式的一种实现,它可以给现有的组件添加额外的功能。
策略模式
策略模式定义了一系列的算法,并将每一个算法封装起来,使他们可以相互替换。
例如,在进行表单验证时,可以为每一种验证定义一个策略,如邮箱验证、手机号验证等。
命令模式
命令模式将请求封装成一个对象,由发送者和接收者解耦。这在 Redux 的 action 中可以看到体现,action 事实上就是一个发送给 reducer 的命令。
在开发大型应用时,你是如何组织和管理你的Vue组件的?你是如何决定何时需要新建一个组件,何时应该重用一个现有的组件?
- 文件和目录结构:在项目的根目录下创建一个专门存放所有组件的目录,如
components。这个目录可以进一步根据功能、页面或组件类型(如基础组件、布局组件、容器组件等)分成子目录。对于特定功能或页面专用的组件,可以考虑与对应的 Vuex 模块或路由一起放在一个特定的目录中。 - 命名规范:给组件使用具有描述性的名称,可以考虑遵循某种命名规范或模式,如帕斯卡命名(PascalCase)或短划线分隔命名(kebab-case)。如果组件之间有层级关系,可以在名称中体现出来,如
UserList和UserListItem。 - 组件抽象:对于重复出现的 UI 模式,应该尽量抽象成可重用的组件。考虑创建一些基础组件(如按钮、输入框、卡片等),这些组件应该尽可能的纯粹和无状态,不包含业务逻辑,只通过 props 接收数据和事件。
- 组件拆分:在组件变得过于复杂或庞大时,应该考虑拆分成更小的子组件。一个好的经验法则是单一职责原则(SRP):每个组件只做一件事情,并做好它。
- 使用 props 和事件进行父子组件通信:避免在子组件中直接修改 props,而应该通过事件向父组件报告需要的更改。
- 使用 Vuex 管理应用状态:将共享的或全局的状态放在 Vuex 中,可以让组件保持简洁和无状态,更容易测试和复用。
- 利用 Vue 的混入(mixins)和插槽(slots)功能:对于一些跨多个组件的共享逻辑,可以考虑使用混入进行复用。对于一些需要自定义内容的通用组件,可以使用插槽来实现。
什么时候需要创建一个新组件呢?
- 代码中有重复的模式,或者预计将来会需要重用某个功能模块时,应该考虑创建一个新的组件。
- 一个组件的代码变得过长或者复杂时,应该进行拆分
- 一个更大的视角来考虑用户体验时,比如动画、布局、交互方式
对于JavaScript的异步编程,回调函数和Promise有何异同?在你的实际项目中,你更偏向于使用哪一种,为什么?
相同点很简单,就是用来做异步操作的
主要来说说不同的地方:
回调函数
这是一种比较早的操作方式
本质就是把一个函数作为参数传给另一个函数
但是这样会变成回调地狱,导致代码难以理解和维护
Promise
这是ES6引入的新方法,返回一个对象,一个尚未完成的操作,解决了回调地狱的问题
它还有其他优势,比如可以 catch 来捕获错误,可以使用 async/await
前端有哪些常见的攻击,你如何预防?
XSS(跨站脚本攻击)
通常发生在一个网站允许用户输入 HTML 或者 JavaScript 代码,并在其他用户的浏览器中执行这些代码。这就给了攻击者一个机会,他们可以插入恶意代码,从而窃取用户数据或者进行其他破坏行为。
常见的防御就是对所有用户输入内容进行转义,避免HTML标签和JS代码被解析执行
CSRF(跨站请求伪造)
利用用户已登录身份,诱导用户去点击一个链接或者加载一个图片,这样会向受攻击者的网站发送一个请求,这个请求里包含了用户身份凭证,被认为是一个正常请求
使用Token进行防御,这种Token会随每次请求一起发送到服务器。因为攻击者无法获取这个Token,所以他们也就无法模仿用户发起请求。另外,我还会尽量避免在GET请求中修改数据。
点击劫持
利用透明、不可见的元素让用户在不知情的情况下重新排列了网页元素,诱导他们进行其他操作
在HTTP响应头中设置X-Frame-Options来防止页面被嵌入到其他网站的iframe中。
中间人攻击
攻击用户与服务器之间的通信,窃取或者篡改信息
要确保所有通信为HTTPS,攻击者就无法解析传输数据了
SQL注入
这是后端问题,通过输入特殊字符,破坏SQL语句执行
在前端要尽量避免拼接SQL语句,如果要使用,就需要保证SQL语句不会被解析执行
你是如何防范这些攻击的?
说说闭包和它的应用场景
当一个函数访问了它的词法作用域之外的变量,就创建了一个闭包。
这种特性在很多场景中都很有用,比如在创建私有变量情况下。在我的项目中,我通常使用闭包来创建模块,这样可以隐藏内部实现,只暴露必要的接口。
解释一下原型和原型链
你能描述一下JavaScript的Promises和async/await吗?你在项目中是如何处理异步操作的?
JavaScript的Promises和async/await都是处理异步操作的工具。Promise提供了一种方式来处理异步操作的完成和失败。而async/await则是基于Promise的语法糖,它允许我们以同步的方式来写异步代码,使得代码更易读和理解。在我的项目中,我通常使用async/await来处理异步操作,因为它使得代码更简洁。
对CSS动画了解多少?
CSS3中,主要的动画方式有两种:transition 和 animation
Transition
过渡是一种简单动画,允许我们在一定时间改变元素的某些属性
比如悬停改变按钮颜色
Animation
允许我们指定动画序列,和每一步的详细状态和时间
比如做一个加载动画,让一个图标在一定时间内旋转360度
复杂的动画,还是建议我们使用js库,比如 anime.js
移动设备上要尤其注意性能,因为移动端的处理能力通常低于桌面设备
JavaScript的Event Loop是什么?请描述一下它是如何影响JavaScript异步行为的?
JavaScript的Event Loop是JavaScript引擎的执行模型。JavaScript是单线程的,所以它使用Event Loop来处理异步任务。Event Loop包含一个任务队列,当有异步任务(例如setTimeout、Promise)完成时,会将回调函数加入到任务队列。当主线程空闲时,就会从任务队列中取出一个任务执行。这就是为什么JavaScript的异步任务不会立即执行,而是等到当前的执行栈为空时才执行。 1.