手写高质量深拷贝:攻克循环引用、Symbol、WeakMap等核心难点

手写高质量深拷贝:攻克循环引用、Symbol、WeakMap等核心难点

引言

在JavaScript开发中,拷贝数据是日常高频操作。浅拷贝仅能复制数据的表层结构,对于引用类型(Object、Array、Map、Set等),拷贝后仍会共享底层数据,修改新数据会同步影响原数据。而深拷贝的核心目标是创建一个与原数据完全独立的新数据副本,所有层级的引用类型都被重新创建,修改副本不会对原数据产生任何影响

原生提供的拷贝方案(如JSON.parse(JSON.stringify()))存在诸多局限性:无法处理循环引用、会丢失Symbol类型属性、无法拷贝特殊引用类型(Map、Set、WeakMap)、会忽略undefined/函数属性等。要实现一个健壮、全面的深拷贝,必须手动攻克这些核心难点。本文将从浅拷贝与深拷贝的区别入手,逐步拆解深拷贝的实现逻辑,重点解决循环引用、Symbol、WeakMap等关键问题,最终实现一个工业级可用的深拷贝函数。

一、前置认知:浅拷贝 vs 深拷贝 vs 赋值

在动手实现深拷贝前,首先要明确赋值、浅拷贝、深拷贝三者的核心差异,这是理解深拷贝本质的基础。

1.1 赋值(赋值引用)

赋值操作仅传递引用类型的内存地址,原变量与新变量指向同一个数据对象,任何一方修改都会同步影响另一方。

ini 复制代码
const obj = { name: "张三", hobbies: ["篮球", "游泳"] };
const obj2 = obj; // 仅赋值引用

obj2.name = "李四";
obj2.hobbies.push("跑步");

console.log(obj.name); // 李四(被修改)
console.log(obj.hobbies); // ["篮球", "游泳", "跑步"](被修改)

1.2 浅拷贝(表层拷贝)

浅拷贝仅复制数据的表层结构,对于基本类型(string、number、boolean等)会复制具体值,对于引用类型仅复制内存地址,深层引用类型仍会共享。

常见浅拷贝方法:Object.assign()、扩展运算符{...}Array.prototype.slice()

ini 复制代码
const obj = { name: "张三", hobbies: ["篮球", "游泳"] };
const obj2 = { ...obj }; // 浅拷贝

obj2.name = "李四"; // 修改表层基本类型,不影响原数据
obj2.hobbies.push("跑步"); // 修改深层引用类型,影响原数据

console.log(obj.name); // 张三(不受影响)
console.log(obj.hobbies); // ["篮球", "游泳", "跑步"](被修改)

1.3 深拷贝(全层级拷贝)

深拷贝会递归遍历原数据的所有层级,为每一层的引用类型创建新的实例,最终生成一个与原数据完全独立的副本,两者无任何内存共享。

javascript 复制代码
const obj = { name: "张三", hobbies: ["篮球", "游泳"] };
const obj2 = deepClone(obj); // 手写深拷贝

obj2.name = "李四";
obj2.hobbies.push("跑步");

console.log(obj.name); // 张三(不受影响)
console.log(obj.hobbies); // ["篮球", "游泳"](不受影响)

二、深拷贝的核心难点剖析

手写深拷贝的核心挑战不在于表层数据的复制,而在于处理各种特殊场景和数据类型,其中最关键的三个难点如下:

  1. 循环引用问题 :原数据中存在自身引用或相互引用(如obj.self = obj),递归拷贝时会陷入无限循环,最终导致栈溢出。
  2. Symbol类型处理:Symbol是ES6新增的基本数据类型,可作为对象的属性名,原生浅拷贝方案无法正确复制Symbol属性,深拷贝需要主动识别并拷贝Symbol类型的键与值。
  3. 特殊引用类型处理:除了普通Object和Array,JavaScript还有Map、Set、WeakMap、WeakSet等内置引用类型,这些类型有专属的构造函数和数据结构,需要针对性处理才能完成正确拷贝。
  4. 额外难点:处理undefined、null、函数、日期、正则等特殊数据,保证拷贝后的数据类型和功能与原数据一致。

三、分步实现:从基础版本到工业级版本

我们将采用"循序渐进"的方式,从最基础的递归拷贝开始,逐步加入对各难点的解决方案,最终实现一个健壮的深拷贝函数。

3.1 版本1:基础递归拷贝(仅支持普通Object/Array)

