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

JavaScript类和构造函数的场景适配

2022-05-292.5k 阅读

JavaScript类和构造函数的基本概念

构造函数

在JavaScript中,构造函数是一种特殊的函数,用于创建对象。构造函数使用 new 关键字来调用,当使用 new 调用构造函数时,会发生以下几件事:

  1. 创建一个新的空对象。
  2. 这个新对象的 [[Prototype]] 被设置为构造函数的 prototype 属性。
  3. 构造函数内部的 this 指向这个新创建的对象。
  4. 执行构造函数内部的代码,对新对象进行初始化。
  5. 如果构造函数没有显式返回一个对象,则返回这个新创建并初始化的对象。

下面是一个简单的构造函数示例:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayHello = function() {
        console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
    };
}

let person1 = new Person('John', 30);
person1.sayHello(); 

在上述代码中,Person 是一个构造函数,它接受 nameage 两个参数,并在新创建的对象上设置相应的属性和方法。

ES6 引入了类的概念,它为创建对象提供了一个更简洁、更直观的语法。类本质上是对构造函数的语法糖。一个类可以包含构造函数、方法和属性。

以下是用类来重写上面的 Person 示例:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    sayHello() {
        console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
    }
}

let person1 = new Person('John', 30);
person1.sayHello(); 

在这个类的定义中,constructor 方法是类的构造函数,用于初始化对象的属性。sayHello 是类的一个实例方法。

类和构造函数的场景适配

创建对象的场景

  1. 简单对象创建场景:当需要创建少量的、相对简单的对象时,使用普通对象字面量可能是最直接的方式。例如,创建一个配置对象:
let config = {
    server: 'localhost',
    port: 8080
};

然而,当需要创建多个具有相似结构和行为的对象时,构造函数或类就显得更合适。比如创建多个用户对象:

function User(name, email) {
    this.name = name;
    this.email = email;
}

let user1 = new User('Alice', 'alice@example.com');
let user2 = new User('Bob', 'bob@example.com');

使用类来创建用户对象则更为简洁:

class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }
}

let user1 = new User('Alice', 'alice@example.com');
let user2 = new User('Bob', 'bob@example.com');
  1. 复杂对象创建场景:如果对象的创建过程涉及复杂的初始化逻辑,比如需要从数据库加载数据、进行网络请求或执行复杂的计算,构造函数或类中的 constructor 方法可以很好地封装这些逻辑。

假设我们要创建一个 Product 对象,它需要从服务器获取产品的详细信息:

function Product(id) {
    this.id = id;
    // 模拟从服务器获取数据
    setTimeout(() => {
        this.name = `Product ${id}`;
        this.price = Math.random() * 100;
        console.log(`Product ${this.id} initialized with name ${this.name} and price ${this.price}`);
    }, 1000);
}

let product1 = new Product(1);

用类来实现同样的功能:

class Product {
    constructor(id) {
        this.id = id;
        setTimeout(() => {
            this.name = `Product ${id}`;
            this.price = Math.random() * 100;
            console.log(`Product ${this.id} initialized with name ${this.name} and price ${this.price}`);
        }, 1000);
    }
}

let product1 = new Product(1);

继承场景

  1. 基于构造函数的继承:在ES6 类出现之前,JavaScript通过原型链来实现继承。通过设置构造函数的 prototype 属性来实现继承关系。
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;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
    console.log(`${this.name} barks.`);
};

let dog1 = new Dog('Buddy', 'Golden Retriever');
dog1.speak(); 
dog1.bark(); 

在上述代码中,Dog 构造函数继承自 Animal 构造函数。Animal.call(this, name) 用于调用 Animal 构造函数来初始化 name 属性。通过 Object.create(Animal.prototype) 创建一个新的原型对象,并将其设置为 Dog.prototype,同时修正 Dog.prototype.constructor 以指向 Dog 本身。

  1. 基于类的继承:ES6 类提供了更简洁的继承语法,使用 extends 关键字。
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 dog1 = new Dog('Buddy', 'Golden Retriever');
dog1.speak(); 
dog1.bark(); 

