js继承

原文链接

js的继承有6种方式,大致总结一下它们各自的优缺点,以及它们之间的关系。


1.原型链

js的继承机制不同于传统的面向对象语言,采用原型链实现继承,基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。理解原型链必须先理解原型,以下是对于原型的一些解释:

无论什么时候,只要创建了一个新函数,就会根据一组特定规则为该函数创建一个prototype属性。这个属性指向函数的原型对象,所有原型对象都会自动获得一个constructor属性,这个属性是一个指向prototype属性所在函数的指针。创建自定义的构造函数之后,其原型对象只会取得constructor属性,其他方法都是从Object继承来的。当调用构造函数创建一个新实例之后,该实例的内部包含一个指针,指向构造函数的原型对象,即[[Prototype]],在浏览器中为_proto_

也就是说,构造函数和实例实际上都是存在一个指向原型的指针,构造函数指向原型的指针为其prototype属性。实例也包含一个不可访问的指针[[Prototype]](实际在浏览器中可以用_proto_访问),而原型链的形成真正依赖的是_proto_而非[[Prototype]]

举个例子

下边是一个最简单的继承方式的例子:用父类实例充当子类原型对象。

function SuperType(){                        
    this.property = true;
    this.arr = [1];
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}
SubType.prototype = new SuperType();              
//在此继承,SubType的prototype为SuperType的一个实例
SubType.prototype.getSubValue = function(){
    return this.subproperty;
};
var instance = new SubType();
var instance2 = new SubType();                                   
c(instance.getSuperValue());                      //true
c(instance.getSubValue());                        //false
c(instance.__proto__.prototype);                  //undefined
//SubType继承了SuperType,SuperType继承了Object。
//instance的_proto_是SubType的原型对象,即SubType.prototype。
//而SubType.prototype又是SuperType的一个实例。
//则instance._proto_.prototype为undefined,
//因为SuperType的实例对象不包含prototype属性。
instance.arr.push(2);
c(instance.arr);                                  //[1,2]
c(instance2.arr);                                 //[1,2]
//子类们共享引用属性

需要注意的一点:无论以什么方式继承,请谨慎使用将对象字面量赋值给原型的方法,这样会重写原型链。

优缺点

原型链继承方式的优点在于简单,而缺点也十分致命:

  1. 子类之间会共享引用类型属性
  2. 创建子类时,无法向父类构造函数传参

2.借用构造函数

又叫经典继承,借用构造函数继承的主要思想:在子类型构造函数的内部调用超类型构造函数,即用call()apply()方法给子类中的this执行父类的构造函数,使其拥有父类拥有的属性实现继承,这种继承方法完全没有用到原型。下边是借用构造函数的实现:

function SuperType(){                                 
    this.colors = ["red","blue","green"];
}
function SubType(){
    SuperType.call(this);     //借用构造函数
}
var instance1 = new SubType();
instance1.colors.push("black");
c(instance1.colors);          //["red","blue","green","black"]
var instance2 = new SubType();
c(instance2.colors);          //["red","blue","green"]

举个例子

借用构造函数,相当于将父类拥有的属性在子类的构造函数也写了一遍,使子类拥有父类拥有的属性,这种方法在创建子类实例时,可以向父类构造函数传递参数 。

function SuperType(name){           
    this.name = name;
}
function SubType(name){
    SuperType.call(this,name);          //借用构造函数模式传递参数
    this.age = 29;
}
var instance = new SubType("something");
c(instance.name);                       //something
c(instance.age);                        //29

优缺点

借用构造函数模式,不同于原型式继承和原型模式,它不会共享引用类型属性,而且也可以向超类型构造函数传递参数。但是相对的,由于不会共享属性,也无法实现代码复用,相同的函数在每个实例中都有一份。为了实现代码复用,提示效率,大神们又想出了下边的继承方法。


3.组合继承

组合继承有时也叫伪经典继承,是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。

即用原型链实现对原型属性和方法的继承(需要共享的),通过借用构造函数实现对实例属性的继承(不共享的)。这样的方法实现了函数复用,而且每个实例拥有自己的属性。

举个例子

function SuperType(name) {                       //父类的实例属性
    this.name = name;
    this.colors = ["red", "blue", "green"];      
}
SuperType.prototype.sayName = function() {       //父类的原型属性
    c(this.name);
};

function SubType(name, age) {                    //借用构造函数继承实例属性
    SuperType.call(this, name);
    this.age = age;
}
SubType.prototype = new SuperType();             //原型链继承原型属性
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    c(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
c(instance1.colors);        //"red,blue,green,black"
delete instance1.colors;    
//删除从实例属性继承来的colors,读取colors会成为从原型继承来的实例属性
c(instance1.colors);        //"red,blue,green"
instance1.sayName();        //Nicholas
instance1.sayAge();         //29
var instance2 = new SubType("Greg", 27);
c(instance2.colors);        //"red,blue,green"
instance2.sayName();        //Greg
instance2.sayAge();         //27

优缺点

这是所有继承方式中最常用的,它的优点也十分明显:

  1. 可以在创建子类实例时向父类构造函数传参。
  2. 引用类型属性的值可以不共享。
  3. 可以实现代码复用,即可以共享相同的方法。

但是这种方法依然有一点不足,调用了两次父类的构造函数,最后会讲到一种理论上接近完美的继承方式,即寄生组合式继承。


4.原型式继承

原型式继承借助原型基于已有对象创建新对象,需要一个对象作为另一个对象的基础:

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

上边段代码就是原型式继承的核心代码,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。

举个例子

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);      //在此继承
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
c(person.friends);              //["Shelby","Court","Van","Rob","Barbie"]
c(person.name);                 //Nicholas
c(anotherPerson.name);          //Greg
c(yetAnotherPerson.name);       //Linda
delete yetAnotherPerson.name;   
//删除子类的属性,就会解除对父类属性的屏蔽,暴露出父类的name属性
c(yetAnotherPerson.name);       //Nicholas

