JavaScript原型链与继承:优化与扩展的深度探索

news2025/2/4 23:50:15

在 JavaScript 的世界里,万物皆对象,而每个对象都有一个与之关联的原型对象,这就构成了原型链的基础。原型链,简单来说,是一个由对象的原型相互连接形成的链式结构 。每个对象都有一个内部属性[[Prototype]](在大多数浏览器中可以通过__proto__属性访问,不过__proto__是非标准属性,更推荐使用Object.getPrototypeOf()方法来获取原型),它指向该对象的原型对象。

以一个简单的例子来说明,我们创建一个构造函数Person:

function Person(name) {

this.name = name;

}

Person.prototype.sayName = function() {

console.log('My name is'+ this.name);

};

let person1 = new Person('Alice');

在这个例子中,person1是Person构造函数创建的实例对象。person1的__proto__属性指向Person.prototype,而Person.prototype也是一个对象,它同样有自己的__proto__属性,指向Object.prototype,Object.prototype的__proto__则为null,这就形成了一条完整的原型链:person1 -> Person.prototype -> Object.prototype -> null。

当我们访问person1的属性或方法时,比如调用person1.sayName(),JavaScript 引擎会首先在person1自身上查找是否有sayName方法。由于person1自身并没有定义sayName方法,引擎就会沿着原型链向上查找,在Person.prototype中找到了sayName方法,于是就执行该方法。如果在Person.prototype中也没有找到,就会继续向上在Object.prototype中查找,直到找到该属性或方法,或者到达原型链的顶端(null)。如果一直到原型链顶端都没有找到,就会返回undefined。

继承机制基础

JavaScript 基于原型链的继承机制是其实现代码复用和对象层次化结构的核心方式。简单来说,通过将一个对象的原型设置为另一个对象,新对象就可以继承原型对象的属性和方法。

继续以上面的Person构造函数为例,我们创建一个新的构造函数Student,让Student继承Person:

function Student(name, grade) {

Person.call(this, name);

this.grade = grade;

}

Student.prototype = Object.create(Person.prototype);

Student.prototype.constructor = Student;

Student.prototype.sayGrade = function() {

console.log('My grade is'+ this.grade);

};

let student1 = new Student('Bob', 10);

在这段代码中,首先在Student构造函数内部通过Person.call(this, name)调用了Person构造函数,这一步的作用是让Student实例能够继承Person构造函数中定义的属性,比如name。然后,通过Student.prototype = Object.create(Person.prototype)将Student.prototype的原型设置为Person.prototype,这样Student的实例就可以继承Person.prototype上的属性和方法,比如sayName方法。最后,重新设置Student.prototype.constructor为Student,以确保构造函数的指向正确。

通过这样的方式,student1既拥有自己特有的属性grade和方法sayGrade,又继承了Person的属性name和方法sayName,实现了对象间的属性和方法继承,充分体现了 JavaScript 基于原型链继承机制的灵活性和强大之处。

现有继承方式剖析

原型链继承

原型链继承是 JavaScript 中最基本的继承方式,它通过将子类的原型指向父类的实例,从而实现子类对父类属性和方法的继承。下面是一个简单的示例:

function Animal(name) {

this.name = name;

}

Animal.prototype.speak = function() {

console.log(this.name +'makes a sound.');

};

function Dog(name, breed) {

this.breed = breed;

this.name = name;

}

Dog.prototype = new Animal();

Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {

console.log(this.name +'barks.');

};

let myDog = new Dog('Buddy', 'Golden Retriever');

在这个例子中,Dog.prototype = new Animal();这行代码将Dog的原型设置为Animal的一个实例,这样Dog的实例就可以通过原型链访问到Animal原型上的属性和方法,比如speak方法。

优点

  • 简单直观:实现方式简单,易于理解,通过原型链的机制,自然地实现了属性和方法的继承。
  • 共享方法:父类原型上的方法可以被所有子类实例共享,节省内存空间,提高代码复用性。例如,多个Dog实例都可以调用speak方法,而不需要在每个实例中都创建该方法的副本。

