你真的理解了 javascript 中的原型及原型链?

JavaScript原型与原型链:javascript 继承的基础

引言

故事开始于

面试官:"说说你对原型以及原型链的理解"
我:原型是这样的..., 原型链是这样的..., 说的很抽象,听得也很抽象
面试官接着问:"说说javascript 怎么实现继承的"

作为前端开发者,我们经常会听到「原型」和「原型链」这两个概念,但你真的理解它们吗?它们是JavaScript面向对象编程的核心机制,掌握它们对于理解JavaScript的运行原理至关重要。

本文将从基础概念出发,逐步深入解析JavaScript原型与原型链的工作原理,结合大量代码示例和可视化图解,让你轻松掌握这一核心知识点。

一、原型的基本概念

1. 什么是原型?

在JavaScript中,每个对象都有一个原型对象(__proto__),对象可以从原型中继承属性和方法。原型对象也可以有自己的原型,这样就形成了一个链式结构,称为「原型链」。

通过console控制台,可以看到foo对象_proto_对象上面有个age等于18的属性,而age又是通过构造函数Foo的prototype添加上去的。

所以有了 foo.__proto__ === Foo.prototype

2. 原型的作用

原型主要有两个作用:

  • 属性继承:对象可以继承原型的属性
  • 方法共享:多个对象可以共享原型上的方法,节省内存空间

3. 代码示例:原型的基本使用

javascript 复制代码
// 创建一个普通对象
const person = {
  name: 'John',
  age: 30
};

// 获取person的原型
const proto = Object.getPrototypeOf(person);
console.log(proto); // 输出:[Object: null prototype] {}
console.log(proto === Object.prototype); // 输出:true

二、__proto__与prototype的区别

这是初学者最容易混淆的两个概念,让我们来彻底搞清楚它们:

1. proto(隐式原型)

  • 定义 :每个对象都有一个__proto__属性,指向它的原型对象
  • 作用:用于实现原型链查找
  • 注意 :这是一个非标准属性,推荐使用Object.getPrototypeOf()Object.setPrototypeOf()代替

2. prototype(显式原型)

  • 定义 :只有函数才有prototype属性
  • 作用 :当函数作为构造函数使用时,新创建的对象会将这个prototype作为自己的__proto__
  • 组成prototype对象包含constructor属性,指向构造函数本身

3. 可视化对比

特性 proto prototype
所属对象 所有对象 只有函数
指向 对象的原型 构造函数创建的实例的原型
作用 实现原型继承 定义构造函数的实例共享属性和方法
标准性 非标准(建议使用Object.getPrototypeOf) 标准属性

4. 代码示例:__proto__与prototype

javascript 复制代码
// 构造函数
function Person(name) {
  this.name = name;
}

// 构造函数的prototype属性
console.log(Person.prototype); // 输出:Person {}(包含constructor属性)

// 创建实例
const alice = new Person('Alice');

// 实例的__proto__指向构造函数的prototype
console.log(alice.__proto__ === Person.prototype); // 输出:true

// 构造函数的prototype的constructor指向构造函数
console.log(Person.prototype.constructor === Person); // 输出:true

三、构造函数与原型的关系

1. 构造函数创建实例的过程

当使用new关键字调用构造函数创建实例时,发生了以下几件事:

  1. 创建一个新的空对象
  2. 将这个新对象的__proto__指向构造函数的prototype
  3. 将构造函数的this指向这个新对象
  4. 执行构造函数体内的代码
  5. 如果构造函数没有返回对象,则返回这个新对象

2. 代码示例:构造函数创建实例

javascript 复制代码
// 构造函数
function Car(brand, model) {
  this.brand = brand;
  this.model = model;
}

// 在原型上添加方法
Car.prototype.drive = function() {
  console.log(`驾驶 ${this.brand} ${this.model}`);
};

// 创建两个实例
const car1 = new Car('Toyota', 'Camry');
const car2 = new Car('Honda', 'Accord');

// 调用原型上的方法
car1.drive(); // 输出:驾驶 Toyota Camry
car2.drive(); // 输出:驾驶 Honda Accord

// 两个实例共享同一个原型方法
console.log(car1.drive === car2.drive); // 输出:true

四、原型链的形成与查找机制

1. 什么是原型链?

当访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript会沿着__proto__属性向上查找,直到找到该属性或方法,或者到达原型链的末端(null)。这个链式查找结构就是「原型链」。

2. 原型链的末端

原型链的末端是Object.prototype,它的__proto__指向null,表示原型链的结束。

javascript 复制代码
// Object.prototype是原型链的顶端之一
console.log(Object.prototype.__proto__); // 输出:null

