资讯专栏INFORMATION COLUMN

js原型和继承

Hujiawei / 2152人阅读

摘要:举例说明组合继承组合继承利用原型链借用构造函数的模式解决了原型链继承和类式继承的问题。示例组合式继承是比较常用的一种继承方法,其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

对js原型和继承的理解一直处于“不懂-懂-不懂-懂-不懂。。。”的无限循环之中,本来打算只是简单总结下js继承方式,可看了些网上的资料后,发现又不懂继承了。。。这篇文章只是一个阅读笔记,总结了我所看到的js文章,参考见文末。

一、原型与构造函数 1、prototype属性

       Js所有的函数都有一个prototype属性,这个属性引用了一个对象,即原型对象,也简称原型。这个函数包括构造函数和普通函数,我们讲的更多是构造函数的原型,但是也不能否定普通函数也有原型。譬如普通函数:

function F(){};
alert(F.prototype instanceof Object);//true

默认情况下,原型对象也会获得一个constructor属性,该属性包含一个指针,指向prototype属性所在的函数

Person.prototype.constructor === Person

在面向对象的语言中,我们使用类来创建一个自定义对象。然而js中所有事物都是对象,那么用什么办法来创建自定义对象呢?这就需要用到js的原型:
我们可以简单的把prototype看做是一个模版,新创建的自定义对象都是这个模版(prototype)的一个拷贝 (实际上不是拷贝而是链接,只不过这种链接是不可见,新实例化的对象内部有一个看不见的__Proto__指针,指向原型对象)。

关于_proto_

_proto_是对[[propertyName]]属性的实现(是只能对象可以拥有的属性,并且是不可访问的内部属性),它指向对象的构造函数的原型对象。如下:

function Person(name) {
    this.name = name;
}
var p1 = new Person();
p1._proto_ === Person.prototype;//true

__proto__只是浏览器的私有实现,目前ECMAScript标准实现方法是Object.getPrototypeOf(object),如下:

function Person(name) {
    this.name = name;
}
var p1 = new Person();
Object.getPrototypeOf(p1) === Person.prototype;//true
2、通过构造函数实例化对象过程

       构造函数,也即构造对象。首先了解下通过构造函数实例化对象的过程

function A(x){
     this.x=x;
}
var obj = new A(1);

实例化obj对象有三步:

(1).创建obj对象:obj=new Object();
(2).将obj的内部__proto__指向构造他的函数A的prototype,同时obj.constructor===A.prototype.constructor(这个是永远成立的,即使A.prototype不再指向原来的A原型,也就是说:类的实例对象的constructor属性永远指向"构造函数"的prototype.constructor),从而使得obj.constructor.prototype指向A.prototype(obj.constructor.prototype===A.prototype,当A.prototype改变时则不成立,下文有遇到)obj.constructor.prototype与的内部_proto_是两码事,实例化对象时用的是_proto_,obj是没有prototype属性的,但是有内部的__proto__,通过__proto__来取得原型链上的原型属性和原型方法,FireFox公开了__proto__,可以在FireFox中alert(obj.__proto__);
(3).将obj作为this去调用构造函数A,从而设置成员(即对象属性和对象方法)并初始化。

当这3步完成,这个obj对象就与构造函数A再无联系,这个时候即使构造函数A再加任何成员,都不再影响已经实例化的obj对象了。此时,obj对象具有了x属性,同时具有了构造函数A的原型对象的所有成员,当然,此时该原型对象是没有成员的。

3、原型对象

原型对象初始是空的,也就是没有一个成员(即原型属性和原型方法)。可以通过如下方法验证原型对象具有多少成员。

function A(x){
    this.x=x;
}

var num = 0;
for(o in A.prototype){
    alert(o);// alert 原型属性名字
    num++;
} 
alert(num);//0

但是,一旦定义了原型属性或原型方法,则所有通过该构造函数实例化出来的所有对象,都继承了这些原型属性和原型方法,这是通过内部的_proto_链来实现的。

例如:

A.prototype.say=function(){alert("haha");}

那所有的A的对象都具有了say方法,这个原型对象的say方法是唯一的副本给大家共享的,而不是每一个对象都有关于say方法的一个副本。

二、原型与继承

由于js不像java那样是真正面向对象的语言,js是基于对象的,它没有类的概念。所以,要想实现继承,可以通过构造函数和原型的方式模拟实现类的功能。

js里常用的如下两种继承方式:

    原型链继承(对象间的继承)
    类式继承(构造函数间的继承)   

以下通过实例详细介绍这两种继承方式的原理

1、类式继承详解

类式继承是在子类型构造函数的内部调用超类型的构造函数。

