一文搞懂Js中的面向对象


前言

面向对象是Js中永恒不变的话题,学好Js的面向对象编程能很大程度上提高代码的重用率和可维护性。这篇文章打算从基本概念到重点难点通过案例演示带你掌握面向对象编程。

基本概念

  • 对象是程序中描述现实中一个具体事物的属性和功能的程序结构,程序中集中存储一个事物的属性和功能的一个存储空间
  • 面向对象编程就是程序都是先将数据保存在对象中,再按需使用对象中的成员数据。今后所有程序基本都使用面向对象思想开发的。
  • 为什么要使用面向对象编程呢?就是便于大量数据的维护和使用。
  • 面向对象三大核心: 封装 继承 多态

封装

封装就是创建一个对象结构,集中存储一个事物的属性和功能,这样便于大量数据的维护和使用,今后只要使用面向对象编程,都要先把数据和功能封装在对象中,再按需使用。那么如何封装对象呢?有三种方式。

1、 用{ } 创建

如果在创建对象时,就知道对象中的所有内容就直接用大括号创建。

var obj={
  属性名: 属性值,
    ... : ... ,
  方法名: function(){
    ... this.属性名 ...
  }
}

有几个需要注意的点,我们来用案例引出。

var lilei = {
  sname:"li Lei",
  sage:25,
  intr:function(){
    console.log(`I am ${sname},I am ${sage} years old`)
  }
};
lilei.intr();

有一个对象lilei,sname、sage是它的属性,intr()是它的方法(功能,会自我介绍,和对象的概念结合着理解一下对象到底是什么)

扩展:为什么有些函数叫函数,有些函数叫方法?保存在对象中的函数叫方法,不属于任何对象的函数就叫函数,比如ParseInt(),ParseFloat()。因为方法其实就是保存在对象内的函数,所以,方法除了查找方式不一样之外,其余用法和普通函数完全一样。

  • 那大家看看这段代码输出什么,有些人可能看出来了,报错sname is not defined。大部分人都知道,intr里的sname、sage前都要加this,那为什么不加this就报错呢,对象里的方法为什么就访问不了对象里的属性呢?非要加个this才能访问。首先,intr里存的是什么?是函数吗?intr只是个变量,变量只能存原始值,存不了函数,intr里存的是引用function的地址,每次调用intr()都临时创建function作用域。
  • 那我们来看一下function的作用域链,只有function本身的临时作用域和window,有人会说,那lilei呢?要知道Js中没有块级作用域,只有函数作用域,lilei根本不是作用域,所以不存在于作用域链中,程序只能访问作用域链中的变量,而window和临时创建的function中都没有sname和sage,所以报错。
  • 那怎么能让它访问到呢(它就是程序,我们可以把它当作一个人)?我就在想lilei里有啊,它怎么不会去lilei里拿呢?程序是很守规矩的,它不会擅自去对象里拿东西,即使它可以去拿,程序里那么多的对象,它也会懵逼的,我去哪个对象拿哪个sname?
  • 我们要给它一个明确的信号让它进lilei里,就是点操作,我们可以在function的sname、sage前加 lilei.,意为先找到lilei再访问里面的属性,也可以拿到sname、sage输出,那为什么我们都知道加this而不加对象名呢?紧耦合,对象名是可能改变的,如果对象名lilei改变了,function中的lilei也要改,不改就会报错,这就是紧耦合。
  • 程序中我们要尽量避免紧耦合,提高代码的可维护性,这就要用到this。this是自动获取正在调用当前函数 . 前的对象的关键词,如果我们希望对象名怎么变化以及是否有对象名都能获得当前对象自己时,就要用this。注意: this的判断一定不要看它保存在哪个对象下,this只和调用时点前的主语对象有关!调用时点前是谁,this就自动指向那个函数。举例演示一下
    var lilei = {
    sname:"li Lei",
    sage:25,
    intr:function(){
      console.log(`I am ${this.sname},I am ${this.sage} years old`)
    }
    };
    lilei.intr();
    var fun = lilei.intr;
    fun();
    这段代码中 fun 和 lilei.intr 引用着同一个函数,运行之后却发现一个能输出一个输出 undefined,这就是因为this的指向不同,fun()调用时没有点,前面没点的自动属于window,也就是说fun()就是window.fun(),thid指向window,所以window.sname和window.sage为undefined。关于this还有很多用法,有兴趣的可以去搜一下资料,我在这就不一一说明了。

2、用new创建 2步

如果在创建对象时我们还暂时不知道中的成员,就只能先创建一个空对象,等之后知道了属性成员再随时向对象中添加属性。

  • 1.先创建一个空对象:
    var obj=new Object();
    其中new可省略, ()可省略,但是不能同时省略
  • 2.向对象中添加新属性和新方法:
    使用强行赋值方式,就可自动添加新属性和方法:
    obj.属性名=值
    如果obj中有这个属性,则修改该属性的值。如果obj中没有这个属性,就会自动添加一个,并将值保存进去。
    obj.方法名=function(){ ... }
    如果obj中有这个方法,则替换方法中的函数为新函数。如果obj中没有这个方法,就会自动创建这个方法,并将等号后的function保存进去。

