你真的理解了 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运算符的工作原理(提示:基于原型链) 欢迎在评论区分享你的理解和思考,让我们一起进步!

参考资料


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

相关推荐
冴羽1 小时前
10 个 Nano Banana Pro 专业级生图技巧
前端·人工智能·aigc
7ayl1 小时前
Vue3 - runtime-core的渲染器初始化流程
前端·vue.js
前端老宋Running2 小时前
React 的“时光胶囊”:useRef 才是那个打破“闭包陷阱”的救世主
前端·react.js·设计模式
yinuo2 小时前
前端跨页面通讯终极指南③:LocalStorage 用法全解析
前端
隔壁的大叔2 小时前
正则解决Markdown流式输出不完整图片、表格、数学公式
前端·javascript
胡楚昊2 小时前
CTF SHOW逆向
java·服务器·前端
San302 小时前
深入 JavaScript 原型与面向对象:从对象字面量到类语法糖
javascript·面试·ecmascript 6
拉不动的猪2 小时前
前端JS脚本放在head与body是如何影响加载的以及优化策略
前端·javascript·面试
shykevin2 小时前
Actix-Web完整项目实战:博客 API
前端·数据库·oracle