该版本实现核心递归逻辑,能够处理普通对象和数组的深拷贝,但无法解决循环引用、Symbol、特殊引用类型等问题。

实现思路
  1. 先判断数据类型,对于基本类型(除Symbol外),直接返回原值(基本类型赋值即拷贝)。
  2. 对于Array,创建新数组,递归遍历原数组的每一项,拷贝后放入新数组。
  3. 对于Object,创建新对象,遍历原对象的可枚举属性,递归拷贝属性值后赋值给新对象。
  4. 对于其他未处理的类型,暂时直接返回原值。
javascript 复制代码
/**
 * 版本1:基础递归深拷贝(仅支持普通Object/Array)
 * @param {*} target 要拷贝的目标数据
 * @returns 拷贝后的新数据
 */
function deepCloneV1(target) {
  // 1. 处理基本类型(null 单独判断,因为 typeof null === 'object')
  if (target === null || typeof target !== 'object') {
    return target;
  }

  // 2. 处理 Array
  let result;
  if (target instanceof Array) {
    result = [];
    for (let i = 0; i < target.length; i++) {
      result[i] = deepCloneV1(target[i]);
    }
  }

  // 3. 处理 普通 Object
  else if (target instanceof Object) {
    result = {};
    for (const key in target) {
      // 仅拷贝自身可枚举属性(排除原型链属性)
      if (target.hasOwnProperty(key)) {
        result[key] = deepCloneV1(target[key]);
      }
    }
  }

  // 4. 其他未处理的类型(Map、Set、Symbol 等)
  return result;
}
测试验证
javascript 复制代码
// 测试普通对象
const obj1 = { name: "张三", age: 20, hobbies: ["篮球", "游泳"] };
const obj1Clone = deepCloneV1(obj1);
obj1Clone.hobbies.push("跑步");
console.log(obj1.hobbies); // ["篮球", "游泳"](拷贝成功)
console.log(obj1Clone.hobbies); // ["篮球", "游泳", "跑步"]

// 测试循环引用(会报错:Maximum call stack size exceeded 栈溢出)
const obj2 = { name: "李四" };
obj2.self = obj2; // 循环引用
// const obj2Clone = deepCloneV1(obj2); // 执行会栈溢出

// 测试 Symbol 属性(无法拷贝)
const symKey = Symbol("id");
const obj3 = { [symKey]: 123, name: "王五" };
const obj3Clone = deepCloneV1(obj3);
console.log(obj3Clone[symKey]); // undefined(Symbol 属性丢失)
存在问题
  1. 无法处理循环引用,会导致栈溢出。
  2. 无法拷贝Symbol类型的属性名和属性值。
  3. 无法处理Map、Set、WeakMap等特殊引用类型。
  4. 无法处理日期、正则等特殊对象。

3.2 版本2:解决Symbol类型拷贝问题

Symbol类型有两种使用场景:作为对象的属性名、作为属性值。要正确拷贝Symbol,需要:

  1. 遍历对象时,不仅要遍历普通字符串键,还要遍历Symbol键(使用Object.getOwnPropertySymbols())。
  2. 对于属性值为Symbol类型的,直接返回新的Symbol(或原值,Symbol是基本类型,赋值即拷贝)。
实现思路
  1. 增加对Symbol类型的判断,若目标是Symbol类型,直接返回Symbol(target.description)(保持描述一致,创建新的Symbol实例)。
  2. 处理普通Object时,先遍历普通字符串键,再遍历Symbol键,将所有自身属性拷贝到新对象。
javascript 复制代码
/**
 * 版本2:解决 Symbol 类型拷贝问题
 * @param {*} target 要拷贝的目标数据
 * @returns 拷贝后的新数据
 */
function deepCloneV2(target) {
  // 1. 处理基本类型(包含 null)
  if (target === null || typeof target !== 'object') {
    // 单独处理 Symbol 基本类型
    if (typeof target === 'symbol') {
      return Symbol(target.description);
    }
    return target;
  }

  // 2. 处理 Array
  let result;
  if (target instanceof Array) {
    result = [];
    for (let i = 0; i < target.length; i++) {
      result[i] = deepCloneV2(target[i]);
    }
  }

  // 3. 处理 普通 Object(包含 Symbol 键)
  else if (target instanceof Object) {
    result = {};
    // 3.1 遍历普通字符串键
    for (const key in target) {
      if (target.hasOwnProperty(key)) {
        result[key] = deepCloneV2(target[key]);
      }
    }
    // 3.2 遍历 Symbol 键
    const symbolKeys = Object.getOwnPropertySymbols(target);
    for (const symKey of symbolKeys) {
      if (target.hasOwnProperty(symKey)) {
        result[symKey] = deepCloneV2(target[symKey]);
      }
    }
  }

  // 4. 其他未处理的类型
  return result;
}
测试验证
javascript 复制代码
// 测试 Symbol 属性名和属性值
const symKey = Symbol("id");
const symValue = Symbol("userName");
const obj4 = { 
  [symKey]: 123, 
  name: symValue, 
  info: { age: 20 }
};
const obj4Clone = deepCloneV2(obj4);

