原型和原型链是学习 JavaScript 过程中极为关键的概念,是深入理解 JavaScript 面向对象编程的核心要点。
一、理解原型
(一)原型的基本概念
在 JavaScript 中,几乎所有的对象都有一个__proto__
属性,这个属性指向了该对象的原型对象。可以把原型对象想象成一个 "模板",它为对象提供了一些默认的属性和方法。当对象本身没有某个特定属性或方法时,JavaScript 引擎就会沿着__proto__
所指的方向,到原型对象中去寻找。
比如,创建一个简单的对象:
javascript
let animal = {
name: '小狗',
speak() {
console.log('我会叫');
}
};
console.log(animal.__proto__);
这里的animal
对象就有一个__proto__
属性,它指向了一个隐藏的原型对象。
原型对象的创建
1. 构造函数方式
构造函数是创建对象的一种模板。当我们定义一个构造函数时,JavaScript 会自动为其创建一个与之关联的原型对象。例如:
javascript
function Animal(name) {
this.name = name;
}
// Animal.prototype 即为与 Animal 构造函数关联的原型对象
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound.`);
};
在上述代码中,Animal
是一个构造函数。当我们定义 Animal.prototype.speak
方法时,实际上是在为 Animal
构造函数的原型对象添加一个 speak
方法。之后,通过 new Animal('Lion')
创建的所有 Animal
实例都能访问到这个 speak
方法。
2. Object.create () 方式
Object.create()
方法为我们提供了一种更为灵活的创建原型对象的途径。它允许我们基于一个已有的对象来创建新的对象,新对象的原型将指向传入的对象。例如:
javascript
const animalPrototype = {
speak: function() {
console.log(`${this.name} makes a sound.`);
}
};
const dog = Object.create(animalPrototype);
dog.name = 'Buddy';
dog.speak(); // Buddy makes a sound.
这里,我们首先定义了一个 animalPrototype
对象,然后使用 Object.create(animalPrototype)
创建了 dog
对象。dog
对象的原型就是 animalPrototype
,所以它能够调用 speak
方法。
3. 字面量方式
使用对象字面量创建对象时,该对象会默认继承 Object.prototype
作为其原型对象。对象字面量是一种简洁创建对象的方式。
示例代码如下:
javascript
// 使用对象字面量创建对象 cat
const cat = {
name: 'Cat'
};
// cat 对象的原型是 Object.prototype
console.log(cat.__proto__ === Object.prototype);
在这段代码中,cat
对象是通过对象字面量创建的,它的 __proto__
属性指向 Object.prototype
。
4. 类语法(ES6 及以后)
ES6 引入了类语法,虽然本质上类还是基于原型实现的,但它提供了更接近传统面向对象语言的语法。类中的 prototype
可以用来定义共享的方法。
示例代码如下:
javascript
// 定义一个类 Animal
class Animal {
constructor(name) {
this.name = name;
}
// 定义共享方法 speak
speak() {
console.log(`${this.name} makes a sound.`);
}
}
// 通过类创建实例
const tiger = new Animal('Tiger');
tiger.speak();
Animal
类有一个 prototype
属性,也就是 Animal.prototype
,它是一个对象,这个对象就是通过 Animal
类创建的实例的原型对象。
(二)函数的 prototype 属性
在 JavaScript 中,函数具有独特的性质。它和普通对象一样拥有 __proto__
属性,此外还具备特有的 prototype
属性。prototype
是函数独有的一个属性,指向一个对象,用于实现构造函数的原型继承。
当使用构造函数创建新对象时,实际上是新对象的内部 [[Prototype]]
属性会指向构造函数的 prototype
属性所对应的对象。__proto__
是访问这个内部 [[Prototype]]
属性的一种方式(不过 __proto__
并非标准属性,在现代 JavaScript 里,更推荐使用 Object.getPrototypeOf()
方法来访问对象的原型)。这种机制构建起了新对象与构造函数原型之间的关键联系。
例如:
javascript
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log(`大家好,我是${this.name},今年${this.age}岁`);
};
let tom = new Person('Tom', 25);
console.log(tom.__proto__ === Person.prototype);
tom.sayHello();
在上述代码里,Person
作为构造函数,其 prototype
是一个对象,我们在上面定义了 sayHello
方法。使用 new
关键字创建 tom
对象后,tom
的 [[Prototype]]
指向 Person.prototype
,__proto__
反映了这一关系。调用 tom.sayHello()
时,JavaScript 引擎会先在 tom
自身查找该方法,找不到就会顺着 [[Prototype]]
去 Person.prototype
中查找并执行。
这种特性让通过同一构造函数创建的对象能共享 prototype
上的属性和方法,避免为每个对象单独创建副本,实现代码复用与内存节省。
__proto__
、prototype
和 [[Prototype]]
的区别
__proto__
是一个实际存在于对象中的属性,它是对对象内部[[Prototype]]
的引用。- 借助
__proto__
,你能够在代码里直接访问对象的原型。 __proto__
并非标准属性,尽管大多数浏览器都支持它,但不建议在生产环境中使用,因为它可能会引发性能问题,并且在某些环境下可能不被支持。例如:
- 借助
javascript
let obj = {};
console.log(obj.__proto__ === Object.prototype); // true
`
prototype
是函数特有的属性。当一个函数作为构造函数使用时,通过new
关键字创建的对象,其__proto__
属性会指向构造函数的prototype
属性。- 也就是说,
prototype
是为了实现构造函数的原型继承而设计的。例如:
- 也就是说,
javascript
function MyClass() {}
MyClass.prototype.someMethod = function() {
console.log('This is a method on the prototype');
};
let instance = new MyClass();
console.log(instance.__proto__ === MyClass.prototype); // true
instance.someMethod(); // This is a method on the prototype
[[Prototype]]
是 JavaScript 内部的一个隐式属性,它体现了对象之间的原型关联。- JavaScript 引擎在查找对象的属性与方法时,会依据
[[Prototype]]
构建的原型链来进行搜索。 - 不过,
[[Prototype]]
无法直接在代码里访问,它是一个抽象的概念,用来描述对象的原型继承关系。
- JavaScript 引擎在查找对象的属性与方法时,会依据
(三)原型对象的作用
属性与方法继承
原型对象最核心的作用之一就是实现属性和方法的继承。当我们访问一个对象的属性或方法时,如果该对象自身没有这个属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(Object.prototype
)。例如:
javascript
function Dog(name) {
this.name = name;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} barks.`);
};
const myDog = new Dog('Max');
myDog.speak(); // Max makes a sound.
myDog.bark(); // Max barks.
在这个例子中,Dog
构造函数的原型继承自 Animal
构造函数的原型。因此,myDog
作为 Dog
的实例,不仅能够访问 Dog.prototype
上定义的 bark
方法,还能访问 Animal.prototype
上的 speak
方法。
节省内存
通过原型对象,多个对象可以共享相同的属性和方法,从而节省内存空间。假设我们有一个 Circle
构造函数,并且为其原型定义了一个 draw
方法:
javascript
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.draw = function() {
console.log(`Drawing a circle with radius ${this.radius}`);
};
const circle1 = new Circle(5);
const circle2 = new Circle(10);
circle1
和 circle2
这两个对象共享 Circle.prototype
上的 draw
方法,而不是每个对象都拥有一份独立的 draw
方法副本。这在创建大量对象时,能够显著减少内存的占用。
二、原型链的奥秘
(一)原型链的形成机制
-
在 JavaScript 里,原型链通过对象原型层层相连构成。
-
引擎首先会在对象本身的属性列表中进行查找,检查目标属性或方法是否直接存在于该对象上。若对象自身拥有此属性或方法,引擎会立即使用它。
-
如果对象自身并不包含目标属性或方法,引擎就会借助对象的
__proto__
属性来深入探索原型链。__proto__
提供了对对象内部[[Prototype]]
的访问途径,[[Prototype]]
指向该对象的原型对象。引擎会顺着这个链接进入原型对象,继续在原型对象的属性和方法中搜索目标内容。 -
若在当前原型对象中仍未找到目标,引擎不会停止,而是会继续沿着原型链向上查找,不断通过每个原型对象的
__proto__
(即其自身的[[Prototype]]
)访问上一级原型对象,持续搜索,直至到达原型链的顶端(Object.prototype
,其[[Prototype]]
为null
)。如果遍历完整个原型链都未找到目标属性或方法,最终会返回undefined
。
以数组为例:
javascript
let arr = [1, 2, 3];
arr.push(4);
数组对象arr
自身没有push
方法。但arr
的__proto__
指向Array.prototype
,而Array.prototype
上定义了push
方法。借助原型链,arr
便能调用push
方法。
(二)原型链的结构特性与查找终点
原型链有个重要特性:它的终点是null
。
以数组arr
来说,arr
的__proto__
指向Array.prototype
,Array.prototype
的__proto__
指向Object.prototype
,Object.prototype
的__proto__
为null
,这样就形成了完整的原型链。
这一特性影响着查找过程。当在arr
中查找属性或方法时,从arr
自身开始,沿__proto__
依次在Array.prototype
、Object.prototype
中查找。一旦到达Object.prototype
,且其__proto__
为null
时还未找到目标,就可确定该属性或方法不存在 。null
作为原型链终点,为查找提供了明确的终止信号,保证了查找的完整性。
(三)原型链的作用
- 代码复用 :在原型对象上定义属性和方法,可实现多个对象对这些内容的共享,有效减少代码冗余。如上述
Person
构造函数,所有通过Person
创建的对象都能共享Person.prototype
上的sayHello
方法。 - 实现继承 :JavaScript 没有传统面向对象语言的类继承语法,而是借助原型链实现继承。例如创建一个继承自
Person
的Student
对象:
javascript
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.study = function() {
console.log(`${this.name}正在学习,年级是${this.grade}`);
};
let lucy = new Student('Lucy', 18, '高三');
lucy.sayHello();
lucy.study();
在这段代码中,Student
通过原型链继承了Person
的属性和方法,同时添加了自身特有的study
方法,充分展示了原型链在实现对象间继承关系方面的重要作用 。
三、原型和原型链的常见问题
(一)原型污染
如果不小心修改了原型对象上的属性,可能会影响到所有基于该原型创建的对象,这就是原型污染。例如:
javascript
Object.prototype.newProp = '新属性';
let obj1 = {};
let obj2 = {};
console.log(obj1.newProp);
console.log(obj2.newProp);
这里在Object.prototype
上添加了一个新属性newProp
,导致所有对象都有了这个属性,这可能会带来意想不到的问题。所以在开发中要避免对原型对象进行不必要的修改。
(二)理解constructor
属性
每个原型对象都有一个constructor
属性,它指向创建该原型对象的构造函数。但是在修改原型对象时,要注意constructor
属性的指向是否正确。比如在上面Student
继承Person
的例子中,修改Student.prototype
后,需要手动将Student.prototype.constructor
指向Student
,否则constructor
属性会指向错误的构造函数。
javascript
Student.prototype = Object.create(Person.prototype);
// 手动设置 constructor 属性
Student.prototype.constructor = Student;
let student = new Student('Lucy', 18, '高三');
console.log(student.constructor === Student); // true
希望通过这篇文章,大家能对 JavaScript 的原型和原型链有更清晰的认识,在 JavaScript 的学习道路上迈出更坚实的步伐。