1    function A(x){
2        this.x = x;
3    }
4    function B(x,y){
5        this.tempObj = A;
6        this.tempObj(x);
7        delete this.tempObj;
8        this.y=y;
9    }

解释
       第5、6、7行:创建临时属性tmpObj引用构造函数A,然后在B内部执行(注意,这里执行函数的时候并没有用new),执行完后删除。当在B内部执行了this.x=x后(这里的this是B的对象),B当然就拥有了x属性,当然B的x属性和A的x属性两者是独立,所以并不能算严格的继承。第5、6、7行有更简单的实现,就是通过call(apply)方法:A.call(this,x);
       这两种方法都有将this传递到A的执行里,this指向的是B的对象,这就是为什么不直接A(x)的原因。这种继承方式即是类继承(js没有类,这里只是指构造函数),虽然继承了A构造对象的所有属性方法,但是不能继承A的原型对象的成员。而要实现这个目的,就是在此基础上再添加原型继承。

2、原型链继承详解

原型式继承是借助已有的对象创建新的对象,将子类的原型指向父类,就相当于加入了父类这条原型链。

1    function A(x){
2        this.x = x;
3    }
4    A.prototype.a = "a";
5    function B(x,y){
6        A.call(this,x);
7        this.y = y;
8    }
9    B.prototype.b1 = function(){
10        alert("b1");
11    }
12   B.prototype = new A();
13   B.prototype.b2 = function(){
14        alert("b2");
15    }
16   B.prototype.constructor = B;
17   var obj = new B(1,3);

解释
       这个例子讲的就是B继承A。第7行类继承:A.call(this.x);上面已讲过。实现原型继承的是第12行:B.prototype = new A();
       就是说把B的原型指向了A的1个实例对象,这个实例对象具有x属性,为undefined,还具有a属性,值为"a"。所以B原型也具有了这2个属性(或者说,B和A建立了原型链,B是A的下级)。而因为方才的类继承,B的实例对象也具有了x属性,也就是说obj对象有2个同名的x属性,此时原型属性x要让位于实例对象属性x,所以obj.x是1,而非undefined。第13行又定义了原型方法b2,所以B原型也具有了b2。虽然第9~11行设置了原型方法b1,但是你会发现第12行执行后,B原型不再具有b1方法,也就是obj.b1是undefined。因为第12行使得B原型指向改变,原来具有b1的原型对象被抛弃,自然就没有b1了。
       第12行执行完后,B原型(B.prototype)指向了A的实例对象,而A的实例对象的构造器是构造函数A,所以B.prototype.constructor就是构造对象A了(换句话说,A构造了B的原型)。alert(B.prototype.constructor)出来后就是"function A(x){...}" 。同样地,obj.constructor也是A构造对象,alert(obj.constructor)出来后就是"function A(x){...}" ,也就是说B.prototype.constructor===obj.constructor(true),但是B.prototype===obj.constructor.prototype(false),因为前者是B的原型,具有成员:x,a,b2,后者是A的原型,具有成员:a。如何修正这个问题呢,就在第16行,将B原型的构造器重新指向了B构造函数,那么B.prototype===obj.constructor.prototype(true),都具有成员:x,a,b2。
       如果没有第16行,那是不是obj = new B(1,3)会去调用A构造函数实例化呢?答案是否定的,你会发现obj.y=3,所以仍然是调用的B构造函数实例化的。虽然obj.constructor===A(true),但是对于new B()的行为来说,执行了上面所说的通过构造函数创建实例对象的3个步骤,第一步,创建空对象;第二步,obj.__proto__ === B.prototype,B.prototype是具有x,a,b2成员的,obj.constructor指向了B.prototype.constructor,即构造函数A;第三步,调用的构造函数B去设置和初始化成员,具有了属性x,y。虽然不加16行不影响obj的属性,但如上一段说,却影响obj.constructor和obj.constructor.prototype。所以在使用了原型继承后,要进行修正的操作。
       关于第12、16行,总言之,第12行使得B原型继承了A的原型对象的所有成员,但是也使得B的实例对象的构造器的原型指向了A原型,所以要通过第16行修正这个缺陷。

三、js继承的6种方法 1、原型链继承

为了让子类继承父类的属性(也包括方法),首先需要定义一个构造函数。然后,将父类的新实例赋值给构造函数的原型。

function Parent(){
    this.name = "mike";
}

function Child(){
    this.age = 12;
}
Child.prototype = new Parent();//Child继承Parent,通过原型,形成链条

var test = new Child();
alert(test.age);
alert(test.name);//得到被继承的属性
//继续原型链继承
function Brother(){   //brother构造
    this.weight = 60;
}
Brother.prototype = new Child();//继续原型链继承
var brother = new Brother();
alert(brother.name);//继承了Parent和Child,弹出mike
alert(brother.age);//弹出12

