阅读视图

发现新文章,点击刷新页面。

前端JS: 数组扁平化

JavaScript 数组扁平化实现详解

一、扁平化概念

数组扁平化是指将一个多维数组转换为一维数组的过程:

// 多维数组
const arr = [1, [2, [3, [4, 5]], 6], 7];

// 扁平化后
// [1, 2, 3, 4, 5, 6, 7]

二、原生方法(ES2019+)

1. Array.prototype.flat()

const arr = [1, [2, [3, [4, 5]], 6], 7];

// 默认只展开一层
console.log(arr.flat()); // [1, 2, [3, [4, 5]], 6, 7]

// 指定展开深度
console.log(arr.flat(2)); // [1, 2, 3, [4, 5], 6, 7]

// 完全展开(Infinity表示无限深度)
console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5, 6, 7]

// 移除空位
console.log([1, 2, , 3, 4].flat()); // [1, 2, 3, 4]

三、手动实现方法

1. 递归实现(基础版)

function flatten(arr) {
  let result = [];
  
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result = result.concat(flatten(arr[i]));
    } else {
      result.push(arr[i]);
    }
  }
  
  return result;
}

// 使用示例
const arr = [1, [2, [3, [4, 5]], 6], 7];
console.log(flatten(arr)); // [1, 2, 3, 4, 5, 6, 7]

2. 递归实现(可指定深度)

function flattenDepth(arr, depth = 1) {
  if (depth === 0) return arr.slice(); // 深度为0,直接返回副本
  
  let result = [];
  
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i]) && depth > 0) {
      result = result.concat(flattenDepth(arr[i], depth - 1));
    } else {
      result.push(arr[i]);
    }
  }
  
  return result;
}

// 使用示例
const arr = [1, [2, [3, [4, 5]], 6], 7];
console.log(flattenDepth(arr, 1)); // [1, 2, [3, [4, 5]], 6, 7]
console.log(flattenDepth(arr, 2)); // [1, 2, 3, [4, 5], 6, 7]
console.log(flattenDepth(arr, Infinity)); // [1, 2, 3, 4, 5, 6, 7]

3. 使用reduce实现

function flattenReduce(arr) {
  return arr.reduce((result, current) => {
    return result.concat(
      Array.isArray(current) ? flattenReduce(current) : current
    );
  }, []);
}

// 带深度的reduce版本
function flattenReduceDepth(arr, depth = 1) {
  return depth > 0
    ? arr.reduce((acc, val) => 
        acc.concat(Array.isArray(val) 
          ? flattenReduceDepth(val, depth - 1) 
          : val
        ), [])
    : arr.slice();
}

4. 使用栈实现(非递归)

function flattenStack(arr) {
  const stack = [...arr];
  const result = [];
  
  while (stack.length) {
    const next = stack.pop();
    
    if (Array.isArray(next)) {
      // 将数组元素推入栈中(注意保持顺序)
      stack.push(...next.slice().reverse());
    } else {
      result.push(next);
    }
  }
  
  return result.reverse();
}

// 优化版本(保持顺序)
function flattenStackOrdered(arr) {
  const stack = [];
  const result = [];
  let current = arr;
  let i = 0;
  
  while (current !== undefined) {
    if (i < current.length) {
      const item = current[i];
      i++;
      
      if (Array.isArray(item)) {
        // 保存当前状态
        stack.push({ current, i });
        // 切换到子数组
        current = item;
        i = 0;
      } else {
        result.push(item);
      }
    } else if (stack.length > 0) {
      // 恢复上一个状态
      const saved = stack.pop();
      current = saved.current;
      i = saved.i;
    } else {
      current = undefined;
    }
  }
  
  return result;
}

5. 使用toString()方法

function flattenToString(arr) {
  return arr.toString()
    .split(',')
    .map(item => {
      // 转换回适当的数据类型
      const num = Number(item);
      return isNaN(num) ? item : num;
    });
}

// 注意:这种方法会将所有元素转为字符串再解析
// 只适用于纯数字数组或可转换为字符串的元素
const arr = [1, [2, [3, [4, 5]], 6], 7];
console.log(flattenToString(arr)); // [1, 2, 3, 4, 5, 6, 7]

