跟着 MDN 学 HTML day_39:(DOMException 异常接口完全解析)

一、什么是 DOMException

DOMException 是 Web API 中用于描述错误条件的标准接口。当调用方法或访问 Web API 属性时发生了不正常的事件,就会抛出 DOMException 对象。与普通的 JavaScript 错误不同,DOMException 专门用于表示 DOM 操作中出现的异常情况。

每个 DOMException 都有一个 name 属性,这是一个采用驼峰命名法的简短字符串,用于精确识别错误类型。DOMException 还具备可序列化的特性,这意味着可以使用 structuredClone 进行克隆,或者通过 postMessage 在 Worker 之间传递。

javascript 复制代码
// 尝试一个会产生 DOMException 的操作
try {
  // 创建一个只有单个字符的元素名
  const div = document.createElement('div');
  // 尝试设置一个不合法的 ID 属性
  div.setAttribute('id', '');
} catch (error) {
  if (error instanceof DOMException) {
    console.log('错误类型:', error.name);
    console.log('错误信息:', error.message);
  }
}

// 创建自定义的 DOMException
const customException = new DOMException('自定义错误消息', 'SyntaxError');
console.log(customException.name);     // SyntaxError
console.log(customException.message);  // 自定义错误消息

二、构造函数与基本用法

DOMException 提供了标准的构造函数,可以直接创建异常对象。构造函数接受两个参数:可选的错误消息和可选的错误名称。这使得开发者可以在适当的时候手动抛出 DOMException,保持错误处理的一致性。

javascript 复制代码
// 创建各种类型的 DOMException

// 创建一个基础的 DOMException
const exception1 = new DOMException('发生了一个错误');
console.log(exception1.name);    // 默认为 Error
console.log(exception1.message); // 发生了一个错误

// 创建指定名称的 DOMException
const exception2 = new DOMException('索引超出范围', 'IndexSizeError');
console.log(exception2.name);    // IndexSizeError
console.log(exception2.message); // 索引超出范围

// 创建安全相关的异常
const exception3 = new DOMException('操作不被允许', 'SecurityError');
console.log(exception3.name);    // SecurityError

// 在实际代码中手动抛出 DOMException
function validateIndex(index, maxLength) {
  if (index < 0 || index >= maxLength) {
    throw new DOMException(
      `索引 ${index} 超出范围 [0, ${maxLength - 1}]`,
      'IndexSizeError'
    );
  }
  return true;
}

try {
  validateIndex(5, 3);
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
  // IndexSizeError: 索引 5 超出范围 [0, 2]
}

三、实例属性详解

DOMException 实例包含三个重要的只读属性:name、message 和 code。name 属性返回错误名称的字符串,是最常用的标识方式。message 属性提供了关于错误的详细描述信息。code 属性是一个遗留属性,返回数字错误代码,现代开发中应优先使用 name 属性。

javascript 复制代码
// 演示 DOMException 的各个属性

// 方式一:捕获 API 抛出的异常
try {
  // 创建一个文档片段
  const fragment = document.createDocumentFragment();
  // 尝试将片段添加到自身(非法操作)
  fragment.appendChild(fragment);
} catch (error) {
  console.log('name 属性:', error.name);
  console.log('message 属性:', error.message);
  console.log('code 属性:', error.code);
  console.log('是否是 DOMException:', error instanceof DOMException);
}

// 方式二:创建异常并查看属性
const exceptions = [
  new DOMException('无效字符', 'InvalidCharacterError'),
  new DOMException('未找到元素', 'NotFoundError'),
  new DOMException('操作被拒绝', 'NotAllowedError')
];

exceptions.forEach(ex => {
  console.log(`名称: ${ex.name}, 消息: ${ex.message}, 代码: ${ex.code}`);
});

// 可序列化特性演示
const originalError = new DOMException('测试数据', 'DataCloneError');
const clonedError = structuredClone(originalError);

console.log(originalError.name === clonedError.name);     // true
console.log(originalError.message === clonedError.message); // true
console.log(originalError !== clonedError);                // true(不同对象)

四、IndexSizeError 索引大小错误