这里我们来思考个问题,为什么js中一个对象创建后,竟然可以随时添加新属性和新方法?这在其它语言里简直不可能。这里就揭示了Js中比较重要的一个原理: Js中一切对象底层其实都是关联数组

有人会说,你咋知道?那我们来找一下证据
对象和关联数组相比:
1、结构: 都是名值对儿的集合
2、访问对象或数组中不存在的位置,不报错,而是返回undefined而已!
3、强行给对象或数组中不存在的位置赋值,不会报错,而是自动添加新成员
4、无论是访问对象的属性还是访问数组的元素,都可用两种方式访问:[“成员名”] 和 .成员名 翻译为[“成员名”] ,注意: 如果属性名是一个变量,或动态生成的名字,就只能用[],不能用点。
5、都能用for in遍历。
6、都能用delete关键词删除一个现有属性。

3、用构造函数创建

前两种创建对象的方式都有一个问题: 一次只能创建一个对象。如果反复创建多个相同结构的对象时,代码就会很繁琐!这个时候就要用构造函数创建了。
构造函数是描述同一类型的多个对象统一结构的函数。 如果需要反复创建多个相同结构的对象时,才需要构造函数,目的为了重用结构
使用构造函数创建对象需要2步:

  • 定义构造函数,描述该类型所有对象的统一结构。
    function 类型名(形参,...){
    this.属性名=形参; 
        ... = ...
    this.方法名=function(){
      ... this.属性名 ...
    }
    }
  • 调用构造函数,反复创建多个相同结构的对象。
    var obj=new 类型名(实参,...)

我们做个例子演示一下

function Student(sname,sage){
  this.sname = sname;
  this.sage = sage;
  this.intr = function(){
    console.log(`I am ${this.sname},I am ${this.sage} years old`)
  }
};
var lilei = new Student("lilei",25);
console.log(lilei);
var hmm = new Student("hmm",21);
console.log(hmm);

大家想一下为什么new一个Student就出来一个新的对象呢,new做了什么呢?

  • 其实new做了4件事:
    1.创建一个新的空对象
    2.自动让新创建的子对象继承构造函数的原型对象
    3.调用构造函数: 1). 将所有this指向new出来的新对象 2). 通过强行赋值的方式为新对象添加规定好的新属性和新方法
    4.返回新对象的地址保存在变量中

问题: 大家可以发现打印出来的lilei和hmm中都有一个intr函数,创建出来的所有子对象都会有intr函数。因为方法的定义放在了构造函数中。那么每次调用构造函数,都要创建一遍相同方法的副本。调用几次,就反复创建几个相同方法的副本。——浪费内存!
解决: 继承,往下看。

继承

  • 继承就是:父对象的成员,子对象无需重复创建,就可直接使用!只要发现,多个子对象,都需要使用一个函数或属性值时,都要用继承方式保存这个共有的成员。可以让代码重用,还可节约内存。

  • js中的继承都是通过原型对象方式实现的,那么什么是原型对象呢?原型对象就是集中存储一类对象共有成员的父对象,原型对象不用自己创建,每创建一个构造函数(我们把它称为妈妈),自动附赠一个原型对象(我们称为爸爸)。当多个子对象都需要一个功能或属性值时,都要集中定义在父对象中一次即可,所有子对象自动继承使用。

  • 构造函数中都有一个prototype属性,指向自己的原型对象。继承也不用自己设置,new的第2步: 自动让新创建的子对象继承构造函数的原型对象。子对象都有一个__proto__(双下划线),被自动指向构造函数的原型对象

  • 凡是从__proto__指出的关系都是继承关系, 当我们试图用孩子调用一个函数或访问一个属性时,如果孩子自己有,则优先使用孩子自己的成员。万一孩子没有,js引擎会自动沿__proto__向上去父对象中查找

  • 有了原型对象,今后构造函数中不应该再包含方法定义了!应该只包含属性定义。所有方法定义,都要集中放在原型对象中:

    构造函数.prototype.方法名=
    function(){
    
    }

说了那么多不知道你理解没有,我们用继承来优化一下上面的例子吧

function Student(sname,sage){
  this.sname = sname;
  this.sage = sage;
};
Student.prototype.intr = function(){
    console.log(`I am ${this.sname},I am ${this.sage} years old`)
  }
var lilei = new Student("lilei",25);
console.log(lilei);
lilei.intr();
var hmm = new Student("hmm",21);
console.log(hmm);
hmm.intr();