缺点

  • 引用类型属性共享问题:由于子类实例共享父类原型上的属性,对于引用类型的属性,一个子类实例对其进行修改,会影响到其他子类实例。比如,如果Animal原型上有一个friends属性,是一个数组,当一个Dog实例向friends数组中添加元素时,其他Dog实例的friends数组也会发生变化。
  • 无法向父类构造函数传参:在创建子类实例时,无法直接向父类构造函数传递参数,这在很多情况下会限制代码的灵活性。例如,我们无法在创建Dog实例时,直接为Animal构造函数中的name属性赋值。

借用构造函数继承

借用构造函数继承,也称为经典继承,是通过在子类构造函数中使用call或apply方法调用父类构造函数,从而实现子类对父类实例属性的继承。示例如下:

function Animal(name) {

this.name = name;

this.species = 'Animal';

}

function Dog(name, breed) {

Animal.call(this, name);

this.breed = breed;

}

let myDog = new Dog('Max', 'Poodle');

在上述代码中,Animal.call(this, name);这行代码在Dog构造函数的作用域内调用了Animal构造函数,使得Dog实例拥有了Animal构造函数中定义的属性,如name和species。

优点

  • 解决引用类型属性共享问题:每个子类实例都有自己独立的属性副本,不会出现引用类型属性共享导致的相互影响问题。例如,每个Dog实例都有自己独立的name和breed属性,一个Dog实例修改自己的属性,不会影响其他Dog实例。
  • 可以向父类构造函数传参:在创建子类实例时,可以方便地向父类构造函数传递参数,灵活地初始化父类属性。比如在创建Dog实例时,可以直接为Animal构造函数中的name属性传值。

缺点

  • 无法继承父类原型方法:只能继承父类构造函数中的属性和方法,无法继承父类原型对象上的方法。例如,Animal原型上定义的方法,Dog实例无法直接访问和调用。
  • 方法无法复用:由于方法是在构造函数中定义的,每次创建子类实例时,都会重新创建一遍方法,造成内存浪费,降低了代码的复用性。

组合式继承

组合式继承结合了原型链继承和借用构造函数继承的优点,通过原型链继承父类的原型属性和方法,通过借用构造函数继承父类的实例属性。示例如下:

function Animal(name) {

this.name = name;

this.colors = ['black', 'white'];

}

Animal.prototype.speak = function() {

console.log(this.name +'makes a sound.');

};

function Dog(name, breed) {

Animal.call(this, name);

this.breed = breed;

}

Dog.prototype = new Animal();

Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {

console.log(this.name +'barks.');

};

let myDog = new Dog('Cooper', 'Labrador');

在这个例子中,Dog.prototype = new Animal();实现了原型链继承,使得Dog实例可以访问Animal原型上的方法,如speak;Animal.call(this, name);实现了借用构造函数继承,让Dog实例拥有自己独立的name和breed属性。

优点

  • 融合两者优势:既实现了原型方法的复用,又保证了每个实例都有自己独立的属性,避免了原型链继承中引用类型属性共享的问题,也克服了借用构造函数继承中无法继承原型方法的缺陷。例如,Dog实例既可以共享Animal原型上的speak方法,又有自己独立的name、breed和colors属性。

缺点

  • 父类构造函数调用两次:在创建子类实例时,父类构造函数会被调用两次。一次是在设置子类原型时(Dog.prototype = new Animal();),另一次是在子类构造函数内部(Animal.call(this, name);)。这会导致子类实例中存在两份相同的父类实例属性,浪费内存,降低性能。

原型式继承与寄生式继承

原型式继承

原型式继承是基于已有对象创建新对象,通过一个临时构造函数将已有对象作为其原型,然后返回这个临时构造函数的实例,从而实现新对象对已有对象属性和方法的继承。ES5 中通过Object.create()方法规范化了原型式继承。示例如下:

let person = {

name: 'John',

friends: ['Alice', 'Bob']

};

let anotherPerson = Object.create(person, {

name: {

value: 'Jane'

}

});

在这个例子中,anotherPerson通过Object.create(person)创建,它继承了person的属性和方法,并且可以通过第二个参数为新对象定义额外的属性。

优点

  • 简单灵活:不需要定义构造函数,就能快速基于已有对象创建新对象,适用于简单的对象复制和继承场景。

缺点

  • 引用类型属性共享问题:和原型链继承一样,对于引用类型的属性,新对象和原对象会共享该属性,一个对象对其修改会影响另一个对象。例如,anotherPerson和person共享friends数组,anotherPerson.friends.push('Eve')会使person.friends也发生变化。

