原型和原型链

📌 为什么需要原型?—— 解决资源浪费问题

想象一下,我们要创建许多个“人”的对象。每个对象都有名字、年龄,并且都能说话。如果没有原型:​​

// 每次调用这个函数,都会创建一个新的 sayHello 函数
function createPerson(name, age) {
  return {
    name: name,
    age: age,
    sayHello: function () {
      console.log(`Hello, I'm ${this.name}`);
    },
  };
}
const p1 = createPerson("Alice", 25);
const p2 = createPerson("Bob", 30);
console.log(p1.sayHello === p2.sayHello); // false

🚨 ​问题:​​ 虽然 p1p2sayHello 功能一样,但它们却是两个不同的函数实例,占用了两份内存空间。如果有成千上万个对象,这种浪费会非常严重。

💡 解决方案 —— 将共用资源放到一个共享地方

我们希望所有“人”对象都共享同一个​ sayHello 函数。这个共享的地方,就是原型Prototype)。

JavaScript 中,每当你创建一个函数时,都会同时创建一个对应的原型对象。

// 1. 我们创建一个构造函数(首字母大写是一种约定)
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 2. JS 会自动为 Person 函数创建 prototype 属性
//    这个属性指向一个空对象,我们称之为“原型对象”
//    我们可以在这个共享对象上添加方法
Person.prototype.sayHello = function () {
  console.log(`Hello, I'm ${this.name}`);
};
// 3. 使用 new 关键字来创建对象(实例)
//    new 关键字会做一件非常重要的事:
//    它将新对象的__proto__属性指向Person.prototype
const person1 = new Person("Alice", 25);
const person2 = new Person("Bob", 30);
// 4. 现在,所有由Person创建的对象,都能访问同一个方法
person1.sayHello(); // Hello, I'm Alice
person2.sayHello(); // Hello, I'm Bob
// 5. 验证它们确实是同一个函数 // true 🎉
console.log(person1.sayHello === person2.sayHello);

关键点:​​

  • prototype​:是函数才有的属性,它指向一个对象(原型对象),这个对象的用途就是让所有由该函数创建的实例共享方法和属性。
  • __proto__​:是每个对象​(含函数对象)都有的属性,它指向创建该对象的构造函数的 prototype
    • person1.__proto__ === Person.prototype
    • ❗注意:​​ __proto__ 是一个非标准的历史遗留属性,用于学习和调试。在现代代码中,建议使用 Object.getPrototypeOf(obj) 获取原型。

🔗 什么是原型链?—— 查找机制

当我们访问一个对象的属性(或方法)时,JavaScript 引擎会:

  1. 先在对象自身上查找
  2. 如果没找到,就通过 __proto__ 去它的原型对象找
  3. 如果原型对象上也没找到,就再去原型对象的原型对象上找(因为原型对象本身也是一个对象,它也有 __proto__)。
  4. 一直这样向上查找,直到找到某个原型对象为 null 为止(这是查找的终点)。
  5. 如果直到终点都没找到,则返回 undefined

这种一层一层向上链接、形成链式关系的结果,就叫做原型链。

代码演示如下:

// 1. 在自身查找 name
console.log(person1.name); // 'Alice'(在自身找到)

// 2. 自身没有 sayHello,
//    通过 __proto__ 到 Person.prototype 上找
console.log(person1.sayHello); // 在原型上找到了

// 3. 我们来看看原型链的尽头
// Person.prototype 也是一个对象,它是被 Object()
// 这个内置构造函数创建的,所以
// Person.prototype.__proto__ === Object.prototype
log(person1.__proto__ === Person.prototype); // true
// true,此处应该是 ===,图片显示不下,所以用 = 代替
log((Person.prototype.__proto__ = Object.prototype));

// 4. Object.prototype 是绝大多数对象的原型链终点,
// 它的 __proto__ 是 null
console.log(Object.prototype.__proto__); // null

// 5. 尝试查找一个不存在的属性,查找路径:
//    person1 -> Person.prototype ->
//    Object.prototype -> null
console.log(person1.notExistProp); // undefined

📊 图示化理解:​​ 下图清晰地展示了实例对象 person1构造函数 Person原型对象之间的完整链条关系:

💬 细心的宝子可能发现了,图中为什么多出了 Function.prototype? 🚀 这是因为在 js 中,函数本身也是对象,所以 PersonFunction 构造函数的实例。所以 Person__proto__ 指向 Function.prototype

📝 总结

  1. ​原型 (Prototype)
  • 一个简单的对象,用于存储可以被其他对象共享的属性和方法。
  • 函数的prototype属性规定了由它创建的对象能共享什么。
  • 对象的[[Prototype]] (可通过 __proto__Object.getPrototypeOf() 访问)​规定了它自己能使用什么共享资源。
  1. 原型链 (Prototype Chain)
  • 由多个对象通过 __proto__ 链接形成的链式结构
  • ​机制​:当访问一个对象的属性时,如果自身没有,就沿着这条链向上查找,直到找到或到达终点(null)。
  • ​意义​:实现了 JavaScript 的“继承”机制。一个对象可以“继承”其原型链上所有对象的属性和方法。
  1. 补充说明:constructor 属性​

每个函数的 prototype 对象都有一个 constructor 属性,指回函数本身。

  • Person.prototype.constructor === Person
  • person1.constructor === Person(这样实例就能知道自己是由谁创建的了)

🥳🥳🥳 ==你已经看完了,赶快去挑战下吧!

✨赶快测验下吧~✨

function Vehicle() {
  this.wheels = 4;
}
Vehicle.prototype.drive = function () {
  console.log("Drive");
};

function Car(brand) {
  this.brand = brand;
}
Car.prototype = new Vehicle();
Car.prototype.constructor = Car;

const myCar = new Car("Toyota");
  1. myCar.hasOwnProperty('wheels') 输出什么?
  2. myCar.proto === Car.prototype 输出什么?
  3. Car.prototype.proto === ? // 填写完整等式