面向对象编程【JavaScript】

JavaScript 面向对象编程(OOP)是一种编程范式,旨在通过对象来组织代码,从而使代码更加模块化、可重用和易于维护。JavaScript 是一种动态语言,它支持面向对象编程风格,但与其他传统的面向对象语言(如 Java、C++)存在一些差异。

基本概念

  • 对象(Object):在 JavaScript 中,几乎所有的事物都是对象。对象是属性和方法的集合。属性是对象的特征,方法是对象可以执行的操作。

  • 类(Class):尽管 JavaScript 在 ES5 之前没有类的概念,但从 ES6 开始引入了类的语法,使得定义对象模板更加直观。类是用来创建对象的蓝图。

  • 继承(Inheritance):JavaScript 支持原型继承,允许一个对象从另一个对象继承属性和方法。

  • 封装(Encapsulation):封装是将对象的状态(属性)与行为(方法)隐藏在对象内部,只暴露必要的接口供外部使用。

  • 多态(Polymorphism):不同的对象能够通过同一接口调用不同的方法,显示出不同的行为。

1. 对象是什么?

对象是面向对象编程的基本构建块,代表现实世界的一个实体。对象包含属性(对象的状态)和方法(对象的行为)。

实例:
我们可以定义一个表示“汽车”的对象,它有属性如颜色品牌型号,以及方法如加速()刹车()

const car = {
    color: '红色',
    brand: '丰田',
    model: '卡罗拉',
    accelerate: function() {
        console.log('汽车加速');
    },
    brake: function() {
        console.log('汽车刹车');
    }
};

car.accelerate(); // 输出 '汽车加速'

2. 构造函数

构造函数是一个特殊的函数,用于创建对象的模板。当使用new关键字调用这个函数时,会创建一个新对象。

实例:

function Car(color, brand, model) {
    this.color = color;
    this.brand = brand;
    this.model = model;
}

const myCar = new Car('蓝色', '本田', '思域');
console.log(myCar); // 输出 Car { color: '蓝色', brand: '本田', model: '思域' }

new

new是一个关键字,用于创建对象的实例,它会执行以下操作:

  • 创建一个空对象
  • 让这个对象的原型指向构造函数的原型
  • 执行构造函数并将this指向新对象
  • 返回新对象(如果构造函数没有显式返回对象的话)

constructor

当使用构造函数创建对象时,构造函数的标识符即为constructor,可以通过instanceof运算符确认对象的构造函数。

示例

下面通过一个简单的JavaScript示例来解释new关键字constructor属性

假设我们有一个名为Person的构造函数,它用于创建表示人的对象。这个构造函数接受两个参数:nameage,分别表示人的姓名和年龄。

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

现在,我们可以使用new关键字来创建一个Person对象的实例:

let john = new Person('John Doe', 30);

在这个例子中,new关键字执行了以下操作:

  1. 创建一个空对象:首先,JavaScript引擎创建了一个空的对象{}
  2. 设置原型:然后,这个新对象的[[Prototype]]内部属性被设置为Person.prototype。这意味着新对象可以访问Person构造函数原型上定义的所有属性和方法。
  3. 执行构造函数:接下来,Person构造函数被执行,其中this关键字指向新创建的对象。在构造函数中,我们给this对象添加了nameagegreet属性。
  4. 返回新对象:最后,如果构造函数没有显式返回一个对象,那么new表达式的结果就是新创建的对象。在这个例子中,john变量现在持有对新创建的Person对象的引用。

现在,我们可以通过john对象来访问其属性和方法:

console.log(john.name); // 输出: John Doe
console.log(john.age); // 输出: 30
john.greet(); // 输出: Hello, my name is John Doe and I am 30 years old.

至于constructor属性,它是原型对象(prototype)上的一个属性,指向用于创建该对象实例的构造函数。在JavaScript中,每个函数都有一个prototype属性,这个属性是一个对象,它有一个constructor属性指回该函数本身。因此,我们可以使用constructor属性来检查对象的构造函数:

console.log(john.constructor === Person); // 输出: true
console.log(john instanceof Person); // 输出: true

