一、数据类型:基本类型与引用类型

JavaScript 的数据类型核心分为基本数据类型(原始类型) 和引用数据类型,二者在存储方式、值特性、比较规则上存在本质差异。

1. 基本数据类型(6 种)

  • 类型列表:Undefined、Null、Boolean、Number、String、Symbol(ES6 新增,代表独一无二的值)。
  • 核心特性
    • 按值访问:变量直接存储具体的值,而非引用地址。
    • 值不可变:无法直接修改基本类型的值,所有看似 “修改” 的操作(如字符串方法)都会返回新值,原变量值不变。
      var str = "123hello321";
      str.toUpperCase(); // 返回"123HELLO321"(新值)
      console.log(str);  // 仍为"123hello321"(原值不变)
      
    • 比较规则:值比较== 仅比较值(会触发隐式类型转换),=== 既比较值也比较数据类型(无隐式转换)。
      var a = 1;
      var b = true;
      console.log(a == b);  // true(值通过隐式转换后相等)
      console.log(a === b); // false(数据类型不同:Number vs Boolean)
      
    • 存储位置:栈内存(Stack):变量标识符(变量名)和值直接存储在栈中,赋值时直接拷贝值,修改一个变量不影响另一个。
      var a = "zyj";
      var b = a; // 栈中拷贝"zyj"给b
      a = "呵呵"; // 修改a的栈值,不影响b
      console.log(a); // "呵呵"
      console.log(b); // "zyj"
      

2. 引用数据类型

  • 类型范围:除 6 种基本类型外,其余均为引用类型,核心包括 Object、Array、Date、RegExp、Function 等,统称为 Object 类型。
  • 核心特性
    • 按引用访问:变量存储的是 “指向堆内存中对象的指针”,而非对象本身。
    • 值可修改:可直接修改对象的属性或方法(本质是修改堆内存中的对象内容)。
      var obj = {name: "zyj"};
      obj.name = "percy"; // 直接修改堆中对象的name属性
      obj.age = 21;       // 给堆中对象新增age属性
      
    • 比较规则:引用比较:即使两个对象内容完全相同,只要它们在堆内存中是不同的对象(指针不同),比较结果就为 false。
      var obj1 = {}; // 堆中创建对象1,obj1存指向它的指针
      var obj2 = {}; // 堆中创建对象2,obj2存指向它的指针
      console.log(obj1 == obj2);  // false(指针不同)
      console.log(obj1 === obj2); // false(同上)
      
    • 存储位置:栈 + 堆内存
      • 栈内存:存储变量标识符和 “指向堆内存的指针”。
      • 堆内存:存储对象的具体内容(属性、方法)。
      • 赋值时仅拷贝指针,修改一个变量的对象内容,会影响所有指向该堆对象的变量。
        var a = {name: "percy"}; // 栈存a的指针,堆存{name: "percy"}
        var b = a;               // 栈拷贝a的指针,b与a指向同一堆对象
        a.name = "zyj";          // 修改堆对象的name属性
        console.log(b.name);     // "zyj"(b的指针未变,堆对象已改)
        

二、浅拷贝与深拷贝(面试必考)

拷贝的核心区别:修改原数据后,新数据是否会同步变化

1. 浅拷贝

  • 定义:仅拷贝 “表层数据”,若原数据包含引用类型(如数组、对象),新数据会与原数据共享同一堆内存中的引用类型,修改引用类型会同步影响两者。
  • 常见实现方式
    1. 直接赋值(=):完全共享引用,修改任意一方的引用类型均同步变化。
      var person1 = {name: "张三", hobby: ['篮球','足球']};
      var person2 = person1; // 浅拷贝(共享引用)
      person2.name = "hello"; // 原数据person1.name也变为"hello"
      person1.hobby[0] = "乒乓球"; // 新数据person2.hobby[0]也变为"乒乓球"
      
    2. 循环遍历赋值:仅拷贝第一层基本类型,深层引用类型仍共享。
      var person1 = {name: "张三", hobby: ['篮球','足球']};
      var person2 = {};
      for (let k in person1) {
        person2[k] = person1[k]; // 第一层name(基本类型)拷贝值,hobby(数组)拷贝引用
      }
      person1.name = "李四"; // person2.name不变(基本类型独立)
      person1.hobby[0] = "羽毛球"; // person2.hobby[0]同步变化(引用共享)
      
    3. 扩展运算符(...):同循环遍历,仅拷贝第一层,深层引用类型共享。
      var person2 = {...person1}; // 浅拷贝,hobby仍与person1共享
      
    4. 数组 concat () 方法:数组拼接时,仅拷贝第一层元素,深层数组仍共享引用。
      var arr1 = ['张三', 18, ['篮球','足球']];
      var arr2 = arr1.concat('郑州'); // 浅拷贝
      arr1[0] = "李四"; // arr2[0]不变(基本类型独立)
      arr1[2][0] = "羽毛球"; // arr2[2][0]同步变化(深层数组共享)
      
    5. Object.assign():用于对象合并,仅拷贝第一层基本类型,深层引用类型共享(常被误认是深拷贝,实际是浅拷贝)。
      var obj1 = {a: {c: 1}, b: 2};
      var obj2 = Object.assign({}, obj1); // 浅拷贝
      obj2.a.c = 10; // obj1.a.c同步变为10(深层对象共享)
      

