JS 面向对象编程

前言

我觉得,JS的面向对象编程属于它最难的部分,一直无法真正理解它的精髓,直到最近才弄懂了,本文是我对这块知识的理解。

先说一下面向对象编程在JS中的地位

在前端环境下,面向对象思想作用其实并不大,因为在前端:组合优于继承

组合是指将一个对象的实例作为另一个对象的属性来使用。这种方式可以让我们在不改变原有代码的情况下,灵活地扩展对象的功能,从而更好地分离关注点,使代码更加模块化。比如,我们可以定义一个Car对象,然后将Engine对象作为它的属性来使用,这样我们就可以在不改变Car对象的情况下,灵活地更换不同的引擎。

而继承则将一个类的属性和方法直接继承到子类中,这样子类就可以直接使用父类的属性和方法。但是,继承往往会导致子类和父类之间的紧耦合,使得代码更加难以维护和扩展。

综上所述,虽然继承在某些情况下也是有用的,但是在大多数情况下,组合更加灵活和可维护。

对比一下前后端的开发环境,可以发现这样的规律: 后端程序只有"数据+行为 ",关注点不多而且容易预测,继承将会很好用。而前端是"数据+行为+展现+交互 ",多出来的"展现+交互"决定了前端的关注点多且无从预测 ,也就无法准确的定义"模板",使用继承,并不是好的选择。

当然,有些环境下,也适合继承,比如网页游戏开发,元素的对象化是很重要的。

总之,之所以JS面向对象被提及这么多,我猜还是因为很多JS技术人员是受C++/JAVA等面向对象思维的影响,同时,JS本身就是一枚面向对象的语言,理解原型继承等知识是非常必要的

再说一下JS继承机制的设计思想

原型和原型链的设计机制很令人费解,prototype对象是如何产生的Function和Object对象为何会如此特殊?真相其实很简单!

  1. 1994年,工程师Brendan Eich负责开发一种网页脚本语言,时间很紧,他觉得没必要设计得很复杂,这种语言只要能够完成一些简单操作就够了,比如判断用户有没有填写表单。
  2. 1994年正是面向对象编程最兴盛的时期,Brendan Eich也受到了影响,JS里面所有的数据类型都是对象,这与Java非常相似。
  3. 但是,他随即就遇到了一个难题,到底要不要设计"继承"机制呢?如果真的是一种简易的脚本语言,其实不需要有"继承"机制,但是,JS里面都是对象,必须有一种机制,将所有对象联系起来。所以,Brendan Eich最后还是设计了"继承"。
  4. 但是,他不打算引入"类"(class)的概念,因为一旦有了"类",JS就是一种完整的面向对象编程语言了,这好像有点太正式了,而且增加了初学者的入门难度。
  5. 他想到C++和Java使用new命令时,都会调用"类"的构造函数。他就做了一个简化的设计,在JS语言中,new命令后面跟的不是类,而是构造函数,所以JS中构造函数充当了"模板"的作用。
  6. 这时,用构造函数生成实例对象,会有一个缺点,那就是无法共享属性和方法,这是极大的资源浪费。考虑到这一点,Brendan Eich决定为构造函数设置一个prototype属性,这个属性包含一个对象,即prototype对象或原型对象。
  7. 所有实例对象需要共享的属性和方法,都放在原型对象里,那些不需要共享的属性和方法,就放在构造函数里。
  8. 实例对象一旦创建,将自动引用其构造函数原型对象的属性和方法,对象中指向其构造函数的原型对象的属性是_proto_。
  9. 有了"原型"、"继承"机制后,就可以构造JS中互联的对象系统。
  10. JS最先产生出两个凭空出来的对象,Function和Object.prototype,其背后是用C++语言实现。前者的作用是构造其它对象,后者的作用是使得原型对象、构造函数对象、实例对象能联系起来。
  11. Function创建了Object、Date、包括Function本身等构造函数对象。
  12. Object创建了Function.prototype等原型对象,但Object自己的原型对象不是由Object创建的,它是凭空产生的,所以其它原型对象的_proto_属性指向Object.prototype,而Object.prototype的_proto_指向null。

其实都在下面这张图里面:品,细品!

详解JS面向对象核心概念

理解对象

对象是什么?

从内容上看,对象是实物的抽象;从形式上看,对象是一个容器,封装了属性和方法。

在Js中,除了基本类型外,所有值都是对象,但即使是基本类型,也可以当作是对象一样使用("装箱"操作),所以在JS里面,万物皆对象

装箱 :在JS中,基本类型(如字符串、数字、布尔值、null和undefined)不是对象,它们是原始值,虽然这些值不是对象,但是JS提供了一种方式来访问它们的属性和方法,当你尝试访问基本类型的属性或方法时,JS会自动将基本类型转换为对象,这个过程称为"装箱"。例如,当你使用"hello".toUpperCase()时,JS会自动将字符串"hello"转换为一个String对象,以便你可以调用toUpperCase()方法,但是,一旦调用完成,JS就会将String对象转换回原始字符串值。因此,虽然基本类型可以像对象一样使用,但它们并不是真正的对象。

对象的创建

JS中创建对象的四种方式:(1)字面量(2)构造函数(3)class(4)Object.create()

虽然表面上看,JS对象有四种来源,但本质上,除了Function和Object.prototype,JS中所有的对象都是通过构造函数创建出来的。通过字面量创建的对象,或者原型对象,本质是通过 Object 构造函数创建的对象,所以其_proto_会指向Object.prototype。构造函数创建对象的底层实现是通过Object.create(),而Object.create()创建对象的底层实现是通过Function(),本文对此不做拓展了!

