回头重看前端的面向对象知识

面向对象之前要知道什么是对象

面向对象(Object-Oriented Programming, OOP)是一种编程范式,它将软件设计和开发集中在对象的创建和操作上。面向对象编程旨在通过对象这一抽象实体来模拟现实世界中的事物,并通过对象之间的交互来实现复杂的系统功能。对象的三个特性(唯一标识性、状态、行为)帮助我们更好地管理和组织代码,从而提升代码的可读性、可维护性和可扩展性。

  1. 唯一标识性

    • 定义:每个对象都有其独特的身份,即使两个对象拥有完全相同的属性和方法,它们依然是独立的两个对象。
    • 意义:这强调了对象的个体性和不可替代性。在程序设计中,这意味着每个对象都有其独特的内存地址或标识符。
  2. 状态

    • 定义:对象有状态,这意味着同一个对象在不同时间点可能处于不同的状态。
    • 意义:对象的状态是通过其属性来体现的,而属性的值会变化。这种状态变化在程序运行期间对对象行为与功能有直接影响。
  3. 行为

    • 定义:对象具有行为,它的状态可能因为其行为而改变。
    • 意义:对象的行为是由方法实现的,通过调用这些方法,能够对对象的属性(状态)进行修改或者执行操作。
js 复制代码
var a1 = {o:1}
var a2 = {o:2}
console.log(a1 === a2) // false 我们都知道对象本质是内存地址的指向,因此a1和a2看起来像本质也是不同的

// '行为'就是'方法','状态'就是'属性'
var o = { 
		d: 1, // 我是属性
		f() { // 我也是属性
				console.log(this.d);
		}    
};

面向对象编程的核心概念

  1. 对象(Object):对象是类的实例,集合了数据(属性)和行为(方法)。它们在运行时可以动态创建和销毁。

  2. 类(Class):类是对象的蓝图或模板,定义了一类对象具有的属性和方法。通过类可以创建多个具有相似特征的对象。

  3. 封装(Encapsulation):封装是将对象的属性和方法隐藏起来,只暴露必要的接口。这样可以保护对象的数据不被外部随意修改。

  4. 继承(Inheritance):继承是通过扩展已有类来创建新类的一种机制。新类称之为子类或派生类,它可以继承父类(基类)的方法和属性,并可以增加新的方法和属性或重写父类的方法。

  5. 多态(Polymorphism):多态是指同一个方法在不同对象中有不同的实现。多态使得可以用统一的接口来调用不同对象的方法,提高了代码的灵活性和可扩展性。

  6. 抽象(Abstraction):抽象是一种建模技术,关注对象的高层特征,而忽略具体的实现细节。它帮助简化复杂系统,使开发者可以专注于更高层次的问题。

面向对象编程的常用语言有Java、C++、Python、C#等。使用面向对象编程的主要优点包括:

  • 代码复用性高:通过继承和组合可以重用代码。
  • 可维护性强:代码的模块化和封装性提高了系统的可维护性。
  • 易于理解和组织:符合人类对现实世界的思考方式,便于理解和组织复杂系统。

面向对象两大类实现方式

在开始讨论面向对象编程(OOP)之前,先简要回顾一下编程语言的两大类实现方式:基于类和基于原型。这两种方式在实现OOP时虽然途径不同,但都是为了达成相同的目标。

1. 基于类(Class-based)

大部分开发者可能都是从学习基于类的编程语言开始,比如C++和Java。基于类的语言中,编程流程通常如下:

  • 定义类:先定义一个类,类包含属性(变量)和方法(函数)。
  • 实例化对象:通过类创建对象,称为类的实例。

这种方式促使开发者形成一种思维定式,认为所有编程语言都需要通过定义类来实现面向对象。这类语言强调分类及类之间的关系,常见的关系如继承和组合。类通常与语言的类型系统整合,提供了编译时的类型检查和多态性等能力。

2. 基于原型(Prototype-based)

JavaScript是基于原型设计的语言。在原型机制的编程语言中,有两种主要方式:

  • 引用原型:新对象持有一个原型对象的引用,共享其属性和方法。
  • 复制对象:直接复制原型对象,生成新的独立对象。

总结

根据以上描述,我们可以将编程语言分为两种类型:

  1. 基于类的系统

    • 强调分类和类之间的关系
    • 对象通过类实例化
    • 类与类之间形成继承、组合等复杂关系
    • 类与类型系统整合,提供编译时能力
  2. 基于原型的系统

    • 没有显式定义的类,通过对象实例创建新对象
    • 属性和方法可直接添加到对象实例中
    • 允许更高的灵活性,如随时添加新属性

