js高级----回顾prototype、__proto__、constructor与JavaScript的原型链继承

JavaScript没有传统面向对象语言的类继承机制,而是基于原型链继承实现的,其本质是使用函数模拟类的特征。我们可以通过prototype将属性写到原型链上,调用new操作符创建对象(实例化)时,对象实例会把类原型链上的属性关联到自身的__proto__属性上;而子类继承父类时,是将子类的prototype属性指向父类的prototype属性,并在子类prototype属性添加自己的方法和属性实现对父类的扩展

1. 类与实例

1.1 类定义

在JavaScript中我们会像下面这样模拟一个类:

    function Person(name,sex) {
        this.name = name;
        this.sex = sex;
    }
    Person.prototype.sayName = function() {
        console.log(this.name)
    }
    var p = new Person('muzi','男');
    p.sayName();

该类包含两个属性和一个方法。

1.2 实例化

类定义后,就可以通过new关键创建类实例:

    var person = new Person('muzidigbig','男');
    console.log(person.__proto__ === Person.prototype);//true

JavaScript中实例化不同与传统面向对象语言(如:Java、C++等),其实例化基于对象原型。

__proto__是一个对象内部属性,继承自Object.prototype.__proto__。当类被实例化时,对象的(类实例)的__proto__属性会指向类的原型,即:类的prototype属性。

这就是基于原型的类的实现方式,也就是原型链的实现方式。实例化后当调用对象的属性或方法时,会有如下过程:

  • 查找对象是否有该属性或方法,如果则在则返回或调用
  • 如果不存在,则通过__proto__属性,在原型链上查找有没有属性或方法

2. 类的继承(4步曲)

继承、封装、多态是面向对象语言三大特征,JavaScript基于原型同样可以模拟出这三个特性。单就继承来说又分为单继承和多继承,但JavaScript只能实现单继承。

我可以像下面这样模拟一类继承:

    //第一步:创建父类
    function Person(name,sex) {
        this.name = name;
        this.sex = sex;
    }
    //在父类原型对象上创建方法
    Person.prototype.sayName = function() {
        console.log(this.name+',我是一个人')
    }

    // 等同于Object.create的方法
    function object(prop) {
        var F = function() {};
        F.prototype = prop;
        return new F();
    }
    
    //第二步:创建子类
    function Student(name,sex) {
        Person.call(this,name,sex);
    }

    //第三步:创建继承的关系
    //等价于Student.prototype = Object.create(Person.prototype)
    Student.prototype = object(Person.prototype);

    //第四步:改变子类的构造器
    Student.prototype.constructor = Student;

    Student.prototype.work = function() {
        console.log('我的工作是学习');
    }

在这个继承过程中,我们做了以下几件事:

  • 在子类的构造函数中调用父类的构造函数
  • 将父类的原型属性prototype复制到子类的原型prototype
  • 将子类的构造器constructor指向子类的构造函数

JavaScript中继承的本质是原型链的复制,创建子类的实例后,其__proto__属性会指定子类的prototype,但它同时是一个父类的实例。

    console.log(student.__proto__ === Person.prototype);//false
    console.log(student.__proto__ === Student.prototype);//true
    console.log(student instanceof Student);//true
    console.log(student instanceof Person);//true

仍然可以通过__proto__.__proto__访问父类中的方法:

student.__proto__.__proto__.sayName(); // undefined,我是一个人

除上述的自定义object方法外,还可以使用Object.create()实现类原型的复制。无论使用哪种方式,这只对面向对象继承的一种模拟,其与传统的类的实例化与继承相比有以下几个特点:

  • 在传统的类中,“类名”同进是一个构造函数,而JavaScript使用函数模拟了构造函数
  • 在传统的类中有可以被继承到子类中的属性和方法,而而JavaScript使用prototype实现了继承
  • 传统的类通过new关键字来实例化一个类,而JavaScript的实例化过程是对prototype属性的复制

注意:在ECMAScript 2015(ES6)中新增了class类定义的方式,但其本质仍然是基于函数的类模拟。

    //Object.getPrototypeOf()方法读取实例的原型对象
    console.log(Object.getPrototypeOf(student));
    //isPrototypeOf()函数用于指示对象是否存在于另一个对象的原型链中。如果存在,返回true,否则返回false。

 

补充:

  1. constructor属性

每个函数都有constructor这个属性指向构造函数,例如:{}和new Object()的contructor指向同一个构造函数