寄生式继承

寄生式继承是在原型式继承的基础上,通过一个函数对新创建的对象进行增强,添加新的属性或方法,最后返回这个增强后的对象。示例如下:

function createAnother(original) {

let clone = Object.create(original);

clone.sayHi = function() {

console.log('Hi!');

};

return clone;

}

let person = {

name: 'Nicholas',

friends: ['Shelby', 'Court', 'Van']

};

let anotherPerson = createAnother(person);

在这个例子中,createAnother函数通过Object.create(original)创建了一个新对象clone,然后为其添加了sayHi方法,最后返回这个增强后的对象anotherPerson。

优点

  • 增强对象功能:可以在不修改原对象的基础上,为新对象添加特定的属性和方法,增强了对象的功能。

缺点

  • 方法无法复用:和借用构造函数继承类似,每次创建新对象时,添加的方法都是新创建的,无法实现方法的复用,降低了效率。同时,它也存在原型式继承中引用类型属性共享的问题。

寄生组合式继承

寄生组合式继承是对组合式继承的优化,它通过借用构造函数来继承属性,通过原型链来继承方法,但避免了组合式继承中父类构造函数被多次调用的问题。其核心是创建一个仅包含父类原型的副本的对象,然后将子类的原型指向这个副本。示例如下:

function inheritPrototype(subType, superType) {

let prototype = Object.create(superType.prototype);

prototype.constructor = subType;

subType.prototype = prototype;

}

function Animal(name) {

this.name = name;

this.colors = ['black', 'white'];

}

Animal.prototype.speak = function() {

console.log(this.name +'makes a sound.');

};

function Dog(name, breed) {

Animal.call(this, name);

this.breed = breed;

}

inheritPrototype(Dog, Animal);

Dog.prototype.bark = function() {

console.log(this.name +'barks.');

};

let myDog = new Dog('Charlie', 'Bulldog');

在这个例子中,inheritPrototype函数创建了一个Animal.prototype的副本prototype,并将其constructor指向Dog,然后将Dog.prototype指向这个副本。这样,既保证了Dog实例可以继承Animal的属性和方法,又避免了多次调用Animal构造函数。

优点

  • 高效性能:只调用了一次父类构造函数,避免了在子类原型上创建多余的属性,大大提高了性能,减少了内存浪费。
  • 原型链保持完整:原型链保持不变,能够正常使用instanceof和isPrototypeOf等操作符来判断对象的类型和继承关系。

优化策略

减少原型链查找次数

在 JavaScript 中,原型链查找是一个相对耗时的操作,因为每次查找属性或方法时,引擎都需要沿着原型链逐级向上搜索,直到找到目标或到达原型链的顶端。为了提高代码性能,我们可以采取以下几种方式来减少原型链查找次数。

合理设计对象结构:在创建对象时,尽量将常用的属性和方法直接定义在对象自身上,而不是依赖原型链查找。例如,在一个频繁使用的工具函数对象中,如果某个方法被频繁调用,就可以直接将该方法定义在对象实例上:

let utils = {

// 直接定义常用方法

calculateSum: function(a, b) {

return a + b;

}

};

// 直接调用,避免原型链查找

console.log(utils.calculateSum(3, 5));

这样,每次调用calculateSum方法时,JavaScript 引擎可以直接在utils对象自身上找到该方法,而不需要在原型链上进行查找,大大提高了访问效率。

使用闭包缓存属性和方法:利用闭包的特性,将需要频繁访问的属性或方法缓存起来,减少原型链查找的次数。例如,假设有一个对象dataObject,其内部的某个属性dataValue被频繁访问:

function createDataObject() {

let dataValue = 10;

return {

getData: function() {

// 闭包缓存dataValue,避免每次访问都查找原型链

return dataValue;

},

setData: function(newValue) {

dataValue = newValue;

}

};

}

let myData = createDataObject();

console.log(myData.getData());

在这个例子中,getData方法通过闭包访问并缓存了dataValue,每次调用getData时,不需要在原型链上查找dataValue,提高了访问速度。

避免不必要的继承层次

在设计继承结构时,保持继承层次的简洁性至关重要。过深的继承层次会带来诸多问题,如性能开销增大、代码维护困难等。

