各位观众老爷们,大家好!今天咱们就来聊聊 JavaScript 里这对让人头大的兄弟:__proto__
和 prototype
。它们长得像,名字也像,但用法和意义却大相径庭。咱们今天就用大白话,把它们扒个底朝天,顺便再聊聊性能上的那点事儿。
开场白:祖传秘方与族谱
想象一下,prototype
就像是你们家的祖传秘方,记载了做菜的独门绝技。而 __proto__
呢,更像是你个人的族谱,记录了你从哪家哪户继承了这些绝技。虽然都跟家族血脉有关,但用途和意义可不一样。
第一幕:prototype
– 构造函数的秘密武器
首先,我们要明确一点:prototype
属性只有函数才有!
function Person(name) {
this.name = name;
}
console.log(Person.prototype); // 输出: {constructor: ƒ}
看到了吗?Person.prototype
存在,而且是一个对象。这个对象是干嘛的呢?
- 创造实例的蓝图:
Person.prototype
里面的属性和方法,会被所有通过new Person()
创建的实例继承。
Person.prototype.sayHello = function() {
console.log("Hello, my name is " + this.name);
};
const john = new Person("John");
john.sayHello(); // 输出: Hello, my name is John
你看,我们给 Person.prototype
加了一个 sayHello
方法,john
实例就能直接调用了。这就是原型继承的魅力!
constructor
属性: 默认情况下,prototype
对象会有一个constructor
属性,指向它所属的构造函数。
console.log(Person.prototype.constructor === Person); // 输出: true
这个 constructor
属性很重要,它在某些情况下可以帮助我们判断对象的类型。
第二幕:__proto__
– 实例的寻根问祖
现在轮到 __proto__
登场了。注意,__proto__
属性是所有对象都有的,包括函数!
const obj = {};
console.log(obj.__proto__); // 输出: {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
console.log(Person.__proto__); // 输出: ƒ () { [native code] }
__proto__
指向的是创建该对象的构造函数的 prototype
。换句话说,它指向的是对象的原型。
console.log(john.__proto__ === Person.prototype); // 输出: true
john
实例的 __proto__
指向了 Person.prototype
,所以它才能调用 Person.prototype
上的 sayHello
方法。
第三幕:原型链 – 继承的真相
__proto__
最重要的作用就是构建了原型链。原型链是 JavaScript 实现继承的关键。
当你要访问一个对象的属性或方法时,JavaScript 引擎会按照以下步骤查找:
- 先在对象自身查找。
- 如果找不到,就沿着
__proto__
向上查找,直到找到为止。 - 如果一直找到
null
(Object.prototype.__proto__ === null),还没找到,就返回undefined
。
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(this.name + " is eating.");
};
function Dog(name, breed) {
Animal.call(this, name); // 调用父构造函数
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype); // 关键的一步:继承 Animal 的原型
Dog.prototype.constructor = Dog; // 修正 constructor 指向
Dog.prototype.bark = function() {
console.log("Woof!");
};
const fido = new Dog("Fido", "Golden Retriever");
fido.eat(); // 输出: Fido is eating. (从 Animal.prototype 继承)
fido.bark(); // 输出: Woof! (Dog.prototype 自己的方法)
console.log(fido instanceof Animal); // 输出: true
console.log(fido instanceof Dog); // 输出: true
console.log(Dog.prototype.__proto__ === Animal.prototype); // 输出: true
在这个例子中,Dog
继承了 Animal
。Dog.prototype = Object.create(Animal.prototype)
这一步至关重要,它让 Dog.prototype
的 __proto__
指向了 Animal.prototype
,从而建立了原型链。
第四幕:__proto__
vs prototype
– 终极对比
特性 | prototype |
__proto__ |
---|---|---|
存在对象 | 只有函数才有 | 所有对象都有 (包括函数) |
指向 | 指向一个对象,这个对象是该构造函数创建的实例的原型。 | 指向创建该对象的构造函数的 prototype 。 |
用途 | 定义实例可以继承的属性和方法,是实现原型继承的基础。 | 访问和设置对象的原型,用于查找继承的属性和方法。 |
本质 | 构造函数的属性,用于定义实例的蓝图。 | 实例的属性,指向实例的原型。 |
标准化程度 | ES5 引入,所有现代浏览器都支持。 虽然 __proto__ 已经被标准化,但是不推荐直接使用 __proto__ 设置对象的原型,更推荐使用 Object.setPrototypeOf 或者 Object.create 。 |
早期浏览器非标准属性,后来逐渐被标准化。 虽然 __proto__ 已经被标准化,但是不推荐直接使用 __proto__ 设置对象的原型,更推荐使用 Object.setPrototypeOf 或者 Object.create 。 |
第五幕:性能考量 – 避免原型链过长
原型链的深度会影响属性查找的性能。如果原型链很长,JavaScript 引擎需要花费更多的时间去查找属性,从而降低程序的性能。
- 避免过度继承: 不要为了代码复用而滥用继承,只继承必要的属性和方法。
- 减少原型链的深度: 尽量让原型链保持较短的长度。
- 使用
hasOwnProperty
检查属性: 如果你想确定一个属性是对象自身拥有的,而不是从原型链继承的,可以使用hasOwnProperty
方法。
const obj = {
name: "Object"
};
console.log(obj.hasOwnProperty("name")); // 输出: true
console.log(obj.hasOwnProperty("toString")); // 输出: false (toString 是从 Object.prototype 继承的)
第六幕:最佳实践 – 更优雅的原型操作
虽然 __proto__
已经标准化了,但直接使用它来设置对象的原型并不是最佳实践。更推荐使用以下方法:
Object.create()
: 创建一个新对象,并指定它的原型。
const animal = {
eat: function() {
console.log("Eating...");
}
};
const dog = Object.create(animal);
dog.bark = function() {
console.log("Woof!");
};
dog.eat(); // 输出: Eating...
dog.bark(); // 输出: Woof!
Object.setPrototypeOf()
: 设置一个对象的原型。
const animal = {
eat: function() {
console.log("Eating...");
}
};
const dog = {};
Object.setPrototypeOf(dog, animal);
dog.bark = function() {
console.log("Woof!");
};
dog.eat(); // 输出: Eating...
dog.bark(); // 输出: Woof!
这两种方法都比直接使用 __proto__
更清晰,也更符合现代 JavaScript 的编程规范。
第七幕:性能实战 – 避免不必要的属性查找
我们来通过一个例子来演示原型链查找对性能的影响。
function A() {
this.a = 1;
}
A.prototype.b = 2;
function B() {
this.c = 3;
}
B.prototype = new A(); // B 的原型是 A 的实例
const b = new B();
// 性能测试
console.time("Access a");
for (let i = 0; i < 1000000; i++) {
b.a; // 访问 A 的属性
}
console.timeEnd("Access a");
console.time("Access c");
for (let i = 0; i < 1000000; i++) {
b.c; // 访问 B 的属性
}
console.timeEnd("Access c");
console.time("Access b");
for (let i = 0; i < 1000000; i++) {
b.b;
}
console.timeEnd("Access b");
在这个例子中,访问 b.a
需要沿着原型链向上查找,而访问 b.c
只需要在 b
对象自身查找。访问 b.b
需要访问 B.prototype
查找。理论上,访问 b.a
的速度应该比访问 b.c
慢,访问 b.b
应该比访问 b.c
慢,但比访问b.a
快。
(请注意,实际的性能差异可能会受到 JavaScript 引擎的优化影响,但这仍然可以说明原型链的深度会影响性能。)
总结:原型链是 JavaScript 的灵魂
__proto__
和 prototype
是 JavaScript 原型继承的核心概念。理解它们之间的区别和联系,能够帮助你更好地掌握 JavaScript 的继承机制,编写更高效、更可维护的代码。
prototype
是构造函数的属性,用于定义实例的蓝图。__proto__
是实例的属性,指向实例的原型。- 原型链是 JavaScript 实现继承的关键,但过长的原型链会影响性能。
- 推荐使用
Object.create()
和Object.setPrototypeOf()
来操作对象的原型。
希望今天的讲座能帮助大家更好地理解 __proto__
和 prototype
。记住,理解原型链,你就掌握了 JavaScript 的灵魂! 散会!