普通视图

发现新文章,点击刷新页面。
昨天以前首页

Vue3 中的 <keep-alive> 详解

2025年12月26日 09:56

<keep-alive> 是 Vue3 内置的抽象组件(自身不会渲染为真实 DOM 元素),核心作用是缓存包裹在其中的组件实例,保留组件的状态和 DOM 结构,避免组件反复创建和销毁带来的性能损耗,常用于需要保留状态的场景(如标签页切换、列表页返回详情页等)。

一、核心特性与作用

1. 核心功能

  • 缓存组件状态:被 <keep-alive> 包裹的组件,在切换隐藏时不会触发 unmounted(销毁),而是被缓存起来;再次显示时不会触发 mounted(重新创建),而是恢复之前的状态。
  • 优化性能:避免组件反复创建 / 销毁、数据重新请求、DOM 重新渲染,减少资源消耗。
  • 保留组件上下文:比如表单输入内容、滚动条位置、组件内部的状态数据等,切换后仍能保持原有状态。

2. 关键特点

  • 是抽象组件,不生成 DOM 节点,也不会出现在组件的父组件链中;
  • 仅对动态组件<component :is="componentName">)或路由组件生效;
  • 可通过属性配置缓存规则(指定缓存 / 排除缓存的组件)。

二、基本使用方式

1. 基础用法:包裹动态组件

用于切换多个组件时,缓存不活跃的组件状态:

<template>
  <div>
    <!-- 切换按钮 -->
    <button @click="currentComponent = 'ComponentA'">组件A</button>
    <button @click="currentComponent = 'ComponentB'">组件B</button>

    <!-- keep-alive 包裹动态组件,缓存组件实例 -->
    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';

// 控制当前显示的组件
const currentComponent = ref('ComponentA');
</script>

此时切换组件 A/B,组件不会被销毁,再次切换回来时会保留之前的状态(如 ComponentA 中的输入框内容)。

2. 常用场景:包裹路由组件

在路由切换时缓存页面状态(如列表页滚动位置、筛选条件),是项目中最常用的场景:

<!-- App.vue 或路由出口组件 -->
<template>
  <router-view v-slot="{ Component }">
    <!-- 缓存路由组件 -->
    <keep-alive>
      <component :is="Component" />
    </keep-alive>
  </router-view>
</template>

三、核心属性:配置缓存规则

<keep-alive> 提供 3 个核心属性,用于灵活控制缓存的组件范围:

1. include:指定需要缓存的组件

  • 类型:String | RegExp | Array

  • 作用:只有名称匹配的组件才会被缓存(组件名称通过 name 选项定义,Vue3 单文件组件中 <script> 内的 name 或 <script setup> 配合 defineOptions({ name: 'xxx' }) 定义)。

  • 示例:

    <!-- 字符串(逗号分隔多个组件名) -->
    <keep-alive include="ComponentA,ComponentB">
      <component :is="currentComponent"></component>
    </keep-alive>
    
    <!-- 正则表达式(需用 v-bind 绑定) -->
    <keep-alive :include="/^Component/">
      <component :is="currentComponent"></component>
    </keep-alive>
    
    <!-- 数组(需用 v-bind 绑定) -->
    <keep-alive :include="['ComponentA', 'ComponentB']">
      <component :is="currentComponent"></component>
    </keep-alive>
    

2. exclude:指定不需要缓存的组件

  • 类型:String | RegExp | Array

  • 作用:名称匹配的组件不会被缓存,优先级高于 include

  • 示例:

    <keep-alive exclude="ComponentC">
      <component :is="currentComponent"></component>
    </keep-alive>
    

3. max:设置缓存组件的最大数量

  • 类型:Number

  • 作用:限制缓存的组件实例数量,当缓存实例超过 max 时,会按照「LRU(最近最少使用)」策略,销毁最久未使用的组件缓存。

  • 示例:

    <!-- 最多缓存 3 个组件实例 -->
    <keep-alive :max="3">
      <component :is="currentComponent"></component>
    </keep-alive>
    

四、缓存组件的生命周期钩子

被 <keep-alive> 缓存的组件,不会触发 mounted/unmounted,而是触发专属的生命周期钩子:

1. onActivated:组件被激活时触发

  • 时机:缓存的组件从隐藏状态切换为显示状态时(第一次渲染时,会在 mounted 之后触发;后续激活时,仅触发 onActivated)。
  • 用途:恢复组件激活后的状态(如重新监听事件、刷新数据等)。

2. onDeactivated:组件被失活时触发

  • 时机:缓存的组件从显示状态切换为隐藏状态时(不会触发 unmounted)。
  • 用途:清理组件失活后的资源(如取消事件监听、清除定时器等)。

示例:组件内使用钩子

<!-- ComponentA.vue -->
<template>
  <div>组件A:<input type="text" v-model="inputValue"></div>
</template>

