DAY_25 JavaScript 原型、原型链与值类型/引用类型 ── 深度全解(上)

摘要 :本文系统梳理 JavaScript 面向对象的核心机制:this 指向规则、原型(Prototype)、原型链(Prototype Chain)以及值类型 vs 引用类型。结合 MDN 规范、ECMAScript 标准与大量可运行示例,帮助你彻底理解 JS 运行时对象模型。


目录

  1. 名词速查表
  2. [回顾:this 指向规则](#回顾:this 指向规则)
    • 核心规则速查
    • [Mermaid --- this 判断流程](#Mermaid — this 判断流程)
    • 案例完整解析
      • 案例 01 --- this 题目 01(普通调用 / new 调用 / 方法调用)
      • 案例 02 --- this 题目 02(方法引用 vs 方法调用)
  3. 原型(Prototype)
    • 3.1 原型的概念
    • 3.2 如何获取对象的原型
      • 方式一:隐式原型 __proto__(遗留 API)
      • 方式二:Object.getPrototypeOf()(推荐)
      • 方式三:构造函数的 prototype 属性(显式原型)
      • 三种方式对比表 · 完整可运行示例
    • 3.3 对象、构造函数、原型三角关系
    • 3.4 自定义构造函数时原型的应用
      • 案例:购物车场景
      • 错误写法对比(方法放构造函数内 vs 原型上)
    • 3.5 [hasOwnProperty 判断属性归属](#hasOwnProperty 判断属性归属)
      • 名词解析:自有属性 vs 继承属性
      • in 运算符 vs hasOwnProperty 对比
    • 3.6 [Object.create 创建对象并自定义原型](#Object.create 创建对象并自定义原型)
      • 语法说明 · 六种用法示例
      • 三种创建对象的原型设置对比
  4. [原型链(Prototype Chain)](#原型链(Prototype Chain))
    • 4.1 原型链的概念与作用
      • 属性查找四步算法 · Mermaid 流程图
      • 属性遮蔽(Property Shadowing)演示
    • 4.2 原型链与构造函数的终极图谱
      • 完整 Mermaid 关系图
      • 四条必背核心规律
      • 案例:原型链属性查找完整演示
    • 4.3 [instanceof 运算符原理](#instanceof 运算符原理)
      • 工作原理 Mermaid 图 · 案例 · 手写 myInstanceof
      • instanceof vs 其他类型检测方法对比
    • 4.4 [constructor 属性](#constructor 属性)
      • 默认行为 · 陷阱:替换 prototype 导致 constructor 丢失
      • 三种状态说明 · 修复方案
    • 4.5 原型链属性查找完整示例
  5. 值类型与引用类型
  6. 经典面试题解析
    • 6.1 [this 指向题](#this 指向题)
      • 题目 01(含逐行答案)
      • 题目 02(含逐行答案)
    • 6.2 原型链题
      • 题目 01:对象 f 有方法 a 和 b 吗?
      • 题目 02:输出结果(Object/Function.prototype 挂载方法)
      • 题目 03:原型替换后的输出(原型替换陷阱)
    • 6.3 值类型与引用类型题
      • 题目 1~6(含完整答案与注释)
  7. 综合经典场景实战
  8. 深入理论扩展
    • 8.1 [设计哲学:基于原型 vs 基于类](#设计哲学:基于原型 vs 基于类)
      • Brendan Eich 的设计决策 · 委托 vs 复制
      • 对比表:Class-based vs Prototype-based
    • 8.2 [new 运算符的内部执行步骤(规范级)](#new 运算符的内部执行步骤(规范级))
      • 四步骤 Mermaid 图 · 手写 myNew 实现
      • return 对象 vs return 原始值的差异
    • 8.3 [属性描述符(Property Descriptor)](#属性描述符(Property Descriptor))
      • 数据描述符 vs 访问器描述符对比表
      • Object.defineProperty · Object.freeze
      • 描述符与原型链的交互规则
    • 8.4 [[[Get]] 操作与原型链查找算法](#[[Get]] 操作与原型链查找算法)
      • [[Get]] Mermaid 流程图 · [[Set]] 赋值规则图
      • delete 只删除自有属性的原理
    • 8.5 [this 绑定规则优先级与 call/apply/bind](#this 绑定规则优先级与 call/apply/bind)
      • 四级优先级 Mermaid 图
      • call / apply / bind 对比表
      • 手写 myBind 实现
    • 8.6 深拷贝与浅拷贝(引用类型核心应用)
      • 浅拷贝三种方式 · 深拷贝四种方式
      • 循环引用处理(WeakMap)· 方法特性对比表
    • 8.7 垃圾回收机制与内存管理
      • 标记清除算法 Mermaid 图
      • 四大内存泄漏场景 · WeakMap 解决方案
      • 引用计数算法及其缺陷
    • 8.8 [ES6 class 语法与原型链的对应关系](#ES6 class 语法与原型链的对应关系)
      • class → ES5 原型写法编译对比 Mermaid 图
      • 完整双写法等价示例
      • class vs ES5 构造函数 8 个关键差异表
    • 8.9 [V8 引擎的对象内部表示与性能优化](#V8 引擎的对象内部表示与性能优化)
      • Hidden Class(隐藏类)原理与 Mermaid 图
      • Inline Cache(内联缓存)工作机制
      • Fast / Slow properties 对比表
      • 性能优化实践 5 条
    • 8.10 [Well-known Symbols 与原型链的元编程扩展](#Well-known Symbols 与原型链的元编程扩展)
      • Symbol.iterator · Symbol.hasInstance · Symbol.toPrimitive · Symbol.toStringTag · Symbol.species
      • Well-known Symbols 速查表
    • 8.11 [Proxy 与 Reflect --- 拦截和反射原型操作](#Proxy 与 Reflect — 拦截和反射原型操作)
      • Proxy 陷阱 Mermaid 图
      • 属性访问日志 · 数据验证 · 只读保护
      • Vue 3 响应式核心原理(简化版完整示例)
      • Reflect API 与 Proxy 陷阱对比表
    • 8.12 原型链的设计模式
      • Mixin 混入模式(Serializable + Validatable + EventEmittable)
      • 组合优于继承原则(Composition over Inheritance)
      • OLOO 模式(Objects Linking to Other Objects)
      • 四种设计模式对比表
    • 8.13 [ECMAScript 规范中的对象内部方法](#ECMAScript 规范中的对象内部方法)
      • 普通对象 11 个内部方法对比表
      • 函数对象的 [[Call]] 与 [[Construct]]
      • 箭头函数没有 [[Construct]] 的规范级原因
      • 通过 Proxy 让内部方法"可见"的完整演示
  9. 各知识点特点全面总结
  10. 知识脑图总结
  11. 参考资料

名词速查表

术语 英文 解释
原型 Prototype 每个对象内部指向的另一个对象,提供属性继承
隐式原型 __proto__ / [[Prototype]] 对象自身访问其原型的方式(已被 Object.getPrototypeOf() 取代)
显式原型 Constructor.prototype 构造函数上的属性,新建实例时会将该对象设为实例的 [[Prototype]]
原型链 Prototype Chain 对象的 [[Prototype]] 一路向上组成的链式结构,终点为 null
构造函数 Constructor new 调用并负责初始化新对象的函数
实例 Instance 通过构造函数 new 出来的对象
instanceof --- 二元运算符,沿着左操作数的原型链查找是否存在右操作数的 prototype
constructor --- 原型对象上默认存在的属性,指回创建该原型所属实例的构造函数
值类型 / 原始类型 Primitive Type numberstringbooleannullundefinedsymbolbigint
引用类型 Reference Type Object(包括 Array、Function、Date 等)
栈内存 Stack 存放局部变量和函数调用帧,LIFO(后进先出),存取极快
堆内存 Heap 动态分配的大块内存区域,对象的实际数据存于此
属性遮蔽 Property Shadowing 对象自身拥有与原型同名属性时,原型上的属性被"遮蔽"
原型污染 Prototype Pollution 修改 Object.prototype 导致所有对象行为异常的安全漏洞

回顾:this 指向规则

核心规则速查

复制代码
① 全局作用域          → this === window(浏览器)
② 普通函数调用         → this === window(非严格模式)
③ 方法调用            → this === 调用该方法的对象("点"前面是谁就是谁)
④ 构造函数调用 (new)   → this === 新创建的对象实例
⑤ 箭头函数            → 没有自己的 this,继承外层词法作用域的 this

Mermaid --- this 判断流程







严格
非严格
遇到 this
是否用 new 调用?
this = 新创建的实例对象
是否作为对象方法调用?
this = 点号前的对象
是否是箭头函数?
继承外层 this
是否严格模式?
this = undefined
this = window

案例完整解析

案例 01 --- this题目01

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>this 题目 01</title>
</head>
<body>
  <script>
    // 全局变量 => window.age = 10
    var age = 10;

    function func(name, age) {
      this.name = name;
      this.age  = age;
      this.getInfo = function () {
        console.log(this.name);
        console.log(this.age);
      };
      console.log(this.name);
    }

    // ① 普通调用:this === window
    func('Tom', 19);
    // 执行后 window.name='Tom'  window.age=19
    // 控制台输出:Tom

    // ② new 调用:this === 新对象 o
    var o = new func('Tom', 19);
    // 控制台输出:Tom

    // ③ o.getInfo() --- 方法调用,this === o
    o.getInfo();
    // 控制台输出:Tom  19

    // ④ window.age 被 func('Tom',19) 改写为 19
    console.log(age);   // 19

    // ⑤ new o.getInfo() --- 以构造函数形式调用
    //    this 是全新对象,name/age 均未设置
    new o.getInfo();
    // 控制台输出:undefined  undefined
  </script>
</body>
</html>

关键陷阱func('Tom', 19) 以普通函数形式调用时,this 指向 window,所以会覆盖全局变量 age(从 10 变为 19)。

📋 代码逐行解析

执行语句 this 指向 原因 输出
func('Tom', 19) window 普通函数调用,默认绑定规则 'Tom'window.name 被赋值)
var o = new func('Tom', 19) 新对象 o new 绑定,优先级最高 'Tom'
o.getInfo() o 方法调用,隐式绑定 'Tom', 19
console.log(age) --- 读取 window.age,已被第1步改写为 19 19
new o.getInfo() 新的空对象 new 绑定,该对象上无 name/age 属性 undefined, undefined

🌐 经典使用场景

复制代码
场景:表单验证组件
  同一个验证函数既可作为独立工具调用(普通函数),
  也可绑定到表单实例上(方法调用),
  还可用 new 创建独立的验证器对象。
  理解 this 的变化是正确使用这类多态函数的前提。

💼 业务价值

  • 代码复用:同一个函数可在不同上下文中表现不同行为,无需为每个场景单独编写
  • 防 Bug :95% 的 this 相关 Bug 源于忘记它是动态绑定的------掌握此规则是写出健壮代码的基础
  • 框架理解 :React 类组件中必须手动 bind(this) 或用箭头函数,正是为了防止事件回调中 this 变为 undefined

案例 02 --- this题目02(方法引用 vs 方法调用)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>this 题目 02</title>
</head>
<body>
  <script>
    var age = 20;

    var obj = {
      age: 10,
      getAge: function () {
        console.log(this.age);
      }
    };

    var obj1 = { age: 30 };
    obj1.prop = obj;

    // fn 只是拿到了函数引用,调用时没有"点",this === window
    var fn = obj1.prop.getAge;

    obj.getAge();           // 10  (this === obj)
    obj1.prop.getAge();     // 10  (this === obj,因为点前面是 obj1.prop === obj)
    fn();                   // 20  (this === window)
  </script>
</body>
</html>

记忆口诀:"谁最后调用,this 就指向谁;光赋值不调用,this 看调用时的上下文。"

📋 代码逐行解析

复制代码
var fn = obj1.prop.getAge;

这一行只是把函数的引用地址 赋给了 fn,相当于拿到了一个没有主人的函数。调用 fn() 时,函数前面没有 "." 也没有对象,引擎使用默认绑定规则,this 退化为 window

复制代码
obj1.prop.getAge()

链式调用时,this 永远绑定到最后一个点前面 的对象。obj1.prop 等于 obj,所以 this === obj,输出 obj.age = 10

🌐 经典使用场景

复制代码
场景:事件回调与方法引用脱钩
  document.getElementById('btn').onclick = obj.handleClick;
  // ❌ 这样写 handleClick 里的 this 是 <button>,不是 obj
  
  document.getElementById('btn').onclick = obj.handleClick.bind(obj);
  // ✅ 用 bind 锁定 this

场景:数组高阶方法传入对象方法
  [1,2,3].forEach(obj.process);  // ❌ this 丢失
  [1,2,3].forEach(obj.process.bind(obj)); // ✅
  [1,2,3].forEach(item => obj.process(item)); // ✅ 箭头函数转发

💼 业务价值

  • 事件系统 :DOM 事件绑定、消息队列回调、WebSocket 消息处理器,都需要通过 bind 或箭头函数保证 this 的正确指向
  • Vue/React 组件:理解方法引用 vs 方法调用的差异,是理解为什么框架需要特殊处理事件绑定的底层原因
  • 可读性 :团队规范中明确函数传递方式,能有效避免 this 丢失导致的线上问题

原型(Prototype)

3.1 原型的概念

原型 是 JavaScript 实现基于原型的继承(Prototypal Inheritance)的核心机制。

官方定义(ECMAScript 规范):每个对象都有一个内部槽 [[Prototype]],其值要么是另一个对象,要么是 null

两句话理解原型:

  1. 每个对象都有原型,原型本身也是一个普通对象。
  2. 对象可以"继承"使用原型上的属性,无需把属性复制一份到自身。

\[Prototype\]

\[Prototype\]

\[Prototype\]

实例对象 obj
原型对象 proto
Object.prototype
null

3.2 如何获取对象的原型

方式一:隐式原型 __proto__(遗留 API,了解即可)
js 复制代码
var arr = [1, 2, 3];
console.log(arr.__proto__);          // Array.prototype
console.log(arr.__proto__ === Array.prototype);  // true

注意__proto__ 在 ECMAScript 2015 规范中被纳入附件 B(遗留特性),MDN 标注为已废弃。在生产代码中应使用以下方法。

方式二:Object.getPrototypeOf()(推荐)
js 复制代码
var arr = [1, 2, 3];
console.log(Object.getPrototypeOf(arr));               // Array.prototype
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
方式三:构造函数的 prototype 属性(显式原型)
js 复制代码
console.log(Array.prototype);   // 即所有数组实例共享的原型对象
三种方式对比
方式 语法 推荐度
隐式原型 obj.__proto__ ⚠️ 废弃,避免使用
获取原型 Object.getPrototypeOf(obj) ✅ 推荐
构造函数原型 Constructor.prototype ✅ 推荐

完整可运行示例

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>获取对象原型</title>
</head>
<body>
  <script>
    // 1. 数组
    var arr1 = [10, 20, 30];
    var arr2 = ['hello', 'world'];

    // 三种方式均获取到同一个 Array.prototype
    console.log('arr1.__proto__:', arr1.__proto__);
    console.log('Object.getPrototypeOf(arr1):', Object.getPrototypeOf(arr1));
    console.log('Array.prototype:', Array.prototype);
    console.log('三种方式相等:', arr1.__proto__ === Array.prototype);

    // 同类型的不同实例共享同一原型
    console.log('arr1 和 arr2 共享原型:', arr1.__proto__ === arr2.__proto__); // true

    // 2. 普通对象
    var user = { name: 'Alice' };
    console.log('user 的原型:', Object.getPrototypeOf(user)); // Object.prototype

    // 3. 自定义构造函数
    function Product(name, price) {
      this.name  = name;
      this.price = price;
    }
    var p = new Product('键盘', 299);
    console.log('p 的原型 === Product.prototype:', Object.getPrototypeOf(p) === Product.prototype); // true
  </script>
</body>
</html>

📋 代码逐行解析(获取对象原型示例)

js 复制代码
// arr1.__proto__  ← 隐式原型,直接从对象实例访问,等于 Array.prototype
// Object.getPrototypeOf(arr1)  ← 标准 API,读取 [[Prototype]] 内部槽
// Array.prototype  ← 显式原型,从构造函数访问
// 三者指向同一个对象,输出 true
console.log(arr1.__proto__ === Array.prototype);  // true

// 同类型的所有实例共享同一个原型对象(内存中只有一份)
// arr1 和 arr2 都是 Array 类型,原型相同
console.log(arr1.__proto__ === arr2.__proto__);  // true

// 自定义构造函数 Product 实例化后
// p 的 [[Prototype]] === Product.prototype
// 这是 new 运算符在步骤2中自动完成的

🌐 经典使用场景

复制代码
场景一:框架插件扩展
  jQuery 通过 $.fn(即 jQuery.prototype)挂载方法,
  所有 jQuery 对象实例自动拥有这些方法。
  理解 prototype 是使用/扩展 jQuery 插件的前提。

场景二:类型检测工具
  封装通用 getType(obj) 函数时,通过原型链判断对象真实类型:
  Object.getPrototypeOf(obj) === Array.prototype → 是数组
  这比 typeof 更精确(typeof [] === 'object' 无法区分数组和对象)。

场景三:调试与溯源
  在 Chrome DevTools 中查看对象的 [[Prototype]],
  快速定位某个方法来自哪一层原型,是排查继承问题的常用手段。

💼 业务价值

  • 统一接口:同类实例共享原型方法,确保所有实例行为一致,降低接口维护成本
  • 内存效率:100 个对象只保存 1 份共享方法,对于大量实例化的业务对象(如列表渲染)有显著内存收益
  • 运行时扩展 :生产环境中可通过修改 prototype 为旧版本代码热补丁(Polyfill),无需重新部署

3.3 对象、构造函数、原型三角关系

.prototype
.constructor

\[Prototype\]\] / __proto__ \[\[Prototype\]\] / __proto__ new → 创建 new → 创建 构造函数 Constructor (如 Array、User) 原型对象 prototype (如 Array.prototype) 实例 instance1 实例 instance2 **关系小结:** | 关系 | 描述 | |-----------|-------------------------------------| | 构造函数 → 实例 | 通过 `new` 创建,可以有无数个实例 | | 实例 → 构造函数 | 一个实例只有一个构造函数(通过 `constructor` 属性反查) | | 实例 → 原型 | 每个实例有且只有一个原型 | | 原型 → 实例 | 一个原型可以作为多个实例的原型 | | 构造函数 → 原型 | `Constructor.prototype` 直接引用 | | 原型 → 构造函数 | `proto.constructor` 反向引用 | *** ** * ** *** #### 3.4 自定义构造函数时原型的应用 **为什么要把方法放在原型上,而不是放在构造函数内?** ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/bb62194e0fc84da4bc5c36c393d0176c.png) 当方法定义在构造函数内部时,每次 `new` 都会在新对象上创建一份独立的函数副本,浪费内存。将方法挂在原型上,所有实例共享同一份函数引用。 100 个 User 实例 × 构造函数内定义方法 = 100 份函数副本(❌ 浪费内存) 100 个 User 实例 × 原型上定义方法 = 1 份函数(✅ 共享复用) **案例:购物车场景** ```html 自定义构造函数 - 原型方法 ``` **错误写法对比** ```html 错误写法 - 方法定义在构造函数内 ``` **📋 代码逐行解析(购物车场景)** ```js // ① 构造函数只负责存储「实例独有」的数据:name、age、address // 每个用户数据不同,必须存在实例自身上 function User(name, age, address) { this.name = name; // 实例自有属性:每人不同 this.age = age; // 实例自有属性:每人不同 this.address = address;// 实例自有属性:每人不同 } // ② 方法挂在原型上:所有 User 实例共享同一份函数 // addShopcart 的逻辑对每个用户都一样,不需要各自复制一份 User.prototype.addShopcart = function (product) { ... }; User.prototype.buy = function (product) { ... }; // ③ 验证共享:u1 和 u2 的 addShopcart 是同一个函数引用 console.log(u1.addShopcart === u2.addShopcart); // true // 内存中只有1份函数,无论创建多少个 User 实例 ``` **黄金法则总结** 「数据(状态)放实例 this」 ←→ 「行为(方法)放原型 prototype」 实例独有、实例间不同 → 构造函数 this.xxx = ... 实例共用、逻辑相同 → Constructor.prototype.xxx = function() {} **🌐 经典使用场景** 场景一:电商平台用户系统 User 实例:name、id、cart(购物车数据)→ 存 this User 方法:addToCart()、checkout()、getOrderHistory() → 存 prototype 平台同时在线数万用户时,原型方法只有1份,极大节省内存 场景二:游戏角色系统 Character 实例:name、hp、mp、position → 存 this(每个角色独立) Character 方法:attack()、move()、useSkill() → 存 prototype(共享逻辑) 大型游戏地图中同时存在数百个 NPC,原型方法设计是性能关键 场景三:前端组件系统(如 jQuery 插件体系) $.fn(即 jQuery.prototype)上挂载了所有 jQuery 实例方法 每个 $('selector') 返回的对象都能访问所有方法,正是原型共享的直接应用 **💼 业务价值** * **内存优化**:电商、游戏等高并发场景中,方法放原型 vs 放实例的内存差异可达数十 MB * **统一维护**:修改原型方法一处,所有实例立即更新,维护成本接近零 * **架构设计**:理解这个原则,是从"写能跑的代码"升级到"写高质量可维护代码"的关键一步 *** ** * ** *** #### 3.5 hasOwnProperty 判断属性归属 ##### 名词解析:自有属性 vs 继承属性 * **自有属性(Own Property)**:直接定义在对象实例上的属性。 * **继承属性(Inherited Property)**:来自原型链的属性。 `in` 运算符检查属性是否**可访问** (自有 + 继承均算), `hasOwnProperty()` 只检查属性是否**直接属于该对象本身**。 ```html hasOwnProperty 演示 ``` > **经典使用场景** :`for...in` 循环会遍历原型链上的可枚举属性,用 `hasOwnProperty` 过滤,是 ES6 之前的常见技巧。现代代码推荐使用 `Object.keys()` 或 `for...of`。 **📋 代码逐行解析(hasOwnProperty 示例)** ```js var arr = [10, 20, 30, 40, 50, 60]; arr.username = 'Alice'; // 在 arr 实例自身添加属性 Array.prototype.address = '上海'; // 在原型(所有数组共享的对象)上添加属性 // in 运算符:检查属性名是否「可访问」(自有 + 整条原型链) 'length' in arr // true --- arr 自有属性(数组天生有 length) 'push' in arr // true --- 来自 Array.prototype(原型属性) 'username' in arr // true --- arr 自有属性(我们刚添加的) 'address' in arr // true --- 来自 Array.prototype(我们刚添加的) 'age' in arr // false --- 整条链上都不存在 // hasOwnProperty:只检查「对象自身」,不爬原型链 arr.hasOwnProperty('length') // true --- length 在 arr 自身 arr.hasOwnProperty('username') // true --- username 在 arr 自身 arr.hasOwnProperty('push') // false --- push 在 Array.prototype,不在 arr 自身 arr.hasOwnProperty('address') // false --- address 在 Array.prototype,不在 arr 自身 // for...in 会遍历原型链可枚举属性,需要 hasOwnProperty 过滤 for (var key in obj) { if (obj.hasOwnProperty(key)) { // 只处理真正属于 obj 的属性,避免处理从原型继承的意外属性 } } ``` **属性检测方法对比** | 检测方式 | 自有属性 | 原型链属性 | 不存在 | |-------------------------------------|----------------------|----------------------|---------| | `'prop' in obj` | `true` | `true` | `false` | | `obj.hasOwnProperty('prop')` | `true` | `false` | `false` | | `Object.keys(obj).includes('prop')` | `true`(可枚举) | `false` | `false` | | `obj.prop !== undefined` | `true`(若值非undefined) | `true`(若值非undefined) | `false` | **🌐 经典使用场景** 场景一:安全的对象序列化 将对象序列化为 JSON 时,只序列化自有属性, 避免把原型上的"污染属性"意外包含进去: JSON.stringify 内部就是只处理自有可枚举属性。 场景二:继承体系中的属性过滤 父类在原型上添加的属性,子类迭代时不想重复处理, 用 hasOwnProperty 精确过滤是最可靠的方式。 场景三:Mixin 模式实现 手动混入(mixin)对象时,用 hasOwnProperty 确保只复制 源对象自身的属性,不把其原型链属性带入目标对象: function mixin(target, source) { for (var key in source) { if (source.hasOwnProperty(key)) target[key] = source[key]; } } 场景四:数据校验 检查接口返回的 JSON 对象是否包含某个必要字段时, hasOwnProperty 比 in 更严格、更安全。 **💼 业务价值** * **数据安全**:避免原型链属性污染业务数据序列化/传输过程,防止敏感字段泄露 * **框架稳定性** :Lodash、Underscore 等工具库内部大量使用 `hasOwnProperty` 保证操作精度 * **防御性编程** :在与第三方库共用原型的场景中,`hasOwnProperty` 是最后一道防线 *** ** * ** *** #### 3.6 Object.create 创建对象并自定义原型 ##### 语法 ```js Object.create(proto) Object.create(proto, propertiesObject) ``` * `proto`:新对象的原型,传 `null` 则创建无原型的纯净对象。 * `propertiesObject`:可选的属性描述符对象。 ```html Object.create 演示 ``` ##### 三种创建对象的原型设置对比 | 创建方式 | 原型设置时机 | 原型来源 | |------------------------|----------|-------------------------| | `{}` / `new Object()` | 自动 | `Object.prototype` | | `new Constructor()` | 自动 | `Constructor.prototype` | | `Object.create(proto)` | **手动指定** | 任意对象或 `null` | **📋 代码逐行解析(Object.create 示例)** ```js // 1. {} 字面量 --- 自动以 Object.prototype 为原型(最常见) var obj1 = {}; // obj1 拥有 toString/hasOwnProperty/valueOf 等所有 Object.prototype 方法 // 2. Object.create([10,20,30,40]) --- 以数组为原型 var obj2 = Object.create([10, 20, 30, 40]); // obj2 自身是空对象,但原型是 [10,20,30,40] // 访问 obj2[0] 时会沿原型链找到 10 // 实用价值:可以让一个普通对象"继承"数组的行为 // 3. Object.create(new String('hello')) --- 以 String 对象为原型 var obj3 = Object.create(new String('hello')); // obj3 可以访问 'hello' 的字符索引和 length // 4. Object.create(null) --- 创建无原型的纯净对象 var obj4 = Object.create(null); // obj4 是一个真正空白的对象,没有 toString/hasOwnProperty 等任何继承方法 // 这就是为什么 Redis/LRU 缓存等工具常用它------避免 key 和原型方法冲突 // 5. 实现原型继承:dog 的原型是 animal var dog = Object.create(animal); // 等价于:dog.__proto__ = animal(但更安全) // dog 可以访问 animal 的所有属性,且 dog 自身的属性不影响 animal ``` **🌐 经典使用场景** 场景一:Object.create(null) 作为安全字典 var cache = Object.create(null); // 纯净键值表 cache['toString'] = '某个值'; // 不会和 Object.prototype.toString 冲突 cache['__proto__'] = '某个值'; // 不会触发原型设置逻辑 适用于:路由表、配置表、权限映射表等纯键值存储场景 场景二:ES5 时代实现原型继承 SubClass.prototype = Object.create(SuperClass.prototype); SubClass.prototype.constructor = SubClass; 这是 ES6 class extends 的底层等价实现 场景三:不可变配置对象 Object.create(Object.freeze(defaultConfig)) 创建以冻结配置为原型的新对象,实例可覆盖特定配置, 但无法修改"默认配置原型",保证配置基线安全 场景四:Mixin 基类 var EventEmitter = Object.create(null); EventEmitter.on = function() { ... }; EventEmitter.emit = function() { ... }; // 其他对象可通过 Object.create(EventEmitter) 获得事件能力 **💼 业务价值** * **安全性** :`Object.create(null)` 彻底消除原型污染风险,在解析用户输入的 JSON 键时尤为重要 * **灵活继承**:在不修改任何现有代码的情况下,为对象动态添加继承关系 * **框架底层** :Vue 2 的响应式系统、Node.js 的模块系统内部均使用 `Object.create` 建立原型链 *** ** * ** *** ### 原型链(Prototype Chain) #### 4.1 原型链的概念与作用 **原型链** 是由对象的 `[[Prototype]]` 一级一级串联起来的链状结构。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ab9c6a387c36468eb4883437e619264c.png) **属性查找算法(三步走):** 1. 先在对象自身查找属性 ├── 找到 → 直接使用,停止查找 └── 没找到 → 沿 [[Prototype]] 向上 2. 在原型对象上查找 ├── 找到 → 使用,停止查找 └── 没找到 → 继续向上 3. 到达链顶 Object.prototype ├── 找到 → 使用,停止查找 └── 没找到 → 继续到 null 4. 到达 null → 返回 undefined 是 否 是 否 是 否 访问 obj.prop obj 自身有 prop? 使用 obj.prop obj.__proto__ 有 prop? 使用 obj.__proto__.prop Object.prototype 有 prop? 使用 Object.prototype.prop 返回 undefined **属性遮蔽(Property Shadowing)演示** ```html 属性遮蔽演示 ``` **📋 代码逐行解析(属性遮蔽示例)** ```js // arr01 自身有:[10,20,30,40,50](数组元素)+ username='Alice'(手动添加) // arr01 原型(Array.prototype)有:age=100, address='上海'(手动添加),以及 push/pop 等内置方法 arr01.username // 'Alice' → 第1步:在 arr01 自身找到,直接返回 arr01.age // 100 → 第1步:arr01 自身没有 → 第2步:在 Array.prototype 上找到 arr01.address // '上海' → 同上,在 Array.prototype 上找到 arr02.username // undefined → arr02 自身没有,Array.prototype 上也没有(username 是 arr01 的自有属性) // 属性遮蔽(Property Shadowing): arr02.address = '成都'; // 这一行在 arr02「自身」添加了 address 属性 // 现在访问 arr02.address:第1步就在自身找到 '成都',不再向上查找原型 // Array.prototype.address 还是 '上海',arr01 不受影响 console.log(arr01.address); // '上海' ← 原型上的值,未受 arr02 操作影响 console.log(arr02.address); // '成都' ← arr02 自身的值,遮蔽了原型 ``` **🌐 经典使用场景** 场景一:默认配置与实例配置 Config.prototype.timeout = 5000; // 默认超时(所有实例共享) var c1 = new Config(); c1.timeout = 30000; // c1 自身覆盖默认值(遮蔽) var c2 = new Config(); c2.timeout; // 5000(c2 未遮蔽,使用原型默认值) → 这正是"约定大于配置"设计模式的实现机制 场景二:国际化多语言系统 I18n.prototype.lang = 'zh-CN'; // 默认语言 var enUser = new I18n(); enUser.lang = 'en-US'; // 特定用户覆盖 → 属性遮蔽实现"个性化"覆盖,不影响其他用户 场景三:A/B 测试 Feature.prototype.enabled = false; // 默认关闭 var betaUser = new Feature(); betaUser.enabled = true; // beta 用户开启,遮蔽默认值 → 精确控制特性开关,原型作为"全局开关",实例属性作为"个人开关" **💼 业务价值** * **灵活的默认值机制**:原型属性天然充当"全局默认值",实例属性充当"个人化覆盖",无需复杂的配置合并逻辑 * **零侵入覆盖**:修改一个实例的行为不影响其他实例,是前端"状态隔离"的基础机制 * **性能**:原型属性只存一份,不被遮蔽时内存开销接近零 *** ** * ** *** #### 4.2 原型链与构造函数的终极图谱 这是 JavaScript 中最复杂也是最重要的知识点之一。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0b6e4eafd57e40cfbe7b69b1fdaee53f.png) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/6842d158b55249a29c715043468506fc.png) 构造函数层 原型层 实例层 \[\[Prototype\]

\[Prototype\]

\[Prototype\]

\[Prototype\]

\[Prototype\]

\[Prototype\]

\[Prototype\]

\[Prototype\]

\[Prototype\]\] = __proto__ \[\[Prototype\]\] = __proto__ \[\[Prototype\]\] = __proto__ \[\[Prototype\]\] = __proto__ .prototype .prototype .prototype .prototype arr = \[

fn = function(){}
f = new fn()
user = {}
Array.prototype

构造函数: Object
Function.prototype

构造函数: Object
fn.prototype

构造函数: fn
Object.prototype

构造函数: Object
Array

(函数对象)
Object

(函数对象)
Function

(函数对象)
fn

(自定义函数)
null

核心规律总结(必背):

复制代码
规律一:所有函数(包括内置函数和自定义函数)的 [[Prototype]] 都指向 Function.prototype
        Array.__proto__    === Function.prototype  ✅
        Object.__proto__   === Function.prototype  ✅
        Function.__proto__ === Function.prototype  ✅(Function 是自己的实例!)

规律二:所有原型对象(除 Object.prototype 外)的 [[Prototype]] 都指向 Object.prototype
        Array.prototype.__proto__    === Object.prototype  ✅
        Function.prototype.__proto__ === Object.prototype  ✅

规律三:Object.prototype 是原型链的终点,其 [[Prototype]] === null

规律四:Function.prototype 的构造函数是 Object(不是 Function!)

案例完整演示

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>原型链 - 属性查找完整演示</title>
</head>
<body>
  <script>
    Array.prototype.username  = '小乐';
    Function.prototype.age    = 10;
    Object.prototype.address  = '上海';

    var arr   = [];
    var Array_ = Array;  // 这是函数对象

    // arr 的原型链:arr → Array.prototype → Object.prototype
    console.log(arr.username);   // '小乐'    ✅ 在 Array.prototype 找到
    console.log(arr.age);        // undefined ❌ arr 的链上没有 Function.prototype
    console.log(arr.address);    // '上海'    ✅ 在 Object.prototype 找到

    // Array(函数对象)的原型链:Array → Function.prototype → Object.prototype
    console.log(Array_.username);  // undefined ❌
    console.log(Array_.age);       // 10        ✅ 在 Function.prototype 找到
    console.log(Array_.address);   // '上海'    ✅ 在 Object.prototype 找到

    // 清理
    delete Array.prototype.username;
    delete Function.prototype.age;
    delete Object.prototype.address;
  </script>
</body>
</html>

📋 代码逐行解析(原型链查找完整示例)

js 复制代码
// 给三个不同层级的原型挂上属性,观察查找路径

Array.prototype.username  = '小乐';    // 挂在"第1层原型"
Function.prototype.age    = 10;         // 挂在"函数原型"
Object.prototype.address  = '上海';    // 挂在"顶层原型"(所有对象的祖先)

// arr 是数组实例,原型链:arr → Array.prototype → Object.prototype
arr.username;   // '小乐'  ← 第1跳:Array.prototype 上找到,终止
arr.age;        // undefined  ← 第1跳 Array.prototype 没有,第2跳 Object.prototype 没有
                //               Function.prototype 不在 arr 的链上!
arr.address;    // '上海'  ← 第2跳:Object.prototype 上找到

// Array(函数对象)的原型链:Array → Function.prototype → Object.prototype
Array.username; // undefined  ← Array.prototype 不在 Array(函数对象)的链上
Array.age;      // 10         ← 第1跳:Function.prototype 上找到
Array.address;  // '上海'     ← 第2跳:Object.prototype 上找到

// 关键认知:arr(实例)和 Array(构造函数)的原型链是不同的!
// 实例链:arr → Array.prototype → Object.prototype
// 函数链:Array → Function.prototype → Object.prototype

🌐 经典使用场景

复制代码
场景一:Polyfill(垫片)实现
  if (!Array.prototype.includes) {
    Array.prototype.includes = function(val) { ... };
  }
  → 利用原型链特性,为旧浏览器添加缺失的 API,
    所有数组实例立即获得 includes 方法,无需修改调用代码

场景二:方法扩充(慎用)
  String.prototype.trim = String.prototype.trim || function() {
    return this.replace(/^\s+|\s+$/g, '');
  };
  → ES5 之前兼容旧浏览器的常见手段,现代代码中已不推荐扩展内置原型

场景三:全局工具方法
  Object.prototype.deepClone = function() { ... };
  // ❌ 危险!这会让所有对象都有 deepClone 方法,可能污染 for...in 循环
  // → 这类全局扩展要极其谨慎,通常只对自定义构造函数的原型做扩展

💼 业务价值

  • Polyfill 体系:现代前端工程化中,core-js 等工具包正是基于原型链扩展机制为旧环境提供新 API
  • 工具链设计:理解函数对象与实例对象的不同原型链,避免工具方法挂载错位(挂到了找不到的链上)
  • 调试能力:当某个方法莫名"找不到"时,通过原型链图谱排查是高效调试手段

4.3 instanceof 运算符原理

语法
复制代码
对象 instanceof 构造函数
工作原理

instanceof 沿着左操作数 的原型链向上查找,看能否找到右操作数的 prototype




obj instanceof Constructor
p = obj.[[Prototype]]
p === Constructor.prototype?
返回 true
p === null?
返回 false
p = p.[[Prototype]],继续

案例完整演示

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>instanceof 演示</title>
</head>
<body>
  <script>
    // 数组:arr → Array.prototype → Object.prototype
    var arr = [10, 20, 30];
    console.log(arr instanceof Array);    // true  (Array.prototype 在链上)
    console.log(arr instanceof Object);   // true  (Object.prototype 在链上)
    console.log(arr instanceof Function); // false (Function.prototype 不在链上)

    // 自定义构造函数
    var fn   = function () {};
    var user = new fn();
    // user → fn.prototype → Object.prototype

    console.log(user instanceof fn);       // true
    console.log(user instanceof Function); // false
    console.log(user instanceof Object);   // true

    // 手动模拟 instanceof 逻辑
    function myInstanceof(obj, Constructor) {
      var proto = Object.getPrototypeOf(obj);
      while (proto !== null) {
        if (proto === Constructor.prototype) return true;
        proto = Object.getPrototypeOf(proto);
      }
      return false;
    }

    console.log('手动 instanceof:');
    console.log(myInstanceof(arr, Array));    // true
    console.log(myInstanceof(arr, Object));   // true
    console.log(myInstanceof(arr, Function)); // false
  </script>
</body>
</html>

经典面试题[] instanceof Arraytrue[] instanceof Objecttrue,这说明 instanceof 检查的是整条原型链,而不仅仅是直接构造函数。

📋 代码逐行解析(instanceof 示例)

js 复制代码
var arr = [10, 20, 30];
// arr 的原型链:arr → Array.prototype → Object.prototype → null

arr instanceof Array;
// 查找过程:Array.prototype 在 arr 的链上(第1跳就找到)→ true

arr instanceof Object;
// 查找过程:第1跳 Array.prototype ≠ Object.prototype
//           第2跳 Object.prototype === Object.prototype → true
// → 所有对象(含数组)都 instanceof Object!

arr instanceof Function;
// 查找过程:第1跳 Array.prototype ≠ Function.prototype
//           第2跳 Object.prototype ≠ Function.prototype
//           第3跳 null → 终止,返回 false
// Function.prototype 不在 arr 的原型链上

// 手写 myInstanceof 验证理解:
// 原理就是一个 while 循环沿链向上找,找到返回 true,到 null 返回 false
function myInstanceof(obj, Constructor) {
  var proto = Object.getPrototypeOf(obj);
  while (proto !== null) {
    if (proto === Constructor.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

instanceof vs 其他类型检测方法对比

检测方法 适用场景 局限性
typeof 基本类型检测 typeof []'object'(无法区分数组和对象)
instanceof 检测引用类型和继承关系 跨 iframe 失效;不能检测原始类型
Object.prototype.toString.call() 最精确的类型检测 写法较繁琐
Array.isArray() 专门检测数组 只能检测数组
constructor 属性 检测直接构造函数 可被覆盖,不可信

🌐 经典使用场景

复制代码
场景一:函数参数类型守卫
  function process(input) {
    if (input instanceof Array) {
      // 处理数组
    } else if (input instanceof Date) {
      // 处理日期
    } else if (typeof input === 'string') {
      // 处理字符串
    }
  }
  → 根据参数类型分支处理,是工具函数/SDK 的标准防御写法

场景二:继承体系中的权限控制
  if (user instanceof AdminUser) {
    showAdminPanel();
  } else if (user instanceof PremiumUser) {
    showPremiumFeatures();
  }
  → 基于继承层次的功能开关

场景三:错误类型判断
  try { ... }
  catch (e) {
    if (e instanceof TypeError) { ... }
    else if (e instanceof RangeError) { ... }
  }
  → Node.js / 浏览器错误处理的标准范式

场景四:Vue/React 中判断组件类型
  if (Component instanceof Function) {
    // 函数式组件
  } else {
    // 对象式组件
  }

💼 业务价值

  • 健壮性:通过 instanceof 类型守卫,防止函数接收到错误类型参数时静默出错
  • 可维护性:继承体系配合 instanceof,使权限系统、功能开关的代码语义清晰,易于扩展
  • 标准化:error instanceof XXXError 是 Node.js 生态标准的错误处理规范,理解它有助于阅读/贡献主流开源项目

4.4 constructor 属性

默认行为

每个函数在定义时,JavaScript 引擎会自动在其 prototype 对象上添加 constructor 属性,指向该函数自身。

复制代码
假设对象 a 的原型是 b:
  - a 自身通常没有 constructor 属性
  - b(原型)自身有 constructor,值是 a 的构造函数
  - 因此 a.constructor 沿原型链查到 b.constructor

案例演示

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>constructor 演示</title>
</head>
<body>
  <script>
    var arr = [10, 20, 30];
    // arr → Array.prototype → Object.prototype

    console.log(arr.constructor);              // Array(从 Array.prototype 继承)
    console.log(Array.prototype.constructor);  // Array(Array.prototype 自身的)
    console.log(Object.prototype.constructor); // Object

    var user = {};
    console.log(user.constructor);             // Object

    // 自定义构造函数
    function Animal(name) { this.name = name; }
    var cat = new Animal('咪咪');
    console.log(cat.constructor);              // Animal
    console.log(cat.constructor === Animal);   // true

    // ⚠️ 危险操作:替换整个 prototype 会丢失 constructor
    function Dog(name) { this.name = name; }
    Dog.prototype = {
      bark: function () { console.log('汪汪!'); }
    };
    var d = new Dog('旺财');
    console.log(d.constructor); // Object(不再是 Dog!)

    // ✅ 正确做法:补回 constructor
    Dog.prototype = {
      constructor: Dog,  // 手动补回
      bark: function () { console.log('汪汪!'); }
    };
    var d2 = new Dog('小黑');
    console.log(d2.constructor); // Dog ✅
  </script>
</body>
</html>

陷阱警告 :替换整个 prototype 对象(如 Foo.prototype = {...})会导致 constructor 属性丢失,必须手动补回 constructor: Foo

📋 代码逐行解析(constructor 示例)

js 复制代码
// 默认情况下,每个函数在创建时,引擎自动执行:
// Animal.prototype = { constructor: Animal }
// 所以 Animal.prototype.constructor === Animal 永远成立

var cat = new Animal('咪咪');
cat.constructor;  // Animal
// cat 自身没有 constructor,沿链找到 Animal.prototype.constructor = Animal

// ⚠️ 危险操作:用字面量对象整体替换 prototype
Dog.prototype = {
  bark: function() { console.log('汪!'); }
};
// 这个新的 {} 字面量对象的 constructor 是 Object(不是 Dog!)
// 因为 {} 的原型是 Object.prototype,constructor 沿链找到 Object

var d = new Dog('旺财');
d.constructor;  // Object(错误!期望是 Dog)

// ✅ 修复:替换时手动补回 constructor
Dog.prototype = {
  constructor: Dog,       // 关键!手动恢复 constructor 指向
  bark: function() { console.log('汪!'); }
};
// 此时 Dog.prototype.constructor === Dog,行为恢复正常

constructor 的三种状态

复制代码
状态一(默认正常):
  function Foo() {}
  Foo.prototype.constructor === Foo  // ✅ 自动设置

状态二(替换 prototype 后损坏):
  Foo.prototype = { ... }  // 没有手动指定 constructor
  Foo.prototype.constructor === Object  // ❌ 指向错误

状态三(手动修复):
  Foo.prototype = { constructor: Foo, ... }  // ✅ 正确恢复

🌐 经典使用场景

复制代码
场景一:工厂模式中的类型识别
  function createSimilar(obj) {
    return new obj.constructor(); // 通过 constructor 创建同类型的新实例
  }
  var arr = [1, 2, 3];
  var newArr = createSimilar(arr); // 相当于 new Array(),得到空数组

场景二:继承体系补全
  SubClass.prototype = Object.create(SuperClass.prototype);
  SubClass.prototype.constructor = SubClass; // 必须补回,否则 instanceof 等可能异常

场景三:序列化/反序列化
  function serialize(obj) {
    return { type: obj.constructor.name, data: obj };
  }
  // 通过 constructor.name 记录类型信息,反序列化时可以重建对应类型的实例

💼 业务价值

  • 类型系统完整性 :维护正确的 constructor 是保证继承体系类型元信息完整的基础,影响调试工具、日志系统等的展示准确性
  • 反射能力 :基于 constructor 的工厂方法、序列化工具在企业级框架中广泛使用(如 NestJS 的依赖注入)
  • 规范性 :在团队协作中,遗漏 constructor 恢复是代码审查时常见的问题,了解它可提升 code review 质量

4.5 原型链属性查找完整示例

以下是一个综合演示,展示了原型链在实际开发中的工作方式:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>原型链查找综合演示</title>
</head>
<body>
  <script>
    // 构建一个三层原型链
    function Foo() {}
    var f1 = new Foo();
    var f2 = new Foo();

    // 创建普通对象
    var o1 = {};
    var o2 = {};

    // 分析各原型链
    console.log('=== f1 的原型链 ===');
    console.log('f1            :', f1);
    console.log('f1.__proto__  :', f1.__proto__);        // Foo.prototype
    console.log('Foo.prototype :', Foo.prototype);
    console.log('相等?', f1.__proto__ === Foo.prototype); // true
    console.log('Foo.prototype.__proto__ :', Foo.prototype.__proto__);  // Object.prototype
    console.log('Foo.prototype.__proto__ === Object.prototype:', Foo.prototype.__proto__ === Object.prototype);
    console.log('Object.prototype.__proto__ :', Object.prototype.__proto__); // null

    console.log('');
    console.log('=== f1 和 f2 共享原型 ===');
    console.log(f1.__proto__ === f2.__proto__); // true
    console.log(o1.__proto__ === o2.__proto__); // true

    console.log('');
    console.log('=== Foo 函数的原型链 ===');
    console.log('Foo.__proto__             :', Foo.__proto__);             // Function.prototype
    console.log('Function.prototype        :', Function.prototype);
    console.log('相等?', Foo.__proto__ === Function.prototype);             // true
    console.log('Function.prototype.__proto__:', Function.prototype.__proto__); // Object.prototype
  </script>
</body>
</html>

值类型与引用类型

5.1 内存模型:栈与堆

堆内存 Heap
栈内存 Stack
引用/指针
var a = 10

直接存储值 10
var b = 'hello'

直接存储值 'hello'
var ref = 0xFF34

存储堆地址指针
对象实际数据

{ name: 'Alice', age: 28 }

类型 存储位置 存储内容
值类型(原始类型) 变量直接保存实际值
引用类型 栈 + 堆 栈中保存内存地址 ,堆中保存实际数据

值类型包括(7 种):

复制代码
number、string、boolean、null、undefined、symbol(ES6)、bigint(ES11)

引用类型:

复制代码
Object(包括普通对象 {}、数组 Array、函数 Function、日期 Date、正则 RegExp 等)

5.2 赋值与传参行为对比

值类型:赋值复制值
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>值类型赋值行为</title>
</head>
<body>
  <script>
    // 赋值行为:复制值
    var num1 = 10;
    var num2 = num1;   // num2 得到 10 的副本
    num1 = 20;         // 修改 num1,不影响 num2

    console.log('num1:', num1);  // 20
    console.log('num2:', num2);  // 10  ← 独立的副本

    // 函数传参:也是复制值
    var num = 50;
    function f1(num) {
      num = 60;             // 修改的是函数内的局部变量
      console.log('函数内 num:', num);  // 60
    }
    f1(num);
    console.log('函数外 num:', num);    // 50  ← 未被修改
  </script>
</body>
</html>
引用类型:赋值复制引用地址
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>引用类型赋值行为</title>
</head>
<body>
  <script>
    // 场景一:两个变量指向同一对象
    var a = [1, 2];
    var b = a;        // b 和 a 指向同一个数组

    a[0] = 20;        // 修改数组内容 → b 也受影响
    console.log('a:', a);  // [20, 2]
    console.log('b:', b);  // [20, 2]  ← 同一对象!

    console.log('');

    // 场景二:重新赋值 ≠ 修改对象内容
    var x = [1, 2];
    var y = x;
    x = [20, 2];  // x 指向了新数组,y 还是原来的
    console.log('x:', x);  // [20, 2]  ← 新数组
    console.log('y:', y);  // [1, 2]   ← 原数组,未受影响

    console.log('');

    // 场景三:函数传参引用类型
    function modifyArr(arr) {
      for (var i = 0; i < arr.length; i++) {
        arr[i] += 2;  // 修改原数组的内容
      }
    }
    var myArr = [1, 2];
    modifyArr(myArr);
    console.log('myArr after modify:', myArr);  // [3, 4]  ← 被修改了!

    console.log('');

    // 场景四:函数内重新赋值,不影响外部
    function reassign(arr) {
      arr = [100, 200]; // 只改变了局部参数的指向,不影响外部
    }
    var nums = [1, 2];
    reassign(nums);
    console.log('nums after reassign:', nums);  // [1, 2]  ← 未受影响
  </script>
</body>
</html>

📋 代码逐行解析(值类型 vs 引用类型赋值)

复制代码
值类型赋值(num1 = 10, num2 = num1):
  内存示意:
    栈: num1 → [  10  ]   num2 → [  10  ]
  num1 和 num2 各自持有独立的值 10。
  修改 num1 → num1 的格子变为 20,num2 的格子不受影响。

引用类型赋值(a = [1,2], b = a):
  内存示意:
    栈: a → [ 0xFF01 ]   b → [ 0xFF01 ]
    堆: 0xFF01 → [ 1, 2 ]
  a 和 b 栈中保存的都是同一个堆地址 0xFF01。
  a[0] = 20 → 修改堆中 0xFF01 处的数据 → b 看到的也是修改后的数据。

引用类型重新赋值(x = [20,2]):
  栈: x → [ 0xFF99 ]  (新的堆地址)  y → [ 0xFF01 ] (旧地址不变)
  堆: 0xFF01 → [ 1, 2 ]  0xFF99 → [ 20, 2 ]
  x 重新指向了新数组,y 仍指向旧数组,互不影响。

函数传参 modifyArr(arr) ← myArr:
  函数参数 arr 是 myArr 引用地址的副本,两者指向同一数组。
  arr[i] += 2 → 修改堆中数组内容 → myArr 也"看到"了修改。

函数内重新赋值 arr = [100,200]:
  arr 指向了新的堆地址,与 nums 断开连接。
  nums 的引用未变,仍指向旧数组,不受影响。

🌐 经典使用场景

复制代码
场景一:Redux/Vuex 状态不可变原则
  // ❌ 直接修改原状态(引用类型共享,框架检测不到变化)
  state.items.push(newItem);

  // ✅ 返回新数组(新引用,框架检测到变化并触发重渲染)
  return { ...state, items: [...state.items, newItem] };
  → 理解引用类型赋值,才能理解为什么状态管理库要求不可变更新

场景二:函数纯函数设计
  // ❌ 不纯:修改了传入的数组(副作用)
  function sort(arr) { arr.sort(); }

  // ✅ 纯函数:返回新数组,不影响原数组
  function sort(arr) { return [...arr].sort(); }
  → 函数式编程中,理解引用传递是设计纯函数的前提

场景三:Vue 响应式系统
  Vue 2 通过 Object.defineProperty 监听对象属性变化。
  直接替换数组元素(arr[0] = 1)Vue 2 无法检测,
  但 push/pop 等方法被重写过,能触发更新。
  → 这正是因为替换元素修改的是堆数据,Vue 的 setter 感知不到。

💼 业务价值

  • 状态管理:React/Vue 的不可变状态设计、Redux 的 reducer 纯函数,都建立在对引用类型赋值行为的深刻理解上
  • API 设计:设计函数是否修改传入参数(mutation)还是返回新值,直接影响 API 的易用性和可预期性
  • 性能优化:浅对比(===)检测引用是否变化是 React.memo、Vue3 computed 等性能优化的底层逻辑

5.3 判等方式

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>值类型 vs 引用类型判等</title>
</head>
<body>
  <script>
    // 值类型:比较值
    console.log(10 === 10);         // true
    console.log('hello' === 'hello'); // true
    console.log(true === true);       // true

    // 引用类型:比较引用地址(是否同一个对象)
    var arr1 = [1, 2, 3];
    var arr2 = [1, 2, 3];
    var arr3 = arr1;

    console.log(arr1 === arr2); // false(内容相同,但不是同一对象)
    console.log(arr1 === arr3); // true (指向同一对象)

    // 对象同理
    console.log({} === {});     // false(不同对象)
    var obj = {};
    var ref = obj;
    console.log(obj === ref);   // true(同一对象)
  </script>
</body>
</html>

📋 代码逐行解析(判等方式)

复制代码
值类型判等 ------ 比较「值本身」:
  10 === 10         → 两边都是数字 10,值相同 → true
  'hello' === 'hello' → 字符串内容相同 → true
  true === true     → 布尔值相同 → true
  ------内存中可能存了两份 10,但 === 只比值,不在乎地址

引用类型判等 ------ 比较「内存地址」:
  var arr1 = [1,2,3];  → 堆中分配地址 0xAA01
  var arr2 = [1,2,3];  → 堆中分配地址 0xAA02(全新的另一个数组!)
  arr1 === arr2  → 0xAA01 !== 0xAA02 → false(内容相同但地址不同)

  var arr3 = arr1;     → arr3 复制了 arr1 的地址 0xAA01
  arr1 === arr3  → 0xAA01 === 0xAA01 → true(同一地址,同一对象)

常见误区:
  {} === {}   → false(每次 {} 都新建对象,地址不同)
  [] === []   → false(同上)
  null === null → true(null 是原始值,按值比较)

🌐 经典使用场景

复制代码
场景一:React shouldComponentUpdate / PureComponent
  prevProps.items === nextProps.items
  → 比较的是数组引用是否变化,而非内容
  → 这正是 immutable 状态更新的意义:改变内容必须换引用,否则组件不会重渲染

场景二:Vue3 响应式 computed 缓存
  computed 内部用 === 判断依赖值是否变化
  → 引用类型直接修改属性不会触发 === 变化,需要用 reactive/ref 包裹

场景三:深比较工具(lodash.isEqual)
  需要比较两个对象"内容"是否相同时,=== 不够用
  → lodash.isEqual 递归比较每个属性,实现内容级深比较

💼 业务价值

  • 性能优化的基础:所有 memo/缓存/diff 算法都依赖引用比较(===),理解它才能正确使用这些优化手段
  • 防 Bug :用 === 判断两个对象"内容是否一样"是极常见的误用,掌握本节能彻底避免

5.4 可变性(Mutability)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>可变性演示</title>
</head>
<body>
  <script>
    // 值类型不可变:对字符串做任何操作都返回新字符串
    var str = 'hello';
    str.toUpperCase(); // 不会修改 str
    console.log(str);  // 'hello'  ← 未变

    var upper = str.toUpperCase();
    console.log(upper); // 'HELLO'  ← 新字符串
    console.log(str);   // 'hello'  ← 原字符串不变

    // 引用类型可变:可以直接修改对象的属性
    var user = { name: 'Alice', age: 25 };
    user.age = 26;           // 修改属性
    user.email = 'a@b.com';  // 添加属性
    delete user.name;        // 删除属性
    console.log(user);       // { age: 26, email: 'a@b.com' }
  </script>
</body>
</html>

📋 代码逐行解析(可变性演示)

复制代码
值类型不可变------字符串操作永远返回新值:
  var str = 'hello';
  str.toUpperCase();   // 调用了方法,但 str 本身不变
  console.log(str);    // 'hello'  ← 原值未改变

  // 要得到转换后的值,必须用变量接收:
  var upper = str.toUpperCase(); // upper 是新字符串 'HELLO'
  console.log(str);   // 'hello' ← str 还是原来那个

  // 原因:JS 引擎在处理字符串方法时,临时装箱为 String 对象,
  //       调用方法返回新字符串,原始值内存地址的内容不允许被修改。
  //       这保证了「同一字符串字面量在内存中只存一份」的优化。

引用类型可变------对象内容可以直接修改:
  var user = { name: 'Alice', age: 25 };
  user.age = 26;           // 修改属性:堆内存 age 格子的值从 25 改为 26
  user.email = 'a@b.com';  // 添加属性:堆内存新增 email 格子
  delete user.name;        // 删除属性:堆内存 name 格子被移除
  console.log(user);       // { age: 26, email: 'a@b.com' }
  // 注意:user 变量保存的堆地址没有变,变的是那块内存的「内容」

🌐 经典使用场景

复制代码
场景一:函数式编程中的不可变数据
  // 值类型天然不可变,是函数式编程的理想数据载体
  // 对象需要借助 Object.freeze() 或 Immer.js 实现不可变
  const frozen = Object.freeze({ x: 1, y: 2 });
  frozen.x = 99; // 静默失败(严格模式报错)
  console.log(frozen.x); // 1 ← 未被修改

场景二:字符串拼接性能陷阱
  // 每次字符串 + 操作都创建新字符串(不可变的代价)
  var result = '';
  for (var i = 0; i < 10000; i++) result += 'x'; // 创建 10000 个临时字符串!
  // ✅ 推荐:用数组收集,最后 join
  var parts = [];
  for (var i = 0; i < 10000; i++) parts.push('x');
  result = parts.join('');

场景三:对象修改的追踪
  // 引用类型的可变性让"追踪变化"变得困难
  var before = { count: 0 };
  var after  = before;
  after.count = 1;
  // before.count 也变成 1 了(同一对象),无法对比"修改前后"
  // → 这正是 Redux 要求每次返回新对象的原因

💼 业务价值

  • 字符串安全:值类型不可变保证了字符串作为 Map key、枚举值的可靠性,多处引用同一字符串不会互相影响
  • 状态溯源:理解引用类型的可变性是实现「时间旅行调试」(如 Redux DevTools)的前提------必须每次返回新对象才能保留历史快照
  • 并发安全:Web Worker 通信中,传递基本类型比传递可变对象更安全(共享内存的竞态问题)

四维对比表

维度 值类型(原始类型) 引用类型
内存存储 栈(直接存值) 栈存地址 + 堆存数据
赋值方式 复制值(独立副本) 复制引用地址(共享数据)
可变性 不可变(immutable) 可变(mutable)
判等 比较值是否相等 比较引用地址是否相同

经典面试题解析

6.1 this 指向题

题目 01(含答案详解)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>this 面试题 01 --- 答案详解</title>
</head>
<body>
  <script>
    var age = 10;

    function func(name, age) {
      this.name = name;
      this.age  = age;
      this.getInfo = function () {
        console.log(this.name);
        console.log(this.age);
      };
      console.log(this.name);
    }

    // 1. 普通调用:this === window
    //    window.name = 'Tom', window.age = 19 (覆盖了 var age = 10)
    //    输出: 'Tom'
    func('Tom', 19);

    // 2. new 调用:this === 新对象 o
    //    o.name = 'Tom', o.age = 19
    //    输出: 'Tom'
    var o = new func('Tom', 19);

    // 3. o.getInfo():this === o
    //    输出: 'Tom', 19
    o.getInfo();

    // 4. age 是全局变量,被 func('Tom',19) 修改为 19
    //    输出: 19
    console.log(age);

    // 5. new o.getInfo():以构造函数形式调用 getInfo
    //    this === 新建的空对象,没有 name 和 age 属性
    //    输出: undefined, undefined
    new o.getInfo();
  </script>
</body>
</html>

题目 02(含答案详解)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>this 面试题 02 --- 答案详解</title>
</head>
<body>
  <script>
    var age = 20;

    var obj = {
      age: 10,
      getAge: function () { console.log(this.age); }
    };

    var obj1 = { age: 30 };
    obj1.prop = obj;

    var fn = obj1.prop.getAge;

    // obj.getAge():this === obj  → 输出 10
    obj.getAge();

    // obj1.prop.getAge():最后的调用者是 obj1.prop,即 obj  → 输出 10
    obj1.prop.getAge();

    // fn():fn 是赤裸函数调用,this === window  → 输出 20
    fn();
  </script>
</body>
</html>

📋 代码逐行解析(面试题 this 02)

复制代码
var age = 20;              // window.age = 20
var obj = { age: 10, getAge: function(){ console.log(this.age); } };
var obj1 = { age: 30 };

obj1.prop = obj;
// obj1.prop 和 obj 指向同一个对象({age:10, getAge:fn})

var fn = obj1.prop.getAge;
// 仅做「赋值」,没有调用
// fn 只是拿到函数引用,与 obj 脱离了关联

// --------- 调用 1 ---------
obj.getAge();
// 「obj.getAge」:点号前是 obj → this = obj → this.age = 10
// 输出:10

// --------- 调用 2 ---------
obj1.prop.getAge();
// 「obj1.prop.getAge」:最后一个点号前是 obj1.prop,而 obj1.prop === obj
// → this = obj → this.age = 10
// 输出:10(不是 30!obj1 只是「中间人」,最终 this 绑定到 obj)

// --------- 调用 3 ---------
fn();
// fn 调用时前面没有任何对象(无点号,无 call/apply/bind,无 new)
// 默认绑定规则:this = window → window.age = 20
// 输出:20

关键记忆点

复制代码
obj1.prop.getAge() 和 obj.getAge() 输出相同(都是 10),
因为 this 绑定到「最后一个点前面的对象」= obj1.prop = obj。

obj1.age = 30 完全无关,因为 this 指向的是 obj,不是 obj1。

6.2 原型链题

题目 01:对象 f 有方法 a 和方法 b 吗?

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>原型链题 01 --- 答案详解</title>
</head>
<body>
  <script>
    var F = function () {};
    Object.prototype.a   = function () { console.log('a'); };
    Function.prototype.b = function () { console.log('b'); };

    var f = new F();

    // f 的原型链:f → F.prototype → Object.prototype
    // 方法 a 在 Object.prototype 上 → f.a 存在 ✅
    console.log('f 有方法 a:', typeof f.a === 'function'); // true
    f.a(); // 'a'

    // 方法 b 在 Function.prototype 上
    // f 是普通对象,原型链上没有 Function.prototype
    // f → F.prototype → Object.prototype → null
    // Function.prototype 不在链上 → f.b 不存在 ❌
    console.log('f 有方法 b:', typeof f.b === 'function'); // false
    console.log('f.b:', f.b); // undefined

    // 但是 F(函数对象)有 b:F → Function.prototype
    console.log('F 有方法 b:', typeof F.b === 'function'); // true
    F.b(); // 'b'

    // 清理
    delete Object.prototype.a;
    delete Function.prototype.b;
  </script>
</body>
</html>

📋 代码逐行解析(原型链题 01)

复制代码
原型链图:
  f  → F.prototype → Object.prototype → null
  F  → Function.prototype → Object.prototype → null

方法 a 挂在:Object.prototype
方法 b 挂在:Function.prototype

问题:f.a 存在吗?
  查找路径:f → F.prototype → Object.prototype ✅ 找到 a
  → f.a 存在,输出 'a'

问题:f.b 存在吗?
  查找路径:f → F.prototype → Object.prototype → null ❌ 全链上无 Function.prototype
  → f.b 不存在,返回 undefined,调用 f.b() 报错

问题:F.b 存在吗?(F 是函数,不是实例)
  查找路径:F → Function.prototype ✅ 找到 b
  → F.b 存在,输出 'b'

核心区分:
  f 是「普通对象实例」,原型链是对象链
  F 是「函数对象」,原型链含 Function.prototype
  两者的原型链不同,能访问到的方法也不同!

题目 02:输出结果

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>原型链题 02 --- 答案详解</title>
</head>
<body>
  <script>
    Object.prototype.a   = function () { console.log('a'); };
    Function.prototype.b = function () { console.log('b'); };

    var F = function () {};  // F 是函数对象
    var f = [];              // f 是数组对象

    // f 的原型链:f → Array.prototype → Object.prototype
    // a 在 Object.prototype 上,f 能访问到
    f.a();     // 'a'  ✅

    // F 的原型链:F → Function.prototype → Object.prototype
    // a 在 Object.prototype 上,F 能访问到
    F.a();     // 'a'  ✅

    // b 在 Function.prototype 上,F 能访问到
    F.b();     // 'b'  ✅

    // f 的原型链上没有 Function.prototype,无法访问 b
    // f.b() 会报错:f.b is not a function
    try {
      f.b();
    } catch (e) {
      console.log('f.b 不存在:', e.message);
    }

    // 清理
    delete Object.prototype.a;
    delete Function.prototype.b;
  </script>
</body>
</html>

📋 代码逐行解析(原型链题 02)

复制代码
结论速查表(在 Object.prototype 挂 a,Function.prototype 挂 b):

对象     原型链                              能访问 a?  能访问 b?
─────────────────────────────────────────────────────────────
f = []   f → Array.prototype → Object.p      ✅ 第2跳   ❌ 无 Function.p
F = fn   F → Function.prototype → Object.p   ✅ 第2跳   ✅ 第1跳

f.a()  → 'a'  ✅(Object.prototype 在数组链的第2层)
F.a()  → 'a'  ✅(Object.prototype 在函数链的第2层)
F.b()  → 'b'  ✅(Function.prototype 在函数链的第1层)
f.b()  → TypeError ❌(Function.prototype 不在数组/对象的原型链上)

易混点:
  Object.prototype 是所有对象的共同祖先 → 任何对象都能访问 a
  Function.prototype 只在函数对象的链上 → 只有函数(Array/Object/自定义fn等)能访问 b
  实例化的普通对象不在函数链上 → 不能访问 b

题目 03:原型替换后的输出

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>原型链题 03 --- 原型替换陷阱</title>
</head>
<body>
  <script>
    function User() {}

    // 1. 替换整个 prototype 为 { name: 'aaaa' }
    User.prototype = { name: 'aaaa' };

    // 2. 实例化 u:此时 u 的 [[Prototype]] === { name: 'aaaa' }
    var u = new User();
    console.log(u.name);  // 'aaaa'  ← 从原型继承

    // 3. 修改现有原型对象上的属性
    User.prototype.name = 'bbb';
    // u 的 [[Prototype]] 还是同一个对象,只是该对象的 name 属性被改了
    console.log(u.name);  // 'bbb'  ← 原型被修改,u 看到了变化

    // 4. 再次替换整个 prototype,指向一个全新对象
    User.prototype = { name: 'ccc' };
    // u 的 [[Prototype]] 还是指向旧的那个对象,新的 prototype 与 u 无关
    console.log(u.name);  // 'bbb'  ← u 仍然引用旧原型对象
  </script>
</body>
</html>

关键原理new 创建实例时,实例的 [[Prototype]] 指向的是当时 Constructor.prototype 所引用的那个对象。之后替换 Constructor.prototype,已有实例不受影响。

📋 代码逐行解析(原型链题 03 --- 原型替换陷阱)

复制代码
内存地址示意(用符号代替地址):

步骤1:User.prototype = { name: 'aaaa' }
  User.prototype ──► 对象A { name: 'aaaa' }

步骤2:var u = new User()
  new 执行时,把「当时 User.prototype 指向的对象」设为 u 的原型
  u.[[Prototype]] ──► 对象A { name: 'aaaa' }
  u.name → 从对象A找到 'aaaa' ✅

步骤3:User.prototype.name = 'bbb'
  修改的是「对象A」的 name 属性,对象A变成 { name: 'bbb' }
  User.prototype 还是指向对象A
  u.[[Prototype]] 还是指向对象A(未变)
  u.name → 从对象A找到 'bbb' ✅(看到了变化)

步骤4:User.prototype = { name: 'ccc' }
  User.prototype ──► 对象B { name: 'ccc' }(全新对象,不是对象A)
  u.[[Prototype]] 还是指向「对象A」(new 时就固定了,不会随 prototype 改变而改变)
  u.name → 从对象A找到 'bbb' ✅(不是 'ccc',因为 u 与对象B无关)

结论:
  修改原型对象的「属性」→ 已有实例立即可见(共享同一对象)
  替换 prototype 为「新对象」→ 已有实例不受影响(链接已固定)

6.3 值类型与引用类型题

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>值类型与引用类型练习题解析</title>
</head>
<body>
  <script>
    console.log('=== 题目 1 ===');
    var num1 = 10;
    var num2 = num1;  // 复制值
    num1 = 20;
    console.log(num1);  // 20
    console.log(num2);  // 10  ← 独立副本,不受影响

    console.log('=== 题目 2 ===');
    var num = 50;
    function f1(num) {
      num = 60;              // 修改的是局部参数
      console.log(num);      // 60
    }
    f1(num);
    console.log(num);        // 50  ← 未被修改

    console.log('=== 题目 3 ===');
    var num1 = 55;
    var num2 = 66;
    function f2(num, num1) {
      num  = 100;   // 修改局部参数
      num1 = 100;   // 修改局部参数
      num2 = 100;   // 修改的是全局 num2(无 var 声明的变量向上查)
      console.log(num);   // 100
      console.log(num1);  // 100
      console.log(num2);  // 100
    }
    f2(num1, num2);
    console.log(num1);    // 55  ← 局部参数改动不影响外部
    console.log(num2);    // 100 ← 全局变量被修改了!
    try { console.log(num); } catch(e) { console.log('num 未定义'); }

    console.log('=== 题目 4 ===');
    var a = 10;
    var b = 20;
    function add(a, b) {
      a = 30;         // 修改局部参数 a
      return a + b;   // 30 + 20 = 50
    }
    add(a, b);
    console.log(a);   // 10  ← 外部 a 未被修改

    function f3(arr) {
      for (var i = 0; i < arr.length; i++) {
        arr[i] += 2;    // 修改的是原数组内容
      }
      console.log('函数内 arr:', arr);  // [3, 4]
    }
    var arr = [1, 2];
    f3(arr);
    console.log('函数外 arr:', arr);    // [3, 4]  ← 同一对象,已被修改

    console.log('=== 题目 5 ===');
    var x = [1, 2];
    var y = x;      // y 和 x 指向同一数组
    x[0] = 20;      // 修改数组内容
    console.log(y); // [20, 2]  ← 同一对象

    console.log('=== 题目 6 ===');
    function Person(name, age, salary) {
      this.name   = name;
      this.age    = age;
      this.salary = salary;
    }
    function f4(pp) {
      pp.name = 'ls';               // 通过引用修改对象属性
      pp = new Person('aa', 18, 10);// pp 重新指向新对象,外部 p 不受影响
    }
    var p = new Person('zs', 18, 1000);
    console.log(p.name);  // 'zs'
    f4(p);
    console.log(p.name);  // 'ls'  ← pp.name='ls' 修改了 p 的属性
    // console.log(pp.name); // ReferenceError: pp is not defined(pp 是函数局部变量)
  </script>
</body>
</html>

📋 代码逐行解析(6道综合练习题)

复制代码
题目1:值类型赋值
  num2 = num1  → 复制值 10,num2 独立拥有 10
  num1 = 20    → 只改 num1 的格子,num2 格子不变
  输出:num1=20, num2=10

题目2:函数传值类型参数
  f1(num) 传入 50 的副本,函数内修改的是局部参数
  外部 num 格子未被触碰
  输出:函数内=60, 函数外=50

题目3:函数参数同名遮蔽 + 全局变量修改(易错!)
  f2(num, num1) 调用时:
    形参 num  接收 num1(55) 的副本 → 函数内修改不影响外部 num1
    形参 num1 接收 num2(66) 的副本 → 函数内修改不影响外部 num2
    num2 = 100 → 函数内没有局部 num2,向外查找,修改了全局 num2!
  输出:函数内 100/100/100,函数外 num1=55(未改), num2=100(被改!)

题目4:引用类型传参
  add(a,b):形参 a 接收 10 的副本,a=30 只改局部,外部 a 仍是 10
  modifyArr(arr):arr 和 myArr 指向同一数组,arr[i]+=2 修改了堆数据
  → myArr 也看到了变化:[3,4]

题目5:同一引用的两种操作
  var y=x 后,x[0]=20 修改数组内容 → y[0] 也变(同一对象)
  若是 x=[20,2] 则 x 指向新数组,y 不变

题目6:函数内操作引用 vs 重新赋值(最难!)
  f4(pp) 调用时,pp 是 p 引用地址的副本
  pp.name='ls' → 通过 pp 修改堆中 p 的数据 → 外部 p.name 变为 'ls'
  pp = new Person(...) → pp 重新指向新对象,与 p 断开联系
  → 外部 p 不受最后一行影响,p.name 还是 'ls'

综合经典场景实战

场景一:递归实现数组扁平化

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>数组扁平化 - 递归实现</title>
</head>
<body>
  <script>
    var nums = [
      [1000, 2000, 3000],
      'hello',
      [
        [10, 20, 30],
        ['a', 'b', ['A', 'B', 'C']],
        '小乐'
      ],
      12313,
      [101, 202, 303]
    ];

    // 方式一:递归(版)
    var newNums = [];
    function flatArray(arr) {
      for (var i = 0; i < arr.length; i++) {
        if (arr[i] instanceof Array) {
          flatArray(arr[i]);  // 递归
        } else {
          newNums.push(arr[i]);
        }
      }
    }
    flatArray(nums);
    console.log('递归扁平化:', newNums);
    // [1000, 2000, 3000, 'hello', 10, 20, 30, 'a', 'b', 'A', 'B', 'C', '小乐', 12313, 101, 202, 303]

    // 方式二:纯函数版(无副作用,返回新数组)
    function flatArrayPure(arr) {
      var result = [];
      for (var i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
          var sub = flatArrayPure(arr[i]);
          for (var j = 0; j < sub.length; j++) {
            result.push(sub[j]);
          }
        } else {
          result.push(arr[i]);
        }
      }
      return result;
    }
    console.log('纯函数扁平化:', flatArrayPure(nums));

    // 方式三:ES6 Array.prototype.flat()(现代写法)
    var nested = [1, [2, [3, [4, 5]]]];
    console.log('flat(1):', nested.flat(1));     // [1, 2, [3, [4, 5]]]
    console.log('flat(2):', nested.flat(2));     // [1, 2, 3, [4, 5]]
    console.log('flat(Infinity):', nested.flat(Infinity)); // [1, 2, 3, 4, 5]
  </script>
</body>
</html>

📋 代码逐行解析(递归数组扁平化)

js 复制代码
function flatArray(arr) {
  for (var i = 0; i < arr.length; i++) {
    if (arr[i] instanceof Array) {
      // 递归边界:当前元素是数组 → 继续向下递归
      // 每次递归调用都在处理"更深一层"的子数组
      flatArray(arr[i]);
    } else {
      // 递归出口:当前元素不是数组(是叶子节点)→ 直接放入结果
      newNums.push(arr[i]);
    }
  }
}

// 递归调用栈示意(以 [[1,2], 'hello'] 为例):
// flatArray([[1,2], 'hello'])
//   ├─ [1,2] 是数组 → flatArray([1,2])
//   │     ├─ 1 不是数组 → newNums.push(1)
//   │     └─ 2 不是数组 → newNums.push(2)
//   └─ 'hello' 不是数组 → newNums.push('hello')
// 结果:[1, 2, 'hello']

// 纯函数版:每次调用返回新数组,不依赖外部变量(更好的设计)
function flatArrayPure(arr) {
  var result = [];
  for (var i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      // Array.isArray 比 instanceof 更可靠(跨 iframe 环境)
      var sub = flatArrayPure(arr[i]);
      for (var j = 0; j < sub.length; j++) result.push(sub[j]);
    } else {
      result.push(arr[i]);
    }
  }
  return result;
}

🌐 经典使用场景

复制代码
场景一:树形数据扁平化(最常见)
  后端返回的菜单树、分类树、评论树等嵌套结构,
  前端需要扁平化后建立 id → node 的快速索引:
  var flat = flatArrayPure(treeData);
  var indexMap = flat.reduce((map, node) => { map[node.id] = node; return map; }, {});
  → O(1) 查找任意节点,而无需每次遍历树

场景二:表格数据合并
  电商后台中,多层分类商品列表 → 扁平表格展示
  [{category: '数码', items:[{name:'手机'}, {name:'平板'}]}, ...]
  → flatten 后直接渲染 <table>

场景三:权限路由扁平化
  路由配置通常是嵌套结构,权限校验需要扁平列表:
  var flatRoutes = flatArray(nestedRoutes);
  flatRoutes.forEach(route => route.meta.requiresAuth && checkPermission(route));

场景四:搜索功能实现
  对嵌套数据结构做全文搜索时,先扁平化再过滤,
  比递归搜索更简单、更高效(O(n) 线性搜索 vs 重复递归)

💼 业务价值

  • 数据处理:前端 80% 的数据处理都涉及对后端返回的嵌套结构做变换,扁平化是最基础的一步
  • 性能:预先扁平化 + 建立索引,将后续的多次查找从 O(n) 递归降至 O(1),在大数据量列表中效果显著
  • 代码复用:封装成纯函数的扁平化工具,在同一项目中可多处复用(树形菜单、分类树、评论列表等场景高度相似)

相关推荐
csbysj20201 小时前
C 标准库 - `<time.h>`
开发语言
小陈的进阶之路1 小时前
Python系列课(10)——SQL
开发语言·python·sql
淑子啦1 小时前
TS 和组件绑定深耕(泛型表格)
前端·javascript·react.js
测试员周周1 小时前
【Appium 系列】第03节-驱动初始化 — BaseDriver 的设计与实现
开发语言·人工智能·python·功能测试·appium·测试用例·web app
坚果派·白晓明9 小时前
【鸿蒙PC三方库移植适配框架解读系列】第八篇:扩展lycium框架使其满足rust三方库适配
c语言·开发语言·华为·rust·harmonyos·鸿蒙
花间相见9 小时前
【PaddleOCR教程01】PP-OCRv5 全面指南:从模型架构到实战部署
开发语言·r语言
小短腿的代码世界10 小时前
Qt 股票订单撮合引擎:高频交易系统的核心心脏
开发语言·数据库·qt·系统架构·交互
不会敲代码110 小时前
手写 Zustand:三十分钟带你搞懂状态管理库的核心原理
前端·javascript·源码
神奇的程序员10 小时前
重构了自己5年前写的截图插件
前端·javascript·架构