在这个类继承的例子中,Dog 类继承自 Animal 类。super(name) 用于调用父类的构造函数来初始化 name 属性。这种语法更加直观和易于理解,减少了手动处理原型链的复杂性。

模块化和代码组织场景

  1. 构造函数在模块化中的应用:在JavaScript模块中,构造函数可以用于封装特定的功能和数据。例如,在一个图形绘制模块中,我们可以定义一个 Shape 构造函数及其相关的子构造函数。
// shape.js
function Shape(color) {
    this.color = color;
}

Shape.prototype.draw = function() {
    console.log(`Drawing a ${this.color} shape.`);
};

function Circle(radius, color) {
    Shape.call(this, color);
    this.radius = radius;
}

Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;

Circle.prototype.draw = function() {
    console.log(`Drawing a ${this.color} circle with radius ${this.radius}.`);
};

export { Shape, Circle };

然后在其他模块中可以导入并使用这些构造函数:

// main.js
import { Shape, Circle } from './shape.js';

let shape1 = new Shape('red');
shape1.draw(); 

let circle1 = new Circle(5, 'blue');
circle1.draw(); 
  1. 类在模块化中的应用:使用类来组织代码在模块化中同样非常有效。以一个用户管理模块为例:
// user.js
class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }

    getInfo() {
        return `Name: ${this.name}, Email: ${this.email}`;
    }
}

class Admin extends User {
    constructor(name, email, role) {
        super(name, email);
        this.role = role;
    }

    getInfo() {
        return `${super.getInfo()}, Role: ${this.role}`;
    }
}

export { User, Admin };

在另一个模块中使用这些类:

// main.js
import { User, Admin } from './user.js';

let user1 = new User('Alice', 'alice@example.com');
console.log(user1.getInfo()); 

let admin1 = new Admin('Bob', 'bob@example.com', 'admin');
console.log(admin1.getInfo()); 

类的语法使得代码结构更加清晰,易于维护和扩展。

性能相关场景

  1. 构造函数的性能考量:当创建大量对象时,构造函数的性能可能会成为一个问题。因为每个对象实例都会有自己的方法副本,如果方法定义在构造函数内部,会导致内存浪费。例如:
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayHello = function() {
        console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
    };
}

for (let i = 0; i < 10000; i++) {
    let person = new Person(`Person ${i}`, i);
}

在这个例子中,每个 Person 对象都有自己独立的 sayHello 方法副本,占用了较多的内存。为了优化,可以将方法定义在原型上:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.sayHello = function() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
};

for (let i = 0; i < 10000; i++) {
    let person = new Person(`Person ${i}`, i);
}

这样所有 Person 对象共享同一个 sayHello 方法,节省了内存。

  1. 类的性能考量:类在性能方面与构造函数类似,因为类本质上是基于构造函数和原型链的语法糖。同样,将方法定义在类的原型上(即类的实例方法)可以提高性能。
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    sayHello() {
        console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
    }
}

for (let i = 0; i < 10000; i++) {
    let person = new Person(`Person ${i}`, i);
}

在这个类的实现中,sayHello 方法是定义在类的原型上,所有 Person 对象共享这个方法,避免了内存浪费。

与其他JavaScript特性结合的场景

  1. 与闭包结合:构造函数和类都可以与闭包很好地结合。闭包可以用于封装数据,实现私有变量的效果。
function Counter() {
    let count = 0;

    this.increment = function() {
        count++;
        console.log(`Count is now ${count}`);
    };

    this.getCount = function() {
        return count;
    };
}

let counter1 = new Counter();
counter1.increment(); 
console.log(counter1.getCount()); 

在上述构造函数 Counter 中,count 变量是一个私有变量,只能通过 incrementgetCount 方法来访问和修改。

使用类和闭包实现类似的功能:

class Counter {
    constructor() {
        let count = 0;

        this.increment = function() {
            count++;
            console.log(`Count is now ${count}`);
        };

        this.getCount = function() {
            return count;
        };
    }
}

