JavaScript原型与原型链:深入解析与最佳实践

520 阅读5分钟

JavaScript原型与原型链:深入解析与最佳实践

一、原型系统基础:JavaScript的继承机制

1.1 原型的概念

在JavaScript中,每个对象都有一个原型对象(prototype),它是一个引用对象,用于实现属性和方法的继承。当访问对象的属性时,如果对象本身没有该属性,JavaScript引擎会沿着原型链向上查找。

// 创建对象
const animal = {
  eats: true
};

const rabbit = {
  jumps: true
};

// 设置rabbit的原型为animal
Object.setPrototypeOf(rabbit, animal);

console.log(rabbit.jumps); // true (自身属性)
console.log(rabbit.eats);  // true (继承自原型)
1. 原型对象(prototype)

每个 JavaScript 函数都有一个特殊的 prototype 属性,它指向一个对象,这个对象称为原型对象

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

// 原型对象
console.log(Person.prototype); 
// 输出: {constructor: ƒ Person(name)}
2. 实例的 __proto__

当使用构造函数创建实例时,实例内部会包含一个 __proto__ 属性,指向构造函数的原型对象。

const alice = new Person('Alice');
console.log(alice.__proto__ === Person.prototype); // true

1.2 构造函数与原型的关系

每个函数都有一个prototype属性(箭头函数除外),它指向一个对象,该对象将作为通过new调用该函数创建实例的原型。

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

// 在原型上添加方法
Person.prototype.sayHello = function() {
  console.log(`你好,我是${this.name}`);
};

const john = new Person('john');
john.sayHello(); // 你好,我是john

1.3 原型链图示

1750675664308.png

二、原型链机制详解

2.1 属性查找过程

当访问对象属性时,JavaScript引擎:

  1. 检查对象自身属性
  2. 如果找不到,沿着 __proto__ 到原型对象上查找
  3. 如果还没找到,沿原型链向上查找
  4. 直到找到属性或到达原型链末端(null)
const grandparent = { a: 1 };
const parent = { b: 2 };
const child = { c: 3 };

Object.setPrototypeOf(parent, grandparent);
Object.setPrototypeOf(child, parent);

console.log(child.c); // 3 (自身属性)
console.log(child.b); // 2 (父级属性)
console.log(child.a); // 1 (祖父级属性)
console.log(child.d); // undefined (未找到)

2.2 原型链的顶点

所有原型链的终点都是Object.prototype,其原型是null

console.log(Object.getPrototypeOf(Object.prototype)); // null

2.3 检测原型关系

  • obj instanceof Constructor:检查Constructor.prototype是否在obj的原型链上
  • Constructor.prototype.isPrototypeOf(obj):同上,更安全
  • Object.getPrototypeOf(obj):获取对象的原型
  • Object.create(proto,[propertiesObject]) : 是 JavaScript 中用于创建新对象的重要方法,它允许你明确指定新对象的原型。proto:新创建对象的原型对象(必须);propertiesObject(可选):要添加到新对象的属性描述符(类似于 Object.defineProperties 的第二个参数)
function Animal() {}
function Dog() {}

//Object.create() 是 JavaScript 中用于创建新对象的重要方法,它允许你明确指定新对象的原型。
Dog.prototype = Object.create(Animal.prototype);
const myDog = new Dog();

console.log(myDog instanceof Dog);     // true
console.log(myDog instanceof Animal);  // true
console.log(Animal.prototype.isPrototypeOf(myDog)); // true

三、构造函数与new操作符

3.1 new操作符的工作流程

当使用new调用函数时:

  1. 创建一个新对象
  2. 将新对象的[[Prototype]]指向构造函数的prototype
  3. this绑定到新对象
  4. 执行构造函数体
  5. 如果构造函数未返回对象,则返回新对象
// 模拟new操作符
function myNew(constructor, ...args) {
  // 1. 创建新对象,链接原型
  const obj = Object.create(constructor.prototype);
  
  // 2. 绑定this并执行构造函数
  const result = constructor.apply(obj, args);
  
  // 3. 如果构造函数返回对象则返回该对象,否则返回新对象
  return result instanceof Object ? result : obj;
}

3.2 构造函数 vs 普通函数

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

// 作为构造函数
const user = new User('Alice'); // 正确:创建实例
console.log(user);  // {name:'Alice',[[Prototype]]:Object}

