2024web前端开发面试题
- 1.元素水平垂直居中
- 2.vue2和vue3的生命周期
- 3.vue2和vue3的双向绑定
- 4. 浏览器从输入地址展示都有什么阶段
- 5.三次握手过程
- 6.cookie和seesion区别
- 7.vuex的配置及作用
- 8.vue2和vue3的组件间传值方式
- 9.js如何继承
- 10.防抖和节流
- 11.闭包的实现以及作用
- 12.箭头函数和普通函数的区别
- 13.cookie、localStorage和sessionStorage的区别
- 14.深浅拷贝及深拷贝json.parse的副作用
- 15.字符串数组的常用方法
- 16.前端怎么解决跨域
- 17.不想让浏览器缓存怎么实现
- 18.vue中watch和computer的区别
- 19.vue的双向绑定什么场景下不生效
- 20.绝对定位和相对定位都是相对于什么定位的
- 21. vue中如何防范xss和csrf
1.元素水平垂直居中
1)absolute + transform(父相子绝)
.parent{
position: relative;
width: 500px;
height: 500px;
border: 1px solid blue;
}
.child{
background: green;
/* 核心代码 */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
2)ine-height + vertical-align
把当前元素设置为行内元素,然后通过设置父元素的 text-align: center来实现水平居中;同时通过设置当前元素的 vertical-align: middle来实现垂直居中;最后设置当前元素的 line-height: initial来继承父元素的line-height
.parent{
width: 500px;
border: 1px solid blue;
/* 核心代码 */
line-height: 500px;
text-align: center;
}
.child{
background: green;
/* 核心代码 */
display: inline-block;
vertical-align: middle;
line-height: initial;
}
3)flex 布局(推荐)
.parent{
width: 500px;
height: 500px;
border: 1px solid blue;
/* 核心代码 */
display: flex;
/* 水平居中 */
justify-content: center;
/* 垂直居中 */
align-items: center;
}
.child{
background: green;
}
4)flex + margin auto
.parent{
width: 500px;
height: 500px;
border: 1px solid blue;
/* 核心代码 */
display: flex;
}
.child{
background: green;
/* 核心代码 */
margin: auto;
}
2.vue2和vue3的生命周期
vue2 | vue3 | |
---|---|---|
beforeCreate | setup | 在实例初始化之后,数据观测和事件配置之前被调用。此时实例还未完成初始化,无法访问到data、computed、watch、methods等属性和方法 |
created | setup | 在实例创建完成后被立即调用。此时,实例已完成了数据观测和属性计算,同时也完成了methods和watch/event事件的配置。 |
beforeMount | onBeforeMount | 在挂载开始之前被调用,此时模板已经编译完成,但尚未渲染成html。 |
mounted | onMounted | 在实例挂载到页面之后调用。此时真实DOM已经渲染完成,可以进行DOM操作。 |
beforeUpdate | onBeforeUpdate | 在数据更新之前被调用,该方法可用于在更新之前访问现有的DOM,例如手动保存滚动位置。 |
updated | onUpdated | 在数据更新之后被调用,该方法还会在组件的子组件更新之后被调用。 |
beforeDestroy | onBeforeUnmount | 在实例销毁之前调用。此时实例仍然完全可用。 |
destroyed | onUnmounted | 在实例销毁之后调用。此时实例所有的指令和事件都已经卸除,所有的子实例都已经被销毁。 |
activated | onActivated | 被包含在 中的组件,会多出两个生命周期钩子函数,被激活时执行;(面试可不答) |
deactivated | onDeactivated | 比如从 A 组件,切换到 B 组件,A 组件消失时执行(面试可不答) |
errorCaptured | onErrorCaptured | 当捕获一个来自子孙组件的异常时激活钩子函数。(面试可不答) |
vue2 -------> vue3
beforeCreate(){} --------> setup(()=>{})
created(){} --------> setup(()=>{})
beforeMount(){} --------> onBeforeMount(()=>{})
mounted(){} --------> onMounted(()=>{})
beforeUpdate(){} --------> onBeforeUpdate(()=>{})
updated(){} --------> onUpdated(()=>{})
beforeDestroy(){} --------> onBeforeUnmount(()=>{})
destroyed(){} --------> onUnmounted(()=>{})
activated(){} --------> onActivated(()=>{})
deactivated(){} --------> onDeactivated(()=>{})
errorCaptured(){} --------> onErrorCaptured(()=>{})
3.vue2和vue3的双向绑定
Vue.js的双向数据绑定是其核心特性之一,无论是Vue 2.x还是Vue 3.x版本,都沿用了这一设计。双向绑定基于MVVM(Model-View-ViewModel)模式,简单来说就是当模型(model)的数据发生变化时,视图(view)会自动更新;反之,用户在视图上做出交互也会实时反映到模型中。
Vue 2.0 双向绑定原理:
响应式属性:Vue会在初始化数据时,将所有可写的data属性标记为“响应式”,它们都会创建相应的getter和setter方法。
劫持Object.defineProperty: 当访问或修改这些响应式属性时,Vue内部会检测并更新相应地进行数据更新。
深度检测:Vue能检测数组和对象的深度变化,并处理这种情况下的更新。
使用Object.defineProperty()做一个简单的功能实现:
let obj = {
name: ''
};
// 深克隆obj
let newObj = JSON.parse(JSON.stringify(obj));
Object.defineProperty(obj, 'name', {
// 这里相当于给数据设置set get
get() {
return newObj.name
},
set(val) {
if (val === obj.name) return;
newObj.name = val;
observer()
}
})
function observer() {
spanName.innerHTML = obj.name
inpName.value = obj.name
}
observer();
setTimeout(() => {
obj.name = inpName.value
}, 1000)
// 这里在vue相当于v-model
inpName.oninput = function () {
obj.name = this.value
}
Vue 3.0 双向绑定原理:
虚拟DOM:Vue 3继续使用虚拟DOM技术,提高性能。不过,核心变化在于移除了依赖于Object.defineProperty的依赖,转而采用了自定义的响应式系统,叫做Reactive。
Composition API:引入了更灵活的API组合策略(Composition API),允许开发者更好地控制组件间的依赖和状态管理。
Proxy:Vue 3使用JavaScript的Proxy代理对象来实现对数据的深层次监听,这使得Vue可以在不直接改变原始数据的情况下观察和修改数据。
使用Proxy()做一个简单的功能实现:
new Proxy(target, {
get(target, key, receiver) {
// 依赖收集等逻辑可以放在这里(Vue内部实现)
console.log(`Getting ${key}`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// 触发更新等逻辑可以放在这里(Vue内部实现)
console.log(`Setting ${key} to ${value}`);
const result = Reflect.set(target, key, value, receiver);
// 假设这里有一个通知更新的函数
notifyUpdate(key);
return result;
}
}
4. 浏览器从输入地址展示都有什么阶段
浏览器从输入地址到展示页面的过程,可以清晰地归纳为以下几个阶段:
1)URL解析
用户输入:用户在浏览器的地址栏中输入网址(URL)。
URL解析:浏览器首先解析输入的URL,确保URL格式正确,并提取出协议(如http、https)、域名(domain)和端口号(如果有的话)等信息。
2) 缓存检查
浏览器缓存:浏览器检查自身缓存中是否存在请求资源的副本。如果存在且未过期,则直接从缓存中读取并展示页面,跳过后续步骤。
系统缓存和路由器缓存:如果浏览器缓存中没有找到,可能会进一步检查系统缓存和路由器缓存。
3)DNS解析
域名解析:如果缓存中没有找到资源,浏览器会进行DNS解析,将域名转换为对应的IP地址。
浏览器缓存:首先检查浏览器自身的DNS缓存。
系统缓存:然后检查操作系统的DNS缓存。
hosts文件:接着检查本地的hosts文件,看是否有对应的IP地址映射。
DNS查询:如果以上都没有找到,浏览器会向本地DNS服务器发送DNS查询请求,本地DNS服务器可能继续向根DNS服务器、顶级域名服务器、权限域名服务器发起查询,最终获得IP地址。
4)TCP连接
三次握手:浏览器使用解析到的IP地址,通过TCP三次握手协议与目标服务器建立连接,确保数据传输的可靠性和正确性。
5)发送HTTP请求
构建请求:浏览器构建HTTP请求,包含请求的资源(如HTML页面)、HTTP版本、请求头等信息。
发送请求:通过已建立的TCP连接,浏览器向服务器发送HTTP请求。
6)服务器处理请求
接收请求:服务器接收并解析HTTP请求。
处理请求:根据请求的内容,服务器进行相应的处理,如查询数据库、执行服务器端逻辑等。
生成响应:服务器将处理结果封装成HTTP响应,包含响应状态码、响应头、响应体(如HTML页面、图片等)等信息。
7)浏览器接收响应
接收数据:浏览器接收HTTP响应。
解析响应:浏览器解析响应内容,如果是HTML页面,则解析HTML结构,并请求页面中引用的其他资源(如CSS、JavaScript、图片等)。
8)页面渲染
构建DOM树:浏览器将HTML解析成DOM树。
构建CSSOM树:解析CSS样式,构建CSSOM树。
渲染树构建:将DOM树和CSSOM树合并成渲染树(Render Tree)。
布局(Reflow):计算渲染树中每个节点的位置和大小。
绘制(Paint):将渲染树的内容绘制到屏幕上。
9)JavaScript执行
执行脚本:如果页面中包含JavaScript代码,浏览器会执行这些代码,实现交互和动态效果。
10)页面显示
完成渲染:经过上述步骤,页面最终渲染完成并展示给用户。
这个过程是浏览器从输入网址到展示页面的完整流程,每个阶段都涉及到复杂的网络协议和浏览器内部机制。
5.三次握手过程
1) 第一次握手
发起方(客户端):客户端向服务器发送一个SYN(同步序列编号)包,请求建立连接。这个SYN包中,SYN标志位被设置为1,同时客户端会随机生成一个序列号(seq=x),并将这个序列号放入SYN包中发送给服务器。客户端发送完SYN包后,进入SYN_SEND状态,等待服务器的确认。
作用:客户端通过发送SYN包来告诉服务器,它想要建立连接,并初始化了一个序列号x用于后续的数据传输。
2)第二次握手
接收方(服务器):服务器收到客户端的SYN包后,首先会确认SYN包的有效性(即SYN=1),然后为这个连接分配缓存和变量,并向客户端发送一个SYN+ACK(同步序列编号+确认)包作为响应。在这个SYN+ACK包中,ACK标志位被设置为1,确认号(ack)被设置为客户端的序列号加1(ack=x+1),表示服务器已经收到了客户端的SYN包。同时,服务器也会生成一个自己的序列号(seq=y),并将这个序列号放入SYN+ACK包中发送给客户端。服务器发送完SYN+ACK包后,进入SYN_RECV状态。
作用:服务器通过发送SYN+ACK包来告诉客户端,它已经收到了客户端的连接请求,并确认了这个请求。同时,服务器也初始化了一个序列号y用于后续的数据传输。
3)第三次握手
发起方(客户端):客户端收到服务器的SYN+ACK包后,会检查这个包的有效性(即SYN=1且ACK=1,ack=x+1)。如果检查通过,客户端会向服务器发送一个ACK包作为最后的确认。在这个ACK包中,ACK标志位被设置为1,确认号(ack)被设置为服务器的序列号加1(ack=y+1),表示客户端已经收到了服务器的SYN+ACK包。此时,客户端的序列号也会自增(seq=x+1),但这个序列号在这个ACK包中并不重要,因为ACK包本身不包含数据。客户端发送完ACK包后,进入ESTABLISHED状态,表示连接已经建立。
服务器:服务器收到客户端的ACK包后,也会检查这个包的有效性。如果检查通过,服务器也进入ESTABLISHED状态,表示连接已经成功建立。
作用:客户端通过发送ACK包来告诉服务器,它已经收到了服务器的SYN+ACK包,并确认了这个连接。至此,三次握手过程完成,客户端和服务器之间建立起了可靠的连接。
6.cookie和seesion区别
1)数据存放位置不同:
cookie数据存放在客户的浏览器上,session数据放在服务器上。
2)安全程度不同:
cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用session。
3)性能使用程度不同:
session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
4)数据存储大小不同:
单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,而session则存储与服务端,浏览器对其没有限制。
5)会话机制不同
session会话机制:session会话机制是一种服务器端机制,它使用类似于哈希表(可能还有哈希表)的结构来保存信息。
cookies会话机制:cookie是服务器存储在本地计算机上的小块文本,并随每个请求发送到同一服务器。 Web服务器使用HTTP标头将cookie发送到客户端。在客户端终端,浏览器解析cookie并将其保存为本地文件,该文件自动将来自同一服务器的任何请求绑定到这些cookie。
7.vuex的配置及作用
在 Vuex 中,Module、state、mutation、action、getter 是构成 Vuex 状态管理的核心概念,它们各自扮演着不同的角色。下面我将逐一解释这些概念,并给出相应的例子。
1) Module
Module 允许你将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。
例子:
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementIfOddOnRootSum({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
},
getters: {
doubleCount(state) {
return state.count * 2
}
}
}
const store = new Vuex.Store({
modules: {
a: moduleA
},
state: {
count: 0
}
})
在这个例子中,我们定义了一个名为 moduleA 的模块,它有自己的 state、mutations、actions 和 getters。然后,我们将这个模块添加到 Vuex store 的 modules 选项中。
2)State
State 是 Vuex 管理的状态对象,它是响应式的。Vue 组件可以从 store 中读取状态,并且当状态变更时,视图也会自动更新。
例子(已在上面的 Module 例子中展示):
state: () => ({
count: 0
})
3) Mutation
Mutation 是更改 Vuex store 中状态的唯一方法。Vuex 中的 mutation 必须是同步函数。
例子(已在上面的 Module 例子中展示):
mutations: {
increment(state) {
state.count++
}
}
4)Action
Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作,调用接口可以在这里执行。
例子:
javascript
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit(‘increment’)
}, 1000)
}
}
5)Getter
Getter 允许组件从 store 中派生一些状态。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
例子(已在上面的 Module 例子中展示):
javascript
getters: {
doubleCount(state) {
return state.count * 2
}
}
在 Vue 组件中,你可以通过 this.
s
t
o
r
e
.
g
e
t
t
e
r
s
.
d
o
u
b
l
e
C
o
u
n
t
来访问这个
g
e
t
t
e
r
。如果你使用了
V
u
e
x
的辅助函数,你也可以在组件的计算属性中直接返回
t
h
i
s
.
store.getters.doubleCount 来访问这个 getter。如果你使用了 Vuex 的辅助函数,你也可以在组件的计算属性中直接返回 this.
store.getters.doubleCount来访问这个getter。如果你使用了Vuex的辅助函数,你也可以在组件的计算属性中直接返回this.store.getters.doubleCount,这样它就会像本地计算属性一样被缓存。
8.vue2和vue3的组件间传值方式
Vue 2 组件间传值
1)父传子 - Props
父组件通过props向子组件传递数据。
父组件:
<template>
<div>
<child-component :message="parentMessage"></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
parentMessage: 'Hello from parent'
}
}
}
</script>
子组件 (ChildComponent.vue):
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
props: ['message']
}
</script>
2)子传父 - Events
子组件通过$emit触发事件,父组件监听这个事件来接收数据。
子组件:
<template>
<button @click="notifyParent">Notify Parent</button>
</template>
<script>
export default {
methods: {
notifyParent() {
this.$emit('update:message', 'Hello from child');
}
}
}
</script>
父组件:
<template>
<div>
<child-component @update:message="handleUpdate"></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
methods: {
handleUpdate(message) {
console.log(message); // 输出: Hello from child
}
}
}
</script>
3) 兄弟组件间通信 - Event Bus 或 Vuex
对于兄弟组件间的通信,Vue 2官方没有直接提供方法,但可以使用Event Bus(一个空的Vue实例)或Vuex进行状态管理。
Event Bus 示例(不推荐,Vue 3中更倾向使用Provide/Inject或Vuex):
// eventBus.js
import Vue from 'vue';
export const EventBus = new Vue();
// 组件A发送
EventBus.$emit('message', 'Hello from A');
// 组件B接收
created() {
EventBus.$on('message', this.handleMessage);
},
beforeDestroy() {
EventBus.$off('message', this.handleMessage);
},
methods: {
handleMessage(message) {
console.log(message);
}
}
4)作用域插槽传值(子传父)
在 Vue 2 中,slot 主要是用来分发内容的,它本身并不直接支持像 props 那样的数据传递机制。不过,你可以通过作用域插槽(Scoped Slots)来实现类似传值的效果。作用域插槽允许子组件将数据“暴露”给父组件的插槽内容。
作用域插槽的基本用法
子组件定义作用域插槽:在子组件的模板中,使用 v-slot:slotName=“slotProps” 的形式来定义作用域插槽,并通过 slotProps 传递数据给父组件。
父组件接收作用域插槽数据:在父组件中,使用带有 v-slot:slotName=“slotProps” 的 标签来接收子组件传递的数据。
示例
子组件 (ChildComponent.vue)
<template>
<div>
<slot name="custom" :user="user"></slot>
</div>
</template>
<script>
export default {
data() {
return {
user: { name: 'John Doe', age: 30 }
}
}
}
</script>
在这个例子中,子组件 ChildComponent 通过作用域插槽 custom 传递了一个 user 对象给父组件。
父组件 (ParentComponent.vue)
<template>
<child-component>
<template v-slot:custom="slotProps">
<p>User Name: {{ slotProps.user.name }}</p>
<p>User Age: {{ slotProps.user.age }}</p>
</template>
</child-component>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
}
}
</script>
在父组件中,我们通过 接收了子组件传递的 user 对象,并通过 slotProps 访问这个对象的数据。
注意事项
在 Vue 2.6.0+ 中,v-slot 是新的语法糖,用于替代旧的 slot 和 slot-scope 指令。但在 Vue 2.5.x 及更早版本中,你需要使用 slot-scope 而不是 v-slot。
作用域插槽允许你将子组件中的数据“暴露”给父组件的插槽内容,从而实现了类似传值的效果,但本质上这是通过模板的渲染上下文来传递的。
在 Vue 3 中,v-slot 指令的语法保持不变,但 Vue 3 的 Composition API 提供了更多的灵活性和选项来处理组件间的数据共享和通信。
5)ref传值(父传子)
在 Vue 2 中,ref 主要用于给元素或子组件注册引用信息,以便在 Vue 实例的 $refs 对象中直接访问它们。ref 本身并不是直接用来“传值”的,但它可以用来访问子组件的实例或 DOM 元素,进而可以调用子组件的方法或访问子组件的数据,间接实现数据的传递或交互。
如果你想要通过 ref 来“传值”给子组件,你实际上是在父组件中通过 $refs 访问子组件实例,然后调用子组件的方法来传递数据,或者修改子组件的 props(虽然直接修改 props 是不推荐的,但可以通过触发事件来更新)。
示例:
父组件
<template>
<div>
<child-component ref="childRef"></child-component>
<button @click="sendDataToChild">Send Data to Child</button>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
methods: {
sendDataToChild() {
// 通过 $refs 访问子组件实例,并调用其方法或修改其数据
this.$refs.childRef.receiveData('Hello from Parent');
}
}
}
</script>
子组件
<template>
<div>
<p>Data from Parent: {{ dataFromParent }}</p>
</div>
</template>
<script>
export default {
data() {
return {
dataFromParent: ''
}
},
methods: {
receiveData(data) {
// 接收来自父组件的数据
this.dataFromParent = data;
}
}
}
</script>
在这个例子中,父组件通过 $refs.childRef 访问了子组件的实例,并调用了子组件的 receiveData 方法来传递数据。子组件则通过修改其内部数据 dataFromParent 来接收并显示这些数据。
注意
直接通过 $refs 修改子组件的 props 或内部状态通常不是最佳实践,因为它违反了组件之间的数据流和封装原则。更推荐的方式是使用自定义事件来通信(子组件触发事件,父组件监听事件并作出响应)。
在 Vue 3 中,虽然基本概念相同,但组合式 API(如 ref 和 reactive)的引入为状态管理和组件间通信提供了更多的灵活性和选项。然而,对于组件间的直接引用和通信,ref 的用法在 Vue 2 和 Vue 3 之间是相似的。
6)pubsub
发布消息
在需要发送数据的组件中,使用 PubSub.publish 方法来发布消息。这个方法接受两个参数:一个是主题(字符串),另一个是要发送的数据(可以是任何类型)。
export default {
methods: {
sendDataToOtherComponents() {
const message = { text: 'Hello from Component A!' };
PubSub.publish('myChannel', message);
}
}
}
订阅消息
在需要接收数据的组件中,使用 PubSub.subscribe 方法来订阅一个主题,并提供一个回调函数来处理接收到的数据。
export default {
mounted() {
// 订阅 myChannel 主题,并注册回调函数
this.token = PubSub.subscribe('myChannel', (msg, data) => {
console.log(data); // 输出: { text: 'Hello from Component A!' }
// 在这里处理接收到的数据
});
},
beforeDestroy() {
// 组件销毁前取消订阅,避免内存泄漏
if (this.token) PubSub.unsubscribe(this.token);
}
}
注意事项
确保在组件销毁前取消订阅,以避免内存泄漏。
PubSub.subscribe 方法返回一个 token,用于后续的取消订阅。
使用发布/订阅模式时,确保你正确管理消息主题,避免命名冲突。
在大型项目中,过度使用发布/订阅模式可能会导致代码难以维护,因此请考虑使用 Vuex 或其他状态管理库。
通过这种方式,你可以在 Vue 2 的不同组件之间灵活地传递数据,而无需直接引用组件实例或使用其他复杂的解决方案。
vue3组件间传值
在Vue 3中,组件间的传值方式多种多样,每种方式都有其特定的使用场景和优势。以下是对Vue 3中所有主要组件间传值方式的清晰归纳和解释:
1)Props 和 Emit
父向子传值:
Props:父组件通过属性绑定的方式向子组件传递数据。在子组件中,通过defineProps(或在<script setup>
中直接使用props)来接收这些数据。
示例:父组件<ChildComponent :info="parentData" />
,子组件const props = defineProps({ info: String })。
子向父传值:
Emit:子组件通过触发事件的方式向父组件传递数据。在子组件中,使用defineEmits来定义可以触发的事件,并通过context.emit
(或在<script setup>
中解构emit)来触发事件。
示例:子组件const emit = defineEmits(['update']); emit('update', newData);
,父组件<ChildComponent @update="handleUpdate" />
。
2) v-model
双向绑定:
v-model:在Vue 3中,v-model可以实现父子组件间的数据双向绑定。默认情况下,v-model绑定的是modelValue
属性和update:modelValue
事件,但也可以通过v-model:xxx
来自定义绑定的属性和事件。
示例:父组件<ChildComponent v-model:pageNo="pageNo" />
,子组件通过$emit('update:pageNo', newValue)
来更新pageNo
。
3)Provide 和 Inject
跨代组件传值:
Provide
:在祖先组件中使用provide函数来提供数据或方法,这些数据或方法可以被后代组件通过inject来接收。
Inject
:在后代组件中,使用inject函数来接收祖先组件通过provide提供的数据或方法。
示例:祖先组件provide('key', value)
,后代组件const injectedValue = inject('key')
。
4)Vuex/pinia
全局状态管理:
Vuex:对于大型应用,推荐使用Vuex来进行全局状态管理。Vuex允许你在不同的组件之间共享状态,并通过mutations、actions等方式来更新状态。
其他库:如Pinia,也是一个轻量级、模块化的Vue状态管理库,可以作为Vuex的替代方案。
Store(存储):
Store 是 Pinia 中最基本也是最重要的部分,它是一个容器,用于保存和管理应用的状态(state)以及与之相关的逻辑(如 actions 和 getters)。
每个 Store 都是独立的,可以在多个组件之间共享。
State(状态):
State 是 Store 中的核心部分,它代表了应用的状态。
State 是一个函数,该函数返回一个对象,该对象包含了所有的状态属性。
在组件中,可以通过计算属性(computed)或直接访问 Store 的状态来获取和显示状态值。
Getters(计算属性):
Getters 类似于 Vue 组件中的计算属性(computed)。
它们是基于 state 的派生值,用于对 state 进行转换或计算。
Getters 可以是同步的,并且具有缓存功能,只有在依赖的 state 发生变化时才会重新计算。
Actions(动作):
Actions 是用于修改 state 的函数。
它们可以是同步的,也可以是异步的。
Actions 提供了更灵活的方式来处理状态变更的逻辑,包括调用其他 actions、getters 或进行 API 请求等。
创建 Pinia Store
在你的 Vue 3 项目中,你需要创建一个或多个 Pinia store 来管理状态。Store 是用于封装状态、修改状态的逻辑(actions)和计算属性(getters)的地方。
// stores/useCounterStore.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
incrementBy(amount) {
this.count += amount
}
}
})
在 Vue 组件中使用 Pinia Store
接下来,你可以在 Vue 组件中通过 useCounterStore 钩子来访问和修改这个 store 的状态。
<template>
<div>
<p>Count is: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
<button @click="incrementBy(10)">Increment by 10</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useCounterStore } from './stores/useCounterStore'
const counterStore = useCounterStore()
// 直接访问 state
const count = computed(() => counterStore.count)
// 调用 actions
const increment = () => counterStore.increment()
const decrement = () => counterStore.decrement()
const incrementBy = (amount) => counterStore.incrementBy(amount)
</script>
5)attrs和listeners
透传属性和事件:
**attrs∗∗:包含了父作用域中不作为组件props被识别(且获取)的特性绑定(class和style除外)。当一个组件没有声明任何props时,这里会包含所有父作用域的绑定(class和style除外),并且可以通过‘v−bind=“attrs” 传入内部组件。 **listeners∗∗:包含了父作用域中的(不含.native修饰器的)v−on事件监听器。它可以通过‘v−on="listeners" 传入内部组件。但在Vue 3的<script setup>中,通常使用useAttrs和useListeners(虽然useListeners不是Vue核心API的一部分,但可以通过组合式API实现类似功能)或简单地通过attrs‘和‘emit
来处理。
6)ref 和 $parent
直接访问组件实例:
提及到ref可能会想到它可以获取元素的DOM或者获取子组件实例的VC。既然可以在父组件内部通过ref获取子组件实例VC,那么子组件内部的方法与响应式数据父组件可以使用的。
父组件代码:
<template>
<div>
<h1>ref与$parent</h1>
<Son ref="son"></Son>
</div>
</template>
<script setup lang="ts">
import Son from "./Son.vue";
import { onMounted, ref } from "vue";
const son = ref();
onMounted(() => {
console.log(son.value);
});
</script>
但是需要注意,如果想让父组件获取子组件的数据或者方法需要通过defineExpose对外暴露,因为vue3中组件内部的数据对外“关闭的”,外部不能访问
子组件代码:
<script setup lang="ts">
import { ref } from "vue";
//数据
let money = ref(1000);
//方法
const handler = ()=>{
}
defineExpose({
money,
handler
})
</script>
$parent可以获取某一个组件的父组件实例VC,因此可以使用父组件内部的数据与方法。必须子组件内部拥有一个按钮点击时候获取父组件实例,当然父组件的数据与方法需要通过defineExpose方法对外暴露
<button @click="handler($parent)">点击我获取父组件实例</button>
7)插槽
可以理解为,子组件数据由父组件提供,但是子组件内部决定不了自身结构与外观(样式)
子组件Todo代码如下:
<template>
<div>
<h1>todo</h1>
<ul>
<!--组件内部遍历数组-->
<li v-for="(item,index) in todos" :key="item.id">
<!--作用域插槽将数据回传给父组件-->
<slot :$row="item" :$index="index"></slot>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
defineProps(['todos']);//接受父组件传递过来的数据
</script>
<style scoped>
</style>
父组件内部代码如下:
<template>
<div>
<h1>slot</h1>
<Todo :todos="todos">
<template v-slot="{$row,$index}">
<!--父组件决定子组件的结构与外观-->
<span :style="{color:$row.done?'green':'red'}">{{$row.title}}</span>
</template>
</Todo>
</div>
</template>
<script setup lang="ts">
import Todo from "./Todo.vue";
import { ref } from "vue";
//父组件内部数据
let todos = ref([
{ id: 1, title: "吃饭", done: true },
{ id: 2, title: "睡觉", done: false },
{ id: 3, title: "打豆豆", done: true },
]);
</script>
<style scoped>
</style>
9.js如何继承
1) 原型链继承(Prototype Inheritance)
这是JavaScript中最基本的继承方式,通过原型链实现。子类型的原型是一个对象,这个对象的原型是父类型的实例。
function Parent() {
this.name = 'Parent';
}
Parent.prototype.sayHello = function() {
console.log('Hello from ' + this.name);
};
function Child() {
this.age = 10;
}
// Child继承自Parent
Child.prototype = new Parent();
// 重置constructor指向
Child.prototype.constructor = Child;
var child = new Child();
child.sayHello(); // Hello from Parent
console.log(child.age); // 10
2) 借用构造函数继承(Constructor Stealing/Borrowing)
这种方式又被称为伪造对象或经典继承。通过在子类型构造函数中调用父类型构造函数,可以使用父类型的构造函数来增强子类型实例。
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name); // 继承Parent
this.age = age;
}
var child = new Child('Child', 5);
console.log(child.name); // Child
console.log(child.age); // 5
3) 组合继承(Combination Inheritance)
组合继承是原型链继承和借用构造函数继承的组合。它使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。这种方式避免了原型链继承和借用构造函数继承的缺点。
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHello = function() {
console.log('Hello from ' + this.name);
};
function Child(name, age) {
Parent.call(this, name); // 继承实例属性
this.age = age;
}
Child.prototype = new Parent(); // 继承原型属性和方法
Child.prototype.constructor = Child; // 修复constructor
var child = new Child('Child', 5);
child.sayHello(); // Hello from Child
console.log(child.age); // 5
4) 原型式继承(Prototypal Inheritance)
这种方式基于现有的对象来创建新对象,而不是使用构造函数。它通过Object.create()方法实现。
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
console.log(anotherPerson.friends); // ["Shelby", "Court", "Van"]
5) 寄生式继承(Parasitic Inheritance)
寄生式继承是通过创建一个仅用于封装继承过程的函数来实现的,该函数在内部以某种方式来增强对象,最后返回这个对象。
6) 寄生组合式继承(Parasitic Combination Inheritance)
寄生组合式继承是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的思想是:不必为了指定子类型的原型而调用父类型的构造函数,我们需要的仅仅是父类型的一个实例,且这个实例是作为子类型原型的基础。
这种方式结合了组合继承和寄生式继承,是ES6的类继承之前的最佳继承模式。
以上是JavaScript中主要的继承方式。ES6引入了class和extends
关键字,使得继承更加直观和易于理解,但在底层仍然基于上述某种形式的原型继承。
10.防抖和节流
在前端开发中,防抖(Debouncing)和节流(Throttling)是两种常用的性能优化技术,主要用于控制事件处理的频率,以避免在高频事件(如滚动、窗口大小调整、键盘输入等)触发时导致的性能问题。下面分别给出防抖和节流的例子。
防抖(Debouncing)
防抖技术主要用在事件被频繁触发时,但在事件停止触发后只执行一次回调函数。比如,在搜索框中输入文字时,我们希望用户停止输入后才去执行搜索操作,而不是每输入一个字符就执行一次。英雄联盟回城
示例代码(使用JavaScript原生实现):
function debounce(func, wait) {
let timeout;
return function() {
const context = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
// 使用示例
const searchInput = document.getElementById('search-input');
const search = debounce(function(e) {
console.log(e.target.value); // 这里可以放置搜索逻辑
}, 500);
searchInput.addEventListener('input', search);
节流(Throttling)
节流技术则确保在固定时间间隔内只执行一次函数。比如,在滚动事件中,我们可能不希望每次滚动都触发某些操作,而是希望每隔一段时间(如200ms)才执行一次。
示例代码(使用JavaScript原生实现):
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
// 使用示例
window.addEventListener('scroll', throttle(function() {
console.log('Scrolling...');
}, 200));
注意:上面的节流实现是一个简单的版本,它确保了在指定的时间间隔内至少执行一次函数。然而,如果连续触发事件的间隔时间恰好等于limit,那么可能会导致在两次间隔中都不执行回调函数。根据具体需求,你可能需要调整实现方式。
在实际开发中,也可以使用lodash等库来更方便地实现防抖和节流功能,这些库提供了更加完善和灵活的API。
11.闭包的实现以及作用
闭包的作用主要体现在以下几个方面:
封装数据:
闭包可以将变量和函数封装在一个作用域内,避免全局变量的污染,同时也可以隐藏一些细节,提供更加清晰的接口。
保持状态:
闭包可以在函数执行完毕后,仍然保持外部函数的状态。这意味着闭包可以记住函数执行时的上下文环境,包括变量的值和状态,使得函数可以在之后的调用中继续使用这些状态。
实现私有变量:闭包可以在函数内部创建局部变量,并将其保留在内存中,即使函数执行完毕。这使得这些变量对外部是不可见的,实现了一种类似于私有变量的效果。
延迟执行:
闭包可以将函数作为返回值,从而实现延迟执行的效果。可以在外部函数执行完毕后,将内部函数作为回调函数传递给其他函数,以实现异步操作或延迟执行的需求。
高阶函数的参数和返回值:闭包可以作为高阶函数的参数或返回值,使得函数可以更加灵活地组合和使用。
闭包的优点与缺点
优点:
方便调用上下文中声明的局部变量。
逻辑紧密,可以在一个函数中再创建个函数,避免了传参的问题。
缺点:
闭包可以使得函数在执行完不被销毁,保留在内存中,如果大量使用闭包就会造成内存泄露,内存消耗很大。
闭包的实际应用
闭包在实际开发中有着广泛的应用,例如:
回调函数
:在JavaScript中,闭包常用于实现回调函数,确保回调函数能够访问到定义时的外部变量。
防抖(Debounce)和节流(Throttle)
:在事件处理中,防抖和节流技术常常通过闭包来实现,以控制函数的执行频率。
柯里化(Currying)
:柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数的技术,闭包常用于实现柯里化。
function createCounter() {
let count = 0; // 这是一个局部变量,通常在函数执行完毕后会被销毁
return function() { // 这里返回了一个匿名函数,它形成了闭包
count = count + 1; // 匿名函数可以访问并修改外部函数中的count变量
console.log(count);
};
}
// 使用createCounter函数
const counter = createCounter();
// 调用返回的匿名函数
counter(); // 输出: 1
counter(); // 输出: 2
counter(); // 输出: 3
// 注意,这里我们并没有在全局作用域中直接访问到count变量,
// 但是通过闭包,我们可以间接地修改和访问它。
12.箭头函数和普通函数的区别
写法与命名
箭头函数:使用箭头(=>)来定义,且总是匿名的,即它们没有显式的函数名。
普通函数:使用function关键字定义,可以是匿名的,也可以有具体的名称(具名函数)。
构造函数用途
箭头函数:不能用作构造函数,即不能使用new关键字来创建对象实例。
普通函数:可以用作构造函数,通过new关键字来创建对象实例。
this的指向
箭头函数:没有自己的this,它会捕获其所在上下文的this值(即父级作用域或词法作用域中的this值),并作为自己的this值。这意味着箭头函数中的this不会随调用方式改变而改变。
普通函数:this的指向取决于函数的调用方式。在全局环境中,它指向全局对象(在浏览器中是window);在对象方法中,它指向调用该方法的对象;作为构造函数时,它指向新创建的对象实例。
arguments对象
箭头函数:不具有arguments对象,即不能通过arguments对象访问传递给函数的参数列表。如果需要类似的功能,可以使用剩余参数(…)语法。
普通函数:每次调用后都会有一个arguments对象,包含了传递给函数的所有参数。
原型与prototype
箭头函数:不具有prototype属性,因此不能用作构造函数,也无法通过prototype链来添加方法或属性。
普通函数:具有prototype属性,可以通过它来添加方法或属性,这些方法和属性可以被该函数创建的实例所继承。
Generator函数与yield关键字
箭头函数:不能用作Generator函数,即不能使用yield关键字。
普通函数:可以定义为Generator函数,使用yield关键字来实现暂停和恢复执行的功能。
其他特性
箭头函数:由于其简洁的语法和this指向的特性,使得它非常适合于非方法函数的定义,如回调函数、定时器函数等。
普通函数:提供了更丰富的功能和灵活性,如构造函数、this指向的多样性、arguments对象等,适用于各种复杂的编程场景。
13.cookie、localStorage和sessionStorage的区别
存储位置与写入方式
cookie:cookie是由服务器端写入的,存储在客户端浏览器中。当浏览器向服务器发送请求时,cookie信息会包含在HTTP请求头中发送给服务器。
localStorage:localStorage和sessionStorage都是由前端(客户端)写入的,存储在用户浏览器中。
存储大小
cookie:存储空间较小,单条cookie的大小一般限制在4KB左右。
localStorage 和 sessionStorage:存储空间相对较大,一般浏览器支持的存储空间为5MB(sessionStorage的容量在某些浏览器中可能有所不同,但通常也是类似的量级)。
生命周期
cookie:生命周期由服务器端在写入时设置,可以是会话级别的(浏览器关闭即销毁),也可以设置具体的过期时间。
sessionStorage:生命周期仅限于当前会话(浏览器标签页或窗口),页面关闭或标签页关闭后,存储的数据会被清除。
localStorage:数据会一直存储在浏览器中,直到被手动删除或清除浏览器缓存。
数据共享
三者都遵循同源策略,即只能被同一源(协议、域名、端口号均相同)的页面访问。
sessionStorage 还进一步限制为必须是同一个页面(或标签页)内才能共享数据。
发送请求时是否携带
cookie:在前端给后端发送请求时,浏览器会自动携带cookie中的数据。
localStorage 和 sessionStorage:不会在HTTP请求中自动携带数据。
安全性
cookie:由于HTTP请求中会自动携带cookie,因此存在被拦截的风险。可以通过设置HttpOnly和Secure属性来提高安全性,但即使如此,也不能完全避免安全风险。
localStorage 和 sessionStorage:存储在客户端,不随HTTP请求发送,相对更安全,但也要注意不要存储敏感信息,因为浏览器端的存储都有可能被用户通过开发者工具等方式查看。
应用场景
cookie:常用于存储登录验证信息(如SessionID或Token),以判断用户是否登录。
sessionStorage:适用于存储临时数据,如音乐播放器的播放进度条、多页表单的填写信息等。
localStorage:常用于存储不易变动的数据,如用户的偏好设置、购物车信息等,以减轻服务器的压力。
14.深浅拷贝及深拷贝json.parse的副作用
浅拷贝
浅拷贝只复制对象的第一层属性,如果对象的属性值是一个对象或数组,那么复制的是这个对象或数组的引用,而不是对象或数组本身。
使用 Object.assign()
Object.assign() 方法可以将所有可枚举的自有属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。这是实现浅拷贝的一种简单方式。
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1);
obj2.a = 10; // 修改成功
obj2.b.c = 20; // 也修改了obj1.b.c
console.log(obj1); // { a: 1, b: { c: 20 } }
console.log(obj2); // { a: 10, b: { c: 20 } }
使用展开运算符(…)
展开运算符也可以用于对象的浅拷贝。
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { ...obj1 };
obj2.a = 10; // 修改成功
obj2.b.c = 20; // 也修改了obj1.b.c
console.log(obj1); // { a: 1, b: { c: 20 } }
console.log(obj2); // { a: 10, b: { c: 20 } }
深拷贝
深拷贝会递归地复制对象的所有层级,包括嵌套的对象和数组。
手动实现
手动实现深拷贝需要递归地遍历对象的所有属性,如果属性值是对象或数组,则继续递归复制。
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return null; // null 的情况
if (obj instanceof Date) return new Date(obj); // 日期对象直接返回一个新的日期对象
if (obj instanceof RegExp) return new RegExp(obj); // 正则对象直接返回一个新的正则对象
// 如果循环引用了就用 weakMap 来解决
if (hash.has(obj)) return hash.get(obj);
let allDesc = Object.getOwnPropertyDescriptors(obj);
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);
hash.set(obj, cloneObj);
for (let key of Reflect.ownKeys(obj)) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
cloneObj[key] = deepClone(obj[key], hash);
} else {
cloneObj[key] = obj[key];
}
}
return cloneObj;
}
// 使用
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = deepClone(obj1);
obj2.a = 10; // 修改成功
obj2.b.c = 20; // 不会修改obj1.b.c
console.log(obj1); // { a: 1, b: { c: 2 } }
console.log(obj2); // { a: 10, b: { c: 20 } }
使用第三方库
使用如 lodash 的 _.cloneDeep() 方法可以方便地实现深拷贝。
import _ from 'lodash';
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = _.cloneDeep(obj1);
obj2.a = 10; // 修改成功
obj2.b.c = 20; // 不会修改obj1.b.c
console.log(obj1); // { a: 1, b: { c: 2 } }
console.log(obj2); // { a: 10, b: { c: 20 } }
JSON.parse(JSON.stringify())实现深拷贝的副作用
虽然JSON.parse(JSON.stringify())方法在处理简单的纯数据对象时可以实现一定程度的深拷贝,但在处理复杂对象时存在诸多不足,其副作用主要包括以下几个方面:
循环引用问题:
当对象之间存在循环引用关系时(一个对象的属性引用了另一个对象,而后者又反过来引用了前者),JSON.stringify()在遇到循环引用时会抛出错误,导致无法完成深拷贝。
不支持函数和undefined:
JSON.stringify()会忽略对象中的函数和undefined值,因此通过这种方式深拷贝的对象将丢失这些数据。
无法复制特殊类型的属性:
诸如Date、RegExp、Map、Set、Error、Symbol等JavaScript内置对象及其属性,在序列化和反序列化过程中会被转换为基本的字符串或数组形式,导致恢复出来的对象不再是原来的数据类型。
精度损失:
JSON.stringify()对于数字的处理有精度限制,特别是对于非常大或非常小的数值,可能会造成精度损失。
对象方法丢失:
对象的方法(functions)不能被正确地复制,因为在JSON格式中不包含函数类型的表达。
不支持自定义类类型:
如果对象是自定义的类实例,其构造函数、原型链上的方法以及其他与类相关的元信息都无法通过JSON的序列化和反序列化得到保留。
只能序列化对象的可枚举的自有属性:
如果对象中存在不可枚举的属性或继承自原型的属性,这些属性在序列化过程中会被忽略。
15.字符串数组的常用方法
字符串常用方法
charAt(index)
:返回指定索引处的字符。
let str = "Hello";
console.log(str.charAt(1)); // 输出: e
concat(...strings)
:连接两个或多个字符串。
let str1 = "Hello, ";
let str2 = "world!";
console.log(str1.concat(str2)); // 输出: Hello, world!
indexOf(searchValue, fromIndex)
:返回指定值在字符串中首次出现的索引,如果未找到则返回-1。
let str = "Hello world";
console.log(str.indexOf("world")); // 输出: 6
slice(start, end):
提取字符串的一部分,并返回一个新字符串(不会修改原字符串)。
let str = "Hello world";
console.log(str.slice(0, 5)); // 输出: Hello
split(separator, limit):
通过分隔符将字符串分割成数组。
let str = "apple,banana,cherry";
console.log(str.split(",")); // 输出: ["apple", "banana", "cherry"]
toLowerCase()
和 toUpperCase()
:将字符串转换为小写或大写。
let str = "Hello World";
console.log(str.toLowerCase()); // 输出: hello world
console.log(str.toUpperCase()); // 输出: HELLO WORLD
trim():
去除字符串两端的空白字符。
let str = " Hello World ";
console.log(str.trim()); // 输出: Hello World
数组常用方法
push()
:向数组的末尾添加一个或多个元素,并返回新的长度。
let fruits = ['Apple', 'Banana'];
fruits.push('Orange');
console.log(fruits); // ['Apple', 'Banana', 'Orange']
pop()
:从数组中删除最后一个元素,并返回那个元素的值。
let fruits = ['Apple', 'Banana', 'Orange'];
let lastFruit = fruits.pop();
console.log(fruits); // ['Apple', 'Banana']
console.log(lastFruit); // 'Orange'
shift():
从数组中删除第一个元素,并返回那个元素的值。
let fruits = ['Apple', 'Banana', 'Orange'];
let firstFruit = fruits.shift();
console.log(fruits); // ['Banana', 'Orange']
console.log(firstFruit); // 'Apple'
unshift()
:向数组的开头添加一个或多个元素,并返回新的长度。
let fruits = ['Banana', 'Orange'];
fruits.unshift('Apple');
console.log(fruits); // ['Apple', 'Banana', 'Orange']
splice()
:通过删除现有元素和/或添加新元素来更改一个数组的内容。如果只删除了一个元素,则返回一个只包含该元素的数组。如果没有删除元素,则返回一个空数组。
let fruits = ['Apple', 'Banana', 'Orange', 'Mango'];
let removed = fruits.splice(1, 2, 'Lemon', 'Grape');
// 从索引1开始删除2个元素,并在该位置添加'Lemon'和'Grape'
console.log(fruits); // ['Apple', 'Lemon', 'Grape', 'Mango']
console.log(removed); // ['Banana', 'Orange']
slice()
:返回一个新的数组对象,这一对象是一个由 begin 到 end(不包括 end)选择的数组的一部分浅拷贝。
let fruits = ['Apple', 'Banana', 'Orange', 'Mango', 'Grape'];
let citrus = fruits.slice(1, 3);
// 从索引1开始到索引3(不包括3)的浅拷贝
console.log(citrus); // ['Banana', 'Orange']
concat()
:用于合并两个或多个数组。此方法不会改变现有的数组,而是返回一个新数组。
let fruits = ['Apple', 'Banana'];
let moreFruits = ['Orange', 'Mango'];
let allFruits = fruits.concat(moreFruits);
console.log(allFruits); // ['Apple', 'Banana', 'Orange', 'Mango']
join():
把数组的所有元素放入一个字符串。元素通过指定的分隔符进行分隔。
let fruits = ['Apple', 'Banana', 'Orange'];
let text = fruits.join(', ');
console.log(text); // 'Apple, Banana, Orange'
map()
:创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。
let numbers = [1, 4, 9];
let roots = numbers.map(Math.sqrt);
console.log(roots); // [1, 2, 3]
filter()
:创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。
let numbers = [1, 2, 3, 4, 5, 6];
let evenNumbers = numbers.filter(function(num) {
return num % 2 === 0;
});
console.log(evenNumbers); // [2, 4, 6]
16.前端怎么解决跨域
使用CORS
服务器端配置CORS:
服务器端需要设置适当的HTTP头部来允许跨域请求。这通常包括Access-Control-Allow-Origin头部,它可以设置为*(允许所有域)或特定的域名。
还可以设置其他CORS相关的头部,如Access-Control-Allow-Methods(允许的HTTP方法)、Access-Control-Allow-Headers(允许的HTTP头部)等。
前端无需特别配置:
一旦服务器配置了CORS,前端JavaScript代码就可以像平常一样发起请求,浏览器会自动处理跨域问题。
JSONP(不推荐,有安全风险)
JSONP是一种老旧的跨域技术,它利用<script>
标签不受同源策略限制的特性。
服务器端需要支持JSONP,将响应包装在一个函数调用中。
前端通过动态创建<script>
标签并设置其src属性为请求URL(包含回调函数名作为查询参数)来发起请求。
回调函数需要在全局作用域中定义,以便从响应中接收数据。
注意:由于JSONP使用<script>
标签,它只能用于GET请求,并且存在安全风险(如XSS攻击)。
代理服务器
在开发环境中,可以使用代理服务器来转发请求,从而绕过浏览器的同源策略。
例如,在前端项目中配置Webpack的devServer代理,或在Node.js后端设置一个代理路由。
这种方法允许前端代码向代理服务器发送请求,而代理服务器则向实际的目标服务器发送请求,并将响应返回给前端。
使用现代浏览器的CORS策略
确保前端请求遵守CORS策略的要求,如发送正确的HTTP头部。
使用fetch或XMLHttpRequest等现代API时,可以捕获跨域错误并相应地处理它们。
反向代理
在生产环境中,可以使用反向代理(如Nginx、Apache)来转发请求,隐藏实际的后端服务器地址。
反向代理服务器可以配置为允许跨域请求,并转发请求到后端服务器。
CORS插件或浏览器设置(仅限开发)
在开发过程中,可以使用浏览器插件(如CORS Unblock)来临时禁用CORS策略。
注意,这种方法仅适用于开发环境,并且不应在生产环境中使用。
17.不想让浏览器缓存怎么实现
前端设置
使用Meta标签
在HTML文档的部分添加特定的meta标签,可以指示浏览器不要缓存页面。
示例代码:
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Expires" content="0">
这些meta标签告诉浏览器不要缓存页面,每次访问都从服务器加载。
动态修改资源URL
对于CSS、JavaScript等资源文件,可以在它们的URL后添加随机数或时间戳作为查询参数,使浏览器认为每次请求的都是新资源。
示例代码(JavaScript):
document.write('<script type="text/javascript" src="js/test.js?time=' + new Date().getTime() + '"></script>');
document.write('<link rel="stylesheet" href="css/test.css?time=' + new Date().getTime() + '"/>');
或者,在HTML中直接添加带有查询参数的URL。
Ajax请求设置
在使用Ajax请求时,可以设置cache: false选项来防止缓存。
示例代码(jQuery):
$.ajax({
url: 'your-url',
dataType: 'json',
cache: false,
success: function(response) {
// 处理响应
}
});
也可以使用beforeSend函数来设置请求头,如If-Modified-Since和Cache-Control,但通常cache: false更简单直接。
后端设置
设置HTTP响应头
在服务器端,可以通过设置HTTP响应头来控制浏览器缓存。
使用Cache-Control头部是最常见的方法,可以设置no-cache、no-store、must-revalidate等指令。
示例代码(PHP):
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
这些头部告诉浏览器不要缓存响应内容,并且每次请求都需要向服务器验证。
修改资源URL或内容
与前端方法类似,服务器端也可以在资源URL后添加版本号或时间戳,使浏览器认为资源已更改。
或者,修改资源内容本身(如添加注释或修改文件时间戳),使浏览器缓存失效。
18.vue中watch和computer的区别
computed(计算属性)
1)基于它们的依赖进行缓存:只有当计算属性依赖的响应式属性发生变化时,它才会重新求值。这意味着只要依赖没有改变,多次访问计算属性会立即返回之前的计算结果,而不会再次执行计算逻辑。
2)主要用于声明式的描述一些数据:当你需要在模板中多次引用某个基于响应式数据派生出的值时,使用计算属性是更合适的选择。
3)计算属性默认只有getter
,但你也可以提供一个setter
,用于当需要基于计算属性的值来改变响应式数据的场景。
示例:假设你有一个用户对象,你需要在模板中多次显示用户的全名(firstName + lastName
)。这时,使用计算属性来定义全名是一个很好的选择。
computed: {
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
watch(侦听器)
1)更通用:它可以侦听数据的变化并执行异步操作或开销较大的操作,这是计算属性做不到的。
2)用于观察和响应Vue实例上数据的变化:当需要在数据变化时执行异步或开销较大的操作时,使用侦听器。
3)两个主要参数:newVal
(新值)和oldVal
(旧值),用于比较数据变化前后的值。
4)可以侦听任何数据变化:包括Vue实例上的数据、计算属性甚至是其他组件的props
。
示例:如果你需要根据用户输入动态地发送请求或调用一个函数,并且这个函数依赖于用户输入的变化,那么使用侦听器会更合适。
watch: {
// 侦听query参数的变化
query(newVal, oldVal) {
// 当query变化时,执行一些操作,比如发送请求
}
}
19.vue的双向绑定什么场景下不生效
1)对象和数组的直接赋值
对象赋值:在Vue中,如果你直接替换对象(如this.someObject = { ... }
),而不是修改其属性,Vue将不会跟踪到这些变化,因为Vue的响应式系统是基于属性的getter/setter
实现的。这会导致双向绑定失效。
数组赋值:同样地,如果你直接替换整个数组(如this.someArray = [...]
),而不是使用Vue提供的变异方法(如push、pop、splice
等)来修改数组,Vue也无法跟踪到这些变化。
2) 动态添加的属性
Vue只能检测到在初始化时就已经存在的属性的变化。如果你在Vue实例创建后,动态地向响应式对象上添加新的属性,Vue将不会跟踪这个新属性的变化,从而导致双向绑定失效。
3) 异步更新问题
Vue的数据更新是异步的,即数据变化后,Vue会在下一个事件循环中更新DOM。这意味着如果你在数据变化后立即访问DOM或进行某些依赖于更新后数据的操作,可能会得到旧的值,从而误以为双向绑定没有生效。
4)深度监听和复杂数据结构
当处理复杂的数据结构(如嵌套对象或数组)时,Vue的默认监听可能无法深入到所有层级。虽然可以使用watch
的deep
选项来深度监听对象或数组的变化,但这会增加性能开销,并且仍然可能无法覆盖所有情况。
解决方法
1)对于对象和数组的直接赋值问题,可以使用Vue提供的Vue.set(object, propertyName, value)
或实例方法this.$set(object, propertyName, value
)来确保属性是响应式的。
2)对于动态添加的属性问题,同样可以使用Vue.set
或this.$set
来添加新的响应式属性。
3)对于异步更新问题,可以使用Vue.nextTick(callback)
来在DOM
更新完成后执行回调函数,确保此时的数据是最新的。
4)对于复杂数据结构,可以考虑使用Vuex
等状态管理库来管理共享状态,它提供了更强大和灵活的状态管理能力。
20.绝对定位和相对定位都是相对于什么定位的
绝对定位(Absolute Positioning)
绝对定位是相对于其最近的已定位(即非static
)祖先元素进行定位的。如果元素没有已定位的祖先元素,则它会相对于初始包含块(通常是浏览器窗口或HTML
文档的<html>
元素)进行定位。这里的“已定位”指的是元素的position
属性被设置为relative、absolute、fixed
或sticky
。
特性:
绝对定位的元素会从文档流中完全删除,并相对于其最近的已定位祖先元素或初始包含块进行定位。
元素的位置可以通过top、right、bottom、left
属性来设置。
如果一个绝对定位的元素没有设置这些属性,它将保持在它原来的位置(即,在文档流中的位置)。
相对定位(Relative Positioning)
相对定位是相对于元素在文档流中的原始位置进行定位的。这意味着,即使你设置了相对定位并调整了元素的top、right、bottom、left
属性,元素仍然会占据它原本在文档流中的空间。
特性:
相对定位不会使元素脱离文档流,它只是在视觉上调整了元素的位置。
元素的位置通过top、right、bottom、left
属性相对于其原始位置进行调整。
这些偏移值可以是正值或负值,允许元素在其原始位置的基础上向上、向下、向左或向右移动。
21. vue中如何防范xss和csrf
防范XSS(跨站脚本攻击)
1) 内容转义:
确保所有从用户输入或不可信的源接收的数据在插入到DOM
之前都进行了适当的转义。Vue.js
的模板引擎默认会转义绑定到HTML
属性或元素内容的数据,以防止XSS
攻击。但是,当你使用v-html
指令时,Vue
不会转义HTML
,因此必须确保这部分内容是安全的。
2) 避免使用v-html:
尽可能避免使用v-html
指令,因为它会渲染原始的HTML
字符串,这可能包含恶意脚本。如果必须使用,确保HTML
内容是可信的或已经过适当的清理。
3) 使用CSP(内容安全策略):
实施CSP
可以帮助减少XSS
攻击的风险。CSP
通过减少或消除在网页上执行恶意脚本的机会来保护用户。你可以在你的服务器响应头中设置CSP
策略。
4) 清理用户输入:
在将用户输入存储到数据库或显示在页面上之前,使用适当的库(如DOMPurify
)来清理HTML、JavaScript
等潜在的危险内容。
防范CSRF(跨站请求伪造)
1) 使用CSRF令牌:
在每个表单提交或AJAX
请求中附带一个CSRF
令牌。这个令牌应该是唯一的,并且每次请求时都会变化。服务器应该验证这个令牌是否有效,并且与当前会话或用户相关联。
Vue.js
项目中,你可以使用axios
这样的HTTP
客户端库,并在请求拦截器中自动添加CSRF
令牌。
2) SameSite Cookie属性:
对于使用Cookie来跟踪用户会话的应用,设置Cookie
的SameSite
属性为Strict或Lax
可以减少CSRF
的风险。这可以防止第三方网站通过跨站请求发送Cookie。
3) 验证HTTP Referer:
虽然这不是完全可靠的,因为Referer
可以被伪造或禁用,但在一些情况下,检查Referer头部是否与你的应用域名匹配,可以作为额外的安全层。
4) 使用安全的HTTP方法:
尽可能使用GET、HEAD、OPTIONS和TRACE
之外的HTTP
方法,因为GET和HEAD
请求通常不包含敏感数据,而OPTIONS和TRACE
请求可能会被用于信息泄露。对于需要修改服务器状态的操作,使用POST、PUT、DELETE
等方法,并在这些请求中实施CSRF
保护。
5) 避免在GET请求中传输敏感数据:
虽然这与CSRF
直接关联不大,但避免在GET
请求中传输敏感数据(如密码、令牌等)是一个好的安全实践。