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

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

面向对象(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 迁移中更简便。
相关推荐
百思可瑞教育35 分钟前
在Vue项目中Axios发起请求时的小知识
前端·javascript·vue.js·北京百思教育
患得患失9491 小时前
【个人项目】【前端实用工具】OpenAPI to TypeScript 转换器
前端·javascript·typescript
大前端helloworld1 小时前
前端梳理体系从常问问题去完善-基础篇(html,css,js,ts)
前端·javascript·面试
trsoliu1 小时前
前端基于 TypeScript 使用 Mastra 来开发一个 AI 应用 / AI 代理(Agent)
前端·人工智能
鸡吃丸子1 小时前
前端权限控制:深入理解与实现RBAC模型
前端
Larry_zhang双栖1 小时前
低版本Chrome 内核兼容性问题的优美解决
前端·chrome
良木林1 小时前
浅谈原型。
开发语言·javascript·原型模式
qq_12498707532 小时前
基于node.js+vue的医院陪诊系统的设计与实现(源码+论文+部署+安装)
前端·vue.js·node.js·毕业设计
袁煦丞2 小时前
9.12 Halo的“傻瓜建站魔法”:cpolar内网穿透实验室第637个成功挑战
前端·程序员·远程工作
universe_013 小时前
day27|前端框架学习
前端·笔记