js的面向对象
前言
说起起面向对象的概念,大家大部分的印象都是与后端相关的。
其实并不是这样,我觉得面-向对象这种思想适合在任何的场景,甚至在现实的场景中。
因为后端相关语言在实践和理论概念的资料和资源能看到的多很多。
而且历史原因,前端最开始之初,大部分时间和精力都是在切页面和利用js操作dom。
所以前端对这方面的知识也相对弱很多。
但是对于前端开发来说,把这些面向对象的思想领会并应用到项目中,能够将自己的领域更上一个层次。
好处
1、易维护
采用面向对象思想设计的结构,可读性高,由于继承的存在,即使改变需求,那么维护也只是在局部模块,所以维护起来是非常方便和较低成本的。
2、高质量和高速度
在设计时,可重用现有的,在以前的项目的领域中已被测试过的类使系统满足业务需求并具有较高的质量。
3、易扩展
由于继承、封装、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低。
类和实例
说明:因为js前期是一种典型的
基于原型(prototype)的面向对象
,而目前又变成了基于类(class)的面向对象
。所以为了能理解的更透彻,一下例子中,我会用两种不同的写法来讲解。
一般通俗理解,在没有class关键子的年代,都是利用function来模拟这种特性,所以可以简单的把function理解为老版的类
大多数人都能在class中找到es5之前语法对象的实现特性,进而说class是原型的语法糖以及如何使用原型来实现class这一语法糖。但切记我们使用原型的目的并不是来模拟class的,抛开模拟这一点,class的oop本身也有很多知识点。
要想了解面向对象,首先我们要了解类和对象的概念
类:类是一个模板,它描述一类对象的行为和状态。类是抽象的
对象:对象是类的一个实例,有状态和行为。而对象是具体的
例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。
下图中男孩boy、女孩girl为类class,而具体的每个人为该类的对象object
Js中的对象
现在让我们深入了解什么是对象。看看周围真实的世界,会发现身边有很多对象,车,狗,人等等。所有这些对象都有自己的状态和行为。
拿一条狗来举例,它的状态有:名字、品种、颜色,行为有:叫、摇尾巴和跑。
对比现实对象和软件对象,它们之间十分相似。
软件对象也有状态和行为。软件对象的状态就是属性,行为通过方法体现。
在软件开发中,方法操作对象内部状态的改变,对象的相互调用也是通过方法来完成。
Js中的类
类可以看成是创建Js对象的模板。
通过下面一个简单的类来理解下Js中类的定义:
class Dog{
age;
constructor(age){
this.age = age;
}
sleeping(){
}
}
function Dog(age){
this.age = age;
this.sleeping = function(){
}
}
类都有哪些成员
class Dog {
static color = 'blue'; // 类成员之类属性
name; // 实例成员之实例属性
constructor(name) { // 构造函数
this.name = name;
}
sleeping() { // 实例成员之实例方法
}
static introduce() { // 类成员之类方法
console.log('我是狗类')
}
}
function Dog(name){
this.name = name;
};
Dog.prototype.sleeping = function(){};
Dog.color = 'blue';
Dog.introduce = function () {
console.log('我是狗类');
};
常见内容
按照静态和非静态划分可以分为:静态(类)和非静态(实例/对象)成员
按照类型来分,可以划分为:属性和方法
类的成员有3类:属性、方法、构造器.(目前尚不支持代码块和类部类)
- 属性是用来描述一类东西特有的状态,是个名词n
- 方法也叫函数,用来定义一类东西特有的动作行为,是个动词v
- 构造函数,构造函数也是函数,只不过是一种用于初始化对象的特殊方法。当你用new来创建对象的时候,调用的就是构造函数来初始化的这个对象,并在内存中为此对象开辟一个空间放入。
每个类都有构造方法。如果没有显式地为类定义构造方法,js引擎将会为该类提供一个默认无参的构造方法,当然你也可以用固定的constructor
方法名来显式覆盖默认的构造函数。
其他内容
- 局部成员,方法内部成员。
方法执行的时候被加载到内存,执行完毕,被GC回收 - static是修饰符。
来修饰属性和方法以及代码块(这个js目前还不支持)的。
属于类本身的,当类被加载到内存,就开始执行static修饰的代码,且只执行初始化一次
可以用类名直接调用,非静态的元素则只能用new出来的对象去调用。需要注意的是,因为类加载的时候,还没有对象被创建,当前class的环境中是不存在this的,所以类成员无法调用实例成员。
简单说来说就是实例可以调用静态的,静态的无法调用实例的。
class Dog {
private sleeping() {
console.log(123);
}
public static introduce() {
// 类方法调用静态方法,在js中运行时报错,在ts中编译时直接报错(无法找到this)
this.sleeping();
}
}
const xiaohua: Dog = new Dog();
Dog.introduce();
优点: 静态方法的好处就是不用生成类的实例就可以直接调用。非常方便,常用于工具类
缺点: 是静态资源常驻内存,很难被回收
静态属性被回收的唯一情况是类本身被回收,这种情况很难发生,比如刷新页面,程序退出,内存溢出等
实例属性是跟实例相关的,故只有类实例化后,才会放入内存,当实例被回收后,实例相关的自然也不存在了
创建一个对象
对象是根据类创建的。在js中,使用关键字new来创建一个新的对象。
const xiaohua = new Dog('小花');
- 声明:声明一个对象,包括对象名称和对象类型(ts特有)。
- 实例化:使用关键字new来创建一个对象。
- 初始化:使用new创建对象时,会调用构造方法初始化对象。
其实这一个简单的new操作,也有大量的知识点,创建对象的顺序和过程如下:
1,new用到了Dog,所以会先找到Dog加载进内存中
2,如果有静态成员,先执行之,给这个类进行初始化的
3,在堆内存中开辟空间,分配内存地址(如:0X00328F)
4,在堆内存中建立对象的特有属性,并进行默认初始化(因为没有js数据类型,所以都是初始化值都是undefined),然后再进行显式初始化(es7之后才有:name = '小红'),最后再执行构造函数覆盖初始化(this.name = name)
5,将内存地址(c中叫指针)赋给栈内存中的xiaohua变量
js支持字面量的方式创建对象
比较而言这种方式创建对象更加简洁
基本的数据类型都支持字面量创建对象,而后内部还通过 拆箱/装箱来方便你使用基本类型包装类的方法,不仅如此,js还提供了一个用大括号即可创建对象的便捷方式,这比java真的时方便太多了。
conststr1 = '哈哈';
conststr2 = newString('哈哈');
constobj1 = {name:'丁少华'};
constobj1 = newObject();
obj1.name = '丁少华';
面向对象之封装
我们前端常用组件化,其实就是封装的一种. 通俗理解,封装就是包裹
在面向对象程式设计方法中,封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。
封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。
要访问该类的代码和数据,必须通过严格的接口控制。
封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。
适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。
封装的优点
- 良好的封装能够减少耦合。
- 类内部的结构可以自由修改
- 可以对成员变量进行更精确的控制。
- 隐藏信息,实现细节。
把大象装冰箱分几步
冰箱一个类 ,大象一个类 ,装进冰箱 是冰箱类的一个方法
然后(1)打开冰箱门 (2)把大象放进去 (3) 关上冰箱门。
这是一种面向对象的思想体现
//定义冰箱
class Firdge {
open() {
console.log("打开冰箱");
}
close() {
console.log("关上冰箱");
}
}
//定义大象
class Elephant {
load(dx) {
console.log("存储大象"+dx);
}
}
//测试
//创建冰箱对象
const f = new Firdge();
//创建大象对象
const eh = new Ehephant();
//第一步:打开冰箱门
f.open();
//第二步:把大象放进去
eh.load(eh);
//第三步:关上冰箱
f.close();
讲一下js封装具体办法
es6(不完美)
最方便的class封装(但是尚不支持可见性修饰符,需要借助于ts,但是可见性修饰符一直都是js的保留字,相信不久就可以实现)
class Utils {
getTime() {
return new Date();
}
}
const utils = new Utils();
console.log(utils.getTime());
es5(有完美的办法)
在基于原型的面向对象方式中,对象(object)则是依靠构造函数(constructor)和原型(prototype)构造出来的。实现起来则较为复杂
面向对象语言的第一个特性毫无疑问是封装,在 JS 中,封装的过程就是把一些属性和方法放到对象中“包裹”起来,那么我们要怎么去封装属性和方法,或者说怎么去创建对象呢(后文统一说创建对象)?下面用逐步推进的方式阐述:
象字面量 --> 工厂模式 --> 构造函数 --> 原型模式 --> 构造函数+原型模式
这种创建对象,对象属性是没有权限限制的,跟es6的类本质上是一样的,都是公开的,都可以obj.xxx
来获取,可以配合闭包的方式完成有权限(可见性)修饰的效果。进而达到完美的封装
。即借助闭包来实现一个完美的类来达到封装
如:
const Animal = function () {
return function (sName) {
this.name = sName;
const desc = '哈哈';
this.getDesc = function () {
console.log(this.name+'-'+desc);
}
}
}();
Animal.prototype.show = function () {
console.log(this.name+'-'+this.desc);
};
var animal = new Animal('cat');
animal.show();//cat undefined
animal.getDesc();//animal cat
面向对象之继承
继承是 OO 语言中的一个最为人津津乐道的概念。许多 OO 语言都支持...其实现继承主要是依靠原型链来实现的。ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。
以上摘抄自JavaScript高级程序设计(第3版)
但是几年前的es6发布之后,js便可以通过class简单的extends来实现继承。(现在有些浏览器不支持,经过babel编译后,又变成了通过原型链继承。所以class称其为语法糖)
继承的概念
继承是js面向对象编程技术的一块基石,因为它允许创建分等级层次的类。
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
生活中的继承:
兔子和羊属于食草动物类,狮子和豹属于食肉动物类。
食草动物和食肉动物又是属于动物类。
所以继承需要符合的关系是:is-a,父类更通用,子类更具体。
虽然食草动物和食肉动物都是属于动物,但是两者的属性和行为上有差别,所以子类会具有父类的一般特性也会具有自身的特性。
js中的继承
在 Js 中通过 extends 关键字可以申明一个类是从另外一个类继承而来的,一般形式如下:
class Animal{
sleep(){
console.log('睡觉');
}
}
class Cat extends Animal{
}
class Dog extends Animal{
}
const xiaohua: Dog = new Dog();
xiaohua.sleep();
// 寄生组合继承
function Animal() {
}
Animal.prototype.sleep = function(){};
function Dog() {
Animal.call(this);
}
Dog.prototype = Object.create(Animal.prototype);
var xiaohua = new Dog();
console.log(xiaohua.name)
如果不用继承来做的话,代码存在重复了,代码量大且臃肿,而且维护性不高(维护性主要是后期需要修改的时候,就需要修改很多的代码,容易出错),所以要从根本上解决这两段代码的问题,就需要继承,将两段代码中相同的部分提取出来组成 一个父类Animal。这个Animal类就可以作为一个父类,然后猫类和狗类继承这个类之后,就具有父类当中的属性和方法,子类就不会存在重复的代码,维护性也提高,代码也更加简洁,提高代码的复用性(复用性主要是可以多次使用,不用再多次写同样的代码)
继承类型
受限单一继承树的原型继承模型来说,不存在菱形问题,身为语法糖的class同样不支持多继承,但支持多重继承。但是火狐文档上说可通过混合的方式Mix-ins来实现,这个具体用到的较少不做多讲。
继承关键字
继承可以使用 extends 和 implements(ts特有) 这两个关键字来实现继承,而且所有的类都是继承于 Object,当一个类没有继承的两个关键字,则默认继承object(这个类在宿主(浏览器)环境内置中,所以不需要 import)祖先类。
extends是继承了具体的内容(属性和方法定义) 而 implements只继承了内容的预定义
class A{
}
interface B {
eat():any;
sleep():any;
}
interface C {
show():any;
}
class C extends A implements B,C {
eat(): any {
}
show(): any {
}
sleep(): any {
}
}
super 与 this 关键字
super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。
this关键字:指向自己的引用。
class Animal {
eat() {
console.log("animal : eat");
}
}
class Dog extends Animal {
eat() {
console.log("dog : eat");
}
eatTest() {
this.eat(); // this 调用自己的方法
super.eat(); // super 调用父类方法
}
}
const a = new Animal();
a.eat();
const d = new Dog();
d.eatTest();
构造器
子类是不继承父类的构造器的,它只是调用(隐式或显式)。
如果子类不显式声明构造,则默认继承父类
若声明,则用自己的。
声明了还想用父类的,请用方法super(xxx)
因为js中的方法没有唯一签名,所以构造无法重载,所以类中的构造函数永远只有一个,也自然不存在有/无参构造
这种说法
class Animal {
name: string;
age:number = 0;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name:string) {
super(name);
}
}
const d = new Dog('xiaohua');
console.log(d); // Dog { age: 0, name: 'xiaohua' }
面向对象之重写与重载
重写(Override)
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
在面向对象原则里,重写意味着可以重写任何现有方法。实例如下:
class Animal {
public move(): void {
console.log('动物可以移动');
}
}
class Dog extends Animal {
public move(): void {
console.log('狗可以跑和走');
}
}
const a: Animal = new Animal(); // Animal 对象
const b: Dog = new Dog(); // Dog 对象
a.move();// 执行 Animal 类的方法
b.move();//执行 Dog 类的方法
在上面的例子中可以看到,尽管 b 属于继承了 Animal ,但是它运行的是 Dog 类的 move方法内的内容。
重载(Overload)
因为js概念里,函数也是普通对象类型(这跟java不一样,java函数是特殊类型,且存在由方法名和形参列表共同组成方法签名,来确保每个方法的唯一性质),且没有函数签名js的参数是由包含0或者多个值的数组来表示的。
它所谓的命名参数只是提供便利,但不是必须的js没有这些条条框框,解析器不会验证命名参数,所以说js没有签名。
由于没法确定每个同名函数的唯一性,且是普通对象,重复定义会被最后定义覆盖。所以js没有方法重载这个概念。
重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。
不过我们可以通过判断arguments来模拟重载特性
也可以通过ts来书写重载。ts是支持的,写法一样,但是和java等语言的原理不同。
ts重载是发生在编译阶段(因为js本身不支持)。而java是本身支持运行时候去调用识别
面向对象之接口(ts特有【类类型】)
与C#或Java里接口的基本作用一样,TypeScript也能够用它来明确的强制一个类去符合某种契约。进而来规范开发。
接口(英文:Interface),是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。
接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。
除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。
接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。
interface Animal {
eat(): void;
sleep(): void;
}
class Dog implements Animal{
public eat(): void{
console.log("狗吃");
}
public sleep(): void{
console.log("狗睡");
}
}
面向对象之抽象类(ts特有)
介于类和接口之间的特性,属于特殊类类型。
也不可以被实例化。
内部既可以由方法的声明,也可以有完正的方法的实现。
用来被继承的,继承的类可以实现部分接口
面向对象之多态
多态指的是一类事物有多种形态,即同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。是面向对象编程的又一个重要特征。
多态不是一个语法,而是符合一些特征表现叫法。
多态性是对象多种表现形式的体现。
现实中,比如我们按下 F1 键这个动作:
- 如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;
- 如果当前在 Word 下弹出的就是 Word 帮助;
- 在 Windows 下弹出的就是 Windows 帮助和支持。
多态的好处:
增加了程序的灵活性, 以不变应万变,不论对象千变万化,使用者都是同一种形式去调用
多态存在的三个必要条件
重写(继承)、重载、向上转型:
其中重写,js支持
重载js不支持,可以通过额外处理来支持
向上转型,因为js是弱类型语言,不支持数据类型,ts支持(编译阶段)
也就是说对于多态的支持,目前js只支持一种
总结
面向对象三大特征:封装、继承、多态
基于封装,引出了js如何创建对象(class、function、原型+闭包创建私有变量等)
基于继承,引出了继承的一些知识点,如extends、super、重写重载、abstract、interface
总的来说 js的面向对象支持还没有其他传统语言那么丰富严谨,正式因为这些宽松
语法,也让js变得更加有趣。随着js不断的更新迭代,相信其语法特性也更加丰富和严谨
扩展
可以了解aop,面向切面编程。比如ts的装饰器(注解),就是其具体的应用