<script setup>
import { ref, onActivated, onDeactivated, onMounted } from 'vue';

const inputValue = ref('');

// 第一次渲染时触发(后续激活不触发)
onMounted(() => {
  console.log('组件A 首次挂载');
});

// 组件被激活时触发(切换显示时)
onActivated(() => {
  console.log('组件A 被激活');
  // 可在此恢复滚动条位置、重新请求最新数据等
});

// 组件被失活时触发(切换隐藏时)
onDeactivated(() => {
  console.log('组件A 被失活');
  // 可在此取消定时器、取消事件监听等
});
</script>

五、高级用法:结合路由配置缓存

在实际项目中,常需要针对特定路由进行缓存,可通过「路由元信息(meta)」配合 <keep-alive> 实现精准缓存:

1. 配置路由元信息

在 router/index.js 中,给需要缓存的路由添加 meta.keepAlive: true

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import ListPage from '../views/ListPage.vue';
import DetailPage from '../views/DetailPage.vue';
import HomePage from '../views/HomePage.vue';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: HomePage,
    meta: { keepAlive: false } // 不缓存
  },
  {
    path: '/list',
    name: 'List',
    component: ListPage,
    meta: { keepAlive: true } // 需要缓存
  },
  {
    path: '/detail/:id',
    name: 'Detail',
    component: DetailPage,
    meta: { keepAlive: false } // 不缓存
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;

2. 根据路由元信息缓存

在路由出口处,通过 v-if 判断路由的 meta.keepAlive 属性,决定是否缓存:

<!-- App.vue -->
<template>
  <router-view v-slot="{ Component, route }">
    <!-- 缓存需要保留状态的路由组件 -->
    <keep-alive>
      <component
        :is="Component"
        v-if="route.meta.keepAlive"
      />
    </keep-alive>
    <!-- 不缓存的组件直接渲染 -->
    <component
      :is="Component"
      v-if="!route.meta.keepAlive"
    />
  </router-view>
</template>

六、注意事项与常见问题

1. 注意事项

  • <keep-alive> 仅对动态组件或路由组件生效,对普通组件(直接渲染的组件)无效;
  • 组件名称必须正确定义:<script setup> 中需通过 defineOptions({ name: 'XXX' }) 定义组件名,否则 include/exclude 无法匹配;
  • 缓存的组件会占用内存,若缓存过多组件,可能导致内存泄漏,建议通过 max 属性限制缓存数量;
  • 对于需要实时刷新数据的组件,避免使用 <keep-alive>,或在 onActivated 钩子中手动刷新数据。

2. 常见问题

  • 问题 1:缓存后组件数据不更新?解决方案:在 onActivated 钩子中重新请求数据或更新组件状态,确保激活时获取最新数据。

  • 问题 2include/exclude 配置不生效?解决方案:检查组件名称是否正确定义,正则 / 数组形式是否通过 v-bind 绑定,避免直接写字面量。

  • 问题 3:路由切换后滚动条位置未保留?解决方案:在 onDeactivated 中记录滚动条位置,在 onActivated 中恢复滚动条位置:

    // ListPage.vue
    import { ref, onActivated, onDeactivated } from 'vue';
    
    // 记录滚动条位置
    const scrollTop = ref(0);
    
    onDeactivated(() => {
      // 失活时记录滚动位置
      scrollTop.value = document.documentElement.scrollTop || document.body.scrollTop;
    });
    
    onActivated(() => {
      // 激活时恢复滚动位置
      document.documentElement.scrollTop = scrollTop.value;
      document.body.scrollTop = scrollTop.value;
    });
    

总结

  1. <keep-alive> 是 Vue3 内置抽象组件,核心作用是缓存组件实例、保留组件状态、优化性能;
  2. 基础用法:包裹动态组件或路由组件,通过 include/exclude/max 配置缓存规则;
  3. 生命周期:缓存组件触发 onActivated(激活)和 onDeactivated(失活),替代 mounted/unmounted
  4. 高级用法:结合路由元信息 meta.keepAlive,实现特定路由的精准缓存;
  5. 注意:合理控制缓存数量,避免内存泄漏,需要实时刷新数据的场景在 onActivated 中手动更新。

JavaScript 中的深拷贝与浅拷贝详解

2025年12月26日 09:16

深拷贝和浅拷贝是 JavaScript 中处理引用类型数据(对象、数组等)的核心概念,二者的本质区别在于是否复制引用类型的深层嵌套数据,直接影响数据操作的独立性,是开发中避免数据污染的关键。

一、先明确:为什么需要拷贝?(引用类型的特性)

JavaScript 数据类型分为两类,拷贝行为仅对引用类型有区分(原始类型为值传递,不存在深浅拷贝):

数据类型类别 包含类型 拷贝特性
原始类型 String、Number、Boolean、Null、Undefined、Symbol、BigInt 赋值 / 拷贝时传递「值本身」,修改新值不会影响原值
引用类型 Object(普通对象、数组、函数、正则等) 赋值 / 浅拷贝时传递「内存地址(引用)」,修改新数据会影响原数据;深拷贝才会复制数据本身,实现完全独立

示例:引用类型的默认赋值(引用传递,非拷贝)

// 引用类型:数组
const arr1 = [1, 2, { name: "张三" }];
const arr2 = arr1; // 仅传递引用,不是拷贝
arr2[0] = 100;
arr2[2].name = "李四";
console.log(arr1); // [100, 2, { name: "李四" }](原值被修改)
console.log(arr2); // [100, 2, { name: "李四" }]

二、浅拷贝(Shallow Copy):仅复制表层数据

1. 核心定义

浅拷贝是指只复制引用类型的表层属性(第一层数据) ,对于深层嵌套的引用类型(如对象中的对象、数组中的数组),仅复制其内存地址(引用),新旧数据的深层嵌套部分会共享同一块内存,修改其中一个的深层数据会影响另一个。

2. 常见实现方式

(1)数组浅拷贝

  • Array.prototype.slice()

    const arr1 = [1, 2, { age: 25 }];
    const arr2 = arr1.slice(); // 浅拷贝数组
    // 修改表层数据:不影响原值
    arr2[0] = 100;
    console.log(arr1[0]); // 1
    console.log(arr2[0]); // 100
    // 修改深层引用类型:影响原值
    arr2[2].age = 30;
    console.log(arr1[2].age); // 30(原值被修改)
    console.log(arr2[2].age); // 30
    
  • Array.prototype.concat()

    const arr1 = [1, 2, { age: 25 }];
    const arr2 = arr1.concat(); // 浅拷贝
    
  • 扩展运算符 [...arr]

    const arr1 = [1, 2, { age: 25 }];
    const arr2 = [...arr1]; // 浅拷贝
    

(2)对象浅拷贝

  • Object.assign(target, ...sources)

    const obj1 = { name: "张三", info: { age: 25 } };
    const obj2 = Object.assign({}, obj1); // 浅拷贝到空对象
    // 修改表层数据:不影响原值
    obj2.name = "李四";
    console.log(obj1.name); // 张三
    console.log(obj2.name); // 李四
    // 修改深层引用类型:影响原值
    obj2.info.age = 30;
    console.log(obj1.info.age); // 30(原值被修改)
    console.log(obj2.info.age); // 30
    
  • 扩展运算符 {...obj}

    const obj1 = { name: "张三", info: { age: 25 } };
    const obj2 = { ...obj1 }; // 浅拷贝
    

3. 浅拷贝的特点

  • 优点:实现简单、性能开销小,适合仅包含表层数据的引用类型;
  • 缺点:无法独立深层嵌套数据,修改深层数据会造成原数据污染;
  • 适用场景:只需复制表层数据,无需修改深层嵌套内容的场景(如展示数据副本、临时修改表层属性)。

三、深拷贝(Deep Copy):复制所有层级数据

1. 核心定义

深拷贝是指递归复制引用类型的所有层级数据,不仅复制表层属性,还会对深层嵌套的每个引用类型都创建独立的副本,新旧数据完全隔离,修改其中一个不会影响另一个,实现真正意义上的 “复制”。

2. 常见实现方式

(1)JSON 序列化 / 反序列化(简单场景首选)

通过 JSON.stringify() 将对象转为 JSON 字符串,再通过 JSON.parse() 解析为新对象,实现深拷贝。

const obj1 = { name: "张三", info: { age: 25 }, hobbies: ["篮球", "游戏"] };
const obj2 = JSON.parse(JSON.stringify(obj1)); // 深拷贝

// 修改表层数据:不影响原值
obj2.name = "李四";
// 修改深层数据:不影响原值
obj2.info.age = 30;
obj2.hobbies[0] = "足球";

console.log(obj1.name); // 张三
console.log(obj1.info.age); // 25
console.log(obj1.hobbies[0]); // 篮球
console.log(obj2.name); // 李四
console.log(obj2.info.age); // 30
console.log(obj2.hobbies[0]); // 足球

注意:JSON 方式的局限性(无法处理特殊类型)

  • 无法拷贝函数、正则表达式、Date 对象(会转为字符串 / 对象字面量,丢失原有特性);
  • 无法拷贝 Symbol 类型属性、undefined 类型属性(会被忽略);
  • 无法处理循环引用(如 obj.a = obj,会报错)。

(2)手动递归实现(灵活可控,支持特殊类型)

通过递归遍历对象 / 数组的每一层,对原始类型直接赋值,对引用类型创建新副本,可自定义处理特殊类型。

// 深拷贝工具函数
function deepClone(target) {
  // 1. 处理原始类型和 null
  if (typeof target !== "object" || target === null) {
    return target;
  }

  // 2. 处理 Date 对象
  if (target instanceof Date) {
    return new Date(target);
  }

  // 3. 处理 RegExp 对象
  if (target instanceof RegExp) {
    return new RegExp(target.source, target.flags);
  }

  // 4. 处理数组和普通对象(创建新副本)
  const result = Array.isArray(target) ? [] : {};

  // 5. 递归遍历,拷贝所有层级属性
  for (let key in target) {
    // 仅拷贝自身属性,不拷贝原型链属性
    if (target.hasOwnProperty(key)) {
      result[key] = deepClone(target[key]);
    }
  }

  return result;
}

// 测试
const obj1 = {
  name: "张三",
  info: { age: 25 },
  hobbies: ["篮球", "游戏"],
  birth: new Date("1999-01-01"),
  reg: /abc/gi,
  fn: () => console.log("hello")
};
const obj2 = deepClone(obj1);

obj2.info.age = 30;
obj2.birth.setFullYear(2000);
obj2.fn = () => console.log("world");

console.log(obj1.info.age); // 25(不影响原值)
console.log(obj1.birth.getFullYear()); // 1999(不影响原值)
console.log(obj1.fn()); // hello(函数独立)
console.log(obj2.fn()); // world

(3)第三方库(成熟稳定,推荐生产环境)

  • Lodash 库的 _.cloneDeep()(支持所有类型,处理循环引用)

    // 安装:npm i lodash
    const _ = require("lodash");
    
    const obj1 = { name: "张三", info: { age: 25 }, a: obj1 }; // 循环引用
    const obj2 = _.cloneDeep(obj1); // 深拷贝,正常处理循环引用
    
    obj2.info.age = 30;
    console.log(obj1.info.age); // 25
    
  • jQuery 库的 $.extend(true, {}, obj)(true 表示深拷贝)

    const obj1 = { name: "张三", info: { age: 25 } };
    const obj2 = $.extend(true, {}, obj1); // 深拷贝
    

3. 深拷贝的特点

  • 优点:新旧数据完全独立,修改任意一方不会影响另一方,避免数据污染;
  • 缺点:实现复杂(手动递归需处理多种特殊类型)、性能开销大(递归遍历所有层级);
  • 适用场景:需要修改拷贝后的数据,且数据包含深层嵌套引用类型的场景(如表单提交、状态管理、复杂数据处理)。

四、深拷贝 vs 浅拷贝 核心对比

对比维度 浅拷贝(Shallow Copy) 深拷贝(Deep Copy)
拷贝层级 仅拷贝表层(第一层)数据 递归拷贝所有层级数据
引用类型处理 深层嵌套引用类型仅复制内存地址(共享) 深层嵌套引用类型创建独立副本(不共享)
数据独立性 深层数据共享,修改会相互影响 完全独立,修改互不影响
实现难度 简单(原生 API 即可实现) 复杂(需处理特殊类型、循环引用)
性能开销 小(仅遍历表层) 大(递归遍历所有层级)
适用场景 表层数据拷贝、无需修改深层数据 复杂嵌套数据拷贝、需要独立修改数据
常见实现 数组:slice、concat、[...arr];对象:Object.assign、{...obj} JSON.parse (JSON.stringify ())、手动递归、_.cloneDeep ()

五、常见误区

  1. 认为 Object.assign 是深拷贝Object.assign 仅对第一层数据实现值拷贝,深层引用类型仍为引用传递,属于浅拷贝;
  2. JSON 方式能处理所有数据:JSON 序列化无法处理函数、正则、循环引用、Symbol 等类型,仅适用于简单 JSON 数据;
  3. 原始类型需要深浅拷贝:原始类型赋值时直接传递值,不存在引用,无需区分深浅拷贝;
  4. 深拷贝一定优于浅拷贝:深拷贝性能开销大,若数据无深层嵌套,浅拷贝更高效,无需过度使用深拷贝。

总结

  1. 核心区别:是否拷贝深层嵌套的引用类型,决定数据是否独立;
  2. 原始类型无深浅拷贝之分,引用类型才需要区分;
  3. 浅拷贝:简单高效,适合表层数据,推荐 [...arr]/{...obj}/Object.assign
  4. 深拷贝:完全独立,适合复杂嵌套数据,简单场景用 JSON.parse(JSON.stringify()),生产环境推荐 _.cloneDeep()
  5. 选型原则:根据数据结构选择,无需深层独立时优先浅拷贝,避免性能浪费。

JS原型链详解

2025年12月24日 13:52

原型链是 JavaScript 实现继承的核心机制,本质是一条「实例与原型之间的引用链条」,用于解决属性和方法的查找、共享与继承问题,理解原型链是掌握 JavaScript 面向对象编程的关键。

一、先搞懂 3 个核心概念(原型链的基础)

在讲原型链之前,必须先明确 prototype__proto__constructor 这三个不可分割的概念,它们是构成原型链的基本单元。

1. prototype(原型属性 / 显式原型)

  • 定义:只有函数(构造函数)才拥有 prototype 属性,它指向一个对象(称为「原型对象」),这个对象的作用是存放所有实例需要共享的属性和方法

  • 通俗理解:构造函数的「原型仓库」,所有通过该构造函数创建的实例,都能共享这个仓库里的内容,避免方法重复创建浪费内存。

  • 示例

    // 构造函数
    function Person(name) {
      this.name = name; // 实例私有属性
    }
    // prototype 指向原型对象,存放共享方法
    Person.prototype.sayName = function() {
      console.log('我的名字:', this.name);
    };
    
    console.log(Person.prototype); // { sayName: ƒ, constructor: ƒ Person() }
    

2. __proto__(原型链指针 / 隐式原型)

  • 定义:几乎所有对象(除 null/undefined都拥有 __proto__ 属性(ES6 规范中称为 [[Prototype]]__proto__ 是浏览器提供的访问接口),它指向创建该对象的构造函数的原型对象(prototype

  • 通俗理解:对象的「原型导航器」,通过它可以找到自己的 “原型仓库”,进而向上查找属性 / 方法。

  • 示例

    const person1 = new Person('张三');
    // person1 的 __proto__ 指向 Person.prototype
    console.log(person1.__proto__ === Person.prototype); // true
    console.log(person1.__proto__.sayName === Person.prototype.sayName); // true
    

3. constructor(构造函数指向)

  • 定义:原型对象(prototype)中默认包含 constructor 属性,它指向对应的构造函数本身,用于标识对象的创建来源。

  • 作用:修复原型指向后,保证实例能正确追溯到构造函数(避免继承时构造函数指向混乱)。

  • 示例

    // 原型对象的 constructor 指向构造函数
    console.log(Person.prototype.constructor === Person); // true
    // 实例可通过 __proto__ 找到 constructor
    console.log(person1.__proto__.constructor === Person); // true
    console.log(person1.constructor === Person); // true(自动向上查找)
    

二、原型链的核心定义与形成过程

1. 核心定义

原型链是由 __proto__ 串联起来的「对象 → 原型对象 → 上层原型对象 → ... → null」的链式结构,当访问一个对象的属性 / 方法时,JavaScript 会先在对象自身查找,找不到则通过 __proto__ 向上查找原型对象,依次类推,直到找到属性 / 方法或到达原型链末端(null)。

2. 原型链的形成过程(三步成型)

我们以 Person 实例为例,拆解原型链的形成:

  1. 第一步:创建构造函数 Person,其 prototype 指向 Person 原型对象(包含 sayName 方法和 constructor);
  2. 第二步:通过 new Person() 创建实例 person1person1.__proto__ 指向 Person.prototype(形成第一层链接);
  3. 第三步Person.prototype 是一个普通对象,它的 __proto__ 指向 Object.prototype(JavaScript 所有对象的根原型),Object.prototype.__proto__ 指向 null(原型链末端)。

最终形成的原型链:

plaintext

person1(实例)
  ↓ __proto__
Person.prototypePerson 原型对象)
  ↓ __proto__
Object.prototype(根原型对象)
  ↓ __proto__
null(原型链末端)

可视化示例

// 验证原型链结构
const person1 = new Person('张三');

// 第一层:person1 -> Person.prototype
console.log(person1.__proto__ === Person.prototype); // true
// 第二层:Person.prototype -> Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype); // true
// 第三层:Object.prototype -> null
console.log(Object.prototype.__proto__ === null); // true
// 完整原型链:person1 -> Person.prototype -> Object.prototype -> null

三、原型链的核心作用:属性 / 方法查找机制

这是原型链最核心的功能,遵循「自身优先,向上追溯,末端终止」的规则:

1. 查找规则步骤

  1. 当访问对象的某个属性 / 方法时,先在对象自身的属性中查找(比如 person1.name,直接在 person1 上找到);
  2. 如果自身没有,就通过 __proto__ 向上查找原型对象(比如 person1.sayName(),自身没有,找到 Person.prototype 上的 sayName);
  3. 如果原型对象也没有,继续通过原型对象的 __proto__ 向上查找上层原型(比如 person1.toString()Person.prototype 没有,找到 Object.prototype 上的 toString);
  4. 直到找到目标属性 / 方法,或到达原型链末端 null,此时返回 undefined(属性)或报错(方法)。

2. 代码示例

const person1 = new Person('张三');

// 1. 查找自身属性:name
console.log(person1.name); // 张三(自身存在,直接返回)

// 2. 查找原型方法:sayName
console.log(person1.sayName()); // 我的名字:张三(自身没有,向上找到 Person.prototype)

// 3. 查找上层原型方法:toString
console.log(person1.toString()); // [object Object](Person.prototype 没有,向上找到 Object.prototype)

// 4. 查找不存在的属性:age
console.log(person1.age); // undefined(原型链末端仍未找到,返回 undefined)

3. 注意:属性修改仅影响自身,不影响原型

原型链是「只读」的查找链路,修改对象的属性时,只会修改对象自身,不会改变原型对象的属性(除非直接显式修改原型):

// 错误:试图修改原型方法(实际是给 person1 新增了一个私有方法 sayName,覆盖了原型查找)
person1.sayName = function() {
  console.log('我是私有方法:', this.name);
};
person1.sayName(); // 我是私有方法:张三(优先访问自身方法)
console.log(Person.prototype.sayName()); // 我的名字:undefined(原型方法未被修改)

四、原型链与继承的关系

原型链是 JavaScript 继承的底层支撑,所有继承方式(原型链继承、组合继承等)本质都是通过修改 __proto__ 或 prototype,构建新的原型链结构,实现子类对父类属性 / 方法的继承。

示例:简单继承的原型链结构

// 父类构造函数
function Animal(name) {
  this.name = name;
}
Animal.prototype.sayName = function() {
  console.log('名称:', this.name);
};

// 子类构造函数
function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}
// 构建继承:让 Dog.prototype.__proto__ 指向 Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 子类实例的原型链
const dog1 = new Dog('旺财', '中华田园犬');
// 原型链:dog1 -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
console.log(dog1.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
// 继承生效:dog1 能访问 Animal.prototype 的 sayName 方法
dog1.sayName(); // 名称:旺财

五、原型链的末端:Object.prototype 与 null

  1. Object.prototype:是 JavaScript 所有对象的「根原型」,所有对象最终都会继承它的属性和方法(如 toString()hasOwnProperty()valueOf() 等);

  2. null:是原型链的「终点」,Object.prototype.__proto__ 指向 null,表示没有上层原型,查找过程到此终止;

  3. 验证

    console.log(Object.prototype.__proto__); // null
    console.log(Object.prototype.hasOwnProperty('toString')); // true(根原型的自有方法)
    console.log(person1.hasOwnProperty('name')); // true(自身属性)
    console.log(person1.hasOwnProperty('sayName')); // false(原型上的方法,非自身属性)
    

六、常见误区

  1. 混淆 prototype 和 __proto__prototype 是函数的属性,__proto__ 是对象的属性,两者的关联是「对象.proto = 构造函数.prototype」;
  2. 原型链是可写的__proto__ 可以手动修改(不推荐,会破坏原有继承结构,影响性能);
  3. 所有对象都有 prototype:只有函数才有 prototype,普通对象只有 __proto__
  4. hasOwnProperty 能查找原型属性hasOwnProperty 仅判断对象自身是否有该属性,不会向上查找原型链。

总结

  1. 原型链的核心是 __proto__ 串联的链式结构,末端是 null,根节点是 Object.prototype
  2. 3 个核心概念:prototype(函数的原型仓库)、__proto__(对象的原型指针)、constructor(原型的构造函数指向);
  3. 核心功能:实现属性 / 方法的分层查找(自身 → 原型 → 上层原型 → ... → null),支撑 JavaScript 继承机制;
  4. 本质:通过共享原型对象的属性 / 方法,实现代码复用,减少内存消耗。

JS继承方式详解

2025年12月24日 13:47

JavaScript 继承基于原型链实现,不存在类继承的原生语法(ES6 class 是语法糖,底层仍为原型继承),常见继承方式按演进逻辑可分为以下 6 种,各有优劣与适用场景:

一、原型链继承(最基础的继承方式)

核心原理

将父类的实例作为子类的原型(SubType.prototype = new SuperType()),子类实例通过原型链向上查找父类的属性和方法,实现继承。

代码示例

javascript

运行

// 父类
function Animal(name) {
  this.name = name; // 实例属性
  this.colors = ['black', 'white']; // 引用类型实例属性
}
Animal.prototype.sayName = function() { // 原型方法
  console.log('动物名称:', this.name);
};

// 子类
function Dog() {}
// 核心:将父类实例赋值给子类原型
Dog.prototype = new Animal('小狗');
Dog.prototype.constructor = Dog; // 修复构造函数指向

// 测试
const dog1 = new Dog();
const dog2 = new Dog();
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white', 'brown'](引用类型属性被共享)
dog1.sayName(); // 动物名称:小狗

优点与缺点

  • 优点:实现简单,子类可继承父类原型上的所有方法;

  • 缺点

    1. 父类的引用类型实例属性会被所有子类实例共享(一个实例修改会影响其他实例);
    2. 无法向父类构造函数传递参数(子类实例创建时,无法自定义父类实例属性)。

二、构造函数继承(借用父类构造函数)

核心原理

在子类构造函数中,通过 call()/apply() 调用父类构造函数,将父类的实例属性绑定到子类实例上,实现实例属性的继承。

代码示例

javascript

运行

// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
  this.sayName = function() {
    console.log('动物名称:', this.name);
  };
}

// 子类
function Dog(name, breed) {
  // 核心:借用父类构造函数,传递参数
  Animal.call(this, name);
  this.breed = breed; // 子类自有属性
}

// 测试
const dog1 = new Dog('旺财', '中华田园犬');
const dog2 = new Dog('小白', '萨摩耶');
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'](引用类型属性不共享)
dog1.sayName(); // 动物名称:旺财
console.log(dog1.breed); // 中华田园犬

优点与缺点

  • 优点

    1. 解决了原型链继承中引用类型属性共享的问题;
    2. 可以向父类构造函数传递参数;
  • 缺点

    1. 只能继承父类的实例属性和方法,无法继承父类原型上的方法(每个子类实例都会复制一份父类方法,浪费内存);
    2. 子类实例无法共享父类方法,违背原型链的设计初衷。

三、组合继承(原型链 + 构造函数,最常用)

核心原理

结合原型链继承和构造函数继承的优点:

  1. 原型链继承继承父类原型上的方法(实现方法共享);
  2. 构造函数继承继承父类的实例属性(避免引用类型共享,支持传参)。

代码示例

javascript

运行

// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}
Animal.prototype.sayName = function() {
  console.log('动物名称:', this.name);
};

// 子类
function Dog(name, breed) {
  // 构造函数继承:继承实例属性,传参
  Animal.call(this, name);
  this.breed = breed;
}
// 原型链继承:继承原型方法,实现方法共享
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog; // 修复构造函数指向
// 子类原型方法
Dog.prototype.sayBreed = function() {
  console.log('犬种:', this.breed);
};

// 测试
const dog1 = new Dog('旺财', '中华田园犬');
const dog2 = new Dog('小白', '萨摩耶');
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'](引用类型不共享)
dog1.sayName(); // 动物名称:旺财(继承父类原型方法)
dog1.sayBreed(); // 犬种:中华田园犬(子类自有方法)
console.log(dog1 instanceof Animal); // true( instanceof 检测正常)

优点与缺点

  • 优点

    1. 兼顾了原型链继承和构造函数继承的优点,既实现了方法共享,又避免了引用类型属性共享;
    2. 支持向父类传参,instanceof 检测正常;
  • 缺点:父类构造函数被调用了两次(一次是创建子类原型时 new Animal(),一次是子类构造函数中 Animal.call(this)),导致子类原型上存在多余的父类实例属性(虽不影响使用,但造成内存冗余)。

四、原型式继承(基于已有对象创建新对象)

核心原理

通过 Object.create()(或手动封装的原型方法),以一个已有对象为原型,创建新的对象,实现对已有对象属性和方法的继承。

代码示例

javascript

运行

// 已有对象(作为原型)
const animal = {
  name: '动物',
  colors: ['black', 'white'],
  sayName: function() {
    console.log('动物名称:', this.name);
  }
};

// 核心:用 Object.create 创建新对象,继承 animal
const dog = Object.create(animal);
dog.name = '旺财'; // 重写实例属性
dog.breed = '中华田园犬'; // 新增自有属性

// 测试
const cat = Object.create(animal);
dog.colors.push('brown');
console.log(dog.colors); // ['black', 'white', 'brown']
console.log(cat.colors); // ['black', 'white', 'brown'](引用类型属性共享)
dog.sayName(); // 动物名称:旺财
console.log(dog.breed); // 中华田园犬

优点与缺点

  • 优点:无需定义构造函数,实现简单,适合快速创建基于已有对象的新对象;

  • 缺点

    1. 引用类型属性会被所有新对象共享(与原型链继承一致);
    2. 无法向父对象传递参数,只能在创建新对象后手动修改属性。

五、寄生式继承(原型式继承的增强版)

核心原理

在原型式继承的基础上,封装一个创建对象的函数,在函数内部为新对象添加自有属性和方法,增强新对象的功能,最后返回新对象。

代码示例

javascript

运行

// 封装创建继承对象的函数(寄生函数)
function createAnimal(proto, name, breed) {
  // 原型式继承:创建新对象
  const obj = Object.create(proto);
  // 增强新对象:添加自有属性和方法
  obj.name = name;
  obj.breed = breed;
  obj.sayBreed = function() {
    console.log('犬种/品种:', this.breed);
  };
  return obj;
}

// 原型对象
const animal = {
  colors: ['black', 'white'],
  sayName: function() {
    console.log('名称:', this.name);
  }
};

// 测试
const dog = createAnimal(animal, '旺财', '中华田园犬');
const cat = createAnimal(animal, '咪咪', '橘猫');
dog.colors.push('brown');
console.log(dog.colors); // ['black', 'white', 'brown']
console.log(cat.colors); // ['black', 'white', 'brown'](引用类型共享)
dog.sayName(); // 名称:旺财
dog.sayBreed(); // 犬种/品种:中华田园犬

优点与缺点

  • 优点:无需定义构造函数,可灵活增强新对象的功能,实现简单;

  • 缺点

    1. 引用类型属性共享问题依然存在;
    2. 每个新对象的自有方法都是独立的(无法共享),浪费内存;
    3. 无法实现方法的复用,类似构造函数继承的缺点。

六、寄生组合式继承(完美继承方案)

核心原理

结合组合继承和寄生式继承的优点,解决组合继承中父类构造函数被调用两次的问题:

  1. 寄生式继承继承父类的原型(仅继承原型方法,不调用父类构造函数);
  2. 构造函数继承继承父类的实例属性(支持传参,避免引用类型共享)。

代码示例

javascript

运行

// 寄生函数:继承父类原型,不调用父类构造函数
function inheritPrototype(SubType, SuperType) {
  // 创建父类原型的副本(避免直接修改父类原型)
  const prototype = Object.create(SuperType.prototype);
  prototype.constructor = SubType; // 修复构造函数指向
  SubType.prototype = prototype; // 将副本赋值给子类原型
}

// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}
Animal.prototype.sayName = function() {
  console.log('名称:', this.name);
};

// 子类
function Dog(name, breed) {
  // 构造函数继承:继承实例属性,传参(仅调用一次父类构造函数)
  Animal.call(this, name);
  this.breed = breed;
}

// 核心:寄生式继承父类原型
inheritPrototype(Dog, Animal);

// 子类原型方法
Dog.prototype.sayBreed = function() {
  console.log('犬种:', this.breed);
};

// 测试
const dog1 = new Dog('旺财', '中华田园犬');
const dog2 = new Dog('小白', '萨摩耶');
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'](引用类型不共享)
dog1.sayName(); // 名称:旺财
dog1.sayBreed(); // 犬种:中华田园犬
console.log(dog1 instanceof Animal); // true
console.log(Dog.prototype.constructor === Dog); // true(构造函数指向正确)

优点与缺点

  • 优点

    1. 父类构造函数仅被调用一次,避免了内存冗余;
    2. 实现了方法共享,避免了引用类型属性共享;
    3. 支持向父类传参,instanceof 检测和构造函数指向均正常;
    4. 是 JavaScript 继承的 “完美方案”,ES6 class extends 底层基于此实现。
  • 缺点:实现相对复杂(需封装寄生函数),但可复用该函数。

七、ES6 Class 继承(语法糖)

核心原理

通过 class 定义类,extends 关键字实现继承,super() 调用父类构造函数,底层仍是寄生组合式继承,只是语法更简洁、更接近传统类继承。

代码示例

javascript

运行

// 父类
class Animal {
  constructor(name) {
    this.name = name;
    this.colors = ['black', 'white'];
  }

  sayName() {
    console.log('名称:', this.name);
  }
}

// 子类:extends 实现继承
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 必须先调用 super(),才能使用 this
    this.breed = breed;
  }

  sayBreed() {
    console.log('犬种:', this.breed);
  }
}