在这个例子中,john.constructor指向Person构造函数,因为john对象是通过Person构造函数创建的。同样地,instanceof运算符用于确认john对象是否是Person构造函数的实例,结果也是true

3. 原型对象

每个对象都由原型共享属性和方法。对象的__proto__属性指向其构造函数的原型。这使得方法得以共享,而不必每次都在实例中重新定义。

实例:

function Car() {}

Car.prototype.accelerate = function() {
    console.log('汽车加速');
};

const myCar = new Car();
myCar.accelerate(); // 输出 '汽车加速'

4. 创建对象的5种模式

4.1 字面量方式

问题:创建多个对象会造成冗余的代码。

示例

let person1 = { name: "Alice", age: 25 };
let person2 = { name: "Bob", age: 30 };
let person3 = { name: "Charlie", age: 28 };

在这个例子中,我们使用字面量方式创建了三个person对象。每个对象的结构相同,代码重复,造成冗余。

4.2 工厂模式

解决对象字面量方式创建对象的问题:通过一个工厂函数来创建对象,可以避免冗余。

示例

function createPerson(name, age) {
    return {
        name: name,
        age: age
    };
}

let person1 = createPerson("Alice", 25);
let person2 = createPerson("Bob", 30);
let person3 = createPerson("Charlie", 28);

在这个例子中,createPerson函数用于生成person对象,从而消除了重复代码。

问题:对象识别的问题,虽然使用了工厂函数,但我们无法直接知道对象的构造来源。

4.3 构造函数模式

解决工厂模式的问题:使用构造函数来创建对象,有助于识别对象的类型。

示例

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

let person1 = new Person("Alice", 25);
let person2 = new Person("Bob", 30);

在这个例子中,我们使用构造函数Person来创建对象。通过new关键字,我们可以很清晰地识别出person对象的构造来源。

问题:方法重复被创建,每个实例的创建都会重新创建方法。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.getDetails = function() {
        return `${this.name}, Age: ${this.age}`;
    };
}

let person1 = new Person("Alice", 25);
let person2 = new Person("Bob", 30);
// 每个对象都有自己的getDetails方法,重复了代码

4.4 原型模式

解决构造函数模式创建对象的问题:使用原型来共享方法,有助于减少内存使用。

示例

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

Person.prototype.getDetails = function() {
    return `${this.name}, Age: ${this.age}`;
};

let person1 = new Person("Alice", 25);
let person2 = new Person("Bob", 30);

// person1和person2共享getDetails方法

在这个例子中,getDetails方法被定义在Person的原型上,所有实例共享同一个方法。

问题:给当前实例定制的引用类型的属性会被所有的实例所共享。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.hobbies = []; // 这是一个引用类型的属性,所有实例共享
}

let person1 = new Person("Alice", 25);
let person2 = new Person("Bob", 30);
person1.hobbies.push("Reading");
console.log(person2.hobbies); // person2的hobbies也受到了影响

4.5 组合模式(构造函数和原型模式)

构造函数模式:定义实例属性。

示例

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

原型模式:用于定义方法和共享的属性,还支持向构造函数中传递参数。

示例

Person.prototype.getDetails = function() {
    return `${this.name}, Age: ${this.age}`;
};

let person1 = new Person("Alice", 25);
let person2 = new Person("Bob", 30);

在这个例子中,Person构造函数定义了实例属性,而getDetails方法被定义在原型上,所有实例共享这个方法,并且避免了方法重复创建的问题,同时可以通过构造函数传递参数。

5. 实现继承的5种方式

5.1 原型链继承

特点:

  • 重写子类的原型对象,父类原型对象上的属性和方法都会被子类继承。

问题:

  • 在父类中定义的实例引用类型的属性,一旦被修改,其他实例也会被修改。
  • 当实例化子类的时候,不能传递参数到父类。

实例代码:

function Animal() {
    this.colors = ['red', 'blue'];
}

Animal.prototype.speak = function() {
    console.log('Animal speaks');
};

function Dog() {}

// 重写Dog的原型对象,使其指向Animal的实例
Dog.prototype = new Animal();

