源本科技 | 码上会

JavaScript Prototype

2026/01/13
25
0

学习目标

  • 理解 JavaScript 原型机制的核心原理

  • 掌握 prototype[[Prototype]] 的区别与联系

  • 学会通过原型实现方法共享与继承

  • 能够扩展内置对象(如 Array、String)的功能

  • 理解原型链的查找机制及其在内存优化中的作用


什么是原型 Prototype

在 JavaScript 中,一切皆对象,包括函数、数组、字符串等。JavaScript 采用基于原型的继承模型,而非传统面向对象语言中的类继承。

原型可以理解为“模具”:所有由该模具制造出的实例,都共享模具上的属性和方法。

// 向 Object 的原型添加方法
Object.prototype.sayHi = function() {
    console.log("你好,来自对象原型!");
};

const user = { name: "张三" };
user.sayHi(); // 输出:你好,来自对象原型!

关键点

  • 每个对象都有一个内部属性 [[Prototype]](可通过 __proto__Object.getPrototypeOf() 访问)

  • 函数对象额外拥有一个可写的 prototype 属性,用于定义其构造实例的原型

  • 当访问对象属性时,若自身没有,则沿原型链向上查找


原型的应用场景

1. 扩展内置对象

谨慎使用

为数组添加实用方法:

// 添加获取首个元素的方法
Array.prototype.first = function() {
    return this.length > 0 ? this[0] : undefined;
};

// 添加求和方法
Array.prototype.sum = function() {
    let total = 0;
    for (let i = 0; i < this.length; i++) {
        total += this[i];
    }
    return total;
};

const scores = [85, 92, 78, 96];
console.log(scores.first()); // 85
console.log(scores.sum());   // 351

注意:扩展内置原型可能引发命名冲突或影响第三方库,生产环境中应谨慎使用,或使用 Symbol 避免污染。

2. 性能优化:共享方法

对比两种定义方法的方式:

// 不推荐:每次创建实例都新建函数
function BadPerson(name) {
    this.name = name;
    this.greet = function() {
        return `你好,${this.name}`;
    };
}

// 推荐:方法定义在原型上
function GoodPerson(name) {
    this.name = name;
}
GoodPerson.prototype.greet = function() {
    return `你好,${this.name}`;
};

const p1 = new BadPerson("小明");
const p2 = new GoodPerson("小红");

// p1.greet 和 p2.greet 是不同函数(BadPerson)
// p2.greet 与其他 GoodPerson 实例共享同一函数

3. 动态方法赋值

实例级扩展

function User(name) {
    this.name = name;
}

const user1 = new User("王五");
const user2 = new User("赵六");

// 仅为 user1 添加专属方法
user1.debugInfo = function() {
    return `调试信息:用户 ${this.name}`;
};

console.log(user1.debugInfo()); // 调试信息:用户 王五
console.log(user2.debugInfo);   // undefined(未定义)

适用于需要个别实例特殊行为的场景。


通过原型实现继承

JavaScript 使用原型链实现继承:

// 父类
function Animal(species) {
    this.species = species;
}
Animal.prototype.describe = function() {
    return `这是一只 ${this.species}`;
};

// 子类
function Bird(name, color) {
    // 调用父类构造函数,初始化继承属性
    Animal.call(this, "鸟");
    this.name = name;
    this.color = color;
}

// 设置原型继承
Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird; // 修复 constructor 指向

// 子类特有方法
Bird.prototype.fly = function() {
    return `${this.name} 正在飞翔!`;
};

// 重写父类方法
Bird.prototype.describe = function() {
    return `${this.name} 是一只 ${this.color} 的 ${this.species}`;
};

const parrot = new Bird("小绿", "绿色");
console.log(parrot.describe()); // 小绿 是一只 绿色 的 鸟
console.log(parrot.fly());      // 小绿 正在飞翔!

关键步骤:

  1. 使用 Parent.call(this, ...) 调用父构造函数

  2. Object.create(Parent.prototype) 建立原型链

  3. 修正 constructor 指向


原型链的全局结构

JavaScript 中所有对象最终都继承自 Object.prototype

Object.prototype.universalMethod = function() {
    console.log("我是万物皆可调用的方法");
};

const obj = { x: 1 };
const arr = [1, 2, 3];
const str = "hello";
function fn() {}

obj.universalMethod();   // ✓
arr.universalMethod();   // ✓
str.universalMethod();   // ✓
fn.universalMethod();    // ✓

这是因为:

  • 数组 → Array.prototypeObject.prototype

  • 字符串 → String.prototypeObject.prototype

  • 函数 → Function.prototypeObject.prototype

整个结构形成一棵以 Object.prototype 为根的树状原型链


原型的优势

优势

说明

内存高效

方法仅存储一份,所有实例共享

支持继承

通过原型链实现对象间属性 / 方法继承

动态扩展

可在运行时修改原型,影响所有现有及未来实例

代码复用

公共逻辑集中管理,避免重复定义

性能优化

减少对象创建时的内存分配开销


重点总结

  • 每个函数都有 prototype 属性,每个对象都有 [[Prototype]] 内部链接

  • 实例通过 __proto__ 指向其构造函数的 prototype

  • 属性查找沿原型链向上进行,直到 null

  • 方法应优先定义在原型上以节省内存

  • 使用 Object.create(Parent.prototype) 实现安全的原型继承

  • 扩展内置原型需谨慎,避免全局污染

  • 箭头函数没有 prototype 属性,也不能作为构造函数使用


思考题

  1. 为什么 Function.prototype[[Prototype]] 指向 Object.prototype,而 Object 本身又是 Function 的实例?这是否构成循环依赖?请解释 JavaScript 引擎如何处理这一关系。

  2. 编写一个 MyArray 构造函数,使其继承自原生 Array,并添加 average() 方法计算平均值。要求新数组实例仍能使用 pushpop 等原生方法。

  3. 如果同时向 Array.prototype 和某个具体数组实例添加同名方法(如 customMethod),调用时会执行哪一个?为什么?请结合原型链查找规则说明。