引言
最近有几次面试,被狠狠拷打了,深刻体会到准备不足是多么致命。于是痛定思痛,我决定总结一下面试遇到的各种题目和挑战。在这份面经里,不说废话,直接上干货,希望能帮助到正在为面试发愁的你。
CSS 动画制作
CSS动画可以通过多种方式实现,主要包括使用transition、@keyframes结合animation属性。每种方法都有其特定的应用场景和优势。下面是详细说明:
1. 使用 transition
transition 提供了一种简单的方式来定义元素从一种样式变换到另一种样式的过渡效果。它通常用于响应用户交互(如悬停、点击)或者动态更改元素的样式属性。
transition 的基本语法
transition 是一个简写属性,可以同时设置以下四个子属性:
transition-property: 指定应用过渡效果的CSS属性名称。transition-duration: 定义过渡效果持续的时间。transition-timing-function: 描述过渡效果的时间曲线(速度如何变化)。transition-delay: 定义过渡效果开始前的延迟时间。
2. 使用 @keyframes 和 animation
这种方法提供了更强大的控制力,可以定义动画序列中的多个关键帧,从而实现复杂的动画效果。
-
定义
@keyframes: 首先,你需要定义动画的关键帧,这描述了动画在其持续时间内应该经历的变化。@keyframes slidein { from { transform: translateX(0%); } to { transform: translateX(100%); } } -
应用
animation: 然后,通过animation属性将这些关键帧应用于元素上,并指定动画的其他细节,如持续时间、播放次数等。
3. 结合 transform 使用
结合 transform 属性与 transition 或 animation,可以创造出丰富且流畅的视觉效果。transform 支持多种变换方式,包括但不限于:
- 平移(Translate) :用于改变元素的位置而不影响页面布局中的其他元素。
- 旋转(Rotate) :使元素围绕一个点进行旋转。
- 缩放(Scale) :调整元素的大小,可以是等比例放大缩小或仅在特定方向上进行。
- 倾斜(Skew) :按照指定的角度扭曲元素,产生倾斜效果。
此外,transform 还支持三维变换,如 translate3d(x, y, z),这不仅增加了设计的灵活性,还能利用图形处理单元(GPU)加速渲染过程,确保动画运行更加流畅,尤其是在高帧率的情况下。
使用 transform 和 GPU 加速的组合,开发者能够有效地减少动画对CPU的负担,提高网页的整体性能。
4. 利用 will-change 提高性能
will-change 属性可以提示浏览器哪些属性可能会改变,让浏览器提前做出优化,提高动画性能。
方法对比
- 灵活性:
@keyframes提供了更高的灵活性和控制力,适用于需要精细调整的复杂动画;而transition更适合简单的状态变化。 - 性能: 在某些情况下,
transition可能会比@keyframes更加高效,因为它只在属性值改变时触发,而不需要预先定义完整的动画过程。 - 使用场景: 如果你只需要在状态之间进行简单的过渡,
transition就足够了;若要实现连续的动画效果,则应使用@keyframes和animation。
浏览器样式兼容
一、了解浏览器
主流浏览器 有五个:IE(Trident内核)、Firefox(火狐:Gecko内核)、Safari(苹果:webkit内核)、Google Chrome(谷歌:Blink内核)、Opera(欧朋:Blink内核)
四大内核:Trident(IE内核)、Gecko(Firefox内核)、webkit内核、Blink(Chrome内核)
二、为什么浏览器会存在兼容性问题?
- 不同的渲染引擎:各浏览器使用不同的渲染引擎,导致对网页的解析和显示有所差异。
- 标准支持程度不一:尽管有W3C等组织制定的标准,但不同浏览器对这些标准的支持速度和程度不同。
- 私有前缀和扩展:为了引入新功能,浏览器可能添加需要特定前缀(如
-webkit-,-moz-)的私有CSS属性或JavaScript API。 - 用户环境差异:用户的浏览器版本、操作系统和设备特性也会影响网页的表现,增加跨浏览器兼容性的复杂度。
三、CSS兼容问题及解决方案
-
CSS Reset/Normalize:
- 使用通用选择器(*)重置所有元素的默认边距和填充,或者使用更细致的normalize.css来标准化默认样式。
-
使用标准化的CSS属性:
- 尽量采用符合最新标准的CSS属性,并确保其在目标浏览器中得到良好支持。
-
利用浏览器前缀:
- 对于较新的CSS属性,为其添加适当的浏览器前缀以提高兼容性。
css 深色版本 .example { -webkit-border-radius: 10px; /* Safari 和 Chrome */ -moz-border-radius: 10px; /* Firefox */ border-radius: 10px; /* 标准语法 */ } -
Autoprefixer工具:
- 自动化工具如Autoprefixer可以在编译时自动为CSS添加必要的前缀,减少手动操作。
-
Polyfills和Shims:
- 针对旧版浏览器不支持的新特性,可以使用Polyfills或Shims提供类似的功能。
-
CSS Hack:
- 当需要针对特定浏览器写特定的CSS样式时,可以使用条件hack、属性级hack或选择符级hack。
-
条件样式表:
- 虽然不是首选方法,但在某些情况下,为特定浏览器或版本加载独立的样式表是解决兼容性问题的有效手段。
四、JavaScript兼容问题及解决方案
-
特性检测而非浏览器嗅探:
- 通过检测浏览器是否支持特定功能来编写代码,而不是根据浏览器版本判断。
-
使用库或框架:
- 如jQuery、React、Vue.js等,它们能抽象底层的浏览器差异,简化开发流程。
-
Polyfills:
- 对于不被所有目标浏览器支持的新JavaScript特性,使用Polyfills填补缺口。
-
Transpiling代码:
- 借助Babel等工具将现代JavaScript转换成向后兼容的版本,以便在老式浏览器中运行。
-
测试与调试:
- 使用BrowserStack等工具进行多浏览器测试,并利用开发者工具进行调试,确保应用能在各种环境中正常工作。
if 判断为false的数据类型
- 布尔值
false:最直接的假值。 - 数字
0和-0:数值零被视为假值。 - 空值
null:表示没有值的情况。 - 未定义
undefined:当访问一个未初始化的变量或对象属性时可能出现。 - 空字符串
"":不包含任何字符的字符串也被视为假值。 - 特殊数字
NaN(非数字) :当尝试将无效数据转换为数字时得到的结果。
了解这些假值对于编写健壮的条件逻辑非常重要,可以帮助你正确处理各种输入情况,避免意外的行为。同时,在JavaScript中使用宽松相等(==)比较时要注意,它可能会执行类型转换,这有时会导致一些意想不到的结果。因此,在可能的情况下,推荐使用严格相等(===)进行比较。
Vue 组件通信
一、父子组件通信(垂直对话)
1. 使用 Props 向子组件传递数据
父组件可以使用props将数据传递给子组件。这是一种单向的数据流机制,意味着父级状态的变化会自动向下传递到子组件,但子组件不能直接修改父组件的状态。这种设计有助于保持应用状态的一致性和可预测性。
2. 使用 $emit 在子组件中触发事件通知父组件
子组件可以通过调用$emit方法触发自定义事件,并向父组件发送信息。这种方式允许子组件与父组件进行交互,比如通知父组件发生了某个动作或需要更新数据。父组件通过监听这些自定义事件来响应子组件的操作。
二、双向绑定(父子实时互动)
3. v-model(智能存钱罐)
- 原理:props + emit 的语法糖
- 场景:表单输入、实时数据同步
- 代码示例:
<!-- 父组件 -->
<SmartInput v-model="message" />
<!-- 子组件(Vue3简化版) -->
<script setup>
const modelValue = defineModel() // 魔法变量名
</script>
高级用法:多账户管理
<UserForm
v-model:name="userName"
v-model:age="userAge"
/>
三、跨层级通信(家族广播)
4. Event Bus(事件总线方案)
Event Bus是一种简单而有效的跨组件通信解决方案。它特别适用于没有直接父子关系的组件之间的通信。通过创建一个空的Vue实例作为事件总线,组件可以通过这个总线发送和接收事件,从而实现消息的传递。不过,随着应用规模的增长,维护多个事件可能变得复杂,因此这种方法更适合小型项目或特定场景下的使用。
// event-bus.js
import mitt from 'mitt'
export const emitter = mitt()
// 发送方组件
emitter.emit('user-login', { user: 'admin' })
// 接收方组件
emitter.on('user-login', (user) => {
console.log('Login:', user)
})
// 组件卸载时取消监听(重要!)
onUnmounted(() => {
emitter.off('user-login')
})
注意:Vue3 官方移除 on/on/off,推荐使用 mitt/tiny-emitter
5. 使用 Provide / Inject(Vue 2.2.0+)
祖先组件可以使用provide选项提供数据给所有子孙组件,而子孙组件则通过inject选项来接收这些数据。
祖先组件提供数据:
// AncestorComponent.vue
export default {
provide() {
return {
message: 'Hello from ancestor'
};
}
};
子孙组件接收数据:
// DescendantComponent.vue
export default {
inject: ['message'],
mounted() {
console.log(this.message); // 输出 "Hello from ancestor"
}
};
四、全局通信(中央管理系统)
6. Vuex/Pinia(状态管理)
// Pinia Store
export const useUserStore = defineStore('user', {
state: () => ({ name: 'Guest' }),
actions: {
setName(newName) {
this.name = newName
}
}
})
// 组件中使用
const store = useUserStore()
store.setName('Admin')
选择建议:
- Vue2 项目 → Vuex
- Vue3 新项目 → Pinia
五、特殊场景通信
7. 透传 Attributes($attrs)
<!-- 父组件 -->
<BaseInput label="用户名" placeholder="输入..." />
<!-- BaseInput 组件 -->
<template>
<div class="input-wrapper">
<label>{{ label }}</label>
<input v-bind="$attrs" />
</div>
</template>
<script setup>
defineProps(['label'])
</script>
注意:Vue3 中 $listeners 已合并到 $attrs
8. 作用域插槽通信
<!-- 父组件 -->
<DataTable :items="users">
<template #item="props">
<span class="highlight">{{ props.item.name }}</span>
</template>
</DataTable>
<!-- DataTable 组件 -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot name="item" :item="item"></slot>
</li>
</ul>
</template>
六、常见问题解答
Q1:为什么修改props会报错?
答:就像不能修改别人的银行账户,props是父组件的财产。需要修改时应该通过$emit申请
Q2:Event Bus会导致内存泄漏吗?
答:如果忘记退订事件,就像手机APP后台常驻会耗电。务必在组件卸载时调用off()
Q3:何时该用Vuex何时用Pinia?
答:新项目直接Pinia,老项目逐步迁移。就像手机系统升级,新功能用最新版本更好
Q4:provide的数据子组件能修改吗?
答:默认可以,但建议提供readonly版本,就像重要文件给复印件而不是原件
Pinia 与 Vuex
以下是Pinia与Vuex在几个关键方面的对比:
| 特性 | Pinia | Vuex |
|---|---|---|
| 设计理念 | 更加模块化和直观,易于扩展 | 传统的集中式状态管理模式 |
| 安装与使用 | 简单的API,更少的概念需要学习 | 需要理解更多的概念,如mutations、actions等 |
| 模块系统 | 模块之间默认是独立的,但可以轻松组合 | 模块支持命名空间,有助于大型项目管理复杂的状态 |
| 插件支持 | 提供了灵活的插件系统 | 支持插件,但相对不如Pinia灵活 |
| TypeScript支持 | 出色的TypeScript支持,类型推导更加智能 | 支持TypeScript,但在某些情况下类型推导不够智能 |
| 调试工具 | 官方提供了专门的Devtools支持 | Vuex自带集成Vue Devtools支持 |
| 性能 | 轻量级,优化了内部实现以提高性能 | 性能良好,但在处理大规模应用时可能稍逊于Pinia |
| 编码风格 | 更加接近现代JavaScript的编码习惯 | 遵循较为传统的方式,有明确的mutation和action分离 |
| 兼容性 | Vue 3.x | Vue 2.x 和 Vue 3.x(通过不同的版本) |
| 社区支持 | 新兴框架,社区正在快速增长 | 成熟稳定,拥有庞大的社区支持 |
总结
- Pinia:适合那些寻求现代化、模块化以及对TypeScript有更好的支持的开发者。它的设计哲学更符合当前JavaScript开发的趋势,提供了一个更为简洁和直观的方式来管理应用状态。
- Vuex:作为Vue官方的状态管理模式,长期以来被广泛应用于各种规模的Vue项目中。尽管其设计相对于Pinia来说稍微复杂一些,但它为大型项目的复杂状态管理提供了强大的支持。
父子通信
Vue中的父子组件通信是构建复杂用户界面的基础之一。父组件可以向子组件传递数据,而子组件可以通过事件将信息反馈给父组件。以下是几种主要的父子组件通信方法:
1.父组件向子组件传递数据(Props)
在Vue中,父组件可以通过props属性向下传递数据到子组件。props是一个数组或对象,用于接收来自父组件的数据。
- 机制:父组件通过属性绑定(
v-bind或:)传递数据,子组件通过props选项接收。
<!-- 父组件 -->
<template>
<ChildComponent :message="parentMessage" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
data() {
return { parentMessage: 'Hello from Parent!' };
}
};
</script>
<!-- 子组件 -->
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
props: ['message'] // 声明接收的prop
};
</script>
2.子组件向父组件发送消息(Events)
当需要从子组件向父组件发送消息时,可以使用$emit触发一个自定义事件,并携带参数。父组件监听这个事件并处理相应的逻辑。
- 机制:子组件通过
$emit触发事件,父组件通过v-on或@监听事件并处理数据。
<!-- 子组件 -->
<template>
<button @click="sendData">Send Data</button>
</template>
<script>
export default {
methods: {
sendData() {
this.$emit('data-sent', { value: 42 }); // 触发事件并传递数据
}
}
};
</script>
<!-- 父组件 -->
<template>
<ChildComponent @data-sent="handleData" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
methods: {
handleData(payload) {
console.log(payload.value); // 输出 42
}
}
};
</script>
3. 双向数据绑定:v-model 或 .sync 修饰符
-
v-model:默认利用
valueprop 和input事件,可自定义。<!-- 父组件 --> <ChildComponent v-model="parentData" /> <!-- 子组件 --> <input :value="value" @input="$emit('input', $event.target.value)" /> <script> export default { props: ['value'] }; </script> -
.sync 修饰符:简化双向绑定,子组件通过
update:propName事件更新。<!-- 父组件 --> <ChildComponent :title.sync="pageTitle" /> <!-- 子组件 --> <button @click="$emit('update:title', newTitle)">Update Title</button>
父组件直接访问子组件:ref
有时可能需要直接访问子组件实例,比如调用子组件的方法或者获取其内部状态。这可以通过ref属性实现。
- 机制:父组件通过
ref引用子组件实例,直接调用方法或访问数据(谨慎使用)。
<!-- 父组件 -->
<template>
<ChildComponent ref="child" />
<button @click="callChildMethod">Call Child Method</button>
</template>
<script>
export default {
methods: {
callChildMethod() {
this.$refs.child.childMethod();
}
}
};
</script>
<!-- 子组件 -->
<script>
export default {
methods: {
childMethod() {
console.log('Method called in child');
}
}
};
</script>
Vue2 与 Vue3
| 特性/方面 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式系统 | 使用 Object.defineProperty 实现双向数据绑定,存在监听数组变化和动态添加属性的问题。 | 使用 Proxy 对象实现,支持监听对象的所有属性变化,包括数组和动态添加的属性 。 |
| 性能 | 初次渲染速度和更新效率较低,内存使用相对较高。 | 初次渲染快了 55%,更新快了 133%,内存使用减少了 54% 。 |
| 组件开发方式 | 选项型 API(Options API),逻辑分散在不同的选项中如 data, methods, computed 等。 | 组合型 API(Composition API),允许按功能模块化组织代码,增强代码可读性和复用性 。 |
| 生命周期钩子 | 生命周期钩子名称简单直接,例如 beforeCreate, created, beforeMount, mounted 等。 | 生命周期钩子名称加上了 on 前缀,例如 onBeforeMount, onMounted 等,并新增了一些钩子函数 。 |
| 全局 API | 全局 API 直接挂载在 Vue 构造函数上,例如 Vue.config, Vue.use()。 | 全局 API 必须作为 ES 模块构建的命名导出进行访问,使用 createApp 创建应用实例 。 |
| 片段(Fragments) | 不支持多根节点组件,每个组件必须有一个唯一的根元素。 | 支持多根节点组件(即片段),允许组件包含多个根节点 。 |
| 静态树提升 | 缺乏对完全静态子树的优化处理。 | 在编译阶段自动将完全静态的子树提升,减少不必要的渲染成本 。 |
| Teleport 和 Suspense | 不支持 Teleport 和 Suspense 组件。 | 新增了 Teleport 可将部分 DOM 移动到 Vue 应用之外的位置;Suspense 提供异步组件的支持 。 |
| TypeScript 支持 | 对 TypeScript 的支持有限,类型推断不够完善。 | 完全支持 TypeScript,源码本身是用 TypeScript 编写的,提供了更好的类型安全性和编辑器支持 。 |
| 打包大小 | 包含了一些不常用的 API,导致最终打包体积较大。 | 移除了不常用的 API 并优化了 Tree-shaking,使打包体积相比 Vue 2 减小约 10% 。 |
KeepAlive
<KeepAlive> 是 Vue.js 中的一个内置组件,用于缓存动态切换的组件实例。这意味着当一个组件被包裹在 <KeepAlive> 标签内时,即使它从当前视图中消失(例如通过路由切换或条件渲染),它的状态也会被保留,而不是像通常那样被销毁和重新创建。
基本用法
在 Vue 2 和 Vue 3 中使用 <KeepAlive> 的基本语法是相似的:
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
在这个例子中,currentComponent 是一个可以动态变化的组件名称或组件对象。当 currentComponent 变化时,旧的组件实例会被缓存而不是销毁。
属性
- include:字符串或正则表达式,指定哪些组件应该被缓存。
- exclude:字符串或正则表达式,指定哪些组件不应该被缓存。
- max:数字,限制缓存的最大数量,超过这个数量后将根据最近最少使用原则移除组件实例。
示例:
<KeepAlive include="a,b">
<component :is="view" />
</KeepAlive>
在这个例子中,只有名称为 'a' 或 'b' 的组件会被缓存。
实际应用
<KeepAlive> 在实际应用中非常有用,特别是对于那些需要保持状态的页面或组件。比如在一个多步骤表单中,用户可能需要在不同步骤之间来回切换,如果每次切换都重新加载数据或初始化状态,用户体验会大打折扣。此时,<KeepAlive> 可以确保用户返回到之前的步骤时,看到的是之前的状态 。
此外,结合 Vue Router 使用 <KeepAlive> 也非常常见,可以帮助我们缓存路由对应的组件,从而提升用户体验 。
需要注意的是,并不是所有的组件都需要或者适合被缓存。例如,对于那些内容频繁更新或依赖外部状态的组件,可能不适合使用 <KeepAlive>,因为这可能导致状态不一致的问题 。
为了在这种情况下实现更新,你可以采取以下几种策略:
1. 使用 activated 生命周期钩子
当一个被 <KeepAlive> 缓存的组件再次变为活跃状态时,activated 钩子会被触发。你可以在该钩子中执行任何必要的逻辑来更新组件的状态,例如从服务器获取最新的数据或者同步 Vuex 状态。
2. 监听外部状态变化
如果组件依赖于外部状态(如路由参数、Vuex store 或者 props),你可以设置监听器来监视这些状态的变化,并在变化时更新组件的内容。
3. 强制组件更新
虽然不推荐经常使用,但在某些特定场景下,你可能需要强制组件重新渲染。Vue 提供了 $forceUpdate() 方法来手动触发组件的重新渲染。但是,请注意这种方法应当谨慎使用,因为它可能导致性能问题和不必要的复杂性 。
this.$forceUpdate();
4. 更新 Key 属性
通过改变绑定到组件上的 key 属性,可以迫使 Vue 认为这是一个全新的组件实例,从而销毁旧的实例并创建一个新的实例。这种方法可以用来“重置”组件的状态。
<keep-alive>
<component :is="currentComponent" :key="componentKey"></component>
</keep-alive>
然后在适当的地方修改 componentKey 的值:
this.componentKey += 1; // 每次增加 key 的值都会导致组件重新渲染
5. 使用条件渲染
如果你不想让某个组件总是被缓存,可以通过条件判断来决定是否包裹在 <KeepAlive> 标签内。例如,可以根据某个条件来动态地决定是否使用 <KeepAlive>。
<template v-if="shouldCache">
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>
</template>
<template v-else>
<component :is="currentComponent"></component>
</template>
结合以上策略,你可以灵活应对不同场景下的需求,确保即使在使用 <KeepAlive> 缓存组件的情况下,也能保持组件内容的最新和准确。
浏览器输入网址到页面渲染完成经历的过程
- DNS解析:当您输入一个网址(URL)并按下回车键时,浏览器首先需要通过DNS(域名系统)将该网址的域名部分(如www.example.com)转换为对应的IP地址。这是找到托管网站服务器位置的关键步骤。
- TCP连接建立:一旦获取了目标服务器的IP地址,浏览器会尝试与该服务器建立TCP连接。这通常涉及到三次握手的过程,确保客户端和服务器双方都准备好进行数据传输。
- TLS/SSL握手(如果使用HTTPS) :如果访问的是一个HTTPS站点,那么在TCP连接建立之后,还需要进行TLS/SSL加密握手以确保通信的安全性。这一过程包括交换加密算法、生成会话密钥等活动,确保后续的数据传输是加密的。
- HTTP/HTTPS请求发送:建立了安全连接后,浏览器向服务器发送HTTP或HTTPS请求。这个请求可能包括多种类型,比如GET用于请求资源,POST用于提交数据等。
- 服务器响应:服务器接收到请求后,开始处理并返回相应的响应。这通常包含HTML文档以及状态码(如200表示成功,404表示未找到)。
- 浏览器下载HTML内容:浏览器接收到来自服务器的响应后,开始下载HTML内容。
- HTML解析与构建DOM树:浏览器解析下载下来的HTML代码,并将其转化为DOM(文档对象模型)树。在这个过程中,如果遇到外部资源链接(如CSS文件、JavaScript脚本等),浏览器会发起额外的请求来获取这些资源。
- 构建CSSOM(CSS对象模型) :同时,浏览器也会加载并解析CSS样式表,形成CSSOM。CSSOM和DOM结合使用来确定每个元素最终的样式。
- 执行JavaScript:如果网页中包含有JavaScript代码,浏览器会在适当的时候执行这些脚本。需要注意的是,某些情况下,JavaScript的执行可能会阻塞页面的进一步渲染。
- 渲染页面(Layout、Painting) :一旦DOM和CSSOM都准备好了,并且所有必要的资源都已经加载完毕,浏览器就开始布局页面元素的位置和大小,然后将它们绘制出来。这个过程可能需要重复几次,特别是在动态更新页面内容的情况下。
- 显示页面:最后,经过上述所有步骤后,页面被完整地显示给用户。
JS 堆与栈的数据结构
在JavaScript中,理解堆(Heap)和栈(Stack)这两种数据结构如何工作对于掌握内存管理和程序执行流程非常重要。它们各自有不同的用途和特点。
栈(Stack)
栈是一种后进先出(LIFO, Last In First Out)的数据结构。JavaScript引擎使用栈来管理函数调用以及局部变量的存储。每次当一个函数被调用时,一个新的栈帧(也称为活动记录)就会被创建,并被推入栈顶。这个栈帧包含了函数参数、局部变量等信息。当函数执行完毕后,它的栈帧就会从栈顶弹出,控制权返回给调用者。
- 特点:
- 固定大小:栈的大小是固定的,通常比堆小得多。
- 快速访问:由于栈的操作只发生在栈顶,因此访问速度非常快。
- 局部性:用于存储局部变量和函数调用信息。
堆(Heap)
堆是一个没有固定大小限制的区域,用于动态分配内存。与栈不同,堆上的内存分配不是按照线性顺序进行的;相反,当你需要分配内存时,会在堆中找到一块足够大的空间并分配给你。JavaScript中的对象、数组等复杂数据类型通常存储在堆上,因为它们的大小在编译时无法确定。
- 特点:
- 动态大小:可以根据需求动态地增加或减少。
- 较慢的访问速度:相比于栈,访问堆中的数据要慢一些,因为它涉及到指针的追踪。
- 共享性:多个对象可以共享堆中的同一块内存区域。
总结
- 栈主要用于存储基本类型的值(如数字、布尔值等)和引用类型的指针(即对象、数组等在堆中的地址)。它支持快速存取操作,并且其生命周期与函数调用直接相关。
- 堆则用来存储那些在运行时大小未知或者较大的数据,比如对象和数组。虽然堆提供了更大的灵活性,但相比栈来说,它的管理和垃圾回收更加复杂。
了解这些概念有助于优化代码性能、避免内存泄漏等问题。同时,这也是理解更高级概念如闭包、作用域链等的基础。
JS 绑定DOM元素的方法
一、选择 DOM 元素
1. 通过 ID 选择元素
const element = document.getElementById('elementId');
2. 通过类名选择元素(返回类数组对象)
const elements = document.getElementsByClassName('className');
3. 通过标签名选择元素(返回类数组对象)
const elements = document.getElementsByTagName('div');
4. 通过 CSS 选择器选择元素
-
选择第一个匹配的元素:
const element = document.querySelector('.class-name'); -
选择所有匹配的元素:
const elements = document.querySelectorAll('div.highlight');
5. 通过属性选择元素
const element = document.querySelector('[data-id="123"]');
二、操作 DOM 元素
1. 修改元素内容
element.textContent = '新文本内容'; // 纯文本(推荐)
element.innerHTML = '<strong>HTML 内容</strong>'; // 解析 HTML(注意 XSS 风险)
2. 修改元素属性
element.setAttribute('href', 'https://example.com'); // 设置属性
const value = element.getAttribute('data-id'); // 获取属性
element.removeAttribute('target'); // 删除属性
3. 修改元素样式
element.style.color = 'red'; // 直接修改行内样式
element.style.backgroundColor = '#f0f0f0'; // 注意驼峰命名
element.classList.add('active'); // 添加类名
element.classList.remove('disabled'); // 删除类名
element.classList.toggle('hidden'); // 切换类名
4. 绑定事件监听
element.addEventListener('click', (event) => {
console.log('元素被点击了', event.target);
});
5. 创建新元素并插入
// 创建元素
const newElement = document.createElement('div');
newElement.textContent = '新创建的元素';
// 插入到父元素末尾
parentElement.appendChild(newElement);
// 插入到某个元素前面
parentElement.insertBefore(newElement, referenceElement);
三、实用技巧
1. 事件委托(动态元素绑定)
document.getElementById('parent').addEventListener('click', (event) => {
if (event.target.matches('.child')) {
console.log('子元素被点击', event.target);
}
});
2. 使用 dataset 操作自定义属性
<div data-user-id="123" data-status="active"></div>
运行 HTML
const userId = element.dataset.userId; // 获取 data-user-id 的值
element.dataset.status = 'inactive'; // 修改 data-status 的值
3. 动态生成 HTML 模板
const items = ['Apple', 'Banana', 'Orange'];
const list = document.getElementById('list');
list.innerHTML = items.map(item => `<li>${item}</li>`).join('');
4. 使用 DocumentFragment 优化性能
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
document.body.appendChild(fragment);
四、注意事项
- 性能优化:频繁操作 DOM 会导致页面重绘,尽量批量操作(如使用
DocumentFragment)。 - 事件解绑:使用
removeEventListener避免内存泄漏。 - 现代 API:优先使用
querySelector和classList等现代方法。 - 兼容性:旧版浏览器(如 IE)可能不支持部分 API,需引入 Polyfill(如
classList.js)。
行内元素 块级元素
在HTML中,元素可以大致分为两类:行内元素(inline elements)和块级元素(block-level elements)。它们之间的主要区别在于它们如何布局以及它们对页面其他部分的影响。
行内元素(Inline Elements)
- 特性:
- 不会以新行开始,多个行内元素通常会在同一行显示,直到一行放不下才会换行。
- 宽度仅由其内容决定,无法通过设置宽度或高度来改变其尺寸。
- 可以设置水平方向的
margin和padding,但垂直方向的margin和padding不会影响周围元素的布局。
- 常见例子:
<a>:超链接<span>:用于组合文档中的行内元素<img>:图像<button>:按钮<input>:输入框<label>:表单控件的说明标签
块级元素(Block-level Elements)
- 特性:
- 总是从新行开始,并且会占据其父容器的整个宽度。
- 可以设置宽度、高度、行高(line-height)、内外边距等样式属性。
- 默认情况下,块级元素会按顺序垂直排列,每个元素另起一行。
- 常见例子:
<div>:最常用的块级容器元素<h1>到<h6>:标题标签<p>:段落<form>:表单<header>、<footer>、<article>、<section>等语义化标签
转换行为
CSS提供了display属性,允许你改变元素的默认显示类型。例如:
display: inline;:将块级元素转换为行内元素。display: block;:将行内元素转换为块级元素。display: inline-block;:使元素像行内元素一样排版,但允许设置宽高和内外边距,这为布局提供了更大的灵活性。
垂直水平居中方式
以下是几种常用的方法:
1. Flexbox 方法
使用CSS3中的Flexbox布局模型是目前最推荐的方式之一,因为它简单且灵活。
.parent {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
height: 100vh; /* 确保父容器有足够的高度 */
}
2. Grid 方法
CSS Grid布局也是一种强大的工具,特别适合复杂的页面布局。
.parent {
display: grid;
place-items: center; /* 同时水平和垂直居中 */
height: 100vh; /* 父容器的高度 */
}
3. 绝对定位与负边距
这种方法适用于已知子元素宽高情况下的居中处理。
.parent {
position: relative;
height: 100vh; /* 父容器高度 */
}
.child {
position: absolute;
top: 50%;
left: 50%;
width: 100px; /* 子元素宽度 */
height: 100px; /* 子元素高度 */
margin-top: -50px; /* 高度的一半 */
margin-left: -50px; /* 宽度的一半 */
}
4. 绝对定位与transform
当不知道子元素的具体尺寸时,可以使用transform属性来实现居中。
.parent {
position: relative;
height: 100vh; /* 父容器高度 */
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
5. 表格布局
通过将父容器设置为表格显示模式,并将其直接子元素作为表格单元格,也可以轻松实现居中。
.parent {
display: table;
width: 100%;
height: 100vh; /* 父容器高度 */
}
.child {
display: table-cell;
text-align: center; /* 水平居中 */
vertical-align: middle; /* 垂直居中 */
}
6. 内边距(Padding)
对于具有固定尺寸的父元素,可以通过内边距使内容居中。
.parent {
padding: 50px; /* 根据需要调整 */
box-sizing: border-box;
}
总结
- Flexbox 和 Grid 是现代Web开发中最常用来进行元素居中的方法,它们提供了简洁、灵活的解决方案。
- 对于特定场景或旧版浏览器支持的需求,可以考虑使用绝对定位结合负边距或
transform等技术。 - 使用表格布局也是一种可行的方法,尤其适用于需要兼容早期浏览器的情况。
性能优化
性能优化可以根据不同的关注点和层次进行分类,主要可以分为前端性能优化、后端性能优化以及网络层优化。
前端性能优化
- 减少资源加载时间
- 合并文件:合并CSS、JavaScript文件以减少HTTP请求次数。
- 压缩资源:使用工具压缩CSS、JavaScript和HTML代码,减小文件大小。
- 图片优化:选择合适的图片格式(如WebP),适当调整图片尺寸,使用CSS Sprites或内联小图。
- 异步加载与懒加载
- 异步加载非关键资源:对于不影响页面首次渲染的资源,采用异步加载方式。
- 懒加载:对图片、视频等媒体资源实施懒加载,仅在用户滚动到相关内容时才加载。
- 缓存机制
- 浏览器缓存:通过设置适当的Cache-Control, ETag等HTTP头信息,利用浏览器缓存来加快重复访问速度。
- Service Worker:用于离线存储和加速资源加载。
- 代码分割与Tree Shaking
- 代码分割:根据路由或组件动态加载JavaScript模块,减少初始加载时间。
- Tree Shaking:移除未使用的代码,减少打包体积。
- 使用CDN
- 利用内容分发网络(CDN)来分布静态资源,缩短用户与服务器之间的物理距离,提高访问速度。
后端性能优化
- 数据库优化
- 索引优化:为查询频繁的字段添加索引,提高查询效率。
- SQL语句优化:避免全表扫描,合理设计查询逻辑,减少复杂度。
- 分库分表:当单个数据库无法满足需求时,考虑水平或垂直拆分数据库。
- 缓存策略
- 内存缓存:如Redis,用于快速读取热点数据。
- 页面缓存:缓存完整的页面或部分内容,减少服务器负载。
- 异步处理
- 使用消息队列处理耗时任务,如邮件发送、数据分析等,释放主线程资源。
- 负载均衡
- 通过负载均衡器分散流量至多个服务器,提高系统可用性和响应速度。
- 微服务架构
- 将大型应用分解为多个小型服务,每个服务独立部署运行,便于扩展和维护。
网络层优化
- HTTP/2升级
- 升级至HTTP/2协议,支持多路复用、头部压缩等功能,显著提升传输效率。
- SSL/TLS优化
- 实施TLS 1.3,启用会话恢复机制,降低握手延迟。
- 减少DNS查找
- 减少外部资源链接,或使用DNS预解析技术提前解析域名。
- Gzip/Brotli压缩
- 对文本资源启用Gzip或Brotli压缩,减少传输数据量。
这些优化措施并非孤立存在,而是相互关联、共同作用于整个系统的性能提升。实际操作中应根据具体情况综合考虑,优先解决那些影响最大的瓶颈问题。同时,持续监控系统性能,并基于数据驱动的方法不断迭代优化策略。
localStorage、sessionStorage 和 Cookies区别
| 特性 | localStorage | sessionStorage | Cookies |
|---|---|---|---|
| 存储容量 | 通常为5MB左右(根据浏览器不同有所差异) | 同localStorage | 通常每个域名4KB左右 |
| 生命周期 | 没有过期时间,除非用户手动清除或通过代码删除 | 页面会话期间有效,关闭标签页即失效 | 可设置过期时间,默认是会话结束时失效 |
| 作用域 | 同源策略下所有同源窗口/tab共享 | 仅在相同文档源下的同一个标签页内共享 | 可通过domain、path属性控制访问范围 |
| 传输到服务器 | 不自动发送给服务器 | 不自动发送给服务器 | 在HTTP请求头中自动发送给服务器(除非设置HttpOnly) |
| 安全性 | 无任何安全机制 | 同localStorage | 支持Secure和HttpOnly标志增强安全性 |
| 适用场景 | 长期保存用户偏好设置、应用状态等 | 临时保存会话数据,比如表单内容 | 用户登录信息、跟踪用户行为等 |
| API操作简便性 | 使用.setItem(), .getItem(), .removeItem()等方法进行操作 | API与localStorage完全一致 | 设置需要通过document.cookie字符串操作,相对复杂 |
| 跨窗口同步 | 所有同源的窗口和tab之间即时同步 | 仅限于创建它的标签页 | 在所有同源的窗口和tab间同步 |
v-if 与 v-show 的区别
为了清晰地对比Vue.js中的v-if与v-show指令,我们可以将它们的主要特性整理成一个表格。
| 特性 | v-if | v-show |
|---|---|---|
| 初始渲染 | 当条件为假时,元素不会被渲染到DOM中 | 无论条件真假,元素都会被渲染到DOM中 |
| 切换开销 | 切换条件时,会触发完整的销毁和重建过程 | 只是简单地切换元素的CSS属性display值 |
| 适用场景 | 条件很少改变、或者需要完全移除/添加元素的情况 | 需要频繁切换显示状态的情况 |
| 性能影响 | 初始加载时如果条件为假,则性能较好;切换时性能消耗较大 | 初始加载时性能消耗固定;切换显示状态性能较好 |
| 包含内容 | 如果条件不满足,子节点以及所有相关的事件监听器等资源也会被移除 | 仅通过CSS隐藏元素,所有的子节点及事件监听器仍然存在于DOM中 |
| 嵌套组件 | 当条件不满足时,嵌套的组件不会被初始化 | 嵌套的组件总是会被初始化 |
总结
v-if更加适用于那些不需要频繁切换显示状态的场景,因为它涉及到更多的DOM操作,包括创建和销毁元素。v-show则更适合于那些需要频繁显示或隐藏元素的场景,因为它的实现方式仅仅是通过CSS的display属性来控制元素的可见性,避免了重复渲染带来的性能损耗。
watch 与 computed 两种机制的区别
| 特性 | computed | watch |
|---|---|---|
| 用途 | 用于计算基于其他数据的变化而变化的值,适合需要根据已有状态派生出新状态的情况 | 监听某个特定的数据源,并在该数据源发生变化时执行异步操作或开销较大的操作 |
| 依赖追踪 | 自动追踪其依赖的数据(响应式依赖),当这些依赖发生改变时自动更新结果 | 不会自动追踪依赖;需明确指定要监听的数据源 |
| 执行时机 | 在其依赖的数据发生变化时立即重新计算 | 当被监听的数据发生变化时触发回调 |
| 缓存机制 | 结果会被缓存,只有当相关依赖的数据发生变化时才会重新计算 | 没有缓存机制 |
| 适用场景 | 需要基于某些状态进行复杂逻辑处理并返回结果的场景 | 需要在数据变动后执行异步操作、开销较大的操作或者需要深度定制响应行为的场景 |
| 代码简洁性 | 通常可以使代码更加简洁,尤其是对于简单的逻辑表达式 | 对于复杂的逻辑或者需要执行额外操作的情况更为灵活 |
| 同步/异步支持 | 主要用于同步计算 | 支持异步操作,如通过AJAX请求获取数据 |
实际应用示例
-
computed示例computed: { fullName() { return `${this.firstName} ${this.lastName}`; } }这里,
fullName会根据firstName和lastName的变化自动更新。 -
watch示例watch: { firstName(newValue, oldValue) { console.log(`firstName changed from ${oldValue} to ${newValue}`); // 可以在这里执行异步操作,比如发送网络请求 } }使用
watch可以在firstName发生变化时执行一些额外的操作,如日志记录或异步通信。
总结
computed更适用于基于已有数据进行简单到中等复杂度的计算,并且希望计算结果能够随着依赖数据的变化而自动更新的场景。watch则更适合用于监听特定数据的变化,并在此基础上执行异步任务或其他较为耗时的任务,以及当你需要对数据变化做出更复杂的响应时。
Vue 的插槽
在Vue.js中,插槽(Slots)是组件间内容分发的核心机制,它允许你在组件的模板中定义可替换的部分,并让使用该组件的地方能够填充这些部分。插槽使得组件更加灵活和可复用。
一、插槽的基本类型
1.基本插槽
基本插槽允许你将内容插入到组件的指定位置。
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<slot>默认内容</slot> <!-- 如果没有提供内容,则显示默认内容 -->
</div>
</template>
<!-- 父组件 ParentComponent.vue -->
<template>
<ChildComponent>
这里是通过插槽传递的内容!
</ChildComponent>
</template>
2.具名插槽
具名插槽允许你在同一个组件内定义多个插槽,每个插槽都有一个唯一的名称。
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<!-- 父组件 ParentComponent.vue -->
<template>
<ChildComponent>
<template v-slot:header>
<h1>这里是头部内容</h1>
</template>
<p>这是主要内容。</p>
<template v-slot:footer>
<p>这里是页脚内容</p>
</template>
</ChildComponent>
</template>
注意:v-slot指令是从Vue 2.6.0开始引入的,用来替代已废弃的slot和slot-scope属性。
3.作用域插槽
作用域插槽允许子组件向父组件传递数据,使得父组件可以在其模板中使用子组件的数据。
<!-- 子组件 ListComponent.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="index"></slot>
</li>
</ul>
</template>
<!-- 父组件 -->
<ListComponent :items="userList">
<!-- Vue 3 语法 -->
<template v-slot="{ item, index }">
<span>{{ index + 1 }}. {{ item.name }}</span>
</template>
<!-- Vue 3 简写 -->
<template #default="{ item }">
<span>{{ item.email }}</span>
</template>
</ListComponent>
二、Vue 2 和 Vue 3 的插槽语法对比
| 功能 | Vue 2 语法 | Vue 3 语法(推荐) |
|---|---|---|
| 具名插槽 | slot="name" | v-slot:name 或 #name |
| 作用域插槽 | slot-scope="props" | v-slot="props" |
| 默认插槽作用域 | 不支持 | v-slot="props" 或 #default="props" |
三、高级技巧
1. 动态插槽名
<template #[dynamicSlotName]>
<!-- 内容 -->
</template>
2. 插槽透传(Renderless 组件)
<!-- 子组件 HoverComponent.vue -->
<template>
<div @mouseover="isHovered = true" @mouseleave="isHovered = false">
<slot :isHovered="isHovered"></slot>
</div>
</template>
<!-- 父组件 -->
<HoverComponent v-slot="{ isHovered }">
<div :class="{ active: isHovered }">悬停效果</div>
</HoverComponent>
四、插槽的注意事项
- 作用域隔离:插槽内容在父组件作用域中编译,无法直接访问子组件的数据。
- 性能优化:频繁更新的插槽内容可能导致子组件重新渲染,可结合
v-once或v-memo优化。 - 避免滥用:过度使用插槽可能让组件结构复杂化,优先考虑 Props 和简单插槽组合。
Vue 的虚拟DOM
Vue.js使用虚拟DOM(Virtual DOM)作为其响应式系统的一部分,以提高应用的性能和开发体验。虚拟DOM是一个内存中的轻量级副本,它代表了真实DOM的状态。通过对比新旧虚拟DOM树的不同之处,Vue可以高效地更新实际的DOM,仅对发生变化的部分进行必要的修改。以下是关于Vue虚拟DOM的关键概念和工作原理:
关键概念
-
虚拟节点(VNode):在Vue中,每个DOM元素都有一个对应的虚拟节点表示。这些虚拟节点包含了描述真实DOM元素所需的所有信息,如标签名、属性、子节点等,但它们本身并不直接渲染到页面上。
-
Patch算法:当数据变化导致虚拟DOM树更新时,Vue会通过所谓的“patch”过程来比较新旧两棵虚拟DOM树的不同,并根据差异执行最小化的DOM操作,从而减少不必要的重绘和回流,提升性能。
工作流程
- 初始化渲染:当首次加载或完全重新渲染组件时,Vue会创建一个完整的虚拟DOM树。
- 状态变更检测:当组件内部的数据发生改变时,Vue能够检测到这些变化,并生成一个新的虚拟DOM树。
- Diff算法:Vue采用高效的Diff算法来对比新的虚拟DOM树与旧的版本之间的差异。
- DOM更新:基于Diff的结果,Vue只更新那些真正发生了变化的部分到真实的DOM中,而不是整个重新渲染页面。
优势
- 性能优化:通过最小化DOM操作,虚拟DOM机制显著减少了浏览器的工作量,提高了应用的性能。
- 跨平台兼容性:由于虚拟DOM不依赖于特定平台的具体实现细节,因此Vue可以在不同的环境(如Web、Weex、NativeScript等)下运行,增强了框架的通用性和灵活性。
实际应用注意事项
虽然虚拟DOM提供了许多好处,但在实践中也需要注意一些事项:
- 尽量避免直接操作DOM,因为这可能会干扰Vue的虚拟DOM机制。
- 对于大量静态内容的场景,考虑使用
v-once指令来避免不必要的重新渲染。 - 合理设计组件结构,尽量减少不必要的嵌套层级,有助于提高Diff算法的效率。
Vue-router有几种模式 有什么区别
Vue Router是Vue.js的官方路由管理器,它允许你通过定义路由来构建单页面应用(SPA)。Vue Router支持几种不同的模式来配置路由行为,主要包括:hash模式、history模式。
| 特性 | Hash 模式 | History 模式 |
|---|---|---|
| URL 形式 | 带有 # 的URL | 无 # 的真实URL |
| 后端配置要求 | 无需特别配置 | 需要配置以支持 HTML5 History API |
| SEO 影响 | 不利于SEO优化 | 对SEO更加友好 |
| 兼容性 | 广泛兼容旧版浏览器 | 主要适用于现代浏览器 |
| 页面刷新问题 | 不受直接影响 | 如果后端未正确配置,直接访问子路由会导致404 |
选择哪种模式取决于你的具体需求,包括是否需要考虑SEO、目标浏览器的支持情况以及是否有能力配置后端服务器。对于大多数现代Web应用来说,如果可以适当地配置后端服务器,推荐使用history模式以获得更佳的用户体验和SEO效果。然而,在一些情况下,比如快速原型设计或对旧版浏览器的支持更为关键时,hash模式可能是更合适的选择。
浏览器的同源策略
同源策略是浏览器的一种关键安全机制,它限制了一个源(协议+域名+端口)的文档或脚本如何与另一个源的资源进行交互。目的是为了防止恶意网站读取另一个网站的敏感数据,从而保护用户信息安全。
- 源的定义:两个URL只有在协议、域名和端口号完全相同的情况下才被认为是同源。
- 限制行为包括但不限于:
- 不能读取非同源网页的内容(如通过
XMLHttpRequest或fetch请求获取数据)。 - 不能操作不同源页面的DOM。
- 不能访问不同源页面的Cookies、LocalStorage等存储信息。
- 不能读取非同源网页的内容(如通过
跨域问题及解决方案
当一个请求的URL的协议、域名或端口号与当前页面不同时,就会发生跨域问题。为了解决这个问题,有多种方法可以采用:
1. CORS (Cross-Origin Resource Sharing)
CORS是一种W3C标准,它允许服务器声明哪些源可以从客户端浏览器中访问其资源。实现方式是通过添加特定的HTTP头部来告知浏览器该资源是否可以被共享给其他源。
- 简单请求:满足一定条件的GET、POST或HEAD请求可以直接发送,并且浏览器会自动处理预检请求。
- 预检请求(Preflight Request) :对于非简单请求(如PUT, DELETE),浏览器会先发送一个OPTIONS请求到服务器以确认实际请求是否安全可接受。
服务器需要设置适当的响应头,例如
Access-Control-Allow-Origin指定允许访问的源。
2. JSONP (JSON with Padding)
一种早期用于解决跨域问题的技术,适用于GET请求。它利用了HTML <script> 标签不受同源策略限制的特点,让服务器返回一段调用指定回调函数的JavaScript代码。
不过,由于JSONP只能支持GET请求,并且存在安全隐患,现代应用中已较少使用。
3. 代理服务器
如果前端无法直接请求目标资源,可以通过配置一个后端代理服务器来间接请求资源。即前端向同源的代理服务器发起请求,代理服务器再去请求外部资源并返回结果给前端。 这种方法虽然有效,但增加了额外的复杂性和可能的性能开销。
4. WebSocket
WebSocket协议本身并不遵循同源策略,因此可以用来绕过跨域限制。然而,这通常只适用于需要实时双向通信的应用场景。
5. PostMessage API
用于不同窗口或iframe之间的安全通信。如果你的应用涉及多个子域下的iframe通信,或者需要与第三方页面通信时,可以使用这个API来发送消息。
总结
解决跨域问题的方法有很多,选择哪种取决于具体的应用场景和技术栈。随着CORS成为主流解决方案,许多旧有的跨域技术(如JSONP)逐渐被淘汰。正确理解和实施这些策略不仅有助于提高开发效率,还能增强Web应用的安全性。
GET 与 POST 请求的区别
| 特性 | GET | POST |
|---|---|---|
| 数据传输方式 | 通过URL参数(查询字符串)传递数据 | 通过请求体(request body)传递数据 |
| 数据长度限制 | 受限于浏览器地址栏长度(通常不超过2048字符),具体取决于浏览器和服务器设置 | 理论上没有长度限制,但实际受服务器配置等影响 |
| 安全性 | 数据直接显示在URL中,可能被浏览器历史记录、Web服务器日志保存,并可被收藏或缓存 | 数据不在URL中显示,相对更安全,但仍需注意使用HTTPS加密 |
| 缓存与书签 | 请求可以被浏览器缓存,且用户可以将包含查询参数的URL添加为书签 | 请求通常不会被缓存,也不能被加入到书签中 |
| 幂等性 | 幂等操作,多次相同的GET请求应产生相同的结果,不应引起副作用 | 非幂等操作,同样的POST请求可能会导致不同的结果 |
| 用途 | 主要用于获取信息,不应有其他影响 | 用于提交数据给服务器,如表单提交、文件上传等 |
| 对SEO的影响 | 可以被搜索引擎索引 | 不适用于需要被搜索引擎索引的内容 |
| 适用场景示例 | 检索文章、图片、视频等资源 | 提交订单、登录验证、上传文件等 |
总结
- GET:更适合用于那些不需要修改服务器状态的操作,特别是获取数据时。它简单、快速,但由于其特性,不适合用于处理敏感信息或执行会改变服务器状态的操作。
- POST:适用于需要发送大量数据、涉及敏感信息或执行非幂等操作的情况。虽然比GET复杂一些,但它提供了更高的灵活性和安全性。
CSS 盒子模型
CSS 盒子模型是CSS布局的核心概念之一,它描述了每一个HTML元素如何被视为一个矩形的“盒子”,并且这个盒子由四个部分组成:外边距(Margin)、边框(Border)、内边距(Padding)和内容区域(Content)。
CSS 盒子模型的组成部分
-
内容区域 (Content)
- 这是盒子模型的最内层部分,用于放置实际的内容,比如文本、图片等。
- 可以通过
width和height属性来定义内容区域的尺寸。
-
内边距 (Padding)
- 内边距位于内容区域与边框之间,用来增加内容与其边框之间的空间。
- 通过
padding属性设置,可以为上下左右分别设置不同的值,如padding-top,padding-right,padding-bottom,padding-left或简写形式padding: top right bottom left;。
-
边框 (Border)
- 边框围绕着内边距,并且可以通过
border属性进行设置,包括边框的宽度、样式和颜色。 - 类似于内边距,边框也可以针对每个方向单独设置,例如
border-width,border-style,border-color或简写的border: width style color;。
- 边框围绕着内边距,并且可以通过
-
外边距 (Margin)
- 外边距位于边框之外,主要用于控制元素与其他元素之间的距离。
- 使用
margin属性设置,同样支持单独设置各个方向的外边距,或使用简写形式margin: top right bottom left;。
标准盒子模型 vs IE盒子模型
标准盒子模型(W3C 标准)和 IE 盒子模型(怪异模式)的核心区别在于 width 和 height 的计算方式。
1. 标准盒子模型(content-box)
-
计算规则:
width和height仅表示 内容区域 的尺寸。- 实际占用空间 = 内容(
width/height) + 内边距(padding) + 边框(border) + 外边距(margin)。
-
示意图:
Total Width = width + padding-left + padding-right + border-left + border-right Total Height = height + padding-top + padding-bottom + border-top + border-bottom -
示例:
div { width: 200px; padding: 20px; border: 10px solid black; margin: 30px; }- 内容宽度:
200px - 总宽度:
200px + 40px(padding) + 20px(border) = 260px - 实际占位宽度:
260px + 60px(margin) = 320px
- 内容宽度:
2. IE盒子模型(border-box)
-
计算规则:
width和height包含 内容 + 内边距 + 边框。- 实际占用空间 =
width/height(已包含 padding 和 border) + 外边距(margin)。
-
示意图:
Total Width = width(已包含 padding 和 border) Total Height = height(已包含 padding 和 border) -
示例:
div { width: 200px; padding: 20px; border: 10px solid black; margin: 30px; box-sizing: border-box; }- 总宽度:
200px(包含40px padding + 20px border) - 内容宽度:
200px - 40px(padding) - 20px(border) = 140px - 实际占位宽度:
200px + 60px(margin) = 260px
- 总宽度:
3. 对比表格
| 特性 | 标准盒子模型(content-box) | IE盒子模型(border-box) |
|---|---|---|
| width/height | 仅内容区域 | 内容 + 内边距 + 边框 |
| 实际尺寸 | 需要手动累加 padding 和 border | 直接由 width/height 定义 |
| 兼容性 | 所有浏览器默认模式 | IE 怪异模式,现代浏览器需显式设置 |
| 适用场景 | 需要精确控制内容尺寸 | 简化布局计算(如响应式设计) |
4. 如何切换盒子模型?
通过 box-sizing 属性控制:
-
标准模型:
box-sizing: content-box; /* 默认值 */ -
IE模型:
box-sizing: border-box; /* 推荐全局使用 */
HTTP请求 1.0 与 2.0 的区别
| 特性 | HTTP/1.0 | HTTP/2.0 |
|---|---|---|
| 连接管理 | 每个请求都需要新建一个TCP连接(除非使用Keep-Alive) | 单一持久连接支持多路复用,允许多个请求和响应共享同一个连接 |
| 并发处理 | 不支持多路复用,限制了同时处理的请求数量 | 支持多路复用,可以在一个连接上并行发送多个请求和响应 |
| 头部压缩 | 无头部压缩,每次请求都携带完整的HTTP头信息 | 使用HPACK算法对HTTP头部进行压缩,减少传输的数据量 |
| 服务器推送 | 服务器只能被动响应客户端请求 | 支持服务器主动推送资源到客户端,提高页面加载速度 |
| 性能优化 | 性能受限于连接建立和关闭的开销 | 显著提高了页面加载速度和整体性能,减少了延迟 |
| 安全性 | 本身不强制加密,但可以通过HTTPS增强安全性 | 虽然协议本身不要求加密,但大多数实现基于TLS,提供更好的安全性 |
| 协议开销 | 较高的协议开销,特别是对于频繁交互的应用 | 降低了协议开销,通过更高效的编码和传输机制 |
总结
- HTTP/1.0 更简单直接,但其设计导致了在处理现代Web应用时的一些瓶颈,如高延迟和低效的资源利用。
- HTTP/2.0 引入了多项改进,旨在克服HTTP/1.0的局限性,提供了更好的性能、更低的延迟以及更高的效率,特别适合于需要快速响应和高效数据传输的现代Web应用。
对闭包的理解
闭包(Closure)是JavaScript中一个非常重要的概念,它指的是一个函数能够记住并访问它的词法作用域,即使这个函数在其词法作用域之外执行。简单来说,闭包使得函数可以“捕获”和“保留”其创建时的作用域环境。
1. 定义与基本原理
- 定义:当一个函数嵌套在另一个函数内部,并且这个内部函数引用了外部函数的变量,则这个内部函数就形成了一个闭包。
- 基本原理:闭包允许内部函数访问外部函数的作用域链中的变量,即便外部函数已经执行完毕,这些变量依然保留在内存中,因为闭包持有对外部函数局部变量的引用。
2. 闭包的形成条件
- 嵌套函数:内部函数定义在外部函数内。
- 内部函数引用外部变量:内部函数使用外部函数的作用域变量。
- 内部函数被外部引用:内部函数被返回、传递给其他函数或绑定到全局对象,导致其生命周期延长。
3. 闭包的作用机制
- 作用域链保留:当函数创建时,会生成一个包含其定义时所在作用域链的作用域对象。即使外部函数执行完毕,只要内部函数仍被引用,其作用域链不会被销毁。
- 变量持久化:闭包使得外部函数的变量在内存中保留,不会被垃圾回收,直到闭包本身不再被引用。
示例:
function outer() {
let count = 0;
return function inner() {
count++;
return count;
};
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
inner函数引用了outer的变量count,形成闭包。count变量在outer执行后仍保留在内存中。
4. 闭包的常见应用场景
a. 封装私有变量
const createCounter = () => {
let count = 0;
return {
increment: () => count++,
getCount: () => count
};
};
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
count无法被外部直接访问,仅通过闭包暴露的方法操作。
b. 解决循环中的异步问题
// 错误示例(输出5个5)
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000);
}
// 正确方案:闭包保存每次循环的i值
for (var i = 0; i < 5; i++) {
((j) => {
setTimeout(() => console.log(j), 1000);
})(i);
}
// 输出0,1,2,3,4
// 或使用let块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000);
}
c. 模块化开发
const module = (() => {
let privateData = 0;
const privateMethod = () => privateData;
return {
publicMethod: () => privateMethod()
};
})();
module.publicMethod(); // 访问私有数据
5. 闭包的优缺点
| 优点 | 缺点 |
|---|---|
| 实现数据私有化,避免全局污染 | 内存占用高,可能引发内存泄漏 |
| 保持变量状态,实现函数工厂 | 过度使用导致代码可读性降低 |
| 支持高阶函数和模块化设计 | 需注意循环引用问题 |
6. 内存泄漏与解决策略
-
风险场景:闭包引用DOM元素或大型对象,且未及时释放。
-
解决方案:
- 及时解除引用:手动将闭包赋值为
null。 - 避免循环引用:如闭包引用DOM元素,元素销毁前移除事件监听。
- 及时解除引用:手动将闭包赋值为
示例:
function init() {
const element = document.getElementById('myButton');
element.onclick = () => {
console.log('Clicked:', element.id); // 闭包引用element
};
// 移除时解除引用
element.onclick = null;
}
7. 闭包与词法作用域
- 词法作用域(静态作用域) :函数的作用域在定义时确定,而非执行时。
- 闭包的本质:函数携带其定义时的作用域环境,无论在哪里调用。
8. 总结
闭包是JavaScript强大特性之一,它不仅增强了语言的表现力,还支持了诸如函数式编程等高级编程技巧的应用。理解闭包如何工作以及何时使用它们对于编写高效、可维护的代码至关重要。
正确使用闭包可以帮助我们更好地控制变量的作用范围,提高代码的安全性和灵活性。然而,开发者也需要注意避免因滥用闭包而导致的潜在问题,如内存泄漏等。
讲讲垃圾回收机制
JavaScript 的垃圾回收机制(Garbage Collection, GC)是一种自动管理内存的机制,通过定期检测和回收不再使用的内存空间,防止内存泄漏并优化性能。以下是核心要点和运行原理:
垃圾回收的核心目标
- 自动释放不再使用的内存:开发者无需手动管理内存(如 C/C++ 中的
malloc/free)。 - 避免内存泄漏:防止无用的对象长期占用内存。
垃圾回收的关键算法
JavaScript 引擎(如 V8)主要使用以下两种算法:
1. 标记清除(Mark-and-Sweep) (主流算法)
-
步骤:
- 标记阶段:从根对象(全局变量、当前执行上下文中的变量等)出发,遍历所有可访问的对象,标记为“存活”。
- 清除阶段:遍历堆内存,清除未被标记的对象,释放其内存。
-
优点:解决了循环引用问题。
-
示例:
let obj1 = { ref: null }; let obj2 = { ref: null }; obj1.ref = obj2; // obj1 引用 obj2 obj2.ref = obj1; // obj2 引用 obj1(循环引用) obj1 = null; obj2 = null; // 标记清除会正确回收这两个对象
2. 引用计数(Reference Counting) (已基本淘汰)
- 原理:记录每个对象被引用的次数,当引用数为 0 时立即回收。
- 缺点:无法处理循环引用。
function problem() { let a = {}; let b = {}; a.ref = b; // a 引用 b(计数为 1) b.ref = a; // b 引用 a(计数为 1) } problem(); // 函数执行完,a 和 b 的引用数仍为 1,无法回收
V8 引擎的优化策略
现代 JavaScript 引擎(如 V8)在标记清除的基础上,结合以下策略优化性能:
1. 分代回收(Generational Collection)
- 内存分代:
- 新生代(Young Generation) :存放存活时间短的对象(如局部变量),使用 Scavenge 算法(复制存活对象到新空间)。
- 老生代(Old Generation) :存放存活时间长的对象(如全局变量、闭包变量),使用 标记-清除 和 标记-整理(Mark-Compact) 。
- 优点:针对不同生命周期的对象采用不同策略,提高效率。
2. 增量回收(Incremental GC)
- 将垃圾回收任务分成多个小步骤执行,避免长时间阻塞主线程。
3. 空闲回收(Idle-Time GC)
- 在浏览器空闲时执行垃圾回收(如 Chrome 的
requestIdleCallback)。
内存泄漏的常见场景
虽然垃圾回收机制能自动管理内存,但以下情况仍会导致内存泄漏:
1. 意外的全局变量
function leak() {
leakedVar = '全局变量'; // 未用 let/const/var 声明,成为全局变量!
}
2. 未清理的定时器或事件监听
const button = document.getElementById('myButton');
button.addEventListener('click', onClick);
// 若不再需要监听,需手动移除:removeEventListener('click', onClick)
3. 脱离 DOM 的引用
const elements = {
button: document.getElementById('myButton'),
};
// 即使 DOM 中移除了该按钮,elements.button 仍保留引用
4. 闭包不当使用
function createHeavyClosure() {
const bigData = new Array(1000000).fill('data');
return function() {
// 闭包持有 bigData 的引用,即使不再需要
};
}
const closure = createHeavyClosure();
// 不再使用时需解除引用:closure = null;
手动触发垃圾回收(仅调试用)
- 浏览器:通过
window.gc()(需启动 Chrome 时添加--js-flags="--expose-gc")。 - Node.js:调用
global.gc()(需启动时添加--expose-gc)。
总结
JavaScript 的垃圾回收机制通过标记清除、分代回收等策略高效管理内存,但开发者仍需警惕内存泄漏场景。合理使用开发者工具分析内存,结合代码优化,才能构建高性能应用。