JS继承

  谈到面向对象编程就避不开继承这个概念,JS的继承主要依赖原型链来实现的,今天主要总结一下在JS中的多种继承方式。主要内容如下:

  1. 什么叫原型链
  2. 原型链继承
  3. 经典继承
  4. 组合继承
  5. 原型式继承
  6. 寄生组合式继承
  7. 简析JQuery的Extend函数
  8. 简析ExtJs的Extend函数

1.什么是原型链

  原型与实例的链状结构叫做原型链。

  构造函数prototype指向一个对象的实例,利用这个构造函数创建实例的时候,实例对象就会拥有原型的属性和方法。如果这个构造函数的原型是另一个构造函数的实例,而另一个构造函数又是别的构造函数的实例,如此层层递进的关系构成实例与原型的链条,我们把这种链状结构叫做原型链。

  JS的继承主要通过原型链来实现的

2.原型链继承

  原型链继承的结构如下:

 1 function Sup() {
 2     this.name = "sub...";
 3     this.getName = function() {
 4         return this.name;
 5     };
 6 }
 7         
 8 function Sub() {}
 9 Sub.prototype = new Sup();
10         
11 var obj = new Sub();
12 alert(obj.getName());//sub...

  定义了一个超类Sup和一个子类Sub,子类的原型指向超类的实例,所以在通过子类构造的实例也拥有超类的name以及getName方法。当然,如果子类也定义了自己的name属性,调用getName函数返回的将是子类的name。

  构造函数构造函数的时候会自动添加一个构造器(constructor)指向当前的构造函数。所有以构造函数形式产生的原型实例,最后都会指向Objec对象,这也是它们拥有Objec定义的函数的原因。

3.经典继承

  经典继承又叫借用构造函数,伪造对象等等。主要运用apply与call函数。

  apply与call除了参数有区别外,功能是一样的,都是改变调用者的上下文环境。这里只展示借用构造函数的示例,不详细探讨两个函数的具体细节。

  经典继承的结构如下:

 1 function Sup() {
 2     this.name = "sup...";
 3     this.getName = function() {
 4         return this.name;
 5     };
 6 }
 7 
 8 function Sub() {
 9     Sup.apply(this, arguments);
10 }
11 
12 var obj = new Sub();
13 alert(obj.getName());//sup...

  定义一个超类Sup和一个子类Sub,其中子类的代码是超类调用apply函数。Sup.apply(this, arguments)表示是在this的环境下执行Sup这个构造函数,效果等同于Sub也拥有了name与getName两个属性。

  需要强调的是它不会继承超类原型中的属性。即在Sup函数下面加一段Sup.prototype.getAge = function() {//...},Sub实例里是访问不到getAge的。

4.组合继承

  组合继承是引用最广泛的继承方式,它弥补了原型链继承与经典继承的缺点,达到取长补短。

  原型链继承方式主要优点是共享,但这也恰恰成为了它的缺点。我们知道,在JS中对象的求职策略是按引用传递的,这意味着超类定义的对象将会被共享,任何一个用子类构造函数构造的实例修改了该对象,都会反映到其它子类实例中去(当然,超类中的非对象属性没有这种弊端)。

 1 function Sup() {
 2     this.arr = [1, 2, 3];
 3     this.getArr = function() {
 4         return this.arr.toString();
 5     };
 6 }
 7 
 8 function Sub() {}
 9 Sub.prototype = new Sup();
10 
11 var obj1 = new Sub();
12 var obj2 = new Sub();
13 obj1.arr.push(4);
14 alert(obj2.getArr());//1,2,3,4

  经典继承不会有原型链的那种弊端,但却无法做倒我们原型设计的初衷——共享。在子类调用构造函数效果等同于在子类中把超类代码重新拷贝一遍,事实上,子类继承的超类函数是完全一样的。

  我们希望需要被共享的属性被共享,不需要被共享的属性不被共享,于是产生了原型链与借用构造函数的组合继承方式。

  组合继承方式如下:

function Sup() {
    this.arr = [1, 2, 3];//继承后不需要共享的属性
}
Sup.prototype.getArr = function() {//借用构造函数不会继承超类原型里的东西
    return this.arr.toString();
};

function Sub() {
    Sup.apply(this);
}
Sub.prototype = new Sup();

var obj1 = new Sub();
var obj2 = new Sub();
obj1.arr.push(4);
alert(obj1.getArr());//1,2,3,4
alert(obj2.getArr());//1,2,3

  组合继承足以应对各种继承场景了,但是道格拉斯.克罗克福德提出了一种基于已有的类型创建对象的继承方式——原型式继承。

5.原型式继承

  原型式继承在ECMAJavascript 5中被定义到Object对象中的create中。

  原型式继承实现思路是定义一个创建函数,将原型对象作为参数,该函数内部封装了继承细节。

  其实现大致如下:

 1 function create(proto) {
 2     function F() {}
 3     F.prototype = proto;
 4     return new F();
 5 }
 6 
 7 var Book = {publish : "xx出版社"};
 8 
 9 var book = create(Book);
10 alert(book.publish);//xx出版社

  EcmaJavascript5中只需把上面的create函数换成Object.create()即可,这个函数的第一个参数是原型对象,第二个参数是额外的属性对象用以控制属性对应内置属性。这里不做细节描述。

6.寄生组合式继承

  寄生组合式继承是对组合式继承的优化,因为组合式继承会执行两次构造函数(创建子类原型时、借用构造函数时)。

  寄生组合式继承把继承原型的步骤放在原型式继承中进行。

  其代码结构如下:

function inheritprototype(sub, sup) {
    var prototype = Object.create(sup.prototype);
    prototype.constructor = sub;//构造器
    sub.prototype = prototype;
}

function SupObject() {
    this.name = arguments[0] || "超类";
}
SupObject.prototype.getName = function() {
    return this.name;
}

function SubObject() {
    SupObject.apply(this, arguments);
}
inheritprototype(SubObject, SupObject);

var s = new SubObject("子类");
alert(s.getName());//子类

7.JQuery中的Extend函数

  JQuery的继承有个特点:它有个可选参数(deep),用来标识执行浅复制或者深复制。

  用法:JQuery.extend([是否深度复制], 返回的目标对象, [扩展的对象...]);

  深复制与浅复制的区别在于前者会遍历同名属性的值,并区别复制。

  浅复制:扩展对象里的属性与目标对象的属性同名,则直接把拓展对象的属性值赋值给目标对象对应的属性。比如说目标对象是{a:{b:'b', c:'c'}},拓展对象是{a:{d:'d'}},执行浅复制则返回{a:{d:'d'}}。

  深复制:扩展对象里的属性与目标对象的属性同名,则遍历扩展对象(递归的过程),执行属性的复制。比如上面那个例子返回的结果是{a:{b:'b', c:'c',d:'d'}}。

//浅复制
function copy() {
    var target = arguments[0],
        config = arguments[1];
    for(var prop in config) {
        target[prop] = config[prop];
    }
    return target;
}

var target = {
    a : {
        b : 'b', 
        c : 'c'
    }
}
var config = {
    a : {
        d : 'd'
    }
}

var result = copy(target, config);//{a:{d:'d'}}
//深复制
function copy() {
    var target = arguments[0],
        config = arguments[1];
    for(var prop in config) {
        if(typeof target[prop] === 'object') {
            var temp;
            if(Object.prototype.toString.call(target[prop]) === '[object Array]') {
                temp = target[prop] || [];
            }else {
                temp = target[prop] || {};
            }
            target[prop] = copy(temp, config[prop]);
        }else {
            target[prop] = config[prop];
        }
    }
    return target
}

var target = {
    a : {
        b : 'b', 
        c : 'c'
    }
}
var config = {
    a : {
        d : 'd'
    }
}

var result = copy(target, config);//{a:{b:'b', c:'c',d:'d'}}

  在深复制代码里首先用typeof操作符把基本类型(string、boolean、number) 与引用类型区分开来,基本类型拷贝是覆盖(与浅复制相同),需要注意的是引用类型中的数组要给它声明一个空数组。

  在JQuery源码中代码复杂些,但功能也更强大。它增加了防循环引用机制、深复制时拒绝window、DOM、原型继承而来的复杂对象。

8.ExtJs中的Extend

  ExtJs中的apply函数其实就是一种浅复制,而它的applyIf是一种忽略目标对象已有的属性。

  ExtJs的extend函数功能比较复杂,大致做了如下几种工作:

  • 确认子类型
  • 原型链继承
  • 给子类赋予覆盖函数(override)
  • 扩展对象对子类进行覆盖操作
  • 重写子类的继承函数

  1)确认子类型:Ext.extend函数有两种传参方式,三个参数的时候,从左到右传递的分别是子类、超类、扩展对象;两个参数的时候分别是超类与扩展对象。两种传参方式让人用起来很方便其内部实现大致如下:

