跟着MDN学HTML_day_48:(Node接口)

跟着 MDN 学 HTML day_48:深入理解 Node 接口


📑 目录

左列章节 右列章节
[一、了解节点的类型与名称](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize) [二、父子关系的导航属性](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize)
[三、兄弟关系的导航属性](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize) [四、textContent 与 nodeValue](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize)
[五、节点的增删改操作](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize) [六、节点的克隆与比较](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize)
[七、遍历技巧与 normalize](#左列章节 右列章节 一、了解节点的类型与名称 二、父子关系的导航属性 三、兄弟关系的导航属性 四、textContent 与 nodeValue 五、节点的增删改操作 六、节点的克隆与比较 七、遍历技巧与 normalize)

一、了解节点的类型与名称

DOM 树中有多种不同类型的节点,区分它们的最直接方式是使用 nodeTypenodeName 属性。nodeType 返回一个整数,对应不同的节点类型常量;nodeName 返回一个字符串,其具体含义取决于节点类型。

代码示例:检测节点类型
javascript 复制代码
const container = document.getElementById('container');

// 检查容器本身
console.log('容器 nodeType:', container.nodeType);  // 1 表示 ELEMENT_NODE
console.log('容器 nodeName:', container.nodeName);  // DIV

// 遍历容器的所有子节点
const childNodes = container.childNodes;
console.log('子节点数量:', childNodes.length);

for (let i = 0; i < childNodes.length; i++) {
  const node = childNodes[i];
  console.log('节点', i, ':');
  console.log('  nodeType:', node.nodeType);
  console.log('  nodeName:', node.nodeName);

  // 根据 nodeType 判断节点类型
  if (node.nodeType === Node.ELEMENT_NODE) {
    console.log('  这是一个元素节点');
  } else if (node.nodeType === Node.TEXT_NODE) {
    console.log('  这是一个文本节点');
  } else if (node.nodeType === Node.COMMENT_NODE) {
    console.log('  这是一个注释节点');
  }
}

核心结论nodeType 的返回值是一个无符号短整数,常见值包括:1 代表元素节点,3 代表文本节点,8 代表注释节点,9 代表文档节点。在代码中使用 Node 接口提供的常量(如 Node.ELEMENT_NODE)可以让代码更具可读性。nodeName 对于元素节点返回的是标签名的大写形式 ,对于文本节点固定返回 #text,对于文档节点返回 #document


⚠️ 【重点 / 面试考点 / 易错点】

常量 数值 说明
Node.ELEMENT_NODE 1 元素节点(如 <div>
Node.TEXT_NODE 3 文本节点
Node.COMMENT_NODE 8 注释节点(<!-- -->
Node.DOCUMENT_NODE 9 文档节点(document
Node.DOCUMENT_TYPE_NODE 10 文档类型节点(<!DOCTYPE>
Node.DOCUMENT_FRAGMENT_NODE 11 文档片段节点
  • nodeName元素节点返回大写标签名 (如 DIV),不是小写
  • nodeName文本节点返回 #text,不是内容本身
  • 判断节点类型应优先使用 nodeType 而非 instanceof,因为跨窗口/iframe 时构造函数不同

二、父子关系的导航属性

Node 接口提供了一组属性用于在 DOM 树中上下导航 ,包括 parentNodechildNodesfirstChildlastChild。这些属性是遍历和操作 DOM 树的基础工具。

代码示例:父子关系导航
javascript 复制代码
const menu = document.getElementById('menu');

// 获取父节点
console.log('menu 的父节点:', menu.parentNode.nodeName);

// 获取所有子节点(包括文本节点)
const childNodes = menu.childNodes;
console.log('childNodes 数量:', childNodes.length);

// 获取第一个和最后一个子节点
console.log('第一个子节点:', menu.firstChild.nodeName);
console.log('最后一个子节点:', menu.lastChild.nodeName);

childNodes 返回的是一个实时的 NodeList ,这意味着如果 DOM 树的结构发生变化,这个列表会自动更新。需要特别注意的是,childNodes 包含所有类型的子节点,包括元素之间的空白文本节点。

代码示例:parentNode 与 parentElement 的区别
javascript 复制代码
// parentNode 返回父节点(可能是 Document)
// parentElement 只返回父元素节点,如果父节点不是元素则返回 null
const textNode = menu.firstChild;
console.log('文本节点的 parentNode:', textNode.parentNode);       // 元素节点
console.log('文本节点的 parentElement:', textNode.parentElement); // 元素节点

// 检查节点是否已连接到 DOM 树
console.log('menu 是否已连接:', menu.isConnected); // true

// 创建一个未连接的节点
const detachedElement = document.createElement('div');
console.log('新创建的元素是否已连接:', detachedElement.isConnected); // false

三、兄弟关系的导航属性

在 DOM 树中,同一层级之间的节点可以相互访问,通过 nextSiblingpreviousSibling 属性可以在同级节点之间自由移动。

代码示例:兄弟节点导航
javascript 复制代码
const secondParagraph = document.getElementById('second');

// 获取上一个同级节点(可能是文本节点)
const prevNode = secondParagraph.previousSibling;
console.log('上一个节点类型:', prevNode.nodeName);

// 获取下一个同级节点(可能是文本节点)
const nextNode = secondParagraph.nextSibling;
console.log('下一个节点类型:', nextNode.nodeName);

nextSiblingpreviousSibling 返回的可能是任意类型的节点(包括空白文本节点)。在实际开发中,我们通常更关心元素节点,因此需要编写辅助函数来跳过空白文本节点。

代码示例:跳过文本节点获取元素兄弟
javascript 复制代码
function getNextElementSibling(node) {
  let next = node.nextSibling;
  while (next && next.nodeType !== Node.ELEMENT_NODE) {
    next = next.nextSibling;
  }
  return next;
}

function getPreviousElementSibling(node) {
  let prev = node.previousSibling;
  while (prev && prev.nodeType !== Node.ELEMENT_NODE) {
    prev = prev.previousSibling;
  }
  return prev;
}

// 使用辅助函数获取相邻的元素节点
const prevElement = getPreviousElementSibling(secondParagraph);
if (prevElement) {
  console.log('上一个元素节点 ID:', prevElement.id);
}

const nextElement = getNextElementSibling(secondParagraph);
if (nextElement) {
  console.log('下一个元素节点 ID:', nextElement.id);
}

核心结论:这种节点间的横向导航能力在构建 DOM 树遍历算法时非常重要,它允许我们从任意节点出发,在树的同一层级上自由移动,而不需要先回到父节点再向下查找。


四、使用 textContent 和 nodeValue 操作内容

Node 接口提供了两个与内容相关的属性:textContentnodeValue。它们在功能上有所重叠,但使用场景和效果有重要区别。

代码示例:textContent 的使用
javascript 复制代码
const article = document.getElementById('article');

// textContent 获取所有文本内容(包括隐藏元素的文本)
console.log('article 的 textContent:', article.textContent);

// 获取第一个段落的 textContent(忽略 HTML 标签)
const firstParagraph = article.querySelector('p');
console.log('第一个段落的 textContent:', firstParagraph.textContent);

// 设置 textContent 会替换所有子节点为单个文本节点
const testDiv = document.createElement('div');
testDiv.innerHTML = '<p>原始内容</p><span>更多内容</span>';
console.log('设置前的子节点数量:', testDiv.childNodes.length); // 2

testDiv.textContent = '全新的纯文本内容';
console.log('设置后的子节点数量:', testDiv.childNodes.length); // 1
代码示例:nodeValue 的使用
javascript 复制代码
// nodeValue 主要用于文本节点
const textNode = firstParagraph.firstChild;
console.log('文本节点的 nodeValue:', textNode.nodeValue);

// 对于元素节点,nodeValue 返回 null
console.log('元素节点的 nodeValue:', firstParagraph.nodeValue); // null

// 使用 nodeValue 修改文本内容
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
  textNode.nodeValue = '这是修改后的文本内容';
  console.log('修改后的段落:', firstParagraph.textContent);
}

⚠️ 【重点 / 面试考点】

属性 适用节点 返回值 设置时的行为
textContent 所有节点 节点及所有后代的纯文本 移除所有子节点,替换为单个文本节点
nodeValue 文本节点、注释节点 文本内容字符串 修改当前节点的文本内容
innerText 元素节点 渲染后的可见文本 触发重排,性能较差
innerHTML 元素节点 HTML 字符串 解析 HTML,有 XSS 风险
  • textContent 无视 CSS 样式 (包括 display:none 的隐藏元素),innerText 会受样式影响
  • textContent 不会触发重排,innerText 会触发重排,性能差距明显
  • nodeValue元素节点返回 null,这是常见易错点
  • 设置 textContent快速清空元素内容的安全方法(无 XSS 风险)

五、节点的增删改操作

Node 接口提供了完整的节点操作方法,包括 appendChildinsertBeforeremoveChildreplaceChild,这些方法构成了 DOM 操作的基础。

代码示例:appendChild 添加节点
javascript 复制代码
const taskList = document.getElementById('taskList');

// 在末尾添加子节点
const newTask = document.createElement('p');
newTask.textContent = '任务四(新增)';
newTask.style.color = 'blue';
taskList.appendChild(newTask);
console.log('添加后子节点数量:', taskList.children.length);
代码示例:insertBefore 在指定位置插入
javascript 复制代码
const referenceNode = document.getElementById('reference');

const priorityTask = document.createElement('p');
priorityTask.textContent = '紧急任务(插入到参考节点之前)';
priorityTask.style.color = 'red';
taskList.insertBefore(priorityTask, referenceNode);
代码示例:replaceChild 替换节点
javascript 复制代码
const replacementTask = document.createElement('p');
replacementTask.textContent = '任务三(已替换)';
replacementTask.style.textDecoration = 'line-through';
taskList.replaceChild(replacementTask, referenceNode);
代码示例:removeChild 移除节点
javascript 复制代码
function removeNode(nodeToRemove) {
  if (nodeToRemove && nodeToRemove.parentNode) {
    nodeToRemove.parentNode.removeChild(nodeToRemove);
    console.log('节点已移除');
  }
}

const tempNode = document.createElement('p');
tempNode.textContent = '临时任务';
taskList.appendChild(tempNode);

console.log('移除前子节点数量:', taskList.children.length);
removeNode(tempNode);
console.log('移除后子节点数量:', taskList.children.length);

// 被移除的节点仍存在于内存中,可重新插入
console.log('被移除节点的父节点:', tempNode.parentNode);       // null
console.log('被移除节点是否连接到 DOM:', tempNode.isConnected); // false

核心结论 :如果使用 appendChildinsertBefore 移动一个已存在于文档中的节点 ,该节点会从其当前位置被移除,然后添加到新位置,而不是创建副本。removeChild 要求传入的节点必须是调用节点的直接子节点,否则会抛出异常。被移除的节点仍然存在于内存中,可以在之后将它插入到文档的其他位置。


六、节点的克隆与比较

Node 接口提供了 cloneNode 方法用于复制节点,以及 compareDocumentPositionisEqualNodecontains 等方法用于比较和判断节点之间的关系。

代码示例:cloneNode 的浅克隆与深克隆
javascript 复制代码
const originalDiv = document.getElementById('original');

// 浅克隆 - 只克隆节点本身,不包含子节点
const shallowClone = originalDiv.cloneNode(false);
console.log('浅克隆的节点名:', shallowClone.nodeName);           // DIV
console.log('浅克隆的子节点数量:', shallowClone.childNodes.length); // 0
console.log('浅克隆的 ID:', shallowClone.id);                    // original

// 深克隆 - 克隆节点及其所有后代节点
const deepClone = originalDiv.cloneNode(true);
console.log('深克隆的子节点数量:', deepClone.childNodes.length);  // 包含所有子节点

重要注意 :克隆的节点不会自动拥有原始节点的事件监听器,事件监听器需要重新绑定。

代码示例:contains 检查后代关系
javascript 复制代码
const body = document.body;
const h3Element = originalDiv.querySelector('h3');

// contains - 检查节点是否为后代节点
console.log('body 是否包含 h3:', body.contains(h3Element)); // true
console.log('body 是否包含自身:', body.contains(body));      // true(节点包含自身)
代码示例:compareDocumentPosition 比较位置
javascript 复制代码
const firstParagraph = originalDiv.querySelector('p');
const relationship = h3Element.compareDocumentPosition(firstParagraph);
console.log('位置关系值:', relationship);

// 解读 compareDocumentPosition 的返回值(位掩码)
if (relationship & Node.DOCUMENT_POSITION_FOLLOWING) {
  console.log('h3 在段落之前(段落在其后)');
}
if (relationship & Node.DOCUMENT_POSITION_CONTAINS) {
  console.log('h3 包含段落');
}
代码示例:isEqualNode 比较节点结构
javascript 复制代码
const div1 = document.createElement('div');
div1.innerHTML = '<p>测试</p>';

const div2 = document.createElement('div');
div2.innerHTML = '<p>测试</p>';

// 比较两个节点结构是否相等(不比较引用)
console.log('两个结构相同的 div 是否相等:', div1.isEqualNode(div2)); // true
console.log('同一节点引用比较:', h3Element === h3Element);            // true

⚠️ 【重点 / 面试考点】

  • cloneNode(true) 深克隆不复制事件监听器,必须重新手动绑定
  • cloneNode(true) 会复制 ID 属性,插入文档前需修改 ID 避免重复
  • compareDocumentPosition 返回位掩码 ,必须用位运算(&)解读
  • contains自身调用返回 true,这是规范定义的行为
  • isEqualNode 比较结构和内容isSameNode 已废弃,直接用 ===
方法 功能 返回值
cloneNode(deep) 复制节点 新的节点副本
contains(node) 检查是否包含某节点 boolean
compareDocumentPosition(node) 比较文档位置 位掩码数字
isEqualNode(node) 结构是否相等 boolean
isSameNode(node) 是否为同一节点 boolean(已废弃)

七、实用遍历技巧与 normalize 方法

在实际开发中,我们经常需要遍历 DOM 树来执行各种操作。同时,normalize 方法可以帮助我们清理文档中碎片化的文本节点。

代码示例:递归遍历 DOM 树
javascript 复制代码
// 递归遍历所有后代节点的函数
function traverseNodes(node, callback) {
  // 对当前节点执行回调
  const shouldContinue = callback(node);
  if (shouldContinue === false) return false;

  // 递归遍历子节点
  if (node.hasChildNodes()) {
    const children = node.childNodes;
    for (let i = 0; i < children.length; i++) {
      if (traverseNodes(children[i], callback) === false) {
        return false;
      }
    }
  }
}

// 搜索包含指定文本的文本节点
function searchText(rootNode, searchTerm) {
  const matches = [];
  traverseNodes(rootNode, function(node) {
    if (node.nodeType === Node.TEXT_NODE) {
      if (node.textContent.indexOf(searchTerm) !== -1) {
        matches.push(node);
      }
    }
  });
  return matches;
}

// 执行搜索
const matches = searchText(searchArea, '搜索');
console.log('找到的匹配节点数量:', matches.length);
代码示例:normalize 合并文本节点
javascript 复制代码
// normalize 方法演示 - 合并相邻文本节点
const testContainer = document.createElement('div');
testContainer.appendChild(document.createTextNode('第一段文本'));
testContainer.appendChild(document.createTextNode('第二段文本'));
testContainer.appendChild(document.createTextNode('第三段文本'));

console.log('normalize 前子节点数量:', testContainer.childNodes.length); // 3

testContainer.normalize();

console.log('normalize 后子节点数量:', testContainer.childNodes.length); // 1
console.log('合并后的文本:', testContainer.textContent);
代码示例:获取根节点
javascript 复制代码
// getRootNode 在 Shadow DOM 场景中特别有用
console.log('searchArea 的根节点:', searchArea.getRootNode().nodeName); // HTML

// hasChildNodes 快速检查
console.log('searchArea 是否有子节点:', searchArea.hasChildNodes()); // true

核心结论normalize 是一个经常被忽视但非常实用的工具,它会将相邻的文本节点合并为一个 ,并移除空的文本节点。在通过脚本频繁操作文本内容后,调用 normalize 可以保持 DOM 树的整洁。getRootNode 在使用了 Shadow DOM 的场景中特别有用,它可以返回包含影子根的实际根节点。


八、Node 接口核心属性与方法速查

节点类型判断
属性 说明
nodeType 节点类型数值(1=元素, 3=文本, 8=注释, 9=文档)
nodeName 节点名称(元素返回大写标签名)
父子导航
属性 说明
parentNode 父节点(可能是 Document)
parentElement 父元素节点(父节点不是元素时返回 null)
childNodes 所有子节点的实时 NodeList
firstChild 第一个子节点
lastChild 最后一个子节点
hasChildNodes() 是否有子节点
isConnected 是否已连接到文档树
兄弟导航
属性 说明
nextSibling 下一个同级节点
previousSibling 上一个同级节点
内容操作
属性 说明
textContent 获取/设置纯文本内容
nodeValue 文本/注释节点的内容(元素节点返回 null)
节点操作
方法 说明
appendChild(node) 在末尾添加子节点
insertBefore(new, ref) 在参考节点前插入
removeChild(node) 移除指定的子节点
replaceChild(new, old) 替换指定的子节点
cloneNode(deep) 克隆节点
normalize() 合并相邻文本节点
getRootNode() 获取根节点
节点比较
方法 说明
contains(node) 是否包含某节点
compareDocumentPosition(node) 比较文档位置
isEqualNode(node) 结构是否相等
Node 在 DOM 继承链中的位置

EventTarget
Node
Element
Text
Comment
Document
DocumentFragment
HTMLElement
HTMLDivElement
HTMLAnchorElement


✅ 文档总结

  • Node 是 DOM 树中所有节点类型的共同祖先ElementTextCommentDocument 等都继承自它
  • nodeType(数值)和 nodeName(字符串)是判断节点类型的核心属性,优先使用 Node.ELEMENT_NODE 等常量
  • childNodes 返回实时的 NodeList ,包含空白文本节点;children 只返回元素子节点
  • parentNode 返回父节点,parentElement 只返回父元素节点(父节点为 Document 时返回 null)
  • textContent 获取/设置纯文本(无视 CSS、无 XSS 风险),nodeValue 仅对文本/注释节点有效
  • appendChild/insertBefore 移动已存在节点时会先从原位置移除removeChild 移除的节点仍存在于内存中
  • cloneNode(true) 深克隆不复制事件监听器 ,且会复制 ID 属性,插入前需修改 ID
  • normalize() 合并相邻文本节点,getRootNode() 在 Shadow DOM 场景中获取实际根节点
  • compareDocumentPosition 返回位掩码,需用位运算解读节点间的位置关系

完整实践示例
javascript 复制代码
// 综合应用:安全的 DOM 树遍历与操作工具函数

/**
 * 安全地移除所有子节点
 * @param {Node} node - 目标节点
 */
function removeAllChildren(node) {
  while (node.firstChild) {
    node.removeChild(node.firstChild);
  }
}

/**
 * 获取所有元素类型的子节点(跳过文本节点)
 * @param {Node} node - 目标节点
 * @returns {Element[]}
 */
function getElementChildren(node) {
  const elements = [];
  let child = node.firstChild;
  while (child) {
    if (child.nodeType === Node.ELEMENT_NODE) {
      elements.push(child);
    }
    child = child.nextSibling;
  }
  return elements;
}

/**
 * 深度克隆节点并重新绑定事件
 * @param {Node} node - 要克隆的节点
 * @param {Function} eventBinder - 事件绑定回调
 * @returns {Node}
 */
function deepCloneWithEvents(node, eventBinder) {
  const clone = node.cloneNode(true);
  // 修改克隆节点的 ID 避免重复
  if (clone.id) {
    clone.id = clone.id + '-clone-' + Date.now();
  }
  // 重新绑定事件
  if (eventBinder) {
    eventBinder(clone);
  }
  return clone;
}

/**
 * 查找包含指定文本的最近祖先元素
 * @param {Node} textNode - 文本节点
 * @param {string} tagName - 目标标签名
 * @returns {Element|null}
 */
function findAncestorElement(textNode, tagName) {
  let current = textNode.parentNode;
  while (current && current.nodeType === Node.ELEMENT_NODE) {
    if (current.nodeName.toLowerCase() === tagName.toLowerCase()) {
      return current;
    }
    current = current.parentNode;
  }
  return null;
}

// 使用示例
document.addEventListener('DOMContentLoaded', function() {
  const container = document.getElementById('container');

  // 1. 安全清空容器
  removeAllChildren(container);

  // 2. 批量创建并插入内容
  const fragment = document.createDocumentFragment();
  for (let i = 0; i < 5; i++) {
    const p = document.createElement('p');
    p.textContent = '段落 ' + (i + 1);
    p.dataset.index = i;
    fragment.appendChild(p);
  }
  container.appendChild(fragment);

  // 3. 获取所有段落元素(跳过空白文本节点)
  const paragraphs = getElementChildren(container);
  console.log('段落数量:', paragraphs.length);

  // 4. 深克隆第一个段落
  const clonedPara = deepCloneWithEvents(paragraphs[0], function(clone) {
    clone.addEventListener('click', function() {
      console.log('克隆段落被点击:', this.textContent);
    });
  });
  container.appendChild(clonedPara);

  // 5. 合并可能的碎片化文本节点
  container.normalize();

  // 6. 验证节点连接状态
  console.log('容器是否连接:', container.isConnected);
  console.log('容器子节点数:', container.childNodes.length);
});

结语

ShadowRoot 接口是 Shadow DOM 技术的核心,它为 Web 组件提供了真正的封装能力。通过 mode 属性,你可以控制 Shadow DOM 内部实现的可访问性;通过 host 属性,内部逻辑可以与外部宿主元素建立联系;innerHTML 让你方便地构建和修改 Shadow DOM 的结构;activeElementgetSelection 使得焦点和选区追踪能够穿透封装的边界;elementFromPointelementsFromPoint 则让坐标定位能力延伸到 Shadow DOM 的内部。而 delegatesFocus 属性更是为组件在复杂页面中的焦点管理提供了优雅的解决方案。掌握 ShadowRoot 接口的这些核心特性,将帮助你构建出封装良好、可维护性强且用户体验出色的 Web 组件。建议你在实际项目中多加练习,将这些概念应用到自定义元素的开发中,逐步建立起对 Shadow DOM 体系的深入理解。


想要解锁更多HTML 核心标签实战、前端零基础入门干货、开发避坑全指南吗?
持续关注,后续将更新CSS 布局实战、JavaScript 交互基础、全站导航开发等硬核内容,带你从新手快速进阶,轻松搞定前端开发!

相关推荐
为何创造硅基生物2 小时前
嵌入式 LVGL / SquareLine UI 标准命名规则(行业通用版)
windows·ui
PieroPc3 小时前
CAMWATCH — 局域网摄像头监控系统 Fastapi + html
前端·python·html·fastapi·监控
weixin_451431563 小时前
【学习笔记】微博视频页面ajax请求与响应数据分析
笔记·学习·音视频
巴巴博一4 小时前
2026 最新:Trae / Cursor 一键接入 taste-skill 完整教程(让 AI 前端告别“AI 味”)
前端·ai·ai编程
kyriewen4 小时前
半夜三点线上崩了,AI替我背了锅——用AI排错,五分钟定位三年老bug
前端·javascript·ai编程
kyriewen4 小时前
我让 AI 当了 24 小时全年无休的“毒舌考官”
前端·ci/cd·ai编程
hexu_blog4 小时前
vue+java实现图片批量压缩
java·前端·vue.js
IT_陈寒5 小时前
为什么你应该学习JavaScript?
前端·人工智能·后端