DOM事件流与事件委托、判断数据类型、深浅拷贝、对象遍历方式

文章目录

  • [一、DOM 事件流与事件委托](#一、DOM 事件流与事件委托)
    • [1. 什么是 DOM 事件流?](#1. 什么是 DOM 事件流?)
    • [2. 代码示例:控制触发阶段](#2. 代码示例:控制触发阶段)
    • [3. 为什么这样设计?(设计初衷)](#3. 为什么这样设计?(设计初衷))
    • [4. 什么是事件委托 (Event Delegation)?](#4. 什么是事件委托 (Event Delegation)?)
  • [二、Js 中数据类型与类型判断](#二、Js 中数据类型与类型判断)
    • [1. JS 数据类型概览](#1. JS 数据类型概览)
    • [2. 检测数据类型的方法及示例](#2. 检测数据类型的方法及示例)
      • [A. typeof:基础检测](#A. typeof:基础检测)
        • [typeof 的陷阱](#typeof 的陷阱)
      • [B. instanceof:检测原型链](#B. instanceof:检测原型链)
      • [C. constructor:查看构造函数](#C. constructor:查看构造函数)
      • [D. === :查看 null、undefined 以及布尔值](#D. === :查看 null、undefined 以及布尔值)
      • 终极武器:Object.prototype.toString.call()
      • [JavaScript 数据类型检测方法深度对比表](#JavaScript 数据类型检测方法深度对比表)
  • 三、深拷贝与浅拷贝
    • [1. 深浅拷贝的本质:内存地址的博弈](#1. 深浅拷贝的本质:内存地址的博弈)
    • [2. 浅拷贝的方法与示例](#2. 浅拷贝的方法与示例)
      • [A. Object.assign()](#A. Object.assign())
      • [B. 扩展运算符 ... (Spread)](#B. 扩展运算符 ... (Spread))
      • [C. 数组方法:slice() 和 concat()](#C. 数组方法:slice() 和 concat())
    • [3. 深拷贝的方法与示例](#3. 深拷贝的方法与示例)
      • [A. 穷人版:JSON.parse(JSON.stringify())](#A. 穷人版:JSON.parse(JSON.stringify()))
      • [B. 标准版:structuredClone() (现代浏览器原生)](#B. 标准版:structuredClone() (现代浏览器原生))
      • [C. 库函数:Lodash 的 _.cloneDeep()](#C. 库函数:Lodash 的 _.cloneDeep())
      • [D. 进阶版:手写递归实现](#D. 进阶版:手写递归实现)
    • [4. JavaScript 深浅拷贝深度对比表](#4. JavaScript 深浅拷贝深度对比表)
    • [5. 深拷贝常用方法优缺点对比表](#5. 深拷贝常用方法优缺点对比表)
  • 四、对象遍历方式对比
    • [1. 核心遍历方法对比表](#1. 核心遍历方法对比表)
    • [2. 代码案例演示](#2. 代码案例演示)
      • [A. 经典的 for...in (带继承) + hasOwnProperty](#A. 经典的 for...in (带继承) + hasOwnProperty)
      • [B. 现代常用的 Object.entries()](#B. 现代常用的 Object.entries())
      • [C. "全能王" Reflect.ownKeys()](#C. “全能王” Reflect.ownKeys())
    • [3. 性能与选择建议](#3. 性能与选择建议)

一、DOM 事件流与事件委托

1. 什么是 DOM 事件流?

当你在页面上点击一个按钮时,你不仅仅是点击了那个按钮,同时也点击了它的父级容器<body> 乃至整个 window。事件流描述的就是事件在页面中传播的顺序。

标准的 DOM 事件流分为三个阶段:

  • 捕获阶段 (Capture Phase): 事件从 window 向下传播到目标节点。

  • 目标阶段 (Target Phase): 事件到达目标节点。

  • 冒泡阶段 (Bubbling Phase): 事件从目标节点向上传播回 window。

2. 代码示例:控制触发阶段

在 JavaScript 中,我们通过 addEventListener 的第三个参数来控制事件在哪个阶段触发。
基础结构

javascript 复制代码
// 第三个参数是 useCapture (布尔值),默认为 false (冒泡阶段触发)
target.addEventListener('click', handler, useCapture);

3. 为什么这样设计?(设计初衷)

这种设计其实是历史上两大浏览器巨头斗争的产物:Netscape 坚持捕获,而 IE 坚持冒泡。W3C 最终决定兼容两者,制定了先捕获后冒泡的标准。

它的实际用途在于:

  • 控制优先级: 捕获阶段允许父元素在子元素收到事件之前先"截获"它(类似于权限拦截)。

  • 灵活性: 开发者可以自由选择在哪个阶段处理逻辑,为事件委托提供了技术基础。

4. 什么是事件委托 (Event Delegation)?

事件委托是利用"事件冒泡"机制,将原本需要绑定在子元素上的监听器,统一绑定在它们的父元素上。

为什么要这么做?(好处)

  • 节省内存: 如果你有 1000 个列表项(

  • ),给每个

  • 绑定点击事件会消耗大量内存。绑定在父级

    • 上只需要一个监听器。
  • 动态绑定: 如果你通过 AJAX 新增了列表项,这些新元素不需要重新绑定事件,因为它们触发的事件依然会冒泡到父元素。

二、Js 中数据类型与类型判断

1. JS 数据类型概览

JavaScript 目前共有 8 种 内置类型:

  • 基本类型 (Primitive): Number, String, Boolean, Undefined, Null, Symbol (ES6), BigInt (ES2020)。

  • 引用类型 (Reference): Object(包括子类型 Array, Function, Date, RegExp 等)。

2. 检测数据类型的方法及示例

A. typeof:基础检测

主要用于检测基本类型和函数。

javascript 复制代码
console.log(typeof "hello");    // "string"
console.log(typeof 123);        // "number"
console.log(typeof true);       // "boolean"
console.log(typeof undefined);  // "undefined"
console.log(typeof Symbol());   // "symbol"
console.log(typeof 10n);        // "bigint"
console.log(typeof function(){}); // "function"

// 陷阱与局限
console.log(typeof null);       // "object" (历史遗留 Bug)
console.log(typeof []);         // "object"
console.log(typeof {});         // "object"
typeof 的陷阱
  • 无法区分对象子类型: 数组、对象、正则、日期在 typeof 看来全是 "object"。

  • null 的误判: 如上所述,判断 null 需要用 val === null。

  • 暂时性死区: 在 let/const 声明前使用 typeof 会报错,而不是返回 undefined。

B. instanceof:检测原型链

主要用于判断一个实例是否属于某个构造函数。

instanceof 的运行机制是检查右边构造函数的 prototype 是否在左边变量的原型链上。

  • 判断基础类型(Primitive Types):当你使用字面量定义基础类型时,它们不是对象,因此没有原型链连接到对应的构造函数上。
  • 判断 null 和 undefined:null 和 undefined 没有任何属性,也没有原型链,所以 instanceof 总是返回 false。
  • 判断数组([])和 函数(function):对于引用类型,instanceof 非常有效,但要注意它的"向上兼容"特性。
javascript 复制代码
// --- 基础类型字面量 (Primitive Literals) ---
console.log("hello" instanceof String);    // false
console.log(123 instanceof Number);        // false
console.log(true instanceof Boolean);      // false
console.log(Symbol() instanceof Symbol);   // false
console.log(10n instanceof BigInt);        // false

// --- 特殊情况 ---
console.log(undefined instanceof Object);  // false
console.log(null instanceof Object);       // false (它是唯一 typeof 为 object 但 instanceof 为 false 的)

// --- 引用类型 (Reference Types) ---
console.log(function(){} instanceof Function); // true
console.log(function(){} instanceof Object);   // true (函数也是对象)

console.log([] instanceof Array);          // true
console.log([] instanceof Object);         // true (数组也是对象)

console.log({} instanceof Object);         // true

C. constructor:查看构造函数

利用实例的 constructor 属性。

javascript 复制代码
// --- 基础类型 (通过包装类型判断) ---
// 注意:基础类型在访问 constructor 时,JS 会自动将其包装为对象(Boxing)
console.log("hello".constructor === String);    // true
console.log((123).constructor === Number);      // true
console.log(true.constructor === Boolean);      // true
console.log(Symbol().constructor === Symbol);   // true
console.log((10n).constructor === BigInt);      // true

// --- 引用类型 ---
console.log((function(){}).constructor === Function); // true
console.log([].constructor === Array);                // true
console.log({}.constructor === Object);               // true

// --- 陷阱区:null 和 undefined ---
// 报错!因为 null 和 undefined 没有对应的包装对象,也没有 constructor 属性
// console.log(null.constructor === Object); 
// console.log(undefined.constructor === Object);

D. === :查看 null、undefined 以及布尔值

严格来说,单纯使用 ===(全等运算符)并不能像 typeof 那样直接告诉你"这是什么类型"。

但是,=== 是精准判定某些特定值的唯一可靠方式,尤其是在处理 null、undefined 以及布尔值时。

终极武器:Object.prototype.toString.call()

这是 JavaScript 中最可靠、最全面的类型检测方法。

javascript 复制代码
const toString = Object.prototype.toString;

console.log(toString.call(""));           // [object String]
console.log(toString.call(1));            // [object Number]
console.log(toString.call(true));         // [object Boolean]
console.log(toString.call(undefined));    // [object Undefined]
console.log(toString.call(null));         // [object Null]
console.log(toString.call([]));           // [object Array]
console.log(toString.call({}));           // [object Object]
console.log(toString.call(function(){})); // [object Function]
console.log(toString.call(new Date()));   // [object Date]
它的原理是什么?

当调用 toString() 时,引擎会执行以下步骤:

  • 获取内部属性 [[Class]]: 每个内置对象都有一个隐藏的属性 [[Class]],它是一个字符串,代表了对象的类型(如 "Array", "Date")。

  • 通过 call 改变 this: 普通对象调用 toString() 会返回自己的内容,但 Object.prototype 上的原始 toString 方法被设计为返回该 [[Class]] 属性。

  • 格式化输出: 最终返回一个格式为 [object Type] 的字符串。

为什么必须用 .call()?
  • 因为像 Array, Function 等子类通常都 重写(Override) 了自己的 toString 方法。如果不使用 .call(target) 强行调用 Object 原型上的原始方法,你只会得到 "" 或源代码字符串。

JavaScript 数据类型检测方法深度对比表

方法 判定原理 优点 缺点 最佳适用场景
typeof 底层机器码标记位判断 简单、性能最高;检测未定义变量不报错。 无法区分对象子类型(数组、正则等);null 会被误判为 "object" 基本类型 (除 null)和函数的快速检测。
instanceof 检测右侧 prototype 是否在左侧原型链上 能识别继承关系;能区分具体的引用类型(Array, Date)。 不支持基本类型字面量;跨 iframe/窗口失效(构造函数不同)。 自定义实例或单一环境下的复杂对象识别。
constructor 访问实例指向构造函数的属性 typeof 更细致;支持基本类型的装箱检测。 极不稳定 ,属性可被重写;null / undefined 会直接报错。 确定原型链未被篡改时的快速类名比对。
=== (全等) 比较值与类型是否完全一致 绝对精确,没有任何隐式转换或历史 Bug。 无法直接获取类型名称,只能做定值比对。 精准识别 nullundefined 或单例常量。
Object.toString.call() 获取内部属性 [[Class]] 标签 最全能、最准确;跨环境稳定;不会被篡改。 写法较繁琐;性能略低于 typeof 工业级工具库开发、复杂数据结构判定。

三、深拷贝与浅拷贝

深浅拷贝是 JavaScript 中处理引用类型(对象、数组)时最核心的概念。理解它们的本质,其实就是在理解 "数据在内存中到底是怎么存的"

1. 深浅拷贝的本质:内存地址的博弈

在 JS 中,基本类型存放在栈(Stack)中,而引用类型(对象)存放在堆(Heap)中,栈里只存了一个指向堆内存的地址(引用)。

  • 浅拷贝 (Shallow Copy):只拷贝了对象的第一层。如果属性是基本类型,拷贝的就是值;如果属性是引用类型,拷贝的就是内存地址。因此,修改新对象中的子对象,原对象也会跟着变。

  • 深拷贝 (Deep Copy):递归拷贝对象的所有层级。它会在堆内存中开辟一块全新的空间,将原对象的所有属性完全复制一份。修改新对象,对原对象没有任何影响。

2. 浅拷贝的方法与示例

浅拷贝只负责"复刻外壳"。

A. Object.assign()

javascript 复制代码
const obj = { name: 'Gemini', details: { age: 1 } };
const shallow = Object.assign({}, obj);

shallow.details.age = 2; 
console.log(obj.details.age); // 2 (受影响,因为指向同一个地址)

B. 扩展运算符 ... (Spread)

javascript 复制代码
const arr = [1, 2, { a: 3 }];
const copyArr = [...arr];

copyArr[2].a = 99;
console.log(arr[2].a); // 99 (受影响)

C. 数组方法:slice() 和 concat()

javascript 复制代码
const arr = [{ x: 1 }];
const copy1 = arr.slice();
const copy2 = arr.concat();

3. 深拷贝的方法与示例

深拷贝追求的是"彻底切断联系"。

A. 穷人版:JSON.parse(JSON.stringify())

这是开发中最常用的简便方法。

javascript 复制代码
const obj = { a: 1, b: { c: 2 } };
const deep = JSON.parse(JSON.stringify(obj));

deep.b.c = 3;
console.log(obj.b.c); // 2 (不受影响)

缺陷:无法处理 Function、RegExp、Date、Symbol,且会忽略 undefined。

B. 标准版:structuredClone() (现代浏览器原生)

这是 2022 年后 JS 原生支持的深拷贝方法,非常强大。

javascript 复制代码
const obj = { date: new Date(), reg: /\d+/ };
const deep = structuredClone(obj);

console.log(deep.date instanceof Date); // true (保留了内置对象类型)

C. 库函数:Lodash 的 _.cloneDeep()

如果你在处理极其复杂的嵌套对象, Lodash 是最稳健的选择。

javascript 复制代码
const _ = require('lodash');
const deep = _.cloneDeep(original);

D. 进阶版:手写递归实现

这是面试常客,核心逻辑是遍历对象,发现属性是对象就再次递归。

javascript 复制代码
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  
  // 处理循环引用
  if (hash.has(obj)) return hash.get(obj);
  
  let copy = Array.isArray(obj) ? [] : {};
  hash.set(obj, copy);
  
  Reflect.ownKeys(obj).forEach(key => {
    copy[key] = deepClone(obj[key], hash);
  });
  
  return copy;
}

4. JavaScript 深浅拷贝深度对比表

特性 浅拷贝 (Shallow Copy) 深拷贝 (Deep Copy)
复制深度 仅第一层 所有层级(递归)
对原对象影响 修改嵌套对象影响原对象 互不影响,完全隔离
内存空间 共享子对象的内存地址 开辟全新的内存空间
性能 快,内存开销小 慢,内存开销大(随层级增加)
典型方法 ... 扩展符、Object.assign()Array.prototype.slice() structuredClone()JSON.parse(JSON.stringify())、递归函数

5. 深拷贝常用方法优缺点对比表

方法 优点 缺点 推荐指数
JSON.parse/stringify 方便快捷,无库依赖 丢失函数/undefined,不支持循环引用 ⭐⭐⭐
structuredClone 原生支持,支持 Date/Map/Set 和循环引用 不支持函数和原型链拷贝 ⭐⭐⭐⭐⭐
Lodash (_.cloneDeep) 功能最全,处理所有边界情况 增加包体积 ⭐⭐⭐⭐
手写递归 逻辑可控,随心所欲 容易写 Bug,维护成本高 ⭐⭐

四、对象遍历方式对比

在 JavaScript 中,遍历对象的方法有很多,但它们在属性可见性 (是否包含不可枚举属性)和原型链检测(是否包含继承属性)上有很大区别。

ES6中 Map 集合类型知识点、常见使用场景、以及案例------Object 与 Map 遍历详解

1. 核心遍历方法对比表

2. 代码案例演示

A. 经典的 for...in (带继承) + hasOwnProperty

javascript 复制代码
const parent = { prototypeAttr: '我是继承的' };
const child = Object.create(parent); // child 继承自 parent
child.ownAttr = '我是自身的';

for (let key in child) {
  console.log(key); // 输出 "ownAttr" 和 "prototypeAttr"
}

// 避坑:通常需要加判断
for (let key in child) {
  if (child.hasOwnProperty(key)) {
    console.log('仅自身:', key); // 仅输出 "ownAttr"
  }
}

B. 现代常用的 Object.entries()

这是目前最推荐的遍历方式,因为它能直接拿到键和值,且只看对象自身。

javascript 复制代码
const user = { name: 'Gemini', age: 1 };

Object.entries(user).forEach(([key, value]) => {
  console.log(`${key}: ${value}`);
});
// name: Gemini
// age: 1

C. "全能王" Reflect.ownKeys()

javascript 复制代码
const obj = { [Symbol('id')]: 123, normal: 'hello' };
Object.defineProperty(obj, 'hidden', { enumerable: false, value: 'secret' });

console.log(Object.keys(obj));      // ["normal"] (漏掉 Symbol 和不可枚举)
console.log(Reflect.ownKeys(obj));   // ["normal", "hidden", Symbol(id)] (全都有)

3. 性能与选择建议

  • 性能排行:for...in 通常最慢(因为它要爬原型链)。Object.keys() 和普通的 for 循环性能较好。

  • 默认首选:如果你只需要遍历对象自身的属性,Object.keys() 或 Object.entries() 是标准做法。

  • 处理数组:千万不要用 for...in 遍历数组!因为它会遍历数组所有可枚举属性(包括原型上的方法),且不保证顺序。请使用 for...of 或 forEach。

JavaScript 对象遍历方式全维度对比表

方法 遍历范围 (自身/原型) 包含不可枚举属性 包含 Symbol 属性 常用场景
for...in 自身 + 原型链 传统的枚举,常需配合 hasOwnProperty 使用。
Object.keys() 仅自身 获取对象所有可枚举的"键名"数组。
Object.values() 仅自身 只需要对象的值,不关心键名。
Object.entries() 仅自身 配合解构同时获取 [key, value],最常用。
Object.getOwnPropertyNames() 仅自身 需要查看隐藏的(不可枚举)属性名。
Reflect.ownKeys() 仅自身 全能型,获取对象所有类型的键(含 Symbol)。
相关推荐
落魄江湖行1 小时前
进阶篇二 Nuxt4 渲染模式:SSR/SSG/CSR 怎么选
前端·vue.js·typescript·nuxt4
M宝可梦1 小时前
ReAct 与 LLM Agentic 范式:从推理到行动的完整技术科普
前端·react.js·前端框架
x-cmd2 小时前
[260416] 谷歌 Chrome 推出 Skills 功能!帮你保存、复用提示词
前端·chrome·ai·自动化·agent·x-cmd·skill
色空大师2 小时前
【Linux-安装nginx】
linux·运维·前端·nginx·部署
董董灿是个攻城狮2 小时前
封了几百万个账号的 Claude, 路走窄了
前端
heytoo2 小时前
同一个模型,为什么结果差10倍?差的不是模型
前端·agent
霪霖笙箫2 小时前
「JS全栈AI学习」九、Multi-Agent 系统设计:架构与编排
前端·面试·全栈
慕斯fuafua2 小时前
CSS——定位
前端·css
Cache技术分享2 小时前
384. Java IO API - Java 文件复制工具:Copy 示例完整解析
前端·后端