JavaScript原型链与常见继承模式详解

作者:代码拾荒时间:2026-10-25 16:29:53浏览:129

本文详细解析JavaScript原型链机制,涵盖隐式原型与显示原型的指向关系。系统介绍盗用构造函数、组合继承、原型式、寄生式及寄生组合式等常见继承模式,分析各自优缺点及适用场景,帮助开发者掌握对象创建与属性共享的核心原理。

原型链

原型链的概念

在JavaScript中,每个构造函数都拥有一个原型对象,该原型对象包含一个名为constructor的属性,它指向构造函数本身。由该构造函数创建的实例对象,拥有一个隐式原型属性(即__proto__,在部分浏览器中显示为[[Prototype]]),该指针指向构造函数的原型对象。若构造函数的原型对象本身是另一个构造函数的实例,则其原型对象也会通过指针指向该另一个构造函数的原型,如此层层递进,便构成了原型链。值得注意的是,所有未经过特殊重写的函数均由Function实例化而来,除函数外的所有对象均由Object实例化而来。其中,Object本身也是由Function实例化而来,但Object.prototype.__proto__ === null这一判断依然成立。

原型链

需要明确的是,原型链的查找机制是沿着对象的隐式原型逐层向上搜索,直至找到构造函数所创造的实例属性。示例如下:

原型链

上述示例相当于由Student构造函数new出的实例s。当访问其自身的name属性时,若自身未定义,则沿着原型链向上查找,最终在Student的prototype属性中定位到name属性。

原型链

在红框标记的代码(寄生继承的关键部分)中,替换了原本注释掉的代码。此时,实例s无法找到name属性。原因是红框内的代码仅将Student的隐式原型指向了Person的显示原型对象,并未创建任何实际实例,因此不存在具体的属性实例。

原型链的问题

原型链主要存在两个问题:首先,当原型中包含引用值(如数组)时,所有处于该原型链上的实例将共享该属性,导致修改一个实例会影响其他所有实例,即“一发而动全身”。其次,子类在实例化时,无法向父类型的构造函数传递参数。

无法在不影响所有对象实例的情况下把参数传递进父类型的构造函数传参

几种常见的继承方法

盗用构造函数

function SuperType() {
    this.friends = ['张三','李四']
}

function SubType() {
    SuperType.call(this);
}

const p1 = new SubType();
p1.friends.push('王武');

const p2 = new SubType();
console.log(p2.friends); // ['张三','李四', '王武']

盗用构造函数实现继承的核心在于:在子类构造函数内部调用父类构造函数。由于函数本质上是可在特定上下文中执行代码的对象,因此可以利用call()方法,以新创建的对象为上下文执行构造函数,从而实现属性继承。

盗用构造函数的主要缺陷与构造函数模式自定义类型的问题一致:必须在构造函数内部定义方法,这会导致内存浪费。此外,子类无法访问父类原型上定义的方法,因此盗用构造函数通常不单独使用。

组合继承

组合继承,亦称伪经典继承,结合了原型链和构造函数的优点。其基本思路是:利用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。这样既实现了方法的复用(定义在原型上),又保证了每个实例拥有独立的属性。

function SuperType(name) {
    this.name = name;
    this.friends = ['张三','李四'];
}
SuperType.prototype.sayName = function() {
    console.log(this.name)
}
// 继承方法
SubType.prototype = new SuperType();
function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
const p1 = new SubType('赵六', 12);
const p2 = new SubType('赵六2', 22);
// 创建的 p1 和 p2 能够拥有自己的属性并且引用值属性也是独立的,此外,每一个实例能够公用父类的方法。

组合继承已接近完美,但存在调用两次父类构造函数的问题。本质上,子类型最终包含超类对象的所有实例属性,子类构造函数只需在执行时重写自己的原型即可,这为减少一次父类构造函数调用提供了优化思路。

原型式继承

const person = {
    name: 'zs',
    friends: ['ls','ww']
}
// 创造出一个实例,这个实例的隐式原型指向 person
const anotherPerosn = Object.create(person);
anotherPerosn.name = 'xm'
anotherPerosn.friends.push('zl')

console.log(anotherPerosn.name) // xm
console.log(anotherPerosn.friends) // ['ls','ww', 'zl'];

const anotherPerosn2 = Object.create(person);
anotherPerosn.name = 'xh'
anotherPerosn.friends.push('dd')

console.log(anotherPerosn2.name) // xh
console.log(anotherPerosn2.friends) // ['ls','ww', 'zl', 'dd'];

对于原型链继承机制,前文已做详细解释,此处不再赘述。

寄生式继承

寄生式继承与原型式继承相似,同样存在属性引用值共享的问题。

function createAnotherPerson(original) {
    const clone = Object.create(original); //通过调用函数创建一个新的对象
    clone.sayHi = function() { // 以某种方式增强这个对象
        console.log('Hi');
    }
    return clone;
}

此外,寄生式继承中函数无法进行复用,这也是其局限性之一。

寄生组合式继承

// 实现了寄生式组合继承的核心逻辑
function inheritPrototype(subFn, parentFn){
    subFn.prototype = Object.create(parentFn.prototype); // 创建赋值对象
    Object.defineProperty(subFn.prototype,'constructor', {   // 增强对象
        enumerable: false,
        writable: false,
        configurable: false,
        value: subFn,
    })
}
function Person(name, age, address) {
    this.name = name;
    this.age = age;
    this.address = address;
}
Person.prototype.eating = function() {
        console.log(this.name + "正在吃饭");
} // 共享方法

function Student(name, age, address, sno) {
    Person.call(this, name, age, address); // 绑定 this 确保创建出来的对象是相互独立的
    this.sno = sno;
    this.studing = function() {
        console.log(`${this.name}正在学习`)
    }
}

function Teacher(name, age, address, tno) {
    Person.call(this, name, age, address)
    this.tno = tno;
}

寄生组合式继承(结合构造函数与原型链)完美解决了前述各种继承方式存在的问题,是目前最理想的继承模式。

至此,关于JavaScript原型链及常见继承方法的介绍完毕。如需了解更多JS原型链相关内容,建议搜索本站历史文章或继续浏览后续相关文章,感谢支持!

作者最新文章