春招实习错过了,这次被百度校招宠幸有点开心。校招相比实习面试难度大一些,这不刚面完,就又接到百度实习的面试通知。 想必是没有实习经验,校招不收,可能面的还行,再扔到实习岗继续毒打....
百度校招一面
百度校招, 100 人,各位都来吧。非常喜欢百度招聘网上的那句“年轻无所畏 热AI有所为”,冲!
什么时候开始学习前端?
-
时间和动机
我在大学二年级开始学习前端,因为所在的软件协会有挺多学长在字节、百度等大厂做前端。
-
学习经历
学长推荐了《你不知道javascript》,就先刷了下,把js 基础打牢是很重要的。 接着跟着github.com/bradtravers… demo 然后在掘金找感兴趣的内容/小册学习,自己也会把所想所感写文章发表到掘金。 学习vue全家桶 了解react koa框架全栈开发 ai相关
-
成长与成就 掘金lv5级 写了多个项目
懒加载
-
懒加载思路
懒加载也叫延迟加载,指的是在长网页中延迟加载图像,是一种很好优化网页性能的方式。用户滚动到它们之前,可视区域外的图像不会加载。在长网页上使用延迟加载将使网页加载更快。在某些情况下,它还可以帮助减少服务器负载。常适用图片很多,页面很长的电商网站场景中。
-
为什么要用懒加载
- 能提升用户的体验,不妨设想下,用户打开像手机淘宝长页面的时候,如果页面上所有的图片都需要加载,由于图片数目较大,等待时间很长,用户难免会心生抱怨,这就严重影响用户体验。
- 减少无效资源的加载,这样能明显减少了服务器的压力和流量,也能够减小浏览器的负担。
- 防止并发加载的资源过多会阻塞js的加载,影响网站的正常使用
-
懒加载的原理
首先将页面上的图片的 src 属性设为空字符串,而图片的真实路径则设置在data-original属性中, 当页面滚动的时候需要去监听scroll事件,在scroll事件的回调中,判断我们的懒加载的图片是否进入可视区域,如果图片在可视区内将图片的 src 属性设置为data-original 的值,这样就可以实现延迟加载。
-
懒加载实现步骤
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lazyload</title>
<style>
.image-item {
display: block;
margin-bottom: 50px;
height: 200px;//一定记得设置图片高度
}
</style>
</head>
<body>
<img src="" class="image-item" lazyload="true" data-original="images/1.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/2.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/3.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/4.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/5.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/6.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/7.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/8.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/9.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/10.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/11.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/12.png"/>
<script>
var viewHeight =document.documentElement.clientHeight//获取可视区高度
function lazyload(){
var eles=document.querySelectorAll('img[data-original][lazyload]')
Array.prototype.forEach.call(eles,function(item,index){
var rect
if(item.dataset.original==="")
return
rect=item.getBoundingClientRect()// 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
if(rect.bottom>=0 && rect.top < viewHeight){
!function(){
var img=new Image()
img.src=item.dataset.original
img.onload=function(){
item.src=img.src
}
item.removeAttribute("data-original")//移除属性,下次不再遍历
item.removeAttribute("lazyload")
}()
}
})
}
lazyload()//刚开始还没滚动屏幕时,要先触发一次函数,初始化首页的页面图片
document.addEventListener("scroll",lazyload)
</script>
</body>
</html>
- 图片示例
- IntersectionObserver方案
然后创建了一个 IntersectionObserver
实例,它会在任何被观察的目标元素进入视口时触发回调函数。在这个回调函数中,我们检查 isIntersecting
属性,如果为 true
,则说明元素已经在视口中,此时可以安全地设置图片的 src
属性,从而开始加载图片。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>图片懒加载示例</title>
<style>
.lazy {
opacity: 0.5;
transition: opacity 0.5s;
}
.lazy-loaded {
opacity: 1;
}
</style>
</head>
<body>
<img class="lazy" data-src="example.jpg" alt="Example image">
<script>
// 创建一个IntersectionObserver实例
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 获取图片的真实路径
const img = entry.target;
const src = img.getAttribute('data-src');
// 设置图片的src属性
img.src = src;
// 添加类名以改变样式(可选)
img.classList.remove('lazy');
img.classList.add('lazy-loaded');
// 不再观察该元素
observer.unobserve(img);
}
});
});
// 获取所有需要懒加载的图片
const lazyImages = document.querySelectorAll('.lazy');
// 开始观察每一个图片元素
lazyImages.forEach(img => {
observer.observe(img);
});
</script>
</body>
</html>
-
后端配合前端懒加载
- 分页返回接口数据
- 分屏渲染
前端在用户浏览到某个区域时再请求后端渲染并返回相应的内容片段。
实现瀑布流的思路
女友看了小红书的pc端后,问我这个瀑布流的效果是怎么实现的?🤔瀑布流虚拟列表前置篇,分析小红书瀑布流特点,实现不定高卡 - 掘金 (juejin.cn)
翻转二叉树
leetcode 226 简单题。
-
二叉树用数组怎么存储?
二叉树可以用数组表示,通常通过层序遍历(从上到下,从左到右)的方式将节点存储到数组中。
左子节点的索引是
2 * i + 1
右子节点的索引是
2 * i + 2
父节点的索引是
(i - 1) // 2
//
是地板除法,也称为整数除法,表示结果只取商的整数部分,忽略小数。 -
为何用递归
- 树形结构非常适合用递归
- 二叉树的每个节点都可以看作是一个小的二叉树。一个节点的左子树和右子树本身就是两棵独立的二叉树,翻转一棵树就是翻转它的左右子树,这个问题可以被分解为多个相同的子问题。
- 二叉树的叶子节点的子节点都是
null
,这为递归提供了明确的终止条件——当遇到null
节点时停止递归
- 代码
function invertTree(root) {
// 如果当前节点为空,返回 null
if (root === null) {
return null;
}
// 递归翻转左右子树
const left = invertTree(root.left);
const right = invertTree(root.right);
// 交换左右子树
root.left = right;
root.right = left;
// 返回翻转后的根节点
return root;
}
- 先序遍历二叉树,并输出数组
前序遍历二叉树是指按照 根节点 -> 左子树 -> 右子树 的顺序进行遍历
function preorderTraversal(root) {
const result = [];
function traverse(node) {
if (node === null) {
return;
}
// 访问根节点
result.push(node.val);
// 前序遍历左子树
traverse(node.left);
// 前序遍历右子树
traverse(node.right);
}
traverse(root);
return result;
}
- ts 定义BST
二叉搜索树(BST)是一种二叉树结构,其中每个节点的左子树节点值小于该节点,右子树节点值大于该节点。
中序遍历 BST 可以输出有序的数据,常用于排序算法
class TreeNode<T> {
val: T;
left: TreeNode<T> | null;
right: TreeNode<T> | null;
constructor(val: T, left: TreeNode<T> | null = null, right: TreeNode<T> | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
class BinarySearchTree<T> {
private root: TreeNode<T> | null = null;
// 插入节点
insert(val: T): void {
const newNode = new TreeNode(val);
if (this.root === null) {
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}
private insertNode(node: TreeNode<T>, newNode: TreeNode<T>): void {
if (newNode.val < node.val) {
if (node.left === null) {
node.left = newNode;
} else {
this.insertNode(node.left, newNode);
}
} else {
if (node.right === null) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
}
// 搜索节点
search(val: T): boolean {
return this.searchNode(this.root, val);
}
private searchNode(node: TreeNode<T> | null, val: T): boolean {
if (node === null) {
return false;
}
if (val < node.val) {
return this.searchNode(node.left, val);
} else if (val > node.val) {
return this.searchNode(node.right, val);
} else {
return true; // 找到节点
}
}
// 前序遍历
preorderTraversal(): T[] {
const result: T[] = [];
this.preorder(this.root, result);
return result;
}
private preorder(node: TreeNode<T> | null, result: T[]): void {
if (node !== null) {
result.push(node.val);
this.preorder(node.left, result);
this.preorder(node.right, result);
}
}
// 中序遍历
inorderTraversal(): T[] {
const result: T[] = [];
this.inorder(this.root, result);
return result;
}
private inorder(node: TreeNode<T> | null, result: T[]): void {
if (node !== null) {
this.inorder(node.left, result);
result.push(node.val);
this.inorder(node.right, result);
}
}
// 后序遍历
postorderTraversal(): T[] {
const result: T[] = [];
this.postorder(this.root, result);
return result;
}
private postorder(node: TreeNode<T> | null, result: T[]): void {
if (node !== null) {
this.postorder(node.left, result);
this.postorder(node.right, result);
result.push(node.val);
}
}
}
const bst = new BinarySearchTree<number>(); bst.insert(10); bst.insert(6); bst.insert(15); bst.insert(3); bst.insert(8); bst.insert(20); console.log(bst.preorderTraversal()); // 输出: [10, 6, 3, 8, 15, 20] console.log(bst.inorderTraversal()); // 输出: [3, 6, 8, 10, 15, 20] console.log(bst.postorderTraversal()); // 输出: [3, 8, 6, 20, 15, 10] console.log(bst.search(15)); // 输出: true console.log(bst.search(100)); // 输出: false
买卖股票最佳时机 ||
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
贪心算法:遍历整个价格数组,如果当前价格高于前一天的价格,就计算差价并将其加入总利润中。
function maxProfit(prices) {
let totalProfit = 0;
for (let i = 1; i < prices.length; i++) {
if (prices[i] > prices[i - 1]) {
totalProfit += prices[i] - prices[i - 1];
}
}
return totalProfit;
}
工程化用过哪些插件?
箭头函数的指向
1. 箭头函数的定义
箭头函数 (=>
) 是 ES6 引入的一种更简洁的函数表达方式。与普通函数不同,它没有自己的 this
,而是继承外部上下文的 this
。
2. 箭头函数与普通函数的区别
普通函数在调用时,this
的值取决于调用方式,比如通过对象调用、绑定函数、全局调用等。而箭头函数不会根据调用方式改变 this
。
3. 箭头函数的 this
指向
箭头函数的 this
在定义时就确定了,取决于其定义时的外部上下文,不会因为调用方式或位置的不同而改变。也就是说,箭头函数的 this
是 静态的,绑定到定义时的上下文。
4. 使用场景:避免 this
的动态变化
在某些情况下,普通函数中的 this
会根据调用方式发生变化,这可能导致意外的行为。使用箭头函数时,可以避免 this
被重新绑定,特别是在回调函数、事件处理程序等场景中。
在你的例子中,箭头函数在 `obj` 对象的上下文中定义,但**它并没有绑定到 `obj`**,而是继承了外部作用域的 `this`。
const obj = { name: 'Alice', regularFunc: function () { console.log('regularFunc this:', this.name); // 指向 obj,因为通过 obj 调用 }, arrowFunc: () => { console.log('arrowFunc this:', this.name); // undefined,箭头函数的 this 指向全局或其定义时的上下文 }, };
cors 跨域的底层
CORS(跨域资源共享,Cross-Origin Resource Sharing)的底层机制时,可以从以下几个关键点来详细解释,包括浏览器的安全策略、预检请求、响应头的作用等。以下是逐步解析:
1. 什么是跨域?
跨域是指当浏览器在一个域名(origin
)下的页面向另一个不同域名、协议或端口(跨源)的服务器请求资源时,会触发浏览器的同源策略(Same-Origin Policy) 。同源策略是一种浏览器安全机制,目的是为了防止不同源的网页之间互相访问数据,造成潜在的安全问题。
2. CORS 的出现
CORS 是为了解决跨源请求的限制而引入的机制,它允许服务器声明哪些源可以访问服务器的资源。通过设置特定的 HTTP 响应头,服务器可以明确授权某些域名可以访问资源,从而突破同源策略的限制。
3. 底层机制
CORS 的核心在于浏览器通过请求和响应头来判断是否允许跨域访问,整个流程包括以下步骤:
1. 简单请求(Simple Request)
如果浏览器的请求满足以下条件,则属于简单请求:
- 请求方法是
GET
、POST
或HEAD
。 - 请求头只包含
Accept
、Accept-Language
、Content-Language
、Content-Type
(且Content-Type
的值是application/x-www-form-urlencoded
、multipart/form-data
或text/plain
)。
浏览器发送请求后,服务器通过设置 Access-Control-Allow-Origin
响应头来允许跨域访问。
关键响应头:
Access-Control-Allow-Origin
: 指定允许的源,比如*
或特定域名。Access-Control-Allow-Credentials
: 是否允许发送凭证(如 Cookies)。
浏览器会检查这些响应头,如果条件满足,允许跨域资源访问。
2. 预检请求(Preflight Request)
对于复杂请求(如使用 PUT
、DELETE
等方法,或者带有自定义的请求头),浏览器会在正式发送请求之前,先发送一个 预检请求(OPTIONS) ,以确定服务器是否允许该请求。这是为了保护服务器免受意外的影响,比如某些请求方法可能会改变服务器状态。
预检流程:
- 浏览器先发出一个
OPTIONS
请求,询问服务器是否允许跨源访问,主要询问请求方法、请求头等是否被允许。 - 如果服务器允许,会返回带有以下响应头的信息。
预检请求的关键响应头:
Access-Control-Allow-Origin
: 指定允许访问的域名。Access-Control-Allow-Methods
: 允许的 HTTP 请求方法,如GET
、POST
、PUT
、DELETE
。Access-Control-Allow-Headers
: 允许的自定义请求头(如X-Custom-Header
)。Access-Control-Max-Age
: 指定预检请求的结果可以被缓存的时间,减少预检请求的频率。
3. 带凭证的请求
如果客户端发送带有凭证的请求(如带有 Cookies),则服务器必须明确允许凭证传递:
- 浏览器设置请求的
withCredentials
属性为true
。 - 服务器必须在响应头中设置
Access-Control-Allow-Credentials: true
,并且Access-Control-Allow-Origin
不能为通配符*
,而必须指定具体的源。
4. 实际响应中的 CORS 验证流程
- 浏览器根据同源策略发现跨源请求,检查是否符合 CORS 规范。
- 浏览器自动发起预检请求(如果是复杂请求)。
- 服务器通过设置 CORS 相关响应头来允许或拒绝跨源请求。
- 浏览器根据服务器响应头决定是否允许跨域数据的返回。
5. CORS 相关的 HTTP 响应头总结
Access-Control-Allow-Origin
: 允许的来源,可以是具体的域名或*
(允许所有域名)。Access-Control-Allow-Methods
: 允许的 HTTP 方法,比如GET
、POST
、PUT
。Access-Control-Allow-Headers
: 允许的请求头,比如Content-Type
、Authorization
。Access-Control-Allow-Credentials
: 是否允许请求带上凭证(如 Cookies)。Access-Control-Expose-Headers
: 允许客户端访问的自定义响应头。Access-Control-Max-Age
: 预检请求的结果可以缓存多长时间。
6. 安全性和限制
- CORS 并不保证完全的安全性,只是提供了一种授权机制。如果服务器错误配置 CORS,可能导致跨域漏洞(如误设置
Access-Control-Allow-Origin: *
且允许凭证传递)。 - 浏览器只会允许跨域请求时服务器显式允许的资源,未授权的资源无法访问。
面试时总结回答:
CORS 是基于浏览器同源策略的一个机制,它通过 HTTP 请求和响应头的协作,使得服务器可以控制哪些源可以访问其资源。浏览器会根据请求的类型(简单请求或复杂请求)决定是否需要发送预检请求,并通过响应头来确认请求是否被允许。
闭包存在于哪里
闭包存在于函数的词法环境中。当一个函数被定义时,它会捕获并保留它所在的作用域中的变量,即使这个函数在其外部作用域被执行,依然可以访问那些被捕获的变量。因此,闭包存在于函数和它外部作用域之间的关联中。
词法环境是指在代码编写时确定的变量和函数的可访问范围,定义了在特定作用域中可以访问哪些标识符。
事件循环
事件循环(Event Loop)是 JavaScript 的一个机制,用于管理和调度异步操作。它基于单线程模型,通过维护一个调用栈(Call Stack)和一个任务队列(Task Queue)来协调代码的执行。事件循环的主要流程如下:
-
执行栈:同步代码首先被推入调用栈中逐步执行,直到栈为空。
-
任务队列:异步任务(如
setTimeout
、Promise
的then
、fetch
等)在执行时不会直接进入栈,而是被放入任务队列中。 -
事件循环:事件循环不断地从任务队列中取出任务,将其推入调用栈执行。只有在调用栈为空时,事件循环才会处理任务队列中的任务。
-
微任务和宏任务:
- 微任务(Microtasks) :通常指
Promise
的then
回调和MutationObserver
,它们在当前任务执行完后、渲染前被优先处理。 - 宏任务(Macrotasks) :包括
setTimeout
、setInterval
和 I/O 操作等,它们在微任务队列处理完后被处理。
- 微任务(Microtasks) :通常指
-
渲染和更新:事件循环还会在适当的时候进行渲染更新,确保界面的刷新和用户的交互不会被阻塞。
通过这一机制,JavaScript 实现了非阻塞的异步编程模式,允许代码在等待异步操作完成的同时继续执行其他任务,提高了应用的响应性和性能。
这种回答不仅解释了事件循环的基本概念,还涵盖了微任务和宏任务的区别,以及它如何影响 JavaScript 的执行和性能。
keep-alive 怎么工作的
keep-alive
是 Vue 3 中用于缓存组件状态的一个高阶组件,它的工作机制如下:
-
基本概念:
keep-alive
是 Vue 提供的一个功能,用于包裹动态组件,使其在组件切换时能够缓存组件的状态,而不是销毁和重新创建。
-
工作原理:
- 当一个组件被包裹在
keep-alive
中时,Vue 会对该组件的实例进行缓存,当组件从视图中移除时,它的状态和 DOM 不会被销毁,而是被保留在内存中。 - 当组件再次被激活时,Vue 会从缓存中恢复组件的状态,而不是重新创建一个新的组件实例。
- 当一个组件被包裹在
-
使用方式:
vue 复制代码 <template> <keep-alive> <component :is="currentComponent"></component> </keep-alive> </template> <script> export default { data() { return { currentComponent: 'MyComponent' }; }, components: { MyComponent, AnotherComponent } }; </script>
-
主要特性:
-
缓存机制:
keep-alive
会缓存组件的实例,并在组件切换时保留其状态。 -
生命周期钩子:
keep-alive
提供了activated
和deactivated
生命周期钩子,分别在组件被激活和停用时触发,允许开发者在这些阶段执行额外的操作。 -
条件缓存:通过
include
和exclude
属性,可以控制缓存哪些组件。例如:vue 复制代码 <keep-alive :include="['ComponentA']"> <component :is="currentComponent"></component> </keep-alive>
-
-
缓存策略:
include
和exclude
:可以通过这些属性来控制哪些组件应被缓存或排除在缓存之外。include
是一个字符串或正则表达式,指定要缓存的组件名;exclude
则是指定不缓存的组件名。
-
应用场景:
- 页面切换:在有多个页面或视图切换的应用中,
keep-alive
可用于缓存已访问的页面,以提高用户体验并减少不必要的渲染和数据加载。 - 复杂组件:对于状态复杂、计算开销大的组件,
keep-alive
可以显著提升性能,避免每次切换都进行初始化。
- 页面切换:在有多个页面或视图切换的应用中,
总结:keep-alive
是 Vue 3 中用于缓存组件实例的高阶组件,能够有效地保留组件状态并提高性能,通过其内建的缓存机制和生命周期钩子,帮助开发者更灵活地管理组件的生命周期和状态。
这种回答展示了对 keep-alive
的深入理解,涵盖了其基本概念、工作原理、特性、缓存策略和实际应用场景。
http keep-alive
HTTP keep-alive
的作用
-
提高性能:
keep-alive
允许在一个持久的 TCP 连接上发送多个 HTTP 请求和响应,而不是为每个请求重新建立和关闭连接。这减少了连接建立的开销(如三次握手和四次挥手),从而提高了性能和响应速度。
-
减少延迟:
- 由于在同一连接上进行多个请求,可以减少每次请求的延迟,特别是在需要发送多个小请求的情况下,
keep-alive
减少了重新建立连接的时间。
- 由于在同一连接上进行多个请求,可以减少每次请求的延迟,特别是在需要发送多个小请求的情况下,
-
节省资源:
- 减少了服务器和客户端在连接建立和关闭过程中所消耗的计算和网络资源,尤其在高并发情况下,能够显著降低资源消耗。
keep-alive
的工作机制
-
基本原理:
- 在 HTTP/1.0 中,每个请求和响应都需要一个新的 TCP 连接,
keep-alive
在 HTTP/1.1 引入,作为默认机制,允许持久连接,即同一个 TCP 连接可以处理多个请求和响应。
- 在 HTTP/1.0 中,每个请求和响应都需要一个新的 TCP 连接,
-
客户端请求:
-
客户端通过在请求头中添加
Connection: keep-alive
告诉服务器希望保持连接持续开放。例如:vbnet 复制代码 GET /index.html HTTP/1.1 Host: example.com Connection: keep-alive
-
-
服务器响应:
-
服务器可以在响应头中设置
Connection: keep-alive
和Keep-Alive
头部来指示保持连接的状态,以及连接的相关参数,例如:makefile 复制代码 HTTP/1.1 200 OK Connection: keep-alive Keep-Alive: timeout=5, max=100
timeout
:连接保持活跃的时间(秒),如果在指定时间内没有新的请求,连接将被关闭。max
:允许的最大请求数,在达到这个数目后,连接将被关闭。
-
-
连接复用:
- 在
keep-alive
模式下,客户端可以继续使用已建立的 TCP 连接发送后续请求,服务器在处理完当前请求后保持连接打开,等待更多请求。
- 在
-
超时和关闭:
- 如果连接在指定的
timeout
时间内没有收到新的请求,连接将被自动关闭,以释放资源。客户端和服务器都可以在Keep-Alive
头部中配置这个超时时间和最大请求数。
- 如果连接在指定的
总结
- 作用:
keep-alive
通过在一个持久的 TCP 连接上处理多个请求和响应,提高了性能,减少了延迟和资源消耗。 - 工作机制:客户端通过
Connection: keep-alive
请求持久连接,服务器在响应中使用Connection: keep-alive
和Keep-Alive
头部来管理连接的状态和参数,允许连接复用,并在指定时间或请求数后关闭连接。
这种回答不仅解释了 keep-alive
的作用和工作机制,还涵盖了实际的 HTTP 请求和响应头部示例,帮助面试官更好地理解其实际应用。
js 的基本类型
JavaScript 的基本数据类型分为原始数据类型和对象数据类型。原始数据类型包括:
-
Undefined
:- 只有一个值
undefined
,表示一个变量已声明但未赋值。它是 JavaScript 中的默认值,用于表示缺失或未初始化的状态。
- 只有一个值
-
Null
:- 只有一个值
null
,表示“无”或“空”的对象引用。它通常用于表示预期对象缺失的情况,并且是一个用来显式赋值给变量的类型。
- 只有一个值
-
Boolean
:- 代表布尔值
true
或false
。它是逻辑值的基础,广泛用于条件判断和控制流。
- 代表布尔值
-
Number
:- 用于表示整数和浮点数,符合 IEEE 754 标准的双精度浮点数。由于浮点数精度问题,
Number
类型有一定的精度限制,这在涉及高精度计算时需要特别注意。
- 用于表示整数和浮点数,符合 IEEE 754 标准的双精度浮点数。由于浮点数精度问题,
-
BigInt
:- 用于表示任意精度的整数,解决了
Number
类型在处理大整数时的精度问题。BigInt
可以表示大于2^53-1
的整数,并且可以用于高精度的数学计算。
- 用于表示任意精度的整数,解决了
-
String
:- 用于表示文本数据。它是 Unicode 编码的字符序列,支持各种字符串操作,如切割、替换和连接等。
-
Symbol
:- 用于创建唯一且不可变的标识符。每个
Symbol
值都是唯一的,用于对象属性的唯一标识,避免属性名冲突。
- 用于创建唯一且不可变的标识符。每个
-
Object
(虽然通常不算作基本数据类型,但理解其与基本数据类型的关系是重要的):- 包括普通对象、数组、函数、日期等,作为复合数据类型,
Object
用于存储和管理键值对。
- 包括普通对象、数组、函数、日期等,作为复合数据类型,
底层实现与性能
- 内存管理:基本数据类型的值在栈内存中存储,而对象类型通常存储在堆内存中。基本数据类型的值具有固定大小和快速访问的特性,而对象类型由于其动态性和复杂性,可能会带来更高的内存开销和访问延迟。
- 不可变性:原始数据类型(除了对象外)都是不可变的。这意味着对这些类型的任何操作都会创建一个新的值,而不是修改原有值。对比之下,
Object
是可变的,意味着你可以修改对象的属性而不需要重新分配整个对象。
现代 JavaScript 的应用
- 原始数据类型的特性:了解 JavaScript 的原始数据类型及其不可变性对编写高效且无副作用的代码至关重要。例如,函数式编程中重视不可变性来减少副作用和错误。
- 大数处理:
BigInt
的引入使得 JavaScript 可以更好地处理大数运算,这对涉及金融、加密等领域的应用程序尤为重要。 Symbol
的使用:Symbol
可以用作对象属性的唯一标识符,避免了属性名冲突,尤其在大型应用和库开发中尤为有用。- 性能考虑:了解不同数据类型的内存和性能特性,可以帮助开发者在处理数据时做出更合适的选择,特别是在性能敏感的场景下。
这种回答不仅涵盖了基本数据类型的定义,还深入探讨了它们的底层实现和在现代 JavaScript 应用中的实际作用,使面试官能够看到你对这些概念的深刻理解。
普通函数和箭头函数的区别
可以从以下几个高级方面来讨论,包括它们的语法差异、this
绑定、性能考虑以及使用场景。以下是一个详细的回答示例:
箭头函数 vs. 普通函数的高级比较
1. this
绑定
-
箭头函数:
-
词法绑定:箭头函数不会有自己的
this
值,它会捕获其定义时的上下文中的this
,即词法作用域中的this
。这种特性使得箭头函数在回调函数或事件处理器中可以保留外部函数的this
。 -
示例:
javascript 复制代码 class Counter { count = 0; increment() { setTimeout(() => { console.log(this.count); // `this` 指向 Counter 实例 }, 1000); } } new Counter().increment();
-
-
普通函数:
-
动态绑定:普通函数的
this
是在函数调用时动态确定的,它取决于函数的调用方式。通常在方法调用或构造函数中this
会指向调用该函数的对象。 -
示例:
javascript 复制代码 class Counter { count = 0; increment() { setTimeout(function() { console.log(this.count); // `this` 在这里指向 setTimeout 的上下文 }, 1000); } } new Counter().increment(); // `this.count` 为 undefined
-
2. 构造函数
-
箭头函数:
-
不能作为构造函数:箭头函数没有
[[Construct]]
方法,因此不能用作构造函数,也不能使用new
关键字来实例化。 -
示例:
javascript 复制代码 const Person = (name) => { this.name = name; // 错误:箭头函数不能用作构造函数 }; const person = new Person('Alice'); // 报错
-
-
普通函数:
-
可以作为构造函数:普通函数可以作为构造函数使用,允许使用
new
关键字来创建实例,并将this
绑定到新创建的对象上。 -
示例:
javascript 复制代码 function Person(name) { this.name = name; } const person = new Person('Alice'); // 正确 console.log(person.name); // Alice
-
3. arguments
对象
-
箭头函数:
-
没有
arguments
对象:箭头函数不具有arguments
对象,它没有自己的arguments
,只能访问外层作用域的arguments
。 -
示例:
javascript 复制代码 function foo() { const bar = () => { console.log(arguments); // `arguments` 指向 foo 的 arguments }; bar(); } foo(1, 2, 3); // 输出 [1, 2, 3]
-
-
普通函数:
-
具有
arguments
对象:普通函数有自己的arguments
对象,它是一个类数组对象,包含函数调用时的所有参数。 -
示例:
javascript 复制代码 function foo() { console.log(arguments); // 输出所有参数 } foo(1, 2, 3); // 输出 [1, 2, 3]
-
4. super
关键字
-
箭头函数:
-
不能用作
super
的调用:由于箭头函数没有自己的this
,也无法调用super
,因此不能在箭头函数中使用super
。 -
示例:
javascript 复制代码 class Parent { hello() { console.log('Hello from Parent'); } } class Child extends Parent { hello() { const arrowHello = () => { super.hello(); // 错误:箭头函数不能调用 super }; arrowHello(); } } new Child().hello();
-
-
普通函数:
-
可以调用
super
:普通函数可以调用super
,用于在子类中访问父类的方法和属性。 -
示例:
javascript 复制代码 class Parent { hello() { console.log('Hello from Parent'); } } class Child extends Parent { hello() { super.hello(); // 正确:调用父类的方法 console.log('Hello from Child'); } } new Child().hello();
-
5. 性能和优化
-
箭头函数:
- 语法简洁:箭头函数具有简洁的语法,适合用作短小的回调函数或匿名函数。
- 潜在的内存开销:由于箭头函数捕获其定义时的
this
,在某些情况下可能会导致不必要的内存开销。
-
普通函数:
- 灵活性:普通函数提供了更多灵活性,可以作为构造函数,动态绑定
this
,以及使用arguments
对象。 - 优化:在某些优化和性能方面,普通函数可能会由于其更多的功能和灵活性稍微复杂,但对于一些高级用例和大规模应用,灵活性和控制力通常是有益的。
- 灵活性:普通函数提供了更多灵活性,可以作为构造函数,动态绑定
这种回答不仅明确地列出了箭头函数和普通函数的主要区别,还深入讨论了它们在实际编程中的应用场景和潜在的性能影响,展示了对 JavaScript 函数特性的深入理解。
写一个类实现异步函数的顺序调用
class AsyncQueue {
private queue: (() => Promise<void>)[] = [];
// 添加异步函数到队列
add(asyncFn: () => Promise<void>): void {
this.queue.push(asyncFn);
}
// 执行队列中的所有异步函数
async run(): Promise<void> {
for (const fn of this.queue) {
await fn(); // 确保异步函数按顺序执行
}
}
}
// 使用示例
async function example() {
const asyncQueue = new AsyncQueue();
asyncQueue.add(async () => {
console.log('First');
await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟异步操作
});
asyncQueue.add(async () => {
console.log('Second');
await new Promise(resolve => setTimeout(resolve, 500)); // 模拟异步操作
});
asyncQueue.add(async () => {
console.log('Third');
await new Promise(resolve => setTimeout(resolve, 300)); // 模拟异步操作
});
await asyncQueue.run(); // 执行队列中的异步函数
console.log('All done');
}
example();
postMessage 可以传什么数据类型
postMessage
方法可以传递的数据类型
postMessage
是一种在不同窗口(如 iframe、弹出窗口、不同的浏览器标签页)或不同的上下文(如 Web Workers)之间进行安全通信的机制。它的参数是通过 postMessage
发送的消息。可以传递的数据类型包括:
-
基本数据类型:
string
:文本字符串。number
:数字。boolean
:布尔值 (true
或false
)。
javascript 复制代码 window.postMessage('Hello World', '*'); // 发送字符串 window.postMessage(123, '*'); // 发送数字 window.postMessage(true, '*'); // 发送布尔值
-
对象和数组:
object
:普通对象,postMessage
会自动将对象序列化为 JSON 格式。注意,这里的对象应为序列化能够处理的类型,例如不包含函数、不可序列化的 DOM 元素等。array
:数组,同样会被序列化为 JSON 格式。
javascript 复制代码 window.postMessage({ type: 'UPDATE', payload: 'Data' }, '*'); // 发送对象 window.postMessage([1, 2, 3], '*'); // 发送数组
-
structuredClone
支持的类型:ArrayBuffer
:用于处理原始二进制数据。Blob
:表示不可变的原始数据的类,通常用于文件操作。File
:表示文件对象。ImageBitmap
:图像位图对象,可以用于在 Canvas 上绘制图像。MessagePort
:用于在不同上下文之间传递消息端口,用于双向通信。
javascript 复制代码 const buffer = new ArrayBuffer(8); window.postMessage(buffer, '*'); // 发送 ArrayBuffer const blob = new Blob(['Hello World'], { type: 'text/plain' }); window.postMessage(blob, '*'); // 发送 Blob const port = new MessageChannel().port1; window.postMessage(port, '*'); // 发送 MessagePort
注意事项
- 数据序列化:
postMessage
自动对发送的数据进行序列化和反序列化,使用 JSON 方法处理大多数数据类型,但有些复杂的类型,如function
或循环引用的对象,不能正确序列化。 - 安全性:
postMessage
方法的第二个参数是目标源,建议总是指定明确的源(如'https://example.com'
)来增强安全性,防止数据泄露。 - 跨源限制:虽然
postMessage
允许跨源通信,但要确保接收方验证消息的来源和内容,以防止跨站脚本攻击(XSS)等安全问题。
总结
postMessage
方法支持多种数据类型的传递,包括基本数据类型、对象、数组以及 structuredClone
支持的类型(如 ArrayBuffer
和 Blob
)。通过正确使用这些数据类型,可以实现安全和有效的跨窗口或跨上下文的通信。
这种回答不仅覆盖了 postMessage
支持的数据类型,还包含了数据序列化、安全性和跨源限制等方面的考虑,展示了对 postMessage
方法的深入理解。
webscoket 怎么跨域的
在面试中回答 WebSocket 如何实现跨域时,可以从以下几个方面来详细说明,包括 WebSocket 的工作机制、跨域的处理方式以及与传统的 HTTP 跨域处理的区别。以下是一个详细的回答示例:
WebSocket 实现跨域的机制
1. WebSocket 连接的基本机制
- 协议:WebSocket 协议(
ws://
或wss://
)在建立连接时通过一个握手过程(HTTP 升级请求)来创建持久的双向连接。一旦连接建立,WebSocket 允许客户端和服务器之间进行全双工通信。 - 握手过程:WebSocket 的握手请求最初是一个普通的 HTTP 请求,其中包含
Upgrade: websocket
头部,通知服务器客户端希望将连接升级到 WebSocket 协议。
2. 跨域问题
- 初始握手:WebSocket 的跨域问题主要出现在握手阶段。WebSocket 协议本身没有类似于 HTTP 的“同源策略”来限制跨域请求。在 WebSocket 握手过程中,客户端可以向任何域名的服务器发起连接请求。
- 跨域限制:虽然 WebSocket 协议本身不限制跨域,但浏览器对跨域 WebSocket 连接有一些限制,例如同源策略仍然适用 HTTP 协议部分。在握手期间,浏览器会发送带有
Origin
头的请求,服务器可以使用此头部来检查和验证请求来源。
3. 实现跨域
-
服务器端设置:在服务器端,需要处理 WebSocket 握手请求中的
Origin
头,以验证请求是否来自允许的来源。如果允许跨域,服务器应接受来自不同来源的 WebSocket 请求。-
示例(Node.js +
ws
):javascript 复制代码 const WebSocket = require('ws'); const server = new WebSocket.Server({ port: 8080 }); server.on('connection', (ws, req) => { const origin = req.headers.origin; if (origin !== 'https://allowed-origin.com') { ws.close(4000, 'Origin not allowed'); // 关闭连接,并发送关闭代码 return; } ws.on('message', (message) => { console.log(`Received message => ${message}`); }); ws.send('Hello from server'); });
-
-
客户端设置:客户端的 JavaScript 代码可以直接使用 WebSocket API 创建连接,无需特殊设置来处理跨域问题,只要服务器正确处理了握手请求。
-
示例:
javascript 复制代码 const ws = new WebSocket('wss://example.com/socket'); ws.onopen = () => { console.log('WebSocket connection established'); }; ws.onmessage = (event) => { console.log('Message from server', event.data); }; ws.onerror = (error) => { console.error('WebSocket error', error); };
-
4. 安全性
- CORS 不适用:与传统的 HTTP 请求不同,WebSocket 不使用 CORS(跨源资源共享)来处理跨域问题,因为 CORS 是针对 HTTP 请求的,而 WebSocket 协议在握手阶段之外没有同样的机制。
- 安全考虑:尽管 WebSocket 协议不受 CORS 限制,仍需注意跨域安全。服务器端需要对
Origin
头进行验证,并采取适当措施来防止恶意请求。
总结
- 跨域机制:WebSocket 协议允许跨域连接,但在握手阶段,浏览器会通过
Origin
头部传递请求来源,服务器可以根据这个头部进行验证和控制。 - 实现方式:在服务器端需要检查
Origin
头部,决定是否接受跨域连接。客户端只需建立 WebSocket 连接,服务器端的安全配置决定了跨域的处理方式。 - 安全性:WebSocket 不使用 CORS,但服务器需要采取适当的措施来验证和管理跨域连接,以确保安全性。
这种回答不仅涵盖了 WebSocket 的基本跨域机制,还讨论了如何在服务器端实现跨域验证,以及相关的安全考虑,展示了对 WebSocket 跨域问题的全面理解。
setTimeout 是在哪里执行的
-
执行位置:
setTimeout
的回调函数会在延迟时间到达后被推送到宏任务队列中。事件循环会在处理完当前执行栈中的同步代码和微任务队列中的任务后,执行宏任务队列中的任务。 -
任务队列:
setTimeout
属于宏任务,而微任务(如Promise
的回调)在宏任务之前执行。事件循环确保了任务的有序执行和异步操作的协调。
矩阵中找值
在面试中,寻找矩阵中的特定值是一个常见的问题。以下是一个示例代码,展示了如何在二维矩阵中查找特定值。此示例使用 TypeScript,并提供了简单的矩阵查找功能。
示例:在二维矩阵中查找特定值
假设我们有一个二维矩阵 matrix
,我们需要查找一个目标值 target
。这里提供两种方法:线性扫描和优化的二分查找方法。
1. 线性扫描
最简单的方法是线性扫描每个元素。此方法时间复杂度为 O(m * n),其中 m 是矩阵的行数,n 是矩阵的列数。
typescript
复制代码
function findInMatrix(matrix: number[][], target: number): boolean {
for (let row of matrix) {
for (let value of row) {
if (value === target) {
return true;
}
}
}
return false;
}
// 使用示例
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
const target = 5;
console.log(findInMatrix(matrix, target)); // 输出: true
2. 优化的二分查找
如果矩阵的每行和每列都按升序排序,可以使用优化的二分查找算法。此方法时间复杂度为 O(m + n),其中 m 是矩阵的行数,n 是矩阵的列数。
typescript
复制代码
function findInSortedMatrix(matrix: number[][], target: number): boolean {
if (matrix.length === 0 || matrix[0].length === 0) return false;
const rows = matrix.length;
const cols = matrix[0].length;
let row = 0;
let col = cols - 1;
while (row < rows && col >= 0) {
const current = matrix[row][col];
if (current === target) {
return true;
} else if (current < target) {
row++; // 向下移动
} else {
col--; // 向左移动
}
}
return false;
}
// 使用示例
const sortedMatrix = [
[1, 4, 7, 11],
[2, 5, 8, 12],
[3, 6, 9, 16],
[10, 13, 14, 17]
];
const targetSorted = 5;
console.log(findInSortedMatrix(sortedMatrix, targetSorted)); // 输出: true
总结
- 线性扫描:适用于任意二维矩阵,时间复杂度为 O(m * n)。
- 优化的二分查找:适用于每行和每列都按升序排序的矩阵,时间复杂度为 O(m + n),效率更高。
根据矩阵的特性和需求,可以选择适合的查找方法。