JavaScript的原型链-JS实现继承的底层机制

目录

什么是原型链?

‘prototype’属性:

‘__proto__’属性:

prototype和__proto__的区别

原型链的形成

完整的原型链结构代码示例

属性屏蔽规则

“继承”与“独立”的分界线和“赋值 ≠ 修改原型”的直觉偏差

“读写分离”设计的钥匙

原型链的实际应用

new的实现

实现继承的四种方式

原型链继承

构造函数继承

组合继承

寄生组合继承

什么是原型链?

对于如何是原型链,我们要先了解原型链的“钥匙”——‘prototype’和‘__proto__’,对于这两个属性需要知道该用在哪里,而不是随便乱用。

‘prototype’属性:

‘prototype’是函数(Function)对象特有的一个属性。当你创建一个函数时,JavaScript 引擎会自动为该函数添加一个 prototype 属性,这个属性指向一个对象,我们称之为“原型对象”。

这个原型对象有一个特殊的属性constructor,它指回函数本身,形成一个循环引用。

function Person(name) { this.name = name; } console.log(Person.prototype); // 输出:{constructor: ƒ} console.log(Person.prototype.constructor === Person); // 输出:true

核心作用:当使用new关键字调用构造函数创建实例时,该实例的内部[[Prototype]](可通过__proto__访问)会指向构造函数的prototype对象。

const person1 = new Person('Alice'); console.log(person1.__proto__ === Person.prototype); // 输出:true

因此,prototype属性的主要用途是为通过该构造函数创建的所有实例提供一个共享属性和方法的“路径”。

‘__proto__’属性:

‘__proto__’是对象(Object)实例的一个属性(在 ES6 中被标准化,但更推荐使用Object.getPrototypeOf()方法)。它指向该对象的原型,即其构造函数的prototype属性所指向的那个对象——原型对象。

每个 JavaScript 对象(除null外)在创建时都会关联另一个对象,这个被关联的对象就是原型。对象可以从其原型继承属性和方法。

const arr = [1, 2, 3]; console.log(arr.__proto__ === Array.prototype); // 输出:true console.log(arr.__proto__.__proto__ === Object.prototype); // 输出:true console.log(arr.__proto__.__proto__.__proto__); // 输出:null

核心作用:‘__proto__’ 构成了原型链查找的链接。当访问一个对象的属性时,如果该对象自身没有这个属性,JavaScript 引擎就会沿着__proto__指向的原型链向上查找,直到找到该属性或到达链的末端(null)。

prototype和__proto__的区别

  • prototype函数特有的属性
  • __proto__每个对象都有的属性

原型链的形成

原型链正是由__proto__链接起来的一系列对象。‘prototype’ 和 ‘__proto__’ 是理解原型链的关键,通过‘prototype’、 ‘__proto__’和‘constructor’形成如下关系图,其中蓝色链为原型链:

完整的原型链结构代码示例

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); Dog.prototype.constructor = Dog; // 修复constructor指向 Dog.prototype.bark = function() { console.log(`${this.name} is barking`); }; const myDog = new Dog('Buddy', 'Golden Retriever'); // 原型链查找路径: console.log(myDog.__proto__ === Dog.prototype); // true console.log(Dog.prototype.__proto__ === Animal.prototype); // true console.log(Animal.prototype.__proto__ === Object.prototype); // true console.log(Object.prototype.__proto__); // null

属性屏蔽规则

属性屏蔽规则是原型链机制中最核心、最容易被误解的规则之一。它是“继承”与“独立”的分界线、它解释了“赋值 ≠ 修改原型”的直觉偏差、它是理解“读写分离”设计的钥匙。

“继承”与“独立”的分界线和“赋值 ≠ 修改原型”的直觉偏差

原型链让对象可以共享行为(方法),但属性屏蔽确保每个实例可以拥有自己的状态(数据)。没有屏蔽规则,所有实例都会共享所有数据,那将是一场灾难。

