JavaScript高级 ES5 面向对象原型继承

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

原型以及ES5中实现继承

1. 对象和函数的原型

1. 普通对象的原型 [[prototype]]

JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]]这个特殊的对象可以指向另外一个对象

在这里插入图片描述

当我们通过引用对象的属性key来获取一个value时它会触发 [[Get]]的操作这个操作会首先检查该对象是否有对应的属性如果有的话就使用它如果对象中没有该属性那么会访问对象[[prototype]]内置属性指向的对象上的属性

获取原型的方式

通过对象的 __proto__ 属性可以获取到但是这个是早期浏览器自己添加的存在一定的兼容性问题

通过 Object.getPrototypeOf 方法可以获取到

    var obj = {
      name: "why",
      age: 18
    }
    console.log(obj)

    var info = {}

    // 获取对象的原型
    console.log(obj.name, obj.age) // 在这里我们可以直接获取对象自身的属性
    console.log(obj.__proto__) // 不推荐使用
    console.log(Object.getPrototypeOf(obj)) // 标准使用
    console.log(obj.__proto__ === Object.getPrototypeOf(obj)) // true 因为他们指向的是同一个对象

    // 疑问: 这个对象的原型有什么用呢?
    // 当我们通过[[get]]方式获取一个属性对应的value时
    // 1> 它会优先在自己的对象中查找, 如果找到直接返回
    // 2> 如果没有找到, 那么会在原型对象中查找
    obj.__proto__.message = "Hello World"
    console.log(obj.message) // Hello World

在这里插入图片描述

但是不管是通过字面量直接创建一个对象还是使用构造函数只要是对象都会有这样的一个内置属性

2. 函数的原型 prototype

所有的函数都有一个prototype的属性注意不是 __proto__ 但是对象是没有prototype属性的

因为它是一个函数才有了这个特殊的属性而不是它是一个对象所以有这个特殊的属性

    var obj = {}
    function foo() {}

    // 1.将函数看成是一个普通的对象时, 它是具备__proto__(隐式原型)
    console.log(obj.__proto__) // 获取的是对象的原型
    console.log(foo.__proto__) // ƒ () { [native code] }
    // foo这个函数本身是Function的一个实例对象
    console.log(foo.__proto__ === Function.prototype); //true
    //  Function是由其本身自己构造出来的。所有函数都是大写Function的实例
    console.log(Function.__proto__ === Function.prototype); //true


    // 2.将函数看成是一个函数时, 它是具备prototype(显式原型)
    // 作用: 用来构建对象时, 给对象设置隐式原型的
    console.log(foo.prototype)
    // console.log(obj.prototype) 需要注意的是 对象是没有prototype 的

2. new、constructor

1. new 操作符

new 关键字的步骤

  1. 在内存中创建一个新的对象空对象
  2. 将空对象赋值给 this
  3. 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性
  4. 执行函数体的代码
  5. 将这个对象默认返回
    function Foo() {
      // 1.创建空的对象
      // 2.将Foo的prototype原型(显式隐式)赋值给空的对象的__proto__(隐式原型)
    }

    console.log(Foo.prototype)

    var f1 = new Foo()
    var f2 = new Foo()
    var f3 = new Foo()
    var f4 = new Foo()
    var f5 = new Foo()
    console.log(f1.__proto__)
    console.log(f1.__proto__ === Foo.prototype) // true
    console.log(f3.__proto__ === f5.__proto__) // true

在这里插入图片描述

那么也就意味着我们通过构造函数创建出来的所有对象的[[prototype]]属性都指向Foo.prototype

2. constructor属性

原型对象上面是有一个constructor属性的 默认情况下原型上都会添加一个属性叫做constructor这个constructor指向当前的函数对象

    // 非常重要的属性: constructor, 指向Person函数对象
    function Person() {

    }

    // 1.对constructor在prototype上的验证
    var PersonPrototype = Person.prototype
    console.log(PersonPrototype) 
    console.log(PersonPrototype.constructor) // ƒ Person() {}
    console.log(PersonPrototype.constructor === Person)

    console.log(Person.name) // Person
    console.log(PersonPrototype.constructor.name) // Person

    // 2.实例对象p
    var p = new Person()
    console.log(p.__proto__.constructor) // ƒ Person() {}
    console.log(p.__proto__.constructor.name) // Person

