普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月2日首页

JavaScript 继承的进阶之路:从原型链到圣杯模式的架构思考

作者 NEXT06
2026年2月1日 23:34

在面向对象编程的设计哲学中,继承的本质是为了解决两个核心问题:数据的独立性与行为的共享性。对于 JavaScript 这种基于原型的动态语言而言,实现继承的过程,实际上就是不断在“构造函数”与“原型链”之间寻找平衡点的过程。

本文将基于底层原理,剖析从基础的构造函数借用到成熟的圣杯模式(寄生组合式继承)的演进逻辑,揭示其背后的架构思考。

一、 引言:属性与方法的二元对立

JavaScript 的对象包含属性(State)和方法(Behavior)。在继承关系中,这二者有着截然不同的需求:

  1. 属性需要私有化:子类实例必须拥有独立的属性副本。例如,每一只 Cat 都应该有自己独立的 name 和 color,修改一只猫的名字不应影响另一只。
  2. 方法需要复用:父类的方法(如 species 属性或公共函数)应当存在于内存的某一处,供所有子类实例引用,而非在每个实例中重复创建。

为了解决这一矛盾,JavaScript 引入了 call/apply 来处理属性拷贝,利用 prototype 来处理方法复用。

二、 构造函数的借用:属性的物理拷贝

在最早期的继承尝试中,我们首先解决的是属性继承的问题。通过在子类构造函数中强行执行父类构造函数,我们可以“窃取”父类的属性初始化逻辑。

JavaScript

function Animal(name, age) {
    this.name = name;
    this.age = age;
}
Animal.prototype.species = '动物';

function Cat(name, age, color) {
    // 核心逻辑:构造函数借用
    // 将 Animal 的 this 指向当前的 Cat 实例
    Animal.apply(this, [name, age]); 
    this.color = color;
    console.log(this, '////');
}

架构分析

Animal.apply(this, [name, age]) 的底层逻辑在于,它将 Animal 当作一个普通函数执行,并将执行上下文(Context)强制绑定到当前正在创建的 Cat 实例上。这实际上是一次物理拷贝——父类中定义的 this.name 和 this.age 被直接赋值到了子类实例上。

致命缺陷

这种模式仅解决了“属性私有化”,却完全丢失了“行为复用”。
由于 Cat 的原型链并未指向 Animal 的原型,因此定义在 Animal.prototype 上的 species 属性和任何共有方法,对于 Cat 实例来说都是不可见的。

image.png

三、 原型链的连接:简单粗暴的代价

为了让子类能访问父类原型上的方法,最直观的做法是将子类的原型对象指向父类的一个实例。这也是早期很多教程中的标准写法:

JavaScript

// 组合继承的雏形
Cat.prototype = new Animal(); 
Cat.prototype.constructor = Cat;

架构思考与缺陷

这行代码虽然打通了原型链(cat.proto 指向了 Animal 实例,而该实例的 proto 指向 Animal.prototype),但它引入了严重的副作用,这种副作用在大型应用中是不可接受的:

  1. 父类构造函数执行了两次

    • 第一次:new Animal() 赋值给原型时。
    • 第二次:Cat 实例化时内部调用的 Animal.apply。
    • 如果 Animal 初始化逻辑中包含昂贵的操作(如 DOM 绑定、大量计算),这种双重开销是极大的浪费。
  2. 属性冗余与内存污染

    • Cat.prototype 是 Animal 的一个实例,因此它不可避免地拥有了 name 和 age 属性(虽然是 undefined)。
    • 同时,Cat 实例本身通过 apply 也拥有了 name 和 age。
    • 实例属性遮蔽了原型上的同名属性,原型上的这些属性不仅毫无意义,还占用了内存空间。

四、 完美的中间层:圣杯模式(寄生组合式继承)

如何既能继承 Animal.prototype,又不执行 Animal 构造函数从而避免副作用?
答案是引入一个纯净的中间层。这就是所谓的“圣杯模式”或“寄生组合式继承”。

JavaScript

function extend(Child, Parent) {
    // 1. 创建中介函数 F
    var F = function() {}; 
    
    // 2. 将中介的原型指向父类原型
    F.prototype = Parent.prototype;
    
    // 3. 子类原型指向中介的实例
    Child.prototype = new F(); 
    
    // 4. 修正构造函数指针
    Child.prototype.constructor = Child;
    
    // 5. 可选:保存父类原型的引用(Uber/Super)
    Child.prototype.uber = Parent.prototype;
}

核心解构:为何引入空对象 F?

F 在这里充当了一个缓冲带(Buffer)代理(Proxy)的角色。

  1. 性能无损:F 是一个空函数,执行 new F() 几乎不消耗任何 CPU 资源,也不会产生任何多余的实例属性(内存纯净)。
  2. 链条维持:new F() 产生的对象,其 proto 依然指向 F.prototype(即 Parent.prototype)。因此,原型链依然是通畅的:
    Cat实例 -> F实例(空) -> Animal.prototype -> Object.prototype
  3. 隔离副作用:我们成功绕过了 new Animal(),从而避免了父类构造函数的执行。

