文章目录
- [一、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()
-
- 它的原理是什么?
- [为什么必须用 .call()?](#为什么必须用 .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)。 |