性能开销方面:随着继承层次的加深,原型链会变长。当访问对象的属性或方法时,JavaScript 引擎需要在更长的原型链上进行查找,这会显著增加查找时间,降低代码的执行效率。例如,在一个复杂的图形绘制库中,如果存在一个从Shape类开始,经过多层继承得到的ComplexShape类,当调用ComplexShape实例的某个方法时,引擎可能需要在包含多个中间原型对象的原型链上进行查找,这无疑会增加性能损耗。

维护困难方面:过多的继承层次会使代码结构变得复杂,难以理解和维护。当需要修改某个基类的属性或方法时,可能会对多个子类产生意想不到的影响,因为这些子类通过继承链与基类紧密相连。例如,在一个企业级应用中,存在一个多层继承的用户权限管理系统,当修改最顶层的User类的权限验证方法时,可能需要仔细检查每个子类的行为,以确保不会破坏整个权限管理逻辑。

为了避免这些问题,在设计继承结构时,应遵循 “简单即美” 的原则,只在必要时使用继承,并且尽量减少继承的层数。如果某些功能可以通过组合(将不同的对象组合在一起,而不是通过继承)来实现,那么组合可能是更好的选择,因为它可以提供更大的灵活性,同时避免了继承带来的复杂性。

利用 ES6 类语法优化继承

ES6 引入的class和extends关键字为 JavaScript 的继承机制带来了更简洁、易读的语法,同时内部也进行了一些优化,使得继承的实现更加高效和直观。

使用classextends实现继承:通过class关键字定义类,使用extends关键字实现继承,代码结构更加清晰。例如,定义一个Animal类作为父类,再定义一个Dog类继承自Animal:

class Animal {

constructor(name) {

this.name = name;

}

speak() {

console.log(this.name +'makes a sound.');

}

}

class Dog extends Animal {

constructor(name, breed) {

super(name);

this.breed = breed;

}

bark() {

console.log(this.name +'barks.');

}

}

let myDog = new Dog('Rex', 'German Shepherd');

myDog.speak();

myDog.bark();

在这段代码中,Dog类通过extends关键字明确地表明它继承自Animal类,super(name)用于调用父类的构造函数,初始化从父类继承的属性。这种语法比传统的基于原型链的继承方式更加直观,易于理解和维护。

ES6 类语法的内部优化:ES6 的类语法在内部实现上进行了一些优化,提高了继承的性能。例如,类的方法在创建时会被预先解析和优化,使得方法调用更加高效。同时,class和extends的实现方式也对原型链的管理进行了优化,减少了不必要的原型链查找和属性复制,从而提升了整体的性能表现。此外,ES6 类语法在错误处理和代码的可读性方面也有很大的提升,使得开发者在编写和调试继承相关的代码时更加轻松。

扩展方法

为原型添加新方法

在 JavaScript 中,为对象原型添加新方法是扩展对象功能的一种常见方式。通过这种方式,我们可以为所有该类型的对象实例添加通用的方法,从而提高代码的复用性和可维护性。然而,在添加新方法时,需要特别注意避免命名冲突,以免覆盖原生方法或其他已有的重要方法。

以String类型为例,假设我们想要为所有字符串对象添加一个reverse方法,用于将字符串反转。可以通过以下方式实现:

if (!String.prototype.reverse) {

String.prototype.reverse = function() {

return this.split('').reverse().join('');

};

}

let str = 'hello';

console.log(str.reverse());

在这段代码中,首先通过if (!String.prototype.reverse)检查String.prototype上是否已经存在reverse方法。如果不存在,才定义新的reverse方法。这样可以确保不会意外地覆盖已有的reverse方法。新定义的reverse方法先使用split('')将字符串拆分成字符数组,然后调用数组的reverse方法反转数组,最后使用join('')将数组重新拼接成字符串。

再比如,为Array原型添加一个sum方法,用于计算数组元素的总和:

if (!Array.prototype.sum) {

Array.prototype.sum = function() {

return this.reduce((acc, num) => acc + num, 0);

};

}

let numbers = [1, 2, 3, 4, 5];

console.log(numbers.sum());

这里同样先检查Array.prototype上是否有sum方法,避免命名冲突。sum方法利用数组的reduce方法,对数组中的每个元素进行累加,初始值为 0,最终返回数组元素的总和。

实现多重继承

