Skip to content

为什么0.1+0.2 !== 0.3

image.png 计算机使用二进制存储数据,整数转二进制没有误差,而小数无法用二进制准确表达,后面循环。其实是浮点数精度的问题。。 怎么解决呢:

  1. 使用整数进行运算:将小数转换为整数进行运算,然后再将结果转换回小数。
  2. 使用第三方库:有一些第三方库(例如 BigNumber.js / Math.js)可以处理浮点数精度问题
  3. 比较浮点数时使用误差范围:当比较两个浮点数是否相等时,考虑使用一个很小的误差范围来进行比较,而不是直接比较。
javascript

const EPSILON = 0.0000001;
if (Math.abs(a - b) < EPSILON) {
  // a 和 b 接近相等
}

作为一道面试题,我觉得重要的是要讲出一点其他人一般不会答出来的深度。像这道题,可以从原理和解决方案两个地方作为答题点,最好在编一个案例。大致讲自己遇到过这个问题,于是很好奇深入研究了一下,发现是浮点数精度导致……原理怎样怎样……然后又看了业界的库的源码,然后怎样怎样解决。 关于原理,我专门写了一篇文章 mqyqingfeng/Blog#155 来解释,实际回答的时候,我觉得答出来

  1. 非是 ECMAScript 独有
  2. IEEE754 标准中 64 位的储存格式,比如 11 位存偏移值
  3. 其中涉及的三次精度丢失

就已经 OK 了。 再讲解决方案,这个可以直接搜索到,各种方案都了解一下,比较一下优劣,还可以参考业界的一些库的实现,比如 math.js,不过相关的我并没有看过,后面我会研究一下。 如果还有精力的话,可以从加法再拓展讲讲超出安全值的数字的计算问题。 所以我觉得一能回答出底层实现,二能回答出多种解决方案的优劣,三能拓展讲出 bignum 的问题,就是一个非常完美的回答了。

移动端响应式方案有哪些

  1. CSS媒体查询 (Media Queries)
  2. **rem 布局 => flexible.js 阿里的**

rem 是相对于根元素 <html> 的字体大小来作为基准进行动态缩放的。通过 JavaScript 获取设备宽度,动态设置 html 元素的 font-size,其他元素使用 rem 作为单位,即可实现响应式布局。优点是可以非常灵活地设置不同尺寸,缺点是需要一些 JS 计算。

  1. vw/vh布局
    1. Viewport 单位(例如 vw、vh)是相对于视口宽度和高度的长度单位,可以根据视口的大小来设置元素的大小和位置。
    2. 可以结合CSS3 cacl函数使用
  2. flex或者Grid布局

rem和em的区别

remem 都是相对长度单位,

  1. 相对于谁不同
    1. rem相对于根元素即html的字体大小,且不受任何其他因素影响
    2. em相对于元素自身的字体大小,具有继承性,会逐层叠加
  2. 作用范围不同
    • rem 主要用于根元素大小的设置,之后其他元素根据这个基准调整大小
    • em 主要用于局部样式调整,常用于级联效果中
  3. 使用场景不同
    1. rem常用语响应式布局,可以更方便地控制整个页面的布局
    2. em常用于内部元素之间相对调整大小

在React应用中,有多种方法可以进行组件间通信,其中包括Context API和Redux。

Context使用场景 - 状态管理相对简单

  1. 如果父子组件层级不太深,
  2. 数据相对简单

Redux使用场景 - 需要管理复杂的全局状态

  1. 大型项目 对于大型复杂项目,Redux 可以更好地管理全局状态,并使代码更加模块化
  2. 数据流复杂 当数据需要在各个组件之间自由流动,Redux 的单向数据流可以更好的处理这种情况

React中,hooks怎么模拟生命周期

在 React Hooks 出现之前,React 中函数组件没有生命周期方法,只有类组件有生命周期方法。但是通过 Hooks,我们可以在函数组件中模拟类组件生命周期的行为