IndexSizeError 表示索引不在允许的范围内。当传入的索引值超出合法范围时,许多 DOM 方法都会抛出此异常。例如操作数组或类数组对象时,索引必须是有效值。

javascript 复制代码
// IndexSizeError 示例

// 示例一:setRange 方法中的索引错误
try {
  const range = document.createRange();
  const textNode = document.createTextNode('Hello World');
  
  // 设置范围起始位置:索引 20 超出了文本长度 11
  range.setStart(textNode, 20);
} catch (error) {
  if (error.name === 'IndexSizeError') {
    console.log('捕获到 IndexSizeError:', error.message);
  }
}

// 示例二:substringData 方法中的索引错误
try {
  const textNode = document.createTextNode('Hello');
  // 尝试从索引 10 开始删除,但文本只有 5 个字符
  textNode.deleteData(10, 1);
} catch (error) {
  console.log(`${error.name}: 索引超出文本范围`);
}

// 示例三:安全的索引验证函数
function safeSubstring(text, start, length) {
  if (start < 0 || start > text.length) {
    throw new DOMException(
      `开始索引 ${start} 超出文本范围 [0, ${text.length}]`,
      'IndexSizeError'
    );
  }
  if (length < 0) {
    throw new DOMException(
      `长度 ${length} 不能为负数`,
      'IndexSizeError'
    );
  }
  return text.substring(start, start + length);
}

try {
  safeSubstring('Hello', 10, 2);
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
}

五、HierarchyRequestError 层级请求错误

HierarchyRequestError 表示节点树层次结构有误。当尝试将节点插入到不允许的位置时,例如将父节点插入到子节点中,或者将祖先节点插入到后代节点中,就会触发此异常。

javascript 复制代码
// HierarchyRequestError 示例

// 示例一:将元素插入到自己内部
try {
  const div = document.createElement('div');
  div.appendChild(div); // 不能将元素添加到自身
} catch (error) {
  console.log(`${error.name}: 不能将节点添加到自身`);
}

// 示例二:尝试创建循环引用
try {
  const parent = document.createElement('div');
  const child = document.createElement('span');
  
  parent.appendChild(child);
  // 尝试将父元素添加到子元素中(创建循环)
  child.appendChild(parent);
} catch (error) {
  console.log(`${error.name}: 不能创建循环引用`);
}

// 示例三:DocumentFragment 的正确使用
const fragment = new DocumentFragment();
const container = document.createElement('div');
const item = document.createElement('p');

// 正常情况下可以添加
fragment.appendChild(item);
container.appendChild(fragment); // 这会移动 item 到 container

// 但不能将包含关系的节点错误嵌套
try {
  const newDiv = document.createElement('div');
  const newSpan = document.createElement('span');
  newDiv.appendChild(newSpan);
  newSpan.appendChild(newDiv); // 这会创建循环引用
} catch (error) {
  console.log(`避免循环引用: ${error.name}`);
  console.log('正确的做法是使用 cloneNode 创建副本');
  const clone = newDiv.cloneNode(true);
  newSpan.appendChild(clone); // 这是允许的
}

六、InvalidCharacterError 无效字符错误

InvalidCharacterError 发生在字符串包含无效字符时。在 DOM 操作中,创建元素 ID、设置属性名称或解析 XML 时,如果使用了不合法的字符,就会抛出此异常。

javascript 复制代码
// InvalidCharacterError 示例

// 示例一:无效的 ID 属性值
try {
  const element = document.createElement('div');
  // ID 不能以数字开头
  element.id = '123invalid';
} catch (error) {
  if (error.name === 'InvalidCharacterError') {
    console.log(`ID 无效: ${error.message}`);
  }
}

// 示例二:无效的元素名称
try {
  // 元素名不能包含空格
  const invalidElement = document.createElement('div element');
} catch (error) {
  console.log(`${error.name}: 元素名称包含非法字符`);
}