可以看到lilei和hmm里没有intr方法,但是却可以调用lilei.intr和hmm.intr,就是因为我们把intr放到了构造函数的原型对象中,所有新建的子对象自动继承自构造函数的原型对象(妈妈生的孩子继承爸爸,所以爸爸的东西孩子能用),所以父对象里的方法所有孩子都能用。

  • 自有属性和共有属性:

    • 自有属性: 保存在当前对象本地,仅归当前对象自己所有的属性
      我们可以判断一个属性是不是自有属性:
      if(当前对象.hasOwnProperty("属性名")),如果返回true,说明”属性名”归当前对象自己所有。如果返回false,说明”属性名”归父对象所有,多个子对象共用

    • 共有属性: 保存在父对象原型对象中,归多个子对象共有。获取属性值时自有属性和共有属性都可用子对象.直接获得。

    • 修改属性值时: 自有属性,可用子对象.直接修改。共有属性,不能用子对象.直接修改,必须用原型对象.共有属性=值。如果强行用子对象修改共有属性,结果会为这个子对象自动创建一个自有的同名属性,从此这个子对象,在这个属性的使用上,与其他子对象分道扬镳。

  • 内置类型/对象的原型对象:

    • 内置类型/对象: ECMAScript标准中已经规定的,浏览器中自带的类型。
    • 包括11种: String Number Boolean Array Date RegExp Math Error Function Object global (在浏览器中被window代替)。
    • 凡是能new的(除了Math和global都能new)都是一个构造函数,凡是构造函数都牵扯着一个家庭(类型)!每种类型都和Student类型一样包含2部分:

    1.构造函数: 负责创建该类型的子对象,构造函数中的this.属性,都会成为将来孩子身体里的自有属性
    2.原型对象:负责集中保存该类型下所有孩子共用的属性值和方法。凡是原型对象中的属性值和方法,该类型的子对象无需重复创建,都可直接使用

  • 原型链:原型链是由多级父对象,逐级继承形成的链式结构, 每个对象的原型链上保存着这个对象的所有父级对象。只要是这个对象的父级对象,其中的属性,子对象都可直接访问。原型链控制着成员的使用顺序:先自有,再共有。

  • 我们可以把继承关系比作家庭中的成员(看下图理解),新创建的对象lilei是孩子,生出lilei的构造函数Student是妈妈,构造函数的原型对象Student.prototype是lilei的爸爸(妈妈的老公),而lilei的爸爸又继承谁呢?其实每个原型链上都有一个顶级父对象Object.prototype,lilei的爸爸继承Object.prototype(lilei的爷爷)

  • 那么妈妈又继承谁呢?function Student(){}其实就是var Student = new Function(){},也就是说Student的妈妈是Function(lilei的姥姥),那Student就继承Function.prototype(lilei的姥爷),但与现实不同的是lilei的姥爷继承lilei的爷爷(Object.prototype),而妈妈,姥爷,姥姥的东西lilei不能用,只能使用原型链上的方法(红圈圈着的地方)。
    原型链

多态

多态是一个函数在不同情况下表现出不同的状态,包括:

  • 1、重载overload。

  • 2、重写override。主要说一下重写。
    override重写是在子对象中定义和父对象同名的自有成员,因为继承来的东西,不一定都是好用的。只要觉得从原型中继承来的成员不好用,就要在子对象中重写同名的成员。

  • 自定义继承: 3种:

    • 仅修改一个对象的父对象:
      child.__proto__=fatherObject.setPrototypeOf(child, father);
    • 同时修改多个子对象的父对象。构造函数.prototype=新father强调: 必须在创建子对象之前更换。之后创建的所有子对象,都自动继承新爹。
    • 两种类型间的继承: (有空更新吧,或者你也可以搜一下资料了解一下)
    • 有空再详细举例演示一下吧,写到这好累。。。

扩展: for in

  • in : 遍历对象及其父对象中所有可用的属性名
  • in不仅遍历当前对象,而且会沿原型链,继续遍历父对象中可用的属性名。
  • 如果只想遍历当前对象自己的属性时,需要加判断条件:
    if(当前对象.hasOwnProperty(key))才执行操作, 屏蔽了那些不属于该对象自有的,继承来的属性。

希望对你们理解面向对象编程有帮助,欢迎交流 个人博客


文章作者: Love--金哥哥
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Love--金哥哥 !
评论
 上一篇
如今重新回看Es5、Es6你会不会有不一样的理解 如今重新回看Es5、Es6你会不会有不一样的理解
前言当初学习Es5、Es6时你是不是有一些疑问,关于有些用法、为什么这么用以及这样用的好处你现在搞懂了吗?奋力奔跑的同时别忘了那些可以为你加速的“小伙伴”。 ES51. 严格模式:比普通的js运行更严格的机制,js语言存在很多广受诟病的缺陷
2020-04-06
下一篇 
实现浏览器内多个标签页面之间通信的四种方法 实现浏览器内多个标签页面之间通信的四种方法
一一演示实现浏览器内多个标签页面之间通信的四种方式并详细解释其原理
2020-03-30
  目录