MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript子类在不同场景的表现

2022-06-193.5k 阅读

JavaScript 子类在继承中的表现

基于原型链的继承

在 JavaScript 中,原型链是实现继承的重要机制。当一个对象尝试访问某个属性时,如果自身没有该属性,就会沿着原型链向上查找。通过构造函数和 prototype 属性,我们可以创建具有继承关系的子类。

// 定义父类构造函数
function Animal(name) {
    this.name = name;
}

// 为父类添加方法
Animal.prototype.speak = function() {
    console.log(this.name +'makes a sound.');
};

// 定义子类构造函数
function Dog(name, breed) {
    // 调用父类构造函数,绑定 this
    Animal.call(this, name);
    this.breed = breed;
}

// 设置子类的原型为父类的实例,建立原型链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 为子类添加特有方法
Dog.prototype.bark = function() {
    console.log(this.name +'barks.');
};

// 创建子类实例
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // 输出: Buddy makes a sound.
myDog.bark(); // 输出: Buddy barks.

在上述代码中,DogAnimal 的子类。通过 Animal.call(this, name),我们在 Dog 实例中调用 Animal 构造函数,从而让 Dog 实例拥有 Animal 构造函数定义的属性。通过 Dog.prototype = Object.create(Animal.prototype),我们建立了 DogAnimal 之间的原型链关系,使得 Dog 实例可以访问 Animal.prototype 上的方法。

ES6 类继承

ES6 引入了 class 关键字,让 JavaScript 的继承语法更加简洁明了,但其底层实现仍然基于原型链。

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.');
    }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // 输出: Buddy makes a sound.
myDog.bark(); // 输出: Buddy barks.

在 ES6 类继承中,extends 关键字用于声明 Dog 类继承自 Animal 类。在 Dog 类的构造函数中,super(name) 用于调用父类的构造函数,确保父类的初始化逻辑得以执行。这种方式在代码可读性和维护性上有很大提升,同时也遵循了原型链继承的本质。

JavaScript 子类在对象创建与实例化场景的表现

子类实例化过程中的属性初始化

无论是基于原型链的继承还是 ES6 类继承,子类实例化时属性的初始化都有特定的顺序和方式。

在基于原型链的继承中,如前文的 Dog 子类:

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

首先调用 Animal.call(this, name),将 Animal 构造函数中的 name 属性初始化到 Dog 实例中,然后再初始化 Dog 特有的 breed 属性。

在 ES6 类继承中:

class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
}

同样,先通过 super(name) 调用父类构造函数初始化 name 属性,再初始化 breed 属性。这种初始化顺序保证了子类实例能够正确拥有从父类继承的属性以及自身特有的属性。

子类实例的原型属性访问

当子类实例访问属性时,首先在自身查找,如果找不到则沿着原型链向上查找。

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.');
    }
}

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

// 访问自身属性
console.log(myDog.breed); // 输出: Golden Retriever

// 访问原型链上的属性(父类方法)
myDog.speak(); // 输出: Buddy makes a sound.

// 访问原型链上不存在的属性
console.log(myDog.run); // 输出: undefined

在上述代码中,myDog 实例首先尝试在自身查找 breed 属性,找到了并输出其值。当调用 speak 方法时,自身没有该方法,于是沿着原型链找到 Animal.prototype 上的 speak 方法并执行。而当访问 run 属性时,自身和原型链上都不存在该属性,所以返回 undefined

JavaScript 子类在方法重写与调用场景的表现

子类方法重写

子类可以重写从父类继承的方法,以实现更符合自身需求的行为。

class Animal {
    speak() {
        console.log('The animal makes a sound.');
    }
}

class Dog extends Animal {
    speak() {
        console.log('The dog barks.');
    }
}

const myDog = new Dog();
myDog.speak(); // 输出: The dog barks.

在上述代码中,Dog 类重写了 Animal 类的 speak 方法。当 myDog 实例调用 speak 方法时,会执行 Dog 类中重写后的方法,而不是 Animal 类的原始方法。这使得子类能够根据自身特点定制行为。

调用父类被重写的方法

有时候,子类在重写方法时,可能需要调用父类被重写的方法以复用部分逻辑。在 ES6 类继承中,可以使用 super 关键字来调用父类方法。

class Animal {
    speak() {
        console.log('The animal makes a sound.');
    }
}

class Dog extends Animal {
    speak() {
        super.speak();
        console.log('The dog barks.');
    }
}

const myDog = new Dog();
myDog.speak(); 
// 输出: 
// The animal makes a sound.
// The dog barks.

Dog 类的 speak 方法中,通过 super.speak() 调用了父类 Animalspeak 方法,然后再执行自身特有的逻辑。这样既复用了父类的部分行为,又添加了子类特有的行为。

在基于原型链的继承中,调用父类被重写的方法稍微复杂一些:

function Animal() {}
Animal.prototype.speak = function() {
    console.log('The animal makes a sound.');
};

function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.speak = function() {
    Animal.prototype.speak.call(this);
    console.log('The dog barks.');
};

