前言
我觉得,JS的面向对象编程属于它最难的部分,一直无法真正理解它的精髓,直到最近才弄懂了,本文是我对这块知识的理解。
先说一下面向对象编程在JS中的地位
在前端环境下,面向对象思想作用其实并不大,因为在前端:组合优于继承。
组合是指将一个对象的实例作为另一个对象的属性来使用。这种方式可以让我们在不改变原有代码的情况下,灵活地扩展对象的功能,从而更好地分离关注点,使代码更加模块化。比如,我们可以定义一个Car对象,然后将Engine对象作为它的属性来使用,这样我们就可以在不改变Car对象的情况下,灵活地更换不同的引擎。
而继承则将一个类的属性和方法直接继承到子类中,这样子类就可以直接使用父类的属性和方法。但是,继承往往会导致子类和父类之间的紧耦合,使得代码更加难以维护和扩展。
综上所述,虽然继承在某些情况下也是有用的,但是在大多数情况下,组合更加灵活和可维护。
对比一下前后端的开发环境,可以发现这样的规律: 后端程序只有"数据+行为 ",关注点不多而且容易预测,继承将会很好用。而前端是"数据+行为+展现+交互 ",多出来的"展现+交互"决定了前端的关注点多且无从预测 ,也就无法准确的定义"模板",使用继承,并不是好的选择。
当然,有些环境下,也适合继承,比如网页游戏开发,元素的对象化是很重要的。
总之,之所以JS面向对象被提及这么多,我猜还是因为很多JS技术人员是受C++/JAVA等面向对象思维的影响,同时,JS本身就是一枚面向对象的语言,理解原型继承等知识是非常必要的。
再说一下JS继承机制的设计思想
原型和原型链的设计机制很令人费解,prototype对象是如何产生的 ?Function和Object对象为何会如此特殊?真相其实很简单!
- 1994年,工程师Brendan Eich负责开发一种网页脚本语言,时间很紧,他觉得没必要设计得很复杂,这种语言只要能够完成一些简单操作就够了,比如判断用户有没有填写表单。
- 1994年正是面向对象编程最兴盛的时期,Brendan Eich也受到了影响,JS里面所有的数据类型都是对象,这与Java非常相似。
- 但是,他随即就遇到了一个难题,到底要不要设计"继承"机制呢?如果真的是一种简易的脚本语言,其实不需要有"继承"机制,但是,JS里面都是对象,必须有一种机制,将所有对象联系起来。所以,Brendan Eich最后还是设计了"继承"。
- 但是,他不打算引入"类"(class)的概念,因为一旦有了"类",JS就是一种完整的面向对象编程语言了,这好像有点太正式了,而且增加了初学者的入门难度。
- 他想到C++和Java使用new命令时,都会调用"类"的构造函数。他就做了一个简化的设计,在JS语言中,new命令后面跟的不是类,而是构造函数,所以JS中构造函数充当了"模板"的作用。
- 这时,用构造函数生成实例对象,会有一个缺点,那就是无法共享属性和方法,这是极大的资源浪费。考虑到这一点,Brendan Eich决定为构造函数设置一个prototype属性,这个属性包含一个对象,即prototype对象或原型对象。
- 所有实例对象需要共享的属性和方法,都放在原型对象里,那些不需要共享的属性和方法,就放在构造函数里。
- 实例对象一旦创建,将自动引用其构造函数原型对象的属性和方法,对象中指向其构造函数的原型对象的属性是_proto_。
- 有了"原型"、"继承"机制后,就可以构造JS中互联的对象系统。
- JS最先产生出两个凭空出来的对象,Function和Object.prototype,其背后是用C++语言实现。前者的作用是构造其它对象,后者的作用是使得原型对象、构造函数对象、实例对象能联系起来。
- Function创建了Object、Date、包括Function本身等构造函数对象。
- 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中的构造函数和原型的特殊性,最后出现了很多种实现继承的方式,总共有下面这些:
- 构造函数绑定
- 原型模式
- 直接继承原型对象
- 利用空对象作为中介
- 拷贝继承
- 类方式
重点讲一下原型模式和类方式的继承!
原型继承
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面向对象编程的理解,希望对大家有点帮助吧!