php面向对象

面向对象

引入:计算机编程在历史的发展长河中,经历了多次版本变革,变化的轨迹是伴随着硬件的发展和人们对于计算机的认知以及需求。

  • 机器语言:即开发者(科学家)使用

    0
    

    1
    

    组成命令,然后在特定计算机上执行

    • 优点:执行效率高
  • 缺点:开发难度大、移植性差、开发成本高

  • 汇编语言:开发者使用简洁英文字母符号组成,让计算机读取后根据符号进行加工执行

  • 优点:指令简单明了、推广性高

  • 缺点:移植性差、功能简单

  • 高级计算机语言:开发者使用类似自然语言的符号组成,高级语言根据编程思想分为面向过程编程面向对象编程两种,然后系统对程序代码进行编译(需要第三方编译器)然后执行

  • 优点:移植性强、可读性强、推广性非常高

  • 缺点:执行效率降低

面向过程编程

  • 将要解决的问题(功能需求)分解成具体的步骤,然后通过函数编程实现每一个步骤,最后通过函数规定好的顺序调用完成
  • 面向过程编程思想的优点
  • 能够针对步骤拆分,进行模块化封装(函数)
  • 可以实现代码复用,从而节省开发成本
  • 面向过程编程思想的缺点
  • 不够灵活维护,流程一旦确定就必须按照既定方式执行到底。

小结

1、计算机编程从对开发人员要求极高到要求不高,是一代代人坚持不懈的结果

2、面向对象编程是目前最为符合人类思维逻辑的一种编程思想

一、面向对象编程思想

面向对象编程思想概念及原理

定义:面向对象编程也叫做OOP编程(Objected Oriented Programming),是一种基于面向过程的开发思想。与面向过程强调分解事务步骤相似,面向对象更需要追求事务操作的“主体”,也就是对象

  1. 面向对象编程是一种编程思想,不是一种具体技术
  2. 面向对象是在面向过程基础之上发展而来,因此也是一种模块化编程思想(有函数)
  3. 面向对象能够更加方便的实现代码的重复利用(适用于大型项目)
  4. 在面向对象思维中,任何动作的执行或者数据都属于对象(一切皆对象)

小结

  1. 面向对象编程是一种编程思想,与技术无关
  2. 面向对象编程的本质是增加数据和功能的操作主体,即对象
  3. 面向对象中所有的数据和功能都是由主体(对象)来调用和操作

二、面向对象基础

思考:按照面向对象编程思想逻辑,先编写好对应的功能函数,然后再放到某个主体中。那么已经写好的函数如何放到某个主体中去呢?

引入:上述图片中描述的是思想的实现,在实际编写代码的过程中,肯定不是这么去实现的。面向对象中,对于一些思想产物有一些不同的称呼。

1.面向对象关键字说明【掌握】

定义:面向对象关键字说明,即在面向对象思想设计的程序中,有一些关键的结构用词。在面向对象中,关键结构包括以下几个:

  1. 类:class,是定义面向对象主体的最外层结构,用来包裹主体的数据和功能(函数)。类是一类具有共性事务的代表,代表的是事务的共性。
  2. 对象:object,是某类事务的具体代表,也是实际数据和功能操作的具体单元,也被称之为实例(instance)
  3. 实例化:new,从一个抽象的概念(类)得到一个符合抽象概念的具体实例(对象)的过程
  4. 类成员:member,指类class结构中的所有内容,类成员里有三种
  • 方法:method,本质是在类class结构中创建的函数,也称之为成员方法或者成员函数
  • 属性:property,本质是在类class结构中创建的变量,也称之为成员变量
  • 类常量:constant,本质是在类class结构中创建的常量

总结:面向对象基础中没有本质多出很多东西,基本只是将原来面向过程的内容进行一层包裹而已。


思考:面向对象编程到底是如何用代码实现这样的编程思想的呢?

引入:其实所有的编程都大同小异:编写程序–>运行程序。只是根据不同的思想会有不同的关键字让系统识别而已。

2.面向对象简单技术实现【掌握】

定义:面向对象思维的实现,就是利用面向对象提供的关键字和代码规范来进行编程,而这里面最核心的两个部分就是类class和对象object

  1. 定义类基本语法:class 类名{}
<?php
    //定义一个空类
    class Nothing{    
}
  1. 类class是一种结构,如果写好没有语法错误的情况下,代码不会执行(与函数定义一样),也无法打印输出
<?php
clsss Nothing{}
var_dump(Nothing);        //错误,提示未定义的常量
  1. 类的命名规范:类的命名规范与函数类似,区别在于人为的通常会将类名的首字母大写
<?php
//有效类名
class My1{}
class My_1{}
class _My1{}
 
//无效类名
class 1My{}
class 1_my{}
  1. 如果碰到多单词组成的类名,通常使用驼峰法
<?php
class MyClass{}
  1. 类无法直接访问,需要得到类的具体对象才能访问,可以通过实例化new来实现对象的创建:new 类名[()]
<?php
class My{}
new My;            //创建一个My类的对象
new My();        //创建一个My类的对象
  1. 对象创建后可以直接使用或者打印,但是为了方便后续使用,通常使用变量保存实例化的对象
<?php
class My{}
var_dump(new My);    //直接打印输出对象
 
$m = new My;        //保存对象
var_dump($m);
 
//打印结果
object(My)#1 (0) { } object(My)#2 (0) { }
object:对象
(My):类名 
#1:编号
(0):成员变量(属性)个数   
{}:具体成员变量信息(键值对)

总结

  1. 通过class关键字 + 类名 +{}创建类
  2. 类是一种结构,不会自动运行,也不能输出
  3. 通过new 类名实例化对象得到类的具体对象(具象)
  4. 可以通过new实例化无限个对象

思考:上面定义的类就是个空类,里面什么都没有,类里面能够放些什么东西呢?

引入:通常空类没有什么价值,需要往类里填充类成员,这样的类才有价值,这样的类实例化得到的对象才有意义。

3.类成员【掌握】

定义:类成员,指直接定义在类结构{}内部的一级成员,即直接依赖{}的成员。在PHP中类成员有三种:成员变量(属性),成员方法(成员函数)和类常量

  1. 成员变量:就是在类结构{}下直接定义的变量,但是定义的方式与普通变量稍微有点不同,需要在变量名字前使用一个关键字public,定义语法:public 变量名字 [= 值];,成员变量可以赋值也可以不赋值。