const myDog = new Dog();
myDog.speak(); 
// 输出: 
// The animal makes a sound.
// The dog barks.

这里通过 Animal.prototype.speak.call(this) 调用了父类的 speak 方法,并将 this 绑定到当前 Dog 实例,以确保正确的上下文。

JavaScript 子类在动态绑定场景的表现

动态类型与方法调用

JavaScript 是动态类型语言,对象的类型在运行时确定。这对子类的方法调用有重要影响。

class Animal {
    speak() {
        console.log('The animal makes a sound.');
    }
}

class Dog extends Animal {
    speak() {
        console.log('The dog barks.');
    }
}

class Cat extends Animal {
    speak() {
        console.log('The cat meows.');
    }
}

function makeSound(animal) {
    animal.speak();
}

const myDog = new Dog();
const myCat = new Cat();

makeSound(myDog); // 输出: The dog barks.
makeSound(myCat); // 输出: The cat meows.

makeSound 函数中,参数 animal 可以接受任何继承自 Animal 的子类实例。在运行时,根据传递进来的实际对象类型(DogCat),会调用相应子类重写后的 speak 方法。这种动态绑定机制使得代码更加灵活,能够根据不同的对象类型执行不同的行为。

动态添加与修改子类方法

在 JavaScript 中,子类的方法可以在运行时动态添加或修改。

class Animal {
    speak() {
        console.log('The animal makes a sound.');
    }
}

class Dog extends Animal {}

const myDog = new Dog();

// 动态添加方法
Dog.prototype.bark = function() {
    console.log('The dog barks.');
};

myDog.bark(); // 输出: The dog barks.

// 动态修改方法
Dog.prototype.speak = function() {
    console.log('The dog says woof.');
};

myDog.speak(); // 输出: The dog says woof.

通过直接在 Dog.prototype 上添加或修改方法,我们可以在运行时改变子类的行为。这种动态性为程序的灵活性提供了更多可能,但也需要注意维护代码的可理解性和一致性,避免过度复杂的动态操作导致代码难以维护。

JavaScript 子类在模块化与作用域场景的表现

子类在模块中的定义与使用

在 JavaScript 模块中,子类的定义和使用需要遵循模块的作用域规则。

假设我们有一个 animal.js 模块定义父类:

// animal.js
export class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log(this.name +'makes a sound.');
    }
}

然后在 dog.js 模块中定义子类并使用:

// dog.js
import { Animal } from './animal.js';

export class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
    bark() {
        console.log(this.name +'barks.');
    }
}

在另一个模块中使用 Dog 类:

// main.js
import { Dog } from './dog.js';

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak();
myDog.bark();

在上述代码中,通过 importexport 关键字,我们在模块间实现了父类和子类的定义与使用。每个模块都有自己的作用域,避免了变量和类名的冲突,使得代码结构更加清晰。

子类作用域与闭包

子类方法内部的作用域与闭包也有其特点。