// 示例三:XML 命名规则验证函数
function validateElementName(name) {
  // XML 元素名必须以字母或下划线开头
  const validPattern = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
  
  if (!validPattern.test(name)) {
    throw new DOMException(
      `元素名 "${name}" 包含无效字符或格式不正确`,
      'InvalidCharacterError'
    );
  }
  return true;
}

function validateId(id) {
  // HTML5 中 ID 不能包含空格
  if (/\s/.test(id)) {
    throw new DOMException(
      `ID "${id}" 不能包含空白字符`,
      'InvalidCharacterError'
    );
  }
  if (id.length === 0) {
    throw new DOMException(
      'ID 不能为空字符串',
      'InvalidCharacterError'
    );
  }
  return true;
}

try {
  validateElementName('123div');
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
}

try {
  validateId('my id');
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
}

七、SecurityError 与 NotAllowedError 安全相关异常

SecurityError 表示操作不安全,通常与跨域限制、安全策略相关。NotAllowedError 表示用户代理或平台不允许该请求,通常是因为用户拒绝了授权或权限不足。

javascript 复制代码
// SecurityError 示例

// 示例一:跨域访问限制
try {
  // 尝试访问不同源的 iframe 内容会抛出 SecurityError
  const iframe = document.createElement('iframe');
  iframe.src = 'https://example.com';
  document.body.appendChild(iframe);
  
  // 尝试访问跨域 iframe 的内容
  // iframe.contentDocument.body; // 这会抛出 SecurityError
} catch (error) {
  console.log(`${error.name}: 跨域访问被阻止`);
}

// NotAllowedError 示例

// 示例二:未授权状态的操作
async function requestMicrophone() {
  try {
    // 如果用户拒绝授权,会抛出 NotAllowedError
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    console.log('获得麦克风权限');
  } catch (error) {
    if (error.name === 'NotAllowedError') {
      console.log('用户拒绝了麦克风访问请求');
    } else if (error.name === 'SecurityError') {
      console.log('当前环境不支持或不安全');
    }
  }
}

// 示例三:权限检查辅助函数
async function checkPermissionAndExecute(permissionName, action) {
  try {
    // 检查权限状态
    const status = await navigator.permissions.query({ name: permissionName });
    
    if (status.state === 'denied') {
      throw new DOMException(
        `${permissionName} 权限被拒绝`,
        'NotAllowedError'
      );
    }
    
    if (status.state === 'prompt') {
      console.log(`需要请求 ${permissionName} 权限`);
    }
    
    return await action();
  } catch (error) {
    if (error.name === 'NotAllowedError') {
      console.error(`权限不足: ${error.message}`);
    } else if (error.name === 'SecurityError') {
      console.error(`安全限制: ${error.message}`);
    }
    throw error;
  }
}

八、NotFoundError 与 NotSupportedError

NotFoundError 在找不到对象时抛出,例如通过 getElementById 找不到元素,或者操作不存在的节点。NotSupportedError 表示当前环境不支持该操作,通常在调用未实现的方法或使用不兼容的特性时出现。

javascript 复制代码
// NotFoundError 示例

// 示例一:查找不存在的节点
try {
  const container = document.getElementById('nonexistent-id');
  if (!container) {
    throw new DOMException(
      '未找到指定 ID 的元素',
      'NotFoundError'
    );
  }
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
}

// 示例二:移除不存在的子节点
try {
  const parent = document.createElement('div');
  const child = document.createElement('span');
  // child 并未添加到 parent 中
  parent.removeChild(child);
} catch (error) {
  console.log(`${error.name}: 子节点不存在`);
}

// NotSupportedError 示例

// 示例三:使用不支持的方法
try {
  const textNode = document.createTextNode('Hello');
  // 文本节点没有 getElementsByTagName 方法
  if (typeof textNode.getElementsByTagName !== 'function') {
    throw new DOMException(
      '文本节点不支持 getElementsByTagName 方法',
      'NotSupportedError'
    );
  }
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
}

// 示例四:特性检测辅助函数
function isOperationSupported(element, operation) {
  if (typeof element[operation] !== 'function') {
    throw new DOMException(
      `元素 ${element.nodeName} 不支持 ${operation} 操作`,
      'NotSupportedError'
    );
  }
  return true;
}