// 作为普通函数
User('Bob'); // 危险:this指向全局(非严格模式)
console.log(window.name); // 'Bob' (浏览器环境)

四、继承的实现模式

4.1 原型链继承

// 父类
function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};

// 子类
function Dog(name, breed) {
  Animal.call(this, name); // 继承属性
  this.breed = breed;
}

// 设置原型链 (关键步骤)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复构造函数指向

// 添加子类方法
Dog.prototype.bark = function() {
  console.log('Woof!');
};

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.eat();  // "Buddy is eating" (继承自Animal)
myDog.bark(); // "Woof!" (自身方法)

使用 Object.setPrototypeOf 实现原型链继承

// 父类
function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};

// 子类
function Dog(name, breed) {
  Animal.call(this, name); // 继承属性
  this.breed = breed;
}

// 传统方式: Dog.prototype = Object.create(Animal.prototype);
// 现代方式: 使用 Object.setPrototypeOf 设置原型链
Object.setPrototypeOf(Dog.prototype, Animal.prototype);

// 添加子类方法
Dog.prototype.bark = function() {
  console.log('Woof!');
};

// 验证原型链
const myDog = new Dog('Buddy', 'Golden Retriever');

// 测试继承
myDog.eat();  // "Buddy is eating" (继承自Animal)
myDog.bark(); // "Woof!" (自身方法)

// 检查原型链
console.log(Object.getPrototypeOf(myDog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // true

// 由于我们直接修改了原型,不需要额外修复constructor
console.log(Dog.prototype.constructor === Dog); // 仍然为true

4.2 ES6类继承(语法糖)

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name} eats`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父构造函数
    this.breed = breed;
  }
  
  bark() {
    console.log(`${this.name} barks`);
  }
}

const myDog = new Dog('Rex', 'Labrador');
myDog.eat();  // Rex eats
myDog.bark(); // Rex barks

4.3 原型链 vs 类继承

特性原型链继承类继承 (ES6)
实现方式原型对象链接class/extends 语法
构造函数需要手动链接super() 自动处理
方法定义在 prototype 上定义类内部直接定义
静态方法构造函数上定义属性static 关键字
私有字段闭包实现# 语法 (ES2022)
本质基于对象的继承基于类的语法糖

五、原型相关API详解

5.1 Object.create:纯净的原型创建

const proto = { value: 42 };
const obj = Object.create(proto);

console.log(obj.value); // 42
console.log(Object.getPrototypeOf(obj) === proto); // true

5.2 Object.getPrototypeOf:获取原型

// 传统方式: Dog.prototype = Object.create(Animal.prototype);
// 现代方式:const obj1 = { a: 1 };
const obj2 = Object.getPrototypeOf(obj1);

console.log(obj1.prototype === obj2); // false
console.log(obj1.__proto__ === obj2); // true

// 父类
function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};

// 子类
function Dog(name, breed) {
  Animal.call(this, name); // 继承属性
  this.breed = breed;
}

// 传统方式: Dog.prototype = Object.create(Animal.prototype);
// 现代方式: 使用 Object.setPrototypeOf 设置原型链
Object.setPrototypeOf(Dog.prototype, Animal.prototype);

// 添加子类方法
Dog.prototype.bark = function() {
  console.log('Woof!');
};

// 验证原型链
const myDog = new Dog('Buddy', 'Golden Retriever');

// 测试继承
myDog.eat();  // "Buddy is eating" (继承自Animal)
myDog.bark(); // "Woof!" (自身方法)

// 检查原型链
console.log(Object.getPrototypeOf(myDog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // true

// 由于我们直接修改了原型,不需要额外修复constructor
console.log(Dog.prototype.constructor === Dog); // 仍然为true

5.2 Object.setPrototypeOf:动态修改原型

const obj1 = { a: 1 };
const obj2 = { b: 2 };

// 将obj1的原型设置为obj2
Object.setPrototypeOf(obj1, obj2);
console.log(obj1.b); // 2

性能警告:修改已有对象的原型严重影响性能,应在对象创建时确定原型

5.3 __proto__:历史遗留属性

const obj = {};
const proto = { value: 42 };

obj.__proto__ = proto; // 不推荐
console.log(obj.value); // 42

现代替代方案

  • 使用Object.create创建时指定原型
  • 使用Object.getPrototypeOfObject.setPrototypeOf

六、原型链的应用场景

6.1 原型方法扩展

// 扩展数组方法
Array.prototype.last = function() {
  return this[this.length - 1];
};

console.log([1, 2, 3].last()); // 3

// 注意:避免覆盖已有方法

6.2 对象混合(Mixin)

const canEat = {
  eat() {
    console.log(`${this.name} eats`);
  }
};

const canSleep = {
  sleep() {
    console.log(`${this.name} sleeps`);
  }
};

class Animal {
  constructor(name) {
    this.name = name;
  }
}

// 将Mixin方法添加到原型
Object.assign(Animal.prototype, canEat, canSleep);

const dog = new Animal('Rex');
dog.eat();   // Rex eats
dog.sleep(); // Rex sleeps

七、原型链的性能优化

7.1 原型链过长的影响

// 创建深度原型链
let current = {};
for (let i = 0; i < 100; i++) {
  const newObj = {};
  Object.setPrototypeOf(newObj, current);
  current = newObj;
}

// 查找不存在的属性
console.time('原型链查找');
current.someProperty; // 遍历整个原型链
console.timeEnd('原型链查找'); // 耗时明显增加

优化建议

  • 保持原型链扁平(不超过3层)
  • 优先使用组合模式而非深度继承

7.2 对象自身属性检查

// 不推荐:会检查原型链
if (obj.property) { ... }

// 推荐:只检查自身属性
if (obj.hasOwnProperty('property')) { ... }

// 或使用现代语法
if (Object.hasOwn(obj, 'property')) { ... }

八、最佳实践与陷阱规避

8.1 原型使用的黄金法则

  1. 避免修改内置原型:可能导致冲突和兼容性问题
  2. 构造函数首字母大写:提醒使用new调用
  3. 优先使用组合而非继承:降低代码耦合度
  4. 使用现代类语法:更清晰的继承实现

8.2 常见陷阱与解决方案

陷阱1:忘记修复constructor

function Parent() {}
function Child() {}

Child.prototype = Object.create(Parent.prototype);
console.log(Child.prototype.constructor); // Parent (错误)

// 修复
Child.prototype.constructor = Child;

陷阱2:引用类型共享

function BadArray() {}
BadArray.prototype.items = [];

const arr1 = new BadArray();
arr1.items.push(1);

const arr2 = new BadArray();
console.log(arr2.items); // [1] (共享了数组)

// 解决方案:在构造函数中初始化
function GoodArray() {
  this.items = []; // 每个实例独立
}

九、现代JavaScript中的原型

9.1 类语法底层原理

ES6类语法是原型继承的语法糖:

class Person {
  constructor(name) {
    this.name = name;
  }
  
  sayHello() {
    console.log(`Hello, ${this.name}`);
  }
}

// 等价于
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, ${this.name}`);
};