let counter1 = new Counter();
counter1.increment(); 
console.log(counter1.getCount()); 
  1. 与ES6箭头函数结合:虽然箭头函数不能用作构造函数(因为它们没有自己的 this 绑定),但可以在构造函数和类中作为方法的实现。
function Person(name) {
    this.name = name;
    this.sayHello = () => {
        console.log(`Hello, my name is ${this.name}`);
    };
}

let person1 = new Person('John');
person1.sayHello(); 

在类中使用箭头函数作为方法:

class Person {
    constructor(name) {
        this.name = name;
    }

    sayHello = () => {
        console.log(`Hello, my name is ${this.name}`);
    };
}

let person1 = new Person('John');
person1.sayHello(); 

需要注意的是,使用箭头函数作为类的方法时,它的 this 绑定是在定义时确定的,而不是在调用时,这与普通函数方法有所不同。

面向对象编程原则的应用场景

  1. 封装:构造函数和类都可以很好地实现封装。通过将数据和行为封装在对象内部,只暴露必要的接口给外部使用。
function BankAccount(accountNumber, balance) {
    let _accountNumber = accountNumber;
    let _balance = balance;

    this.deposit = function(amount) {
        if (amount > 0) {
            _balance += amount;
            console.log(`Deposited ${amount}. New balance is ${_balance}`);
        } else {
            console.log('Invalid deposit amount.');
        }
    };

    this.withdraw = function(amount) {
        if (amount > 0 && amount <= _balance) {
            _balance -= amount;
            console.log(`Withdrawn ${amount}. New balance is ${_balance}`);
        } else {
            console.log('Invalid withdrawal amount.');
        }
    };

    this.getBalance = function() {
        return _balance;
    };
}

let account1 = new BankAccount('123456', 1000);
account1.deposit(500); 
account1.withdraw(300); 
console.log(account1.getBalance()); 

在这个构造函数 BankAccount 中,_accountNumber_balance 是封装的数据,通过 depositwithdrawgetBalance 方法来操作这些数据,外部代码不能直接访问和修改这些内部数据。

使用类来实现同样的封装:

class BankAccount {
    constructor(accountNumber, balance) {
        this._accountNumber = accountNumber;
        this._balance = balance;
    }

    deposit(amount) {
        if (amount > 0) {
            this._balance += amount;
            console.log(`Deposited ${amount}. New balance is ${this._balance}`);
        } else {
            console.log('Invalid deposit amount.');
        }
    }

    withdraw(amount) {
        if (amount > 0 && amount <= this._balance) {
            this._balance -= amount;
            console.log(`Withdrawn ${amount}. New balance is ${this._balance}`);
        } else {
            console.log('Invalid withdrawal amount.');
        }
    }

    getBalance() {
        return this._balance;
    }
}

let account1 = new BankAccount('123456', 1000);
account1.deposit(500); 
account1.withdraw(300); 
console.log(account1.getBalance()); 
  1. 继承:前面已经详细介绍了构造函数和类的继承场景,通过继承可以实现代码复用和创建对象之间的层次关系,符合面向对象编程的继承原则。
  2. 多态:多态可以通过继承和方法重写来实现。在构造函数和类中,子类可以重写父类的方法,以实现不同的行为。
function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
    console.log(`${this.name} makes a sound.`);
};

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

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.speak = function() {
    console.log(`${this.name} barks.`);
};

function Cat(name) {
    Animal.call(this, name);
}

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

Cat.prototype.speak = function() {
    console.log(`${this.name} meows.`);
};

let dog1 = new Dog('Buddy');
let cat1 = new Cat('Whiskers');

dog1.speak(); 
cat1.speak(); 

在这个例子中,DogCat 类继承自 Animal 类,并分别重写了 speak 方法,实现了多态。

使用类来实现多态:

class Animal {
    constructor(name) {
        this.name = name;
    }

    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}

