• Javascript之对象组合继承


    感悟:

    最近看了一些关于Javascript对象继承的知识,发现自己之前虽然看了一些书,但是很多知识都忘了。虽然很多东西都忘了,但再次看的过程中对这些东西不会再向刚接触时那么陌生,而且理解起来也比之前顺畅和透彻多了。

    充分说明:多看书是有意义的。

    ————————————————————————————————————————————————————————————————————————————————————————————碎碎念

    关于对象之间的继承,在Javascript中主要是通过原型对象链来实现的,这一点与java这种基于类的面向对象语言有明显的不同,Javascript是基于原型的面向对象语言(大部分人说是基于对象的语言两种说法的观点不同)。

    下面来具体说一下继承的实现方式:

    一、组合继承

     1.组合继承将原型链和借用构造函数技术组合在一起。通过使用apply或者是call借用构造函数,借用对象可以得到被借用对象的实例属性。首先来看一个栗子:

    function People(){
        this.species = "人类";
    }
    
    People.prototype.nationality = "中国";
    People.prototype.showSpecies = function(){
        return this.species;
    }
    
    function Person(name, sex){
        People.apply(this,arguments);
    //此处是 Person 的实例属性,当然也可以添加一些实例方法
    this.name = name; this.sex = sex; } var person1 = new Person("二狗","男"); alert(person1.species);//人类 alert(person1.nationality);//undefined alert(person1.showSpecies());//Uncaught TypeError: person1.showSpecies is not a function

    通过结果可以看到,person1 是构造函数 Person 的一个实例,因为构造函数 Person 使用了借用构造函数技术  

     People.apply(this,arguments);

    Person 就可以获得 People 的实例属性 species;但是 Person 无法获得 People 的原型属性:nationality 和原型方法:showSpecies();

    如果想让 Person 获得 People 的原型属性和原型方法,需要让 Person 获得 People 的原型对象(隐式)上的属性和方法。

    本质上讲:就是要重写 Person 的原型对象。

    a. 一种简单实用的做法是直接将 People 的原型属性和原型方法复制给 Person:(也称之为拷贝继承)

    function People(){
        this.species = "人类";
    }
    People.prototype.nationality = "中国";
    People.prototype.showSpecies = function(){
        return this.species;
    }
    
    function Person(name, sex){
        People.apply(this,arguments);
        this.name = name;
        this.sex = sex;
    }
    //遍历并复制
    for(var i in People.prototype){
        Person.prototype[i] = People.prototype[i];
    }
    
    var person1 = new Person("二狗","男");
    alert(person1.species);//人类
    alert(person1.nationality);中国
    alert(person1.showSpecies());人类

     注意:上面这种复制是将 People.prototype 复制给了 Person.prototype。Person 拥有了和 People 一样的隐式原型对象。通过对原型对象上的数组进行操作可以证实:

    function People(){
        this.species = "人类";
    }
    
    People.prototype.colorArray = ["red", "blue"];
    
    function Person(name, sex){
        People.apply(this,arguments);
        this.name = name;
        this.sex = sex;
    }
    
    for(var i in People.prototype){
        Person.prototype[i] = People.prototype[i];
    }
    
    var person1 = new Person("二狗","男");
    alert(person1.colorArray.push("green"));//3
    
    var person2 = new People("二毛","男");
    alert(person2.colorArray.push("black"));//4

    两个不同构造函数实例化得到的对象,他们操作原型对象上的数组是同一个,说明这种原型对象上的原型属性和原型方法的复制是遵循一般的 Javascript 复制规则的。

    可以对这个隐式的原型对象做一些别的操作,比如:

    修改隐式对象对某个已引用方法:

    function People(){
        this.species = "人类";
    }
    People.prototype.nationality = "中国";
    People.prototype.showSpecies = function(){
        return this.species;
    };
    People.prototype.cheers = function(){
        return "中国加油!";
    };
    
    function Person(name, sex){
        People.apply(this,arguments);
        this.name = name;
        this.sex = sex;
    }
    
    for(var i in People.prototype){
        Person.prototype[i] = People.prototype[i];
    }
    
    Person.prototype.cheers = function(){
        return "中国必胜!";
    };
    
    var person1 = new Person("二狗","男");
    alert(person1.species);//人类
    alert(person1.nationality);//中国
    alert(person1.showSpecies());//人类
    alert(person1.cheers());//中国必胜!
    
    var person2 = new People("二毛","男");
    alert(person2.cheers());//中国加油!

    可以看到:person1 利用修改构造函数对应的原型对象中的方法,引用了一个新的方法。也可以向下面的样子先引用一个方法,再通过继承覆盖之前的引用,当然,这样做没什么意义:

    function People(){
        this.species = "人类";
    }
    People.prototype.nationality = "中国";
    People.prototype.showSpecies = function(){
        return this.species;
    };
    People.prototype.cheers = function(){
        return "中国加油!";
    };
    
    function Person(name, sex){
        People.apply(this,arguments);
        this.name = name;
        this.sex = sex;
    }
    
    Person.prototype.cheers = function(){
        return "中国必胜!";
    };
    
    
    for(var i in People.prototype){
        Person.prototype[i] = People.prototype[i];
    }
    
    
    var person1 = new Person("二狗","男");
    alert(person1.species);//人类
    alert(person1.nationality);//中国
    alert(person1.showSpecies());//人类
    alert(person1.cheers());//中国加油!
    
    var person2 = new People("二毛","男");
    alert(person2.cheers());//中国加油!

    b. 将 People 的实例赋值给 Person 的原型对象,同时修改 Person 原型对象的 constructor 属性值为自身。

    如果只是单单将 People 的实例赋值给 Person 的原型对象, 而不修改 Person 原型对象的 constructor 属性会怎么样?

    来看看修改 Person 原型对象前后 Person 构造函数的 Person.prototype.constructor 属性:

    function People(){
        this.species = "人类";
    }
    
    function Person(name, sex){
        People.apply(this,arguments);
        this.name = name;
        this.sex = sex;
    }
    alert(Person.prototype.constructor);//Person这个完整的函数
    
    Person.prototype = new People();
    alert(Person.prototype.constructor);//People这个完整的函数

    可以看到在将 People 实例赋值给 Person 前后,Person 的构造函数变了,如果这个时候什么都不做,那么再对 Person 实例化:

    function People(){
        this.species = "人类";
    }
    
    function Person(name, sex){
        People.apply(this,arguments);
        this.name = name;
        this.sex = sex;
    }
    alert(Person.prototype.constructor);//Person这个完整的函数
    
    Person.prototype = new People();
    alert(Person.prototype.constructor);//People这个完整的函数
    var person1 = new Person("二狗","男"); alert(person1.constructor);//People这个完整的函数

    会发现构造函数 Person 的实例: person1 的构造函数居然不是 Person 而是 People,这显然不对。 

    也就是说构造函数 People 在将其实例赋值给 Person 的原型对象时,同时也将 Person 的原型对象的属性 constructor 也更换了(很明显,因为 constructor 是 prototype 的属性,皮之不存,毛将焉附?),

    而 prototype.constructor 的值表示由当前构造函数对象 实例化的 函数对象的构造函数(简单点说 Person.prototype.constructor 就是告诉构造函数对象 Person 的实例,他们是谁构造的,

    而 person1.constructor 让作为实例对象的 person1 直指它自己的构造函数)。——可以看出,正常情况下 Person.prototype.constructor === person1.constructor 应该成立。

    function People(){
    	this.species = "人类";
    }
    
    function Person(name, sex){
    	People.apply(this,arguments);
    	this.name = name;
    	this.sex = sex;
    }
    alert(Person.prototype.constructor);//Person这个完整的函数
    
    Person.prototype = new People();
    Person.prototype.constructor = Person;
    
    var person1 = new Person("二狗","男");
    
    alert(person1.constructor === Person.prototype.constructor);//true

    通过上面讲到的组合继承的两种实现方式,虽然能够实现属性和方法、原型属性和原型方法的继承,但是存在一个不足:无论在什么情况下,被继承的函数对象都会被调用两次

    先来看一个有趣的地方,假如:在组合继承中不使用 借用构造函数技术 而直接重写原型对象,会发生什么?

    function People(){
        this.species = "人类";
        this.arr = [1];
    }
    People.prototype.nationality = "中国";
    People.prototype.showSpecies = function(){
        return this.species;
    };
    
    function Person(name, sex){
        // People.apply(this,arguments);
        this.name = name;
        this.sex = sex;
    }
    
    Person.prototype = new People();//让People的实例属性变成Person的原型属性
    Person.prototype.constructor = Person;
    
    var person1 = new Person("二狗","男");
    
    person1.arr.push(2);//1,2
    alert(person1.species);//人类
    alert(person1.arr);
    alert(person1.nationality);//中国
    alert(person1.showSpecies());//人类
    
    var person2 = new People("二毛","男");
    person2.arr.push(3);
    alert(person2.arr);//1,3
    alert(person1.arr);//1,2

    这样看来在构造函数 People 和 构造函数 Person 的实例对象中都能正常使用 People 的实例属性。如果是单个构造函数 Person 的多个 实例对象呢?

    function People(){
        this.species = "人类";
        this.arr = [1];
    }
    People.prototype.nationality = "中国";
    People.prototype.showSpecies = function(){
        return this.species;
    };
    
    function Person(name, sex){
        // People.apply(this,arguments);
        this.name = name;
        this.sex = sex;
    }
    
    Person.prototype = new People();//让People的实例属性变成Person的原型属性
    Person.prototype.constructor = Person;
    
    var person1 = new Person("二狗","男");
    var person3 = new Person("三狗","男");
    
    person1.arr.push(2);
    alert(person1.arr);//1,2
    
    person3.arr.push(4);
    alert(person3.arr);//1,2,4
    alert(person1.arr);//1,2,4
    
    var person2 = new People("二毛","男");
    person2.arr.push(3);
    alert(person2.arr);//1,3
    alert(person1.arr);//1,2,4
    var person4 = new People("四毛","男");
    person4.arr.push(5);
    alert(person4.arr);//1,5
    alert(person2.arr);//1,3

    注意看构造函数 People 和构造函数 Person 的实例通过相同操作后结果的差异。

    构造函数 People 的实例会各自拥有自己的数组 arr, 各自的操作间是不会相互影响的,但是构造函数 Person 的实例都拥有相同的数组 arr 引用,他们操作的是同一个数组。那么实例化得到的不同对象无法正常使用各自的 arr。

    所以在组合继承中,借用构造函数和原型链缺一不可。

    这里的 person1.__proto__ 是帮助我们拿到 person1 的构造函数 Person 的原型对象。可以看到,上面直接将 People 的实例属性转化为 Person 的原型属性,而 People 的原型属性和原型方法也成为  Person 的原型属性和原型方法。

    但是,组合继承也存在缺点,最明显的就是:无论在什么情况下,只要实例化了使用组合继承后的对象,被继承的对象都会调用两次。

    function People(){
        this.species = "人类";
    }
    People.prototype.nationality = "中国";
    People.prototype.showSpecies = function(){
        return this.species;
    };
    
    function Person(name, sex){
        People.apply(this,arguments);//第二次调用People()
        this.name = name;
        this.sex = sex;
    }
    
    Person.prototype = new People();//第一次调用People()
    Person.prototype.constructor = Person;
    
    var person1 = new Person("二狗","男");

    第一次发生在将 People 的实例对象潜复制给 Person 的原型对象(可以是直接复制,也可以是通过 People 的实例 new People() );

    第二次发生在调用构造函数 Person 时。

     在第一次调用的时候,其实 Person 就已经拿到了 People 的实例化属性 species,而第二次调用发生在实例化 Person 的时候,通过借用构造函数,Person 又一次的调用了 People,会在 Person1 上创建实例属性 species。

    总结:组合继承通过借用构造函数技术和重写原型对象,可以让一个对象继承另一个对象的属性和方法原型属性和原型方法。在组合继承中借用构造函数技术和重写原型对象都是必要的,缺少了任何一个都会使继承不能正常工作。

    组合继承也存在缺点,主要是被继承的对象的实例属性的重复调用。

  • 相关阅读:
    C# 上传下载文件
    ASP.Net
    C#计算程序执行时间
    关于vs连接access
    CSS
    C# 获取命名空间,类,方法名
    C# Split
    C#方法参数:params,ref,out,可选参数,命名参数
    java学习手记:JDK的安装配置
    Mysql/SQLServer数据类型与java基本数据类型的对应
  • 原文地址:https://www.cnblogs.com/tisikcci/p/5837986.html
Copyright © 2020-2023  润新知