<?php
//定义买家类:买家有姓名,有钱
class Buyer{
    //$name;           //错误
    public $name;         //正确:没有赋值
    public $money = 0;     //正确:有赋值
}
  1. 成员变量访问:成员变量必须通过对象才能进行访问,也就是需要先通过实例化得到对象,然后通过对象实现对成员变量的增删改查:访问语法:$对象名->属性名;
<?php
#定义买家类
class Buyer{
    public $name;    
    public $money = 0;     
}
 
$b = new Buyer();
//访问属性
echo $b->money;
//修改属性
$b->money = 1000;
//删除属性
unset($b->name);
//新增属性
$b->age = 20;

注意:删除属性和新增属性通常使用较少,更多的属性操作是访问和修改

  1. 成员方法:就是在类结构{}下定义的函数,函数内部的内容与原来函数结构一样,可以有分支、循环结构等
<?php
//定义买家类
class Buyer{
    //定义方法
    function display(){
        echo __CLASS__;    
    }
}
  1. 成员方法访问:成员方法也是需要通过对象进行访问的,访问语法为:$对象名->方法名字();
<?php
//定义买家类
class Buyer{
    //定义方法
    function display(){
        echo __CLASS__;    
    }
}
 
//实例化
$b = new Buyer();
$b->display();
  1. 类常量:类常量是在类结构{}下定义的常量,类常量的定义只能使用一种方式:const 常量名 = 值;
<?php
//定义买家类
class Buyer{
    //定义类常量
    const PI = 3.14;
}  

注意:类常量不是由对象来进行访问,所以暂时不做访问讲解,到后面知识再讲。

  1. 类成员中:属性、类常量和方法都可以无限定义,但是定义的原则是相关性。除了以上三个类成员,不能在类结构{}中直接写其他任何代码
<?php
class Buyer{
    echo __CLASS__;                    //错误
    define('PI',3.14);                //错误
    if(true){ echo 'hello world'}     //错误
}    

总结

  1. PHP类结构中有三种成员:属性、方法和类常量
  2. 类结构中只能有三种成员,不限数量,其他代码需要写到方法里面,否则报错
  3. 类中属性和方法的访问方式都是通过对象来调用: 对 象 − > 属 性 名 / 方 法 名 ( ) ; 注 意 属 性 名 不 带 对象->属性名/方法名();注意属性名不带 >/()符号
  4. 类中定义属性不能直接使用属性名,需要使用符号public修饰

思考:属性本质是变量,只是放到类结构中而已,为什么一定要使用public呢?有什么用呢?

引入:在面向对象中,属性或者方法都是用来给对象访问的,但是有的时候并不是所有的内容都是允许对象在外部访问的,这个时候就出现访问修饰限定符来控制访问位置。

4.访问修饰限定符【掌握】

定义:访问修饰限定符,是一种用在属性或者方法前的修饰关键字,是用来控制属性或者方法的访问位置的。在PHP中访问修饰限定符分为三种:public、protected和private

  1. 首先需要明白一组概念类的内部和外部
  • 类内部:是指类定义的内容内部,即类名后{}内部
  • 类外部:是指类定义的外部内容,即类名后{}之外的所有地方
  • 类成员的访问权限控制是内部访问(私有)、链内部访问(受保护)和全部访问(公有)
  1. public:公有,即表示所修饰的内容(属性或者方法)可以在当前类的内部访问,也可以在类的外部访问
<?php
class Saler{
  //公有属性
  public $count = 100;
}
$s = new Saler();
echo $s->count;                //正常输出100
  1. protected:受保护,即表示所修饰的内容只能在类的内部访问(在学习继承时再讲)
<?php
class Saler{
  //受保护属性
  protected $discount = 0.8;
}
$s = new Saler();
echo $s->discount;                //错误:受保护属性不允许在类外访问
  1. private:私有,即表示所修饰的内容只能在当前类的内部访问
<?php
class Saler{
  //私有属性
  private $money = 100;
}
$s = new Saler();
echo $s->money;                //错误:私有属性不允许在类外访问
  1. 访问修饰限定符不只是限定属性,也用来限定方法
<?php
class Saler{
  //属性
  public $count = 100;
  protected $discount = 0.8;
  private $money = 100;
 
  public function getCount(){}    
  protected function getDiscount(){}
  private function getMoney(){}
  function getAll(){}                    #如果没有指定访问修饰限定符,默认public
}

总结

  1. 访问修饰限定符分为三种:public、protected和private,访问权限依次降低
  2. 访问修饰限定符限定的是成员到底在哪里能被访问,私有和受保护都只能在类内部访问,公有可以在任何地方访问(但都必须是对象去访问)
  3. 属性必须写清楚访问修饰限定符,方法可以省去(不建议),因为系统默认是public

思考:前面访问修饰限定符描述的只是在外部调用访问,怎么能够体现在类的内部访问呢?

引入:类的内部如果是在类结构{}后,除了类三种成员,不能有别的代码;但是类成员方法内部可以写任何代码,因此可以在类方法内部实现访问:此时方法运行的内部环境属于类内部。

5.类内部对象【掌握】

定义:类成员属性和方法的访问必须通过对象才能访问,类外部可以通过实例化得到类对象从而实现成员访问,但是类内部又不能访问类外部的对象(未知),此时类的内部就内置了一个对象$this代表来访对象。

  1. 尝试在类内部方法中访问属性
<?php
class Saler{
      //属性
      public $count = 100;
      protected $discount = 0.8;
      private $money = 100;
 
    public function getAll(){
        echo $count,$discount,$money;    //全部错误:提示未定义的“变量”
    }                    
}
$s = new Saler();
$s->getAll();

注意:方法本质是定义在类内部的函数,因此受制于作用域的问题,在方法内部访问的变量系统认定为局部变量(必须内部定义或者参数传入),否则就会提示未定义

  1. 类内部访问类成员,需要通过对象来进行访问
<?php
class Saler{
      //属性
      public $count = 100;
      protected $discount = 0.8;
      private $money = 100;
 