class Animal {
    constructor(name) {
        this.name = name;
    }
    makeSound() {
        const sound = 'generic sound';
        return function() {
            console.log(this.name +'makes a'+ sound);
        };
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
const dogSound = myDog.makeSound();
dogSound.call(myDog); // 输出: Buddy makes a generic sound.

Animal 类的 makeSound 方法中,返回了一个内部函数。这个内部函数形成了一个闭包,它可以访问 makeSound 方法作用域内的 sound 变量。当在 Dog 子类实例 myDog 上调用 makeSound 并执行返回的函数时,需要通过 call 方法将 this 正确绑定到 myDog,以确保能正确访问 name 属性。这种作用域和闭包的特性在子类方法的编写中需要特别注意,以避免出现意外的行为。

JavaScript 子类在错误处理场景的表现

子类中的错误捕获与传递

当子类方法抛出错误时,错误的捕获和传递机制与普通函数类似,但需要考虑继承关系。

class Animal {
    speak() {
        throw new Error('Animal cannot speak.');
    }
}

class Dog extends Animal {
    speak() {
        try {
            super.speak();
        } catch (error) {
            console.log('Caught in Dog class:', error.message);
            throw new Error('Dog specific speaking error.');
        }
    }
}

const myDog = new Dog();
try {
    myDog.speak();
} catch (error) {
    console.log('Final catch:', error.message);
}
// 输出: 
// Caught in Dog class: Animal cannot speak.
// Final catch: Dog specific speaking error.

在上述代码中,Dog 类的 speak 方法调用了父类的 speak 方法,父类方法抛出错误。Dog 类通过 try - catch 捕获了这个错误,并进行了自己的处理,然后又抛出了一个新的错误。最外层的 try - catch 捕获到了 Dog 类抛出的新错误。这种机制使得错误在子类中能够被适当处理并按需传递,有助于提高程序的健壮性。

自定义错误子类

JavaScript 允许我们定义自定义错误子类,以更好地处理特定类型的错误。

class AnimalError extends Error {
    constructor(message) {
        super(message);
        this.name = 'AnimalError';
    }
}

class DogError extends AnimalError {
    constructor(message) {
        super(message);
        this.name = 'DogError';
    }
}

class Animal {
    speak() {
        throw new AnimalError('Animal cannot speak.');
    }
}

class Dog extends Animal {
    speak() {
        try {
            super.speak();
        } catch (error) {
            if (error instanceof AnimalError) {
                throw new DogError('Dog cannot speak like this.');
            }
        }
    }
}

const myDog = new Dog();
try {
    myDog.speak();
} catch (error) {
    if (error instanceof DogError) {
        console.log('Caught DogError:', error.message);
    }
}
// 输出: Caught DogError: Dog cannot speak like this.

通过定义 AnimalErrorDogError 这样的自定义错误子类,我们可以在错误处理时更精确地判断错误类型,并采取不同的处理方式。在 Dog 类的 speak 方法中,捕获到 AnimalError 后,抛出了更具体的 DogError,使得错误处理更加细化。

JavaScript 子类在性能相关场景的表现

子类继承对性能的影响

子类继承虽然提供了代码复用和灵活性,但在性能方面也有一定的影响。

在基于原型链的继承中,每次通过原型链查找属性或方法都需要一定的时间开销。例如:

function Animal() {}
Animal.prototype.speak = function() {
    console.log('The animal makes a sound.');
};

function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

const myDog = new Dog();
myDog.speak();

当调用 myDog.speak() 时,由于 speak 方法不在 myDog 自身,需要沿着原型链查找。如果原型链较长,查找时间会增加。

在 ES6 类继承中,虽然语法更简洁,但底层仍然基于原型链,同样存在原型链查找的性能问题。不过,现代 JavaScript 引擎对原型链查找进行了优化,在大多数情况下,这种性能影响并不显著。

优化子类性能的方法

为了优化子类性能,可以采取一些措施。

  1. 减少原型链长度:尽量避免过深的继承层次,减少属性和方法查找的路径长度。例如,如果有多层继承关系,可以考虑合并一些层次,或者使用组合模式替代部分继承。

  2. 缓存属性和方法:对于频繁访问的属性或方法,可以在子类实例中缓存它们,避免每次都通过原型链查找。

class Animal {
    speak() {
        console.log('The animal makes a sound.');
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
        this.speakCached = this.speak.bind(this);
    }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speakCached();

在上述代码中,通过 this.speak.bind(this)speak 方法绑定到 myDog 实例,并缓存为 speakCached,这样在调用时就不需要通过原型链查找。

  1. 避免不必要的方法重写:如果子类不需要改变父类方法的行为,就不要重写该方法。重写方法会增加代码复杂度和潜在的性能开销。

通过这些优化方法,可以在一定程度上提高子类在应用中的性能表现。

JavaScript 子类在与其他技术结合场景的表现

子类与 DOM 操作

在 JavaScript 与 DOM 结合的场景中,子类可以用于封装特定的 DOM 操作逻辑。

class DOMComponent {
    constructor(selector) {
        this.element = document.querySelector(selector);
    }
    show() {
        this.element.style.display = 'block';
    }
    hide() {
        this.element.style.display = 'none';
    }
}

class Modal extends DOMComponent {
    constructor(selector) {
        super(selector);
        this.overlay = document.createElement('div');
        this.overlay.classList.add('modal - overlay');
        document.body.appendChild(this.overlay);
    }
    open() {
        super.show();
        this.overlay.style.display = 'block';
    }
    close() {
        super.hide();
        this.overlay.style.display = 'none';
    }
}

const myModal = new Modal('#my - modal');
myModal.open();

在上述代码中,DOMComponent 类封装了基本的 DOM 显示和隐藏操作。Modal 类继承自 DOMComponent,并添加了模态框特有的逻辑,如创建遮罩层。通过继承,代码实现了复用,同时又针对模态框进行了定制。

子类与 AJAX 请求

在处理 AJAX 请求时,子类可以用于封装不同类型请求的逻辑。

class APIClient {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
    }
    async makeRequest(url, options = {}) {
        const fullUrl = `${this.baseUrl}${url}`;
        try {
            const response = await fetch(fullUrl, options);
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            return response.json();
        } catch (error) {
            console.error('Request error:', error);
        }
    }
}

class UserAPIClient extends APIClient {
    constructor() {
        super('https://api.example.com/users/');
    }
    async getUsers() {
        return this.makeRequest('');
    }
    async createUser(userData) {
        const options = {
            method: 'POST',
            headers: {
                'Content - Type': 'application/json'
            },
            body: JSON.stringify(userData)
        };
        return this.makeRequest('', options);
    }
}

const userClient = new UserAPIClient();
userClient.getUsers().then(console.log);

在上述代码中,APIClient 类封装了基本的 AJAX 请求逻辑。UserAPIClient 类继承自 APIClient,并针对用户相关的 API 请求进行了定制。通过继承,不同类型的 API 请求逻辑得到了清晰的组织和复用。

综上所述,JavaScript 子类在不同场景下有着丰富的表现,理解这些表现对于编写高效、健壮且可维护的 JavaScript 代码至关重要。无论是继承机制、对象创建、方法调用,还是与其他技术的结合,子类都为开发者提供了强大的工具来构建复杂的应用程序。