js继承关系

  跟传统面向对象语言比起来,js在继承关系方面比较特别,如果第一次看恐怕会有些抓狂,偶就是这样(又透露小白本质#=_=),从哪里说起好呢?函数调用?

  js中函数的调用方式大致可分以下几种:

  1. 普通函数,直接调用

    function Hi(){ alert(233); }
    Hi();
    var f = function(){ alert(666); };
    f();

  2. 作为对象的方法调用

    var obj = { x:1,  m:function(){ alert("hello"); } };
    obj.m();

  3. 构造函数调用,即new 表达式

    var a = new Array(1,2,3);

  4. 间接调用,最常用应该是call或apply了

    function f(){
         alert(this.name);
    }
    var obj = { name:"Apple" };
    f.call(obj);  // 间接调用函数f,输出Apple

  顾名思义就是非直接的调用一个函数了,这里的f.call(obj)的相当于obj.f(),如果直接写obj.f()肯定是错误的,因obj对象压根没定义这个方法,这便是奇妙的地方:obj对象通过call间接调用了f()函数。另一个跟call相似的函数是apply,道理相同。

  还是要牢记在js中除了null和undefined外都是对象,它们的基类都是Object,Object是所有对象的祖宗(好比六道仙人他妈),函数自然也是对象,而且js中所有的函数会自动继承Function这个类(当然最终继承自Object),在我们写js代码时,环境中已经有了这些类,Function这个类里面就定义了一些方法,其中就有call和apply,所以f才能打点这么调。

  call与apply方法的第一个参数是调用者所在的对象,比如这里想达到让obj来调用f()的目的,在f()函数中要对obj对象的属性进行引用,那么第一个参数就传入obj这个对象,这样f()函数中的上下文就是(context,比如全局作用域在浏览器中上下文是window对象,this是对它的引用)obj这个对象,所以在f()函数中this.name引用的是obj的name属性,这个this便是对obj的引用。

  如果f()函数需要传参数的话,用call这种间接调用的方式怎么写?这便是call与apply的不同之处了,假如现在f()是这样的:f(name, age){...},那么这两种方式是:

  call写法:f.call(obj, "Li", 23);

  apply写法:f.apply(obj, ["Li", 23]);

  很明显的区别:call以单个参数形式、逗号分隔传入,有几个写几个,放在第一个对象参数后面,apply则把所有要传入的参数统一放在一个数组中。

  有时调用apply第一个对象参数没有或不必要,可以传undefined或null,而且在全局作用域中用这种间接形式调用时,第一个参数可以是全局对象引用this,或者直接写undefined时,它会自动去把这个全局下文环境作为第一个参数对象,因此我们平常的写的函数可以有很多奇怪的写法:

    // 一般写法
    Math.max(1, 5, 7, 2);
    // 奇怪写法
    Math.max.call(Math, 1, 5, 7, 2);
    Math.max.call(window, 1, 5, 7, 2);
    Math.max.call(this, 1, 5, 7, 2);
    Math.max.call(undefined, 1, 5, 7, 2);
    Math.max.apply(Math, [1, 5, 7, 2]);  // max函数可传入不定个数参数

  大概设计者想让天下没有难调的方法,所以出这么一招,这样的话,理论上任何对象的可调方法,几乎都能被其他无关的对象调用。

  js有面向对象,却没有直接定义的类(第一次听很神奇),对象的实现由构造函数加调用表达式完成

    function Game(name){
        this.name = name;
    }
    var g = new Game("2048");

  在调用new的时候,对象已经形成,接着运行Game函数,Game函数内部的上下文就变成了这个实例化对象的环境,this便是对这个实例化对象的引用,所以Game构造函数里边的语句只是在对这个对象进行一下组装,就好比:

    var g = {};
    g.name = "2048";  // 对象已生成,这里对它设置一些属性

  js的继承方式大致分两种,第一种就是像平常那些语言(php、c++、java等),比如class A extends B{...},class A : public B{...},通过对父类的继承,称为类式继承。一般来说这种方式会在子类构造函数中调用父类构造函数(不是必须),以初始化父类对象中的一些属性、方法等,js同理也会这样

    function A(){
        this.a = "A";
    }
    function B(){
        this.b = "B";
        A.call(this);  // 继承A,调用它的构造函数
    }
    var bObj = new B();
    console.log(bObj.a); // A
    console.log(bObj.b); // B    

  这里便用到了间接调用,而且一般也会这么写,因为需要传入子对象的上下文this引用。父类构造函数A是个函数对象,它的调用只是在子类加了些东西进来而已,或者说初始化父类的属性,它的效果跟下面一样:

    function B(){
        this.b = "B";
        var self = this;
        function A(){ self.a = "A"; }  // 直接定义一个函数,为这个对象添加属性
        A();  // 调用它
    }
    var bObj = new B();
    console.log(bObj.a); // A
    console.log(bObj.b); // B            

  类式继承就这么简单,调用了下就表示继承了另一个类,但并不常用。它的好处是可以在子类实例化时直接给父类的构造函数传参

    function A(platform){
        this.platform = platform;
    }
    function B(name, platform){
        this.name = name;
        A.call(this, platform); // 传参给父类
    }
    var bObj = new B("2048", "ios");

  第二种是原型链继承,首先要大概知道原型(prototype)是什么。在js中每一个函数都关联着一个prototype属性,这是语言本身的机制,这个prototype会指向一个对象,这个对象存放着一些方法和属性,如果B构造函数的prototype属性被赋值为A构造函数创建的实例,就完成了一次原型式继承:B的实例继承自A,类似下面child继承Parent(例1

    function Parent(name){
        this.name = name;
    }
    function Child(age){
        this.age = age;
    }
    Child.prototype = new Parent("Li");
    var child = new Child(25);
    console.log(child.name); // Li
    console.log(child.age);  // 25

  Child.prototype = new Parent("Li")就表示Child的实例化对象继承了Parent这个类,prototype这种继承形式被很多人诟病,但它确实有强大的一面。例如我们都知道对象类型的变量是引用,引用是共享地址的,一个的属性值改变了,另一个的值跟着变,如下

    function Game(){
        this.name = "2048";
    }
    var o1 = new Game();
    console.log(o1.name);  // 2048
    var o2 = o1;
    o2.name = "gta";
    console.log(o1.name);  // gta

  当从Game创建对象时,两个对象的方法是完全分开的,各有一份

    function Game(){
        this.name = "2048";
        this.m = function(name){
            this.name = name;
        }
    }
    var o1 = new Game();
    var o2 = new Game();
    console.log(o1.m == o2.m);  // false,两个对象的方法不相等
    o1.m("gta");
    o2.m("angry bird");
    console.log(o1.name);  // gta
    console.log(o2.name);  // angry bird,o1、o2各改各的值

  但在原型中定义的属性

    // 以上例为基础
    Game.prototype.f = function(){ this.name = "unknown"; };
    console.log(o1.f === o2.f);  // true,o1、o2的f方法相等

  即在prototype中定义的属性,是被所有实对象共享的,普通对象的属性,每实例化一次,它们的属性、方法就被复制一份存到各自的对象里面去,但prototype中定义的只有一份,因为一个构造函数只关联一个prototype,而所有的对象从这个构造函数实例化而来。故理论上所有函数均可作为构造函数并用作原型继承(当然还有一点限制),他们都有prototype属性。

  那么当具体访问对象的某一属性时,如何找到?比如o1.name,首先在o1对象中找,当它自己对象中没有该属性时,他会到它的构造函数的原型---Game.prototype所指向的对象中去找,而Game.prototype又是由另一个构造函数设为A,实例化得到的对象,这个对象对应的构造函数A也有自己的prototype---A.prototype,当在Game.prototype中没找到时,又会去A.prototype对中去找,直到最终的老祖宗Object的prototype---它是undefined,此时如还未找到就会报错,找到了的话在哪个里面就用哪个。这种一级一级的、回溯的prototype形成的链式结构被称为原型链,这种方式也被称为原型链继承。

    function Animal(){
        this.name = ‘animal‘;
    }
    function Dog(){
        this.type = ‘dog‘;
    }
    Dog.prototype = new Animal();  // 原型继承
    function Husky(){
        this.weight = 2.3;
    }
    Husky.prototype = new Dog();

    var o = new Husky();
    // 顺着原型链查照
    console.log(o.name);
    console.log(o.type);
    console.log(o.weight);

  原型的第一大优点就是属性(方法)只保留一份,各对象共享,节省空间,还有个优点是可以动态的添加属性。上例中,o1、o2已经实例化了,在函数对象Game的prototype属性上添加其他值时,对象依然可以访问到,原因就是先找自己对象中的属性,再顺着原型链找,原型中有那就用它了。

  只有函数才有prototype属性且可以访问到,其他对象实例是没有的,但其他对象实例有一个__proto__,据说它是一个内在的指针,指向对象所继承的类,很多浏览器都视其为私有属性而不可访问,貌似火狐将其暴露了出来。在chrome的控制台中可以大致可以看到这种风结构

       

  对象的__proto__指向Object,数组的__proto__指向Array,正因为如此,我们才能调用到一些对象的方法,比如var a = [1,2,3]; a.push(4);,因为push已经被封装在Array里面了,它们在调用属性、方法时也会顺着__proto__这个链去找。

  原型或者说原型对象,是类的唯一标识,也就是说当两个对象继承自同一个原型对象时,它们才属于同一个类的实例。当两个构造函数的prototype是指向同一个对象时,由它们创建的实例才是同类的,如何判断对象是不是属于某个类?可用instanceof运算符,如

o instanceof Husky

如果o继承自Husky.prototype(注意不是Husky),则返回true。既然这样为何不直接写 o instanceof Husky.prototype ?结果报错了:instanceof右边必须是个函数。另一种方法是isPrototypeOf,如

  Husky.prototype.isPrototypeOf(o)

  若o为Husky的实例化对象则返回true,Husky.prototype是o的原型。

  如果观察了chrome的控制台打印对象的话,特别是通过new表达式生成的对象,就会发现在对象的原型__proto__所指的对象中有一个constructor属性,这个属性的值就是它自己的构造函数(对象)

    

  对于一个完整的原型对象来说,它的constructor属性是它本身的构造函数对象。所以更好的处理是这样的

    function Animal(){
        this.name = "animal";
    }
    function Dog(){
        this.type = "dog";
    }
    Dog.prototype = new Animal();
    Dog.prototype.constructor = Dog;  // 将原型constructor属性指向自己的构造函数 

  这里有几个容易让人糊涂的东西:构造函数对象、实例化对象、原型对象。梳理一下:

  1. js中除了null和undefined外都是对象,函数当然也是对象,定义了一个函数function fun(){}(不管fun是否作为构造函数使用),fun就是个函数对象;
  2. 实例化对象,通过new结合构造函数创建的对象,前面说过,类似function Game(){ this.name=”2048”; } var g = new Game(); ,当调用new的时候实例化对象已经生成了,然后再执行Game函数体代码,函数的上下文变成了这个对象,this是对这个对象的引用,this.name只不过是给这个对象添加属性,即构造函数内的代码只是对已生成的对象进行一下组装而已;
  3. 原型prototype是构造函数对象的一个属性而已,只不过它恰好也是一个对象,通过原型链继承的实例化对象,继承自这个原型所指向的对象。这个prototype对象自己也有一些属性,比如constructor,它的值又是自己的这个函数对象。如下图

     

  稍微标准点的原型式继承不是直接B.prototype = new A(),而是封装到函数里面,如下

    function inherit(obj){                // 返回继承自对象obj的新实例
        if(obj === null) return null;
        if(Object.create)    // 使用ECMAScript5函数
            return Object.create(obj);
        var t = typeof obj;
        if(t != "object" && t != "function") return null;
        function f(){}  // 定义一个空函数
        f.prototype = obj;
        return new f();  // 返回继承原型的实例对象
    }
     var obj = { x:5, m:function(){console.log(this.x);} };
     var subObject = inherit(obj); // 子对象

  这里用到了Object.create()函数,它的第一个参数是对象,如果只传第一个参数如obj,它将返回指定原型为obj对象的新对象。如果Object.create不存在,创建一个空函数,让函数的prototype指向obj,再返回以这个空函数的实例对象,也完成了原型式继承(有的把这个称作寄生式继承)。再看下例

    function A(){ this.name = "A"; }
    function B(){ this.type = "B";   }
    B.prototype = inherit(A.prototype);
    B.prototype.constructor = B;  // 将constructor指向自己的构造函数
    var o = new B();

  o是B的实例,B是A的子类,则需保证B的原型对象继承自A的原型对象,如果不这样,B的原型只继承自Object.prototype,那么o跟一个普通实例没有差别。OK,这种写法,o里面有没有属性name?事实是没有。前面在说类式继承时,父类构造函数在子类构造函数里面调用一下,其实相当于是给子类实例添加了属性,所以子类实例才拥有这个属性,这里B的prototype只继承自A的prototype,也不是前面的原型继承时写的B.prototype = new A()(这样写o.name是存在的),都是原型继承,两种不同写法造成了这一差别。所以原型继承的核心还是在于:o的构造函数对象B的prototype原型到底包没包含所需的属性。

  针对以上类式继承和原型式继承各自的优缺点,又来了个新方式:组合继承。组合当然是兼具类式继承和原型式继承的优点了。

    function Parent(){
        this.name = "parent";
    }
    function Child(){
        this.type = "child";
    }
    // 先处理原型链
    Child.prototype = new Parent();
    Child.prototype.constructor = Child;
    // 然后添加需继承的属性和方法
    Child.prototype.p = "2333";
    Child.prototype.f = function(){ console.log("hello"); };

    var o = new Child();

以上只是零散的组合先设置好原型链,然后手动给原型添加属性和方法,代替类式继承的直接传参,以此克服弊端。如果写的标准点,首先准备个给原型添加属性的小函数:

    // 将对象q的属性添加到p中
    extend(p, q){
        if(p == null || q == null) return;
        if(typeof p != "object" || typeof q != "object") return;
        for(var prop in q)
            p[prop] = q[prop];
    }

  另一个工具工具函数

   /*
    * 实现子类的继承
    * @param superClass 父类构造函数对象
    * @param subClass 子类构造函数对象
    * @param methods 包含函数属性的对象
    * @param props 包含属性的对象
    * return function  组合继承后的构造函数
    */
    function inheritClass(superClass, subClass, methods, props){
      // 使子类在原型上继承自父类
      subClass.prototype = inherit(superClass.prototype);
      subClass.prototype.constructor = subClass;
      // 将所需方法添加至子类原型中
      if(typeof methods == "object") extend(subClass.prototype, methods);
      // 将所需属性添加至子类原型中
      if(typeof props == "object") extend(subClass,.prototype, props);
      // 返回子类
      return subClass;
   }

  superClass是父类构造函数,subClass是子类构造函数,methods与props为将要添加的方法和属性的集合,以对象的方式包装起来。借用了inherit和extend方法,inherit方法设置好原型链,为了克服无法直接传参的弊端,将方法和属性包装好后,动态的添加到子类构造函数的prototype中。

  如inheritClass方法添加方法和属性时,这样写extend(subClass, methods),将给subClass这个函数对象添加的是私有属性,对象打点无法访问,只有构造函数打点可以访问,在其他语言中,静态变量或常量类似这种形式,所以这些属性在js中称为类的私有属性。

  测试代码打印下看效果,依然在chrome控制台

    function Parent(){
        this.p = "parent";
    }
    function Child(){
        this.c = "child";
    }
    // 添加的方法对象
    var mObj = { sayHello:function(){ console.log("hello") } };
    // 添加的属性对象
    var pObj = { pos:"developer" };
    // 实现继承
    var InheritedChild = inheritClass(Parent, Child, mObj, pObj);

    var obj = new InheritedChild();
    console.log(obj);

    

  console.table打印一个树形结构,obj是对象,所以是__proto__,它关联给自己实例化的构造函数的原型对象,表现上就是__proto__: Child,这个原型中有constructor属性,值为Child函数对象,还有添加进去的pos属性和sayHello方法,这个原型又关联这个Parent的prototype原型对象:__proto__:Parent,同理这个原型有自己的constructor属性,并最终继承自Object。

  继承关系上大致就是这样,乱七八糟一堆,也许一个$.extend()方法就解决了~

时间: 2024-10-10 13:41:01

js继承关系的相关文章

js继承的常用方式

写在前面的话:这篇博客不适合对面向对象一无所知的人,如果你连_proto_.prototype...都不是很了解的话,建议还是先去了解一下JavaScript面向对象的基础知识,毕竟胖子不是一口吃成的. 我们都知道面向对象语言的三大特征:继承.封装.多态,但JavaScript不是真正的面向对象,它只是基于面向对象,所以会有自己独特的地方.这里就说说JavaScript的继承是如何实现的. 学习过Java和c++的都知道,它们的继承通过类实现,但JavaScript没有类这个概念,那它通过什么机

[技术学习]js继承

今天又看了一遍js的面向对象方面的知识,重点看了继承相关内容,已经记不得看了第几次这个内容,终于觉得自己好像懂了,特记录下来过程. js面向对象继承分为两大类,主要分为对象继承和非对象继承(拷贝继承),这次主要谈对象继承.对象继承主要有两种:原型继承和对象冒充继承. 一.原型继承,将子类的原型引用父类的实例,从而达到将子类的原型与父类的原型和父类的构造函数关联起来的目的. function Person(name){ this.name=name; } Person.prototype.sayN

JS继承的实现方式

前言 JS作为面向对象的弱类型语言,继承也是其非常强大的特性之一.那么如何在JS中实现继承呢?让我们拭目以待. JS继承的实现方式 既然要实现继承,那么首先我们得有一个父类,代码如下: // 定义一个动物类 function Animal (name) { // 属性 this.name = name || 'Animal'; // 实例方法 this.sleep = function(){ console.log(this.name + '正在睡觉!'); } } // 原型方法 Animal

JS继承的几种方式

JS作为面向对象的弱类型语言,继承也是其非常强大的特性之一. 既然要实现继承,那么我们先定义一个父类: // 定义一个动物类 function Animal (name) { // 属性 this.name = name || 'Animal'; // 实例方法 this.sleep = function(){ alert(this.name + '正在睡觉!'); } } // 原型方法 Animal.prototype.eat = function(food) { alert(this.na

Q_OBJECT信号槽与继承关系fZ1

一.问题说明对象子类化过程中,或者新添加的类对象,当你需要使用信号槽机制的时候,那么,你就必须加上宏变量:问题来了,你的这个类就必须继承与类,同时集成于你的积累,如或者.那么,又有新的问题,继承关系必须拟清楚,集成于,继承于.例子:,二.问题分析解决上面就显得多余,而且继承关系错乱.三.问题深度剖析.纵使需要当前类继承类,但是类是继承的,继承,所以这里只需要继承就可以了,简单易读..同时,使用,你还必须重新,并且..当你的父类(自己实现的父类),有一个继承的子类,父类又有自己写的虚拟函数,那么子

JS继承的一些见解

JS继承的一些见解 js在es6之前的继承是五花八门的.而且要在项目中灵活运用面向对象写法也是有点别扭,更多的时候还是觉得面向过程的写法更为简单,效率也高.久而久之对js的继承每隔一段时间就会理解出现困难.所以这次我要把对对象的理解写下来,这样应该就深刻一点了. 我们先来看看一个对象是怎么生成的 // 三种创建对象的方法 var obj = {} var obj2 = new Object() var obj3 = Object.create(null) // 创建一个空字符串对象 var ob

QObject提供了QMetaObject元类信息(相当于RTTI和反射),信号与连接,父子关系,调试信息,属性,事件,继承关系,窗口类型,线程属性,时间器,对象名称,国际化

元类信息(相当于RTTI和反射),信号与连接,父子关系,调试信息,属性,事件,继承关系,窗口类型,线程属性,时间器,对象名称,国际化其中元类又提供了:classInfo,className,构造函数,多重祖先元类,method, property, Enumerator, Signal, Slot等等 http://doc.qt.io/qt-5/qobject.html http://doc.qt.io/qt-5/qmetaobject.html 我感觉Qt的出现,除了提供GUI以外,主要就是提

EF-CodeFirst 继承关系TPH、TPT、TPC

继承关系 面向对象的三大特征之一:继承 ,在开发中起到了重要的作用.我们的实体本身也是类,继承自然是没有问题.下面开始分析 EF里的继承映射关系TPH.TPT.TPC 现在我们有这样一个需求,用户里要有一批超级用户,他们有着与生具来的优越.可以体验到更高级的服务.但是超级用户也是用户,可以去继承我们的普通用户类 (其实个人感觉不是很合理,因为我们有UserRole表,给一个超级用户的角色就可以了.这里仅做演示) /// <summary> /// 超级用户 /// </summary&g

Hibernate之jpa实体映射的三种继承关系

在JPA中,实体继承关系的映射策略共有三种:单表继承策略(table per class).Joined策略(table per subclass)和Table_PER_Class策略. 1.单表继承策略 单表继承策略,父类实体和子类实体共用一张数据库表,在表中通过一列辨别字段来区别不同类别的实体.具体做法如下: a.在父类实体的@Entity注解下添加如下的注解: @Inheritance(Strategy=InheritanceType.SINGLE_TABLE)@Discriminator