console.log(obj4[symKey]); // 123
console.log(obj4Clone[symKey]); // 123(Symbol 键拷贝成功)
console.log(obj4.name === obj4Clone.name); // false(Symbol 值创建了新实例,拷贝成功)
console.log(obj4.name.description); // userName
console.log(obj4Clone.name.description); // userName(描述保持一致)

obj4Clone.info.age = 30;
console.log(obj4.info.age); // 20(深层拷贝成功)
存在问题
  1. 仍无法处理循环引用,会栈溢出。
  2. 无法处理Map、Set、WeakMap等特殊引用类型。

3.3 版本3:解决循环引用问题(核心:WeakMap 缓存)

循环引用的本质是"递归过程中,再次遇到已经拷贝过的对象",要解决这个问题,需要通过一个缓存容器记录已经拷贝过的对象,当再次遇到该对象时,直接返回缓存中的副本,而不是继续递归拷贝

为什么选择 WeakMap 作为缓存容器?
  1. 键可以是对象类型:我们需要以"原对象"作为键,"拷贝后的新对象"作为值,Map和WeakMap都支持对象作为键,而普通Object仅支持字符串/Symbol作为键。
  2. 弱引用特性,避免内存泄漏:WeakMap的键是对对象的弱引用,当原对象不再被其他变量引用时,垃圾回收机制(GC)可以直接回收该对象,不会因为缓存容器的强引用而导致内存无法释放。如果使用Map作为缓存,会形成强引用,即使原对象被销毁,缓存中的对象也无法被GC回收,长期运行会导致内存泄漏。
  3. 自动清理无效引用:WeakMap不会阻止垃圾回收,当缓存的对象被回收后,对应的键值对会自动从WeakMap中移除,无需手动清理,更适合作为深拷贝的缓存容器。
实现思路
  1. 新增一个cache参数(WeakMap类型),用于缓存已拷贝的对象,默认值为new WeakMap()

  2. 递归拷贝前,先判断cache中是否存在当前目标对象:

    1. 若存在,直接返回缓存中的新对象,终止递归。
    2. 若不存在,创建新对象(数组/普通对象),将"原对象-新对象"的映射存入cache,再进行递归拷贝。
  3. 为了方便调用,对外暴露的函数无需传入cache,内部递归时传递cache

javascript 复制代码
/**
 * 版本3:解决循环引用问题(使用 WeakMap 缓存)
 * @param {*} target 要拷贝的目标数据
 * @param {WeakMap} cache 缓存容器,记录已拷贝的对象
 * @returns 拷贝后的新数据
 */
function deepCloneV3(target, cache = new WeakMap()) {
  // 1. 处理基本类型(包含 null、Symbol)
  if (target === null || typeof target !== 'object') {
    if (typeof target === 'symbol') {
      return Symbol(target.description);
    }
    return target;
  }

  // 2. 处理循环引用:若缓存中存在,直接返回缓存的新对象
  if (cache.has(target)) {
    return cache.get(target);
  }

  // 3. 处理 Array
  let result;
  if (target instanceof Array) {
    result = [];
    // 3.1 存入缓存(原对象 -> 新对象)
    cache.set(target, result);
    // 3.2 递归拷贝数组项
    for (let i = 0; i < target.length; i++) {
      result[i] = deepCloneV3(target[i], cache);
    }
  }

  // 4. 处理 普通 Object(包含 Symbol 键)
  else if (target instanceof Object) {
    result = {};
    // 4.1 存入缓存
    cache.set(target, result);
    // 4.2 遍历普通字符串键
    for (const key in target) {
      if (target.hasOwnProperty(key)) {
        result[key] = deepCloneV3(target[key], cache);
      }
    }
    // 4.3 遍历 Symbol 键
    const symbolKeys = Object.getOwnPropertySymbols(target);
    for (const symKey of symbolKeys) {
      if (target.hasOwnProperty(symKey)) {
        result[symKey] = deepCloneV3(target[symKey], cache);
      }
    }
  }

  // 5. 其他未处理的类型
  return result;
}
测试验证
javascript 复制代码
// 测试循环引用(不再栈溢出,拷贝成功)
const obj5 = { name: "李四", age: 25 };
obj5.self = obj5; // 自身循环引用
obj5.friend = { name: "王五", ref: obj5 }; // 相互循环引用
const obj5Clone = deepCloneV3(obj5);