    public function getAll(){
       //需要获取到对象名字:因为方法本身就是函数,访问外部全局变量可以通过global引入实现
        global $s;
        echo $s->count,$s->discount,$s->money;        #正确输出
    }                    
}
 
$s = new Saler();
$s->getAll();
  1. 思考这样一个问题:类一开始定义好,但是对象实例化是在需要使用该类的时候来调用的,如何保证外部保存对象的变量一定是 s 呢 ? 对 象 可 以 创 建 多 个 , 如 果 有 多 个 对 象 变 量 , 那 内 部 怎 么 实 现 呢 ? 如 果 系 统 内 部 能 够 检 测 到 是 哪 个 对 象 在 调 用 方 法 , 并 且 能 够 复 制 对 应 对 象 就 好 了 , 这 个 就 是 类 成 员 方 法 ‘ 内 置 对 象 s呢?对象可以创建多个,如果有多个对象变量,那内部怎么实现呢?如果系统内部能够检测到是哪个对象在调用方法,并且能够复制对应对象就好了,这个就是类成员方法`内置对象 sthis`
<?php
class Saler{
      //属性
      public $count = 100;
      protected $discount = 0.8;
      private $money = 100;
 
    public function getAll(){
        var_dump($this);
        echo $this->count,$this->discount,$this->money;        #正确输出
    }                    
}
 
$s = new Saler();
$s->getAll();
  1. 由上述代码可见: t h i s 代 表 的 是 对 象 , 而 this代表的是对象,而 thisthis所在环境为类内部的方法内部,所以$this对象是在类内部访问,因此可以访问所有的属性和方法,不受访问修饰限定符限制
  2. $this、class和new之间的关系原理
  • class是定义类结构,属于非执行段代码,因此会被加载到代码段(编译阶段)
  • new是实例化对象,先判定类在内存(代码段)是否存在
  • 类不存在,报错;
  • 类存在,将类内部的属性部分复制一份,然后在内存(堆区)开辟一块内存空间,将属性放到里面,同时内部有一个指针指向类的内存空间(代码段)
  • 对象访问属性即访问的是对象空间里存储的部分
  • 对象访问方法是对象通过内部指针找到类空间中的方法,然后在内存(栈区)开辟运行
  • $this是系统在方法内置的对象通用名字
  • 对象在调用方法的时候,系统会自动找到对象所保存的内存地址(堆区),然后把地址赋值给$this
  • 方法内部的 t h i s 就 代 表 调 用 当 前 this就代表调用当前 thisthis所在方法的外部对象
  • $this的本质是函数内部的一个局部变量,只是系统自动对其进行赋值,而且一定是调用方法的对象本身

总结

  1. 类内部方法内有一个内置对象$this,代表访问该方法的外部对象
  2. 类在实例化对象的时候要保证内存中有该类
  3. 一个类可以实例化多个对象,每个对象访问成员方法时,$this就代表对应对象

思考:面向对象中,属性在定义类结构的时候是否需要赋值?到底该如何使用访问修饰限定符呢?

引入:站在开发者的角度,并没有任何强制规定属性在类中定义的时候就需要初始化,也没有任何规定访问修饰限定符到底该用哪个。但是从业务需求和隐私保护角度出发,有一些规则需要我们遵循。

6.面向对象开发规范【掌握】

定义:面向对象规范是指我们在开发时,在技术没有强制规定的情况下,应该采用什么样的方式来协助我们进行代码的书写。

  1. 属性的初始化:属性是类对于同类事务所抽离出来的共性数据,本身在类结构中没有价值,是当具体对象产生之后,属于对象本身的。因此在进行类中定义属性的时候,通常不会对属性进行初始化,除非属性本身的值也具有共性
<?php
class Saler{
      //属性
      public $count;             
    //某个卖家拥有的商品数量,每位具体卖家对象拥有的不可能一样,所以没必要初始化
      protected $discount;
    //某个卖家针对销售的折扣,同样没有统一的价值
      private $money = 0;
    //某个卖家的账户余额,任何一位卖家一开始做生意的时候,账户余额都为0,所以可以初始化
 
?>
  1. 属性的初始化:属性在类中初始化的基本判定就是数据是否初始化的时候统一,如果数据统一(即初始化),那么所有对象在生成之后,就拥有对应的值;如果不初始化,意味着所有对象在生成后,应该对属性进行赋值。
<?php
class Saler{
      //属性
      public $count;             
      private $money = 0;
}  
 
//实例化
$s1 = new Saler();
$s2 = new Saler();    //所有对象都有两个属性,其中$count都没有数据,而$money都为0
 
//生成对象后,对象对自己的属性进行初始化
$s1->count = 100;    
$s2->count = 1000;
?>
  1. 访问修饰限定符选择:访问修饰限定符是用来限制类成员被对象访问时对象所处位置的。访问的权限从public、protected到private依次变小。使用规则如下:
  • 设定好的类成员本身不会被外部用到,那么应该使用private或者protected
  • 设定好的类成员一定会给外部访问,使用public
  • 属性通常private居多,如果需要外部访问属性,通常会定义相关方法来实现属性的查看和修改,因为可以在方法内对数据逻辑进行代码控制,安全
  • 总之一句话:尽可能增加类对成员的控制(尽可能多使用private,少使用public)
<?php
class Saler{
      //属性
      public $count;             
      private $money = 0;
 
    //增加方法操作私有属性money
    public function getMoney(){
        return $this->money;
    }
    public function setMoney($money){
        //可以对逻辑进行修改,对数据进行安全判定,保证数据的安全性
        $this->money = $money;
        //$this->money是属性,$money是外部传入的参数,二者同名但是性质完全不同
    }
}  

总结

  1. 属性在类中定义的时候,通常不会初始化值,除非所有类实例化得到的对象的某个属性需要是统一值
  2. 应该尽可能增加类对成员的控制,即使用范围较小的访问修饰限定符优先
  3. 属性通常是私有化的,一般是通过设定方法来实现属性的访问和修改

思考:属性一般不会在类定义的时候就初始化,那实例化对象的时候,属性的数据不都是为空吗?这样的数据有什么价值呢?

引入:属性属于对象使用,在类中初始化的确没有太多价值,但是对象实例化之后如果属性数据为空也没有价值。因此通常属性都是在对象实例化的时候进行初始化的,这个时候就需要用到一种自动构造的方式来完成

7.构造方法【掌握】

定义:构造方法__construct(),是一种类结构特有的特殊方法,该方法由系统规定好,开发人员在定义的时候只需要写一遍,有了构造方法的类在实例化对象之后,对象就会自动调用。