对象的性质

js使用属性描述符 来描述对象的每个成员,它可以用于控制对象属性的访问与修改权限、枚举和删除行为

属性描述符也是一个对象,可以通过 Object.getOwnPropertyDescriptor() 方法来获取对象属性的描述符,也可以通过 Object.defineProperty() 方法来定义或修改对象属性的特性。

  • value:属性的值
  • configurable:是否可以重新定义
  • enumerable:是否允许被遍历,会影响for-in循环
  • writable:是否允许被修改
  • get():读取属性时,得到的是该方法的返回值
  • set(val):设置属性时,会把值传入val,调用该方法

理解构造函数

JS中,它充当了"模板"角色,用于创建其它对象。

在JS中创建构造函数的方式有三种:(1)function关键字函数声明(不是函数表达式)(2)通过class声明(3)通过Function创建

理解类

JS中的类,本质上是"特殊的函数",它在使用上主要跟四个关键字有关:class、constructor()、extends、super。

class

用于声明创建一个基于原型继承的新类。

constructor()

它是类中的构造函数,一个类中只能有一个名为"constructor"的构造函数,如果不显式指定一个构造函数,则会添加默认的构造函数。

基类的默认构造函数是:

js 复制代码
constructor() {} 

派生类的默认构造函数是:

js 复制代码
constructor(...args) {
  super(...args);
}

extends

用来创建一个普通类或者内建对象的子类。

super

用于调用对象的父对象上的函数,包括构造函数,比如:

js 复制代码
class Cat {
     constructor(name) {
        this.name = name;
       }

      speak() {
   	 console.log(this.name + " makes a noise.");
       }
}

class Lion extends Cat {
      constructor(name){
        super(name);// 调用父对象的构造函数
        }
      speak() {
    	super.speak(); // 调用父对象的函数
    	console.log(this.name + " roars.");
        }
}

理解原型

所有对象本质上都是构造函数创建的(除了Object.prototype),所以所有对象都有_proto_属性,其指向其构造函数的原型对象。

所有对象都有原型,但只有构造函数对象才具有prototype属性,它指向其原型对象,同时原型对象上有一个constructor属性,指向此原型对象对应的构造函数(并不是生成该原型对象的构造函数Object)。

理解原型链

通过_proto_属性连接起来的原型对象,组成了原型链,原型链的终点是Object.prototype,它的_proto_属性指向null。

关于封装

JS把数据和方法封装在对象里,通过原型的方式解决了数据不能共享、浪费内存、效率低的问题,封装性具体体现在私有字段、公有字段和静态字段。

私有字段

私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。

实现方式是在命名上加以区别,可以使用增加前缀 # 的方法来定义私有类字段, # 是名称本身的一部分,访问时也需要加上。

公有字段

公有方法和公有属性,是在类的外部也能访问的方法和属性,类的方法和属性在默认情况下是公有的

静态字段

加上static关键字定义静态字段,表示该方法或属性不会被实例继承,而是直接通过类来调用Class 本身的方法或属性。(这与java中的静态字段有很大不同)

关于继承

Brendan Eich原本只是想设计一个比较简单的继承机制,直接使用构造函数充当"模板",再让一个构造函数继承另一个构造函数,但由于JS中的构造函数和原型的特殊性,最后出现了很多种实现继承的方式,总共有下面这些:

  1. 构造函数绑定
  2. 原型模式
  3. 直接继承原型对象
  4. 利用空对象作为中介
  5. 拷贝继承
  6. 类方式

重点讲一下原型模式和类方式的继承!

原型继承

js 复制代码
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;

将子类的原型设置为父类的实例,再将子类原型对象的constructor属性指向它本身。

第一句让子类能访问父类的原型对象,子类原型对象的_proto_属性(原型的原型)指向父类的原型对象,实现了继承。

第二句,是因为当执行完第一句后,SubClass.prototype.constructor变成了父类,这会导致继承链的紊乱,所以需要人为把这个指向纠正过来。

类继承

js 复制代码
class Animal {
   constructor(name) {
    	this.name = name;
     }

   speak() {}
}

class Dog extends Animal {
   constructor(name) {
    	super(name); 
     }

   speak() {}
}

类继承的本质是原型继承,通过extends实现,写法上与java接近。

关于多态

多态是同一个行为具有多个不同表现形式或形态的能力,由于JS本身是动态的,天生就支持多态

总结

以上就是我关于JS面向对象编程的理解,希望对大家有点帮助吧!

相关推荐
晴空万里藏片云1 小时前
elment Table多级表头固定列后,合计行错位显示问题解决
前端·javascript·vue.js
曦月合一1 小时前
html中iframe标签 隐藏滚动条
前端·html·iframe
奶球不是球1 小时前
el-button按钮的loading状态设置
前端·javascript
kidding7231 小时前
前端VUE3的面试题
前端·typescript·compositionapi·fragment·teleport·suspense
无责任此方_修行中3 小时前
每周见闻分享:杂谈AI取代程序员
javascript·资讯
Σίσυφος19003 小时前
halcon 条形码、二维码识别、opencv识别
前端·数据库
学代码的小前端3 小时前
0基础学前端-----CSS DAY13
前端·css
dorabighead4 小时前
JavaScript 高级程序设计 读书笔记(第三章)
开发语言·javascript·ecmascript
css趣多多5 小时前
案例自定义tabBar
前端
姑苏洛言6 小时前
DeepSeek写微信转盘小程序需求文档,这不比产品经理强?
前端