美文网首页
老掉牙,但永不过时的面向对象——继承

老掉牙,但永不过时的面向对象——继承

作者: _BuzzLy | 来源:发表于2020-04-27 19:05 被阅读0次

    几乎所有语言都有面向对象的概念,JavaScript 的面向对象实质是基于原型的对象系统。说到面向对象,不得不提的就是继承。

    认识 new

    一个没有其他语言经验的人要更容易理解 JavaScript 的继承。
    不同于其他语言的类的继承,JavaScript 中使用的是原型继承,不过从表面上看更像是基于类的继承,原因可能是因为 new 关键字的使用。new 关键字是用来调用构造函数的,一个函数之所以称为构造函数,并不是因为函数本身有什么特性,而是因为 new 。也就是说只有通过 new 调用的函数才可能成为构造函数。
    new 既然这么神奇,有必要探究下内部到底实现了什么。

    • 创建一个空对象,并让这个对象继承构造函数的prototype。
    • 将构造函数的this指向这个空对象,并执行构造函数。
    • 如构造函数执行后返回的是对象类型就直接返回,否则返回上面创建的对象。

    下面是简易的实现代码:

    function myNew(constructor, param) {
      // constructor 就是构造函数,param 模拟构造函数的参数,这里只用一个参数举例
      
      // 创建一个空对象,且这个空对象继承构造函数的 prototype 属性
      const obj = Object.create(constructor.prototype)
    
      // 将构造函数的 this 指向 obj,执行构造函数得到返回结果
      const result = constructor.apply(obj, param)
    
      // 如果造函数执行后,返回结果是对象类型,就直接返回,否则返回 obj 对象
      return (typeof result === 'object' && result != null) ? result : obj
    }
    
    function Animal(species) {
      this.species = species
    }
    
    const cat = myNew(Animal, '猫科动物')
    
    console.log(cat)
    
    // {species: "猫科动物"}
    

    结合上面 new 实现的三步,再看代码就比较容易理解了。
    需要注意的就是,如果构造函数有显式返回值,并且返回值类型为对象。那么构造函数返回的结果不再是目标实例,而是这个显式的返回值。

    如何实现继承

    JavaScript 实现继承的方式有多种,各有优缺点,下面就将常见的几种方式一一列出。

    一、构造函数的继承方式

    function Animal(species) {
        this.species = species || "动物"
    }
    function Cat(name, color, species) {
        Animal.call(this, species)
        this.name = name
        this.color = color
    }
    Animal.prototype.age = 10
    var cat1 = new Cat("毛毛", "黑色", "猫科动物")
    console.log(cat1.species) // 猫科动物
    console.log(cat1.age) // undefined
    

    这种继承的实现方式是在子类构造函数中执行父类构造函数,并将父类构造函数的this指向子类的实例。优点简单易懂,但是缺点也很明显。
    ==缺点==:无法继承父类原型上的属性和方法。

    二、原型链继承模式

    function Animal() {
        this.species = "动物"
        this.list = [1, 2, 3]
    }
    function Cat(name, color) {
        this.name = name
        this.color = color
    }
    
    // 将Cat的prototype对象指向一个Animal的实例
    // 它相当于完全覆盖了 prototype 对象原先的值。
    Cat.prototype = new Animal()
    // 本行下面会有详细解释
    Cat.prototype.constructor = Cat
    
    var cat1 = new Cat("大黄", "黄色")
    var cat2 = new Cat("小黄", "黑色")
    
    //这种方式实现的继承,不同实例的原型对象都指向同一个Animal的实例,访问属性的时候,如果实例内没有该属性,就会向上找到Cat.prototype(Animal的一个实例)中。
    //但是这里调用cat1.species去赋值,不会向上寻找,而只是在cat1实例中添加一个species属性,并不会影响cat1原型对象(Animal实例)中的属性,因此cat2.species的值没有变化,这种方式并不能说明问题,看下面的代码
    cat1.species = "猫科动物"
    console.log(cat1.species) // 猫科动物
    console.log(cat2.species) // 动物
    
    // 调用数组的push方法,就会顺着原型链搜索,找到原型对象中的list并修改值,上面说了不同实例的原型对象都指向同一个Animal的实例,所以 cat2.list 读取到的值也是改变后的。
    cat1.list.push(4)
    console.log(cat1.list) // [1,2,3,4]
    console.log(cat2.list) // [1,2,3,4]
    

    关于Cat.prototype.constructor = Cat,是给Cat构造函数的原型对象上的constructor属性重新赋值。
    因为任何一个prototype对象都有一个constructor属性,指向它的构造函数,如果没有Cat.prototype = new Animal()时,Cat.prototype.constructor是指向Cat的,但是执行了这句代码后Cat.prototype.constructor指向Animal
    相当于

    Cat.prototype.constructor === Animal //true
    

    并且,构造函数创造出的每一个实例也有一个constructor属性,读取的是构造函数的prototype对象的constructor属性。
    相当于

     cat1.constructor === Cat.prototype.constructor // true
    

    因此,cat1.constructor也指向Animal

    cat1.constructor === Animal // true
    

    这样导致的结果是继承关系混乱
    手动修改了constructor,虽然解决了这个关系混乱的问题,但是代码中也可以看到这种实现方式也是有缺点的。
    ==优点==:
    实例是子类实例,同时也是父类的实例;
    实例可以访问到父类新增的原型属性和原型方法;
    子类原型共享父类原型,父类原型不共享子类原型;

    ==缺点==:
    继承的实例属性,所有子类共享同一个父类实例的实例属性;
    无法向父类构造函数传参;

    三、原型链继承改版,直接继承prototype
    基于第二种原型链方式的改进,想要解决之前方式的缺点。

    function Animal() {
        this.age = 10
    }
    function Cat() {}
    Animal.prototype.species = "动物"
    
    Cat.prototype = Animal.prototype
    Cat.prototype.constructor = Cat
    
    var cat1 = new Cat()
    
    console.log(cat1.species) // 动物
    console.log(cat1.age) // undefined
    
    Cat.prototype.gender = "formall"
    var a = new Animal()
    console.log(a.gender) // formall
    

    这种方式跳过new Animal()直接继承Animal.prototype。想象的是不共享同一个父类实例属性,但是又导致一个问题,Cat.prototypeAnimal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会体现到Animal.prototype上。同时子类实例无法访问父类实例属性。
    ==缺点==:
    子类父类共享原型对象;
    无法继承父类实例属性;

    四、原型链继承改版,利用空对象
    先来解决子类父类共享原型对象的问题,实用的办法是创建一个中间对象。

    function Animal() { 
        this.age = 10 
    }
    function Cat() { }
    
    var F = function () { }
    F.prototype = Animal.prototype
    Cat.prototype = new F()
    Cat.prototype.constructor = Cat
    
    Animal.prototype.species = "动物"
    Cat.prototype.gender = "formall"
    var cat1 = new Cat()
    var a = new Animal()
    console.log(cat1.species)  // 动物
    console.log(cat1.age)  // undefined
    console.log(a.gender) // undefined
    

    显然这种方式解决了子父类共享原型对象的问题,但是无法继承父类实例属性的问题还在。依然不能访问父类的实例属性。
    ==优点==:
    子类添加原型属性,父类不会更新;

    ==缺点==:
    无法继承父类实例属性;

    五、组合继承(构造函数+原型链)
    实现了这么多种继承,但是每种都有缺点不足。能不能去其糟粕取其精华呢?实现一个较优的继承方式。

    function Animal(species, age) {
       this.species = species || "动物"
       this.age = age || 10
       this.list = [1,2,3]
    }
    function Cat() {
       Animal.call(this)
    }
    Cat.prototype = Object.create(Animal.prototype)
    Cat.prototype.constructor = Cat
    
    var c1 = new Cat('cat1')
    var c2 = new Cat('cat2')
    var a1 = new Animal('ani',10)
    
    // 验证子父类原型对象共享问题
    Animal.prototype.area = "Asia"
    Cat.prototype.gender = "formall"
    console.log(c1.area) // Asia
    console.log(a1.gender) // undeifined
    
    // 验证无法访问父类实例属性问题
    console.log(c1.species) // 动物
    
    // 验证不同实例共享父类实例属性问题
    c1.list.push(4)
    console.log(c1.list) // [1,2,3,4]
    console.log(c2.list) // [1,2,3]
    
    

    其实继承还有很多种实现方式,就不一一举例了。并没有最好的方式,不同的实现有各自的优缺点,找到最适合的就是最好的。

    相关文章

      网友评论

          本文标题:老掉牙,但永不过时的面向对象——继承

          本文链接:https://www.haomeiwen.com/subject/zfbuwhtx.html