JS `__proto__` 与 `prototype` 属性的本质区别与性能影响

各位观众老爷们,大家好!今天咱们就来聊聊 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 引擎会按照以下步骤查找:

  1. 先在对象自身查找。
  2. 如果找不到,就沿着 __proto__ 向上查找,直到找到为止。
  3. 如果一直找到 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 继承了 AnimalDog.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 的灵魂! 散会!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注