普通视图

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

1. 《手写系列:面试官问我 new 的原理,我直接甩出三个版本》

作者 NEXT06
2026年2月2日 18:16

今天我们来聊聊 JavaScript 中那个既熟悉又神秘的 new 操作符。相信很多小伙伴在面试时都经历过这样的“名场面”:面试官微微一笑,推过来那个熟悉的键盘:“来,能不能手写一个 new 的实现?”

这时候,如果你只是背诵了代码,稍微问深一点可能就露怯了。今天,我们就把这个“黑盒”拆开,从底层原理到完美实现,彻底搞懂它!

一、核心原理拆解:new 到底干了啥?

我们在日常开发中,const person = new Person('Fog', 18) 写得飞起。但 new 背后到底发生了什么?

简单来说,new 就是一个**“生产车间”**。它拿着你的图纸(构造函数),给你造出一个实实在在的产品(实例对象)。

这个过程,标准流程只有四步(核心四步法):

  1. 建空房:创建一个全新的空对象 {}。
  2. 挂牌子:将这个空对象的原型链(proto)链接到构造函数的原型对象(prototype)上。(这步最关键,决定了你能用这一类的公共方法)。
  3. 搞装修:将构造函数内部的 this 指向这个新对象,并执行构造函数。(给对象添加属性,如 name, age)。
  4. 交钥匙:判断构造函数的返回值。如果构造函数自己返回了一个对象(或函数),那就以它为准;否则,默认返回我们在第一步创建的那个新对象。

image.png

二、面试官到底在考什么?

面试官让你手写 new,绝对不是为了看你默写代码。通过这寥寥几行代码,他在考察你以下四大内功:

  1. 原型链的理解:你知不知道实例和类是怎么关联起来的?
  2. this 指向机制:你懂不懂怎么用 call 或 apply 改变函数执行上下文?
  3. 函数参数处理:面对不定参数,你会用 arguments 还是 ...args?
  4. 边界情况处理:**这是高分点!**如果构造函数里写了 return,你的代码还能正常工作吗?

三、手写进阶之路

接下来,我们由浅入深,演示三个版本的实现。

V1.0 青铜版:ES5 经典写法

这是最基础的写法,也是很多老教材里的标准答案。我们需要处理 arguments 这个“伪数组”。

JavaScript

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.sayName = function () {
    console.log(this.name);
}

// 核心实现
function objectFactory() {
    // 1. 创建一个空对象
    var obj = new Object();
    
    // 2. 获取构造函数
    // arguments 是类数组,没有 shift 方法,我们借用数组原型的 shift
    // 这行代码有两个作用:取出第一个参数(Constructor),同时 arguments 里剩下的就是参数了
    var Constructor = [].shift.call(arguments);
    
    // 3. 链接原型:让 obj 能访问 Person.prototype 上的属性
    obj.__proto__ = Constructor.prototype;
    
    // 4. 绑定 this 并执行
    // 使用 apply 将 remaining arguments 传进去
    var result = Constructor.apply(obj, arguments);
    
    // 5. 返回值处理
    // 这是一个常见的简易判断,但其实有漏洞(稍后在王者版揭晓)
    return typeof result === 'object' && result !== null ? result : obj;
}

// 测试
var awei = objectFactory(Person, '阿伟', 20);
console.log(awei.name); // 阿伟
awei.sayName(); // 阿伟

重点解析:

  • 为什么用 [].shift.call(arguments)?
    arguments 是一个类数组对象(有 length,有索引,但没数组方法)。通过 call,我们强行让它借用了数组的 shift 方法,切掉并拿到了第一个参数(构造函数),剩下的正好传给 apply。

V2.0 黄金版:ES6 现代化写法

时代变了,我们有了更优雅的语法糖。proto 虽然好用,但在生产环境中被视为非标准(尽管浏览器支持),性能也不如 Object.create。

image.png JavaScript

// 使用 ...args 剩余参数,告别 arguments
function objectFactory(Constructor, ...args) {
    // 1. & 2. 创建对象并直接链接原型
    // Object.create(proto) 创建一个新对象,带着指定的原型,性能更好,更符合规范
    const obj = Object.create(Constructor.prototype);
    
    // 3. 执行构造函数
    const result = Constructor.apply(obj, args);
    
    // 4. 返回值处理 (依然沿用旧逻辑)
    return typeof result === 'object' && result !== null ? result : obj;
}

重点解析:

  • Object.create 的优势:它直接创建一个已经连接好原型的对象,避免了创建后再修改 proto 指针带来的性能损耗(修改原型链在 V8 引擎中是非常昂贵的操作)。

V3.0 王者版:无懈可击的最终版

注意了!如果你能写出这个版本,面试官绝对会对你刮目相看。

在 V1 和 V2 中,我们对返回值的判断是 typeof result === 'object'。这有一个巨大的隐形漏洞
如果构造函数返回的是一个 function 呢?

在 JS 原生 new 中,如果构造函数返回函数,new 表达式的结果就是那个函数。但 typeof function 是 'function' 而不是 'object',之前的代码会错误地返回 obj 实例。

JavaScript

function objectFactory(Constructor, ...args) {
    // 0. 参数校验 (严谨性加分项)
    if (typeof Constructor !== 'function') {
        throw new TypeError('Constructor must be a function');
    }

    // 1. 创建对象,链接原型
    const obj = Object.create(Constructor.prototype);
    
    // 2. 绑定 this 执行
    const result = Constructor.apply(obj, args);
    
    // 3. 完美的返回值处理(关键修正!)
    // 如果 result 是对象(非null) 或者 是函数,则返回 result
    // 否则返回新创建的 obj
    const isObject = typeof result === 'object' && result !== null;
    const isFunction = typeof result === 'function';
    
    return (isObject || isFunction) ? result : obj;
}

// 验证特殊情况
function Factory() {
    return function() { console.log('I am a function'); };
}
const test = objectFactory(Factory);
console.log(typeof test); // "function" —— 逻辑正确!

四、总结

你看,所谓的“手写源码”,其实就是对基础知识的排列组合。

  1. 创建:Object.create
  2. 执行:Function.prototype.apply
  3. 判断:类型检测与逻辑运算

掌握了这三点,new 操作符对你来说就不再是黑盒。下次面试遇到,直接展示“王者版”,告诉面试官:我不止会写,我还知道为什么要这么写。

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,你不仅仅是打开了一个网页,你是启动了一个精密运转的现代计算集群。

❌
❌