深入解析JavaScript中的__proto__、prototype与继承机制

本文深入探讨JavaScript中原型链的工作原理,详细解析__proto__与prototype的区别,并通过实例演示原型继承在函数和类中的实际应用,帮助开发者彻底理解JavaScript的继承机制。

proto、prototype和继承在JavaScript中的实际工作原理

你是否曾想过为什么JavaScript中的所有东西都表现得像对象?或者继承在幕后是如何实际工作的?__proto__和prototype之间有什么区别?

字符串方法之谜

让我们从一个简单的例子开始:

1
let name = "Shejan Mahamud";

声明此变量后,我们可以使用字符串方法:

1
2
name.toLowerCase(); // "shejan mahamud"
name.toUpperCase(); // "SHEJAN MAHAMUD"

这看起来很正常,但等等——有些不同寻常的事情正在发生。我们在字符串原始类型上使用点符号,但原始类型通常不是对象。

答案在于理解JavaScript如何处理原始类型和原型。

对象的内部工作原理

当你在JavaScript中创建这样的对象时:

1
2
3
4
const info1 = {
  fName: "Shejan",
  lName: "Mahamud"
};

JavaScript在幕后做了一些有趣的事情。它会自动向你的对象添加一个名为__proto__的隐藏属性。此属性指向Object.prototype,即内置Object类的原型。

Object.prototype也有一个__proto__吗?是的,但它的值为null。这是因为Object.prototype位于原型链的顶部,不继承任何其他东西。

让我们看一个更复杂的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const info1 = {
  fName1: "Shejan",
  lName1: "Mahamud"
};

const info2 = {
  fName2: "Boltu",
  lName2: "Mia",
  __proto__: info1
};

const info3 = {
  fName3: "Habbu",
  lName3: "Mia",
  __proto__: info2
};

我们可以从info3访问fName1吗?

1
console.log(info3.fName1); // "Shejan"

是的,我们可以!

理解原型链

当你尝试访问对象上的属性时,JavaScript遵循特定的查找过程:

  1. 首先,它在对象本身(基础对象)中搜索属性
  2. 如果在那里找不到,它会在对象的__proto__中查找
  3. 如果仍然找不到,它会继续向上链,检查每个__proto__,直到找到属性或到达null

在我们的info3.fName1示例中:

  • JavaScript首先在info3中查找——没有找到fName1
  • 然后检查info3.proto,它指向info2——也没有找到fName1
  • 接下来检查info2.proto,它指向info1——在这里找到了fName1!

这称为原型链,这就是JavaScript中继承的工作方式。

为什么JavaScript中的所有东西都是对象

现在让我们解决我们开始时的谜团:原始类型如何使用对象方法?

在JavaScript中,几乎所有东西都表现得像对象,即使原始类型(如字符串、数字和布尔值)技术上不是对象。这是通过称为自动装箱或包装对象的过程实现的。

当你在字符串上尝试使用方法时:

1
yourName.toLowerCase();

JavaScript在幕后执行以下操作:

  1. 它临时将原始值包装在包装对象中:new String(“Boltu”)
  2. 此临时对象的__proto__自动指向String.prototype
  3. 在String.prototype中找到方法并执行
  4. 操作完成后,包装对象被丢弃
  5. yourName恢复为简单的原始值

这就是为什么你可以在原始类型上使用方法,即使它们不是对象。

__proto__与prototype的区别

什么是prototype?

当你在JavaScript中创建函数或类时,语言会自动创建一个名为prototype的蓝图对象。

例如:

1
2
3
function Person(name) {
  this.name = name;
}

当JavaScript看到此函数时,它在内部执行:

1
2
3
Person.prototype = { 
  constructor: Person 
};

Person函数现在有一个名为prototype的隐藏属性,它是一个包含constructor属性的对象。

你可以向此原型对象添加方法:

1
2
3
Person.prototype.sayHi = function() {
  console.log("Hi, I'm " + this.name);
};

什么是__proto__?

__proto__是存在于每个对象实例(数组、函数、对象——所有东西)上的属性。它是一个内部引用或指针,指示对象继承自哪个原型。

默认情况下,当你创建对象时,其__proto__指向Object.prototype。

它们如何协同工作

当你使用new关键字时:

1
const p1 = new Person("Shejan");

JavaScript在内部执行以下步骤:

  1. 创建一个新的空对象:p1 = {}
  2. 设置对象的__proto__:p1.proto = Person.prototype
  3. 使用新对象调用构造函数:Person.call(p1, “Shejan”)
  4. 返回对象:return p1

现在当你尝试访问方法时:

1
p1.sayHi(); // "Hi, I'm Shejan"

JavaScript首先在p1中查找sayHi。当找不到时,它检查p1.proto,它指向Person.prototype,方法在那里定义。

关系可以表示为:

1
2
p1.__proto__ === Person.prototype; // true
Person.prototype.constructor === Person; // true

总结:

  • prototype是函数/类的属性,用作实例的蓝图
  • __proto__是对象实例的属性,指向它们继承的原型

原型如何与函数一起工作

让我们看一个完整的函数示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 向原型添加方法
Person.prototype.introduce = function() {
  console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);
};

// 创建实例
const person1 = new Person("Alice", 25);
const person2 = new Person("Bob", 30);

person1.introduce(); // "Hi, I'm Alice and I'm 25 years old."
person2.introduce(); // "Hi, I'm Bob and I'm 30 years old."

// 两个实例共享相同的原型
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true
console.log(person1.__proto__ === person2.__proto__); // true

这里的关键好处是内存效率:introduce方法仅存在于Person.prototype中一次,但所有实例都可以通过原型链访问它。

原型如何与类一起工作

ES6引入了类语法,它看起来不同,但在幕后以相同的方式工作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class User {
  constructor(name) {
    this.name = name;
  }

  sayHi() {
    console.log(`Hi, I'm ${this.name}`);
  }
}

const user1 = new User("Charlie");
user1.sayHi(); // "Hi, I'm Charlie"

// 让我们检查实际发生了什么
console.log(typeof User); // "function"
console.log(User.prototype); // { sayHi: f, constructor: f User() }
console.log(user1.__proto__ === User.prototype); // true

类本质上是JavaScript基于原型的继承的语法糖。在内部:

  • 类仍然是构造函数
  • 在类中定义的方法被添加到ClassName.prototype
  • 使用new创建的实例将其__proto__设置为类的prototype

这意味着我们学到的关于函数原型的所有内容也适用于类。

结论

理解原型和原型链对于掌握JavaScript至关重要。这些概念构成了JavaScript实现继承和面向对象编程的基础。

关键要点

让我们回顾一下我们学到的内容:

  • 每个对象都有__proto__:此属性指向对象继承的原型,启用原型链查找机制。
  • 函数和类有prototype:此属性用作使用new关键字创建的实例的蓝图。
  • 原型链启用继承:当JavaScript在对象上找不到属性时,它会向上遍历原型链,直到找到属性或到达null。
  • 原始类型使用包装对象:即使原始类型不是对象,JavaScript也会临时将它们包装在对象中以提供对方法的访问。
  • 类是语法糖:现代类语法更清晰,但在幕后仍然使用原型。

JavaScript起初可能看起来古怪,但一旦你理解了它的幕后工作原理,你就会欣赏其优雅灵活的设计。

祝你编码愉快!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计