《JavaScript-----深入理解面向对象(1)》

806 阅读10分钟

面向对象是现实的抽象方式

  • 对象是JavaScript的一个非常重要的概念,这是因为对象可以将多个相关联的数据封装到一起,更好的描述一个事物
    • 比如我们可以描述一辆车:Car,具有颜色(color)、速度(speed)、价格(price)、品牌(brand)等等
    • 比如我们可以描述一个人:Person,具有姓名(name)、年龄(age)、身高(height)、吃东西的行为(eat)、跑步的行为(run)等
  • 对象描述事物,更有利于我们将现实的事物,抽历程代码中的某个数据结构
    • 所以有一些变成语言就是纯面向对象的变成语言,入java;
    • 在实现任何显示抽象是都需要创建一个类,根据类再去创建对象;
    image.png

JavaScript的面向对象

  • JavaScript其实支持多种编程范式,包括函数式编程面向对象编程
    • JavaScript中的对象被设计成一组属性的无序集合,由keyvalue组成
    • key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型
    • 如果value是函数,那么我们可以称之为是对象的方法
  • 如何创建一个对象?
    • 早期使用创建的对象的方法最多的是使用Objcet类,并且使用new关键字来创建一个对象
    • 后来很多开发者为了方便,都是直接使用字面量的形式来创建对象
   //创建一个空的对象
   var obj1 = new Object()
   obj1.name = "aky"
   obj1.age = 18
   obj1.height = 1.88
   obj.eating = function(){
       console.log(this.name + "在吃东西")
   }
   
   var obj2 = {
     name = "aky"
     age = 18
     height = 1.88
     eating = function(){
        console.log(this.name + "在吃东西")
    }

对属性操作的控制

  • 在我们前面的属性都是直接定义在对象内部,或者直接添加到对象内部
    • 但是这样做的时候我们就不能对这个属性进行一些限制:比如这个属性是否可以通过delete删除、这个属性是否在for-in遍历的时候被遍历出来
    • 如果我们想要对一个属性比较精确的操作控制,那么我们可以使用属性描述符
      • 通过属性描述符可以精确的添加或者修改对象的属性
      • 属性描述符需要使用Object.defineProperty来对属性进行添加或者修改;

Object.defineProperty

  • Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

    Object.defineProperty(obj,prop,descriptor)

    • 可接收三个参数:
      • obj:要定义属性的对象;
      • prop:要定义或者修改的属性的姓名或Symbol;
      • descriptor:要定义或修改属性的属性描述符
    • 返回值:被传递给函数的对象

属性描述符分类

  • 属性描述符的类型有两种:
    • 数据类型描述符
    • 存取类型描述符
标题configurableenumerablevaluewritablegetset
数据描述符可以可以可以可以不可以不可以
存取描述符可以可以不可以不可以可以可以

数据属性描述符

  • 数据描述符有如下四个特性
    • configurable:表示是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将其修改为存取描述符
      • 当我们直接直接在一个对象上定义某个属性时,这个属性默认为true
      • 当我们通过属性属性描述符定义一个属性时,这个属性默认为false
    • enumerable:表示属性是否可以通过for-in或者Object.keys()返回该属性
      • 当我们直接在一个对象上定义某个属性时,这个属性默认为true
      • 当我们通过属性描述符定义一个属性时,这个属性默认为false
    • writable :表示是否可以修改属性的值;
      • 当我们直接在一个对象上定义某个属性时,这个属性默认为true
      • 当我们通过属性描述符定义一个属性时,这个属性默认为false
    • value :属性的value值,读取属性是返回的值,修改属性时,会对其进行修改

存取属性描述符

  • 存取数据描述符有如下四个特性
    • configurable:表示是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将其修改为存取描述符
      • 当我们直接直接在一个对象上定义某个属性时,这个属性默认为true
      • 当我们通过属性属性描述符定义一个属性时,这个属性默认为false
    • enumerable:表示属性是否可以通过for-in或者Object.keys()返回该属性
      • 当我们直接在一个对象上定义某个属性时,这个属性默认为true
      • 当我们通过属性描述符定义一个属性时,这个属性默认为false
    • get:获取属性时会执行的函数。默认为undefined
    • set:修改属性时会执行的函数。默认为undefined
       var obj = {
            name: "aky",
            _age: 18
       }
       Object.defineProperties(obj,"address", {
             value: "北京市",
             configurable: false,
             enumerable: true,
              writable: false,
        })
        Object.defineProperties(obj,"age", {
             configurable: true,
             enumerable: true,
             get: function() {      
                 return this._address
             },
             set: function(value) {      
                  this._address = value
             }
        })
    

同时定义多个属性

  • Object.defineProperies() 方法直接在一个对象上定义多个 新的属性或者修改现有属性,并且返回该对象
    var obj = {
        _age = 18
    }
    Object.defineProperties(obj,{
        name:{
            writable:true,
            value:'aky'
        },
        age:{
            get(){
                return this._age
            }
         }
     })

对象方法的补充

  • 获取的想的属性描述符
    • Object.getOwnPropertyDescriptor9()
    • Object.getOwnPropertyDescriptors()
  • 禁止对象扩展新属性:
    • Object.preventExtensions()
  • 冻结对象,不允许修改现有属性:freeze
    • Object.freezes()

创建多个对象的方案

  • 如果我们现在希望创建一系列的对象:比如person对象
    • 包括张三、李四、王五、李雷等等,他们的信息不相同
    • 那么采用什么方式创建比较好呢?
  • 目前我们已经学习了两种方式:
    • new Object方式
    • 字面量创建方式
  • 这种方式有有一个很大的弊端:创建同样的对象时,需要编写重复的代码;

创建对象的方案—工厂函数

  • 我们可以想到的一种创建方式:工厂模式
    • 工厂模式:创建一个工厂方法(普通函数),通过该工厂方法我们可以生成想要的对象
    function createPerson(name,age,height){
      var p = new Object()
      p.name = name
      p.age =age
      p.height = height
      p.eating = function(){
          console.log(this.name + '在吃东西')
      }
      return p
    }  
    var p1 = createPerson('张三',18,1.84)
    var p2 = createPerson('李四',19,1.85)
    var p3 = createPerson('王五',20,1.86)
    var p4 = createPerson('李雷',22,1.87)

认识构造函数

  • 工厂方法创建对象有一个较大的问题:我们再打印对象时,对象的类型都是Object
    • 但是从某种角度来说,这些个对象应该有一个共同的类型
    • 下面我们来看一下啊另外一个种模式:构造函数的方法;
  • 我们先理解什么是构造函数?
    • 构造函数也称之为构造器(constructor),通常是我们在创建对象时调用的函数
    • 在其他面向的编程语言中,构造函数时存在于类的一个方法,称之为构造方法
    • 但是在JavaScript种构造函数有点不一样
  • JavaScript种的构造函数
    • 构造函数也是一个普通的函数,从表现形式上来说和普通函数没有任何区别
    • 如果一个普通函数被使用new操作符来调用,那么这个函数也称之为构造函数
  • 那么new 调用有什么特殊呢?

new 操作符调用的作用

  • 如果一个函数被使用new操作符调用了,那么他会执行如下操作:
    1. 在内存中创建一个新的对象(空对象)
    2. 将该构造函数的prototype对象赋值给新对象的__proto__
    3. 构造函数内部的this会指向新创建的对象
    4. 执行函数体内部代码
    5. 如果构造函数没有返回非空对象,则返回创建出来的对象
    //这是个人手写的new操作符    非常简单的 勿喷
    
    function myNew(constructor,...args){
          // 创建一个空对象
          let  newObj = null
          // 将对象的隐式原型指向构造函数的显示原型
      
          newObj.__proto__ =  constructor.prtotype
          // 绑定this 并执行函数返回结果
          let result = constructor.apply(newObj,args)   
      
          //判断返回值
          if(typeof result === "object" || typeof result === "function" ){
              return reslut
          }else{
              return newObj
          }
        
          return newObj
     }
     
    

创建对象的方案—构造函数

  • 我们来通过构造函数实现一下:
function Person(name, age, height) {
  this.name = name
  this.age = age
  this.height = height
  this.eating = function() {
    console.log(this.name + "在吃东西~")
  }
}
var p1 = new Person("张三", 18, 1.88)
var p2 = new Person("李四", 20, 1.98)

  • 这个构造函数可以确保我们的对象时Person的类型(实际上是constructor的属性)
  • 构造函数的缺点:我们需要为每个对象的函数去创建一个函数对象实例

认识对象的原型

  • JavaScript当中每个对象都有一个特殊的内置属性[[prototype]] (隐式原型对象),这个特殊的对象可以指向另外一个对象
  • 这个对象有什么用呢?
    • 当我们通过引用对象的属性key来获取一个value是,他会触发[[get]]的操作
    • 这个操作会首先检查该属性是否有对应的属性,如果有的话返回value的值
    • 如果对象中没有该属性,那么就会访问[[prototype]]内置属性指向的对象上的属性
  • 如果通过字面量直接创建一个对象,这个对象也有这样的属性吗? 如果有,该怎么获取这个属性:
  • 答案是有的,只要是对象都会有这样的一个内置属性;
  • 获取的方式有两种:
    • 方式一:通过对象的__proto__属性可以获取(这个是早期浏览器自己添加的,存在一定的兼容性问题)
    • 方式二:通过Object.getPrototypeOf() 方法获取到

函数的原型 prototype

  • 所有的函数都有一个prototoype的属性 (显式原型对象) image.png
  • 是不是因为函数是一个对象,所以他有prototype的属性呢?
    • 不是的,因为他是一个函数,才有了这个特殊的属性
    • 而不是他是一个对象,所以才有这个属性 image.png

再看new操作符

  • 前面说过new操作符的步骤如下:
    1. 在内存种创建一个新的对象 2.这个对象内部的[[prototype]] (隐式原型对象)属性会被赋值为构造函数的prototype属性
  • 那么也就一位置我们通过Person构造含糊创建出来的所有对象的[[prototype]] (隐式原型对象)属性都指向Person.prototype

p1._ proto _ === Person.prototype

p1._ proto _ ===p2._ proto _

创建对象的内存表现

image.png

Person.prototype.name ="curry"

image.png

//Foo 构造函数
function Foo (){}
//创建实例对象
var f1 = new Foo()

var obj = {
    name:'why',
    age:18,
    height:1.88
}   
//将Foo函数的prototype指向新的对象
foo.prototype = obj
// 真实开发中我们可以通过Object.defineProperty方式添加constructor
Object.defineProperty(foo.prototype, "constructor", {
 enumerable: false,
 configurable: true,
 writable: true,
 value: foo
})

image.png

constructor属性

  • 事实上原型对象上面是有一个属性:constructor
    • 默认情况下原型上都会添加一个constructor属性,并且当前的函数对象啊你个 image.png

重写原型对象

  • 如果我们需要在原型上添加过多的属性,我们可以重新整个原型对象 image.png
  • 前面我们说过,没创建一个函数,就会同时创建他的prototype,这个对象会自动获取constructor属性
    • 我们这里相当于给Prototype属性重新赋值了一个新的对象,那么这个新对象的constructor属性就会指向Object构造函数 ,而不是Person构造函数
  • 如果我们希望constructor指向Person,那么就得手动添加 image.png
  • 上面方式虽然可以,但是也会造成constructor的enumerable特性设置了true
    • 默认情况下,原生的constructor属性是不可枚举的

    • 如果要解决这个问题,就可以使用**Object.defineProperty()**方法 image.png

创建对象—构造函数和原型组合

  • 我们在上一个构造函数的方法创建对象时,有一个弊端:会创建出重复的代码, 比如running函数、eating函数等
    • 那么我们就可以将这些方法放到Person.prototype的对象上即可 image.png