console.log(obj5Clone.self === obj5Clone); // true(循环引用处理成功,指向新对象自身)
console.log(obj5Clone.friend.ref === obj5Clone); // true(相互引用处理成功)
console.log(obj5Clone.name); // 李四
console.log(obj5Clone.age); // 25

// 修改副本,不影响原数据
obj5Clone.age = 30;
console.log(obj5.age); // 25
console.log(obj5Clone.age); // 30
存在问题
  1. 仍无法处理Map、Set、WeakMap等特殊引用类型。
  2. 无法处理日期、正则等特殊对象。

3.4 版本4:支持 Map、Set、WeakMap 等特殊引用类型

JavaScript中的内置引用类型(Map、Set、WeakMap、WeakSet、Date、RegExp)都有专属的构造函数和数据结构,拷贝时需要:

  1. 先识别数据类型,通过Object.prototype.toString.call()获取准确的类型标识。
  2. 调用对应类型的构造函数创建新实例。
  3. 针对性遍历数据内容,递归拷贝后存入新实例。
关键说明
  1. Map :可通过entries()遍历键值对,键和值都需要深拷贝,新实例通过new Map()创建。
  2. Set :可通过values()遍历成员,成员需要深拷贝,新实例通过new Set()创建。
  3. WeakMap :由于WeakMap的键是弱引用,且不支持遍历(没有entries()keys()等方法),无法直接拷贝其键值对,本文采用"创建空WeakMap实例"的方案(若需完整拷贝,需借助额外API,且不符合WeakMap的设计初衷)。
  4. Date/RegExp :Date直接通过new Date(target)拷贝,RegExp通过new RegExp(target.source, target.flags)拷贝。
javascript 复制代码
/**
 * 版本4:支持 Map、Set、WeakMap 等特殊引用类型
 * @param {*} target 要拷贝的目标数据
 * @param {WeakMap} cache 缓存容器,记录已拷贝的对象
 * @returns 拷贝后的新数据
 */
function deepCloneV4(target, cache = new WeakMap()) {
  // 1. 处理基本类型(包含 null、Symbol)
  if (target === null || typeof target !== 'object') {
    if (typeof target === 'symbol') {
      return Symbol(target.description);
    }
    return target;
  }

  // 2. 处理循环引用:若缓存中存在,直接返回缓存的新对象
  if (cache.has(target)) {
    return cache.get(target);
  }

  // 3. 获取准确的数据类型标识
  const type = Object.prototype.toString.call(target);
  let result;

  // 4. 处理 Array
  if (type === '[object Array]') {
    result = [];
    cache.set(target, result);
    for (let i = 0; i < target.length; i++) {
      result[i] = deepCloneV4(target[i], cache);
    }
  }

  // 5. 处理 Object(普通对象)
  else if (type === '[object Object]') {
    result = {};
    cache.set(target, result);
    // 5.1 遍历普通字符串键
    for (const key in target) {
      if (target.hasOwnProperty(key)) {
        result[key] = deepCloneV4(target[key], cache);
      }
    }
    // 5.2 遍历 Symbol 键
    const symbolKeys = Object.getOwnPropertySymbols(target);
    for (const symKey of symbolKeys) {
      if (target.hasOwnProperty(symKey)) {
        result[symKey] = deepCloneV4(target[symKey], cache);
      }
    }
  }

  // 6. 处理 Map
  else if (type === '[object Map]') {
    result = new Map();
    cache.set(target, result);
    for (const [key, value] of target.entries()) {
      // Map 的键可以是任意类型,需要深拷贝键和值
      result.set(deepCloneV4(key, cache), deepCloneV4(value, cache));
    }
  }

  // 7. 处理 Set
  else if (type === '[object Set]') {
    result = new Set();
    cache.set(target, result);
    for (const value of target.values()) {
      result.add(deepCloneV4(value, cache));
    }
  }

  // 8. 处理 WeakMap(无法遍历,创建空实例)
  else if (type === '[object WeakMap]') {
    result = new WeakMap();
    cache.set(target, result);
    // 注意:WeakMap 不支持遍历,无法拷贝具体键值对,仅创建空实例
  }

  // 9. 处理 Date
  else if (type === '[object Date]') {
    result = new Date(target);
    cache.set(target, result);
  }

  // 10. 处理 RegExp
  else if (type === '[object RegExp]') {
    result = new RegExp(target.source, target.flags);
    cache.set(target, result);
  }

  // 11. 其他未支持的类型(如 Function、WeakSet)
  else {
    result = target;
    cache.set(target, result);
  }

  return result;
}
测试验证
javascript 复制代码
// 测试 Map
const map1 = new Map();
map1.set(Symbol("key1"), { name: "张三" });
map1.set(123, ["篮球", "游泳"]);
const map1Clone = deepCloneV4(map1);
map1Clone.get(Symbol("key1")).name = "李四";
map1Clone.get(123).push("跑步");
console.log(map1.get(Symbol("key1")).name); // 张三(拷贝成功)
console.log(map1.get(123)); // ["篮球", "游泳"](拷贝成功)

