阅读视图

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

前端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 执行构造函数,最后根据构造函数返回值决定返回什么。

❌