3. 将方法放到原型上

当我们多个对象拥有共同的值时, 我们可以将它放到构造函数对象的显式原型由构造函数创建出来的所有对象, 都会共享这些属性

 /*
    1.什么是函数的显式原型
      * 区分和对象原型区别
    2.函数的原型的作用
      * 在通过new操作创建对象时, 将这个显式原型赋值给创建出来对象的隐式原型
    3.案例Person, 将所有的函数定义放到了显式原型上
    */

    function Student(name, age, sno) {
      this.name = name
      this.age = age
      this.sno = sno

      // 1.方式一: 编写函数, 会创建很多个函数对象
      // 将方法放到原型上可以避免内存的浪费
      // this.running = function() {
      //   console.log(this.name + " running")
      // }
      // this.eating = function() {
      //   console.log(this.name + " eating")
      // }
      // this.studying = function() {
      //   console.log(this.name + " studying")
      // }
    }

    // 当我们多个对象拥有共同的值时, 我们可以将它放到构造函数对象的显式原型
    // 由构造函数创建出来的所有对象, 都会共享这些属性
    Student.prototype.running = function() {
      console.log(this.name + " running")
    }
    Student.prototype.eating = function() {
      console.log(this.name + " eating")
    }

    // 1.创建三个学生
    var stu1 = new Student("ximingx", 18, 111)
    var stu2 = new Student("kobe", 30, 112)
    var stu3 = new Student("james", 18, 111)

    // 隐式原型的作用
    // 1> stu1的隐式原型是谁? Student.prototype对象
    // 2> stu1.running查找:
    //  * 先在自己身上查找, 没有找到
    //  * 去原型去查找
    stu1.running()
    stu2.eating()

4. 创建对象的内存表现

    function Person(name, age) {
      this.name = name
      this.age = age
    }

    Person.prototype.running = function() {
      console.log("running~")
    }

    var p1 = new Person("why", 18)
    var p2 = new Person("kobe", 30)

    // 进行操作
    p1.running()
    p2.running()

在这里插入图片描述

5. 重写原型对象

如果我们需要在原型上添加过多的属性通常我们会重写整个原型对象

每创建一个函数, 就会同时创建它的prototype对象, 这个对象也会自动获取constructor属性

重写整个原型对象相当于给prototype重新赋值了一个对象, 那么这个新对象的constructor属性, 会指向Object构造函数, 而不是Person构造函数了

    function Person() {

    }

    console.log(Person.prototype)
    
    // 在原有的原型对象上添加新的属性
    // Person.prototype.message = "Hello Person"
    // Person.prototype.info = { name: "哈哈哈", age: 30 }
    // Person.prototype.running = function() {}
    // Person.prototype.eating = function() {}

    // console.log(Person.prototype)
    // console.log(Object.keys(Person.prototype))

    // 直接赋值一个新的原型对象
    Person.prototype = {
      message: "Hello Person",
      info: { name: "哈哈哈", age: 30 },
      running: function() {},
      eating: function() {},
      // constructor: Person
    }
    
    Object.defineProperty(Person.prototype, "constructor", {
      enumerable: false,
      configurable: true,
      writable: true,
      value: Person
    })

    console.log(Object.keys(Person.prototype))

在这里插入图片描述

如果希望constructor指向Person那么可以手动添加 上面的方式虽然可以, 但是也会造成constructor的[[Enumerable]]特性被设置了true默认情况下, 原生的constructor属性是不可枚举的如果希望解决这个问题, 就可以使用我们前面介绍的Object.defineProperty()函数

3. 原型链的查找顺序

我们知道从一个对象上获取属性如果在当前对象中没有获取到就会去它的原型上面获取

[Object: null prototype] {} 原型属性已经指向的是null也就是已经是顶层原型

    // 1.{}的本质
    // var info = {}
    // 相当于
    // var info = new Object()
    // console.log(info.__proto__ === Object.prototype)

    // 2.原型链
    var obj = {
      name: "why",
      age: 18
    }

    // 查找顺序
    // 1.obj上面查找
    // 2.obj.__proto__上面查找
    // 3.obj.__proto__.__proto__ -> null 上面查找(undefined)
    // console.log(obj.message)


    // 3.对现有代码进行改造
    obj.__proto__ = {
      // message: "Hello aaa"
    }

    obj.__proto__.__proto__ = {
      message: "Hello bbbb"
    }

    obj.__proto__.__proto__.__proto__ = {
      message: "Hello ccc"
    }

    console.log(obj.message)