常用生命周期对应的 Hooks 的解决方案:

componentDidMount

使用 useEffect 钩子来模拟组件挂载后的操作。在 useEffect 中传入一个空的依赖数组,表示只在组件挂载后执行一次。

javascript
useEffect(() => {
  // componentDidMount
}, []);

componentDidUpdate

useEffect 中传入一个包含需要监测的状态的依赖数组,表示只有当这些状态发生变化时才执行操作。

通过监听特定值的变化来模拟 componentDidUpdate。

javascript
useEffect(() => {
  // 组件更新时执行
}, [prop, state])

componentWillUnmount

useEffect 中返回一个函数,该函数将在组件即将卸载时执行。

javascript
useEffect(() => {
  // 组件挂载和更新时执行
  return () => {
    // 组件卸载时执行
  }
}, [])

shouldComponentUpdate

可以使用 **React.memo **高阶组件来实现对 Props 的浅比较,避免不必要的重渲染。

javascript
const MyComponent = React.memo((props) => {
  // ...
})

总的来说,React Hooks 并没有直接提供生命周期函数的替代方案,而是通过 useEffect 钩子来模拟生命周期的行为。使用 Hooks,可以更方便地在函数式组件中管理状态和效果,并且可以避免类组件中的一些问题,如 this 指向、性能优化等。 总的来说,通过 **useEffect、useCallback、useMemo等 Hooks,**我们可以有效模拟类组件中的大部分生命周期行为,并且代码更加简洁、可读性更好。

在 React 类组件中使用事件处理函数时,通常会遇到 this 的指向问题。

在类组件中声明事件处理函数时,需要小心确保函数中的 this 指向正确。通常有以下几种方式来解决这个问题:

在构造函数中绑定 this

在类的构造函数中,使用 bind 方法将事件处理函数中的 this 绑定到当前实例上。

javascript
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。

jsx
class MyComponent extends React.Component {
  // 公共类字段语法 类似于箭头函数
  handleClick = () => {
    // 这里的 this 指向当前组件实例
  }

  render() {
    return <button onClick={this.handleClick}>Click Me</button>
  }
}

在事件处理函数的回调中使用箭头函数或者bind

在回调中使用箭头函数

在 render 方法中使用箭头函数作为事件处理函数的回调,这样可以确保 this 指向当前实例。

jsx
class MyComponent extends React.Component {
  handleClick() {
    // 这里的 this 指向当前组件实例
  }

  render() {
    return <button onClick={() => this.handleClick()}>Click</button>
  }
}

在回调中使用bind

在 render 方法中通过 bind 方法显式绑定 this。

jsx
class MyComponent extends React.Component {  
  handleClick() {
    // 这里的 this 指向当前组件实例
  }

  render() {
    return <button onClick={this.handleClick.bind(this)}>Click</button>
  }
}

JS怎么实现异步操作

回调函数

最初的异步实现方式,通过将回调函数作为参数传递给另一个函数,在操作完成时被调用。缺点是如果存在多层嵌套回调会导致回调地狱(Callback Hell)。

javascript
function fetchData(callback) {
  setTimeout(() => {
    const data = 'some data';
    // 在异步操作完成后,调用回调函数来处理结果。
    callback(data);
  }, 1000);
}

fetchData((data) => {
  console.log(data);
});

Promise

Promise 是 ES6 中新增的异步解决方案,通过链式调用的方式来处理异步操作,避免了回调地狱的问题。

javascript
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 用于等待一个异步方法执行。可以使异步代码看起来和同步代码一样简洁。

javascript
async function readFile() {
  try {
    const data = await readFilePromise('file.txt');
    // 处理数据
  } catch(err) {
    // 处理错误
  }
}
javascript
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基本数据类型和引用类型区别

  1. 存储在栈内存中
  2. 基本数据类型的值是不可变的(immutable),即一旦创建就不能被修改
  3. 基本数据类型的赋值是按值传递的,每个变量存储的是值本身,不会相互影响。
  4. 比较时直接比较值。