// 定义一个父对象 const parent = { name: 'Parent', age: 30 }; // 创建一个子对象,它的原型指向 parent const child = Object.create(parent); console.log('赋值前 child.name:', child.name); // 'Parent' (来自原型) child.name = 'Child'; child.play = 'basketball' console.log('赋值后 child.name:', child.name); // 'Child' (来自自身) console.log('parent.name 未改变:', parent.name); // 'Parent' (原型未被修改) console.log('创建属性后 child.play:', child.play); // 'basketball' console.log('创建属性后 parent.play:', parent.play); // undefined (不会被子对象影响)

“读写分离”设计的钥匙

可以理解为你写操作拿的是“私有钥匙”(开自己的门),读操作拿的是“公共钥匙”(能看祖先的房间,但不能乱动)。

let proto = { _count: 0, number: 0, get count() { return this._count; }, set count(v) { this._count = v; } }; let obj = Object.create(proto); obj.count = 10; // 远程遥控钥匙,触发了 setter obj.number = 10; // 因为proto.number为普通数据属性,所有会在obj中创建新的属性 console.log(obj.count); // 10(读走链,getter 返回了原型闭包里的值) console.log(obj.number); // 0 (原型没变) console.log(obj.hasOwnProperty('count')); // false(没有装新锁!) console.log(obj.hasOwnProperty('number')); true (屏蔽了原型) console.log(proto._count); // 10(原型的数据被遥控改了)

原型链的实际应用

new的实现

new是 JavaScript 中用于创建实例对象的关键字,在执行过程也是用到了原型链。以下是其完成的四个步骤:

  1. 创建一个新的对象obj
  2. 将对象与构建函数通过原型链连接起来
  3. 将构建函数中的this绑定到新建的对象obj上
  4. 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
function myNew(Func,...agrs){ //1.创建一个新对象 const obj = {}; //2.新对象原型指向构造函数原型对象 obj.__proto__ = Func.prototype //3.将构建函数的this指向新对象 let result = Func.apply(obj,args) //4.根据返回值判断 return result instanceof Object ? result : obj

实现继承的四种方式

原型链继承
function Parent(name) { this.name = name; } function Child(age) { this.age = age; } // 关键:让子类原型指向父类实例 Child.prototype = new Parent(); // 修复constructor指向 Child.prototype.constructor = Child;

问题:

  • 引用类型属性被所有实例共享
  • 无法向父类构造函数传参
构造函数继承
fucntion Person(name){ this.name = name } Person.prototype.getName = function(){ return this.name } function Student(){ Person.apply(this,arguments) } const student = new Student('Back_kk') console.log(student) //Back_kk

问题:

  • 无法继承父类原型上的方法
  • 方法都在构造函数中定义,每次创建实例都会生成新函数(浪费内存)
组合继承
function Person(name) {​ this.name = name;​ }​ Person.prototype.getName = function() {​ return this.name;​ }​ function Student() {​ // 构造函数继承​ Person.apply(this, arguments)​ }​ // 原型式继承​ Student.prototype = new Person();​ ​ // 原型的实例等于自身​ Student.prototype.constructor = Student;​ const student = new Student('Back_kk');​ console.log(student.name); // Back_kk​ console.log(student.getName()); // Back_kk

问题:

  • 调用了两次父类构造函数(Person.apply + new Person)
  • 子类实例和原型上都有父类属性,造成浪费
寄生组合继承
function Person(name) {​ this.name = name;​ }​ Person.prototype.getName = function() {​ return this.name;​ }​ function Student() {​ // 构造函数继承​ Person.apply(this, arguments)​ }​ // 原型式继承​ // Student.prototype = new Person();​ Student.prototype = Object.create(Person.prototype);​​ // 原型的实例等于自身​ Student.prototype.constructor = Student;​ ​ const student = new Student('Back_kk');​ console.log(student.name); // Back_kk​ console.log(student.getName()); // Back_kk

优点:

  • 只调用一次父类构造函数
  • 原型链干净,没有多余的实例属性
  • 所有实例共享原型方法
  • 引用类型属性互不干扰