  1. 构造方法实现:在类中增加一个方法__construct()即可
<?php
class Saler{
      //属性
      public $count;             
      private $money;
 
    //构造方法
    public function __construct(){
        echo __CLASS__;
    }
}
  1. 构造方法也是一个普通方法,不普通的地方在于,类实例化得到的对象会马上自动调用
<?php
//接上述代码
new Saler();                //输出Saler
  1. 构造方法的意义:构造方法是对象实例化的时候用来初始化对象的资源的,所以通常是用来初始化对象的属性或者其他资源初始化
<?php
class Saler{
      //属性
      public $count;             
      private $money;
 
    //构造方法:初始化属性
    public function __construct(){
        $this->count = 100;
        $this->money = 100;
    }
}
  1. 如果属性的数据在构造方法中初始化是固定写死的,那么与直接在定义类的时候初始化属性一样。意味着数据没有任何价值(所有对象都相同),因此通常是通过构造方法的参数来实现数据的外部传入
<?php
class Saler{
      //属性
      public $count;             
      private $money;
 
    //构造方法:初始化属性
    public function __construct($count,$money){
        $this->count = $count;
        $this->money = $money;
    }
}
  1. 一旦构造方法拥有了形参,那么对象在调用该方法的时候就需要传入对应的实参,而构造方法又是自动调用的,所以需要在实例化对象的时候使用new 类名(构造方法对应的实参列表)来实现
<?php
//接上述代码
$s1 = new Saler(100,100);
$s2 = new Saler(1000,1000);

注意:之前所说的new 类名new 类名()没有区别是因为没有构造方法,或者构造方法没有参数限定,一旦构造方法有了参数,那么new 类名 就不能直接使用了。

  1. 构造方法不管再怎么特殊,也是用户定义的方法,言外之意除了在实例化对象时对象会自动调用之外,我们也可以手动调用构造方法(但是一般没有价值,因为对象实例化时会自动调用)
<?php
class Saler{
      //属性
      public $count;             
      private $money;
 
    //构造方法:初始化属性
    public function __construct($count,$money){
        $this->count = $count;
        $this->money = $money;
    }
}
 
//实例化
$s = new Saler(100,100);            //系统在new Saler(100,100)好之后,会自动调用一次
$s->__construct(1000,1000);            //允许手动调用

总结

  1. 构造方法__construct()是一种系统内置的方法,该方法的特性是会在对象实例化之后,对象立即自动调用
  2. 构造方法的目的就是为了初始化资源,包含对象属性和其他资源
  3. 一旦构造方法定义好之后,且构造方法自带参数,那么就只能使用new 类名(参数列表)方式才能正确实例化
  4. 构造方法可以当做普通方法由对象调用(不建议)

思考:构造方法是用来实现资源的初始化的,那么有没有需求来实现资源的释放呢?

引入:理论上讲,一种资源被初始化通常需要在不用的时候给释放掉,这样的程序才是好的程序。而PHP有个特点,就是脚本运行结束后系统自动回收所有内存,这就决定了PHP开发者大多数时候不喜欢额外做释放资源的工作。同样的,对象有构造方法初始化资源,也有析构方法释放资源。

8.析构方法【了解】

定义:析构方法__destruct(),也是一种类结构中特殊的方法,与构造方法一样,也是系统规定好,只需要开发人员一遍即可,对象在被销毁时会自动调用。

  1. 析构方法实现:类中增加一个__destruct()方法
<?php
class Saler{
      //析构方法
    public function __destruct(){
        echo __FUNCTION__;
    }
}
  1. 析构方法调用:析构方法是在对象被销毁时自动,对象的“垂死挣扎”
<?php
//接上述代码
//实例化
$s = new Saler();
unset($s);            //删除对象
  1. 析构方法是对象用来调用释放对象中的资源,不是用来删除对象的
<?php
class Saler{
      //析构方法
    public function __destruct(){
        //销毁对象所占用资源的代码
    }
}
  1. 析构方法也是普通方法,可以由对象直接调用
<?php
//接析构方法实现代码
$s = new Saler();
$s->__destruct();        //思考:此时对象是否被销毁?
  1. 对象销毁情形
  • 保存对象的变量被用来保存其他数据,导致对象内存没有任何变量引用
  • 删除对象变量
  • 脚本执行结束:释放所有变量

总结

  1. 析构方法是一种对象销毁时自动调用的方法
  2. 析构方法是用来对象销毁自身所占用的资源
  3. PHP中脚本执行结束,系统会自动回收所有资源,因此一般PHP中很少使用析构方法

思考:在PHP中,变量赋值是值传递的,即变量赋值给另外一个变量,两个变量是不同的。如果变量保存的是对象,那么给变量赋值的时候,会产生两个对象吗?

引入:对象的产生是由实例化得到的,因此如果是将保存对象的变量赋值给另外一个变量,这个过程是不会产生新的变量的。

9.对象传值【了解】

定义:对象传值,其实就是将保存对象的变量赋值给另外一个变量,在PHP中,对象的传值是引用传递的:即一个对象变量赋值给另外一个变量,两个变量指向同一个对象的内存地址,即只有一个对象。

  1. 对象传值就是保存对象的变量赋值给另外一个变量
<?php
class Saler{}
$s1 = new Saler();
$s2 = $s1;
  1. 对象传值是引用传递,不管对象赋值给多少个变量,内存中只有一个对象
<?php
class Saler{}
$s1 = new Saler();
$s2 = $s1;
 
//证明
var_dump($s1,$s2);        //同一个对象
$s1->name = 'Saler';    //更改一个变量所保存对象的属性
echo $s2->name;            //输出Saler

总结:对象传值是引用传值,一般情况下不会在项目中用到。


思考:前面类成员中,属性和方法都已经清楚的知道是由对象来访问,唯独类常量没有被访问,类常量到底是如何访问的呢?

引入:类常量通过名字分析就知道是属于类的,因此类常量的访问是通过类来进行访问的。

10.范围解析操作符(类常量访问)【掌握】

定义:范围解析操作符,由两个冒号组成“::”,是专门用于类实现类成员操作的,可以实现类直接访问类成员。