关于 Constructor 的修正

重写 Child.prototype 会导致 constructor 属性丢失(或指向 Parent)。虽然这对 JS 引擎的运行影响不大,但为了保持原型链的完整性和可追溯性,手动修正 Child.prototype.constructor = Child 是架构设计中的必要规范。

image.png

五、 封装与现代视角

将上述逻辑封装后,我们得到了一个通用的继承辅助函数。在现代 JavaScript 开发中,这一模式极其重要。

JavaScript

function extend(Parent, Child) {
  var F = function() {}; 
  F.prototype = Parent.prototype;
  Child.prototype = new F(); 
  Child.prototype.constructor = Child;
}

extend(Animal, Cat);
const cat = new Cat('加菲猫', 2, '橘色');

ES6 Class 的本质

ES6 引入的 class extends 语法,本质上就是上述“圣杯模式”的语法糖。
在 ES5 中我们手动创建的 F 实例,在规范层面对应了 Object.create(Parent.prototype)。

JavaScript

// 现代写法的等价逻辑
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Object.create 内部的 Polyfill 实现,正是利用了临时的空构造函数来创建一个新对象并关联原型,这与我们手动编写的 F 异曲同工。

六、 总结

JavaScript 的继承机制并非简单的“复制粘贴”,而是一场关于内存管理与引用关系的博弈。

从直接修改原型链导致的副作用,到引入空对象 F 作为隔离层,圣杯模式的核心价值在于:它在保持原型链引用(实现方法复用)的同时,彻底切断了与父类构造函数实体的直接耦合(实现状态解耦与性能优化)。

理解这一模式,不仅能让你掌握 JavaScript 继承的终极方案,更能深刻理解动态语言中“原型”这一概念的灵活性与本质。

深度拆解 Chrome:从多进程架构到站点隔离的演进之路

作者 NEXT06
2026年2月1日 23:10

浏览器早已不再仅仅是一个用于展示网页的应用程序。当我们点击 Chrome 图标的那一刻,实际上是启动了一个小型的、高度复杂的分布式操作系统。

对于前端工程师而言,理解浏览器的底层架构不仅是面试中的加分项,更是编写高性能、高稳定性代码的基石。本文将结合计算机底层原理,深入剖析 Chrome 如何从早期的单进程模型演进为如今的服务化、站点隔离架构,探讨其中的架构权衡(Trade-off)。

一、 基石:硬件与操作系统

要理解软件架构,必须先理解其运行的物理环境。浏览器的核心工作依赖于计算机的两大计算单元:CPU 和 GPU。

1.1 CPU 与 GPU 的协同

  • CPU (Central Processing Unit) :计算机的大脑。核心数量少(通常 4-8 核),但每个核心非常强大,擅长处理复杂的串行任务、逻辑判断和系统调度。
  • GPU (Graphics Processing Unit) :计算机的图形处理单元。拥有成百上千个简单的核心,擅长并行处理简单的重复任务。最初用于图形渲染,现在也广泛应用于计算加速。

在浏览器中,CPU 负责构建 DOM 树、计算样式、执行 JavaScript;而 GPU 则负责将这些内容光栅化并合成到屏幕上,实现流畅的动画与滚动。

1.2 进程 (Process) 与线程 (Thread)

操作系统通过进程和线程来管理程序的运行,这是理解浏览器架构的关键。

  • 进程 (Process) :资源分配的最小单位。

    • 操作系统为每个进程分配独立的内存空间(堆栈)。
    • 隔离性:一个进程崩溃通常不会影响其他进程。
    • 通信:进程间的数据是无法直接访问的,必须通过 IPC (Inter Process Communication) 机制进行通信。
  • 线程 (Thread) :程序执行的最小单位。

    • 依附于进程存在,一个进程可以包含多个线程。
    • 共享性:同一进程内的线程共享进程的内存资源。
    • 风险:一个线程崩溃(如出现未捕获的异常导致分段错误),往往会导致整个进程崩溃。

屏幕截图 2026-02-01 230057.png

二、 演进:从单进程到多进程架构

2.1 单进程时代的痛点

在早期的浏览器(如旧版 IE)中,整个浏览器应用运行在一个进程中。网络请求、JS 执行、页面渲染、插件运行都在同一个线程或进程空间内。这就导致了著名的“三个一”问题:

  1. 不稳定:一个插件或标签页崩溃,导致整个浏览器闪退。
  2. 不流畅:JS 代码的死循环或繁重的渲染任务会阻塞 UI 线程,导致浏览器界面无法响应用户操作。
  3. 不安全:由于内存共享,恶意的 JavaScript 代码可能窥探到其他页面的数据。

2.2 Chrome 的多进程变革

Chrome 的发布重新定义了浏览器架构。其核心理念是:将浏览器的不同功能模块拆分为独立的进程,通过 IPC 协作。