这两种方式的差异主要在于:基于类的系统先定类后实例化对象,而基于原型的系统直接从对象创建新对象,无需类的定义。虽然JavaScript是基于原型的语言,但由于其在设计早期模仿了Java,引入了许多基于类的语言特性,如newthis。这使得开发者在实际使用中经常忽略了JavaScript原型链的独特优势。

需要注意的是,像Java这种基于类的语言,往往不允许随意向对象添加属性。而JavaScript则提供了这种灵活性,允许开发者在运行时随时为对象添加新属性或方法。这种灵活性也是JavaScript的一个显著优势。

无论是基于类还是基于原型,最终目标都是实现面向对象编程。两者只是实现方式不同,但本质都是通过对象进行封装和操作,旨在提高代码的复用性、可维护性和可理解性。因此,在选择编程语言和方法时,应根据具体需求和背景考虑,以便充分发挥各自的优势。

前端怎么创建对象

在JavaScript中,有多种创建对象的方式,每种方式都有其优缺点。主要包括字面量创建对象、工厂函数、以及构造函数。以下是对这几种方式的总结:

1. 字面量创建对象

利用字面量形式,手动创建对象。

javascript 复制代码
const obj = {
    name: 'Alice',
    age: 25
};

优点

  • 简洁直接

弊端

  • 当需要创建多个相同结构的对象时,需要编写重复的代码。

2. 工厂函数

通过封装一个函数,批量创建对象。

javascript 复制代码
function createPerson(name, age) {
    const person = {};
    person.name = name;
    person.age = age;
    return person;
}

const p1 = createPerson('w', 15);
const p2 = createPerson('ww', 155);

优点

  • 允许批量创建对象,减少代码重复。

弊端

  • 在打印对象时,对象的类型都是Object类型,不能明确区分出用哪个工厂函数创建的对象。

3. 构造函数

构造函数在JavaScript中扮演了其他语言中类的角色,通过new关键字创建对象。

Java中的构造函数

java 复制代码
public class Person {
    Person() { // 构造函数
        System.out.println("无参的构造方法");
    }
}

JavaScript中的构造函数和类

  • ES5方式
javascript 复制代码
function Person(name, age) { // 构造函数
    this.name = name;
    this.age = age;
}

const p1 = new Person('w', 15);
const p2 = new Person('ww', 155);
  • ES6方式
javascript 复制代码
class Person {
    constructor(name, age) { // 构造函数
        this.name = name;
        this.age = age;
    }
}

const p1 = new Person('w', 15);
const p2 = new Person('ww', 155);

优点

  • 使用new关键字创建对象时,能够明确对象的类型。
  • 支持面向对象编程的很多特性(如继承、方法定义等)。

手动实现一个 new 语法糖

利用工厂函数创建对象,因此可以发现下面手动实现一个new ,其中new 很像一个构造函数语法糖

js 复制代码
function createPerson(name,age){
	const per = {}
	per.name = name
	per.age =age
	return per
}
const p1 = createPerson('w',15)
const p2 = createPerson('ww',155)

了解 Object.create

Object.create(proto,[propertiesObject]) 静态方法以一个现有对象作为原型,创建一个新对象,这是一种更直接地基于原型链实现继承的方式。它允许我们创建一个新对象,并将其原型直接指定为传入的对象。这种方式绕过了构造函数,为原型继承提供了一种更为灵活和低级别的控制。

  • proto :要作为新对象原型的对象,或 null。
  • propertiesObject (可选):包含一个或多个属性描述符的对象,这些属性描述符将被添加到新对象中。这些属性对应于 Object.defineProperties() 的第二个参数

通过 Object.create 实现原理其实可以看出来 Object.create(proto, [propertiesObject]) 创建的新对象的内部 [[Prototype]] 其实链接的是传入参数 proto