JavaScript 本身并不直接支持多重继承,但我们可以通过一些技术手段来模拟实现多重继承的效果,其中比较常用的方法是混入(mixin)模式。混入模式通过将多个对象的属性和方法合并到一个目标对象中,使目标对象能够拥有多个来源的功能。

下面是一个简单的混入函数示例:

function mixin(target,...sources) {

sources.forEach(source => {

for (let key in source) {

if (source.hasOwnProperty(key)) {

target[key] = source[key];

}

}

});

return target;

}

let obj1 = {

method1: function() {

console.log('Method 1');

}

};

let obj2 = {

method2: function() {

console.log('Method 2');

}

};

let targetObj = {};

mixin(targetObj, obj1, obj2);

targetObj.method1();

targetObj.method2();

在这个例子中,mixin函数接受一个目标对象target和多个源对象sources。通过forEach遍历每个源对象,再使用for...in循环遍历源对象的属性。hasOwnProperty方法用于确保只复制源对象自身的属性,而不包括从原型链继承的属性。最后将源对象的属性和方法复制到目标对象中,使目标对象拥有了多个源对象的功能。

在实际应用中,混入模式常用于插件开发、组件开发等场景。例如,在一个前端组件库中,可能有多个不同功能的模块,通过混入模式可以将这些模块的功能组合到一个组件中,实现组件的功能扩展。比如,一个基础的表单组件可能只包含基本的表单元素和验证功能,通过混入其他模块的属性和方法,可以为表单组件添加数据提交、实时校验提示等更多功能。

基于继承实现设计模式

JavaScript 的继承机制为实现各种设计模式提供了有力的支持。通过合理运用继承,我们可以创建出结构清晰、可维护性高且具有良好扩展性的代码。以下介绍如何利用继承机制实现工厂模式和单例模式。

工厂模式

工厂模式是一种创建对象的设计模式,它将对象的创建和使用分离,通过一个工厂函数或类来负责创建对象,使得代码的依赖关系更加清晰,也便于代码的维护和扩展。

使用函数实现简单的工厂模式:

function ShapeFactory(type) {

switch (type) {

case 'circle':

return {

draw: function() {

console.log('Drawing a circle');

}

};

case'rectangle':

return {

draw: function() {

console.log('Drawing a rectangle');

}

};

default:

return null;

}

}

let circle = ShapeFactory('circle');

circle.draw();

在这个例子中,ShapeFactory函数根据传入的参数type创建不同类型的形状对象。每个形状对象都有一个draw方法,用于绘制相应的形状。通过这种方式,我们可以将形状对象的创建逻辑封装在工厂函数中,使用者只需要调用工厂函数并传入所需的参数,就可以获取到相应的形状对象,而无需关心对象的具体创建过程。

单例模式

单例模式确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。在 JavaScript 中,可以通过闭包和原型链来实现单例模式。

let Singleton = (function() {

let instance;

function MySingleton() {

// 私有属性和方法

let privateProperty = 'This is a private property';

function privateMethod() {

console.log('This is a private method');

}

// 公共属性和方法

this.publicProperty = 'This is a public property';

this.publicMethod = function() {

console.log('This is a public method');

privateMethod();

console.log(privateProperty);

};

}

return {

getInstance: function() {

if (!instance) {

instance = new MySingleton();

}

return instance;

}

};

})();

let singleton1 = Singleton.getInstance();

let singleton2 = Singleton.getInstance();

console.log(singleton1 === singleton2);

singleton1.publicMethod();

在这段代码中,通过立即执行函数表达式(IIFE)创建了一个闭包。在闭包内部,定义了一个MySingleton构造函数,它包含私有属性和方法以及公共属性和方法。getInstance方法用于获取单例实例,如果实例不存在,则创建一个新的MySingleton实例并返回;如果实例已存在,直接返回已有的实例。这样就确保了整个应用中只有一个MySingleton实例。通过这种方式实现的单例模式,不仅保证了实例的唯一性,还可以隐藏内部实现细节,只对外暴露必要的公共接口,提高了代码的安全性和可维护性。

实践案例分析

大型项目中的原型链与继承优化

在一个大型的电商前端项目中,商品展示和购物车功能是核心部分。为了实现高效的代码管理和性能优化,充分运用了原型链和继承机制。

在商品展示模块,定义了一个基础的Product类,包含商品的基本属性和方法,如商品名称、价格、图片路径以及获取商品信息的方法。代码如下:

class Product {

constructor(name, price, imageUrl) {

this.name = name;

this.price = price;

this.imageUrl = imageUrl;

}

getProductInfo() {

return `Name: ${this.name}, Price: ${this.price}`;

}

}

然后,针对不同类型的商品,如电子产品、服装等,创建了各自的子类,继承自Product类,并添加了特定的属性和方法。以ElectronicProduct类为例:

class ElectronicProduct extends Product {

constructor(name, price, imageUrl, brand, model) {

super(name, price, imageUrl);

this.brand = brand;

this.model = model;

}

getProductInfo() {

return `${super.getProductInfo()}, Brand: ${this.brand}, Model: ${this.model}`;

}

}

通过这种继承方式,代码结构更加清晰,不同类型商品的共性部分在Product类中实现,减少了重复代码。同时,利用 ES6 类语法的优化,提高了代码的执行效率。

在购物车功能中,为了提高性能,减少原型链查找次数,将一些常用的方法直接定义在购物车对象实例上。例如,计算购物车总价的方法:

class Cart {

constructor() {

this.items = [];

// 直接在实例上定义计算总价的方法

this.calculateTotal = function() {

return this.items.reduce((total, item) => total + item.price, 0);

};

}

addItem(product) {

this.items.push(product);

}

}

这样,每次调用calculateTotal方法时,无需在原型链上查找,直接在实例上就能找到该方法,大大提高了计算效率,尤其是在购物车中商品数量较多的情况下,性能提升更为明显。

常见错误及解决方案

在使用原型链和继承机制时,常常会遇到一些错误,下面列举几个常见的错误及相应的解决方案。

原型对象修改不当:在修改原型对象时,如果不注意,可能会导致意外的结果。例如,在创建实例后修改原型对象的属性,可能会影响到已经创建的实例。

function Person() {}

Person.prototype.name = 'Default Name';

let person1 = new Person();

Person.prototype.name = 'New Name';

let person2 = new Person();

console.log(person1.name);

console.log(person2.name);

在这个例子中,person1创建时,Person.prototype.name的值为Default Name,虽然之后修改了Person.prototype.name的值为New Name,但person1仍然保留着原来的值。这是因为实例在创建时,会获取原型对象的一份 “快照”,之后原型对象的修改不会影响到已经创建的实例。

解决方案:如果需要修改原型对象的属性,并且希望所有实例都能反映出这个修改,最好在创建任何实例之前进行修改。如果在创建实例后必须修改原型对象,可以通过重新定义属性的方式,使其具有可配置性,从而影响到所有实例。例如:

function Person() {}

Person.prototype.name = 'Default Name';

let person1 = new Person();

Object.defineProperty(Person.prototype, 'name', {

value: 'New Name',

writable: true,

enumerable: true,

configurable: true

});

let person2 = new Person();

console.log(person1.name);

console.log(person2.name);

通过Object.defineProperty重新定义name属性,并设置configurable: true,这样修改后的属性会影响到所有实例。

继承方式选择错误:在不同的场景下,选择不合适的继承方式会导致代码出现问题。例如,在需要共享原型方法的情况下,使用了借用构造函数继承,就会导致无法继承原型方法。

function Animal(name) {

this.name = name;

}

Animal.prototype.speak = function() {

console.log(this.name +'makes a sound.');

};

function Dog(name, breed) {

Animal.call(this, name);

this.breed = breed;

}

let myDog = new Dog('Max', 'Poodle');

myDog.speak();

在这个例子中,Dog通过借用构造函数继承了Animal的实例属性,但无法继承Animal原型上的speak方法,所以调用myDog.speak()会报错。

解决方案:根据具体需求选择合适的继承方式。如果需要继承原型方法,应使用原型链继承、组合继承或 ES6 类继承。例如,使用 ES6 类继承可以解决上述问题:

class Animal {

constructor(name) {

this.name = name;

}

speak() {

console.log(this.name +'makes a sound.');

}

}

class Dog extends Animal {

constructor(name, breed) {

super(name);

this.breed = breed;

}

}

let myDog = new Dog('Max', 'Poodle');

myDog.speak();

通过class和extends关键字实现继承,Dog实例可以正确继承Animal原型上的speak方法,避免了继承方式选择错误带来的问题。