2. 深拷贝

  • 定义:完全拷贝原数据的所有层级(包括深层引用类型),新数据与原数据在堆内存中是独立的两个对象,修改任意一方均不影响另一方。
  • 常见实现方式
    1. 递归实现(核心,需手写):通过递归遍历原数据的所有层级,遇到引用类型则创建新的对应类型(数组 / 对象),再继续递归拷贝深层数据。
      function deepClone(obj) {
        // 边界处理:若不是对象(基本类型或null),直接返回值
        if (obj === null || typeof obj !== "object") return obj;
        
        // 判断原数据是数组还是对象,创建对应的新容器
        let clone = Array.isArray(obj) ? [] : {};
        
        // 遍历原数据的自有属性(排除原型链属性)
        for (let key in obj) {
          if (obj.hasOwnProperty(key)) {
            // 递归拷贝深层数据
            clone[key] = deepClone(obj[key]);
          }
        }
        return clone;
      }
      
      // 测试:修改原数据深层引用类型,新数据不变
      var obj = {a: 1, b: [1,2,3], c: {c1: 1}};
      var obj1 = deepClone(obj);
      obj1.b.push(4);
      console.log(obj.b);  // [1,2,3](原数据不变)
      console.log(obj1.b); // [1,2,3,4](新数据独立)
      
       
      • 递归的问题:无法处理循环引用(如obj.self = obj),会导致栈溢出;解决方案:用WeakMapMap记录已拷贝的对象,避免重复递归。
    2. JSON.parse(JSON.stringify()):简单易用,通过 JSON 序列化与反序列化实现深拷贝,但存在明显局限性。
      var arr = ['hello', ['world', ['张三']]];
      var newArr = JSON.parse(JSON.stringify(arr));
      arr[1][1][0] = '1';
      console.log(newArr[1][1][0]); // "张三"(新数据独立)
      
       
      • 缺点
        • 无法处理函数、undefined(会被过滤);
        • 无法处理循环引用(会报错);
        • 特殊对象(RegExp、Error、Date)会被转换成空对象或错误格式;
        • 丢失对象原型链上的属性。

三、数组扁平化

  • 定义:将多维数组(嵌套数组)转换为一维数组,简化数据遍历与处理。
  • 核心场景:处理多层嵌套的数组数据(如[1, [2, [3, 4]]] → [1,2,3,4])。
  • 实现方式
    1. 递归实现:遍历数组,若元素是数组则递归扁平化,否则直接加入结果数组。
      function flattenArray(arr) {
        let flattened = [];
        arr.forEach(item => {
          // 若元素是数组,递归处理后合并
          if (Array.isArray(item)) {
            flattened = flattened.concat(flattenArray(item));
          } else {
            // 非数组元素直接加入
            flattened.push(item);
          }
        });
        return flattened;
      }
      
      // 测试
      const result = flattenArray([1, [2, 3], [4, [5, 6]]]);
      console.log(result); // [1,2,3,4,5,6]
      
    2. 原生方法
      • Array.prototype.flat(depth):参数depth指定扁平化深度(默认 1,Infinity表示完全扁平化)。
        [1, [2, [3, 4]]].flat(Infinity); // [1,2,3,4]
        
      • Array.prototype.flatMap(callback):先对数组元素执行map操作,再对结果执行flat(1)(仅扁平化 1 层)。

四、函数柯里化(Currying)

  • 定义:将 “接受多个参数的函数” 转换为 “一系列接受单个参数的函数”,每次接受一个参数,直到收集完所有必要参数后返回最终结果。
  • 核心作用:提高函数复用性、实现函数组合(如 React 中的事件绑定优化)。
  • 实现方式
    1. 简单柯里化(固定参数数量)
      // 原函数:接受3个参数
      function add(a, b, c) {
        return a + b + c;
      }
      
      // 柯里化工具函数
      function curry(fn) {
        // curried函数收集参数
        return function curried(...args) {
          // 若收集的参数数量 >= 原函数参数数量,执行原函数
          if (args.length >= fn.length) {
            return fn.apply(null, args);
          } else {
            // 否则返回新函数,继续收集参数
            return function(...moreArgs) {
              return curried.apply(null, args.concat(moreArgs));
            };
          }
        };
      }
      
      // 测试:支持分批次传参
      const curriedAdd = curry(add);
      console.log(curriedAdd(1)(2)(3));  // 6(分3次传参)
      console.log(curriedAdd(1, 2)(3));  // 6(先传2个,再传1个)
      console.log(curriedAdd(1)(2, 3));  // 6(先传1个,再传2个)
      
    2. 实用场景:固定部分参数,生成新函数(如固定接口请求的基础 URL)。
      // 原函数:请求接口(baseUrl + path)
      function request(baseUrl, path) {
        return baseUrl + path;
      }
      
      // 柯里化后,固定baseUrl为"https://api.example.com"
      const apiRequest = curry(request)("https://api.example.com");
      
      // 后续请求只需传path
      console.log(apiRequest("/user")); // "https://api.example.com/user"
      console.log(apiRequest("/order")); // "https://api.example.com/order"
      

更多推荐