function safeRemoveChild(parent, child) {
  if (!parent.contains(child)) {
    throw new DOMException(
      '子节点不是父节点的后代',
      'NotFoundError'
    );
  }
  return parent.removeChild(child);
}

// 使用示例
const div = document.createElement('div');
const span = document.createElement('span');
div.appendChild(span);

try {
  safeRemoveChild(div, span);
  console.log('成功移除子节点');
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
}

九、DataCloneError 与 QuotaExceededError

DataCloneError 在对象不可被克隆时抛出,常见于 structuredClone 或 postMessage 操作中。QuotaExceededError 在超出存储配额时抛出,例如 localStorage 存储空间不足或 IndexedDB 超出配额。

javascript 复制代码
// DataCloneError 示例

// 示例一:克隆不可克隆的对象
try {
  const domElement = document.createElement('div');
  // DOM 元素不可被结构化克隆
  const cloned = structuredClone(domElement);
} catch (error) {
  if (error.name === 'DataCloneError') {
    console.log('DOM 元素无法被结构化克隆');
  }
}

// 示例二:包含不可克隆属性的对象
try {
  const obj = {
    name: 'test',
    // 函数不可被克隆
    method: function() { return 'hello'; }
  };
  const cloned = structuredClone(obj);
} catch (error) {
  console.log(`${error.name}: 函数无法被克隆`);
}

// QuotaExceededError 示例

// 示例三:localStorage 存储超限
function saveToLocalStorage(key, data) {
  try {
    const serialized = JSON.stringify(data);
    localStorage.setItem(key, serialized);
    console.log('数据保存成功');
  } catch (error) {
    if (error.name === 'QuotaExceededError') {
      console.error('存储空间不足,无法保存数据');
      // 清理旧数据或提示用户
      console.log('当前存储使用情况:', 
        JSON.stringify(localStorage.length) + ' 个项目');
    }
  }
}

// 示例四:模拟存储超限
function testStorageQuota() {
  let counter = 0;
  try {
    while (true) {
      // 不断添加大量数据直到触发配额错误
      const largeData = 'x'.repeat(1024 * 1024); // 1MB 数据
      localStorage.setItem(`key_${counter++}`, largeData);
    }
  } catch (error) {
    if (error.name === 'QuotaExceededError') {
      console.log(`存储已满,共添加了 ${counter} 个条目`);
      console.log('建议使用 IndexedDB 存储大量数据');
    }
  }
}

// 安全的克隆函数
function safeClone(value) {
  try {
    return structuredClone(value);
  } catch (error) {
    if (error.name === 'DataCloneError') {
      console.warn('对象包含不可克隆的内容,使用 JSON 方法替代');
      return JSON.parse(JSON.stringify(value));
    }
    throw error;
  }
}

// 使用示例
const safeObj = { a: 1, b: 2, c: [3, 4, 5] };
const clonedObj = safeClone(safeObj);
console.log(clonedObj); // 成功克隆

DOMException 作为 Web API 的标准错误接口,理解其各种错误类型对于编写健壮的 Web 应用至关重要。在实际开发中,应该根据不同的错误名称采取相应的处理策略,提供友好的用户提示和有效的降级方案。合理使用 try-catch 捕获 DOMException,能够帮助开发者快速定位问题并提升应用的稳定性。


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

相关推荐
渐儿1 小时前
NestJS 教程 Part 2 — 数据层、API 设计与业务异步
前端
渐儿1 小时前
Next.js 教程 Part 2 — 数据获取、Server Actions 与状态
前端
用户125758524361 小时前
XYGo Admin ArtTable 表格组件:一行代码搞定加载、刷新与分页
前端
用户11489669441051 小时前
Promise解析
javascript·面试
gogoing1 小时前
Prettier 配置说明
前端·javascript
十有八七1 小时前
Hermes Agent 自进化实现:从源码到架构的深度拆解
前端·人工智能
渐儿1 小时前
NestJS 生产级开发教程
前端
前端毕业班1 小时前
uni-app onShareAppMessage hook 原理分析
前端·javascript