最后小总结

在 JavaScript 的世界里,原型链和继承机制是其面向对象编程的核心支柱。通过对原型链的深入理解,我们明晰了对象属性和方法的查找路径,它就像一条无形的纽带,将对象与原型紧密相连,构建起了对象之间的层次关系。而多种继承方式的存在,为我们在不同的开发场景中提供了灵活的选择,每种继承方式都有其独特的优缺点,从原型链继承的简单直观,到寄生组合式继承的高效优化,我们需要根据项目的具体需求来精心挑选,以实现代码的最佳性能和可维护性。

在优化策略方面,减少原型链查找次数和避免不必要的继承层次,能够显著提升代码的执行效率,让我们的程序运行得更加流畅。而 ES6 类语法的出现,不仅为继承带来了更简洁、优雅的表达方式,还在内部实现上进行了优化,使得开发过程更加高效和便捷。

在扩展方法上,为原型添加新方法,为我们定制对象的功能提供了便利,让我们能够根据实际需求,为对象赋予更多的能力。实现多重继承的混入模式,突破了 JavaScript 原生不支持多重继承的限制,为我们构建复杂的对象结构提供了新的思路。基于继承实现的工厂模式和单例模式,更是将继承机制与设计模式相结合,展现了 JavaScript 强大的编程能力,使得我们能够创建出结构清晰、可维护性高的代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2292035.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Med-R2:基于循证医学的检索推理框架:提升大语言模型医疗问答能力的新方法

Med-R2 : Crafting Trustworthy LLM Physicians through Retrieval and Reasoning of Evidence-Based Medicine Med-R2框架Why - 这个研究要解决什么现实问题What - 核心发现或论点是什么How - 1. 前人研究的局限性How - 2. 你的创新方法/视角How - 3. 关键数据支持How - 4. 可…

bypass hcaptcha、hcaptcha逆向

可以过steam,已支持并发,欢迎询问! 有事危,ProfessorLuoMing

python-UnitTest框架笔记

UnitTest框架的基本使用方法 UnitTest框架介绍 框架:framework,为了解决一类事情的功能集合 UnitTest框架:是python自带的单元测试框架 自带的,可以直接使用,不需要格外安装 测试人员用来做自动化测试,作…

掌握API和控制点(从Java到JNI接口)_35 JNI开发与NDK 03

3、 如何载入 .so档案 VM的角色 由于Android的应用层级类别都是以Java撰写的,这些Java类别转译为Dex型式的Bytecode之后,必须仰赖Dalvik虚拟机器(VM: Virtual Machine)来执行之。 VM在Android平台里,扮演很重要的角色。此外,在执…

CDDIS从2025年2月开始数据迁移

CDDIS 将从 2025 年 2 月开始将我们的网站从 cddis.nasa.gov 迁移到 earthdata.nasa.gov,并于 2025 年 6 月结束。 期间可能对GAMIT联网数据下载造成影响。

VSCode设置内容字体大小

1、打开VSCode软件,点击左下角的“图标”,选择“Setting”。 在命令面板中的Font Size处选择适合自己的字体大小。 2、对比Font Size值为14与20下的字体大小。

嵌入式学习---蜂鸣器篇

1. 蜂鸣器分类 蜂鸣器是一种电子发声器件,采用直流电压供电,能够发出声音。广泛应用于计算机、打印机、报警器、电子玩具等电子产品中作为发声部件。一般仅从外形不易分辨蜂鸣器的种类。但是有些蜂鸣器使用广泛,见得多了就很容易分辨。例如常…

【优先算法】专题——前缀和

目录 一、【模版】前缀和 参考代码: 二、【模版】 二维前缀和 参考代码: 三、寻找数组的中心下标 参考代码: 四、除自身以外数组的乘积 参考代码: 五、和为K的子数组 参考代码: 六、和可被K整除的子数组 参…

【Linux】使用管道实现一个简易版本的进程池

文章目录 使用管道实现一个简易版本的进程池流程图代码makefileTask.hppProcessPool.cc 程序流程: 使用管道实现一个简易版本的进程池 流程图 代码 makefile ProcessPool:ProcessPool.ccg -o $ $^ -g -stdc11 .PHONY:clean clean:rm -f ProcessPoolTask.hpp #pr…

找不到msvcp140.dll解决方法