以上原型链继承还缺少一环,那就是Object,所有的构造函数都继承自Object。而继承Object是自动完成的,并不需要我们自己手动继承,那么他们的从属关系可以使用操作符instanceof和函数isPrototypeOf()判断,如下:

alert(test instanceof Parent);//true
alert(test instanceof Child);//true
alert(brother instanceof Parent);//true
alert(brother instanceof Child);//true
alert(Parent.prototype.isPrototypeOf(test));//true
alert(Child.prototype.isPrototypeOf(test));//true
alert(Parent.prototype.isPrototypeOf(brother));//true
alert(Child.prototype.isPrototypeof(brother));//true

问题字面量重写原型会中断关系,使用引用类型的原型,并且子类型还无法给超类型传递参数。

伪类解决引用共享和超类型无法传参的问题,我们可以采用“借用构造函数”技术

2、借用构造函数继承(类式继承,这种方式可以实现多继承)

示例1

function Parent(firstname)
{
    this.fname=firstname;
    this.age=40;
    this.sayAge=function()
    {
        console.log(this.age);
    }
}
function Child(firstname)
{
    this.parent=Parent;
    this.parent(firstname);
    delete this.parent;//以上三行也可以用call和apply函数改写
    this.saySomeThing=function()
    {
        console.log(this.fname);
        this.sayAge();
    }
}
var mychild=new  Child("李");
mychild.saySomeThing();

示例2:用call函数实现

function Parent(firstname)
{
    this.fname=firstname;
    this.age=40;
    this.sayAge=function()
    {
        console.log(this.age);
    }
}
function Child(firstname)
{

    this.saySomeThing=function()
    {
        console.log(this.fname);
        this.sayAge();
    }
   this.getName=function()
   {
       return firstname;
   }

}
var child=new Child("张");
Parent.call(child,child.getName());
child.saySomeThing();

示例3:用apply函数实现

function Parent(firstname)
{
    this.fname=firstname;
    this.age=40;
    this.sayAge=function()
    {
        console.log(this.age);
    }
}
function Child(firstname)
{

    this.saySomeThing=function()
    {
        console.log(this.fname);
        this.sayAge();
    }
    this.getName=function()
    {
        return firstname;
    }

}
var child=new Child("张");
Parent.apply(child,[child.getName()]);
child.saySomeThing();

问题:类式继承没有原型,子类型只是继承了父类型构造对象的属性和方法,没有继承父类型原型对象的成员。

举例说明:

function Father(x){
    this.x=x;
}

Father.prototype.say = function(){
    alert(this.x);
}

function Child(x,y){
    this.y=y;
    this.tempObj=Father;
    this.tempObj(x);
    delete this.tempObj;
}

var obj = new Child(1,2);
alert(obj.x);//1
alert(obj.y);//2
alert(obj.say);//undefined
3、组合继承

组合继承利用原型链+借用构造函数的模式解决了原型链继承和类式继承的问题。
示例

function Parent(age){
    this.name = ["mike","jack","smith"];
    this.age = age;
}
Parent.prototype.say = function(){
    return this.name + "are both" + this.age;
}
function Child(age){
    Parent.call(this,age);
}
Child.prototype = new Parent();
var test = new Child(1);
alert(test.say());

组合式继承是比较常用的一种继承方法,其背后的思路是 使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又保证每个实例都有它自己的属性。

问题:组合继承的父类型在使用过程中会被调用两次;一次是创建子类型的时候,另一次是在子类型构造函数的内部。
举例说明如下:

    function Parent(name){
        this.name = name;
        this.arr = ["哥哥","妹妹","父母"];
    }

    Parent.prototype.run = function () {
        return this.name;
    };

    function Child(name,age){
        Parent.call(this,age);//第二次调用
        this.age = age;
    }

    Child.prototype = new Parent();//第一次调用

这两次调用一次是通过类式继承实现的,另一次是通过原型链继承实现的,在上面原型链继承详解中有介绍。可以用下面介绍的寄生组合继承解决该问题。

4、原型式继承

这种继承借助原型并基于已有的对象创建新对象。

1    function obj(o){
2        function F(){}
3        F.prototype = o;
4        return new F();
5    }
6     var person = {
7        name : "ss",
8        arr : ["hh","kk","ll"]
9    }

10    var b1 = obj(person);
11    alert(b1.name);//ss
12    b1.name = "join";
13    alert(b1.name);//join

14    alert(b1.arr);//hh,kk,ll
15    b1.arr.push("gg");
16    alert(b1.arr);//hh,kk,ll,gg