var dog1 = new Dog();
var dog2 = new Dog();

dog1.colors.push('green');

console.log(dog1.colors); // ['red', 'blue', 'green']
console.log(dog2.colors); // ['red', 'blue', 'green'],dog2也被修改了

dog1.speak(); // Animal speaks

5.2 借用构造函数模式

特点:

  • 在子类构造函数内部间接调用(call()apply()bind())父类的构造函数。
  • 原理:改变父类中的 this 指向。

优点:

  • 仅仅的是把父类中的实例属性当做子类的实例属性,并且还能传参。

缺点:

  • 父类中公有的方法不能被继承下来。

实例代码:

function Animal(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
}

function Dog(name, age) {
    Animal.call(this, name); // 调用父类构造函数,并传入参数
    this.age = age;
}

var dog1 = new Dog('Buddy', 3);
var dog2 = new Dog('Max', 2);

dog1.colors.push('green');

console.log(dog1.colors); // ['red', 'blue', 'green']
console.log(dog2.colors); // ['red', 'blue'],不受dog1影响

// dog1.speak(); // TypeError: dog1.speak is not a function,因为speak方法没有被继承

5.3 组合继承

特点:

  • 结合了原型链继承和借用构造函数继承的优点。
  • 原型链继承:公有的方法能被继承下来。
  • 借用构造函数:实例属性能被子类继承下来。

缺点:

  • 调用了两次两次父类的构造函数。
    1. 实例化子类对象。
    2. 子类的构造函数内部。

实例代码:

function Animal(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
}

Animal.prototype.speak = function() {
    console.log(this.name + ' speaks');
};

function Dog(name, age) {
    Animal.call(this, name); // 借用构造函数继承实例属性
    this.age = age;
}

Dog.prototype = new Animal(); // 原型链继承公有方法
Dog.prototype.constructor = Dog; // 修复构造函数指向

var dog1 = new Dog('Buddy', 3);
var dog2 = new Dog('Max', 2);

dog1.colors.push('green');

console.log(dog1.colors); // ['red', 'blue', 'green']
console.log(dog2.colors); // ['red', 'blue']

dog1.speak(); // Buddy speaks

5.4 寄生式继承

特点:

  • 寄生式继承(Parasitic Inheritance)是一种通过创建一个新对象,然后将该对象的原型设置为一个已有对象,最后对新对象进行增强的模式。
  • 这种方法通常用于创建一个新的对象,并在其基础上添加额外的属性和方法。

优点:

  • 简单、灵活。
  • 可以对已有对象进行增强,而不影响原对象。

缺点:

  • 无法复用原型链上的方法,每次创建新对象时都需要重新定义方法。
  • 不能实现真正的继承,因为它没有通过原型链共享方法。

实例代码:

// 原始对象
const animal = {
    name: '',
    colors: ['red', 'blue'],
    speak: function() {
        console.log(this.name + ' speaks');
    }
};

// 寄生式继承
function createDog(name, age) {
    // 创建一个新对象,继承animal的属性和方法
    const dog = Object.create(animal);
    
    // 增强新对象,添加额外的属性和方法
    dog.name = name;
    dog.age = age;
    dog.bark = function() {
        console.log(this.name + ' barks');
    };
    
    return dog;
}

const dog1 = createDog('Buddy', 3);
const dog2 = createDog('Max', 2);

dog1.colors.push('green');

console.log(dog1.colors); // ['red', 'blue', 'green']
console.log(dog2.colors); // ['red', 'blue'],不受dog1影响

dog1.speak(); // Buddy speaks
dog2.speak(); // Max speaks

dog1.bark(); // Buddy barks
dog2.bark(); // Max barks

5.5 寄生组合式继承

特点:

  • 使用 Object.create(a); 将 a 对象作为 b 实例的原型对象。
  • 把子类的原型对象指向了父类的原型对象。

实例代码:

function Animal(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
}

Animal.prototype.speak = function() {
    console.log(this.name + ' speaks');
};

function Dog(name, age) {
    Animal.call(this, name); // 借用构造函数继承实例属性
    this.age = age;
}

