1 什么是原型?
1.1 定义
每个函数(Function) 都有一个 prototype 属性,这个属性是一个指针,指向一个对象,这个对象就是该函数的原型对象。
1.2 普通构造函数与原型的对比
这里有个构造函数Person:
function Person (name) {
this.name = name;
}
New 两个实例对象:
const p1 = new Person ("张三")
const p2 = new Person ("小美")
console.log(p1 === p2) //false
两个实例对象虽然都是由同一个构造函数 new 出来的,但是他们是相互独立的,给他们添加一个共同方法:
function Person (name) {
this.name = name;
// 定义方法
this.sayHello = function() {
console.log("Hello, I'm " + this.name);
};
}
let p1 = new Person("Alice");
let p2 = new Person("Bob");
p1.sayHello(); // Hello, I'm Alice
p2.sayHello(); // Hello, I'm Bob
但是问题来了,每次创建新实例时,都会重新创建该实例的所有方法,这会导致较高的内存开销,尤其是在创建大量对象时。
所以我们可以用原型来实现共享方法和属性。
function Person (name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log("Hello, I'm " + this.name);
};
let p1 = new Person("Alice");
let p2 = new Person("Bob");
p1.sayHello(); // Hello, I'm Alice
p2.sayHello(); // Hello, I'm Bob
console.log(p1.sayHello === p2.sayHello); // true,表明两个实例共享同一个方法
好处:
- 使用原型可以节省内存,所有实例共享原型上的方法,只有属性是各自独立的,从而大大减少了内存使用。
- 可以动态地向原型添加新的方法或属性,这些更改会立即反映在所有实例上。
- 通过原型链实现继承,允许子类继承父类的方法和属性。
缺点:
- 如果多个人都尝试修改同一个原型,可能会导致意外覆盖或冲突。
对比:
| 特性 | 普通构造函数(不使用原型) | 使用原型 |
|---|---|---|
| 方法定义 | 在构造函数内部为每个实例单独定义 | 在构造函数的原型对象上定义 |
| 内存 使用 | 高,每个实例都有自己的方法副本 | 低,所有实例共享原型上的方法 |
| 灵活性 | 较低,不易于扩展 | 高,支持动态扩展和继承 |
| 维护成本 | 简单但可能导致重复代码 | 更加灵活但需要理解原型链 |
2 原型链
我们知道原型带来了很多好处,那他是如何实现方法属性共享的呢?
原型链。
原型链是实现继承的主要机制。
2.1 原型链的形成
当使用构造函数创建对象时,新创建的对象实例会有一个内部属性 [[Prototype]](通常用 .__proto__表示),它指向构造函数的 prototype 对象。而原型对象本身也可能有自己的 proto,这样就形成了一个链条,直到 Object.prototype,最终指向 null。
2.1.1 属性查找机制
当你访问一个对象的属性或方法时,JavaScript 引擎会先在该对象自身查找,如果没有找到,就会沿着原型链向上查找,直到找到为止,或者到达原型链末端(null)为止。
let obj = {};
console.log(obj.toString());
// 调用了 Object.prototype.toString()
虽然 obj 自己没有 toString() 方法,但它继承了 Object.prototype 上的该方法。
2.1.2 constructor 属性
原型对象 prototype 也是一个对象,也有自己的属性和方法。它上面默认有个 constructor 属性,指向构造函数。
function Star(){
}
此时:它的原型对象有以下属性,还没有方法 (要手动给它添加)
Star.prototype{
constructor : Star // 指向构造函数
}
2.2 原型链的应用与继承
// 1.创建函数Animal
function Animal() {}
// 2.给Animald的原型对象加上eat方法
Animal.prototype.eat = function() {
console.log("Animal is eating.");
};
// 3.创建函数Dog
function Dog() {}
// 4.将Dog的原型对象指向Animal实例
Dog.prototype = new Animal(); // 继承 Animal
// 5.在Dog原型对象上添加bark方法
Dog.prototype.bark = function() {
console.log("Woof!");
};
// 6.创建dog实例对象
let dog = new Dog();
// 7.调用原型链上的方法
dog.eat(); // 来自 Animal
dog.bark(); // 来自 Dog
在这个例子中,Dog 的原型指向了一个 Animal 实例,从而实现了继承。
他的原型链是这样的:
结语
原型是 JavaScript 中最具特色和魅力的特性之一,深入理解原型链的工作原理和应用技巧,不仅能让我们编写出更加高效、优雅的代码,还能帮助我们更好地理解 JavaScript 的面向对象编程模型。无论是性能优化、设计模式,还是解决复杂的业务逻辑,原型都能成为我们手中的得力武器。