3. 代码示例:原型链查找

javascript 复制代码
// 创建对象
const obj = {};

// obj自身没有toString方法
console.log(obj.hasOwnProperty('toString')); // 输出:false

// 但可以调用toString方法,因为它继承自Object.prototype
console.log(obj.toString()); // 输出:[object Object]

// 原型链:obj -> Object.prototype -> null
console.log(obj.__proto__ === Object.prototype); // 输出:true
console.log(Object.prototype.__proto__ === null); // 输出:true

4. 完整原型链示例

javascript 复制代码
// 构造函数
function Animal(type) {
  this.type = type;
}

// 原型方法
Animal.prototype.eat = function() {
  console.log('进食中...');
};

// 子类构造函数
function Dog(name, breed) {
  Animal.call(this, 'dog'); // 调用父类构造函数
  this.name = name;
  this.breed = breed;
}

// 设置Dog的原型为Animal的实例
Dog.prototype = Object.create(Animal.prototype);
// 修复constructor指向
Dog.prototype.constructor = Dog;

// Dog的原型方法
Dog.prototype.bark = function() {
  console.log('汪汪汪!');
};

// 创建实例
const myDog = new Dog('Buddy', 'Golden Retriever');

// 访问自身属性
console.log(myDog.name); // 输出:Buddy

// 访问继承自Dog.prototype的方法
myDog.bark(); // 输出:汪汪汪!

// 访问继承自Animal.prototype的方法
myDog.eat(); // 输出:进食中...

// 访问继承自Object.prototype的方法
console.log(myDog.toString()); // 输出:[object Object]

// 原型链:myDog -> Dog.prototype -> Animal.prototype -> Object.prototype -> null

五、原型链的实际应用

1. 实现继承

原型链是JavaScript实现继承的主要方式。通过将子类的原型设置为父类的实例,可以实现属性和方法的继承。

javascript 复制代码
// 父类
function Parent(name) {
  this.name = name;
  this.family = 'Smith';
}

Parent.prototype.sayFamily = function() {
  console.log(`My family name is ${this.family}`);
};

// 子类
function Child(name, age) {
  Parent.call(this, name); // 继承父类属性
  this.age = age;
}

// 继承父类方法
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
  console.log(`I'm ${this.age} years old`);
};

// 使用
const child = new Child('John', 10);
child.sayFamily(); // 输出:My family name is Smith
child.sayAge(); // 输出:I'm 10 years old

2. 扩展内置对象

我们可以通过修改内置对象的原型来扩展其功能:

javascript 复制代码
// 扩展Array原型,添加求和方法
Array.prototype.sum = function() {
  return this.reduce((total, item) => total + item, 0);
};

// 使用扩展后的方法
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.sum()); // 输出:15

// 扩展String原型,添加反转方法
String.prototype.reverse = function() {
  return this.split('').reverse().join('');
};

// 使用扩展后的方法
const str = 'hello';
console.log(str.reverse()); // 输出:olleh

注意:虽然可以扩展内置对象,但不推荐在生产环境中使用,因为可能会与其他库冲突。

3. 原型链实现对象类型检查

javascript 复制代码
// 判断对象类型的函数
function getType(obj) {
  if (obj === null) return 'null';
  if (typeof obj !== 'object') return typeof obj;
  
  // 使用原型链判断具体类型
  const proto = Object.getPrototypeOf(obj);
  const constructor = proto.constructor;
  return constructor.name;
}

// 测试
console.log(getType(123)); // 输出:number
console.log(getType('hello')); // 输出:string
console.log(getType(true)); // 输出:boolean
console.log(getType(null)); // 输出:null
console.log(getType([])); // 输出:Array
console.log(getType({})); // 输出:Object

六、常见误区与注意事项

1. 误区一:所有对象都是Object的实例

正确理解:除了Object.prototype本身,所有对象都是Object的实例吗?不完全是。比如:

javascript 复制代码
// 创建一个没有原型的对象
const obj = Object.create(null);
console.log(obj.__proto__); // 输出:undefined
console.log(obj instanceof Object); // 输出:false

2. 误区二:原型上的属性修改会立即反映到所有实例

正确理解:是的,但如果是直接给实例添加同名属性,会覆盖原型属性,而不是修改原型:

javascript 复制代码
function Person() {}

Person.prototype.name = 'Anonymous';

const p1 = new Person();
const p2 = new Person();

console.log(p1.name); // 输出:Anonymous
console.log(p2.name); // 输出:Anonymous

// 修改原型属性
Person.prototype.name = 'Default';
console.log(p1.name); // 输出:Default
console.log(p2.name); // 输出:Default