// 测试
const dog1 = new Dog('旺财', '中华田园犬');
const dog2 = new Dog('小白', '萨摩耶');
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'](引用类型不共享)
dog1.sayName(); // 名称:旺财
dog1.sayBreed(); // 犬种:中华田园犬
console.log(dog1 instanceof Animal); // true

优点与缺点

  • 优点:语法简洁直观,符合面向对象编程习惯,易于理解和维护,支持静态方法继承(static 关键字);
  • 缺点:本质是语法糖,底层仍依赖原型链,新手可能忽略原型继承的本质。

八、各类继承方式对比与选型建议

继承方式 核心优点 核心缺点 适用场景
原型链继承 实现简单,方法共享 引用类型共享,无法传参 简单场景,无需传参,不关心引用类型共享
构造函数继承 支持传参,引用类型不共享 无法继承原型方法,方法冗余 仅需继承实例属性,无需共享方法
组合继承 方法共享,支持传参,功能完善 父类构造函数调用两次 常规业务场景,兼容性要求高
原型式继承 无需构造函数,快速创建对象 引用类型共享,无法传参 基于已有对象快速创建新对象
寄生式继承 灵活增强对象功能 方法冗余,引用类型共享 快速创建并增强新对象,简单场景
寄生组合式继承 完美解决所有缺陷,性能最优 实现复杂 追求性能和严谨性的场景,框架开发
ES6 Class 继承 语法简洁,符合 OOP 习惯 底层仍是原型继承 现代项目开发,兼容性良好(ES6+)

总结

  1. 原型链是 JavaScript 继承的基础,所有继承方式均围绕原型链展开;
  2. 寄生组合式继承是 “完美方案”,ES6 class extends 是其语法糖,推荐现代项目优先使用;
  3. 简单场景可使用原型式 / 寄生式继承,兼容旧环境可使用组合继承,仅需实例属性继承可使用构造函数继承。
❌
❌