从上边的代码显示,由object(注意首字母小写,不是对象的构造函数)产生的两个子类会共享父类的引用属性,其中friends数组是共享的,anotherPerson和yetAnotherPerson都是继承自person。实际上相当于创建了两个person对象的副本,但可以在产生之后拥有各自的实例属性。

ECMAScript5新增了Object.create()方法规范化了原型式继承,这个方法接受两个参数:

  1. 一个作为新对象原型的对象(可以是对象或者null)
  2. 另一个为新对象定义额外属性的对象(可选,这个参数的格式和Object.defineProperties()方法的第二个参数格式相同,每个属性都是通过自己的描述符定义的)

下边是一个例子:

var person = {                      //原型式继承规范化为create()函数
    name: "Nicholas",
    friends: ["Shelby","Court","Van"]
};
var anotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});
c(anotherPerson.name);             //"Greg"

优缺点

如果想让一个对象与另一个对象保持类似,原型式继承是很贴切的,但是与原型模式一样,包含引用类型的值得属性会共享相应的值。


5.寄生式继承

寄生式继承与原型式继承紧密相关的一种思路,与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,函数内部以某种方式来增强对象,最后再像真的做了所有工作一样返回对象。

举个例子

function createAnother(original) { 
    var clone = object(original);          //此处用到了原型式继承
    clone.sayHi = function() {
        c("Hi");
    };
    return clone;
}
var person = {                             //父类实例
    name: "Nicholas",
    friends: ["Shelby","Court","Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi();

上边的寄生式继承用到了原型式继承,向实现继承的函数传入一个父类对象实例,再用原型式继承得到一个父类对象实例的副本,再给这个副本添加属性,即增强这个对象,最后返回这个副本对象。由于用到了原型式继承,这个对象的原型指向传入的父类对象实例。上边例子用到的object()函数(原型式继承)并不是必须的,任何能够返回新对象的函数都适用于寄生式继承模式

优缺点

寄生式继承在主要考虑对象而不是创建自定义类型和构造函数时,是十分有用的。但是如果考虑到用寄生式继承为对象添加函数等,由于没有用到原型,做不到函数复用,会导致效率降低。


6.寄生组合式继承

这个名字并不是很贴切,虽然叫寄生组合式继承,但是和寄生式继承关系不是很大,主要是用原型式继承来实现原型属性的继承,用借用构造函数模式继承实例属性。寄生组合式继承和组合继承的区别在于:

  1. 在继承原型属性时,组合继承用原型链继承了整个父类(通过将父类实例赋值给子类构造函数的原型对象来实现),这使子类中多了一份父类的实例属性。而寄生组合式继承用原型式继承只继承了父类的原型属性(把父类构造函数的原型对象用原型式继承复制给子类的构造函数的原型对象)。
  2. 组合继承调用了两次超类型构造函数,寄生组合式继承调用了一次。

举个例子

function inheritPrototype(subType, superType) {             //寄生式继承
    var prototype = Object.create(superType.prototype);     //创建对象
    prototype.constructor = subType;                        //增强对象
    subType.prototype = prototype;                          //指定对象
}
function SuperType(name) {
    this.name = name;
    this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
    c(this.name);
};
function SubType(name, age) {
    SuperType.call(this, name);                 //借用构造函数
    this.age = age;                             //添加子类独有的属性
}
inheritPrototype(SubType, SuperType);           //此处调用实现寄生组合继承的函数
SubType.prototype.sayAge = function() {         //添加子类独有原型属性
    c(this.age);
};
var son = new SubType("erzi",16);
var father = new SuperType("baba");
c(typeof father.sayName);                       //function
c(typeof father.sayAge);                        //SubType独有的方法,返回undefined
SubType.prototype.sayName = function() {        
    c("This function has be changed");          
}   
//更改子类的方法只会影响子类,prototype是对象,添加新属性和更改属性不会影响父类的prototype
father.sayName();                               //baba
son.sayName();                                  //This function has be changed
SuperType.prototype.sayName = function() {      //更改父类的原型属性
    c("This function has be changed");
}
father.sayName();                               //This function has be changed
son.sayName();                                  //This function has be changed

优缺点

这种继承方式理论上是完美的,但是由于出现的较晚,人们大多数使用的是组合继承模式。


以上就是我对于js继承的一些理解,如果你有不一样的想法欢迎讨论。

参考资料:《JavaScript高级程序设计》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容

  • 博客内容:什么是面向对象为什么要面向对象面向对象编程的特性和原则理解对象属性创建对象继承 什么是面向对象 面向对象...
    _Dot912阅读 1,401评论 3 12
  • 我们都曾面临着选择,甚至在这个时间,这个我正提笔写着什么的时候,就有成千上万的人要去选择。“选择”会让我们走上不同...
    you美动听话语录阅读 126评论 0 0
  • 雨 很大 被雷惊醒 或是未曾睡着 天灾人祸 老天爷只是把该收的人收走 谁也不知道谁是该走的人 只是累的想走的无可奈...
    三月品阅读 215评论 0 0