// 给实例添加同名属性(覆盖)
p1.name = 'John';
console.log(p1.name); // 输出:John
console.log(p2.name); // 输出:Default(不受影响)

3. 注意事项:原型链查找的性能

原型链查找是有性能开销的,层级越深,查找速度越慢。因此:

  • 避免在原型链的深层定义常用属性和方法
  • 对于频繁访问的属性,可以考虑直接定义在对象本身

4. 注意事项:不要使用__proto__赋值

直接修改__proto__会影响对象的原型链,可能导致性能问题和意外行为。推荐使用:

  • Object.create()创建指定原型的对象
  • Object.setPrototypeOf()修改对象的原型

七、可视化理解原型链

为了更好地理解原型链,我们可以通过可视化的方式来呈现它的结构:

1. 简单对象的原型链

javascript 复制代码
obj (实例对象)
  └── __proto__ → Object.prototype
                    └── __proto__ → null

2. 构造函数创建的对象原型链

javascript 复制代码
instance (实例对象)
  └── __proto__ → Constructor.prototype
                    └── __proto__ → Object.prototype
                                      └── __proto__ → null

3. 继承关系的原型链

javascript 复制代码
childInstance (子类实例)
  └── __proto__ → Child.prototype
                    └── __proto__ → Parent.prototype
                                      └── __proto__ → Object.prototype
                                                        └── __proto__ → null

4. 代码示例:可视化原型链

javascript 复制代码
// 定义构造函数
function Grandparent() {
  this.grandparentProp = 'grandparent';
}

function Parent() {
  this.parentProp = 'parent';
}

function Child() {
  this.childProp = 'child';
}

// 设置继承关系
Parent.prototype = Object.create(Grandparent.prototype);
Child.prototype = Object.create(Parent.prototype);

// 创建实例
const child = new Child();

// 可视化原型链
console.log('child:', child);
console.log('child.__proto__ (Child.prototype):', child.__proto__);
console.log('child.__proto__.__proto__ (Parent.prototype):', child.__proto__.__proto__);
console.log('child.__proto__.__proto__.__proto__ (Grandparent.prototype):', child.__proto__.__proto__.__proto__);
console.log('child.__proto__.__proto__.__proto__.__proto__ (Object.prototype):', child.__proto__.__proto__.__proto__.__proto__);
console.log('child.__proto__.__proto__.__proto__.__proto__.__proto__ (null):', child.__proto__.__proto__.__proto__.__proto__.__proto__);

八、原型与现代JavaScript

1. ES6 Class与原型的关系

ES6引入了class语法,但它只是原型继承的语法糖,底层仍然是基于原型链实现的:

javascript 复制代码
// ES6 Class
class Animal {
  constructor(type) {
    this.type = type;
  }
  
  eat() {
    console.log('进食中...');
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super('dog');
    this.name = name;
    this.breed = breed;
  }
  
  bark() {
    console.log('汪汪汪!');
  }
}

// 等价于原型继承
console.log(typeof Animal); // 输出:function
console.log(Dog.prototype.__proto__ === Animal.prototype); // 输出:true

2. 原型与组合继承

现代JavaScript中,我们通常使用组合继承模式,结合原型链和构造函数:

javascript 复制代码
// 组合继承模式
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayName = function() {
  console.log(this.name);
};

function Child(name, age) {
  // 继承属性
  Parent.call(this, name);
  this.age = age;
}

// 继承方法
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
  console.log(this.age);
};

九、总结

通过本文的学习,我们已经全面了解了JavaScript原型与原型链的核心概念:

  1. 原型:每个对象都有一个原型,可以继承原型的属性和方法
  2. proto:对象的隐式原型,指向它的原型对象
  3. prototype:函数的显式原型,用于构造函数创建实例时的原型指向
  4. 原型链:对象通过__proto__形成的链式结构,用于属性和方法的查找
  5. 继承:通过原型链实现对象间的继承关系

原型与原型链是JavaScript的核心机制,掌握它们对于理解JavaScript的运行原理、实现面向对象编程至关重要。希望本文的详细解析和丰富示例能帮助你彻底理解这一知识点。

思考与练习

  1. 为什么说原型链是JavaScript实现继承的基础?
  2. 如何优化原型链查找的性能?
  3. ES6 Class和传统原型继承有什么区别?
  4. 尝试实现一个完整的原型链继承案例
  5. 解释instanceof运算符的工作原理(提示:基于原型链) 欢迎在评论区分享你的理解和思考,让我们一起进步!

参考资料


如果你觉得本文对你有帮助,欢迎点赞、收藏、分享,也欢迎关注我,获取更多前端技术干货!

相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax