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。 无法直接获取类型名称,只能做定值比对。 精准识别 null 、undefined 或单例常量。
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)。
相关推荐
扶风ff3 小时前
新品知识更新太快?用练题簿在线刷题,安排企业培训的小测与复盘
前端·学习·小程序
对空六课3 小时前
支持注意力分析的热力图工具有哪些?
前端·数据库·数据分析
Csvn4 小时前
Vue3 响应式与编译:依赖收集如何升级为节点级靶向更新
前端
星栈5 小时前
pnpm 12 升级实测
前端·javascript
Sirens.6 小时前
Java并发锁详解:六类锁策略与 synchronized 底层原理
java·前端·算法
Ai-_Man6 小时前
您您这可以把Microsofat Copilot的多个会话比如说。左侧的多个会话一次性导出吗?不是单条会话里面的多次会对话。AI导出鸭
javascript·人工智能·ai·小程序·电脑·copilot
特创数字科技7 小时前
一个纯本地运行的图片处理工具:压缩 / 裁剪 / 九宫格 / 圆角 / 滤镜 / 拼图 / 水印,终生使用
前端
IT_陈寒8 小时前
Vue的v-if和v-for混用居然是个天坑
前端·人工智能·后端
天衍四九-8 小时前
【无标题】
前端·spring boot·mysql·nginx·docker
广州华水科技8 小时前
2026年单北斗GNSS变形监测系统推荐榜单,解锁GNSS位移监测新高度
前端