您可以尝试以下方案进行修复,看看是否可以解决这个问题: 一、重新注册 msvcp140.dll 运行库文件: “WinR”打开运行,键入:regsvr32 MSVCP140.dll,回车即可; 如果出现找不到该文件的提示&…

【优先算法】专题——位运算

在讲解位运算之前我们来总结一下常见的位运算 一、常见的位运算 1.基础为运算 << &&#xff1a;有0就是0 >> |&#xff1a;有1就是1 ~ ^&#xff1a;相同为0&#xff0c;相异位1 /无进位相加 2.给一个数 n&#xff0c;确定它的二进制表示…

【Cadence仿真技巧学习笔记】求解65nm库晶体管参数un, e0, Cox

在设计放大器的第一步就是确定好晶体管参数和直流工作点的选取。通过阅读文献&#xff0c;我了解到L波段低噪声放大器的mos器件最优宽度计算公式为 W o p t . p 3 2 1 ω L C o x R s Q s p W_{opt.p}\frac{3}{2}\frac{1}{\omega LC_{ox}R_{s}Q_{sp}} Wopt.p​23​ωLCox​Rs…

Docker入门篇(Docker基础概念与Linux安装教程)

目录 一、什么是Docker、有什么作用 二、Docker与虚拟机(对比) 三、Docker基础概念 四、CentOS安装Docker 一、从零认识Docker、有什么作用 1.项目部署可能的问题&#xff1a; 大型项目组件较多&#xff0c;运行环境也较为复杂&#xff0c;部署时会碰到一些问题&#xff1…

开源智慧园区管理系统对比其他十种管理软件的优势与应用前景分析

内容概要 在当今数字化快速发展的时代&#xff0c;园区管理软件的选择显得尤为重要。而开源智慧园区管理系统凭借其独特的优势&#xff0c;逐渐成为用户的新宠。与传统管理软件相比&#xff0c;它不仅灵活性高&#xff0c;而且具有更强的可定制性&#xff0c;让各类园区&#…

【C++】P5734 【深基6.例6】文字处理软件

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;题目描述&#x1f4af;题目描述输入格式输出格式示例输入与输出输入&#xff1a;输出&#xff1a; &#x1f4af;我的做法操作1&#xff1a;在文档末尾插入字符串操作2&…

CSS核心

CSS的引入方式 内部样式表是在 html 页面内部写一个 style 标签&#xff0c;在标签内部编写 CSS 代码控制整个 HTML 页面的样式。<style> 标签理论上可以放在 HTML 文档的任何地方&#xff0c;但一般会放在文档的 <head> 标签中。 <style> div { color: r…

013-51单片机红外遥控器模拟控制空调,自动制冷制热定时开关

主要功能是通过红外遥控器模拟控制空调&#xff0c;可以实现根据环境温度制冷和制热&#xff0c;能够通过遥控器设定温度&#xff0c;可以定时开关空调。 1.硬件介绍 硬件是我自己设计的一个通用的51单片机开发平台&#xff0c;可以根据需要自行焊接模块&#xff0c;这是用立创…

CMake项目编译与开源项目目录结构

Cmake 使用简单方便&#xff0c;可以跨平台构建项目编译环境&#xff0c;尤其比直接写makefile简单&#xff0c;可以通过简单的Cmake生成负责的Makefile文件。 如果没有使用cmake进行编译&#xff0c;需要如下命令&#xff1a;&#xff08;以muduo库echo服务器为例&#xff09;…

OPENPPP2 —— VMUX_NET 多路复用原理剖析

在阅读本文之前&#xff0c;必先了解以下几个概念&#xff1a; 1、MUX&#xff08;Multiplexer&#xff09;&#xff1a;合并多个信号到单一通道。 2、DEMUX&#xff08;Demultiplexer&#xff09;&#xff1a;从单一通道分离出多个信号。 3、单一通道&#xff0c;可汇聚多个…

语言月赛 202412【正在联系教练退赛】题解(AC)

》》》点我查看「视频」详解》》》 [语言月赛 202412] 正在联系教练退赛 题目背景 在本题中&#xff0c;我们称一个字符串 y y y 是一个字符串 x x x 的子串&#xff0c;当且仅当从 x x x 的开头和结尾删去若干个&#xff08;可以为 0 0 0 个&#xff09;字符后剩余的字…