  1. 类常量的普通访问尝试:尝试使用对象进行访问
<?php
class Saler{
    //类常量
    const PI = 3.14;
}
$s1 = new Saler();
echo $s1->PI;            //错误,$s1->PI最终转换的访问方式为:$PI,这个在类中并不存在
  1. 以上案例可以看出,对象无法访问类常量,那是因为类常量的定义本身就是用来给类访问的,对象是用来访问属性和方法的,类常量的访问方式为:类名::常量名
<?php
class Saler{
    //类常量
    const PI = 3.14;
}
 
echo Saler::PI;            //输出3.14
  1. 分析:类常量是固定的,而对象的属性是不同对象而不同的,成员方法简单的理解也是为属性本身进行加工的。因此有一些东西是专属于类的,而有部分内容是专门为对象提供的,所以就会有不同的成员拥有不同的访问方式

总结

  1. 类访问成员的方式是使用范围解析操作符“::”访问,由类名直接访问:类名::类常量
  2. 类本身是通过对同类对象的抽象而形成,所以属性和方法本身都是由对象来访问
  3. 类也需要有一些自身的数据和操作,这些就由类来进行访问

思考:类要访问的成员,都只能是类常量吗?毕竟类常量是一旦定义不可修改的,这样没有多大价值。

引入:在类中,类成员只有三种:类常量、属性和方法,为了区分对象和类访问,可以使用static关键字来修饰属性和方法。

11.静态成员【掌握】

定义:静态成员,使用static关键字修饰的类成员,表示该成员属于类访问。PHP静态成员有两种,静态属性和静态方法。

  1. 静态属性:在类中定义属性的时候使用static关键字修饰,访问的时候只能使用类+范围解析操作符+静态属性访问
<?php
class Saler{
    //属性
    public $money = 0;
    public static $count = 0;    //静态属性
}
 
//静态成员可以直接使用类访问,而不需要先实例化对象
echo Saler::$count;
  1. 静态方法:在定义方法的时候使用static关键字修饰,访问的时候使用类+范围解析操作符+静态方法名字()访问
<?php
class Saler{
    //方法
    public static function showClass(){
        echo __CLASS__;
    }
}
 
//类直接访问
Saler::showClass();
  1. 在类的内部也可以访问静态成员,同样是使用类名+范围解析操作符+静态属性/静态方法()
<?php
class Saler{
    //属性
    private static $count = 0;            //私有,不允许外部直接访问
    //方法
    public static function showClass(){
        echo Saler::$count;
    }
}
 
//类直接访问
Saler::showClass();
  1. 静态方法本质也是类中定义的方法,因此也可以使用对象进行访问,但是不建议
<?php
class Saler{
    //属性
    private static $count = 0;            //私有,不允许外部直接访问
    //方法
    public static function showClass(){
        echo Saler::$count;
    }
}
 
//对象访问静态方法
$s = new Saler();
$s->showClass();                        //输出0
  1. 同理,方法也是在类内部,在编译时就存在,因此可以通过类来进行访问,使用范围解析操作符,但是非常不建议(会报错:因为类只允许访问静态成员和类常量)
<?php
class Saler{
    public function testStatic(){
        echo __FUNCTION__;
    }
}
 
//类访问普通成员方法
Saler::testStatic();                //输出testStatic,但是报错,当前访问的不是静态方法
  1. 静态方法本质是给类访问,所以不允许在静态方法内部使用$this对象
<?php
class Saler{
    public static function testStaticThis(){
        var_dump($this);                //致命错误:$this放到了不该放的位置
    }
}    

总结

  1. 为了保障类能直接访问数据和操作数据,可以在属性和方法前增加static关键字变成静态属性和静态方法
  2. 类通过类名+范围解析操作符+静态成员的方式进行访问
  3. 静态成员也收访问修饰限定符的限定,访问权限与普通属性和方法的限制一样
  4. 对象可以无条件访问静态方法,而类只能访问不带$this的普通方法(不建议)
  5. 静态成员是给类访问的,非静态成员是给对象访问的
  6. 静态成员的访问效率比非静态成员高,因此有种说法是能用静态的时候就不用非静态

思考:类的内部的静态方法内可能会访问到其他静态成员或者类常量,这个时候必须每次使用类名,那如果类名修改的话岂不是很麻烦?

引入:静态成员或者类常量的访问是必须使用类名进行访问的,不过为了方便类名的更新,减少开发者维护成本,系统引入了一个类似$this代表对象的方案:self关键字

12. self关键字【掌握】

定义:self关键字是一种在类的内部(方法里面)使用,代替类名的写法。能够保障用户方便修改类名字。

  1. self是用来代替类名的,与范围解析操作符::一起使用的
<?php
class Saler{
    //属性
    private static $count = 0;            //私有,不允许外部直接访问
    //方法
    public static function showClass(){
        echo Saler::$count;
        echo self::$count;                //代替类名
    }
}
  1. self也可以在类的内部方便实例化对象:比如构造方法被私有化之后,就没有办法在类外部实例化对象,此时可以在类内部进行对象实例化
<?php
class Saler{
    //属性
    private static $count = 0;            //私有,不允许外部直接访问
    private function __construct(){}    //私有,不允许外部实例化(因为对象不能外部调用)
    //方法
    public static function getInstance(){
        return new Saler();                //使用类名实例化
        return new self();                //使用self关键字实例化
    }
}
 
$s = Saler::getInstance();

总结

  1. self是一种在类内部用来代替类名的关键字
  2. self可以用来在类内部访问静态成员(类常量也可以)
  3. self也可以在类内部用来实例化对象(代替类名:new self())

思考:类不管是直接访问静态成员还是先实例化然后访问类成员,都有一个前提:类必须在内存中存在,那是不是意味着所有的类操作之前,都必须先确认类在内存中是否存在呢?

引入:类的任何操作都需要确认类在内存存在的,不过类文件通常是一个类单独形成一个文件的。所以在操作之前需要进行类的加载。

13.类的加载【重要】

定义:所谓类的加载,本质是因为类的访问必须保证类在内存中已经存在,所以需要在用类之前将类所在的PHP文件加载到内存。