引用类型:

  1. 存储在堆内存中
  2. 引用类型的值是可变的(mutable),即可以修改对象的属性、数组的元素等。
  3. 比较时比较的是引用地址

JS数据类型有哪些

基本数据类型:Number String Boolean / undefined null / Symbol BigInt 引用类型:Object ==> Function / Array / RegExp / Date / Math / Error等

基本数据类型和引用类型在内存中如何存储

栈(stack): 基本数据类型的值,存在栈里面 堆 (heap) : 引用数据类型,栈里面存的是地址,这个地址指向堆内存中的数据

javascript
// 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()

深浅拷贝

赋值

赋值: 将某一值或者对象赋给某个变量的过程 基本数据类型:值传递,赋值之后两个变量互不影响 引用数据类型:赋址(地址),两个变量具有相同的引用,指向同一个对象,相互之间有影响

浅拷贝

在堆内存中新开辟一个空间,创建一个新对象 。拷贝原对象第一层基本数据类型的值和引用类型的地址。

实现浅拷贝方式

  1. Object.assign()
  2. 扩展运算符 const b =
  3. arr.concat()
  4. arr.slice()

深拷贝

在堆内存中开辟一个空间,创建一个新对象 递归的拷贝原对象的所有属性和方法,拷贝前后两个对象,相互不影响

实现深拷贝的方式

  1. const res = JSON.parse(JSON.stringify(obj))
javascript
// 2. 它的缺陷 
// 1. 拷贝对象的属性值 如果是 (function / undefined) / Symbol, 
      // 这个键值对丢失 ==> 重点记住
// 2. 如果拷贝对象的属性值是RegExp,会变成空对象{}
// 3. 如果是NaN,Infinity, 属性值会变成null
// 4. 如果拷贝日期对象,会变成日期字符串
  1. 使用一些js库,比如lodash _.cloneDeep()
  2. 手写深拷贝
javascript
// 检测是否是对象
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 区别

javascript
/* ==================== 面试题 ===================== */
// call / apply / bind 区别
// 1. 都是改变this的指向
// 2. call 接收参数列表,apply 接收数组
// 3. call / apply 是立即执行的,bind返回一个函数,需要手动调用
/* ==================== end ===================== */

冒泡排序

时间复杂度 O(n2)

冒泡排序(Bubble Sort)是一种简单的排序算法,它重复地遍历要排序的数组,依次比较相邻的两个元素,并交换它们直到整个数组排序完成。排序过程中,较大的元素会像气泡一样逐渐“浮”到数组的右侧。

javascript
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]

数组和链表的区别,插入元素时间复杂度

  1. 数组
    • 数组是一种线性数据结构,它由一组连续的内存空间组成,每个元素都有一个唯一的索引。
    • 数组支持随机访问,可以通过索引直接访问任意位置的元素。
    • 插入和删除操作可能会涉及元素的移动,尤其是在数组中间插入或删除元素时,需要将后续元素向后或向前移动。
    • 数组的时间复杂度:
      • 随机访问(根据索引获取元素):O(1)
      • 插入和删除(在末尾插入或删除元素):O(1)
      • 插入和删除(在中间插入或删除元素):O(n)
  2. 链表
    • 链表是一种非连续的数据结构,它由一组节点组成,每个节点包含数据和指向下一个节点的指针(指针)。
    • 链表不支持随机访问,只能从头节点开始沿着链表依次遍历,直到找到目标节点。
    • 插入和删除操作只涉及相邻节点之间的指针改变,不需要移动其他节点的位置。
    • 链表的时间复杂度:
      • 随机访问:O(n)
      • 插入和删除(在头部插入或删除元素):O(1)
      • 插入和删除(在中间插入或删除元素):O(1),但需要先找到目标位置

总的来说,数组适合需要随机访问元素的场景,而链表适合需要频繁插入和删除操作的场景。

JS创建10个a标签,点击的时候弹出对应的序号

typescript
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)
  }
})