我们可以得出一个结论原型链最顶层的原型对象就是Object的原型对象

4. 原型链实现的继承

    // 定义Person构造函数(类)
    function Person(name, age, height, address) {
      this.name = name
      this.age = age
      this.height = height
      this.address = address
    }
    Person.prototype.running = function() {
      console.log("running~")
    }
    Person.prototype.eating = function() {
      console.log("eating~")
    }
    // 定义学生类
    function Student(name, age, height, address, sno, score) {
      this.name = name
      this.age = age
      this.height = height
      this.address = address

      this.sno = sno
      this.score = score
    }
    // 创建一个父类的实例对象(new Person()), 用这个实例对象来作为子类的原型对象
    var p = new Person("why", 18)
    Student.prototype = p

    Student.prototype.studying = function() {
      console.log("studying~")
    }

在这里插入图片描述

原型链继承的弊端

第一我们通过直接打印对象是看不到这个属性的

第二这个属性会被多个对象共享如果这个对象是一个引用类型那么就会造成问题

第三不能给Person传递参数让每个stu有自己的属性因为这个对象是一次性创建的没办法定制化

5. 借用构造函数继承

借用继承的做法非常简单在子类型构造函数的内部调用父类型构造函数

因为函数可以在任意的时刻被调用因此通过apply()和call()方法也可以在新创建的对象上执行构造函数

    // 定义Person构造函数(类)
    function Person(name, age, height, address) {
      this.name = name
      this.age = age
      this.height = height
      this.address = address
    }

    Person.prototype.running = function() {
      console.log("running~")
    }
    Person.prototype.eating = function() {
      console.log("eating~")
    }

    // 定义学生类
    function Student(name, age, height, address, sno, score) {
      // 重点: 借用构造函数
      Person.call(this, name, age, height, address)
      // this.name = name
      // this.age = age
      // this.height = height
      // this.address = address

      this.sno = sno
      this.score = score
    }
    
    var p = new Person("why", 18)
    Student.prototype = p

组合继承最大的问题就是无论在什么情况下都会调用两次父类构造函数

除此之外所有的子类实例事实上会拥有两份父类的属性 一份在当前的实例自己里面(也就是person本身的)另一份在子类对应的原型对象中(也就是person.__proto__里面)

	function Person(name, age, height) {}
    function Student() {}

    inherit(Student, Person)
    
    // 1.之前的做法: 但是不想要这种做法
    // var p = new Person()
    // Student.prototype = p

    // 2.方案一:
    var obj = {}
    // obj.__proto__ = Person.prototype
    Object.setPrototypeOf(obj, Person.prototype)
    Student.prototype = Person.prototype

    // 3.方案二:
    function F() {}
    F.prototype = Person.prototype
    Student.prototype = new F()

    // 4.方案三:
    var obj = Object.create(Person.prototype)
    console.log(obj.__proto__ === Person.prototype)
    Student.prototype = obj

6. 寄生组合实现继承

创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象最后再将这个对象返回

    // 创建对象的过程
    function createObject(o) {
      function F() {}
      F.prototype = o
      return new F()
    }
    // 寄生式函数
    function inherit(Subtype, Supertype) {
      Subtype.prototype = createObject(Supertype.prototype)
      Object.defineProperty(Subtype.prototype, "constructor", {
        enumerable: false,
        configurable: true,
        writable: true,
        value: Subtype
      })
    }
  
    // 寄生组合式继承
    // 原型链/借用/原型式(对象之间)/寄生式函数
    function Person(name, age, height) {
      this.name = name
      this.age = age
      this.height = height
    }

    Person.prototype.running = function() {
      console.log("running~")
    }
    Person.prototype.eating = function() {
      console.log("eating~")
    }

    function Student(name, age, height, sno, score) {
      Person.call(this, name, age, height)
      this.sno = sno
      this.score = score
    }
    inherit(Student, Person)
    
    Student.prototype.studying = function() {
      console.log("studying")
    }

    // 创建实例对象
    var stu1 = new Student("why", 18, 1.88, 111, 100)
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: JavaScriptJava