prototype属性又指向了一个prototype对象,注意prototype属性与prototype对象是两个不同的东西,要注意区别。在prototype对象中又有一个constructor属性,这个constructor属性同样指向一个constructor对象,而这个constructor对象恰恰就是这个function函数本身。 

js高级----回顾prototype、__proto__、constructor与JavaScript的原型链继承

js高级----回顾prototype、__proto__、constructor与JavaScript的原型链继承

 我们接着看代码:

function Person(name) {
    this.name=name;
    this.showMe=function(){
        alert(this.name);
    }
};

Person.prototype.from=function() {
    alert('I come from prototype.');
}

var one=new Person('js');

one.showMe();//js,这个结果没有什么好奇怪的
one.from();//I come from prototype.,这个结果有一点奇怪吧

要解释这个结果就要仔细研究一下new这个操作符了.var one=new Person(‘js’);这个语句执行的过程可以分成下面的语句:

var one={};  
Person.call(one,'js'); 

按照《悟透javascript》书中说的,new形式创建对象的过程实际上可以分为三步:
第一步是建立一个新对象(叫A吧);

第二步将该对象(A)内置的原型对象设置为构造函数(就是Person)prototype 属性引用的那个原型对象;

第三步就是将该对象(A)作为this 参数调用构造函数(就是Person),完成成员设置等初始化工作。

其中第二步中出现了一个新名词就是内置的原型对象,注意这个新名词跟prototype对象不是一回事,为了区别我叫它inobj,inobj就指向了函数Person的prototype对象。在person的prototype对象中出现的任何属性或者函数都可以在one对象中直接使用,这个就是javascript中的原型继承了。

又头晕了,上图吧!

js高级----回顾prototype、__proto__、constructor与JavaScript的原型链继承

这样one对象通过内置的原型对象inobj就可以直接访问Person的prototype对象中的任何属性与方法了。这也就解释了上面的代码中为什么one可以访问form函数了。因为prototype对象中有一个constructor属性,那么one也可以直接访问constructor属性。

代码:

    one.showMe();//js,这个结果没有什么好奇怪的  
    one.from();//I come from prototype.,这个结果有一点奇怪吧  
    alert(one.constructor);//function Person(name) {...}  
    alert(Person.prototype.constructor);//function Person(name) {...} 
    console.log(one.constructor === Person.prototype.constructor);//true

再看看继承是如何实现的:

function Person(name)  
{  
   this.name=name;  
   this.showMe=function()  
        {  
           alert(this.name);  
        }  
};  

Person.prototype.from=function()  
{  
  alert('I come from prototype.');  
}  

function SubPerson()  
{  
}  
SubPerson.prototype=new Person();  

var subOne=new SubPerson();  
subOne.from();//I come from prototype.  
alert(subOne.constructor);//function Person(name) {...};  
alert(SubPerson.prototype.constructor);//function Person(name) {...}; 

继承的实现很简单,只需要把子类的prototype设置为父类的一个对象即可。注意这里说的可是对象哦!

那么通过prototype属性实现继承的原理是什么呢?还是先看图形说明,然后编写代码进行验证。

js高级----回顾prototype、__proto__、constructor与JavaScript的原型链继承

注意:红色的方框就是把子类与父类链接起来的地方。这个就应该是传说中的prototype链了吧。下面有代码进行验证。

js代码:

    function Person(name)  
    {  
       this.name=name;  
       this.showMe=function()  
            {  
               alert(this.name);  
            }  
    };  

    Person.prototype.from=function()  
    {  
      alert('I come from prototype.');  
    }  
    var father=new Person('js');//为了下面演示使用showMe方法,采用了js参数,实际多采用无参数  
    alert(father.constructor);//查看构造函数,结果是:function Person(name) {...};  
    function SubPer()  
    {  
    }  
    SubPer.prototype=father;//注意这里  
    SubPer.prototype.constructor=SubPer;  

    var son=new SubPer();  
    son.showMe();//js  
    son.from();//I come from prototype.  
    alert(father.constructor);//function SubPer(){...}  
    alert(son.constructor);//function SubPer(){...}  
    alert(SubPer.prototype.constructor);//function SubPer(){...}  

 

参考文献:https://itbilu.com/javascript/js/NkERWtG.html

contructor参考文献:https://www.cnblogs.com/zjunet/p/4559895.html