每日一题 - 202404
4-30
如何理解 ES6 的 Reflect 对象?
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。
与大多数全局对象不同 Reflect 并非一个构造函数,所以不能通过 new 运算符对其进行调用, 或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是静态的(就像 Math 对象)。
常用方法:
Reflect.apply(target, thisArgument, argumentsList)
调用函数,并传入一个数组作为参数Reflect.get(target, propertyKey[, receiver])
获得对象某个属性的值Reflect.has(target, propertyKey)
判断对象是否存在某个属性Reflect.ownKeys(target);
返回对象自身的属性Reflect.set(target, propertyKey, value[, receiver])
给对象添加一个属性。返回 boolean
4-29
如何理解 ES6 的 Proxy 对象?
ES6 用于创建一个对象的代理,从而实现基本操作的拦截和自定义。 const p = new Proxy(target, handler)
, 其中,target 表示需要包装的目标对象,可以是任意类型的对象。handler 通常为函数,用于定义代理对象 p 的行为。
const handler = {
get: function (obj, prop) {
return prop in obj ? obj[prop] : 37;
},
};
const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log("c" in p, p.c); // false, 37
4-28
如何理解 ES6 的 Promise 对象?
new Promise((resolve, reject) => {
doSomeThing()
return
})
.then(result => {/*正常运行后。。。*/})
.catch(error => {/*报错时。。。*/})
.finally(() => {/*结束时操作。。。*/});
Promise 是异步编程的一种解决方案,用于替代回调函数,更加强大和合理。
传统的回调函数在多个回调中,代码结构会嵌套多层,极大的增加的阅读难度。
Promise 对象通过链式操作降低了代码难度并极大的加强了代码的可读性。
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('得到最终结果: ' + finalResult);
})
.catch(failureCallback);
4-27
什么是 SPA?
SPA 指 single-page application(单页面应用),通过动态重写当前页面 DOM 与用户进行交互,避免了页面之间切换打断用户体验。
4-26
常用的跨域解决方案有哪些
(1)CORS:
跨域资源共享(Cross-Origin Resource Sharing),是一种允许浏览器向跨域服务器发送 Ajax 请求的机制,支持现代浏览器,服务器端需要设置 Access-Control-Allow-Origin 头信息,指定允许的源或通配符,从而实现跨域请求。
(2)代理:
在同源页面内部发送 AJAX 请求到同域服务器,由服务器代理转发请求到跨域服务器,最后再将结果返回给同源页面。
(3)WebSocket:
WebSocket 是一种 HTML5 协议,它使得浏览器和服务器之间可以建立持久化的连接,可以直接使用 Socket 进行通信,避免了浏览器的跨域限制。
4-25
vue 中的 spa 引用如何优化首屏加载速度
- 请求优化:
CDN
将第三方的类库放到 CDN 上,能够大幅度减少生产环境中的项目体积,另外 CDN 能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。 - 缓存:将长时间不会改变的第三方类库或者静态资源设置为强缓存,将 max-age 设置为一个非常长的时间,再将访问路径加上哈希达到哈希值变了以后保证获取到最新资源,好的缓存策略有助于减轻服务器的压力,并且显著的提升用户的体验
- gzip:开启
gzip
压缩,通常开启 gzip 压缩能够有效的缩小传输资源的大小。 - http2:如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同域名的
tcp
连接数量是有限制的(chrome 为 6 个)超过规定数量的 tcp 连接,则必须要等到之前的请求收到响应后才能继续发送,而 http2 则可以在多个 tcp 连接中并发多个请求没有限制,在一些网络较差的环境开启 http2 性能提升尤为明显。 - 懒加载:当 url 匹配到相应的路径时,通过
import
动态加载页面组件,这样首屏的代码量会大幅减少,webpack 会把动态加载的页面组件分离成单独的一个 chunk.js 文件 - 预渲染:由于浏览器在渲染出页面之前,需要先加载和解析相应的 html、css 和 js 文件,为此会有一段白屏的时间,可以添加loading,或者骨架屏幕尽可能的减少白屏对用户的影响体积优化
- 合理使用第三方库:对于一些第三方 ui 框架、类库,尽量使用按需加载,减少打包体积
- 使用可视化工具分析打包后的模块体积:webpack-bundle- analyzer 这个插件在每次打包后能够更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化
- 提高代码使用率:利用代码分割,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程
- 封装:构建良好的项目架构,按照项目需求就行全局组件,插件,过滤器,指令,utils 等做一 些公共封装,可以有效减少我们的代码量,而且更容易维护资源优化
- 图片懒加载:使用图片懒加载可以优化同一时间减少 http 请求开销,避免显示图片导致的画面抖动,提高用户体验
- 使用 svg 图标:相对于用一张图片来表示图标,svg 拥有更好的图片质量,体积更小,并且不需要开启额外的 http 请求
- 压缩图片:可以使用 image-webpack-loader,在用户肉眼分辨不清的情况下一定程度上压缩图片
4-24
如何实现 vue 项目中的性能优化
(1)编码阶段
- 尽量减少 data 中的数据,data 中的数据都会增加 getter 和 setter,会收集对应的 watcher
- v-if 和 v-for 不能连用
- 如果需要使用 v-for 给每项元素绑定事件时使用事件代理
- SPA 页面采用 keep-alive 缓存组件
- 在更多的情况下,使用 v-if 替代 v-show
- key 保证唯一
- 使用路由懒加载、异步组件
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- 图片懒加载
(2)*SEO* 优化
- 预渲染
- 服务端渲染 SSR
(3)打包优化
- 压缩代码
- Tree Shaking/Scope Hoisting
- 使用 cdn 加载第三方模块
- 多线程打包 happypack
- splitChunks 抽离公共文件
- sourceMap 优化
(4)用户体验
- 骨架屏
- PWA
还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启 gzip 压缩等。
4-23
vue 修饰符
(1)事件修饰符
在事件处理程序中调用 event.preventDefault 或 event.stopPropagation 方法是非常常见的需求。尽管可以在 methods 中轻松实现这点,但更好的方式是:methods 只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
常见的事件修饰符如下:
- .stop:阻止冒泡。
- .prevent:阻止默认事件。
- .capture:使用事件捕获模式。
- .self:只在当前元素本身触发。
- .once:只触发一次。
- .passive:默认行为将会立即触发。
(2)按键修饰符
在 vue 中还提供了有鼠标修饰符,键值修饰符,系统修饰符等功能。
- .left:左键
- .right:右键
- .middle:滚轮
- .enter:回车
- .tab:制表键
- .delete:捕获 “删除” 和 “退格” 键
- .esc:返回
- .space:空格
- .up:上
- .down:下
- .left:左
- .right:右
- .ctrl:ctrl 键
- .alt:alt 键
- .shift:shift 键
- .meta:meta 键
(3)表单修饰符
常见的有 .lazy、 .number 和 .trim。
- .lazy:在文本框失去焦点时才会渲染
- .number:将文本框中所输入的内容转换为number类型
- .trim:可以自动过滤输入首尾的空格
4-22
nextTick 的作用
在下次 DOM 更新循环结束之后执行延迟回调。nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用
Promise MutationObserver setImmediate 如果以上都不行则采用setTimeout
定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。
4-21
v-if和v-show的区别
(1)共同点
v-show
与v-if
的作用效果是相同的(不含v-else),都能控制元素在页面是否显示用法上也是相同的:
- 当表达式为
true
的时候,都会占据页面的位置- 当表达式都为
false
时,都不会占据页面位置
(2)不同点
v-show
隐藏则是为该元素添加css--display:none
,dom
元素依旧还在。
v-if
显示隐藏是将dom
元素整个添加或删除。
v-if
有更高的切换消耗;v-show
有更高的初始渲染消耗;
4-20
vue组件的通信方式
(1)使用Props(属性):
父组件可以通过在子组件上绑定属性(props)来向子组件传递数据。
子组件通过在模板中使用props来访问这些属性。
这是一种父向子组件传递数据的单向通信方式。
父->子
props
,子->父$on、$emit
获取父子组件实例
$parent、$children
(2)使用自定义事件:
- 子组件可以触发自定义事件,而父组件可以监听这些事件。
- 这允许子组件向父组件发送消息。
- 这是一种子向父组件传递数据的方式。
(3)使用$refs
:
- 父组件可以通过
ref
属性引用子组件,并直接访问子组件的属性和方法。 - 这是一种直接的通信方式,但通常不推荐在多个子组件之间使用。
(4)使用Vuex(状态管理库):
- 如果父子组件之间的通信较复杂,或者涉及多个组件,可以使用Vuex来实现全局状态管理。
- Vuex允许不同组件共享数据,并通过触发和监听事件来进行通信。
这些方法中的选择取决于您的具体需求和组件之间的关系。在大多数情况下,使用Props和自定义事件是足够
的,但在更复杂的情况下,考虑使用Vuex或其他适当的通信模式。
4-19
vue的响应式原理
Vue在初始化数据时,会遍历组件的data对象,并使用Object.defineProperty
重新定义data中的所有属性,将其所有属性值转化为getter和setter的形式。当页面使用对应属性时,首先会进行依赖收集(收集当前组件的watcher
)如果属性发生变化会通知相关依赖进行更新操作。
Vue3.x改用Proxy
替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
Proxy只会代理对象的第一层,Vue3是怎样处理的?
判断当前Reflect.get的返回值是否为Object,如果是则再通过
reactive
方法做代理, 这样就实现了深度观测。监测数组的时候可能触发多次get/set,如何防止多次触发?
判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。
4-18
HashMap 和 Hashtable 的区别
- HashMap 是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。
- HashMap 是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap。
- HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。
- HashMap 存数据的过程是:HashMap内部维护了一个存储数据的Entry数组,HashMap 采用链表解决冲突,每一个Entry本质上是一个单向链表。当准备添加一个key-value对时,首先通过hash(key)方法计算hash值,然后通过indexFor(hash,length)求该key-value对的存储位置,计算方法是先用hash&0x7FFFFFFF后,再对length取模,这就保证每一个key-value对都能存入HashMap中,当计算出的位置相同时,由于存入位置是一个链表,则把这个key-value对插入链表头。
- HashMap 中key和value都允许为null。key为null的键值对永远都放在以
table[0]
为头结点的链表中。
4-17
力扣题库-1410 HTML 实体解析器
🔥 题目: 请实现一个 HTML 实体解析器,实体包括 "
,'
,&
,>
,<
和 ⁄
。
题解1
回家等通知写法
public String entityParser(String text) {
return text.replace(""", "\"")
.replace("'", "'")
.replace("&", "&")
.replace(">", ">")
.replace("<", "<")
.replace("⁄", "/");
}
题解2
public static String entityParser(String text) {
int p = 0;
StringBuilder stringBuilder = new StringBuilder();
while (p < text.length()) {
if (!"&".equals(text.substring(p, p + 1))) {
stringBuilder.append(text.charAt(p));
p++;
continue;
}
if (text.length() >= p + 6 && """.equals(text.substring(p, p + 6))) {
stringBuilder.append("\"");
p += 6;
continue;
}
if (text.length() >= p + 6 && "'".equals(text.substring(p, p + 6))) {
stringBuilder.append("'");
p += 6;
continue;
}
if (text.length() >= p + 5 && "&".equals(text.substring(p, p + 5))) {
stringBuilder.append("&");
p += 5;
continue;
}
if (text.length() >= p + 4 && ">".equals(text.substring(p, p + 4))) {
stringBuilder.append(">");
p += 4;
continue;
}
if (text.length() >= p + 4 && "<".equals(text.substring(p, p + 4))) {
stringBuilder.append("<");
p += 4;
continue;
}
if (text.length() >= p + 7 && "⁄".equals(text.substring(p, p + 7))) {
stringBuilder.append("/");
p += 7;
continue;
}
stringBuilder.append(text.charAt(p));
p++;
}
return stringBuilder.toString();
}
🔥 查看我的题解
4-16
什么是单调栈?
单调栈是指栈中的元素是单调递增或者单调递减的栈。主要应用场景是解决 Next Greater Element
问题,即找到数组中每个元素的下一个更大的元素。
单调栈的实现方式是使用栈来存储元素的索引,当遍历到一个新元素时,如果栈为空,则将元素的索引入栈;如果栈不为空,则比较栈顶元素和新元素的大小, 如果新元素大于栈顶元素,则将栈顶元素出栈,并将新元素的索引入栈,直到新元素小于栈顶元素。
4-15
力扣题库-121 买卖股票的最佳时机
🔥 题目: 给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
解法1 暴力法
这道题双重循环很快就可以解决,但是会超时:
时间复杂度:
空间复杂度:
。
public static int maxProfit(int[] prices) {
int x = 0;
for (int i = 0; i < prices.length; i++) {
for (int j = i+1; j < prices.length; j++) {
if (prices[j] > prices[i] ) {
if (prices[j] - prices[i] > x) {
x = prices[j] - prices[i];
}
}
}
}
return x;
}
方法二:动态规划
在看完官方题解的思路之后,我自己写了一遍,如下:
时间复杂度:
空间复杂度:
public static int maxProfit(int[] prices) {
int min = Integer.MAX_VALUE;
int max = 0;
for (int p : prices) {
if (p < min) {
min = p;
} else if (p - min > max) {
max = p - min;
}
}
return max;
}
4-14
HashMap get 方法的执行过程?
- 计算键的哈希值: 首先,HashMap 会通过键的 hashCode() 方法计算键的哈希值。哈希值是一个整数,用于确定键在 HashMap 中的位置。
- 确定存储位置: 使用哈希值确定键在 HashMap 的存储桶(buckets)中的位置。HashMap 通过对哈希值进行一些位运算,将其映射到存储桶的索引上。
- 检索键值对: 一旦确定了存储位置,HashMap 会检查该位置上是否存在一个或多个键值对。如果存在多个键值对,可能会使用链表或树等数据结构来存储这些键值对。
- 比较键: 如果在指定位置找到了键值对,HashMap 会比较目标键和存储的键是否相等。这里使用的是键的 equals() 方法来进行比较。
- 返回值: 如果找到了匹配的键,HashMap 会返回对应的值;否则,返回 null,表示未找到匹配的键。
4-13
HashMap put 方法的执行过程?
- 计算 Key 的 Hash 值。
- 根据 Hash 值计算出 Key 在数组中的位置,通常是通过取模运算(hash % 数组长度)来确定位置。
- 如果该位置没有元素,直接插入。
- 如果该位置有元素,判断 Key 是否相等,如果相等则覆盖 Value,如果不相等则处理冲突。
- 通常使用链表或红黑树解决冲突,如果当前位置存在一个链表,则新插入的键值对会被插入到链表(或树)的尾部。
- 如果链表长度超过阈值(通常为 8),链表会转换为红黑树。
- 如果插入成功,返回 null,如果覆盖了 Value,则返回被覆盖的 Value。
4-12
什么是 CDN?
CDN(Content Delivery Network)即内容分发网络,是一种通过在网络中部署节点服务器,将内容缓存到离用户更近的位置,从而提高用户访问速度的技术。
CDN 的工作原理是将内容缓存到离用户更近的位置,当用户请求内容时,CDN 会根据用户的地理位置,选择离用户最近的节点服务器来提供内容,从而减少网络延迟,提高用户访问速度。
4-11
力扣题库-27 移除元素
给定一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
必须仅用 O(1) 额外空间并 原地 修改输入数组。
题解
这道题目相对简单,题目摆明了使用双指针,轻松拿捏😂。
public static int removeElement(int[] nums, int val) {
int p1 = 0, p2 = nums.length;
while (p1 < p2) {
if (nums[p1++] == val) {
nums[--p1] = nums[--p2];
}
}
return p1;
}
在力扣查看我的详细题解
4-10
合并和变基有什么区别?
合并(merge) 和 变基(rebase) 的最终目的都是整合来自不同分支的修改。
合并是一种非破坏性的操作,它不会对现有分支中的提交进行修改,而是创建一个新的提交来整合不同分支的修改。
变基会为原始分支中的每个提交创建全新的提交来重写项目历史记录,能够让代码提交记录更加清晰明了。
参考地址:Merging vs. rebasing
4-9
力扣题库-1 两数之和
🔥 题目: 给定一个整数数组 nums
和一个整数 target
,请你在该数组中找出和为 target
的两个整数,并返回它们的数组下标。
题解1 暴力方法
由于过于简单,所以不再赘述。
时间复杂度:
空间复杂度:
public static int[] twoSum(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
for (int i1 = 0; i1 < nums.length; i1++) {
if (i == i1) {
continue;
}
if (nums[i] + nums[i1] == target) {
return new int[]{i, i1};
}
}
}
return null;
}
题解2 哈希表
对于哈希表的运用我还是不够熟练,这个题解是看完官方题解思路后完成的。
时间复杂度:
空间复杂度:
public static int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[]{map.get(complement), i};
}
map.put(nums[i], i);
}
return null;
}
4-8
为什么推荐使用 isEmpty() 方法判空?
根据《阿里巴巴 Java 开发手册》:
判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size()==0 的方式。
isEmpty()
方法可读性更好,更优雅,isEmpty()
方法的时间复杂度是 O(1),而size()
方法在有些集合中的时间复杂度不是 O(1)。
4-7
力扣题库-88 合并两个有序数组
🔥 题目: 给定两个 非递减顺序 排列的整数数组 nums1
和 nums2
,以及其长度 m
、n
,需要将 nums2
合并到 nums1
中,使 nums1
成为一个有序数组。
题解 1
如果不考虑时间及空间复杂度,可以直接将 nums2
数组拷贝到 nums1
数组后,再进行排序:
public void merge(int[] nums1, int m, int[] nums2, int n) {
if (n >= 0) System.arraycopy(nums2, 0, nums1, m, n);
Arrays.sort(nums1);
}
题解 2
基于官方题解3 双指针的结题思路,进行简单优化得出如下空间复杂度为 O(1) 的解法:
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// 创建两个双指针指向各个数组的末尾
int p1 = m - 1, p2 = n - 1;
while (p1 >= 0 || p2 >= 0) {
// 如果一个数组遍历结束,则直接保留剩余元素即可。
if (p2 < 0) return;
if (p1 < 0) {
nums1[p2] = nums2[p2--];
} else {
// 从后往前遍历,比较大小,将较大的元素放到 nums1 的末尾,并移动指正
nums1[p1 + p2 + 1] = nums1[p1] > nums2[p2] ? nums1[p1--] : nums2[p2--];
}
}
}
}
4-6
BigDecimal
类有哪些常见方法。
add(BigDecimal value)
:加法subtract(BigDecimal value)
:减法multiply(BigDecimal value)
:乘法divide(BigDecimal value)
:除法pow(int n)
:幂运算abs()
:绝对值negate()
:取反setScale(int newScale, RoundingMode roundingMode)
:设置精度compareTo(BigDecimal value)
:比较大小
4-5
什么是 BigDecimal
类?
《阿里巴巴 Java 开发手册》中提到:“为了避免精度丢失,可以使用 BigDecimal 来进行浮点数的运算”。
这是由于计算机中,十进制小数无法精确表示,会存在类似于 2.0f - 1.9f != 1.8f - 1.7f
的情况,所以在 JAVA 中提供了 BigDecimal
类来解决这个问题。
BigDecimal
类是 JAVA 中用于精确计算浮点数的类,它提供了大量的方法用于精确计算浮点数,避免了浮点数计算时的精度丢失问题。
4-4
JAVA 如何做序列化?
在 JAVA 中,如果要对一个对象进行序列化,需要实现 java.io.Serializable
接口,并且添加 serialVersionUID
字段。
import java.io.Serial;
import java.io.Serializable;
public class User implements Serializable {
@Serial // java14 引入的注解,表示该字段是序列化的一部分。详见 {java.io.Serial}
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String address;
public User(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
}
4-3
什么是序列化和反序列化?
在 JAVA 中,如果需要将 JAVA 对象持久存储,或者在网络传输,就需要将对象转变为为字节流,这个过程就是序列化。
将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程,就是反序列化。
4-2
JAVA 中有没有引用传递?
JAVA 中没有引用传递,只有值传递。
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]);
change(arr);
System.out.println(arr[0]);
}
public static void change(int[] array) {
// 将数组的第一个元素变为0
array[0] = 0;
}
上方案例中,输出的结果为 1 0
,说明在 change
方法中修改了数组的第一个元素,但是并没有改变数组的引用。
4-1
值传递&引用传递
- 值传递:传递的是实际的值,会创建一个副本,对形参的修改不会影响实参。
- 引用传递:传递的是实际的地址,对形参的修改会影响实参。