// 局限性示例
const mixedArr = [1, [2, ['a', ['b', 'c']]], 3];
console.log(flattenToString(mixedArr)); // [1, 2, 'a', 'b', 'c', 3]

总结

推荐方法选择

  1. 现代项目(支持ES2019+) :直接使用arr.flat(Infinity)
  2. 需要深度控制:使用递归版本flattenDepth
  3. 大数组或性能敏感:使用栈实现的非递归版本
  4. 需要处理循环引用:使用flattenSafe或完整版
  5. 简单场景:使用reduce或递归基础版

注意事项

  • 方法选择要考虑浏览器兼容性
  • 递归方法可能导致栈溢出(深度过大)
  • 字符串转换方法有类型丢失问题
  • 注意处理稀疏数组和循环引用
  • 性能测试显示原生flat通常最快,栈实现次之

前端手写: Promise封装Ajax

精简版(仅核心功能):

function simpleAjax(url, method = 'GET', data = null) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url, true);
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.statusText));
      }
    };
    xhr.onerror = () => reject(new Error('网络错误'));
    xhr.send(data);
  });
}

完整版

封装说明

  1. 基本功能:支持GET/POST等方法,自动处理JSON数据
  2. 错误处理:包含HTTP状态码、网络错误、超时等错误处理
  3. 响应解析:自动识别JSON响应并解析
  4. 头部支持:可自定义请求头
  5. 扩展性:可根据需要添加超时设置、进度监听等功能
function ajaxPromise(options) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(options.method || 'GET', options.url, true);
    
    // 设置请求头(可自定义)
    if (options.headers) {
      for (let key in options.headers) {
        xhr.setRequestHeader(key, options.headers[key]);
      }
    }
    
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          try {
            const response = xhr.responseText;
            const contentType = xhr.getResponseHeader('Content-Type');
            
            // 自动解析JSON响应
            let data = response;
            if (contentType && contentType.includes('application/json')) {
              data = JSON.parse(response);
            }
            
            resolve({
              data: data,
              status: xhr.status,
              xhr: xhr
            });
          } catch (error) {
            reject(new Error(`解析响应失败: ${error.message}`));
          }
        } else {
          reject(new Error(`请求失败: ${xhr.status} ${xhr.statusText}`));
        }
      }
    };
    
    xhr.onerror = function() {
      reject(new Error('网络错误'));
    };
    
    xhr.ontimeout = function() {
      reject(new Error('请求超时'));
    };
    
    // 发送请求
    if (options.data) {
      let body = options.data;
      if (typeof body === 'object') {
        body = JSON.stringify(body);
        xhr.setRequestHeader('Content-Type', 'application/json');
      }
      xhr.send(body);
    } else {
      xhr.send();
    }
  });
}

// 使用示例
ajaxPromise({
  url: 'https://api.example.com/data',
  method: 'GET'
})
  .then(response => {
    console.log('请求成功:', response.data);
  })
  .catch(error => {
    console.error('请求失败:', error.message);
  });

// POST请求示例
ajaxPromise({
  url: 'https://api.example.com/submit',
  method: 'POST',
  data: { name: '张三', age: 25 },
  headers: {
    'Authorization': 'Bearer token123'
  }
})
  .then(response => console.log('提交成功:', response.data))
  .catch(error => console.error('提交失败:', error));

前端手写: new操作符

手写 new 操作符

1. new 操作符的工作原理

// new 操作符做了以下4件事:
// 1. 创建一个空对象
// 2. 将这个空对象的原型指向构造函数的 prototype
// 3. 将构造函数的 this 绑定到这个新对象
// 4. 执行构造函数
// 5. 返回这个新对象(如果构造函数没有返回对象,则返回 this)

function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 使用 new
const p1 = new Person('张三', 25);

2. 手写 myNew 函数

2.1 基础版本

