为什么0.1+0.2 !== 0.3
计算机使用二进制存储数据,整数转二进制没有误差,而小数无法用二进制准确表达,后面循环。其实是浮点数精度的问题。。 怎么解决呢:
- 使用整数进行运算:将小数转换为整数进行运算,然后再将结果转换回小数。
- 使用第三方库:有一些第三方库(例如 BigNumber.js / Math.js)可以处理浮点数精度问题
- 比较浮点数时使用误差范围:当比较两个浮点数是否相等时,考虑使用一个很小的误差范围来进行比较,而不是直接比较。
const EPSILON = 0.0000001;
if (Math.abs(a - b) < EPSILON) {
// a 和 b 接近相等
}
作为一道面试题,我觉得重要的是要讲出一点其他人一般不会答出来的深度。像这道题,可以从原理和解决方案两个地方作为答题点,最好在编一个案例。大致讲自己遇到过这个问题,于是很好奇深入研究了一下,发现是浮点数精度导致……原理怎样怎样……然后又看了业界的库的源码,然后怎样怎样解决。 关于原理,我专门写了一篇文章 mqyqingfeng/Blog#155 来解释,实际回答的时候,我觉得答出来
- 非是 ECMAScript 独有
- IEEE754 标准中 64 位的储存格式,比如 11 位存偏移值
- 其中涉及的三次精度丢失
就已经 OK 了。 再讲解决方案,这个可以直接搜索到,各种方案都了解一下,比较一下优劣,还可以参考业界的一些库的实现,比如 math.js,不过相关的我并没有看过,后面我会研究一下。 如果还有精力的话,可以从加法再拓展讲讲超出安全值的数字的计算问题。 所以我觉得一能回答出底层实现,二能回答出多种解决方案的优劣,三能拓展讲出 bignum 的问题,就是一个非常完美的回答了。
移动端响应式方案有哪些
- CSS媒体查询 (Media Queries)
**rem 布局 => flexible.js 阿里的**
rem 是相对于根元素 <html>
的字体大小来作为基准进行动态缩放的。通过 JavaScript 获取设备宽度,动态设置 html 元素的 font-size,其他元素使用 rem 作为单位,即可实现响应式布局。优点是可以非常灵活地设置不同尺寸,缺点是需要一些 JS 计算。
- vw/vh布局
- Viewport 单位(例如 vw、vh)是相对于视口宽度和高度的长度单位,可以根据视口的大小来设置元素的大小和位置。
- 可以结合CSS3 cacl函数使用
- flex或者Grid布局
rem和em的区别
rem 和 em 都是相对长度单位,
- 相对于谁不同
- rem相对于根元素即html的字体大小,且不受任何其他因素影响
- em相对于元素自身的字体大小,具有继承性,会逐层叠加
- 作用范围不同
- rem 主要用于根元素大小的设置,之后其他元素根据这个基准调整大小
- em 主要用于局部样式调整,常用于级联效果中
- 使用场景不同
- rem常用语响应式布局,可以更方便地控制整个页面的布局
- em常用于内部元素之间相对调整大小
在React应用中,有多种方法可以进行组件间通信,其中包括Context API和Redux。
Context使用场景 - 状态管理相对简单
- 如果父子组件层级不太深,
- 数据相对简单
Redux使用场景 - 需要管理复杂的全局状态
- 大型项目 对于大型复杂项目,Redux 可以更好地管理全局状态,并使代码更加模块化
- 数据流复杂 当数据需要在各个组件之间自由流动,Redux 的单向数据流可以更好的处理这种情况
React中,hooks怎么模拟生命周期
在 React Hooks 出现之前,React 中函数组件没有生命周期方法,只有类组件有生命周期方法。但是通过 Hooks,我们可以在函数组件中模拟类组件生命周期的行为
常用生命周期对应的 Hooks 的解决方案:
componentDidMount
使用 useEffect 钩子来模拟组件挂载后的操作。在 useEffect 中传入一个空的依赖数组,表示只在组件挂载后执行一次。
useEffect(() => {
// componentDidMount
}, []);
componentDidUpdate
在 useEffect 中传入一个包含需要监测的状态的依赖数组,表示只有当这些状态发生变化时才执行操作。
通过监听特定值的变化来模拟 componentDidUpdate。
useEffect(() => {
// 组件更新时执行
}, [prop, state])
componentWillUnmount
在 useEffect 中返回一个函数,该函数将在组件即将卸载时执行。
useEffect(() => {
// 组件挂载和更新时执行
return () => {
// 组件卸载时执行
}
}, [])
shouldComponentUpdate
可以使用 **React.memo **高阶组件来实现对 Props 的浅比较,避免不必要的重渲染。
const MyComponent = React.memo((props) => {
// ...
})
总的来说,React Hooks 并没有直接提供生命周期函数的替代方案,而是通过 useEffect 钩子来模拟生命周期的行为。使用 Hooks,可以更方便地在函数式组件中管理状态和效果,并且可以避免类组件中的一些问题,如 this 指向、性能优化等。 总的来说,通过 **useEffect、useCallback、useMemo等 Hooks,**我们可以有效模拟类组件中的大部分生命周期行为,并且代码更加简洁、可读性更好。
在 React 类组件中使用事件处理函数时,通常会遇到 this 的指向问题。
在类组件中声明事件处理函数时,需要小心确保函数中的 this 指向正确。通常有以下几种方式来解决这个问题:
在构造函数中绑定 this
在类的构造函数中,使用 bind 方法将事件处理函数中的 this 绑定到当前实例上。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 这里的 this 指向当前组件实例
}
render() {
return <button onClick={this.handleClick}>Click</button>
}
}
使用箭头函数作为类字段
在 React 16.8 引入的类字段语法中,可以使用箭头函数避免手动绑定 this。
class MyComponent extends React.Component {
// 公共类字段语法 类似于箭头函数
handleClick = () => {
// 这里的 this 指向当前组件实例
}
render() {
return <button onClick={this.handleClick}>Click Me</button>
}
}
在事件处理函数的回调中使用箭头函数或者bind
在回调中使用箭头函数
在 render 方法中使用箭头函数作为事件处理函数的回调,这样可以确保 this 指向当前实例。
class MyComponent extends React.Component {
handleClick() {
// 这里的 this 指向当前组件实例
}
render() {
return <button onClick={() => this.handleClick()}>Click</button>
}
}
在回调中使用bind
在 render 方法中通过 bind 方法显式绑定 this。
class MyComponent extends React.Component {
handleClick() {
// 这里的 this 指向当前组件实例
}
render() {
return <button onClick={this.handleClick.bind(this)}>Click</button>
}
}
JS怎么实现异步操作
回调函数
最初的异步实现方式,通过将回调函数作为参数传递给另一个函数,在操作完成时被调用。缺点是如果存在多层嵌套回调会导致回调地狱(Callback Hell)。
function fetchData(callback) {
setTimeout(() => {
const data = 'some data';
// 在异步操作完成后,调用回调函数来处理结果。
callback(data);
}, 1000);
}
fetchData((data) => {
console.log(data);
});
Promise
Promise 是 ES6 中新增的异步解决方案,通过链式调用的方式来处理异步操作,避免了回调地狱的问题。
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = 'some data';
resolve(data);
}, 1000);
});
}
fetchData().then((data) => {
console.log(data);
});
async / await
async/await 是 ES2017 引入的语法糖,结合 Promise 和 generator,使用 async 定义一个异步函数, await 用于等待一个异步方法执行。可以使异步代码看起来和同步代码一样简洁。
async function readFile() {
try {
const data = await readFilePromise('file.txt');
// 处理数据
} catch(err) {
// 处理错误
}
}
async function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = 'some data';
resolve(data);
}, 1000);
});
}
async function getData() {
const data = await fetchData();
console.log(data);
}
getData();
JS基本数据类型和引用类型区别
- 存储在栈内存中
- 基本数据类型的值是不可变的(immutable),即一旦创建就不能被修改
- 基本数据类型的赋值是按值传递的,每个变量存储的是值本身,不会相互影响。
- 比较时直接比较值。
引用类型:
- 存储在堆内存中
- 引用类型的值是可变的(mutable),即可以修改对象的属性、数组的元素等。
- 比较时比较的是引用地址
JS数据类型有哪些
基本数据类型:Number String Boolean / undefined null / Symbol BigInt 引用类型:Object ==> Function / Array / RegExp / Date / Math / Error等
基本数据类型和引用类型在内存中如何存储
栈(stack): 基本数据类型的值,存在栈里面 堆 (heap) : 引用数据类型,栈里面存的是地址,这个地址指向堆内存中的数据
// Q1: 数据类型有哪些?
// 基本数据类型:Number String Boolean / undefined null / Symbol BigInt
// 引用类型:Object ==> Function / Array / RegExp / Date / Math / Error等
// Q2: 基本数据类型和引用类型在内存中如何存储的?
// 栈(stack): 基本数据类型的值,存在栈里面
// 堆 (heap) : 引用数据类型,栈里面存的是地址,这个地址指向堆内存中的数据
如何检测数据类型
typeof检测除了null以外的基本数据类型
instanceof可以检测引用类型,但对于基本数据类型无效
Object.prototype.toString.call()
深浅拷贝
赋值
赋值: 将某一值或者对象赋给某个变量的过程 基本数据类型:值传递,赋值之后两个变量互不影响 引用数据类型:赋址(地址),两个变量具有相同的引用,指向同一个对象,相互之间有影响
浅拷贝
在堆内存中新开辟一个空间,创建一个新对象 。拷贝原对象第一层基本数据类型的值和引用类型的地址。
实现浅拷贝方式
- Object.assign()
- 扩展运算符 const b =
- arr.concat()
- arr.slice()
深拷贝
在堆内存中开辟一个空间,创建一个新对象 递归的拷贝原对象的所有属性和方法,拷贝前后两个对象,相互不影响
实现深拷贝的方式
- const res = JSON.parse(JSON.stringify(obj))
// 2. 它的缺陷
// 1. 拷贝对象的属性值 如果是 (function / undefined) / Symbol,
// 这个键值对丢失 ==> 重点记住
// 2. 如果拷贝对象的属性值是RegExp,会变成空对象{}
// 3. 如果是NaN,Infinity, 属性值会变成null
// 4. 如果拷贝日期对象,会变成日期字符串
- 使用一些js库,比如lodash _.cloneDeep()
- 手写深拷贝
// 检测是否是对象
const isObject = (obj) => {
return obj !== null && typeof obj === "object";
};
const deepClone = (obj, map = new WeakMap()) => {
// 1. 基本数据类型,直接返回
if (isObject(obj)) return obj;
// 2. 处理日期对象
if (obj instanceof Date) return new Date(obj);
// 3. 处理正则
if (obj instanceof RegExp) return new RegExp(obj);
// 4. 解决循环引用问题,如果已经拷贝过当前对象,直接返回
if (map.has(obj)) return map.get(obj);
// 5. 拷贝逻辑 创建一个新的对象或者数组
let clone = Array.isArray(obj) ? [] : {};
// 6. 将新对象clone存储到map中,避免循环引用,obj为key
map.set(obj, clone);
// 7. 递归拷贝
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], map);
}
}
// 8.第7部分改为这个也ok
Reflect.ownKeys(obj).forEach((key) => {
clone[key] = deepClone(obj[key], map);
});
return clone;
};
call / apply / bind 区别
/* ==================== 面试题 ===================== */
// call / apply / bind 区别
// 1. 都是改变this的指向
// 2. call 接收参数列表,apply 接收数组
// 3. call / apply 是立即执行的,bind返回一个函数,需要手动调用
/* ==================== end ===================== */
冒泡排序
时间复杂度 O(n2)
冒泡排序(Bubble Sort)是一种简单的排序算法,它重复地遍历要排序的数组,依次比较相邻的两个元素,并交换它们直到整个数组排序完成。排序过程中,较大的元素会像气泡一样逐渐“浮”到数组的右侧。
function bubbleSort(arr) {
// 外层循环控制每次排序的轮数
for (let i = 0; i < arr.length - 1; i++) {
// 内层循环控制每次排序的比较和交换
for (let j = 0; j < arr.length - 1 - i; j++) {
// 如果前一个元素大于后一个元素,则交换它们的位置
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
// 示例
const arr = [3, 1, 6, 2, 8, 5];
console.log(bubbleSort(arr)); // 输出 [1, 2, 3, 5, 6, 8]
数组和链表的区别,插入元素时间复杂度
- 数组:
- 数组是一种线性数据结构,它由一组连续的内存空间组成,每个元素都有一个唯一的索引。
- 数组支持随机访问,可以通过索引直接访问任意位置的元素。
- 插入和删除操作可能会涉及元素的移动,尤其是在数组中间插入或删除元素时,需要将后续元素向后或向前移动。
- 数组的时间复杂度:
- 随机访问(根据索引获取元素):O(1)
- 插入和删除(在末尾插入或删除元素):O(1)
- 插入和删除(在中间插入或删除元素):O(n)
- 链表:
- 链表是一种非连续的数据结构,它由一组节点组成,每个节点包含数据和指向下一个节点的指针(指针)。
- 链表不支持随机访问,只能从头节点开始沿着链表依次遍历,直到找到目标节点。
- 插入和删除操作只涉及相邻节点之间的指针改变,不需要移动其他节点的位置。
- 链表的时间复杂度:
- 随机访问:O(n)
- 插入和删除(在头部插入或删除元素):O(1)
- 插入和删除(在中间插入或删除元素):O(1),但需要先找到目标位置
总的来说,数组适合需要随机访问元素的场景,而链表适合需要频繁插入和删除操作的场景。
JS创建10个a标签,点击的时候弹出对应的序号
const wrap = document.querySelector('#app')
// 创建文档片段
// 因为文档片段存在于内存中,并不在 DOM 树中,
// 所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)
// 因此,使用文档片段通常会带来更好的性能。
const fragment = document.createDocumentFragment()
const arr = Array(1000).fill(null)
// console.time()
// 先遍历循环添加到文档片段中,再一次性添加到dom元素中(减少回流)
arr.forEach((_, i) => {
let a = document.createElement('a')
a.innerHTML = i + 1
fragment.appendChild(a)
})
wrap.appendChild(fragment)
// console.timeEnd()
wrap.addEventListener('click', (e) => {
if (e.target.tagName === 'A') {
// alert(e.target.innerHTML)
console.log(e.target.innerHTML)
}
})