☰
深入理解JavaScript原型与原型链
2026/11/4 5:28:11 网站建设 项目流程

1. 理解JavaScript原型的本质

在JavaScript的世界里,原型(Prototype)是个既基础又核心的概念。我第一次真正理解原型是在调试一个对象属性访问问题时,console.log打印出的__proto__引起了我的注意。这就像发现了一个隐藏的通道,通往JavaScript对象系统的秘密后院。

每个JavaScript对象(除了用Object.create(null)创建的)都有一个内置的[[Prototype]]属性,在大多数浏览器中可以通过__proto__访问。这个属性指向另一个对象,我们称之为该对象的原型。当你访问一个对象的属性时,如果对象本身没有这个属性,JavaScript引擎会自动去它的原型对象上查找,这就是原型链查询的基本机制。

const animal = { eats: true }; const rabbit = { jumps: true }; rabbit.__proto__ = animal; // 设置rabbit的原型为animal console.log(rabbit.jumps); // true (来自rabbit自身) console.log(rabbit.eats); // true (来自原型animal)

警告:虽然__proto__被大多数浏览器支持,但在生产代码中应该使用Object.getPrototypeOf()和Object.setPrototypeOf()来替代。

2. 构造函数与prototype属性

JavaScript中没有类的概念(ES6的class只是语法糖),构造函数配合prototype属性实现了类似的面向对象特性。这里有个容易混淆的点:函数的prototype属性与对象的[[Prototype]]是完全不同的东西。

当你创建一个函数时,JavaScript会自动为这个函数创建一个prototype对象,这个对象有一个constructor属性指回函数本身。用这个函数作为构造函数(使用new操作符)创建实例时,实例的[[Prototype]]会指向构造函数的prototype对象。

function Person(name) { this.name = name; } // 自动创建Person.prototype = { constructor: Person } Person.prototype.sayHi = function() { console.log(`Hi, I'm ${this.name}`); }; const john = new Person('John'); john.sayHi(); // Hi, I'm John

这个模式的实际应用非常广泛。我在一个大型项目中见过这样的优化:把方法定义在prototype上而不是构造函数内部,这样所有实例共享同一个方法引用,节省了大量内存。

3. 原型链的完整工作机制

原型链是JavaScript实现继承的基础机制。当访问一个对象的属性时,JavaScript引擎会执行以下步骤:

  1. 检查对象自身是否有该属性(hasOwnProperty)
  2. 如果没有,检查对象的[[Prototype]](即原型)
  3. 如果原型上也没有,继续检查原型的原型
  4. 直到找到该属性或到达原型链顶端(Object.prototype的[[Prototype]]是null)
const grandfather = { a: 1 }; const father = { b: 2 }; const son = { c: 3 }; Object.setPrototypeOf(father, grandfather); Object.setPrototypeOf(son, father); console.log(son.a); // 1 (来自grandfather) console.log(son.b); // 2 (来自father) console.log(son.c); // 3 (来自自身)

在实际项目中,我曾遇到过原型链过长导致的性能问题。当属性查找需要遍历5-6层原型链时,会对性能产生可测量的影响。这时可以考虑用组合模式替代深层次的继承链。

4. 实现原型继承的几种方式

4.1 构造函数继承

这是最经典的继承方式,结合了构造函数和原型链:

function Animal(name) { this.name = name; } Animal.prototype.speak = function() { console.log(`${this.name} makes a noise.`); }; function Dog(name) { Animal.call(this, name); // 调用父类构造函数 } // 设置原型链 Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; // 修复constructor指向 Dog.prototype.bark = function() { console.log(`${this.name} barks.`); }; const d = new Dog('Mitzie'); d.speak(); // Mitzie makes a noise. d.bark(); // Mitzie barks.

4.2 Object.create实现纯净继承

如果你不需要构造函数初始化逻辑,可以使用更简洁的Object.create:

const animal = { init(name) { this.name = name; }, speak() { console.log(`${this.name} makes a noise.`); } }; const dog = Object.create(animal); dog.bark = function() { console.log(`${this.name} barks.`); }; const d = Object.create(dog); d.init('Mitzie'); d.speak(); // Mitzie makes a noise. d.bark(); // Mitzie barks.

4.3 ES6的class语法糖

ES6的class让原型继承写起来更像传统面向对象语言:

class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise.`); } } class Dog extends Animal { constructor(name) { super(name); } bark() { console.log(`${this.name} barks.`); } } const d = new Dog('Mitzie'); d.speak(); // Mitzie makes a noise. d.bark(); // Mitzie barks.

虽然class看起来不同,但底层仍然是基于原型的继承。可以通过console.log(typeof Animal)看到它仍然是个函数。

5. 原型相关的重要方法和属性

5.1 检测原型关系

  • instanceof:检查构造函数的prototype是否出现在对象的原型链上
  • Object.prototype.isPrototypeOf():检查对象是否在另一个对象的原型链上
function Foo() {} const f = new Foo(); console.log(f instanceof Foo); // true console.log(Foo.prototype.isPrototypeOf(f)); // true console.log(Object.getPrototypeOf(f) === Foo.prototype); // true

5.2 遍历属性

  • for...in:遍历对象自身和原型链上的可枚举属性
  • Object.keys():只返回对象自身的可枚举属性
  • Object.getOwnPropertyNames():返回对象自身的所有属性(包括不可枚举的)
const parent = { a: 1 }; const child = { b: 2 }; Object.setPrototypeOf(child, parent); console.log(Object.keys(child)); // ['b'] for (let prop in child) { console.log(prop); // 'b', 'a' }

5.3 修改原型

  • Object.create():用指定原型创建新对象
  • Object.setPrototypeOf():设置对象的原型(性能较差,应避免在性能关键代码中使用)
  • Object.getPrototypeOf():获取对象的原型

6. 原型系统的常见陷阱与最佳实践

6.1 原型污染问题

原型污染是指无意或恶意修改Object.prototype等基础原型,导致所有对象都受到影响。我曾在一个项目中遇到一个难以追踪的bug,最后发现是某个第三方库给Object.prototype添加了一个名为toJSON的方法。

// 危险的代码 - 不要这样做! Object.prototype.someMethod = function() { console.log('I affect EVERY object'); }; const obj = {}; obj.someMethod(); // 现在所有对象都有这个方法了

最佳实践是永远不要扩展原生原型,除非是polyfill。即使是polyfill,也应该先检查方法是否已存在。

6.2 性能考量

原型链查找比直接属性访问慢,特别是在深层原型链上。在性能关键的代码中,可以考虑:

  1. 将常用方法直接赋值给对象
  2. 使用浅层原型链
  3. 避免在热代码路径中使用Object.setPrototypeOf()
// 优化前 function HeavyObject() {} HeavyObject.prototype.heavyMethod = function() { /*...*/ }; // 优化后 function HeavyObject() { this.heavyMethod = function() { /*...*/ }; }

6.3 组合优于继承

深层次的继承关系往往会导致代码难以维护。现代JavaScript开发更倾向于使用组合模式:

const canSpeak = { speak() { console.log(`${this.name} speaks.`); } }; const canBark = { bark() { console.log(`${this.name} barks.`); } }; function createDog(name) { const dog = { name }; return Object.assign(dog, canSpeak, canBark); } const d = createDog('Buddy'); d.speak(); d.bark();

7. 原型系统在现代JavaScript中的应用

7.1 类库和框架中的原型应用

许多流行库如jQuery、Lodash都大量使用原型来共享方法。例如jQuery对象的所有方法都定义在jQuery.fn(即jQuery.prototype)上。

// 类似jQuery的实现方式 function $(selector) { return new jQuery(selector); } function jQuery(selector) { this.elements = document.querySelectorAll(selector); } jQuery.prototype.css = function(prop, value) { this.elements.forEach(el => { el.style[prop] = value; }); return this; // 支持链式调用 }; $('div').css('color', 'red');

7.2 内置对象的原型扩展

虽然不建议扩展原生原型,但了解内置对象的原型结构对调试很有帮助。例如数组的所有方法都存储在Array.prototype上:

const arr = []; console.log(Object.getOwnPropertyNames(Array.prototype)); // ['length', 'constructor', 'concat', 'find', 'findIndex', ...]

7.3 Symbol与原型

ES6引入的Symbol属性不会被常规方法枚举,也不会被意外覆盖:

const id = Symbol('id'); function User(name) { this.name = name; this[id] = Math.random().toString(36).substr(2); } User.prototype.getId = function() { return this[id]; }; const u = new User('John'); console.log(Object.keys(u)); // ['name'] console.log(u.getId()); // 随机ID

8. 原型系统的深度解析

8.1 原型与内存效率

原型系统的一个主要优势是内存效率。方法定义在原型上时,所有实例共享同一个函数引用,而不是每个实例都创建自己的函数副本。

function BadCircle(radius) { this.radius = radius; this.area = function() { // 每个实例都会创建新函数 return Math.PI * this.radius ** 2; }; } function GoodCircle(radius) { this.radius = radius; } GoodCircle.prototype.area = function() { // 所有实例共享 return Math.PI * this.radius ** 2; };

在大型应用中,这种差异可能导致显著的内存节省。我曾优化过一个包含数千个实例的代码库,通过将方法移到原型上,内存使用减少了约30%。

8.2 原型与对象创建模式

JavaScript有多种对象创建模式,理解原型有助于选择最适合的模式:

  1. 工厂模式:不使用原型,每次返回全新对象
  2. 构造函数模式:使用new和prototype
  3. Object.create模式:纯净的原型继承
  4. 类模式:ES6语法糖
// 工厂模式 function createCircle(radius) { return { radius, area() { return Math.PI * radius ** 2; } }; } // 构造函数模式 function Circle(radius) { this.radius = radius; } Circle.prototype.area = function() { return Math.PI * this.radius ** 2; }; // Object.create模式 const circleProto = { area() { return Math.PI * this.radius ** 2; } }; function createCircle(radius) { const circle = Object.create(circleProto); circle.radius = radius; return circle; }

8.3 原型与多态性

JavaScript通过原型链实现多态性。子类可以重写父类方法,同时仍然可以通过super或直接访问原型来调用父类实现。

class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise.`); } } class Dog extends Animal { speak() { super.speak(); // 调用父类方法 console.log(`${this.name} barks.`); } } const d = new Dog('Rex'); d.speak(); // Rex makes a noise. // Rex barks.

在ES5中,可以通过直接访问原型实现类似效果:

function Animal(name) { this.name = name; } Animal.prototype.speak = function() { console.log(`${this.name} makes a noise.`); }; function Dog(name) { Animal.call(this, name); } Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.speak = function() { Animal.prototype.speak.call(this); // 相当于super.speak() console.log(`${this.name} barks.`); };

9. 原型系统的调试技巧

9.1 控制台中的原型可视化

现代浏览器的开发者工具可以直观展示原型链。在Chrome中:

  1. console.log一个对象
  2. 点击展开查看__proto__属性
  3. 可以一直展开到Object.prototype和null

9.2 检查属性来源

判断属性是来自对象自身还是原型链:

function hasOwnPropertyDeep(obj, prop) { while (obj !== null) { if (obj.hasOwnProperty(prop)) return true; obj = Object.getPrototypeOf(obj); } return false; } const obj = {}; Object.defineProperty(obj, 'hidden', { value: true, enumerable: false }); console.log('hidden' in obj); // true console.log(obj.hasOwnProperty('hidden')); // true console.log(Object.keys(obj)); // []

9.3 原型链断裂的情况

使用Object.create(null)创建的对象没有原型链,这类对象适合作为纯净的字典使用:

const dict = Object.create(null); // 没有原型链 dict.key = 'value'; console.log('toString' in dict); // false (普通对象会继承toString) console.log(dict.hasOwnProperty); // undefined

10. 原型系统的实际应用案例

10.1 实现混入(Mixin)模式

混入是通过原型实现多重继承的一种方式:

const sayMixin = { say(phrase) { console.log(phrase); } }; const sayHiMixin = { __proto__: sayMixin, // 或者用Object.setPrototypeOf sayHi() { super.say(`Hello ${this.name}!`); }, sayBye() { super.say(`Bye ${this.name}!`); } }; class User { constructor(name) { this.name = name; } } // 拷贝方法 Object.assign(User.prototype, sayHiMixin); new User('John').sayHi(); // Hello John!

10.2 实现自定义错误类型

通过原型继承可以创建自定义错误类型:

function ValidationError(message) { this.message = message; this.name = 'ValidationError'; this.stack = (new Error()).stack; } ValidationError.prototype = Object.create(Error.prototype); ValidationError.prototype.constructor = ValidationError; function test() { throw new ValidationError('Whoops!'); } try { test(); } catch(err) { console.log(err.message); // Whoops! console.log(err.name); // ValidationError console.log(err instanceof ValidationError); // true console.log(err instanceof Error); // true }

10.3 实现可插拔的功能扩展

原型系统可以用来实现灵活的插件架构:

class Core { constructor() { this.plugins = []; } use(plugin) { this.plugins.push(plugin); Object.assign(Core.prototype, plugin); } } const core = new Core(); core.use({ log() { console.log('Logging:', this.plugins); } }); core.log(); // Logging: [{...}]

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询