最近找工作整理了一些面试题,分享给大家一起来学习。如有问题,欢迎指正。
前端面试题系列文章:
VUE
单页面应用(SPA)和多页面应用(MPA)区别
-
单页面应用(SPA):只有一个html页面的应用,页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次
-
SPA的优点
- 页面切换速度快,用户体验较好
- seo优化好,搜索引擎优化比较容易
-
SPA的缺点
- 首屏加载速度慢
- 不易于SEO
-
如何给SPA做SEO
SEO效果差,因为搜索引擎只认识
html
里的内容,不认识js
的内容,而单页应用的内容都是靠js
渲染生成出来的,搜索引擎不识别这部分内容。- 使用服务端渲染SSR:将组件或页面通过服务器生成html,再返回给浏览器。
- 预渲染 prerender-spa-plugin:使用预渲染方式,在构建时(build time)简单地生成针对特定路由的静态HTML文件
-
-
多页面应用(MPA):指一个应用中有多个html页面,页面跳转时是整页刷新,都需要重新加载
html
、css
、js
文件-
MPA的优点
- 首屏加载速度快
- seo优化好,搜索引擎优化比较容易
-
MPA的缺点
- 页面切换加载缓慢,用户体验差
-
对比项 | SPA | MPA |
---|---|---|
结构 | 一个主页面+许多模块的组件 | 许多个完整的页面 |
体验 | 页面切换快,体验良好;但是初次加载文件过多时,需要做相关的调优 | 网速慢的时候页面切换慢,体验不好 |
内容更新 | 相关组件的切换,即局部更新 | 整体HTML切换,重复的HTTP请求 |
资源文件 | 组件公用的资源只需要加载一次 | 每个页面都要自己加载公用的资源 |
适用场景 | 对体验度和流畅度有较高的要求,但不利于SEO | 适用于对SEO要求较高的应用 |
过渡动画 | Vue提供了transition的封装组件,容易实现 | 较难实现 |
路由模式 | hash模式 or history模式 | 普通的链接跳转 |
数据传递 | 使用全局变量(Vuex) | cookie、localStorage等缓存方案,URL参数等 |
相关成本 | 前期开发成本较高、后期维护较为容易 | 前期开发成本低、后期维护比较麻烦(可能牵一发而动全身) |
MVVM、MVC区别
MVVM
表示的是Model-View-ViewModel
-
Model代表数据模型,数据和业务逻辑都在Model层中定义;
-
View代表UI视图,负责数据的展示;
-
ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;
Model 和 View 并无直接关联,而是通过 ViewModel 来进行交互的(即双向数据绑定)
-
MVC
表示的是Model-View-ViewModel
-
Model(模型)存储页面的业务数据,以及对相应数据的操作;
-
View(视图)负责页面的显示逻辑。
-
Controller(控制器)负责用户与应用的响应操作
View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改。
-
VUE3 优势
-
性能: 比vue2.x快1.2~2倍
-
diff算法优化:
- vue2.x: 生成新的虚拟DOM树 与 之前的虚拟DOM树, 全量比较,部分更新
- vue3.0:创建虚拟DOW的时候,根据DOM内容是否发生变化添加patchFlag标志, 虚拟DOM比较时,静态标记的节点进行比较
-
静态提升
- vue2: 无论是否参与更新,每次都会重新创建,然后渲染。
- vue3: 把不需要更新的元素做静态提升,只被创建一次,在渲染时直接复用
-
事件侦听器缓存
-
vue2:我们写的@click="onClick"也是被当作动态属性,diff的时候也要对比。但我们知道它不会变化,比如变成@click="onClick2",绑定别的值。
-
vue3中,如果事件是不会变化的,会将onClick缓存起来(跟静态提升达到的效果类似),该节点也不会被标记上PatchFlag(也就是无需更新的节点)。这样在render和diff两个阶段,事件侦听属性都节约了不必要的性能消耗。
-
-
-
vue3重写了数据双向绑定
使用了ES6的proxy。vue2使用的是defineProperty进行数据劫持,缺陷是对数组数据不友好,需要对数组的原生方法进行重写,并且监听不到对数组的长度。
-
体积小
引入
tree-shaking
,按需编译,在打包的过程中就可以将这些没有被用户使用的API
移除,减少打包体积。 -
组合API(Composition API)逻辑更加分明
Vue2需要使用的数据要在data、method等里面分开写,不是一个整体。现在可以将需要使用的数据和方法放在一起写,或者通过写hook函数进行区分。
-
更好的 ts 支持
vue2 不适合使用 ts,在于它的 Options API 风格。options 是一个简单的对象,而 ts 是一种类型系统、面向对象的语法,两个不匹配。
vue3 新增了 defineComponent 函数,使组件在 ts 下,更好的利用参数类型推断。如:reactive 和 ref 很具有代表性。
VUE 双向绑定原理
Vue2.x
是借助Object.defineProperty()
实现的,而Vue3.x
是借助Proxy
实现的。发布订阅模式(观察者模式)
vue2 双向绑定原理:
vue初始化时会对data中的属性在Object.defineProperty()中定义get和set函数,为每个属性添加dep数组(订阅器)。 模板编译时, 遇到变量触发该属性的get方法,在get方法内创建一个watcher(订阅者),将watcher添加到dep中。 当数据发生变化,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。实现了数据驱动视图。当输入框输入数据时,数据发生变化。实现视图驱动数据。
VUE 生命周期
-
vue 2
生命周期 描述 beforeCreate 组件实例被创建之初 created 组件实例已经完全创建 beforeMount 组件挂载之前 mounted 组件挂载到实例上去之后 beforeUpdate 组件数据发生变化,更新之前 updated 组件数据更新之后 beforeDestroy 组件实例销毁之前 destroyed 组件实例销毁之后 activated keep-alive 缓存的组件激活时 deactivated keep-alive 缓存的组件停用时调用 -
当前路由不使用
<keep-alive>
缓存,离开当前路由会直接调用 beforeDestroy 和 destroyed 销毁 -
当前路由使用
<keep-alive>
缓存,离开当前路由不会直接调用 beforeDestroy 和 destroyed 销毁,需要使用路由钩子函数主动的调用beforeRouteLeave(to, from, next) { this.$destroy(); next(); }
-
-
vue 3
生命周期 描述 beforeCreate 组件实例被创建之初 created 组件实例已经完全创建 beforeMount 组件挂载之前 mounted 组件挂载到实例上去之后 beforeUpdate 组件数据发生变化,更新之前 updated 组件数据更新之后 beforeUnmount 组件实例卸载之前 unmounted 组件实例卸载之后 activated keep-alive 缓存的组件激活时 deactivated keep-alive 缓存的组件停用时调用 errorCaptured 捕获了后代组件传递的错误时调用 setup()执行顺序在 beforeCreate 和 created这两个钩子函数之前。是最早执行的,所以不能使用this,通过ref()和reactive( )函数的使用,可以完全替换掉以前的data{}语法形式。使用组合式API没有 beforeCreate 和 created 这两个生命周期
父子组件生命周期执行顺序
-
加载渲染过程
正常: 父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created-> 子beforeMount-> 子mounted -> 父mounted
子组件懒加载: 父beforeCreated ->父created ->父beforeMounted -> 父mounted ->子beforeCreated ->子created ->子beforeMounted ->子mounted
-
更新过程
父beforeUpdate -> 子beforeUpdate-> 子updated -> 父updated
-
销毁过程
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
对虚拟DOM的理解
-
什么是虚拟 DOM
虚拟 DOM 就是一个普通的 JavaScript 对象,用来描述真实dom结构的js对象。包含了 tag、props、children 三个属性,以这三个属性来描述一个DOM节点
<div id="app"> <p class="text">hello world!!!</p> </div>
将上面的HTML模版抽象成虚拟DOM树:
{ tag: 'div', props: { id: 'app' }, chidren: [ { tag: 'p', props: { className: 'text' }, chidren: [ 'hello world!!!' ] } ] }
-
为什么需要虚拟DOM
- 操作真实DOM∶ 总损耗 = 真实DOM完全增删改+(可能较多的节点)排版与重绘
- 操作虚拟DOM∶ 总损耗 = 虚拟DOM增删改+(与Diff算法效率有关)真实DOM差异增删改+(较少的节点)排版与重绘
由于JavaScript需要借助浏览器提供的DOM接口才能操作真实DOM,再加上修改DOM经常导致页面重绘,所以一般来说,DOM操作越多,网页的性能就越差。虚拟DOM有效地减少对真实DOM的操作,以此来提升网页性能。
DIFF算法的原理
- patch方法:对比当前同层的虚拟节点是否为同一种类型的标签
- 是:继续执行
patchVnode方法
进行深层比对 - 否:没必要比对了,直接整个节点替换成
新虚拟节点
- 是:继续执行
- patchVnode方法,对比虚拟新旧虚拟节点内容,更新真实Dom节点
- 找到对应的
真实DOM
,称为el
- 判断
newVnode
和oldVnode
是否指向同一个对象,如果是,那么直接return
- 如果他们都有文本节点并且不相等,那么将
el
的文本节点设置为newVnode
的文本节点。 - 如果
oldVnode
有子节点而newVnode
没有,则删除el
的子节点 - 如果
oldVnode
没有子节点而newVnode
有,则将newVnode
的子节点真实化之后添加到el
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点,这一步很重要
- 找到对应的
在新老虚拟DOM对比时:
- 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
- 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
- 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
- 匹配时,找到相同的子节点,递归比较子节点
$nextTick
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
原因是,Vue是异步执行dom更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和Dom操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
原因是,Vue是异步执行dom更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和Dom操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。
vue 2 的data为什么是一个方法
因为对象是引用数据类型,如果写成对象,这个组件在多处被引用,只要修改一处的值,那么另一处的值也会变化,造成数据污染。而使用返回对象的函数,由于每次返回的都是一个新对象(Object的实例),引用地址不同,则不会出现这个问题。
vue3的proxy与vue2的defineProperty的对比
-
Object.defineProperty
只能针对定义好的属性进行监听,无法对新增属性或者删除属性监听 -
proxy
是对整个对象进行监听,新增属性或者删除属性都能响应。vue2的
Object.defineProperty
没有实现对数组的监听,只提供了push
,pop
,sort
,reserve
,splice
,unshift
,shift
等七种方法能触发数组监听,所以没有数组下标响应式。proxy
是对整个数组对象进行监听,新增元素或者删除元素都能响应,数组下标也能响应。// Object.defineProperty let name = '答案cp3' let obj = { name } for (let key in obj) { if (obj.hasOwnProperty(key)) { Object.defineProperty(obj, key, { get () { console.log('get函数被调用了') return name }, set (val) { console.log('set函数被调用了') name = val } }) } } obj.name = 'cp3' // set函数被调用了 obj.age = 18 // 没有打印set函数被调用了 console.log(obj.name) // get函数被调用了 console.log(obj.age) // 没有打印 // Proxy let name = '答案cp3' let obj = new Proxy({ name }, { get (target, key) { console.log('get函数被调用了') return target[key] }, set (target, key, val) { console.log('set函数被调用了') target[key] = val } }) obj.name = 'cp3' // set函数被调用了 obj.age = 18 // set函数被调用了 console.log(obj.name) // get函数被调用了 console.log(obj.age) // get函数被调用了
为什么动态给vue的data添加一个新属性为非响应式?
vue2
是通过Object.defineProperty
实现数据响应式,新增的属性,并没有通过Object.defineProperty
设置成响应式数据。
解决方案
- Vue.set()
- Object.assign():应创建一个新的对象,合并原对象和混入对象的属性
- $forcecUpdated():强制刷新
Vue 2为什么没有数组下标响应式
Vue 的双向数据绑定,使得修改数据后,视图就会跟着发生更新,然而直接通过下标修改数组内容后,视图却不发生变化。
但是Vue对数组的7个(push、pop、shift、unshift、splice、sort、reverse)实现了响应式
性能代价和用户体验收益不成正比。对于对象而言,每一次的数据变更都会对对象的属性进行一次枚举,一般对象本身的属性数量有限,所以对于遍历枚举等方式产生的性能损耗可以忽略不计,但是对于数组而言呢?数组包含的元素量是可能达到成千上万,假设对于每一次数组元素的更新都触发了枚举/遍历,其带来的性能损耗将与获得的用户体验不成正比,故vue无法检测数组的变动
解决方案
- this.$set(array, index, data)
- 数组的splice方法
v-model 语法糖实现
- vue 2
<CustomInput v-model="title" /> // 相当于 <CustomInput :value="title" @input="newTitle => title = newTitle"/> // 使用.sync 使得 props属性双向绑定 <CustomInput :title.sync="title" /> // 相当于 <CustomInput :title="title" @update:title="newTitle => title = newTitle"/>
- vue 3
<CustomInput v-model="title" /> // 相当于 <CustomInput :modelValue="title" @update:modelValue="newTitle => title = newTitle" /> // props属性双向绑定 <CustomInput v-model:title="title" /> // 相当于 <CustomInput :title="title" @update:title="newTitle => title = newTitle" />
vue组件间通信
-
组件通信方案:
- 通过 props 传递 和 $emit
- 使用
$refs
和$parent
- EventBus
- attrs 与 listeners
- Provide 与 Inject
- Vuex
-
父子组件通信
- props 和 $emit
$refs
和$parent
-
兄弟组件
- EventBus
-
祖孙组件
- provide 与 inject
$attrs
和$listeners
-
非关系组件
- vuex
-
EventBus实现
// 创建一个中央时间总线类 class Bus { constructor() { this.callbacks = {}; // 存放事件的名字 } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } } // main.js Vue.prototype.$bus = new Bus(); // 将$bus挂载到vue实例的原型上 // 另一种方式 Vue.prototype.$bus = new Vue(); // Vue已经实现了Bus的功能 // children1.vue this.$bus.$emit('foo') // children2.vue this.$bus.$on('foo', this.handle)
vue slot插槽
- slot插槽分类:
- 默认插槽
- 具名插槽
- 作用域插槽
// 子组件 child.vue
<template>
<!-- 默认插槽 -->
<slot></slot>
<!-- 具名插槽 -->
<slot name="conent"></slot>
<!-- 作用域插槽 -->
<slot name="footer" testProps="子组件的值"></slot>
</template>
// 父组件
<child>
<div>默认插槽</div>
<template v-slot:content>
具名插槽
</template>
<template v-slot:footer="slotProps">
作用域插槽:来⾃⼦组件数据:{{slotProps.testProps}}
</template>
</child>
v-show与v-if的区别
v-show
隐藏则是为该元素添加css--display:none
,dom
元素依旧还在。v-if
显示隐藏是将dom
元素整个添加或删除v-if
由false
变为true
的时候,触发组件的beforeCreate
、create
、beforeMount
、mounted
钩子,由true
变为false
的时候触发组件的beforeDestory
、destoryed
方法
v-if和v-for的优先级是什么
v-for
优先级比v-if
高。
v-if
和 v-for
同时用在同一个元素上,会带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
vue中key的原理吗
-
使用
v-for
时,需要给每项加上key
Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed
-
组件更新
key
,手动强制触发重新渲染旧key的组件被移除,新key的组件被加载
key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点
Vue常用的修饰符
-
表单的修饰符
-
lazy: 光标离开标签的时候,才会将值赋予给
value
,也就是在change
事件之后再进行信息同步<input type="text" v-model.lazy="value"> <p>{{value}}</p>
-
trim:自动过滤用户输入的首空格字符,而中间的空格不会过滤
<input type="text" v-model.trim="value">
-
number:自动将用户的输入值转为数值类型,但如果这个值无法被
parseFloat
解析,则会返回原来的值<input type="number" v-model.number="value">
-
-
事件修饰符
- stop:阻止了事件冒泡,相当于调用了
event.stopPropagation
方法 - prevent:阻止了事件的默认行为,相当于调用了
event.preventDefault
方法 - self:只当在
event.target
是当前元素自身时触发处理函数v-on:click.prevent.self
会阻止所有的点击v-on:click.self.prevent
只会阻止对元素自身的点击
- once:绑定了事件以后只能触发一次,第二次就不会触发
- capture:使事件触发从包含这个元素的顶层开始往下触发
- passive:在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符。.passive 和 .prevent不一起使用,passive 会告诉浏览器你不想阻止事件的默认行为
- native:让组件变成像
html
内置标签那样监听根元素的原生事件,否则组件上使用v-on
只会监听自定义事件。 使用.native修饰符来操作普通HTML标签是会令事件失效的
- stop:阻止了事件冒泡,相当于调用了
-
.sync:能对
props
进行一个双向绑定- 使用
sync
的时候,子组件传递的事件名格式必须为update:value
,其中value
必须与子组件中props
中声明的名称完全一致 - 注意带有
.sync
修饰符的v-bind
不能和表达式一起使用,例如v-bind.sync=”{ title: doc.title }”,是无法正常工作的
- 使用
template与render函数对比
- render渲染方式可以让我们将js发挥到极致,因为render的方式其实是通过createElement()进行虚拟DOM的创建。逻辑性比较强,适合复杂的组件封装。
- template是类似于html一样的模板来进行组件的封装。
- render的性能比template的性能好很多
- render函数优先级大于template
Vue Router
history 模式和 hash 模式区别
-
hash 模式
在 url 中的 # 之后对应的是 hash 值。无需发起 http 请求,window 通过hashChange() 事件监听hash值的变化,根据路由表对应的hash值来判断加载对应的路由加载对应的组件。
优点
- 只需要前端配置路由表, 不需要后端的参与
- 兼容性好, 浏览器都能支持
- hash值改变不会向后端发送请求, 完全属于前端路由
缺点
- hash值前面需要加#, 不符合url规范,也不美观
-
history 模式
利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。pushState()方法可以改变URL地址且
不发送请求
,replaceState()方法可以读取历史记录栈,还可以对浏览器记录进行修改。 这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。页面跳转时,使用 pushState() 和 replaceState() 方法对历史记录栈修改,不发送请求。
页面刷新时,会请求服务器。后端需要配置路由指向index.html,否则会出现404
优点
- 符合url地址规范, 不需要#, 使用起来比较美观
缺点
- 在用户手动输入地址或刷新页面时会发起url请求, 后端需要配置index.html
- 兼容性比较差
Vue-Router 的懒加载如何实现
- 使用箭头函数+import动态加载
// 复制代码
const List = () => import('@/components/list.vue')
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
- 使用箭头函数+require动态加载
const router = new Router({
routes: [
{
path: '/list',
component: resolve => require(['@/components/list'], resolve)
}
]
})
- 使用webpack的require.ensure技术,也可以实现按需加载。 这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
const List = resolve => require.ensure([], () => resolve(require('@/components/list')), 'list');
// 路由也是正常的写法 这种是官方推荐的写的 按模块划分懒加载
const router = new Router({
routes: [
{
path: '/list',
component: List,
name: 'list'
}
]
}))
Vue-router 路由钩子在生命周期的体现
- 全局路由钩子
- router.beforeEach 全局前置守卫 进入路由之前
- router.beforeResolve 全局解析守卫(2.5.0+)
- router.afterEach 全局后置钩子 进入路由之后
- 单个路由独享钩子
- beforeEnter 为某些路由单独配置守卫,只在进入路由时触发
- 组件内钩子
- beforeRouteEnter 在渲染该组件的对应路由被验证前调用
- beforeRouteUpdate 在当前路由改变,但是该组件被复用时调用
- beforeRouteLeave 在导航离开渲染该组件的对应路由时调用
- 完整的导航解析流程 导航被触发。 在失活的组件里调用 beforeRouteLeave 守卫。 调用全局的 beforeEach 守卫。 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。 在路由配置里调用 beforeEnter。 解析异步路由组件。 在被激活的组件里调用 beforeRouteEnter。 调用全局的 beforeResolve 守卫(2.5+)。 导航被确认。 调用全局的 afterEach 钩子。 触发 DOM 更新。 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
params和query的区别
用法:this.$route.query.name
和 this.$route.params.name
区别:
- query在浏览器地址栏中显示参数,params后者则不显示
- 刷新页面query里面的数据不会丢失 params会丢失
VUEX
Vuex 核心概念和原理介绍
-
特点:
- Vuex 的状态存储是响应式的
- 能直接改变 store 中的状态。更改store 中的状态的唯一方法是提交 mutation
-
state:存放公共数据的地方;
使用方式
- {{this.$store.state.值名称}}
- ...mapState(["值名称"])
-
getters:获取根据业务场景处理返回的数据;
使用方式
- {{this.$store.getters.值名称}}
- ...mapGetter(["值名称"])
-
mutations:更改state数据的函数,必须是同步函数
使用方式
- store.commit('函数名称')
- ...mapMutations(["函数名称"])
-
actions:提交的是 mutation
使用方式
- store.dispatch('函数名称')
- ...mapActions(["函数名称"])
-
module:Vuex 允许将 store 分割成模块(module) 。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
Vuex中action和mutation的区别
- mutation中的操作是一系列的同步函数,用于修改state中的变量的的状态
- action 可以包含任意异步操作。 action 提交的是 mutation,而不是直接修改state中的变量的的状态。