  1. 手动加载:即要访问某个类之前,使用文件包含将类所在的文件加载进来
类文件:Saler.php
<?php
class Saler{}
?>
 
应用文件:useSaler.php
<?php
//使用Saler类需要先包含Saler类所在的文件
include_once 'Saler.php';                //通常使用include_once,因为类不允许重名
$s = new Saler();
?>
  1. 加载类文件是一种比较消耗资源的方式,所以有的时候不确定类是否在内存中存在,可以事先使用class_exists()函数来判定是否存在,存在就不用加载,不存在才加载
<?php
//使用Saler类,但是不确定内存中是否存在
if(!class_exists('Saler')){
    //不存在:加载
    include_once 'Saler.php';
}
 
//使用
$s = new Saler();
?>
  1. 自动加载:PHP没有那么智能的系统自动加载,所谓自动加载只是PHP提供了一种加载机制:即实现定义一个函数__autoload(),然后当系统需要使用类,而内存中又不存在的时候,系统就会自动调用__autoload()函数来加载类文件.
<?php
//自动加载机制:利用系统提供的__autoload()函数
function __autoload($classname){            //参数为类名:即当前需要访问的类的名字
    //需要人为定义去哪加载,怎么加载   
    include_once $classname . '.php';        //假定为当前目录下,类文件名字为:类名.php
}
 
//使用类:内存目前并没有
$s = new Saler();                            //系统发现内存没有Saler,所以调用__autoload()去加载
?>
  1. 一个系统里,可能类文件会放到不同的路径下,因此一个完整的自动加载函数,应该要进行文件判定以及加载功能
<?php
//定义自动加载
function __autoload($classname){
    //组织文件路径:假设当前路径下,有两个文件夹下都有类c和m
    $c_file = 'c/' . $classname . '.php';        //如c/Saler.php
    if(file_exists($c_file)){
        include_once $c_file;
    }else{
        //说明c文件夹没有对应的文件
        $m_file = 'm/' . $classname . '.php';    //如m/Saler.php
        if(file_exists($m_file)){
            include_once $m_file;
        }
    }
}
 
?>

注意:自动加载是指按照开发者规定的路径去寻找对应的文件,并实现包含。如果文件不存在,那么系统会在使用类的时候报错,因为这是开发者自己犯的错,系统不能规避。

  1. 随着PHP版本的提升,在7以后,不怎么建议直接使用__autoload()函数,而是采用一种注册机制,将用户自定义的函数,放到系统内部,使用spl_autoload_register(定义好的函数)。本质与__autoload()一样
<?php
//定义一个函数,用来加载类文件
function my_autoload($classname){        //也需要一个参数来接收要加载的类名字
    //功能与__autoload()一样
    $c_file = 'c/' . $classname . '.php';        //如c/Saler.php
    if(file_exists($c_file)){
        include_once $c_file;
    }else{
        //说明c文件夹没有对应的文件
        $m_file = 'm/' . $classname . '.php';    //如m/Saler.php
        if(file_exists($m_file)){
            include_once $m_file;
        }
    }
}
 
//此时,上述函数永远不会自动运行,除非将函数注册到系统内部
spl_autoload_register('my_autoload');
 
?>

注意:该方式其实本质就是通过两步完成了__autoload()一步的操作,但是spl_autoload_register()函数可以注册多个自定义的加载函数,更方便管理。

<?php
function c_autoload($classname){        
    $c_file = 'c/' . $classname . '.php';        
    if(file_exists($c_file)){
        include_once $c_file;
    }
}
 
function m_autoload($classname){        
    $m_file = 'm/' . $classname . '.php';        
    if(file_exists($m_file)){
        include_once $m_file;
    }
}
 
//全部注册
spl_autoload_register('c_autoload');    //先尝试调用第一个自定义加载函数:找到了结束;找不到找第二个函数
spl_autoload_register('m_autoload');    
?>
<?php
class Autoload{
    //实现不同文件夹的自动加载
    public static function loadC($classname){
        $c_file = 'c/'.$classname.'.class.php';
        if(file_exist($c_file)) include_once $c_file;
    }
    public static function loadM($classname){
        $m_file = 'm/'.$classname.'class.php';
        if(file_exist($m_file)) include_once $m_file;
    }
}
 
//自动加载
spl_auto_load_register(array('Autoload','loadC'));
spl_auto_load_register(array('Autoload','loadM'));
//实例化
$s = new Saler();

总结

  1. 类的使用必须先保证内存中该类存在
  2. 可以使用手动加载来确保类的使用安全:优点是明确,缺点是繁琐(类文件名字可以随意没有规范)
  3. 可以使用自动加载来让系统按照开发者设定的路径和方式去寻找类,并尝试加载到内存(尽量让类文件名字统一,保证类名和文件名有关联)
  4. 自动加载可以使用__autoload()函数来实现,也可以使用自定义函数+spl_autoload_register()注册共同实现(后者推荐)
  5. 基本上所有的框架都在使用自动加载机制

思考:对象只能通过实例化产生,而且即使是将对象赋值给变量,得到的还是同一个对象。如果有的时候需要得到新的对象,除了实例化之外还有其他方法吗?

引入:对象理论上应该通过实例化来产生,这是对象产生的根本。但是有时候如果想在已有对象上去产生一个新的对象,可以通过克隆来实现。

14.对象克隆【了解】

定义:克隆对象clone,即通过已有的对象复制一个新的同样的对象,但是两者之间并非同一个对象。