class Dog extends Animal {
    speak() {
        console.log(`${this.name} barks.`);
    }
}

class Cat extends Animal {
    speak() {
        console.log(`${this.name} meows.`);
    }
}

let dog1 = new Dog('Buddy');
let cat1 = new Cat('Whiskers');

dog1.speak(); 
cat1.speak(); 

特定编程模式中的应用场景

  1. 工厂模式:工厂模式可以使用构造函数来实现。工厂函数根据不同的条件返回不同类型的对象。
function createShape(type, color) {
    if (type === 'circle') {
        function Circle(color) {
            this.color = color;
        }

        Circle.prototype.draw = function() {
            console.log(`Drawing a ${this.color} circle.`);
        };

        return new Circle(color);
    } else if (type ==='square') {
        function Square(color) {
            this.color = color;
        }

        Square.prototype.draw = function() {
            console.log(`Drawing a ${this.color} square.`);
        };

        return new Square(color);
    }
}

let circle1 = createShape('circle','red');
let square1 = createShape('square', 'blue');

circle1.draw(); 
square1.draw(); 

使用类来实现工厂模式:

class Circle {
    constructor(color) {
        this.color = color;
    }

    draw() {
        console.log(`Drawing a ${this.color} circle.`);
    }
}

class Square {
    constructor(color) {
        this.color = color;
    }

    draw() {
        console.log(`Drawing a ${this.color} square.`);
    }
}

function createShape(type, color) {
    if (type === 'circle') {
        return new Circle(color);
    } else if (type ==='square') {
        return new Square(color);
    }
}

let circle1 = createShape('circle','red');
let square1 = createShape('square', 'blue');

circle1.draw(); 
square1.draw(); 
  1. 单例模式:单例模式确保一个类只有一个实例,并提供一个全局访问点。可以通过构造函数和闭包来实现单例模式。
function Singleton() {
    let instance;

    function createInstance() {
        let obj = {
            data: 'This is singleton data'
        };
        return obj;
    }

    return {
        getInstance: function() {
            if (!instance) {
                instance = createInstance();
            }
            return instance;
        }
    };
}

let singleton1 = Singleton().getInstance();
let singleton2 = Singleton().getInstance();

console.log(singleton1 === singleton2); 

使用类来实现单例模式:

class Singleton {
    constructor() {
        if (Singleton.instance) {
            return Singleton.instance;
        }
        this.data = 'This is singleton data';
        Singleton.instance = this;
        return this;
    }
}

let singleton1 = new Singleton();
let singleton2 = new Singleton();

console.log(singleton1 === singleton2); 

框架和库开发中的应用场景

  1. 在前端框架中的应用:在JavaScript前端框架如React和Vue中,类和构造函数都有广泛的应用。在React中,虽然现在更推荐使用函数式组件,但基于类的组件仍然是一种重要的方式。
import React, { Component } from'react';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    increment = () => {
        this.setState({
            count: this.state.count + 1
        });
    };

    render() {
        return (
            <div>
                <p>Count: {this.state.count}</p>
                <button onClick={this.increment}>Increment</button>
            </div>
        );
    }
}

export default MyComponent;

在这个React类组件中,constructor 用于初始化状态,increment 方法用于更新状态,render 方法用于定义组件的UI。

在Vue中,虽然组件定义更倾向于使用对象字面量,但在一些复杂场景下,也可以使用类来组织代码。

import Vue from 'vue';

class MyClass {
    constructor() {
        this.message = 'Hello from class';
    }

    updateMessage() {
        this.message = 'Message updated';
    }
}

let myInstance = new MyClass();
new Vue({
    el: '#app',
    data: {
        myClass: myInstance
    }
});
  1. 在库开发中的应用:当开发JavaScript库时,类和构造函数可以用于封装库的功能。例如,开发一个图形绘制库,可能会定义 Shape 类及其子类 CircleRectangle 等。
class Shape {
    constructor(color) {
        this.color = color;
    }

    draw() {
        console.log(`Drawing a ${this.color} shape.`);
    }
}