当我们打开一个 Chrome 标签页时,后台实际上启动了多个进程:

  1. 浏览器主进程 (Browser Process)

    • 职责:浏览器的“总管”。负责地址栏、书签栏、前进后退按钮等 UI 显示;负责管理各个子进程的创建与销毁;负责文件访问与高层级的网络协调。
    • 数量:通常仅有一个。
  2. 渲染进程 (Renderer Process)

    • 职责:核心工作区。负责将 HTML/CSS/JS 转换为用户可见的页面。
    • 核心组件:Blink 排版引擎、V8 JavaScript 引擎。
    • 特性:运行在沙箱 (Sandbox)  模式下,被限制访问文件系统和操作系统任意功能,以保证安全。
    • 数量:默认策略下,每个标签页开启一个独立的渲染进程。
  3. GPU 进程 (GPU Process)

    • 职责:负责 CSS 3D 变换、Canvas 绘制以及页面的合成(Compositing)。
    • 独立原因:GPU 驱动程序通常由不同厂商提供,不够稳定。将 GPU 操作隔离在独立进程中,即使驱动崩溃,浏览器主进程也能恢复,而不会导致软件完全瘫痪。
  4. 网络进程 (Network Process)

    • 职责:负责页面的网络资源加载(DNS 查询、TCP 连接、HTTP 解析)。
    • 演进:早期是浏览器主进程中的一个线程,现已独立为进程,防止网络解析库的漏洞影响主进程。
  5. 插件进程 (Plugin Process)

    • 职责:负责 Flash 等插件的运行(现已逐渐淘汰),确保插件崩溃不影响页面。

image.png

三、 进阶:架构的动态性与安全性

Chrome 的架构并非一成不变,而是根据设备性能和安全需求进行动态调整。

3.1 服务化 (Service-fication)

为了适应不同性能的设备,Chrome 引入了面向服务的架构理念。

  • 高性能设备(Desktop) :采取拆分策略。将网络服务、存储服务、设备服务等拆分为完全独立的进程。虽然增加了进程间通信的开销和内存占用,但极大提升了稳定性。
  • 低性能设备(Android Go/Low-end phones) :采取合并策略。将网络、GPU 等服务合并回浏览器主进程中。虽然牺牲了部分隔离性,但显著降低了内存占用(Memory Footprint)。

这种架构的灵活性,使得 Chrome 能够覆盖从高性能工作站到入门级手机的广泛设备。

3.2 站点隔离 (Site Isolation)

这是 Chrome 架构史上为了防御 CPU 级别漏洞(Spectre 和 Meltdown)而进行的最重大升级。

  • 背景:在旧的架构中(Process-per-tab),如果一个标签页 a.com 通过 iframe 嵌入了 b.com,这两个网页通常共享同一个渲染进程。虽然有同源策略(SOP)限制,但在 Spectre 漏洞面前,共享进程内存意味着恶意网页可以读取同一进程内其他网页的敏感数据。

  • 变革Process-per-site-instance。Chrome 现在强制为每个跨站点的 iframe 分配独立的渲染进程。

  • 代价

    1. 内存激增:每个 iframe 都是一个独立的进程,基础库副本增多。
    2. 复杂性:即便是简单的“Ctrl+F”页面查找,现在也需要跨越多个进程进行通信和搜索聚合。开发者工具(DevTools)也必须重构以支持跨进程调试。

image.png

四、 深度思考:性能与资源的博弈

作为开发者,理解架构不仅是为了通过面试,更为了理解代码运行的代价。

4.1 内存 vs 稳定/安全

Chrome 被戏称为“内存怪兽”,这并非技术缺陷,而是有意的架构选择(Architecture Choice)。

  • 以空间换时间/稳定性:多进程意味着每个进程都要加载一份公共的基础设施(如 V8 引擎实例、Node 绑定等)。这带来了额外的内存开销。
  • IPC 的代价:进程间通信比线程间通信慢。架构师必须在“隔离带来的安全稳定”和“通信带来的延迟”之间寻找平衡点。

4.2 开发者的应对之道

在“站点隔离”开启的今天,一个 Tab 页可能对应着 4-5 个渲染进程(如果包含多个跨域 iframe)。

  • 内存泄漏更可怕:单个页面的内存泄漏现在可能导致操作系统级别的卡顿,因为占用的物理内存更多了。
  • 主线程依然是瓶颈:尽管有多进程架构,但每个渲染进程内部的 JS 执行依然是单线程的。不要误以为多进程就能解决 while(true) 造成的页面卡死。

五、 总结

Chrome 的架构演进史,就是一部计算机系统设计的教科书。从早期的单进程一锅端,到多进程的职责分离,再到如今的服务化与站点隔离,Chrome 始终遵循着以下设计哲学:

  1. 隔离 (Isolation) :故障隔离,安全隔离。让崩溃止步于局部,让数据死守于沙箱。
  2. 并行 (Parallelism) :充分利用多核 CPU 和 GPU 的能力,将渲染、网络、计算分发处理。
  3. 适应性 (Adaptability) :通过服务化架构,在不同硬件约束下动态调整策略。

打开 Chrome,你不仅仅是打开了一个网页,你是启动了一个精密运转的现代计算集群。

❌
❌