9.2 私有字段与方法

现代JavaScript支持真正的私有成员:

class Counter {
  #count = 0; // 私有字段
  
  increment() {
    this.#count++;
  }
  
  get count() {
    return this.#count;
  }
}

const counter = new Counter();
counter.increment();
console.log(counter.count); // 1
console.log(counter.count()); // 报错,因为count使用了get关键字
console.log(counter.#count); // 语法错误

十、深入理解:原型链与JavaScript引擎

10.1 隐藏类(Hidden Classes)

现代JavaScript引擎(如V8)使用隐藏类优化属性访问:

  • 相同结构的对象共享隐藏类
  • 动态添加属性会创建新的隐藏类
  • 影响性能的操作:
    • 动态添加/删除属性
    • 修改对象原型

10.2 原型链优化建议

  1. 初始化时完成属性定义
  2. 避免运行时改变对象结构
  3. 使用相同顺序初始化属性
  4. 避免修改对象原型

总结:掌握原型链的核心要点

  1. 原型是JavaScript的继承机制:通过[[Prototype]]链实现

  2. 构造函数与原型分离:函数有prototype,实例有[[Prototype]]

    1. 原型(prototype):每个函数都有的属性,指向原型对象

    2. 原型链:对象通过 __proto__ 连接形成的链式结构

    3. 重要关系

    • 实例.__proto__ === 构造函数.prototype
    • 构造函数.prototype.constructor === 构造函数
  3. 原型链的终点是nullObject.prototype是最顶层原型

  4. 现代替代方案:优先使用classObject.create

  5. 性能至关重要:避免深度原型链和动态修改

"原型是JavaScript的灵魂。理解它,你就能真正掌握这门语言。" —— Douglas Crockford

学习资源推荐