// 寄生组合式继承:不直接调用父类构造函数,而是使用Object.create()创建原型对象
Dog.prototype = Object.create(Animal.prototype); // 设置Dog的原型对象为Animal原型对象的一个空实例
Dog.prototype.constructor = Dog; // 修复构造函数指向

var dog1 = new Dog('Buddy', 3);
var dog2 = new Dog('Max', 2);

dog1.colors.push('green');

console.log(dog1.colors); // ['red', 'blue', 'green']
console.log(dog2.colors); // ['red', 'blue']

dog1.speak(); // Buddy speaks

寄生组合式继承避免了在组合继承中调用两次父类构造函数的问题,同时保留了原型链继承和借用构造函数继承的优点。

6. 构造函数、实例对象和原型对象之间的关系

在 JavaScript 中,构造函数、实例对象和原型对象是理解对象创建和继承的关键概念。以下是三者之间关系的详细说明:

6.1 构造函数

构造函数是一种特殊类型的函数,使用 new 关键字调用时,它可以创建对象实例。构造函数通常以大写字母开头,以示与普通函数的区别。

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

6.2 实例对象

实例对象是通过构造函数创建的实际对象。使用 new 关键字调用构造函数时,会创建一个新的对象并将其返回。

let person1 = new Person('Alice', 30);
let person2 = new Person('Bob', 25);

在上面的代码中,person1 和 person2 是 Person 构造函数的实例对象,它们分别有 name 和 age 属性。

6.3 原型对象

每个函数都有一个 prototype 属性,构造函数的 prototype 属性指向的对象就是原型对象。实例对象会自动拥有一个 __proto__ 链接到构造函数的 prototype,这使得实例对象可以访问原型对象上的属性和方法。

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

通过上面的代码,我们给 Person 的原型添加了一个 sayHello 方法。现在,所有 Person 的实例都可以调用这个方法:

person1.sayHello(); // 输出: Hello, my name is Alice
person2.sayHello(); // 输出: Hello, my name is Bob

6.4 三者之间的关系

  • 构造函数创建实例对象。
  • 实例对象是构造函数的具体实例,可以访问实例自己的属性,以及通过原型链访问原型对象的方法和属性。
  • 原型对象为所有实例共享的方法和属性提供了一个结构,通过 prototype 属性与构造函数关联。
构造函数 通过 new 关键字创建 实例对象
实例对象 具有自己的属性,并可以通过 __proto__ 链接到 原型对象
原型对象 可以提供共享的方法和属性给所有实例对象。

 

7. 存取器

在 JavaScript 中,存取器(Accessor)是指用于获取(get)或设置(set)对象属性的方法。通过使用存取器,你可以在获取或设置属性值时执行自定义逻辑。存取器通常用于封装对象的内部状态,或者在读取或写入属性时执行特定的操作。

7.1 语法

存取器使用 get 关键字定义获取器,使用 set 关键字定义设置器。语法如下:

{
  get propertyName() {
    // 获取属性值时的逻辑
  },

  set propertyName(value) {
    // 设置属性值时的逻辑
  }
}

7.2 示例

以下是一个使用存取器的简单示例:

const person = {
  _firstName: '',  // 私有属性,通常以下划线开头
  _lastName: '',

  get fullName() {
    return `${this._firstName} ${this._lastName}`;
  },

  set fullName(name) {
    const parts = name.split(' ');
    this._firstName = parts[0];
    this._lastName = parts[1];
  }
};

// 使用获取器
console.log(person.fullName);  // 输出: ''

// 使用设置器
person.fullName = 'John Doe';
console.log(person.fullName);  // 输出: 'John Doe'
console.log(person._firstName);  // 输出: 'John'
console.log(person._lastName);  // 输出: 'Doe'