  1. 对象克隆是通过clone关键字实现,即:clone 对象;
<?php
class Saler{
      //属性
      public $count;             
      private $money;
}
//实例化
$s1 = new Saler();
$s1->count = 1;
 
//克隆
$s2 = clone $s1;
?>
  1. 克隆出来的对象与原来对象是两个内存地址,因此是两个不同的对象
<?php
//接上述代码
$s2->count = 2;
 
echo $s1->count;        //1,没有变化
?>
  1. 对象在实例化的时候会自动调用存在的构造方法__construct(),同样的,在类的内部,PHP允许定义一个__clone()的方法,在对象被克隆后,新克隆出来的对象会自动调用
<?php
class Saler{
      //属性
      public $count;             
      private $money;
    //克隆方法
    public function __clone(){
        var_dump($this);            //编号为2,代表是克隆出来的对象
        $this->count++;
    }
}
//实例化
$s1 = new Saler();
$s1->count = 1;
 
//克隆
$s2 = clone $s1;
?>
  1. 如果不允许对象被克隆,可以将__clone()方法私有化(本质是不允许对象在外部被克隆)
<?php
class Saler{
      //属性
      public $count;             
      private $money;
    //私有化克隆方法
    private function __clone(){}
}
//实例化
$s1 = new Saler();
$s1->count = 1;
 
//克隆
$s2 = clone $s1;            //致命错误:不允许对象在外部访问一个私有方法
?>

总结

  1. 对象可以通过克隆来得到新的对象(以前只有实例化)
  2. 克隆出来的对象会自动调用类中对应的__clone()方法(如果有)
  3. 可以通过私有化克隆方法来实现禁止外部对象克隆

思考:现在掌握了很多面向对象相关的内容,但是什么时候封装类?怎么封装类?如何使用类成员?

引入:其实类的封装并没有多么复杂,但是需要一个熟练的过程来确定哪些东西可以放到类里,该用什么样的形式等。我们通过封装一个数据库的操作类来综合练习一下。

15.封装数据库操作类【掌握】

定义:封装数据库操作类,即根据数据库的操作需求,来确认数据库操作类该有什么样的功能,以及这些功能该如何实现。

  1. 一个类通常就是一个文件,所以要先确定文件的名字:通常类文件命名规范有两种
  • 文件名字与类名字一样,如Sql.php
  • 为了区分普通PHP文件,增加中间类描述,如Sql.class.php
  • 现在PHP几乎都是面向对象编程,所以通常采用第一种方式:因此当前命名数据类的文件为:Sql.php
  1. 确定类文件名字后其实也就确定了类名字,因此可以创建一个Sql类
<?php  
//数据库操作类
class Sql{}
?>
  1. 类的创建分两种:一是特定使用,即类里面的所有内容只为某次使用;二是通用,即工具类,以后很多地方可以用。
  • 特定使用,功能可以不用太灵活
  • 通用工具,功能应该大众化,数据的变化会比较多

数据库类以后凡是要操作数据库的地方都可以用得到,很多项目都会用到,所以应该是个通用工具类,因此要考虑其到处可用的特性,让其能够灵活

  1. 数据库的操作最基本的特性不会改变:即需要连接认证,而连接认证的信息是灵活的,所以可以通过设定属性来控制,这些信息也都是不同使用者不同的,应该可以改变,所以可以通过构造方法来实现数据传入
<?php  
//数据库操作类
class Sql{
    //设置属性
    public $host;
    public $port;
    public $user;
    public $pass;
    public $dbname;
    public $charset;
 
    //构造方法初始化数据:数据较多,应该使用数组来传递数据,关联数组,而且绝大部分的开发者本意是用来测试,所以基本都是本地,因此可以给默认数据
    /*
        $info = array(
            'host' => 'localhost',
            'port' => '3306',
            'user' => 'root',
            'pass' => 'root',
            'dbname' => 'test',
            'charset' => 'utf8'
        )
    */
    public function __construct(array $info = array()){
        //初始化:确保用户传入了数据,否则使用默认值
        $this->host = isset($info['host']) ? $info['host'] : 'localhost';
        $this->port = isset($info['port']) ? $info['port'] : '3306';
        $this->user = isset($info['user']) ? $info['user'] : 'root';
        $this->pass = isset($info['pass']) ? $info['pass'] : 'root';
        $this->dbname = isset($info['dbname']) ? $info['dbname'] : 'test';
        $this->charset = isset($info['charset']) ? $info['charset'] : 'utf8';
    }
}
?>

注意:方法设定的原则是一个方法只实现一个简单的功能,不要多个功能堆积到一个方法中。