js 复制代码
// 模拟一个 Object.create
function simpleCreate(proto, props) {
  // 创建一个空对象
  const obj = {};

  // 设置新对象的原型为传入的proto
  if (typeof proto === 'object' || typeof proto === 'function') {
    obj.__proto__ = proto; 
  } else {
    throw new TypeError('Object prototype may only be an Object or null'); 
}

原型链挂在对象

当你使用 Object.create(proto) 时,所创建的新对象具有对 proto 的引用,它作为新对象的原型。在这个新对象上调用任何属性或方法时,如果该对象自身没有这个属性或方法,JavaScript 引擎会在它的原型(即 proto)中查找

dog 继承了 animal 可以用 animal 上的属性和方法

js 复制代码
const animal = {
    eats: true,
		name:'123'
};

const dog = Object.create(animal);
dog.name = '456'

console.log(dog.name); // 456 指向自身的 并不会影响到原型链上的
console.log(animal.name); // 123 并不会改变原型链上的
console.log(dog.eats);      // true, 因为在 dog 的原型链上有 eats 属性
console.log(dog.__proto__); // { eats: true }, 即 dog 的 __proto__ 是 animal 对象
console.log(Object.getPrototypeOf(dog) === animal); // true, 验证 dog 的原型是 animal

创建出来的dog 对象 dog.__proto__.animal.__proto__.Object

挂在到构造函数上

js 复制代码
function Person( name ){
    this.name = name;
};

Person.prototype.getName = function(){
    return this.name;
};

// 这里让o2对象的原型链指向了Person,并且给o2对象自己添加了个属性p
/*
    下面的写法等同于
    const o2 = new Object()
    o2.__proto__ = Person.prototype
    o2.p = 42
    所以o2只是继承了Person.prototype原型链但是没有基础他的name属性
    因此o2是没有name属性的
*/
o2 = Object.create(Person.prototype, {
    p: {
        value: 42, 
        writable: true,
        enumerable: true,
        configurable: true 
    } 
});
// 打印结果如图一
console.log(o2)

const per = new Person()
o3 = Object.create(per, {
    p: {
        value: 42, 
        writable: true,
        enumerable: true,
        configurable: true 
    } 
});
// 打印结果如图一
console.log(o3)

创建空对象

有时候你可能需要创建一个没有任何原型链(尤其是没有默认的 Object 原型链)的对象,这可以通过传入 null 来实现。

js 复制代码
const pureObject = Object.create(null);
pureObject.someProp = 'some value';

console.log(pureObject); // { someProp: 'some value' }
console.log(Object.getPrototypeOf(pureObject)); // Output: null

实现一个new

手动实现主要遵守的四个步骤

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

设计时候要考虑到构造函数传参 new Person('wang') 因此对象设计 var a = objectFactory( Person, 'sven' )

js 复制代码
function Person(name) {
	// constructor.apply(obj, prams); 会将属性或方法添加到obj中
  this.name = name;
}

Person.prototype.getName = function () {
  return this.name;
};

function objectFactory(...args) {
  // 获取构造函数和传入的参数
  const [constructor, ...prams] = args;

  // 将对象的原型指向构造函数的原型 	 创建一个对象
  const obj = Object.create(constructor.prototype);

  // 执行构造函数,将属性或方法添加到obj中
  const result = constructor.apply(obj, prams);
  return typeof result === "object" ? result : obj; // 确保构造器总是会返回一个对象
}

// var objectFactory = function(){
//     // 创建一个 空对象
//     var obj = new Object();
//     // 获取构造函数
//     var  Constructor = [].shift.call( arguments )
//     // 改变当前obj 空对象原型链的指向
//     obj.__proto__ = Constructor.prototype
//     // 改变构造函数指向
//     var ret = Constructor.apply( obj, arguments );    
//     return typeof ret === 'object' ? ret : obj;     // 确保构造器总是会返回一个对象
// }

// 这个写法等同  console.log(new Person('wang'))
var a = objectFactory(Person, "sven");
console.log(a);
console.log(a.name); // 输出:sven
console.log(a.getName()); // 输出:sven
console.log(Object.getPrototypeOf(a) === Person.prototype);

对这个做一个说明 typeof result === "object" ? result : obj;

  • 当构造函数有返回值的时候, return 出来的是一个和 this 无关的对象时,new 命令会直接返回这个新对象,而不是通过 new 执行步骤生成的 this 对象
js 复制代码
function Person(){
   this.name = 'Jack'; 
   return {age: 18}
}
var p = new Person(); 
console.log(p)  // {age: 18}
console.log(p.name) // undefined
console.log(p.age) // 18

当返回时候是非对象,那么它还是会根据 new 关键词的执行逻辑,生成一个新的对象(绑定了最新 this),最后返回出来

js 复制代码
function Person(){
   this.name = 'Jack'; 
   return 'tom';
}
var p = new Person(); 
console.log(p)  // {name: 'Jack'}
console.log(p.name) // Jack

ES6 和 ES5 的写法比较

ES6 出现了class 语法糖更加方便让 js 可以创建对象

  1. ES5 中类的写法:
javascript 复制代码
function PersonType(name) {
    this.name = name;
}
PersonType.prototype.sayName = function () {
    console.log(this.name);
};
var personType = new PersonType('wang');
personType.sayName(); // wang

console.log('PersonType instanceof PersonType:', personType instanceof PersonType); // true
console.log('PersonType instanceof Object:', personType instanceof Object); // true
  1. ES6 中类的写法:
javascript 复制代码
class PersonClass {
    constructor(name) {
        this.name = name;
    }

    sayName() {
        console.log(this.name);
    }
}
let personClass = new PersonClass('wang');
personClass.sayName(); // wang

console.log('PersonClass instanceof PersonClass:', personClass instanceof PersonClass); // true
console.log('PersonClass instanceof Object:', personClass instanceof Object); // true

console.log('typeof PersonClass:', typeof PersonClass); // function

ES6 和 ES5 类的其他不同点

  1. 函数声明提升 vs. 类声明不可提升:

    • 函数声明可以被提升,类声明类似 let 声明,不能被提升。
    • 声明前使用类会导致报错。
  2. 自动严格模式:

    • 类声明的所有代码自动运行在严格模式下。
  3. 方法不可枚举:

    • 在 ES5 中需要手动指定方法不可枚举。
    • ES6 类方法默认不可枚举,挂在对象原型上。
  4. [[Construct]] 方法:

    • 通过 new 调用不含 [[Construct]] 的方法会报错。例如箭头函数
  5. 构造函数必须使用 new 调用:

    • 直接调用类的构造函数会抛出错误。
  6. 类名不可修改:

    • 类声明结束后类名依然可以修改,但在类中修改会报错。
javascript 复制代码
class Foo {
    constructor() {
        Foo = "bar";  // 这里会抛出错误
    }
}
Foo = "bar"; // 类声明结束后才可以修改类名

代码优化与实现

javascript 复制代码
// ES5 版本的符合以上特性的类实现

let PersonType2 = (function () {
    "use strict";
    const PersonType2 = function (name) {
        if (typeof new.target === "undefined") {
            throw new Error("Constructor must be called with new.");
        }
        this.name = name;
    };
    Object.defineProperty(PersonType2.prototype, "sayName", {
        value: function () {
            if (typeof new.target !== "undefined") {
                throw new Error("Method cannot be called with new.");
            }
            console.log(this.name);
        },
        enumerable: false,
        writable: true,
        configurable: true
    });
    return PersonType2;
}());

const a = new PersonType2('wang');
console.log(a.name); // wang

访问器属性写法比较

  1. ES5 实现:
javascript 复制代码
let CustomHTMLElement = (function() {
    "use strict";
    const CustomHTMLElement = function(element) {
        if (typeof new.target === "undefined") {
            throw new Error("Constructor must be called with new.");
        }
        this.element = element;
    }
    Object.defineProperty(CustomHTMLElement.prototype, "html", {
        enumerable: false,
        configurable: true,
        get: function() {
            return this.element.innerHTML;
        },
        set: function(value) {
            this.element.innerHTML = value;
        }
    });
    return CustomHTMLElement;
}());
  1. ES6 类实现:
javascript 复制代码
class CustomHTMLElement {
    constructor(element) {
        this.element = element;
    }
    get html() {
        return this.element.innerHTML;
    }
    set html(value) {
        this.element.innerHTML = value;
    }
}

var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, "html");
console.log("get" in descriptor); // true
console.log("set" in descriptor); // true
console.log(descriptor.enumerable); // false

优化建议

  1. 在大型项目中,使用 ES6 类可以提高代码的可读性和可维护性。
  2. 尽量在类的构造函数中定义所有实例属性,避免属性混乱。
  3. 避免在类中直接修改类名,确保代码逻辑清晰。
  4. 使用 Object.defineProperty 管理 ES5 访问器属性,确保代码在未来的 ES6 迁移中更简便。
相关推荐
zqx_78 分钟前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己25 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2341 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河1 小时前
CSS总结
前端·css
NiNg_1_2341 小时前
Vue3 Pinia持久化存储
开发语言·javascript·ecmascript
读心悦1 小时前
如何在 Axios 中封装事件中心EventEmitter
javascript·http
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
神之王楠2 小时前
如何通过js加载css和html
javascript·css·html