文章目录
- 一面(85 min)
- 自我介绍
- 常用数据结构及其特点
- JS事件循环
- JS浅拷贝和深拷贝
- http1.0、http1.1、http2.0比较
- http和https比较,https加密方法
- react hooks使用过哪些及其特点?
- react router原理,
- hash模式和browser模式比较
- react渲染流程
- 设计模式介绍(工厂模式、观察者模式)
- 代码阅读题:事件循环(async、await、setTimeout、Promise().then()),打印到控制台的结果是什么?
- 手写深拷贝
- 算法题:树的最大深度
- 二面(65 min)
- 自我介绍
- 为什么选择前端?
- 怎样学习前端的?
- 进程和线程
- 进程的调度策略
- 线程的同步和线程间通讯
- 比较const、let和var
- 快速排序
- 代码阅读题:const定义引用类型,判断哪几行报错?哪几行正常运行?
- 用setTimeout实现setInterval
- 算法题:买卖股票最佳时机 Ⅱ(力扣122)
- 三面(35 min)
- 自我介绍
- 为什么选择前端?
- 怎样学习前端?
- 是否重构过代码?怎样重构的?
- 了解什么新的前端的技术?
- 虚拟DOM
- react17的启发式更新算法
- 了解iframe吗?
- HR面(20 min)
- 自我介绍
- 为什么选择前端?
- 怎么看待加班?
- 对抖音电商了解多少?
- 职业规划
一面(85 min)
自我介绍
当在前端面试中进行自我介绍时,你可以按照以下结构进行组织:
- 介绍自己:
开始时,简要介绍自己的姓名和背景。你可以提及自己的学历、专业背景以及工作经验。
- 技术专长:
提及你在前端领域的技术专长和经验。列举你熟悉的编程语言、前端框架、库和工具等。可以着重强调你在哪些方面特别擅长,例如前端开发、响应式设计、用户界面设计、性能优化等。
- 项目经验:
介绍你在过去的项目中所承担的角色和贡献。可以选择其中几个有代表性的项目来详细介绍,包括项目的规模、技术栈、解决的问题以及你在项目中的具体工作。
- 学习与成长:
提及你的学习态度和成长经历。你可以分享你如何持续学习新技术、参与开发社区、阅读技术文章和书籍等。强调你对于前端领域的持续关注和追求,以及你在学习过程中遇到的挑战和如何克服它们。
- 团队合作与沟通能力:
强调你在团队合作中的角色和能力。你可以提及你在过去的团队合作中的贡献,以及你如何与其他团队成员协作、解决问题和有效沟通的能力。
- 总结与展望:
总结你的自我介绍,并表达你对前端领域的热情和对未来的展望。你可以提及你对前端技术的热爱和对新技术的追求,以及你希望在未来的工作中继续学习和成长。
自我介绍需要简明扼要地表达你的关键信息,同时展现你的技术实力、学习能力和团队合作能力。在介绍过程中,注意保持清晰的表达、自信的姿态和积极的态度。记得准备并练习自我介绍,以确保你能在面试中自信地展现自己。
常用数据结构及其特点
在前端开发中,常用的数据结构有以下几种:
1.数组(Array):
动态长度:数组的长度是可变的,可以根据需要添加或删除元素。
弱类型:数组可以容纳不同类型的值,例如数字、字符串、布尔值等。
有序集合:数组中的元素按照它们在数组中的位置有序排列。
零基索引:数组的索引从0开始,表示数组中的第一个元素。
下面是一些JavaScript数组的代码示例:
1.声明和初始化一个数组:
let fruits = ['apple', 'banana', 'orange'];
2.访问数组中的元素:
console.log(fruits[0]); // 输出:'apple'
console.log(fruits[2]); // 输出:'orange'
3.修改数组中的元素:
fruits[1] = 'grape';
console.log(fruits); // 输出:['apple', 'grape', 'orange']
4.添加元素到数组末尾:
fruits.push('watermelon');
console.log(fruits); // 输出:['apple', 'grape', 'orange', 'watermelon']
5.获取数组的长度:
console.log(fruits.length); // 输出:4
6.迭代数组中的元素:
for (let i = 0; i < fruits.length; i++) {
console.log(fruits[i]);
}
2.对象(Object):
JavaScript中的对象是一种复合数据类型,用于存储键值对的集合。以下是JavaScript对象的一些特点:
-
键值对:对象由一组键值对组成,其中每个键都是唯一的,并且与对应的值相关联。
-
无序集合:对象中的键值对是无序的,不像数组那样按照索引有序排列。
-
引用类型:对象是引用类型,可以通过引用进行传递和复制。
-
动态性:可以随时添加、修改或删除对象的属性。
下面是一些JavaScript对象的代码示例:
1.声明和初始化一个对象:
let person = {
name: 'John',
age: 30,
city: 'New York'
};
2.访问对象的属性:
console.log(person.name); // 输出:'John'
console.log(person['age']); // 输出:30
3.修改对象的属性:
person.age = 35;
console.log(person.age); // 输出:35
4.添加新属性到对象:
person.job = 'Engineer';
console.log(person); // 输出:{ name: 'John', age: 35, city: 'New York', job: 'Engineer' }
5.删除对象的属性:
delete person.city;
console.log(person); // 输出:{ name: 'John', age: 35, job: 'Engineer' }
6.迭代对象的属性:
for (let key in person) {
console.log(key + ': ' + person[key]);
}
3.链表(Linked List):
- 特点:由节点组成的数据结构,每个节点包含数据和指向下一个节点的指针。可以动态添加和删除元素,但访问元素需要遍历整个链表。
class ListNode {
constructor(val) {
this.val = val;
this.next = null;
}
}
const node1 = new ListNode(1);
const node2 = new ListNode(2);
node1.next = node2;
4.栈(Stack):
- 特点:具有后进先出(LIFO)的特性,只能在栈顶进行插入和删除操作。适用于需要维护临时数据的场景,如函数调用栈、表单验证等。
const stack = [];
stack.push(1);
stack.push(2);
const topElement = stack.pop();
5.队列(Queue):
- 特点:具有先进先出(FIFO)的特性,只能在队尾插入元素,在队头删除元素。适用于需要顺序处理数据的场景,如消息队列、任务调度等。
const queue = [];
queue.push(1);
queue.push(2);
const frontElement = queue.shift();
6.树(Tree):
- 特点:由节点和边组成的层次结构,每个节点可以有多个子节点。树在前端开发中常用于构建 DOM 结构、树形菜单等。
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
7.图(Graph):
- 特点:由节点和边组成的非线性数据结构,节点之间可以存在多种关系。图在前端开发中用于表示复杂的网络结构、数据关系等。
class Graph {
constructor() {
this.vertices = [];
this.edges = [];
}
}
const graph = new Graph();
graph.vertices = ['A', 'B', 'C', 'D'];
graph.edges = [['A', 'B'], ['B', 'C'], ['C', 'D']];
8.集合(Set):
-
唯一性:Set中的值是唯一的,不会出现重复的元素。
-
无序性:Set中的元素是无序的,不像数组或对象那样有固定的顺序。
-
迭代性:可以通过迭代器遍历Set中的元素。
-
值相等性:Set使用"值相等性"进行元素的比较,而不是"引用相等性"。
下面是一些JavaScript Set的代码示例:
1.创建一个Set并添加元素:
let set = new Set();
set.add(1);
set.add(2);
set.add(3);
set.add(2); // 重复元素,不会被添加进Set
console.log(set); // 输出:Set { 1, 2, 3 }
2.检查Set中是否包含某个元素:
console.log(set.has(2)); // 输出:true
console.log(set.has(4)); // 输出:false
3.获取Set的大小(元素数量):
console.log(set.size); // 输出:3
4.从Set中删除元素:
set.delete(2);
console.log(set); // 输出:Set { 1, 3 }
5.使用迭代器遍历Set中的元素:
for (let item of set) {
console.log(item);
}
6.将Set转换为数组:
let array = Array.from(set);
console.log(array); // 输出:[1, 3]
9.map结构
JavaScript中的Map是一种键值对的数据结构,它具有以下特点:
-
键值对存储:Map中的数据以键值对的形式存储,其中每个键都是唯一的,并且与对应的值相关联。
-
任意类型键:Map中的键可以是任意类型的值,包括基本类型和引用类型。
-
有序性:Map中的键值对按照插入顺序保持有序。
-
动态性:可以随时添加、修改或删除Map中的键值对。
-
迭代性:可以通过迭代器遍历Map中的键值对。
-
值相等性:Map使用"值相等性"进行键的比较,而不是"引用相等性"。
下面是一些JavaScript Map的代码示例:
1.创建一个Map并添加键值对:
let map = new Map();
let key1 = 'name';
let value1 = 'John';
let key2 = { id: 1 };
let value2 = 'Apple';
map.set(key1, value1);
map.set(key2, value2);
console.log(map); // 输出:Map { 'name' => 'John', { id: 1 } => 'Apple' }
2.检查Map中是否包含某个键:
console.log(map.has(key1)); // 输出:true
console.log(map.has('age')); // 输出:false
3.获取Map中的值:
console.log(map.get(key1)); // 输出:'John'
console.log(map.get(key2)); // 输出:'Apple'
4.删除Map中的键值对:
map.delete(key1);
console.log(map); // 输出:Map { { id: 1 } => 'Apple' }
5.获取Map的大小(键值对数量):
console.log(map.size); // 输出:1
6.使用迭代器遍历Map中的键值对:
for (let [key, value] of map) {
console.log(key + ' => ' + value);
}
JS事件循环
JavaScript中的事件循环(Event Loop)是一种机制,用于处理异步事件和任务。它使得JavaScript能够在单线程环境中处理并发的任务,同时保持响应性。
事件循环的执行过程如下:
1.执行同步任务(Synchronous Tasks):
JavaScript引擎首先执行当前执行上下文中的所有同步任务,这些任务按照它们在代码中的顺序依次执行。
2.执行微任务队列(Microtask Queue):
在同步任务执行完毕后,JavaScript引擎会检查并执行微任务队列中的所有微任务。微任务包括Promise回调、MutationObserver回调等。
3.渲染重绘(Rendering and Repainting):
在执行完微任务后,如果需要更新页面的呈现,浏览器会执行渲染和重绘操作,以确保页面的可视部分与最新的DOM状态匹配。
4.执行宏任务队列(Macrotask Queue):
在渲染重绘之后,JavaScript引擎会检查并执行宏任务队列中的一个任务。宏任务包括定时器回调、事件回调(如鼠标点击、键盘事件等)、网络请求回调等。
5.返回第2步:
一旦执行完一个宏任务,JavaScript引擎会回到第2步,继续执行微任务队列中的微任务,然后再进行渲染和重绘,然后执行下一个宏任务。这个过程不断循环,形成事件循环。
下面是一个简单的JavaScript代码示例,演示了事件循环的执行过程:
console.log('Start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
输出结果为:
Start
End
Promise
setTimeout
在这个示例中,首先输出"Start",然后设置一个定时器任务(宏任务)。接着,Promise的微任务被加入到微任务队列中,然后输出"End"。在事件循环的下一轮中,先执行微任务队列中的Promise回调,输出"Promise",然后执行定时器回调,输出"setTimeout"。
JS浅拷贝和深拷贝
简介
在JavaScript中,深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是用于复制对象或数组的两种不同方式。
浅拷贝(Shallow Copy):
浅拷贝是指创建一个新的对象或数组,然后将原始对象或数组的引用复制到新对象或数组中。这意味着新对象或数组与原始对象或数组共享相同的内部数据,修改其中一个会影响到另一个。浅拷贝通常只复制第一层的数据,而不会递归复制嵌套的对象或数组。
以下是几种常见的浅拷贝实现方式:
- 对象的浅拷贝可以使用Object.assign()方法或展开运算符{…obj}。
- 数组的浅拷贝可以使用Array.prototype.slice()方法或展开运算符[…arr]。
深拷贝(Deep Copy):
深拷贝是指创建一个新的对象或数组,并递归地复制原始对象或数组的所有嵌套对象和数组。深拷贝会生成一个完全独立的副本,修改副本不会影响到原始对象或数组。
以下是几种常见的深拷贝实现方式:
-
使用递归和循环遍历原始对象或数组的每个属性或元素,然后创建一个相应的副本。可以使用循环、typeof、Array.isArray()等条件判断来处理不同类型的属性或元素。
-
使用JSON.parse(JSON.stringify(obj))将对象序列化为JSON字符串,然后再将其解析回对象。这种方法对于大多数原始类型和可序列化的对象都有效,但是会忽略函数、正则表达式、循环引用等。
需要注意的是,深拷贝可能会涉及到循环引用的问题,即对象或数组中存在相互引用的情况,需要额外的处理来避免无限循环的复制过程。
在选择拷贝方式时,根据需求选择合适的方式。浅拷贝适用于简单的数据结构,且在内存占用和性能方面更高效。而深拷贝适用于需要创建独立副本,且要求修改副本不影响原始数据的情况。
深拷贝代码示例
当涉及到深拷贝对象和数组时,可以使用递归和循环遍历的方法来实现。下面是一个使用递归和循环遍历的示例,演示如何进行深拷贝:
function deepCopy(obj) {
// 检查是否为基本类型
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 创建新的对象或数组
const copy = Array.isArray(obj) ? [] : {};
// 遍历原始对象或数组的属性或元素
for (let key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
// 递归地深拷贝每个属性或元素
copy[key] = deepCopy(obj[key]);
}
}
return copy;
}
// 示例
const obj = {
name: 'John',
age: 30,
address: {
city: 'New York',
country: 'USA'
},
hobbies: ['reading', 'coding']
};
const copyObj = deepCopy(obj);
console.log(copyObj);
console.log(copyObj === obj); // 输出: false
console.log(copyObj.address === obj.address); // 输出: false
console.log(copyObj.hobbies === obj.hobbies); // 输出: false
在这个示例中,deepCopy函数接受一个对象或数组作为参数,如果参数是基本类型,则直接返回参数本身。如果参数是对象或数组,则创建一个新的空对象或空数组,并递归地深拷贝每个属性或元素,将其复制到新对象或数组中。通过递归调用deepCopy函数,可以处理嵌套的对象和数组,从而实现深拷贝。
需要注意的是,这个示例只涵盖了一般的情况,对于特殊情况如循环引用、函数、正则表达式等需要额外的处理。此外,深拷贝可能会对性能产生一定的影响,特别是在处理大型对象或数组时,请根据实际情况选择适合的拷贝方式。
数组的展开运算 ,是浅拷贝吗
数组的展开运算符(Spread Operator)在 JavaScript 中进行浅拷贝。
展开运算符用于展开数组或类数组对象中的元素,将它们扩展为单独的参数或数组字面量的元素。它可以用于创建一个新的数组,并将原始数组的元素复制到新数组中。但是,展开运算符只会复制数组的第一层元素,对于嵌套的对象或数组,只会复制它们的引用。
下面是一个使用展开运算符进行浅拷贝的示例:
const arr1 = [1, 2, 3];
const arr2 = [...arr1];
console.log(arr2); // 输出: [1, 2, 3]
console.log(arr2 === arr1); // 输出: false
在这个示例中,[…arr1]使用展开运算符将 arr1 中的元素展开为独立的元素,并创建了一个新的数组 arr2。arr2 是通过复制 arr1 中的元素而创建的,两者是两个独立的数组,修改其中一个数组不会影响另一个数组。这是浅拷贝的行为。
然而,如果原始数组中的元素是对象或数组,则展开运算符只会复制它们的引用,而不是进行深层次的复制。这意味着对于嵌套的对象或数组,修改其中一个元素会影响到另一个数组。
const arr1 = [{ id: 1 }, { id: 2 }];
const arr2 = [...arr1];
arr2[0].id = 3;
console.log(arr1); // 输出: [{ id: 3 }, { id: 2 }]
console.log(arr2); // 输出: [{ id: 3 }, { id: 2 }]
在这个示例中,arr1 是一个包含两个对象的数组,通过展开运算符进行浅拷贝得到了 arr2。然后,修改了 arr2[0] 对象的 id 属性,但同时也影响到了 arr1 中的相应对象。
因此,展开运算符只提供了浅拷贝的能力,对于需要进行深拷贝的情况,需要使用其他方法,如递归复制或使用深拷贝函数。
其他深拷贝示例
1. JSON.parse(JSON.stringify(obj)):
这是一种常用的实现深拷贝的方法。它使用 JSON.stringify() 将对象转换为 JSON 字符串,然后再使用 JSON.parse() 将字符串解析为一个新的对象。这种方法可以处理大多数原始类型和可序列化的对象,但它会忽略函数、正则表达式、循环引用等。
const obj = { name: 'John', age: 30 };
const copy = JSON.parse(JSON.stringify(obj));
2.lodash.cloneDeep():
Lodash 是一个流行的 JavaScript 工具库,提供了许多实用的函数,其中 cloneDeep() 函数用于实现深拷贝。它递归地复制对象和数组的所有嵌套对象和数组,并返回一个全新的副本。
const _ = require('lodash');
const obj = { name: 'John', age: 30 };
const copy = _.cloneDeep(obj);
3.rfdc 库:
rfdc(Really Fast Deep Clone)是一个专注于高性能的深拷贝库。它提供了一个简单而快速的 clone() 函数,能够以高效的方式深拷贝对象和数组。
const clone = require('rfdc')();
const obj = { name: 'John', age: 30 };
const copy = clone(obj);
http1.0、http1.1、http2.0比较
HTTP(Hypertext Transfer Protocol)是一种用于传输超文本的应用层协议。HTTP 1.0、HTTP 1.1 和 HTTP 2.0 是 HTTP 协议的不同版本,它们在性能、功能和协议特性上有一些区别。
1. HTTP 1.0:
- 顺序传输:HTTP 1.0 是基于请求/响应模型的协议,每个请求需要等待前一个请求的响应返回才能进行下一次请求。
- 短连接:每个请求和响应都使用独立的连接,完成后立即关闭连接。这意味着每个请求都需要建立新的 TCP 连接,带来较大的开销。
- 无状态:HTTP 1.0 默认是无状态的,每个请求之间相互独立,服务器不会保留客户端请求的任何状态信息。
2. HTTP 1.1:
- 持久连接:HTTP 1.1 引入了持久连接,允许多个请求和响应复用同一个连接。这减少了建立和关闭连接的开销,提高了性能。
- 流水线化:HTTP 1.1 支持请求和响应的流水线化,允许在一个连接上同时发送多个请求,提高了请求的并发性。
- 虚拟主机:HTTP 1.1 支持虚拟主机,允许多个域名共享同一个 IP 地址,提高了服务器资源的利用率。
- 缓存机制:HTTP 1.1 引入了更强大的缓存机制,包括强缓存和协商缓存,减少了网络传输和服务器负载。
3. HTTP 2.0:
- 多路复用:HTTP 2.0 使用二进制分帧层,支持在同一个连接上同时发送多个请求和响应,实现了请求和响应的多路复用,提高了并发性能。
- 服务器推送:HTTP 2.0 支持服务器主动推送资源,服务器可以在客户端请求之前将相关资源发送给客户端,减少了额外的请求延迟。
- 头部压缩:HTTP 2.0 使用首部压缩算法,减少了头部信息的大小,降低了网络传输的开销。
- 二进制传输:HTTP 2.0 将传输数据分解为二进制帧,提高了传输的效率和可靠性。
HTTP 2.0 相对于 HTTP 1.x 版本在性能方面有了显著的提升,主要是通过多路复用、头部压缩和二进制传输等特性实现的。它可以更高效地利用网络资源,减少延迟和带宽消耗。HTTP 2.0 的引入改进了用户体验,提升了网站的性能和效率。
http和https比较,https加密方法
比较
1.数据传输的加密:
-
HTTP传输的数据是明文的,容易被拦截和窃听。HTTPS使用SSL/TLS协议对数据进行加密,确保数据在传输过程中的机密性和完整性。
-
HTTPS使用公钥加密和私钥解密的方式,使得数据只能被预期的接收方解密。
2.安全性和身份验证:
-
HTTPS通过数字证书来验证服务器的身份,确保用户与正确的服务器进行通信,防止中间人攻击。
-
数字证书由受信任的第三方机构(CA)签发,证明服务器是可信的,并提供加密密钥。
3.默认端口:
- HTTP默认使用端口80进行通信,而HTTPS默认使用端口443。
这使得服务器能够识别和处理不同协议的请求。
4.SEO和搜索引擎排名:
-
采用HTTPS可以提升网站在搜索引擎中的排名和可信度。
-
搜索引擎通常更倾向于显示HTTPS网站的结果,认为其更安全可靠。
5.浏览器行为:
-
当访问使用HTTPS的网站时,浏览器会显示一个锁形状的图标,指示连接是安全的。
-
当访问使用HTTP的网站时,浏览器通常不显示任何特殊标志,表示连接是不安全的。
6.性能影响:
-
HTTPS会对服务器和客户端的性能产生一定的影响,因为加密和解密数据需要额外的计算资源。
-
但随着硬件和网络的改进,这种性能影响已经大大降低。
总的来说,HTTPS相较于HTTP提供了更高的安全性,确保数据在传输过程中的机密性和完整性。在需要保护用户隐私和敏感信息的场景下,特别是涉及用户登录、支付等操作时,使用HTTPS是必要的。同时,采用HTTPS也有助于提升网站的可信度和搜索引擎排名。
https加密方式
HTTPS(Hypertext Transfer Protocol Secure)是一种用于安全通信的HTTP协议的扩展。HTTPS使用了加密机制来保护数据的传输,确保在客户端和服务器之间进行的通信是安全的。
HTTPS的加密方式主要包括以下几种:
1.对称加密(Symmetric Encryption):
对称加密使用相同的密钥进行加密和解密。发送方使用密钥将数据加密,接收方使用相同的密钥将数据解密。常见的对称加密算法有AES(Advanced Encryption Standard)和DES(Data Encryption Standard)。
2.非对称加密(Asymmetric Encryption):
非对称加密使用一对密钥,分为公钥和私钥。公钥用于加密数据,私钥用于解密数据。发送方使用接收方的公钥加密数据,只有接收方拥有相应的私钥才能解密数据。常见的非对称加密算法有RSA和Elliptic Curve Cryptography(ECC)。
3.散列函数(Hash Function):
散列函数用于将任意长度的数据转换为固定长度的哈希值。常见的散列函数有MD5(已不安全)、SHA-1(已不安全)、SHA-256等。在HTTPS中,散列函数主要用于生成消息摘要和数字签名。
4.消息摘要(Message Digest):
消息摘要是一种哈希算法,它将数据映射为固定长度的摘要(哈希值)。在HTTPS中,消息摘要用于验证数据的完整性,以防止数据在传输过程中被篡改。常用的消息摘要算法有SHA-256(Secure Hash Algorithm 256-bit)和MD5(Message Digest Algorithm 5)。
5.数字证书(Digital Certificates):
数字证书是由可信任的第三方机构(证书颁发机构,Certificate Authority)颁发的电子文档,用于验证通信中的实体身份和加密密钥的合法性。数字证书使用非对称加密技术,结合了公钥和数字签名,以确保证书的完整性和真实性。
HTTPS通常使用混合加密方式,结合对称加密和非对称加密来实现安全的通信。对称加密用于加密实际的数据传输,而非对称加密用于在通信开始时进行密钥交换和身份验证。
TLS(Transport Layer Security)是HTTPS中常用的加密协议,它使用上述加密方式来保护数据的机密性和完整性。TLS曾经的前身是SSL(Secure Sockets Layer),但由于一些安全漏洞,现在更推荐使用TLS。
react hooks使用过哪些及其特点?
1.React Hooks介绍:
React Hooks 是在 React 16.8 版本中引入的一项重要特性,它改变了组件编写的方式。它允许你在函数组件中使用状态(state)、副作用和其他 React 特性,而无需编写类组件。
使用react hooks 的好处包括:
1.简化组件:
使用 Hooks 可以将逻辑相关的代码组织在一起,使组件更加简洁和易于理解。
2.复用逻辑:
通过自定义 Hooks,可以将组件之间共享的逻辑提取出来,实现逻辑的复用。
3.状态管理:
使用 useState Hook 可以在函数组件中定义和管理状态。
4.副作用管理:
使用 useEffect Hook 可以处理副作用,如数据获取、订阅和事件处理等。
5.上下文管理:
使用 useContext Hook 可以方便地在组件之间共享数据。
2.常用的 Hooks 包括:
1.useState:
用于定义和管理组件的状态。
2.useEffect:
用于处理副作用,如数据获取、订阅和事件处理等。
3.useContext:
用于在组件之间共享数据。
4.useReducer:
用于复杂的状态管理,类似于 Redux 的 reducer。
5.useCallback:
用于缓存回调函数,以便在依赖不变时避免不必要的重新创建。
6.useMemo:
用于缓存计算结果,以便在依赖不变时避免不必要的重新计算。
React Hooks 的引入使得函数组件具备了更多的能力和灵活性,使组件的编写更加简洁、可维护和可测试。使用 8.Hooks 可以摆脱类组件中繁琐的生命周期方法,提高代码的可读性和可维护性。
需要注意的是,使用 Hooks 需要在 React 版本 16.8 或更高版本中。如果使用较旧的 React 版本,需要升级到 React 16.8 或更高版本才能使用 Hooks。
3.部分Hooks使用
下面是除useState和useEffect外,四个常用的React Hooks的介绍和使用示例:
1.useContext:
useContext钩子用于在React组件中访问上下文(Context)。它接收一个上下文对象,并返回该上下文的当前值。使用useContext可以避免通过组件树手动传递上下文值。
import React, { useContext } from 'react';
// 创建一个上下文
const MyContext = React.createContext();
function MyComponent() {
// 使用useContext获取上下文的当前值
const contextValue = useContext(MyContext);
return <div>{contextValue}</div>;
}
function App() {
return (
<MyContext.Provider value="Hello, Context!">
<MyComponent />
</MyContext.Provider>
);
}
2.useReducer:
useReducer钩子用于管理具有复杂状态和行为的组件。它接收一个reducer函数和初始状态,并返回当前状态和dispatch函数,用于触发状态更新。
import React, { useReducer } from 'react';
// 定义reducer函数
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
// 使用useReducer管理状态
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
</div>
);
}
3.useCallback:
useCallback钩子用于在依赖项发生变化时,返回一个稳定的回调函数。它用于避免在每次渲染时创建新的回调函数,以提高性能。
import React, { useState, useCallback } from 'react';
function Button({ onClick }) {
return <button onClick={onClick}>Click</button>;
}
function App() {
const [count, setCount] = useState(0);
// 使用useCallback返回一个稳定的回调函数
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<Button onClick={handleClick} />
<p>Count: {count}</p>
</div>
);
}
4.useMemo:
useMemo钩子用于在依赖项发生变化时,返回一个计算结果。它用于避免在每次渲染时重复计算成本较高的值,以提高性能。
import React, { useState, useMemo } from 'react';
function ExpensiveComponent({ value }) {
// 使用useMemo返回一个计算结果
const expensiveValue = useMemo(() => {
// 假设这里有一个复杂的计算
console.log('Calculating...');
return value * 2;
}, [value]);
return <div>Expensive Value: {expensiveValue}</div>;
}
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ExpensiveComponent value={count} />
</div>
);
}
以上是useContext、useReducer、useCallback和useMemo这四个常用React Hooks的介绍和使用示例。它们提供了一种在函数组件中管理状态、优化性能以及访问上下文的方式,使得React开发更加灵活和高效。
react router原理,
1.router的实现步骤
前端中的路由(Router)是指通过 URL 的变化来实现不同页面之间的切换和导航。路由的原理可以简单描述为以下几个步骤:
1.URL 解析:
当用户在浏览器地址栏中输入或点击链接时,浏览器会解析 URL,获取其中的路径和查询参数。
2.路由匹配:
在前端路由中,通常使用一种路由库(如React Router、Vue Router等)来定义路由规则。这些路由库会将特定的 URL 路径与组件或处理函数进行匹配。
3.组件渲染:
当 URL 路径与路由规则匹配成功后,对应的组件会被加载和渲染到页面上。这可以是一个单独的组件,也可以是一个组件树。
4.URL 更新:
在页面加载完成后,路由库会通过 JavaScript 来更新 URL,以保持与当前页面的状态同步。这样,用户刷新页面或直接访问特定 URL 时,可以保持正确的页面状态。
5.导航操作:
用户在页面上进行导航操作(如点击链接、点击返回按钮等),路由库会拦截这些操作并进行相应的处理。例如,路由库可以通过 JavaScript 来切换页面并更新 URL,而不会触发完整的页面刷新。
路由库通常提供了一些特性和API来处理路由的实现细节,例如:
-
嵌套路由:支持在页面中嵌套不同层级的子路由。
-
路由参数:允许通过 URL 中的参数来传递数据给组件。
-
跳转导航:提供编程式的导航方法,允许在组件中执行跳转操作。
-
导航守卫:提供在导航发生前、发生时或发生后执行额外逻辑的机制,例如身份验证、权限控制等。
总之,前端路由通过解析 URL、匹配路由规则、渲染组件和更新 URL,实现了在单页面应用中切换和导航的功能。它使得用户可以在不刷新整个页面的情况下,以更快速和流畅的方式访问不同的页面内容。
2.路由的底层实现原理
在前端中,路由的底层实现可以基于以下几种方式:
1.History API:
浏览器提供的 History API 允许 JavaScript 与浏览器历史记录进行交互。通过使用 pushState() 和 replaceState() 方法,可以向浏览器历史记录中添加或替换条目,并且不会导致页面的刷新。路由库可以使用 History API 来监听 URL 的变化,并根据需要更新页面组件。
2.Hash(哈希)模式:
在旧版本的浏览器中,History API 可能不被完全支持。为了兼容这些浏览器,路由库可以使用 URL 中的哈希部分(#)来模拟路由。当 URL 的哈希部分发生变化时,可以触发对应的路由匹配和组件渲染。
3.HTML5 <a>
标签的默认行为:
当用户点击页面上的链接时,浏览器会默认发起一个 HTTP 请求并加载新的页面。然而,通过监听<a>
标签的点击事件,并阻止默认行为,可以在前端实现路由的切换和导航,而不需要进行完整的页面刷新。
4.浏览器端的 JavaScript 路由库:许多前端框架和库,如React Router、Vue Router等,提供了自己的路由实现。这些库通过监听浏览器的 URL 变化,使用上述的底层机制(如 History API、哈希模式或默认行为阻止)来实现路由的功能,并提供了更高级的特性和API,以简化开发过程。
需要注意的是,不同的路由库和实现方式可能在底层机制上有所差异,但它们的目标都是在单页面应用中实现路由功能。具体选择使用哪种方式取决于项目需求、浏览器兼容性要求和开发团队的偏好。
hash模式和browser模式比较
1.HashRouter(哈希路由):
HashRouter使用URL中的哈希(#)来模拟路由的改变。在HashRouter中,整个应用程序的URL路径部分位于哈希后面,例如http://example.com/#/route
。哈希部分的改变不会导致浏览器向服务器发送请求,因此在使用HashRouter时,整个应用程序的状态都保存在前端,不会与服务器进行交互。
import { HashRouter, Route } from 'react-router-dom';
function App() {
return (
<HashRouter>
<Route path="/home" component={Home} />
<Route path="/about" component={About} />
</HashRouter>
);
}
使用HashRouter可以方便地部署到任何静态服务器上,并且在前端开发中比较常用。然而,URL中的哈希部分可能不太友好,且不支持服务端渲染。
2.BrowserRouter(浏览器路由):
BrowserRouter使用浏览器的History API来处理路由的改变。它使用真实的URL路径,例如http://example.com/route
。当URL路径发生变化时,BrowserRouter会向服务器发送请求,并与服务器进行交互。
import { BrowserRouter, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Route path="/home" component={Home} />
<Route path="/about" component={About} />
</BrowserRouter>
);
}
BrowserRouter提供了更友好的URL形式,并且对服务端渲染更加友好。但是,使用BrowserRouter需要正确配置服务器以支持URL路径的路由。
总结:
HashRouter使用URL的哈希部分模拟路由,不与服务器进行交互,适用于部署到静态服务器和前端开发。
BrowserRouter使用真实的URL路径和浏览器的History API,与服务器进行交互,适用于更友好的URL形式和服务端渲染。
react渲染流程
React的渲染机制和渲染流程是基于虚拟DOM(Virtual DOM)的概念和Diff算法。下面是React的渲染机制和渲染流程的详细介绍:
1.组件渲染:
React应用程序是由多个组件组成的,每个组件都有自己的状态(state)和属性(props)。当应用程序启动时,React会根据组件的层次结构递归地创建一个虚拟DOM树。
2.虚拟DOM:
虚拟DOM是React内部维护的一个轻量级的JavaScript对象树,它是对真实DOM的抽象表示。每个组件都有一个对应的虚拟DOM节点,包含了组件的状态和属性。
3.初始渲染:
在初始渲染时,React会将整个虚拟DOM树渲染为真实DOM,并将其插入到页面中的根元素中。
4.更新渲染:
当组件的状态或属性发生变化时,React会触发更新渲染。在更新渲染过程中,React首先会创建一个新的虚拟DOM树,表示更新后的组件状态。然后,React会使用Diff算法比较新旧虚拟DOM树的差异,找出需要进行更新的部分。
5.Diff算法:
Diff算法是React用于比较虚拟DOM树的差异的核心算法。它通过比较新旧虚拟DOM节点的类型、属性和子节点,确定哪些部分需要进行更新、添加或删除。Diff算法尽可能高效地找出最小的变化集合,以减少真实DOM的操作次数。
6.更新DOM:
在Diff算法确定了需要进行更新的部分后,React会将这些变化应用于真实DOM,以保持与新虚拟DOM树的一致性。React使用批处理的方式进行DOM更新,将多个操作合并为单个操作,以提高性能。
7.生命周期方法:
在渲染过程中,React还提供了一系列生命周期方法,允许开发者在特定的时机执行自定义逻辑。例如,componentDidMount会在组件首次渲染到真实DOM后调用,componentDidUpdate会在组件更新后调用。
总结:
React的渲染机制通过虚拟DOM和Diff算法实现高效的DOM更新。当组件状态或属性发生变化时,React会重新渲染虚拟DOM树,并通过Diff算法找出需要更新的部分。然后,React将这些变化应用于真实DOM,以实现页面的更新和重新渲染。这种基于虚拟DOM和Diff算法的渲染机制使得React具有高效、灵活和可维护的特性。
设计模式介绍(工厂模式、观察者模式)
工厂模式和观察者模式是前端设计模式中常见的两种模式。它们在不同的场景下有不同的作用和特点。
工厂模式(Factory Pattern):
工厂模式是一种创建型设计模式,用于封装对象的创建过程。它通过一个工厂函数或类来创建对象,隐藏了具体对象的创建细节,使代码更具可读性和可维护性。工厂模式适用于需要创建多个相似对象的情况,可以根据不同的参数或条件返回不同的对象实例。
// 工厂函数
function createProduct(type) {
if (type === 'A') {
return new ProductA();
} else if (type === 'B') {
return new ProductB();
}
// ...
}
// 工厂类
class ProductFactory {
createProduct(type) {
if (type === 'A') {
return new ProductA();
} else if (type === 'B') {
return new ProductB();
}
// ...
}
}
// 使用工厂模式创建对象
const productA = createProduct('A');
const productB = new ProductFactory().createProduct('B');
工厂模式的优点是封装了对象的创建过程,提供了统一的接口来创建对象,便于代码的扩展和维护。它可以隐藏对象的具体实现,客户端只需关心工厂函数或类的使用方式。
观察者模式(Observer Pattern):
观察者模式是一种行为型设计模式,用于定义对象之间的一对多依赖关系。在该模式中,当一个对象的状态发生变化时,它的所有依赖对象(观察者)都会收到通知并自动更新。观察者模式用于解耦对象之间的关联,使得对象之间的交互更松散、灵活。
// 主题(Subject)对象
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
// 观察者(Observer)对象
class Observer {
update(data) {
// 处理接收到的数据
}
}
// 使用观察者模式
const subject = new Subject();
const observerA = new Observer();
const observerB = new Observer();
subject.addObserver(observerA);
subject.addObserver(observerB);
subject.notify(data);
观察者模式的特点是将主题对象与观察者对象解耦,使得它们可以独立变化。当主题对象的状态发生变化时,观察者对象会自动更新,实现了一种松散的依赖关系。观察者模式适用于需要实现事件处理、发布-订阅等场景,提高了代码的灵活性和可维护性。
以上是工厂模式和观察者模式的简要介绍和代码示例。这些设计模式在前端开发中广泛应用,能够帮助开发者提高代码的可读性、可维护性和可扩展性。
代码阅读题:事件循环(async、await、setTimeout、Promise().then()),打印到控制台的结果是什么?
手写深拷贝
在 JavaScript 中,手写深拷贝代码可以使用递归的方式遍历对象的属性,并创建一个新的对象来进行拷贝。下面是一个简单的示例代码来手写实现深拷贝:
function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let copy;
if (Array.isArray(obj)) {
copy = [];
for (let i = 0; i < obj.length; i++) {
copy[i] = deepCopy(obj[i]);
}
} else {
copy = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]);
}
}
}
return copy;
}
使用该 deepCopy 函数,你可以深拷贝任意的对象,包括数组和嵌套对象。例如:
const obj1 = {
name: 'John',
age: 30,
address: {
street: '123 ABC Street',
city: 'New York',
},
};
const obj2 = deepCopy(obj1);
obj2.name = 'Jane';
obj2.address.street = '456 XYZ Street';
console.log(obj1); // { name: 'John', age: 30, address: { street: '123 ABC Street', city: 'New York' } }
console.log(obj2); // { name: 'Jane', age: 30, address: { street: '456 XYZ Street', city: 'New York' } }
在上述示例中,obj1 对象通过深拷贝得到了 obj2 对象。修改 obj2 对象的属性并不会影响到原始的 obj1 对象。
需要注意的是,该示例代码只能处理一般的情况,对于特殊的对象类型(例如函数、正则表达式等),可能需要根据具体情况进行特殊处理。此外,对于存在循环引用的对象结构,该代码也无法处理,需要进行额外的循环引用检测。
算法题:树的最大深度
最后,面试官让我把他没有提到,但是我了解的知识点介绍一遍,只说名字就行
小结:一面时间最长,还有很多问题回忆不起来,主要是对知识体系广度的考察
二面(65 min)
自我介绍
为什么选择前端?
怎样学习前端的?
进程和线程
1.进程与线程介绍
在计算机科学中,进程(Process)和线程(Thread)是操作系统中用于执行任务的概念。
1.进程介绍
进程是指计算机中运行的一个程序的实例。它拥有独立的内存空间和系统资源,并且可以由操作系统进行调度和管理。每个进程都是一个独立的执行单元,具有自己的代码、数据和运行状态。
2.线程介绍
线程是进程中的一个执行路径。一个进程可以包含多个线程,它们共享相同的内存空间和系统资源。线程是执行计算机指令的最小单位,可以独立运行,并且可以并发执行。线程之间可以共享进程的资源,如变量、文件等。
3.进程与线程联系:
进程和线程都是计算机执行任务的基本单位。
进程可以包含多个线程,线程是进程的一个执行路径。
进程和线程都能够并发执行,提高计算机的利用率和响应性。
4.进程与线程区别:
进程是独立的执行实体,拥有自己的内存空间和系统资源,而线程是进程内的执行路径,共享进程的资源。
进程之间相互独立,互不影响,而线程之间共享同一个进程的上下文和资源。
创建和销毁进程比创建和销毁线程的开销更大,进程间的切换开销也比线程大。线程的创建和切换开销相对较小。
进程之间通信需要使用进程间通信(IPC)机制,如管道、消息队列、共享内存等。线程之间可以通过共享内存等方式直接进行通信。
2.JavaScript中的进程与线程
在 JavaScript 中,JavaScript 是一种单线程语言,意味着它只有一个执行线程(通常称为主线程)。这是因为 JavaScript 是一种设计用于浏览器中的脚本语言,主要用于操作 DOM、响应用户交互等任务。JavaScript 运行在浏览器的单个线程上,以确保事件的顺序执行和避免竞态条件。
尽管 JavaScript 本身是单线程的,但通过使用 Web Workers,可以在 JavaScript 中创建多个子线程,以实现并行处理任务。Web Workers 是运行在后台的 JavaScript 线程,可以执行耗时的计算和操作,以避免阻塞主线程,提高页面的响应性。但需要注意,Web Workers 之间是相互独立的,无法直接共享内存或访问 DOM,需要通过消息传递进行通信。
总结:
进程是计算机中运行的程序的实例,线程是进程中的执行路径。
进程具有独立的内存空间和系统资源,线程共享进程的资源。
JavaScript 是一种单线程语言,但可以使用 Web Workers 创建多个子线程实现并行处理。
3.问:一个8核CPU只能创建8个进程吗
不,一个 8 核 CPU 并不限制同时创建的进程数量为 8 个。实际上,可以创建更多的进程,不仅仅取决于 CPU 的核心数,还受操作系统和系统资源的限制。
在一个拥有 8 个核心的 CPU 上,可以同时运行多个进程,每个进程都可以在一个独立的核心上执行。这意味着你可以创建和管理超过 8 个的进程。操作系统负责调度和分配进程到可用的核心上,并根据需要进行进程切换。
实际上,操作系统能够同时管理和运行多个进程,数量取决于操作系统的设计和配置,以及系统资源的可用性。操作系统使用调度算法来确定哪些进程应该运行,并在核心之间进行动态分配。因此,在一个 8 核 CPU 上,可以创建和运行多个进程,数量通常远远超过 CPU 的核心数。
需要注意的是,创建过多的进程可能会对系统资源造成负担,并影响系统性能。因此,在实际应用中,应该根据实际需求和系统资源进行合理的进程管理和调度。
4.系统中,线程数可以大于等于进程数对吗
在一个系统中,CPU 上运行的线程数可以大于或等于进程数
每个进程都是一个独立的执行环境,包含自己的代码、数据和资源。而线程是在进程内部创建的执行单元,共享进程的资源,但拥有独立的执行上下文。
在多线程的情况下,多个线程可以在同一个进程中并行执行,利用多核 CPU 的并行处理能力。操作系统会将线程分配给可用的核心进行执行。
因此,一个进程可以包含多个线程,这些线程可以在多个核心上同时执行。所以在一个系统中,通常会有多个线程在 CPU 上并发执行,而进程的数量可以大于或等于线程的数量。
5.一个进程可以在不同cpu核心上执行吗
一个进程在不同 CPU 核心上执行是可能的。在多核处理器的系统中,操作系统可以将一个进程的不同线程分配到不同的 CPU 核心上并行执行。
当一个进程有多个线程时,操作系统可以根据调度算法将这些线程分配给可用的 CPU 核心,以实现并发执行。这样可以提高系统的并发性和性能。
进程是操作系统分配资源和管理任务的基本单位,而线程是在进程内部创建的执行单元。一个进程可以包含多个线程,这些线程共享进程的地址空间和资源,但拥有独立的执行上下文。
通过将不同线程分配到不同的 CPU 核心上,系统可以利用多核处理器的并行处理能力,提高计算能力和响应性能。每个 CPU 核心独立地执行一个线程,从而实现并发执行。
需要注意的是,进程和线程的调度是由操作系统负责管理的,具体的调度策略和算法会根据系统配置和需求而有所不同。操作系统会根据系统负载、线程优先级、资源需求等因素来决定将进程的哪些线程分配给哪个 CPU 核心执行。
因此,一个进程可以在不同的 CPU 核心上执行,以实现并发执行和提高系统的性能。这种多核并行处理的能力是现代计算机系统的重要特性之一。
6.一个进程的不同线程,可以在不同核心上执行吗
是的,一个进程的不同线程可以在不同的 CPU 核心上执行。现代操作系统具有多核处理器的支持,可以同时运行多个线程,并将它们分配到不同的 CPU 核心上执行。
操作系统通过调度算法来决定哪些线程应该在哪个 CPU 核心上执行。这样可以实现线程级别的并行处理,提高系统的并发性和性能。
具体的线程调度和分配是由操作系统负责管理的,它根据系统负载、线程的优先级和其他因素来做出决策。操作系统会尽可能地将线程分配给可用的 CPU 核心,以实现并行执行和最大化系统资源的利用。
因此,一个进程的不同线程可以在不同的 CPU 核心上并发执行,从而提高系统的性能和效率。这种并发执行可以充分利用多核处理器的优势,实现更高的计算能力和响应速度。
7.一个进程的多个线程在不同核心上执行,他们如何共享这一个进程的资源呢
当一个进程的多个线程在不同核心上执行时,它们可以共享同一个进程的资源。
进程内的线程共享进程的地址空间,包括代码、数据和堆栈。这意味着它们可以访问和修改相同的变量、对象和数据结构。线程可以通过共享内存进行通信,从而实现数据共享和交互。
此外,进程的文件描述符(file descriptor)和打开的文件也是进程级别的资源,线程可以共享这些资源,从而可以访问和操作相同的文件。
在多线程的环境中,需要注意对共享资源的访问控制和同步。由于多个线程可以并发地访问和修改共享资源,可能会出现竞态条件(race condition)等问题。因此,需要使用同步机制如互斥锁(mutex)、信号量(semaphore)等来确保线程之间的安全访问。
操作系统会提供这些同步机制,以及其他的线程间通信方式如条件变量(condition variable)等,来协调线程之间的共享资源和交互。
因此,虽然多个线程可能在不同的 CPU 核心上执行,但它们可以共享同一个进程的资源,通过共享内存和同步机制来实现数据共享和交互。这种共享资源的方式使得多线程编程可以更高效地利用系统资源,并实现并发执行和协作任务的目的。
进程的调度策略
当操作系统中有多个进程需要执行时,调度策略决定了进程如何被选中并分配处理器时间。以下是对不同调度策略的基本概念、介绍和优缺点的重新介绍:
1.先来先服务调度(First-Come, First-Served Scheduling,FCFS):
概念:按照进程到达的顺序进行调度,先到达的进程先执行。
优点:简单且公平,适用于长作业。
缺点:长作业会占用处理器时间,可能导致短作业等待时间过长,产生"饥饿"现象。
2.最短作业优先调度(Shortest Job First Scheduling,SJF):
概念:选择执行时间最短的进程先执行。
优点:能够最大程度上减少平均等待时间。
缺点:需要预测和估计进程的执行时间,对于无法准确预测的情况可能不适用,不适合处理实时任务。
3.轮转调度(Round Robin Scheduling):
概念:采用时间片轮转的方式,每个进程被分配一个固定的时间片,当时间片用完时,进程被移到等待队列末尾。
优点:实现公平调度,确保每个进程都能获得一定的处理器时间。
缺点:可能导致长作业的响应时间较长。
4.优先级调度(Priority Scheduling):
概念:为每个进程分配一个优先级,并根据优先级选择下一个要执行的进程。
优点:可以根据进程的重要性和特性进行灵活的调度。
缺点:可能导致低优先级进程饥饿,需要引入抢占机制以允许高优先级进程抢占正在执行的进程。
5.多级队列调度(Multilevel Queue Scheduling):
概念:将进程划分为多个优先级队列,每个队列可以采用不同的调度策略。
优点:根据进程特性和优先级,可以灵活地进行调度,提高系统的性能和资源利用率。
缺点:需要配置和管理多个队列,调度策略复杂度较高。
每种调度策略都有其适用的场景和优缺点。选择合适的调度策略取决于系统的需求和目标。在实际应用中,通常会结合多种调度策略和算法,采用抢占式调度以处理实时任务、多核处理器等特殊情况,并根据实时监测和反馈进行动态调整。
线程的同步和线程间通讯
1.线程同步:
线程同步是指多个线程在访问共享资源时保持顺序和协调的机制,以避免数据竞争和不一致的结果。在多线程环境中,当多个线程同时对共享资源进行读写操作时,如果没有适当的同步机制,可能会导致数据损坏或结果不可预测。
常见的线程同步机制包括:
-
互斥锁(Mutex):互斥锁用于保护共享资源,只允许一个线程进入临界区(访问共享资源的代码段),其他线程需要等待该线程释放锁才能进入临界区。
-
信号量(Semaphore):信号量用于限制同时访问某个资源的线程数量,它可以允许多个线程同时进入临界区,但是限制同时访问资源的线程数目。
-
条件变量(Condition):条件变量用于线程之间的等待和通知机制,一个线程可以等待某个条件成立,而其他线程可以在某个条件满足时通知等待的线程继续执行。
-
屏障(Barrier):屏障用于等待所有参与线程都到达某个点之后再继续执行,确保线程的执行顺序和协调性。
2.线程间通信:
线程间通信是指多个线程之间传递信息和共享数据的机制。在线程间通信中,线程可以相互发送消息、共享数据或者等待其他线程的信号,以实现协同工作。
常见的线程间通信方式包括:
-
共享内存:多个线程共享一块内存区域,通过读写该共享内存来进行通信。为了确保线程安全,需要使用同步机制(如互斥锁)来保护共享内存的访问。
-
消息传递:线程之间通过发送和接收消息来进行通信。消息传递可以是同步的(发送方等待接收方响应)或异步的(发送方不等待接收方响应)。
-
管道(Pipe):管道是一种半双工的通信方式,可以在两个相关联的线程之间传递数据。一个线程将数据写入管道,而另一个线程从管道中读取数据。
-
队列(Queue):队列是一种线程安全的数据结构,线程可以将数据放入队列的一端,而其他线程从队列的另一端取出数据。队列可以用于实现生产者-消费者模型。
线程同步和线程间通信是多线程编程中解决并发问题的关键技术。正确地使用同步机制和合适的通信方式可以确保多个线程之间的协调运行,避免数据竞争和其他并发问题的发生。
比较const、let和var
在 JavaScript 中,var、let 和 const 是用于声明变量的关键字,它们之间有以下区别:
1.作用域:
-
var 声明的变量具有函数作用域或全局作用域。在函数内部声明的变量只在函数内部有效,而在函数外部声明的变量则具有全局作用域。
-
let 和 const 声明的变量具有块级作用域。块级作用域可以是函数、循环、条件语句或任何使用花括号 {} 包裹的代码块。在块级作用域中声明的变量只在该块内部有效。
2.变量提升:
-
var 声明的变量存在变量提升的特性,即变量可以在声明之前使用。但是它的值会被默认初始化为 undefined。
-
let 和 const 声明的变量不会进行变量提升,即在声明之前使用会导致引用错误(ReferenceError)。
3.重复声明:
-
var 允许对同一个变量进行多次声明,而后续的声明会覆盖前面的声明。
-
let 和 const 不允许在同一个作用域内重复声明同一个变量,重复声明会导致语法错误(SyntaxError)。
4.可修改性:
-
var 和 let 声明的变量可以被重新赋值,即可以修改变量的值。
-
const 声明的变量被称为常量,其值在声明后不能被重新赋值,是只读的。但是对于复杂类型(如对象或数组),其内部的属性或元素仍可以被修改。
综上所述,let 和 const 是在 ES6 中引入的新的变量声明方式,相较于传统的 var,它们提供了更好的作用域控制、不进行变量提升和常量声明等特性,推荐在新的 JavaScript 代码中使用 let 和 const 来声明变量,而尽量避免使用 var。
快速排序
快速排序(Quick Sort)是一种高效的排序算法,它使用分治法(Divide and Conquer)的思想,通过选择一个基准元素,将数组分成两个子数组,并递归地对子数组进行排序,最终将子数组合并成有序的数组。快速排序的核心思想是通过不断地将比基准元素小的元素放到左边,比基准元素大的元素放到右边,从而将数组分割成较小的子数组进行排序。
以下是快速排序算法的示例代码(使用JavaScript语言):
function quickSort(arr) {
if (arr.length <= 1) {
return arr;
}
const pivot = arr[0]; // 选择第一个元素作为基准
const left = [];
const right = [];
for (let i = 1; i < arr.length; i++) {
if (arr[i] <= pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return [...quickSort(left), pivot, ...quickSort(right)];
}
// 示例用法
const arr = [5, 2, 9, 1, 7, 6, 3];
const sortedArr = quickSort(arr);
console.log(sortedArr);
代码解析:
1.定义 quickSort 函数,接受一个数组 arr 作为输入。
2.如果数组长度小于等于 1,直接返回数组,因为只有一个元素或者没有元素时已经是有序的。
否则,选择数组的第一个元素作为基准(pivot)。
3.创建两个空数组 left 和 right,用于存放小于等于基准和大于基准的元素。
4.遍历数组的剩余元素,将小于等于基准的元素放入 left 数组,将大于基准的元素放入 right 数组。
5.对 left 数组和 right 数组分别递归调用 quickSort 函数,得到排序后的子数组。
6.将排序后的子数组和基准元素按顺序连接起来,得到最终的有序数组。
返回最终的有序数组。
快速排序的时间复杂度为 O(nlogn) 平均情况下,最坏情况下为 O(n^2)。它是一种原地排序算法,不需要额外的空间。快速排序在实际应用中表现良好,并且是许多编程语言中标准库中的排序算法之一。
代码阅读题:const定义引用类型,判断哪几行报错?哪几行正常运行?
用setTimeout实现setInterval
在 JavaScript 中,可以使用 setTimeout 函数来模拟实现 setInterval 的功能。下面是使用 setTimeout 实现 setInterval 的代码示例:
function mySetInterval(callback, delay) {
function interval() {
callback();
setTimeout(interval, delay);
}
setTimeout(interval, delay);
}
// 示例用法
function myCallback() {
console.log("Hello, world!");
}
mySetInterval(myCallback, 1000);
代码解析:
定义 mySetInterval 函数,接受一个回调函数 callback 和延迟时间 delay 作为参数。
在 mySetInterval 函数内部,定义了一个名为 interval 的嵌套函数。
interval 函数首先执行回调函数 callback,然后通过 setTimeout 设置下一次执行 interval 函数的延迟时间为 delay。
在 mySetInterval 函数内部,通过 setTimeout 调用 interval 函数,用于启动定时器。
示例用法中的回调函数 myCallback 只是简单地输出 “Hello, world!”,你可以根据实际需求编写自己的回调函数。
调用 mySetInterval 函数,传入回调函数和延迟时间,即可实现类似于 setInterval 的定时执行效果。
需要注意的是,setInterval 和使用 setTimeout 模拟的 setInterval 在长时间运行时可能存在累积的时间误差。原因是 setTimeout 在指定的延迟时间之后执行回调函数,而在执行回调函数期间可能会有其他的 JavaScript 代码执行,导致实际的执行间隔可能比期望的延迟时间要长。如果需要更精确的定时器,可以考虑使用 requestAnimationFrame 或者第三方的时间管理库。
算法题:买卖股票最佳时机 Ⅱ(力扣122)
小结:二面考察不少操作系统的知识,并且针对某些知识点不断深挖,可能有些知识点了解并不够深入,但是面试官会耐心的引导
三面(35 min)
自我介绍
为什么选择前端?
怎样学习前端?
是否重构过代码?怎样重构的?
了解什么新的前端的技术?
虚拟DOM
前端虚拟DOM(Virtual DOM)是一种在Web开发中用于高效更新用户界面的技术。它是一种用JavaScript对象表示页面结构的抽象概念,可以在内存中进行操作,然后与实际的DOM进行比较并进行最小化的更新。
虚拟DOM的思想起源于React框架,但现在已经在其他前端框架中得到广泛应用。它的主要目标是减少直接操作实际DOM所带来的性能开销,从而提高应用的响应速度和用户体验。
下面是虚拟DOM的工作原理的详细步骤:
1.初始化阶段:在页面加载时,通过JavaScript创建一个虚拟DOM树,它是由一系列的JavaScript对象构成,每个对象表示一个实际DOM元素。
2.渲染阶段:虚拟DOM树被映射到实际的DOM树上,并通过实际的DOM元素构建出初始的用户界面。这个过程通常称为首次渲染。
3.更新阶段:当应用状态发生变化时,需要更新用户界面以反映这些变化。在更新阶段,首先通过JavaScript对虚拟DOM进行修改,而不是直接操作实际的DOM。
4.对比阶段:虚拟DOM会与先前的版本进行对比,找出需要进行更新的部分。通过比较两个虚拟DOM树的差异,可以确定需要修改的DOM元素。
5.批量更新:根据对比结果,只对需要更新的部分进行实际的DOM操作。这样可以避免对整个DOM树进行频繁的操作,从而提高性能。
6.应用界面更新:实际DOM的变化会立即反映在用户界面上,用户可以看到界面的更新效果。
通过使用虚拟DOM,前端框架可以优化DOM操作的性能。由于直接操作实际的DOM通常是比较耗费资源的,通过在JavaScript层面上进行虚拟DOM的比较和更新,可以减少实际的DOM操作次数,从而提高应用的性能和响应速度。
虚拟DOM还提供了一些其他的优势,例如跨平台兼容性。由于虚拟DOM是通过JavaScript对象表示的,因此可以在不同的平台上使用相同的代码逻辑进行渲染,如浏览器、移动端等。
总之,前端虚拟DOM是一种用于优化DOM操作性能的技术,通过在JavaScript层面上构建和操作DOM的抽象表示,可以减少实际DOM操作的次数,提高应用的性能和用户体验。
react17的启发式更新算法
React 17 引入了一种称为“启发式更新算法”的改进,以进一步提升组件更新的性能。该算法通过利用组件树的结构和更新模式的特点,减少不必要的组件更新,从而减少了应用的渲染开销。
下面是 React 17 启发式更新算法的一些关键特点和原则:
1.批量更新:
React 17 采用了批量更新的方式,将多个组件的更新操作合并为一次更新,避免了频繁的中间状态渲染。这样可以减少实际的 DOM 操作次数,提高性能。
2.优先级排序:
React 17 根据组件树的结构和更新模式,对更新任务进行优先级排序。具有较高优先级的更新会被优先处理,从而保证重要的更新能够尽快完成。
3.执行跳过:
React 17 引入了执行跳过机制,当父组件更新时,如果子组件的 props 和状态没有发生变化,React 可以跳过对子组件的更新操作。这避免了不必要的 diff 运算和重新渲染,提高了性能。
4.上下文依赖:
React 17 还考虑了组件间的上下文依赖关系。当一个组件的上下文发生变化时,React 会遍历子组件并检查它们是否受到上下文变化的影响。如果不受影响,将跳过子组件的更新,进一步减少渲染开销。
5.副作用刷新:
React 17 通过引入 useEffect 的新行为来优化副作用的刷新。React 可以跳过没有发生变化的副作用,从而减少不必要的副作用触发和处理。
通过这些优化措施,React 17 的启发式更新算法能够更加智能地管理组件更新,减少不必要的操作,提高渲染性能。开发者无需手动进行优化,框架自身会根据更新模式和组件结构进行优化,使应用在性能上获得显著的提升。
了解iframe吗?
小结:三面也就是leader面,面试时间很短,给我的压力最大,leader对于我的学习方式和代码重构都不满意,技术提问环节不超过10分钟,让我一度以为没了希望,最后甚至写代码的环节都没有(反问环节得知,看过我二面的代码,就没必要写了)。最后leader也给了我很多前端学习的方法和思路,面试结束后5分钟就收到三面通过的电话,还是很惊喜的,所以不到最后一刻千万不要放弃啊!
iframe简介
在前端开发中,iframe(内嵌框架)是一种HTML元素,用于在网页中嵌入另一个独立的HTML文档。它可以在同一页面中展示其他网页或嵌入第三方内容,如地图、视频、广告等。以下是对iframe的详细介绍:
- 1.基本结构:iframe通过
<iframe>
标签进行定义,其中的src属性指定了要嵌入的文档的URL。
<iframe src="https://example.com"></iframe>
-
2.内容嵌入:iframe可以嵌入不同源(跨域)的内容,因此它可以用于展示其他网站的内容。但是,出于安全考虑,浏览器会使用同源策略(Same-Origin Policy)来限制对跨域文档的访问。
-
3.跨域通信:由于同源策略的限制,iframe内部的文档和外部文档(父文档)之间的直接通信存在限制。但可以通过一些技术手段实现跨域通信,例如使用postMessage() API 进行消息传递。
-
4.尺寸控制:通过width和height属性,可以控制iframe的宽度和高度。
<iframe src="https://example.com" width="500" height="300"></iframe>
此外,也可以使用CSS样式对iframe进行调整和布局。
-
5.嵌套使用:iframe可以嵌套使用,即一个iframe中可以再嵌套其他iframe,形成多层嵌套结构。每个嵌套层级都有自己的文档上下文。
-
6.无障碍性:使用iframe时,需要考虑无障碍性(Accessibility)问题。确保被嵌入的内容对于屏幕阅读器和其他辅助技术是可访问的。
-
7.SEO和性能影响:iframe中的内容通常被搜索引擎视为独立的文档,而不是嵌入到父文档中的一部分。这意味着搜索引擎可能无法将嵌入的内容归属于父文档,影响到SEO。此外,使用过多的iframe可能会影响页面的加载性能。
尽管iframe在某些情况下很有用,但应谨慎使用。由于其跨域通信、无障碍性和性能方面的考虑,需要权衡利弊并遵循最佳实践。在大多数情况下,可以使用更现代的Web技术,如AJAX、组件化架构和动态加载内容,来替代iframe的使用。
iframe常用属性介绍
<iframe>
元素有许多可用于控制其行为和外观的属性。以下是一些常用的iframe属性的介绍:
1.src:指定要嵌入的文档的URL。可以是同源或跨域的URL。
<iframe src="https://example.com"></iframe>
2.width和height:控制iframe的宽度和高度。可以使用像素值或百分比值。
<iframe src="https://example.com" width="500" height="300"></iframe>
3.frameborder:指定是否显示iframe的边框。设置为0表示不显示边框,1表示显示边框。
<iframe src="https://example.com" frameborder="0"></iframe>
4.scrolling:
控制iframe中内容的滚动条显示方式。可以设置为auto(根据内容自动显示滚动条)、yes(始终显示滚动条)或no(不显示滚动条)。
<iframe src="https://example.com" scrolling="auto"></iframe>
5.sandbox:
定义一个沙盒环境,用于限制iframe内部文档的操作。它是一个空格分隔的属性列表,可以包含allow-forms、allow-same-origin、allow-scripts、allow-popups等。
<iframe src="https://example.com" sandbox="allow-scripts"></iframe>
6.name:
为iframe指定一个唯一的名称,可以在其他地方引用该iframe。
<iframe src="https://example.com" name="myFrame"></iframe>
7.allowfullscreen:指定是否允许iframe进入全屏模式。
<iframe src="https://example.com" allowfullscreen></iframe>
8.loading:指定iframe的加载方式。
可以设置为eager(立即加载)或lazy(延迟加载)。
<iframe src="https://example.com" loading="lazy"></iframe>
这些是iframe常用的属性,它们可以根据需求来配置iframe的外观和行为。除了属性之外,还可以使用CSS样式来进一步定制iframe的样式。
HR面(20 min)
自我介绍
为什么选择前端?
怎么看待加班?
对抖音电商了解多少?
职业规划
题目摘录:https://ac.nowcoder.com/discuss/840386?type=2