  1. 数据库属性会在实例化Sql对象的时候自动初始化
<?php
//接上述代码(类外测试)
$s1 = new Sql();            //使用默认数据库信息
$db = array(
    'host' => 'localhost',
    'user' => 'root',
    'pass' => 'root',
    'dbname' => 'my_database'
);
$s2 = new Sql($db);            //使用外部数据库信息
?>
  1. 数据库要操作的第一件事就是连接认证,所以需要一个连接认证的功能。这里可以使用mysqli面向对象的方法。但是需要建立一个方法来实现连接认证:连接是否成功?
//在上述类中增加一个方法:实现连接认证功能
public function sql_connect(){
    //利用属性可以跨方法访问:5个参数分别为:主机、用户名、密码、数据库、端口
    //利用错误抑制符抑制可能出现的错误:如找不到数据库之类
    $link = @new mysqli($this->host,$this->user,$this->pass,$this->dbname,$this->port);
    //判定连接是否成功
    if($link->connect_error){
        //mysqli对象有两个属性:connect_errno表示错误编号,connect_error表示错误信息:发生错误后终止脚本执行
        die( 'Connect Error ('  .  $link -> connect_errno  .  ') ' .  $link -> connect_error );
    }
}
  1. 用户调用Sql类的目的一定是为了操作数据库,那么用户在实例化之后就需要调用连接认证的方法。为了方便用户操作,可以帮助用户省去调用这一步骤:在构造方法中调用该方法
//在上述构造方法中调用连接认证方法
public function __construct(array $info = array()){
 
    //在属性初始化之后调用连接认证方法
    $this->sql_connnect();
}
  1. 至此,一旦实例化Sql类对象,就可以实现数据库的连接,但是此时还存在一个细节问题:字符集,为了保证数据库连接的正常操作,需要新增一个方法设定字符集
//在Sql类中增加设定字符集的方法
public function sql_charset(){
    $sql = "set names {$this->charset}";        //组织SQL指令
    //此时需要调用mysqli连接数据库得到的对象:发现$link在sql_connect方法中是一个局部变量,不能跨方法
}
  1. 由于sql_connect方法中得到的连接对象mysqli的 l i n k 是 一 个 局 部 变 量 , 无 法 在 其 他 方 法 里 使 用 , 因 此 需 要 将 连 接 对 象 link是一个局部变量,无法在其他方法里使用,因此需要将连接对象 link使link提升为整个类的内部可用:提升为属性
//在Sql类中增加属性,保存对象
public $link;
 
//修改sql_connect方法,使用属性保存连接对象
public function sql_connect(){
    $this->link = @new mysqli($this->host,$this->user,$this->pass,$this->dbname,$this->port);
    //判定连接是否成功
    if($link->connect_error){
        //注意:连接资源保存在$this->link中,所以访问mysqli对象成员时需要$this->link->属性/方法()
        die('Connect Error ('  .  $this->link->connect_errno  .  ') ' .  $this->link->connect_error);
    }
}
  1. 继续完成设定字符集的功能:使用属性保存的mysqli连接对象
//在Sql类中完善设定字符集的方法
public function sql_charset(){
    $sql = "set names {$this->charset}";        //组织SQL指令
    //调用mysqli::query
    $this->link->query($sql);
}
  1. 设置字符集的SQL指令虽然简单,但是因为有数据来源于外部,所以存在出错的风险,因此需要进行SQL错误判定
//在Sql类中完善设定字符集的方法
public function sql_charset(){
    $sql = "set names {$this->charset}";        //组织SQL指令
 
    $res = $this->link->query($sql);
    if(!$res){
        //失败发挥false
        die('Charset Error(' . $this->link->errno . ') ' . $this->link->error);
    }
}
  1. 同样的,字符集的设置也是为了操作数据库的前置条件,属于初始化的一部分。因此需要字符集设置的方法在构造方法中调用,方便用户操作
//在Sql类的构造方法中调用字符集设置方法
public function __construct(array $info = array()){
 
    //在连接数据库方法调用之后调用设置字符集方法
    $this->sql_charset();
}
  1. 至此:数据库的初始化操作已经完成,此时要考虑的事情是用户调用数据库类是为了干什么?为了执行SQL指令,也就是增删改查。在mysqli中所有的SQL执行都是通过mysqli::query()方法执行,但是我们可以根据需求封装两个函数:写方法和查方法(包含一条和多条查询)
//在Sql类中增加一个写方法:SQL指令来自于调用处提供
public function sql_exec($sql){
    //当前不知道SQL指令是什么样的,只是负责执行,所以由外部提供
    $res = $this->link->query($sql);
    //判断SQL有没有语法错误
    if(!$res){
        die('Sql Error(' . $this->link->errno . ') ' . $this->link->error);
    }
 
    //返回执行结果
    return $res;        //本质就是一个true
}
 
//在Sql类中增加一个读方法:Sql指令来自于调用处提供,同时设定参数供用户选择一条或全部结果
public function sql_query($sql,$all = false){
    //$all代表是否获取多条记录,默认false只获取一条记录
    $res = $this->link->query($sql);
    //判断SQL有没有语法错误
    if(!$res){
        die('Sql Error(' . $this->link->errno . ') ' . $this->link->error);
    }
 
    //解析其中的结果:根据用户需求获取一条或者多条记录
    if($all){
        //获取全部:mysqli_result::fetch_all(MYSQLI_ASSOC)表示返回关联数组(默认是索引)
        return $res->fetch_all(MYSQLI_ASSOC);
    }else{
        //获取一条:mysqli_result::fetch_assoc()
        return $res->fetch_assoc();
    }
}
  1. 上述已经完成了数据库类要实现的基本功能:实现SQL指令的执行和结果返回,但是从功能细节的角度出发还需要进行完善:插入操作后要获取自增长id,更新和删除操作受影响的行数,查询操作中记录数量。这种使用可以通过设置方法来实现获取(自增长id),也可以通过增加属性来实现(属性简单)

增加属性:受影响的行数,自增长id,查询记录数

//在Sql类中追加属性
public $affected_rows;    //受影响行数(上次操作)
public $num_rows;        //查询结果记录数(上次操作)

在写操作sql_exec中,为受影响行数赋值

//修改sql_exec方法
public function sql_exec($sql){
    $res = $this->link->query($sql);
    //判断SQL有没有语法错误
    if(!$res){
        die('Sql Error(' . $this->link->errno . ') ' . $this->link->error);
    }
    //成功保存受影响的行数:数据在mysqli对象中
    $this->affected_rows = $this->link->affected_rows;
    //返回执行结果
    return $res;        //本质就是一个true
}

在读操作中,为记录数

//修改sql_query方法
public function sql_query($sql,$all = false){
    $res = $this->link->query($sql);
    //判断SQL有没有语法错误
    if(!$res){
        die('Sql Error(' . $this->link->errno . ') ' . $this->link->error);
    }
 
    //获取记录信息:数据在mysqli_result对象中
    $this->num_rows = $res->num_rows;
 
    //解析其中的结果:根据用户需求获取一条或者多条记录
    if($all){
        //获取全部:mysqli_result::fetch_all(MYSQLI_ASSOC)表示返回关联数组(默认是索引)
        return $res->fetch_all(MYSQLI_ASSOC);
    }else{
        //获取一条:mysqli_result::fetch_assoc()
        return $res->fetch_assoc();
    }
}

增加一个方法专门获取上次插入数据的自增长ID(因为这个是人为区分,不方便放到sql_exec中)

//在Sql类中增加一个方法获取上一次自增长操作id
public function sql_insert_id(){
    //insertid是在mysqli对象执行query方法时获得
    return $this->link->insert_id;
}
  1. 至此:数据库类的功能已经实现,接下来要考虑类的定义规范:类对成员的控制性
  • 属性如果不需要给外部访问,私有
  • 方法如果只是内部调用,私有

总结

  1. 类的封装是以功能驱动为前提,相关操作存放到一个类中
  2. 一个类通常是一个独立的文件,文件名与类名相同(方便后期维护和自动加载)
  3. 类中如果有数据需要管理,设定属性(固定数据可以使用类常量)
  4. 类中如果有功能需要实现(数据加工),设定方法
  5. 一个功能通常使用一个方法实现,方法的颗粒度应该尽可能小(方便复用)
  6. 应该尽可能增加类对成员的控制:即能私有尽可能私有
  • 6
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值