function myNew(constructor, ...args) {
  // 1. 创建一个新对象
  const obj = {};
  
  // 2. 将对象的原型指向构造函数的 prototype
  obj.__proto__ = constructor.prototype;
  // 或者使用:Object.setPrototypeOf(obj, constructor.prototype);
  
  // 3. 将构造函数的 this 绑定到新对象,并执行构造函数
  const result = constructor.apply(obj, args);
  
  // 4. 判断构造函数返回值类型
  // 如果构造函数返回一个对象,则返回这个对象
  // 否则返回新创建的对象
  return result instanceof Object ? result : obj;
}

3. 面试常考版本(精简)

// 面试时能写出的最简版本
function myNew(Con, ...args) {
  const obj = Object.create(Con.prototype);
  const result = Con.apply(obj, args);
  return result instanceof Object ? result : obj;
}

4. 实现原理详解

// 详细解释每一步
function explainNew(constructor, ...args) {
  console.log('1. 获取构造函数:', constructor);
  
  // 步骤1:创建空对象
  console.log('2. 创建一个空对象');
  const obj = {};
  
  // 步骤2:设置原型链
  console.log('3. 设置对象的原型为构造函数的 prototype');
  console.log('   构造函数 prototype:', constructor.prototype);
  obj.__proto__ = constructor.prototype;
  console.log('   新对象的 __proto__:', obj.__proto__);
  
  // 步骤3:执行构造函数
  console.log('4. 执行构造函数,绑定 this 到新对象');
  console.log('   构造函数参数:', args);
  const result = constructor.apply(obj, args);
  console.log('   构造函数返回值:', result);
  console.log('   新对象当前状态:', obj);
  
  // 步骤4:判断返回值
  console.log('5. 判断构造函数返回值类型');
  const shouldReturnResult = result && (typeof result === 'object' || typeof result === 'function');
  console.log('   应该返回构造函数返回值吗?', shouldReturnResult);
  
  return shouldReturnResult ? result : obj;
}

// 测试
function Demo(name) {
  this.name = name;
  this.createdAt = new Date();
}
const demo = explainNew(Demo, '测试');

5. 常见面试问题

Q1: new 操作符做了什么?

A:

  1. 创建一个新对象
  2. 将这个对象的原型指向构造函数的 prototype
  3. 将构造函数的 this 绑定到这个新对象,并执行构造函数
  4. 如果构造函数返回一个对象,则返回这个对象;否则返回新创建的对象

Q2: 手写 new 操作符的思路?

A:

function myNew(Con, ...args) {
  // 1. 创建对象,设置原型
  const obj = Object.create(Con.prototype);
  // 2. 执行构造函数
  const result = Con.apply(obj, args);
  // 3. 判断返回值
  return result instanceof Object ? result : obj;
}

Q3: 构造函数返回基本类型会怎样?

A: 如果构造函数返回基本类型(string, number, boolean, null, undefined),这个返回值会被忽略,new 操作符会返回新创建的对象。

Q4: 构造函数返回对象会怎样?

A: 如果构造函数返回一个对象(包括数组、函数),那么这个对象会作为 new 表达式的结果,而不是新创建的对象。

Q5: 箭头函数能用 new 调用吗?

A: 不能。箭头函数没有自己的 this,也没有 prototype 属性,所以不能作为构造函数使用。

6. 总结

手写 new 的核心步骤

  1. 创建对象:创建一个新对象
  2. 设置原型:将对象的 __proto__指向构造函数的 prototype
  3. 绑定 this:使用 applycall将构造函数的 this 绑定到新对象
  4. 执行构造函数:传入参数执行
  5. 返回结果:判断构造函数返回值,如果是对象则返回,否则返回新对象

一句话总结:new 操作符就是创建一个新对象,将其原型指向构造函数的 prototype,然后以这个对象为 this 执行构造函数,最后根据构造函数返回值决定返回什么。

前端JS: 虚拟dom是什么? 原理? 优缺点?

虚拟DOM (Virtual DOM)

什么是虚拟DOM?

虚拟DOM是一个JavaScript对象,它是真实DOM的轻量级内存表示。它是一个抽象层,用于描述UI应该是什么样子。