7.3 使用场景

  • 数据验证与过滤: 在设置属性时,可以执行数据验证或过滤操作。

    const user = {
      _age: 0,
    
      get age() {
        return this._age;
      },
    
      set age(value) {
        if (typeof value === 'number' && value >= 0) {
          this._age = value;
        } else {
          console.error('Invalid age');
        }
      }
    };
    
    user.age = 25;
    console.log(user.age);  // 输出: 25
    
    user.age = -5;  // 输出: Invalid age
    console.log(user.age);  // 输出: 25(由于数据验证失败,age 值未改变)
    
  • 数据封装: 使用存取器来封装对象的内部状态,防止直接访问私有属性。

    class Circle {
      constructor(radius) {
        this._radius = radius;
      }
    
      get radius() {
        return this._radius;
      }
    
      set radius(value) {
        if (value > 0) {
          this._radius = value;
        } else {
          console.error('Radius must be positive');
        }
      }
    }
    
    const circle = new Circle(10);
    console.log(circle.radius);  // 输出: 10
    
    circle.radius = -5;  // 输出: Radius must be positive
    console.log(circle.radius);  // 输出: 10

 

8. 浅拷贝与深拷贝

在 JavaScript 中,浅拷贝和深拷贝用于复制对象和数组。它们的主要区别在于如何处理嵌套对象(即对象中的对象)。

8.1 浅拷贝(Shallow Copy)

浅拷贝创建一个新的对象或数组,但只复制第一层级的数据。如果原对象或数组包含嵌套对象或数组,这些嵌套对象或数组的引用会被复制,而不是它们的值。因此,浅拷贝后的新对象和原对象共享嵌套对象。

实现浅拷贝的方法:
  • 扩展运算符(Spread Operator) ...:

    let original = { a: 1, b: { c: 2 } };
    let shallowCopy = { ...original };
    
    shallowCopy.b.c = 3; // 修改浅拷贝中的嵌套对象
    
    console.log(original); // { a: 1, b: { c: 3 } }
    console.log(shallowCopy); // { a: 1, b: { c: 3 } }
    
  • Object.assign():

    let original = { a: 1, b: { c: 2 } };
    let shallowCopy = Object.assign({}, original);
    
    shallowCopy.b.c = 3; // 修改浅拷贝中的嵌套对象
    
    console.log(original); // { a: 1, b: { c: 3 } }
    console.log(shallowCopy); // { a: 1, b: { c: 3 } }
    
  • 数组的切片方法 slice():

    let original = [1, [2, 3], 4];
    let shallowCopy = original.slice();
    
    shallowCopy[1][0] = 5; // 修改浅拷贝中的嵌套数组
    
    console.log(original); // [1, [5, 3], 4]
    console.log(shallowCopy); // [1, [5, 3], 4]
    

8.2 深拷贝(Deep Copy)

深拷贝创建一个新的对象或数组,并且递归地复制所有层级的数据。这意味着嵌套对象或数组也会被完全复制,而不是共享引用。因此,深拷贝后的新对象和原对象是完全独立的。

实现深拷贝的方法:
  • 使用 JSON.parse() 和 JSON.stringify():

    let original = { a: 1, b: { c: 2 } };
    let deepCopy = JSON.parse(JSON.stringify(original));
    
    deepCopy.b.c = 3; // 修改深拷贝中的嵌套对象
    
    console.log(original); // { a: 1, b: { c: 2 } }
    console.log(deepCopy); // { a: 1, b: { c: 3 } }
    

    注意: 这种方法有一些限制,例如无法处理函数、undefinedSymbol 等类型的数据。

  • 递归函数:

    function deepCopy(obj) {
      if (obj === null || typeof obj !== 'object') return obj;
    
      if (Array.isArray(obj)) {
        return obj.map(item => deepCopy(item));
      }
    
      let copy = {};
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          copy[key] = deepCopy(obj[key]);
        }
      }
      return copy;
    }
    
    let original = { a: 1, b: { c: 2 } };
    let deepCopy = deepCopy(original);
    
    deepCopy.b.c = 3; // 修改深拷贝中的嵌套对象
    
    console.log(original); // { a: 1, b: { c: 2 } }
    console.log(deepCopy); // { a: 1, b: { c: 3 } }
    

8.3 总结

  • 浅拷贝:只复制第一层级的数据,嵌套对象共享引用。
  • 深拷贝:递归复制所有层级的数据,嵌套对象完全独立。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值