// 测试 Set
const set1 = new Set([1, 2, { age: 20 }]);
const set1Clone = deepCloneV4(set1);
set1Clone.forEach(item => {
  if (typeof item === 'object' && item !== null) {
    item.age = 30;
  }
});
console.log([...set1][2].age); // 20(拷贝成功)
console.log([...set1Clone][2].age); // 30(拷贝成功)

// 测试 WeakMap
const weakMap1 = new WeakMap();
const obj6 = { name: "王五" };
weakMap1.set(obj6, "test");
const weakMap1Clone = deepCloneV4(weakMap1);
console.log(weakMap1Clone instanceof WeakMap); // true(创建空实例成功)

// 测试 Date
const date1 = new Date("2024-01-01");
const date1Clone = deepCloneV4(date1);
date1Clone.setFullYear(2025);
console.log(date1.getFullYear()); // 2024(拷贝成功)
console.log(date1Clone.getFullYear()); // 2025(拷贝成功)

3.5 版本5:最终优化(工业级可用)

在版本4的基础上,进行细节优化,提升函数的健壮性和可用性:

  1. 处理函数类型(虽然函数一般无需深拷贝,直接返回原函数或新函数包装)。
  2. 优化类型判断逻辑,提取公共工具函数。
  3. 增加对undefinedNaN等特殊值的处理。
  4. 严格遵循"深拷贝"原则,确保所有引用类型都完全独立。
javascript 复制代码
/**
 * 公共工具函数:获取准确的数据类型
 * @param {*} target 目标数据
 * @returns 数据类型标识(如 [object Array])
 */
function getType(target) {
  return Object.prototype.toString.call(target);
}

/**
 * 版本5:工业级可用的深拷贝(最终版)
 * 支持:循环引用、Symbol、Map、Set、WeakMap、Date、RegExp 等
 * @param {*} target 要拷贝的目标数据
 * @param {WeakMap} cache 缓存容器,记录已拷贝的对象
 * @returns 拷贝后的新数据
 */
function deepClone(target, cache = new WeakMap()) {
  // 1. 处理基本类型
  if (target === null || typeof target !== 'object') {
    // 处理 Symbol 基本类型
    if (typeof target === 'symbol') {
      return Symbol(target.description);
    }
    // 处理 undefined、NaN、Infinity 等特殊值
    return target;
  }

  // 2. 处理循环引用
  if (cache.has(target)) {
    return cache.get(target);
  }

  const type = getType(target);
  let result;

  // 3. 处理各类引用类型
  switch (type) {
    // 3.1 数组
    case '[object Array]':
      result = [];
      cache.set(target, result);
      target.forEach((item, index) => {
        result[index] = deepClone(item, cache);
      });
      break;

    // 3.2 普通对象
    case '[object Object]':
      result = {};
      cache.set(target, result);
      // 遍历普通键
      Object.keys(target).forEach(key => {
        result[key] = deepClone(target[key], cache);
      });
      // 遍历 Symbol 键
      Object.getOwnPropertySymbols(target).forEach(symKey => {
        result[symKey] = deepClone(target[symKey], cache);
      });
      break;

    // 3.3 Map
    case '[object Map]':
      result = new Map();
      cache.set(target, result);
      target.forEach((value, key) => {
        result.set(deepClone(key, cache), deepClone(value, cache));
      });
      break;

    // 3.4 Set
    case '[object Set]':
      result = new Set();
      cache.set(target, result);
      target.forEach(value => {
        result.add(deepClone(value, cache));
      });
      break;

    // 3.5 WeakMap
    case '[object WeakMap]':
      result = new WeakMap();
      cache.set(target, result);
      // 无法遍历,仅创建空实例
      break;

    // 3.6 Date
    case '[object Date]':
      result = new Date(target);
      cache.set(target, result);
      break;

    // 3.7 RegExp
    case '[object RegExp]':
      result = new RegExp(target.source, target.flags);
      cache.set(target, result);
      break;

    // 3.8 函数(直接返回原函数,函数一般无需深拷贝)
    case '[object Function]':
      result = target;
      cache.set(target, result);
      break;

    // 3.9 其他未支持的类型
    default:
      result = target;
      cache.set(target, result);
      break;
  }

  return result;
}