class Circle extends Shape {
    constructor(color, radius) {
        super(color);
        this.radius = radius;
    }

    draw() {
        console.log(`Drawing a ${this.color} circle with radius ${this.radius}.`);
    }
}

class Rectangle extends Shape {
    constructor(color, width, height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    draw() {
        console.log(`Drawing a ${this.color} rectangle with width ${this.width} and height ${this.height}.`);
    }
}

export { Shape, Circle, Rectangle };

其他开发者可以导入这些类并使用它们来创建图形对象。

错误处理和健壮性场景

  1. 构造函数中的错误处理:在构造函数中,当对象的初始化依赖于某些条件或输入参数时,需要进行错误处理。
function Rectangle(width, height) {
    if (width <= 0 || height <= 0) {
        throw new Error('Width and height must be positive numbers.');
    }
    this.width = width;
    this.height = height;
}

try {
    let rect1 = new Rectangle(5, 10);
    let rect2 = new Rectangle(-2, 5); 
} catch (error) {
    console.error(error.message); 
}
  1. 类中的错误处理:在类的 constructor 方法中同样可以进行错误处理。
class Rectangle {
    constructor(width, height) {
        if (width <= 0 || height <= 0) {
            throw new Error('Width and height must be positive numbers.');
        }
        this.width = width;
        this.height = height;
    }
}

try {
    let rect1 = new Rectangle(5, 10);
    let rect2 = new Rectangle(-2, 5); 
} catch (error) {
    console.error(error.message); 
}

此外,在类的方法中也需要进行适当的错误处理,以确保对象的状态始终保持一致。

class BankAccount {
    constructor(balance) {
        if (balance < 0) {
            throw new Error('Initial balance cannot be negative.');
        }
        this.balance = balance;
    }

    withdraw(amount) {
        if (amount <= 0) {
            throw new Error('Withdrawal amount must be positive.');
        }
        if (amount > this.balance) {
            throw new Error('Insufficient funds.');
        }
        this.balance -= amount;
        return this.balance;
    }
}

try {
    let account1 = new BankAccount(1000);
    let newBalance = account1.withdraw(500);
    console.log(`New balance: ${newBalance}`);
    let newBalance2 = account1.withdraw(1500); 
} catch (error) {
    console.error(error.message); 
}

动态创建对象和类型检查场景

  1. 动态创建对象:在JavaScript中,可以使用构造函数或类来动态创建对象。例如,根据用户输入来创建不同类型的图形对象。
function createShape(type, color) {
    if (type === 'circle') {
        class Circle {
            constructor(color) {
                this.color = color;
            }

            draw() {
                console.log(`Drawing a ${this.color} circle.`);
            }
        }
        return new Circle(color);
    } else if (type ==='square') {
        class Square {
            constructor(color) {
                this.color = color;
            }

            draw() {
                console.log(`Drawing a ${this.color} square.`);
            }
        }
        return new Square(color);
    }
}

let userInputType = 'circle';
let userInputColor ='red';
let shape1 = createShape(userInputType, userInputColor);
shape1.draw(); 
  1. 类型检查:在使用构造函数和类创建对象时,类型检查是很重要的。可以使用 instanceof 操作符来检查一个对象是否是某个构造函数或类的实例。
function Person(name) {
    this.name = name;
}

let person1 = new Person('John');

console.log(person1 instanceof Person); 

class Animal {
    constructor(name) {
        this.name = name;
    }
}

let animal1 = new Animal('Lion');

console.log(animal1 instanceof Animal); 

此外,在函数参数和返回值的类型检查中,也可以使用类似的方法来确保代码的健壮性。

function greet(person) {
    if (!(person instanceof Person)) {
        throw new Error('Expected a Person instance.');
    }
    console.log(`Hello, ${person.name}!`);
}

greet(person1); 

通过以上对JavaScript类和构造函数在各种场景下的适配分析,可以更好地根据具体需求选择合适的方式来创建对象、组织代码以及实现特定的功能,提高代码的质量和可维护性。