function confirSub(sub, sup, config) {
    if(!!sup && Object.prototype.toString.call(sup) === '[object Object]') {
        config = sup;
        sup = sub;
        if(sp.constructor != Object.prototype.constructor) {
            sub = sup.constructor;
        }else {
            sup.apply(this, arguments);
        }
    }
}

  上面代码主要是处理两个参数情况,当第二个参数是对象字面量(扩展对象)时,把它当只传递两条参数处理。然后扩展对象、超类、子类进行重新定位,以便后文与传三个参数的情况统一。其中超类如果是新建对象,则子类用经典继承方式确认,如果超类是通过某些继承机制产生的对象,则将超类的构造器赋给子类。

  2)原型链继承:这个继承代码与寄生组合式继承原型中函数的代码很像。代码大致如下:

function extendProto(sub, sup, config) {
    var F = function() {},
        sbp,//用来指代子类的原型
        spp = sup.prototype;//指代超类的原型
    F.prototyoe = spp;
    sbp = sub.prototype = new F();
    sbp.constructor = sub;
    //在控件子类继承中,扩展对象里的构造函数常调用它,
    //调用方式是:新类型.supperclass.constructor.call(this,arguments)
    sub.supperclass = spp;
}

  3)子类新增覆盖函数(override),覆盖函数与JQuery浅复制类似,这里就不赘言了。

  4)扩展对象对子类进行浅复制:源码里就一行语句Ext.override(sb, overrides);

  5)重写子类extend函数:源码里也是一行解决sb.extend = function(o) {return Ext.extend(sb, o);};其作用是子类的子类只能把当前子类作为超类。

 

原文地址:https://www.cnblogs.com/longhx/p/5404692.html