数字马力 前端开发工程师 一面
一、CSS 垂直居中
1. 基于 line - height(适用于单行内联元素)
- 对于一个单行的文本元素,设置元素的
height
和line - height
值相等,就可以实现垂直居中。例如:
div {
height: 100px;
line - height: 100px;
}
- 这里的
div
中的文本就会在垂直方向上居中。
2. 基于 table - cell(适用于块级元素)
- 把父元素设置为
display: table - cell
,并且设置vertical - align: middle
,可以让子元素垂直居中。例如:
.parent {
display: table - cell;
vertical - align: middle;
}
- 这种方法对于一些复杂的布局,如表格布局的模拟场景很有用。
3. 基于 flex 布局
- 当父元素设置为
display: flex
时,使用align - items: center
可以让子元素在交叉轴(通常是垂直方向)上居中。例如:
.container {
display: flex;
align - items: center;
}
- 并且,如果需要在主轴和交叉轴都居中,可以同时设置
justify - content: center
和align - items: center
。
4. 基于 grid 布局
- 在网格布局中,使用
place - items: center
可以让子元素在网格单元格中垂直和水平都居中。例如:
.grid - container {
display: grid;
place - items: center;
}
- 或者也可以分别设置
align - items: center
(垂直居中)和justify - items: center
(水平居中)。
二、JS 数据类型
1. 基本数据类型
- Number(数字):包括整数和浮点数,例如
1
,3.14
。可以进行数学运算,如加法、减法等。 - String(字符串):是由零个或多个字符组成的序列,用单引号或双引号包裹,如
'hello'
,"world"
。可以进行字符串拼接、截取等操作。 - Boolean(布尔值):只有两个值
true
和false
,用于条件判断,比如if
语句中。 - Null(空值):表示一个空对象指针,只有一个值
null
。 - Undefined(未定义):当一个变量声明但未赋值时,它的值是
undefined
。
2. 引用数据类型
- Object(对象):是一组无序的属性和方法的集合。例如
{name: 'John', age: 30}
,对象的属性可以通过点语法(obj.name
)或者方括号语法(obj['name']
)访问。 - Array(数组):是一种特殊的对象,用于存储多个值,值可以是不同的数据类型。例如
[1, 2, 'three']
,可以通过索引访问数组元素,如arr[0]
。 - Function(函数):函数也是对象,它可以包含可执行的代码块。例如:
function add(a, b) {
return a + b;
}
- 函数可以被调用,传递参数并返回结果。
三、ES6 新特性
1. 变量声明(let 和 const)
- let:
- 块级作用域变量声明。例如:
{
let x = 10;
console.log(x);
}
console.log(x); // 报错,x在块外不可访问
- 不存在变量提升,必须先声明再使用。
- const:
- 用于声明常量,一旦赋值就不能再重新赋值。例如:
const PI = 3.14;
PI = 3.15; // 报错
- 但如果常量是对象或数组,其内部的属性或元素可以被修改。
2. 箭头函数(Arrow Functions)
- 简化函数的写法。例如:
// 普通函数
function add(a, b) {
return a + b;
}
// 箭头函数
const add = (a, b) => a + b;
- 箭头函数没有自己的
this
,它会继承外层作用域的this
,这在处理回调函数中的this
指向问题时很有用。
3. 模板字符串(Template Strings)
- 用反引号(`)包裹,可以包含变量和表达式。例如:
const name = 'John';
const greeting = `Hello, ${name}!`;
console.log(greeting);
- 可以多行书写,不需要使用
+
来拼接字符串。
4. 解构赋值(Destructuring Assignment)
- 可以从数组或者对象中提取值并赋值给变量。例如:
// 数组解构
const [a, b] = [1, 2];
// 对象解构
const {x, y} = {x: 10, y: 20};
- 可以用于函数参数,方便地获取对象的属性作为参数。
四、对象拷贝
1. 浅拷贝
- Object.assign():
- 用于将一个或多个源对象的可枚举属性复制到目标对象。例如:
const source = {a: 1, b: 2};
const target = {};
Object.assign(target, source);
console.log(target); // {a: 1, b: 2}
- 但是对于对象中的对象属性,只是复制了引用。例如:
const source = {a: {x: 1}, b: 2};
const target = {};
Object.assign(target, source);
source.a.x = 2;
console.log(target.a.x); // 2,因为只是浅拷贝,内部对象引用相同
2. 深拷贝
- JSON.parse(JSON.stringify()):
- 先将对象转换为 JSON 字符串,再将 JSON 字符串解析回对象。例如:
const source = {a: {x: 1}, b: 2};
const target = JSON.parse(JSON.stringify(source));
source.a.x = 2;
console.log(target.a.x); // 1,深拷贝后互不影响
- 但是这种方法有局限性,不能处理函数、循环引用等情况。对于复杂的深拷贝场景,可能需要使用递归等自定义方法。
五、字符串数组去重
1. 使用 Set
- Set 是 ES6 中新增的数据结构,它的成员具有唯一性。例如:
const arr = ["a", "b", "a", "c"];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // ["a", "b", "c"]
- 首先通过
new Set(arr)
创建一个 Set 对象,它会自动去除重复元素,然后通过扩展运算符...
将 Set 对象转换回数组。
2. 循环遍历并检查
- 可以通过一个新数组来存储不重复的元素。例如:
const arr = ["a", "b", "a", "c"];
const uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
if (!uniqueArr.includes(arr[i])) {
uniqueArr.push(arr[i]);
}
}
console.log(uniqueArr); // ["a", "b", "c"]
- 这种方法比较直观,但是对于大型数组效率可能不如使用 Set。
六、ES6 中的数字去重(和上面的数组去重类似)
1. 使用 Set
- 例如:
const numbers = [1, 2, 1, 3];
const uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers); // [1, 2, 3]
- 原理和字符串数组去重一样,利用 Set 的唯一性。
2. 传统的循环和条件判断
- 例如:
const numbers = [1, 2, 1, 3];
const uniqueNumbers = [];
for (let i = 0; i < numbers.length; i++) {
if (!uniqueNumbers.includes(numbers[i])) {
uniqueNumbers.push(numbers[i]);
}
}
console.log(uniqueNumbers); // [1, 2, 3]
- 当遇到大型的数字数组时,还可以考虑先对数组进行排序,这样可以减少比较次数,提高效率。
七、浏览器本地存储、Local Storage 和 Session Storage 有什么区别
1. 生命周期
- Local Storage:
- 数据存储是持久化的,除非用户手动清除浏览器缓存或者通过代码清除,否则数据会一直存在。例如,一个网站的用户登录信息可以存储在 Local Storage 中,下次用户打开浏览器访问该网站时,仍然可以获取到登录信息。
- Session Storage:
- 数据的生命周期和会话(session)相关。当浏览器窗口或者标签页关闭时,存储在 Session Storage 中的数据就会被清除。比如,一个购物车应用在用户购物过程中,可以把购物车数据存储在 Session Storage 中,当用户关闭浏览器窗口后,购物车数据就不再需要保留。
2. 存储容量
- 通常,Local Storage 和 Session Storage 的存储容量在大多数浏览器中大约是 5MB 左右,但是不同浏览器可能会有所差异。
3. 作用域
- Local Storage:
- 同一个域名下的所有页面(包括不同的标签页、不同的浏览器窗口)都可以共享 Local Storage 中的数据。例如,一个网站的多个子页面可以通过访问 Local Storage 来获取和更新用户的偏好设置。
- Session Storage:
- 数据仅在当前浏览器窗口或者标签页的会话中有效。不同的浏览器窗口或者标签页之间不能共享 Session Storage 中的数据。
八、IndexDB
1. 概述
- IndexedDB 是一个在浏览器中存储大量结构化数据(包括文件 / Blobs)的 API。它是一个事务型的数据库系统,类似于传统的关系型数据库,但是它是基于 JavaScript 对象存储的。
2. 特点
- 异步操作:所有的操作都是异步的,避免了阻塞主线程。例如,打开数据库、读取数据等操作都通过回调函数或者
async/await
来处理异步结果。 - 数据存储结构:数据以对象仓库(Object Store)的形式存储,每个对象仓库可以存储具有相似结构的对象。例如,可以有一个存储用户信息的对象仓库,其中每个对象包含
name
、age
、email
等属性。 - 索引(Index)支持:可以为对象仓库中的属性创建索引,方便快速查询。比如,为用户信息仓库中的
email
属性创建索引,这样就可以通过email
快速查找用户。
3. 使用场景
- 适合存储大量的离线数据,如 Web 应用的缓存数据、文档管理应用中的文档数据等。
九、节流和防抖
1. 节流(Throttle)
- 概念:节流是指在一定时间内,只允许函数执行一次。比如,一个滚动事件,不管滚动多快,我们只希望每 100 毫秒执行一次相关的函数,这样可以避免频繁地执行函数导致性能问题。
- 实现示例(使用定时器):
function throttle(func, delay) {
let timer = null;
return function() {
if (!timer) {
func.apply(this, arguments);
timer = setTimeout(() => {
timer = null;
}, delay);
}
};
}
- 这个节流函数返回一个新的函数,在新函数内部,只有当
timer
为null
时才执行原始函数func
,然后设置一个定时器,在delay
时间后将timer
重置为null
,这样就限制了函数的执行频率。
2. 防抖(Debounce)
- 概念:防抖是指在事件被触发后,延迟一段时间执行函数,如果在这段延迟时间内事件又被触发,则重新计算延迟时间。例如,一个搜索框的输入事件,只有当用户停止输入一段时间(如 500 毫秒)后才执行搜索函数,这样可以避免在用户输入过程中频繁地进行搜索。
- 实现示例(使用定时器):
function debounce(func, delay) {
let timer = null;
return function() {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
- 当事件触发时,如果已经有定时器存在(说明之前的延迟还没结束),就清除之前的定时器,然后重新设置一个新的定时器,只有当定时器结束后才执行函数。
十、轮询两秒请求一次
1. 使用 setInterval
- 例如,在 JavaScript 中使用
setInterval
函数来实现轮询请求。假设我们有一个fetchData
函数用于发送请求获取数据:
function fetchData() {
// 这里是发送请求的代码,例如使用fetch API
return fetch('https://example.com/api/data')
.then(response => response.json());
}
setInterval(fetchData, 2000);
- 这样就会每隔 2 秒调用一次
fetchData
函数发送请求。不过要注意,在页面关闭时应该清除这个定时器,避免不必要的请求和内存占用。可以使用clearInterval
函数来清除。
2. 考虑使用 WebSockets 或 Server - Sent Events(SSE)作为替代方案
- 在一些场景下,轮询可能不是最优解。WebSockets 可以实现双向通信,服务器可以主动推送数据给客户端。SSE 主要用于服务器向客户端单向推送事件流,它们可以提供更实时的数据更新,并且在性能上可能优于轮询。
十一、第一个请求一直 pending 第二个请求的数据回来,第一个没有回来,数据覆盖怎么处理
1. 为每个请求添加唯一标识
- 可以在发送请求时为每个请求添加一个唯一的标识符,比如一个递增的数字或者一个唯一的字符串(如 UUID)。在处理响应时,根据这个标识符来判断数据应该更新到哪个请求对应的状态。
- 例如,在一个 JavaScript 应用中,假设我们有一个
requests
对象来存储请求相关的信息:
const requests = {};
let requestId = 0;
function sendRequest() {
const id = requestId++;
requests[id] = {status: 'pending'};
// 发送请求,假设使用fetch API
fetch('https://example.com/api/data')
.then(response => response.json())
.then(data => {
// 根据标识符更新对应的请求状态和数据
if (requests[id]) {
requests[id].status = 'fulfilled';
requests[id].data = data;
}
});
return id;
}
- 这样,当数据返回时,就可以根据请求的标识符正确地更新状态和数据,避免覆盖错误的请求。
2. 使用队列来处理请求顺序
- 维护一个请求队列,确保请求按照发送的顺序依次处理。当一个请求的数据返回时,检查队列头部的请求是否是这个数据对应的请求,如果是,则处理数据,然后将队列头部的请求移除;如果不是,则将数据暂存,直到对应的请求到达队列头部。这种方法可以保证数据的顺序和请求的顺序一致。
十二、关于请求的库有没有方案处理
1. Axios 拦截器
- 请求拦截器:
- 可以在请求发送之前进行统一的处理,比如添加请求头、对请求参数进行处理等。例如:
axios.interceptors.request.use((config) => {
// 在请求头中添加认证信息
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
- 响应拦截器:
- 用于在接收到响应后进行处理,如处理错误状态码、对响应数据进行格式化等。例如:
axios.interceptors.response.use((response) => {
// 假设服务器返回的数据格式是{data: {actualData}},提取实际数据
return response.data.data;
}, (error) => {
// 处理错误,比如401错误(未授权)
if (error.response && error.response.status === 401) {
// 跳转到登录页面或者进行重新认证操作
window.location.href = '/login';
}
return Promise.reject(error);
});
2. 并发请求处理
- Axios 可以同时发送多个请求,并且可以使用
axios.all
和axios.spread
来处理并发请求的结果。例如:
axios.all([axios.get('/api/user'), axios.get('/api/posts')])
.then(axios.spread((userResponse, postsResponse) => {
// 同时获取用户信息和帖子信息后的处理
const user = userResponse.data;
const posts = postsResponse.data;
}));
- 这样可以提高效率,特别是当多个请求之间没有依赖关系时。
十三、Vue 数据传递
1. 父子组件数据传递
- 父组件向子组件传递数据:
- 通过
props
属性。在父组件中,在使用子组件标签时,通过属性绑定的方式传递数据。例如,
<template>
<ChildComponent :message="parentMessage" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
parentMessage: 'Hello from parent'
};
}
};
</script>
- 在子组件中,通过
props
选项来接收数据,像这样:
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
props: ['message']
};
</script>
- 子组件向父组件传递数据:
- 通过自定义事件。在子组件中,使用
$emit
方法触发自定义事件,并传递数据。例如:
<template>
<button @click="sendDataToParent">Send Data</button>
</template>
<script>
export default {
methods: {
sendDataToParent() {
const data = 'Data from child';
this.$emit('childData', data);
}
}
};
</script>
- 在父组件中,监听子组件触发的自定义事件来接收数据,如下:
<template>
<ChildComponent @childData="handleChildData" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
methods: {
handleChildData(data) {
console.log(data);
}
}
};
</script>
2. 非父子组件数据传递(兄弟组件等)
- 通过事件总线(Event Bus):
- 创建一个 Vue 实例作为事件总线,通常在一个单独的文件中定义,比如
event-bus.js
:
import Vue from 'vue';
export default new Vue();
- 在发送数据的组件中,引入事件总线并使用
$emit
触发事件来传递数据,例如:
<template>
<button @click="sendData">Send Data</button>
</template>
<script>
import eventBus from './event-bus.js';
export default {
methods: {
sendData() {
const data = 'Data to share';
eventBus.$emit('sharedData', data);
}
}
};
</script>
- 在接收数据的组件中,通过
$on
来监听事件获取数据,比如:
<script>
import eventBus from './event-bus.js';
export default {
mounted() {
eventBus.$on('sharedData', (data) => {
console.log(data);
});
}
};
</script>
- 使用 Vuex(适合更复杂的全局数据管理场景,后面会详细介绍):当多个组件需要共享和操作同一份数据时,可以将数据存储在 Vuex 的状态中,各个组件通过
mapState
等辅助函数来获取数据,通过commit
或dispatch
等方式来修改数据。
十四、介绍 Vuex 和 Pinia 以及区别
1. Vuex
- 概述:Vuex 是 Vue.js 的一个状态管理模式 + 库,用于管理应用中多个组件共享的状态。它采用集中式存储管理应用的所有组件的状态,并以可预测的方式进行状态变更。
- 核心概念:
- State(状态):存储应用的状态数据,类似于组件中的
data
选项,是响应式的,组件可以通过mapState
等辅助函数获取并使用这些状态数据。例如:
const store = new Vuex.Store({
state: {
count: 0
}
});
- Mutations(突变):是唯一能改变状态的途径,它接收一个
state
对象作为第一个参数,通过提交mutation
来同步地修改状态,并且有严格的类型限制,便于调试和追踪状态变化。例如:
mutations: {
increment(state) {
state.count++;
}
}
- Actions(动作):用于处理异步操作,比如发送网络请求等,它可以提交
mutation
来间接改变状态。例如:
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment');
}, 1000);
}
}
- Getters(获取器):类似于计算属性,用于从
state
中派生出一些新的数据,方便组件获取和使用经过处理后的状态数据。例如:
getters: {
doubleCount: state => state.count * 2
}
2. Pinia
- 概述:Pinia 是 Vue.js 的下一代状态管理库,旨在提供一个更简单、更直观且类型友好的方式来管理应用的状态,它对 Vue 3 有更好的适配性,也能在 Vue 2 中使用(通过插件)。
- 核心概念:
- Store(仓库):是 Pinia 的核心概念,通过
defineStore
函数来定义一个仓库,里面可以包含状态、操作状态的方法等。例如:
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++;
}
}
});
- 与 Vuex 不同,Pinia 中没有像
mutation
那样严格区分同步和异步操作的概念,操作状态的方法可以直接在actions
中处理异步逻辑,更加简洁灵活。
3. 区别
- API 简洁性:
- Pinia 的 API 相对更简洁直观,没有像 Vuex 那样复杂的概念区分(如
mutation
的严格同步限制),开发者可以更直接地定义和操作状态。 - 对 Vue 版本的适配:
- Pinia 对 Vue 3 的适配更加自然,利用了 Vue 3 的新特性如
Composition API
等,而 Vuex 虽然也能用于 Vue 3,但 Pinia 在这方面的集成感觉更流畅。同时,Pinia 也能通过插件支持 Vue 2,具有更好的跨版本使用能力。 - 类型支持:
- Pinia 在类型定义方面更加友好,能更好地结合 TypeScript 等进行类型检查,有助于提高代码的可维护性和健壮性,Vuex 在这方面相对弱一些,虽然也可以进行一定的类型定义,但不如 Pinia 方便。
十五、介绍 MVVM
1. 概念
- MVVM(Model-View-ViewModel)是一种软件架构模式,主要用于构建用户界面,它分离了数据(Model)、用户界面(View)以及连接数据和界面的中间层(ViewModel),使得它们各自的职责更加清晰,便于代码的维护和扩展。
2. 各部分职责
- Model(模型):
- 代表应用的数据和业务逻辑,通常是与后端交互获取的数据或者本地存储的数据等,比如用户信息、商品数据等,它与视图层是独立的,不关心数据如何展示。
- View(视图):
- 就是用户看到和交互的界面,比如网页中的 HTML 页面、移动端应用的界面等,它负责展示数据以及接收用户的操作输入,但不直接处理业务逻辑和数据的存储、变更等。
- ViewModel(视图模型):
- 是连接 Model 和 View 的桥梁,它一方面从 Model 获取数据,并进行必要的格式转换、处理等,使其适合在 View 中展示;另一方面,它监听 View 中用户的操作事件,并将其转化为对 Model 的操作,从而更新数据。例如,在一个基于 MVVM 的前端框架中,ViewModel 可以通过数据绑定机制,自动将 Model 中的数据更新反映到 View 上,同时将 View 上用户输入的数据变化更新到 Model 中。
3. 在 Vue 中的体现
- 在 Vue.js 中,
data
选项可以看作是 Model 的一部分,存储着组件的数据;模板(template
)就是 View,负责展示数据;而 Vue 实例本身以及其内部的计算属性、方法等就相当于 ViewModel,它通过双向绑定等机制,实现了数据和视图的自动关联和更新。例如,当在data
中修改一个属性的值时,与之绑定的模板中的内容会自动更新,反之,用户在视图中输入等操作导致的数据变化,也会同步更新到data
中的对应属性上。
十六、Vue2 双向绑定原理
1. Object.defineProperty () 方法
- Vue2 实现双向绑定的核心是通过
Object.defineProperty()
方法来劫持数据的读写操作。在 Vue 实例初始化时,会对data
选项中的数据进行遍历,使用Object.defineProperty()
为每个属性添加get
和set
访问器属性。例如:
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
return value;
},
set: function(newValue) {
if (newValue!== value) {
value = newValue;
// 当数据变化时,通知相关的依赖(如模板中的绑定表达式等)进行更新
}
}
});
}
- 这样,当读取数据时,会触发
get
函数,当修改数据时,会触发set
函数,在set
函数中可以进行更新视图等相关操作。
2. 依赖收集与发布 - 订阅模式
- Vue2 通过一个依赖收集机制来知道哪些地方使用了某个数据,也就是哪些模板、计算属性等依赖了这个数据。在
get
函数被触发时,会收集依赖(通常是一些更新函数),当数据发生变化触发set
函数时,会遍历这些依赖并执行它们,实现视图的更新。这其实就是一种发布 - 订阅模式,数据变化是发布者,依赖的更新函数是订阅者,当有新的数据变化消息(发布)时,对应的订阅者(依赖的更新函数)就会被通知并执行更新操作。 - 例如,在一个组件的模板中有
{{ message }}
这样的绑定表达式,当解析模板时,读取message
数据就会触发其get
函数进行依赖收集,后续如果message
的值改变,就会通过之前收集的依赖来更新模板中对应的部分。
十七、Vue2 响应式原理
1. 响应式数据的创建
- 同上面双向绑定原理中的数据劫持部分,通过
Object.defineProperty()
对data
等选项中的数据进行处理,使其变为响应式数据,也就是让数据的读写能够被 Vue 感知并进行相应的处理。例如,在 Vue 组件的初始化阶段:
function initData(vm) {
let data = vm.$options.data;
data = vm._data = typeof data === 'function'? data.call(vm) : data;
// 遍历_data中的属性,使其成为响应式
Object.keys(data).forEach(key => {
defineReactive(vm._data, key, data[key]);
});
}
- 这样创建出来的数据就是响应式的,其变化可以触发视图的更新等操作。
2. 组件更新机制
- 当响应式数据发生变化时,会触发
set
函数,在set
函数中会通过之前收集的依赖(如Watcher
实例,它代表了对某个数据的依赖,通常由模板解析、计算属性等创建)来通知相关的更新操作。比如,一个组件的data
中的属性变化了,会找到所有依赖这个属性的Watcher
,然后执行Watcher
的更新函数,这个更新函数可能会触发虚拟 DOM 的重新渲染等操作,最终将更新后的内容反映到真实的 DOM 上,实现组件的更新。 - 同时,Vue2 还会进行一些优化,比如异步更新队列等,避免频繁地进行 DOM 操作,将多次数据变化的更新合并为一次,提高性能。
十八、设计模式加实际例子
1. 单例模式
- 概念:单例模式确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。
- 实际例子:在前端应用中,比如创建一个全局的日志记录器,只需要一个实例来记录各种日志信息。代码实现可以如下:
class Logger {
constructor() {
if (!Logger.instance) {
Logger.instance = this;
}
return Logger.instance;
}
log(message) {
console.log(message);
}
}
const logger1 = new Logger();
const logger2 = new Logger();
console.log(logger1 === logger2); // true,说明获取到的是同一个实例
- 这里,无论在应用的哪个地方通过
new Logger()
创建实例,得到的都是同一个Logger
实例,方便统一管理日志记录。
2. 工厂模式
- 概念:工厂模式是一种创建对象的设计模式,它将对象的创建和使用分离,通过一个工厂函数或者工厂类来创建对象,根据不同的条件可以返回不同类型的对象。
- 实际例子:假设要创建不同类型的图形对象(圆形、矩形等),可以使用工厂模式。例如:
function createShape(type, options) {
switch (type) {
case 'circle':
return new Circle(options.radius);
case'rectangle':
return new Rectangle(options.width, options.height);
default:
throw new Error('Invalid shape type');
}
}
class Circle {
constructor(radius) {
this.radius = radius;
}
}
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
}
const circle = createShape('circle', { radius: 5 });
const rectangle = createShape('rectangle', { width: 10, height: 20 });
- 通过
createShape
这个工厂函数,根据传入的类型参数来创建不同的图形对象,使用者不需要知道具体的对象创建细节。
3. 观察者模式
- 概念:观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当主题对象的状态发生变化时,会通知所有的观察者对象,使它们能够自动更新。
- 实际例子:在一个新闻订阅系统中,新闻网站是主题,订阅用户是观察者。代码示例如下:
class NewsPublisher {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(news) {
this.observers.forEach(observer => observer.update(news));
}
}
class NewsSubscriber {
update(news) {
console.log(`Received news: ${news}`);
}
}
const publisher = new NewsPublisher();
const subscriber1 = new NewsSubscriber();
const subscriber2 = new NewsSubscriber();
publisher.subscribe(subscriber1);
publisher.subscribe(subscriber2);
publisher.notify('Breaking news!');
- 新闻发布者(
NewsPublisher
)维护着订阅者列表,当有新新闻时,通过notify
函数通知所有订阅者(NewsSubscriber
),订阅者接收到通知后执行update
函数进行相应的处理。
十九、平时开发使用的 git 操作
1. 基本操作
- 克隆仓库(clone):
- 使用
git clone <repository-url>
命令从远程仓库(如 GitHub、GitLab 等)克隆代码到本地。例如,要克隆一个名为my-project
的仓库,命令如下:
git clone https://github.com/username/my-project.git
- 添加文件(add):
- 使用
git add <file(s)>
命令将文件添加到暂存区,准备提交。可以添加单个文件,如git add index.html
,也可以添加所有修改的文件,用git add.
表示添加当前目录下所有修改的文件。 - 提交更改(commit):
- 通过
git commit -m "<commit-message>"
命令将暂存区的文件提交到本地仓库,<commit-message>
是对本次提交内容的简要描述,例如:
git commit -m "Fix a bug in the login page"
- 推送更改(push):
- 使用
git push origin <branch-name>
命令将本地仓库的更改推送到远程仓库对应的分支上,通常origin
是远程仓库的默认别名,<branch-name>
是分支名称,比如:
git push origin master
2. 分支管理操作
- 创建分支(branch):
- 用
git branch <new-branch-name>
命令创建一个新的分支,例如:
git branch feature-branch
- 切换分支(checkout):
- 通过
git checkout <branch-name>
命令切换到指定的分支,如:
git checkout feature-branch
- 也可以使用
git checkout -b <new-branch-name>
命令同时创建并切换到新分支。 - 合并分支(merge):
- 假设要把
feature-branch
分支合并到master
分支,首先切换到master
分支(git checkout master
),然后使用git merge feature-branch
命令进行合并。
3. 查看操作
- 查看状态(status):
- 使用
git status
- 查看状态(status):
使用git status
命令可以查看当前工作目录的状态,比如哪些文件被修改了、哪些文件是新增的、当前处于哪个分支等信息。例如:
git status
执行后会输出类似如下内容:
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: index.html
no changes added to commit (use "git add" and/or "git commit -a")
这表明当前在master
分支,index.html
文件被修改了但还没添加到暂存区。
- 查看日志(log):
通过git log
命令可以查看提交历史记录,它会显示每次提交的作者、时间、提交信息以及对应的哈希值等内容,方便追踪项目的变更情况。例如:
git log
输出类似如下:
commit 123456789abcdef (HEAD -> master)
Author: Your Name <your@email.com>
Date: Mon Dec 16 10:00:00 2024
Add new feature to the project
commit 987654321fedcba
Author: Another Author <another@email.com>
Date: Fri Dec 13 14:30:00 2024
Fix a bug in the login page
可以添加一些参数来更方便地查看日志,比如--oneline
可以让每条提交记录显示在一行,更简洁直观:
git log --oneline
- 查看文件差异(diff):
git diff
命令用于查看文件修改前后的差异内容,能帮助了解具体做了哪些代码改动。如果想查看工作目录中已修改但未暂存文件与暂存区版本的差异,可以直接执行:
git diff
若想查看已暂存文件与上次提交版本的差异,使用:
git diff --cached
二十、面向对象和面向过程
1. 面向过程
- 概念:面向过程编程是一种以过程(或函数)为中心的编程思想,它将解决问题的步骤分解为一个个具体的操作,按照顺序依次执行这些操作来达成目标。程序的执行流程是线性的,重点关注的是 “怎么做”,数据和对数据的操作相对分离程度较低。
示例
:比如计算一个班级学生成绩的平均分。可以通过以下步骤实现:
- 定义一个数组来存储所有学生的成绩。
- 通过循环依次读取每个学生的成绩并累加到一个变量中。
- 用总成绩除以学生人数得到平均分。
代码示例如下:
scores = [80, 90, 75, 85, 95] # 假设的学生成绩数组
total_score = 0
for score in scores:
total_score += score
average_score = total_score / len(scores)
print(average_score)
这里就是按照顺序依次执行各个操作来完成计算平均分这个任务,整个过程注重的是操作步骤的顺序和执行。
2. 面向对象
- 概念:面向对象编程(OOP)将现实世界中的事物抽象成对象,对象包含属性(数据)和方法(对数据的操作),通过对象之间的交互来解决问题。它强调的是 “谁来做” 以及对象之间的关系,具有封装、继承、多态等特性。
- 示例:同样计算班级学生成绩平均分的例子,用面向对象的思路可以这样设计:
首先定义一个Student
类:
class Student:
def __init__(self, score):
self.score = score
def get_score(self):
return self.score
然后创建学生对象的列表,计算平均分:
students = [Student(80), Student(90), Student(75), Student(85), Student(95)]
total_score = 0
for student in students:
total_score += student.get_score()
average_score = total_score / len(students)
print(average_score)
这里把学生抽象成了Student
类,每个学生对象有自己的成绩属性以及获取成绩的方法,通过对象的交互(调用get_score
方法获取成绩并累加)来完成计算平均分的任务,并且可以方便地对Student
类进行扩展,比如添加更多关于学生的属性和方法等。
3. 两者区别与适用场景
区别
:
- 数据与操作的关联:面向过程中数据和操作相对松散结合,而面向对象将数据和操作封装在对象内部,关联性更强。
- 代码组织方式:面向过程按照解决问题的步骤顺序组织代码,面向对象围绕对象及其关系来组织代码。
- 可维护性和扩展性:面向对象由于有封装、继承、多态等特性,在面对复杂系统和需求变更时往往更容易维护和扩展,而面向过程在简单、流程固定的场景下更直观、易理解。
适用场景
:
- 面向过程:适合简单、线性、需求不太会变化的小型程序,比如一些简单的脚本任务,像数据文件的格式转换等,按照固定的步骤操作即可完成任务。
- 面向对象:更适用于大型、复杂、需要频繁扩展和维护的项目,比如企业级的软件系统开发,通过将不同的业务实体抽象成对象,利用对象之间的关系和特性来构建整个系统,方便应对不断变化的业务需求。
二十一、描述一下登录页(我说的微信小程序登录)
1. 界面布局
- 顶部:通常会有小程序的标题栏,显示小程序的名称或者相关标识,有的还会有返回按钮方便用户返回上一级页面(如果存在上一级页面的话)。
主体部分
:
- 账号输入框:一般会有一个清晰的文本框提示用户输入账号,账号可以是手机号码、邮箱或者自定义的用户名等,文本框旁边可能会有相应的提示文字,比如 “请输入手机号码”,并且有的还会设置输入格式的限制,例如只允许输入数字(针对手机号情况),通过合适的占位符来引导用户输入正确格式的内容。
- 密码输入框:和账号输入框类似,用于用户输入登录密码,为了安全起见,输入内容通常会以加密的小圆点或者星号形式显示,同时也会有相应的提示文字,如 “请输入密码”,有的还支持密码可见切换功能,方便用户确认密码是否输入正确。
- 登录按钮:是页面比较突出的元素,一般会有醒目的颜色(比如常用的主题色)和较大的点击区域,按钮上会明确写有 “登录” 字样,引导用户进行登录操作。
- 第三方登录区域(可选):如果小程序支持第三方登录,比如微信登录、QQ 登录等,会在此区域展示相应的第三方登录图标,用户点击对应的图标即可快速使用第三方账号授权登录,图标通常设计得比较简洁直观,易于识别,例如微信登录就是微信的官方图标。
- 忘记密码 / 找回密码链接:为了方便用户在忘记密码的情况下重置密码,会设置这样一个链接,文字描述清晰明了,点击后可跳转到相应的密码找回页面,引导用户通过验证手机号、邮箱等方式重置密码。
- 底部(可选):可能会有一些版权信息、小程序相关的服务条款链接或者其他一些辅助性说明文字,不过这部分相对简洁,不会过于突出影响主体登录功能。
2. 功能逻辑
- 账号密码验证:当用户输入账号和密码后点击登录按钮,小程序会获取用户输入的内容,首先会对输入的格式进行基本验证,比如手机号是否符合格式要求、密码长度是否在规定范围内等。然后将账号和密码信息发送到后端服务器进行验证,服务器会根据存储的用户数据来判断账号密码是否匹配,如果匹配则登录成功,返回相应的登录凭证(如 token 等),小程序端接收到登录凭证后进行存储(一般存储在本地缓存中),用于后续的接口请求等操作来标识用户身份;如果不匹配,则会在小程序端弹出相应的提示框告知用户账号或密码错误,请重新输入。
- 第三方登录逻辑:若用户选择第三方登录方式,比如微信登录,小程序会调用微信的登录授权接口,用户确认授权后,微信会返回一个唯一标识(如 openid 等)给小程序,小程序将这个标识发送到自己的后端服务器,服务器根据这个标识来判断该用户是否是新用户,如果是新用户则进行注册流程(一般会自动为用户创建一个账号并关联相关信息),如果是已注册用户则直接登录成功,同样返回登录凭证供小程序存储和后续使用。
- 记住密码功能(可选):有的登录页还会提供记住密码的功能,当用户勾选此功能并登录成功后,小程序会将账号和密码信息进行加密存储在本地,下次用户打开登录页面时,会自动填充之前记住的账号和密码,方便用户快速登录,但需要注意保障存储信息的安全性,防止信息泄露。
- 密码找回逻辑:当用户点击忘记密码 / 找回密码链接,会跳转到相应的密码找回页面,一般会要求用户输入注册时使用的手机号或者邮箱等信息,然后发送验证码到对应的手机号或邮箱,用户输入正确的验证码后,可设置新的密码,新密码设置成功后,下次登录就可以使用新密码进行登录了。
二十二、重复登录怎么解决
1. 前端层面
- 按钮状态控制:在登录按钮上添加点击事件处理,当用户第一次点击登录按钮后,立即将按钮设置为不可点击状态(比如改变按钮的样式为灰色、禁用点击事件等),并显示一个加载中的提示(如旋转的小图标),这样可以避免用户在登录请求还未处理完的情况下多次点击登录按钮造成重复登录请求发送。等后端返回登录结果(成功或者失败)后,再恢复按钮的可点击状态以及隐藏加载提示。例如,在微信小程序中可以这样做:
<button bindtap="login" disabled="{{isLoggingIn}}" class="{{isLoggingIn?'disabled-button' : 'normal-button'}}">登录</button>
<view wx:if="{{isLoggingIn}}">
<image src="/images/loading.gif" style="width:20px;height:20px"></image>
</view>
在对应的 JavaScript 逻辑中:
Page({
data: {
isLoggingIn: false
},
login() {
this.setData({
isLoggingIn: true
});
// 这里发送登录请求,假设使用wx.request
wx.request({
url: 'https://example.com/login',
success: (res) => {
// 登录成功处理
this.setData({
isLoggingIn: false
});
},
fail: (res) => {
// 登录失败处理
this.setData({
isLoggingIn: false
});
}
});
}
});
- 路由控制:当用户已经处于登录成功后的页面或者正在登录流程中(比如已经跳转到登录页面且登录请求正在处理),如果用户再次尝试通过某些入口(如点击某个引导登录的按钮等)进入登录页面,可以在路由跳转逻辑中进行判断,阻止重复跳转到登录页面。例如,在小程序的页面跳转拦截中:
wx.onAppRoute((res) => {
const route = res.path;
if (route === '/pages/login/login' && isLoggedIn) { // isLoggedIn表示是否已登录,可根据存储的登录凭证判断
wx.redirectTo({
url: '/pages/home/home' // 已登录就重定向到首页等已登录页面
});
}
});
2. 后端层面
- 请求去重处理:后端服务器接收到登录请求时,可以根据一些唯一标识来判断是否已经有相同的登录请求正在处理。比如可以基于用户的 IP 地址、设备 ID(如果小程序能获取到相关信息并传递给后端)或者为每个登录请求生成一个唯一的请求 ID(由小程序端在请求时一并带上)等,当收到新的登录请求时,先检查是否存在正在处理且标识相同的请求,如果有则忽略这个新请求,等待之前的请求处理完成并返回结果,这样可以避免因网络延迟等原因导致用户多次点击登录按钮而产生的重复请求同时到达后端进行处理。
- 登录状态校验:服务器要维护用户的登录状态,当用户成功登录后,会在服务器端记录该用户已登录(比如通过在数据库中更新登录状态字段或者在缓存中存储登录相关信息等方式),后续再接收到该用户的登录请求时,先检查其登录状态,如果已经处于登录状态,则直接返回相应的提示信息告知用户已经登录,无需再次登录,避免重复登录操作带来的额外资源消耗和可能的逻辑混乱。