四、核心知识点总结与避坑指南

4.1 核心知识点回顾

  1. 深拷贝的本质:递归遍历所有数据层级,为每个引用类型创建新实例,实现数据完全独立。
  2. 循环引用的解决:使用WeakMap作为缓存容器,记录已拷贝的对象,避免无限递归。
  3. WeakMap的优势:弱引用特性,避免内存泄漏,自动清理无效引用,适合作为深拷贝缓存。
  4. Symbol的处理 :遍历Object.getOwnPropertySymbols()获取Symbol键,创建新Symbol实例保持描述一致。
  5. 特殊引用类型的处理:通过准确的类型判断,调用对应构造函数创建新实例,针对性遍历拷贝数据。

4.2 常见避坑点

  1. 忽略 null 的判断typeof null === 'object',若不单独判断,会将null当作对象处理,导致错误。
  2. 使用Map作为缓存:Map会形成强引用,导致原对象无法被垃圾回收,引发内存泄漏,优先使用WeakMap。
  3. 直接修改原对象属性:深拷贝过程中,避免修改原数据,确保拷贝的纯洁性。
  4. 忽略原型链属性 :使用Object.keys()hasOwnProperty()过滤原型链属性,仅拷贝自身可枚举属性。
  5. 函数的深拷贝:函数一般无需深拷贝,因为函数的执行依赖作用域,深拷贝函数可能导致作用域丢失,直接返回原函数即可。

五、总结

手写一个健壮的深拷贝函数,不仅需要掌握递归的核心逻辑,还需要深入理解JavaScript的数据类型、内存模型、垃圾回收等底层知识。本文从基础版本到工业级版本,逐步攻克了循环引用、Symbol、WeakMap等核心难点,最终实现的深拷贝函数能够满足大部分业务场景的需求。

需要注意的是,深拷贝是一个相对昂贵的操作(递归遍历、创建大量新实例),在性能敏感的场景中,应谨慎使用,可考虑:

  1. 按需拷贝:仅拷贝需要使用的数据,而非整个对象。
  2. 浅拷贝替代:若数据仅有表层引用类型,可使用浅拷贝提升性能。
  3. 第三方库:如lodash.cloneDeep(),经过严格测试,支持更多边缘场景,工业项目中可优先使用。
相关推荐
Irene19912 小时前
使用 TypeScript 编写一个 Vue 3 模态框(Modal)组件
javascript·vue.js·typescript
踢球的打工仔2 小时前
typescript-void和never
前端·javascript·typescript
hugo_im2 小时前
GrapesJS 完全指南:从零构建你的可视化拖拽编辑器
前端·javascript·前端框架
盘子素2 小时前
前端实现跳转子系统,但限制只能跳转一次
前端·javascript
小鸡脚来咯2 小时前
Linux 服务器问题排查指南(面试标准回答)
linux·服务器·面试
float_六七2 小时前
JS比较运算符:从坑点速记到实战口诀
开发语言·javascript·ecmascript
奔跑的web.3 小时前
TypeScript 全面详解:对象类型的语法规则
开发语言·前端·javascript·typescript·vue
byzh_rc3 小时前
[微机原理与系统设计-从入门到入土] 微型计算机基础
开发语言·javascript·ecmascript
Murrays3 小时前
【React】01 初识 React
前端·javascript·react.js