17    var b2 = obj(person);
18    alert(b2.name);//ss
19    alert(b2.arr);//hh,kk,ll,gg

疑问:(求解答)
这里的b1b2会共享原型对象person的属性,那么在12行通过b1.name="join"修改了原型对象personname属性后,为什么并没有影响到b2.name,在18行仍然输出ss???
而在15行通过b1.arr.push("gg")修改了原型对象personarr属性后,却影响到了b2.arr,在19行的输出多了gg

5、寄生式继承

这种继承方式是把原型式+工厂模式结合起来,目的是为了封装创建的过程。

function obj(o){
    function F(){}
    F.prototype = o;
    return new F()
}

function create(o){
    var f = obj(o);
    f.say = function() {
        return this.arr
    }
    return f
}
6、寄生组合继承

寄生组合继承解决了组合继承中父类型两次调用问题

function obj(o){
    function F(){}
    F.prototype = o;
    return new F()
}

function create(parent,child){//通过这个函数,实现了原型链继承,但child只含有parent原型链中的属性
    var f = obj(parent.prototype);
    f.constructor = child;
    child.prototype = f;
}

function Parent(name){
    this.name = name;
    this.arr = ["heheh","guagua","jiji"];
}

Parent.prototype.say = function(){
    return this.name
}

function Child(name,age){
    Parent.call(this,name);//类式继承,这里继承parent构造函数中定义的属性
    this.age = age;
}

create(Parent,Child);

var test = new Child("trigkit4",21);
test.arr.push("nephew");
alert(test.arr);//
alert(test.run());//只共享了方法
var test2 = new Child("jack",22);
alert(test2.arr);//引用问题解决
参考

1.js实现继承的5中方式
2.ES6 Class
3.js继承方式详解
4.前段开发必须知道的js(一)原型和继承
5.细说 Javascript 对象篇(二) : 原型对象
6.javascript原型概念(一)
7.Javascript基于 ‘__proto__’ 的原型链

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/87761.html

相关文章

  • JavaScript继承方式详解

    摘要:可以通过构造函数和原型的方式模拟实现类的功能。原型式继承与类式继承类式继承是在子类型构造函数的内部调用超类型的构造函数。寄生式继承这种继承方式是把原型式工厂模式结合起来,目的是为了封装创建的过程。 js继承的概念 js里常用的如下两种继承方式: 原型链继承(对象间的继承) 类式继承(构造函数间的继承) 由于js不像java那样是真正面向对象的语言,js是基于对象的,它没有类的概念。...

    Yangyang 评论0 收藏0
  • JS对象继承原型

    摘要:此用来定义通过构造器构造出来的对象的原型,构造器内部的代码用来给对象初始化。 对象继承 VS 类继承 在 class-based 的面向对象的世界里,要出现对象,必须先有类。类之间可以继承,类再使用 new 操作创建出实体,父子对象之间的继承体现在父类和子类上。你不能说 对象 a 继承了对象 b,只能说 class A 继承了 class B,然后他们各自有一个实例a、b。 JS中实现...

    QLQ 评论0 收藏0
  • 剖析JS原型继承

    摘要:接下来我们来聊一下的原型链继承和类。组合继承为了复用方法,我们使用组合继承的方式,即利用构造函数继承属性,利用原型链继承方法,融合它们的优点,避免缺陷,成为中最常用的继承。 JavaScript是一门面向对象的设计语言,在JS里除了null和undefined,其余一切皆为对象。其中Array/Function/Date/RegExp是Object对象的特殊实例实现,Boolean/N...

    darkerXi 评论0 收藏0
  • JS专题之继承

    摘要:构造函数所以,就有了畸形的继承方式原型链继承三原型链继承改变构造函数的原型对象继承了属性以上例子中,暴露出原型链继承的两个问题包含引用类型数据的原型属性,会被所有实例共享,基本数据类型则不会。 前言 众所周知,JavaScript 中,没有 JAVA 等主流语言类的概念,更没有父子类继承的概念,而是通过原型对象和原型链的方式实现继承。 于是,我们这一篇讲一讲 JS 中的继承(委托)。 ...

    rollback 评论0 收藏0
  • JS学习笔记(第6章)(面向对象之继承——JS继承的六大方式)

    摘要:除此之外,在超类型的原型中定义的方法,对子类型而言也是不可兼得,结果所有类型都只能用构造函数模式。创建对象增强对象指定对象继承属性这个例子的高效率体现在它只调用了一次构造函数。 1、原型链 原型链的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。构造函数、原型和实例的关系:每个构造函数都有一个原型对象;原型对象都包含着一个指向构造函数的指针;实例都包含一个指向原型对象的...

    lscho 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<