核心原理

1. 创建虚拟DOM树

// 虚拟DOM对象示例
const vNode = {
  type: 'div',
  props: {
    className: 'container',
    onClick: () => console.log('clicked')
  },
  children: [
    { type: 'h1', props: {}, children: 'Hello' },
    { type: 'p', props: {}, children: 'Virtual DOM' }
  ]
};

2. Diff算法(差异化比较)

  • 比较策略

    • 同层级比较(时间复杂度O(n))
    • 类型不同 → 直接替换
    • 类型相同 → 比较属性
    • 列表比较(key优化)

3. 渲染流程

真实DOM操作昂贵          虚拟DOM操作快速
     ↓                         ↓
状态变化 → 生成虚拟DOM → Diff比较 → 最小化更新 → 更新真实DOM

核心实现步骤

1. 初始化

// 创建虚拟DOM
function createElement(type, props, ...children) {
  return {
    type,
    props: props || {},
    children: children.flat()
  };
}

2. Diff算法实现思路

function diff(oldVNode, newVNode) {
  // 1. 类型不同,直接替换
  if (oldVNode.type !== newVNode.type) {
    return { type: 'REPLACE', newNode: newVNode };
  }
  
  // 2. 属性比较
  const propPatches = diffProps(oldVNode.props, newVNode.props);
  
  // 3. 子节点比较
  const childrenPatches = diffChildren(oldVNode.children, newVNode.children);
  
  return { propPatches, childrenPatches };
}

优点

1. 性能优化

  • 批量更新:合并多次DOM操作
  • 最小化更新:只更新变化的部分
  • 减少重排重绘:优化渲染性能

2. 开发效率

  • 声明式编程:关注"应该是什么样子"
  • 跨平台能力:一套代码多端渲染
  • 组件化:更好的代码组织和复用

3. 其他优势

  • 抽象真实DOM差异
  • 更好的可测试性
  • 框架级优化支持

缺点

1. 性能开销

  • 内存占用:额外存储虚拟DOM树
  • CPU计算:Diff算法有计算成本
  • 初始渲染慢:需要构建虚拟DOM树

2. 适用场景限制

  • 简单页面不适用:小项目可能得不偿失
  • 实时性要求高:游戏、动画等场景
  • SSR首屏:可能产生双重计算

3. 学习成本

  • 需要理解虚拟DOM概念
  • 框架特定的API学习
  • 调试相对复杂

实际应用对比

原生DOM操作

// 传统方式
const list = document.getElementById('list');
list.innerHTML = '';  // 清空(重绘)
items.forEach(item => {
  const li = document.createElement('li');
  li.textContent = item;
  list.appendChild(li);  // 多次重排
});

虚拟DOM方式

// React示例
function List({ items }) {
  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.text}</li>)}
    </ul>
  );
}
// 只更新变化的li,批量DOM操作

现代框架实现差异

React

  • Fiber架构:可中断的Diff过程
  • 协调器(Reconciler):调度更新优先级
  • 并发模式:更好的用户体验

Vue

  • 响应式依赖追踪
  • 编译时优化:静态节点提升
  • 更细粒度的更新

性能优化建议

1. 合理使用key

// 好的:稳定唯一的key
{items.map(item => (
  <ListItem key={item.id} item={item} />
))}

// 避免:index作为key
{items.map((item, index) => (
  <ListItem key={index} item={item} />  // 不推荐
))}

2. 减少不必要的渲染

  • 使用React.memo、PureComponent
  • 合理使用useMemo、useCallback
  • 避免在render中创建新对象/函数

3. 代码分割

  • 按需加载组件
  • 路由懒加载
  • 减少初始包大小

总结

虚拟DOM是现代前端框架的核心技术,它在大多数应用场景下提供了更好的开发体验和可接受的性能。但对于性能极其敏感或简单的应用,直接操作DOM或使用更轻量的方案可能更合适。

使用建议

  • 大型复杂应用 ✅ 推荐使用
  • 简单静态页面 ❌ 可能过度设计
  • 性能敏感场景 🔍 需要详细评估
❌