构造函数原型的继承方式分析

1.通过原型链继承

function One(age) {
    this.nameArr = [ 'Tom', 'Cat' ];
    this.age = age || 20;
}
One.prototype.say = function() {
    console.log(this.nameArr);
    console.log(this.age);
};

const one = new One(25);
console.log(one); //{nameArr:[ 'Tom', 'Cat' ],age:25}

//现在创建另一个构造函数Two
function Two() {}
//使用One构造函数的实例来作为构造函数Two的原型对象
Two.prototype = new One();
//为构造函数Two添加方法 eat
Two.prototype.eat = function() {
    console.log('eat');
};
//基于Two来创建构造函数Two的实例
const two = new Two(30);
console.log(two); //{} 空对象 因为构造和函数Two函数体没有内容
console.log(two.nameArr, two.age); //[ 'Tom', 'Cat' ] 20
/*虽然 实例two是一个空对象 但是它访问属性的时候会沿着原型链向它的原型对象去找,因为
Two.prototype = new One();所以可以访问到 nameArr 和 age的属性, 因为在指定Two的原型时
调用的 new Two() 没有传参 所以age的属性值是默认的 20,创建two时 传入的参数 30无用
*/

//基于Two创建第二个实例 two_2
const two_2 = new Two();
console.log(two_2); // {}
console.log(two_2.nameArr, two_2.age); // [ 'Tom', 'Cat' ] 20

//现在我们修改Two创建的实例 two的引用类型类型属性 nameArr 以及非引用类型属性 age
two.nameArr.push('kate');
two.age = 100;
console.log(two.nameArr, two.age); //[ 'Tom', 'Cat','kate' ] 100

//现在 我们再来看实例 two_2
console.log(two_2.nameArr, two_2.age); //[ 'Tom', 'Cat','kate' ] 20
console.log(two.say(), two_2.say()); //[ 'Tom', 'Cat','kate' ] 100  [ 'Tom', 'Cat','kate' ] 20
console.log(two.eat(), two_2.eat()); //eating eating
//重写构造函数 Two的say 方法
Two.prototype.say = function() {
    console.log('我是Two的say方法');
};
console.log(two.say(), two_2.say()); //我是Two的say方法 我是Two的say方法
console.log(Two.prototype.constructor === One); // true //可见 Two的 prototype上的constructor也被篡改了

综上我们可以总结出 通过原型链来实现继承的原理
通过原型链来实现继承的原理
原型链继承方案中,父类型创建的实例来作为另一个类型的原型对象,新类型实际上也就变成父类型的一个实例,
像这里的Two.prototype = new One(), 其原型上的引用类型值会被新类型创建的所有实例共享,当该类型创
建的一个实例修改了引用类型的属性值,那么该类型创建的其他实例的该引用类型的属性也会被修改,也就是多
个实例对引用类型的操作会被篡改.
缺点:
1.创建子类型实例时无法向父类型的构造函数传参。
2.子类型多个实例对引用类型的操作有被篡改的风险。
3.子类型的原型上的 constructor 属性被重写了
基于原型链继承的问题,原型链继承通常是不被推荐使用的继承方式

2.借用构造函数来实现的继承(经典继承)

基于原型链的继承存在的不能传参以及原型的引用类型数据在多个实例共享使用时会出现被篡改的问题,出现了借用构造函数来实现的继承方案。

//经典继承,也叫做借用构造函数继承

function One(age) {
    this.nameArr = [ 'tom', 'lili' ];
    this.age = age;
}
One.prototype.say = function() {
    console.log('hello world');
};
const one = new One(15);
console.log(one); // {nameArr:[ 'tom', 'lili' ],age:15}

//创建构造函数 Two 并借用构造函数One来实现Two继承One
function Two(age) {
    One.call(this, age);
}
//给Two添加方法
Two.prototype.sing = function() {
    console.log('sing');
};

//基于Two 创建实例 two1
const two1 = new Two(20);
two1.nameArr.push('kate');
console.log(two1); //{name:[ 'tom', 'lili','kate ],age:20}
//基于Two 创建实例 two2
const two2 = new Two(22);
console.log(two2); //{name:[ 'tom', 'lili' ],age:22}
console.log(two1.sing(), two2.sing()); //sing sing
//console.log(two1.say(), two2.say()); //报错 two1.say is not a function
//为Two定义say方法
Two.prototype.say = function() {
    console.log('hello');
};
console.log(two1.say(), two2.say()); //hello hello

综上,我们可以总结出经典继承的原理
当子类型需要继承父类型时,在子类型构造函数中通过关键字call来调用父类型,在此时机可以实现
传参。这样当我们使用子类型再去创建实例时,每次创建实例都会调用一次父类的方法,从而每个子
类创建的实例都有一份属于当前实例自己的数据,他们是当前父类中定义的属性和方法的副本。
优点:经典继承的解决了子类型创建实例时的传参问题,同时创建的实例都有一份自己的数据,不会存在
属性篡改的问题。
缺点:父类型的原型中定义的方法对于子类型以及子类创建的实例都是不可见的,因此子类型如果需要这些方
法的话,需要将方法都在构造函数中再定义一次(方法都在构造函数中定义,每次创建实例都会创建一遍方法)。
或者在子类型的prototype上再定义一次。
这种经典的继承方案可以使用,但并不是最好的继承方式。

3.js继承的组合继承

/*js继承的组合继承  经典继承和原型链继承的组合*/

//创建父类构造函数
function Person(age, name) {
    this.age = age;
    this.name = name;
    this.arr = [ 1, 2 ];
}
Person.prototype.say = function() {
    console.log(this.age, this.name, this.arr);
};
let p1 = new Person(18, 'tom');
console.log(p1); //{ age: 18, name: 'tom', arr: [ 1, 2 ] }

//创建子类构造函数 使用组合继承方式继承Person的属性和方法
function Man(age, name, sex) {
    Person.call(this, age, name); //借用构造函数实现实例对父类属性和方法的继承
    this.sex = sex;
}
//通过原型链继承父类的原型上定义的方法
Man.prototype = new Person();
console.log(Man.prototype.constructor === Person); //子类的constructor被篡改
//修正constructor
Man.prototype.constructor = Man;

let man1 = new Man(10, 'Tom', 'male');
console.log(man1); //{age:10,name:'Tom',arr:[1,2],sex:'male'}
man1.say(); //10,Tom [1,2]

可以看到,组合继承是原型链继承和构造函数继承的优化组合在一起的继承方案,
也是推荐使用的继承方案,其缺点在于,调用了两次父类构造函数,如果一定要挑
缺点的话,那就是会有效率问题

4.原型式继承

//原型继承

function createObj(obj) {
    function new_Obj() {}
    new_Obj.prototype = obj;
    return new new_Obj();
}

let one = {
    name: 'tom',
    hobby: [ 'sing', 'reading' ],
    say: function() {
        return 'hello';
    }
};

let two = createObj(one);
console.log(two); // {}
console.log(two.name, two.hobby, two.say()); //tom [ 'sing', 'reading' ] hello
two.hobby.push('running'); //修改引用类型属性
let three = createObj(one);
console.log(three.name, three.hobby, three.say()); //tom [ 'sing', 'reading','running' ] hello

综上可知,原型继承是通过在一个函数内创建一个构造函数,然后利用传入的对象修改构造函数的原型属性,最后返回
基于这个构造函数创建的实例,然后我们在调用这个函数的时候,就得到一个实例,本质是对传入对象的浅拷贝
缺点: 无法传参,存在多个实例对引用类型属性修改的篡改问题 不推荐

5.寄生继承

//寄生寄生
function createObj(obj) {
    let newObj = Object.create(obj);
    newObj.say = function() {
        return 'hello';
    };
    return newObj;
}

let one = {
    name: 'tom',
    age: 20,
    hobby: [ 'singing', 'swimming' ],
    sing: function() {
        return 'world';
    }
};
let two = createObj(one);
let three = createObj(one);
console.log(two); //{say:function}
two.hobby.push('reading');
console.log(two.name, two.hobby, two.say()); //tom [ 'sing', 'swimming','reading' ] hello
console.log(three.name, three.hobby, three.say()); //tom [ 'sing', 'swimming','reading' ] hello

本质上寄生继承就是对原型继承的第二次封装,
并且在这第二次封装过程中对继承的对象进行了拓展,这项新创建的对象不仅
仅有父类中的属性和方法而且还有新添加的属性和方法
同样的,存在多个实例对引用类型属性修改的篡改问题,
也是不推荐的继承方案

6.寄生组合继承

/*寄生组合继承形态一    很大程度上可以看作是原型继承的进一步优化*/
//父类 构造函数
function One(age) {
    this.age = age || 20;
    this.nameArr = [ 'tom', 'lucky' ];
}
One.prototype.say = function() {
    console.log('hello');
};
//子类构造函数
function Two(age) {
    One.call(this, age);
    this.sex = 'male';
}
const two1 = new Two(20);
two1.nameArr.push('hehe');
console.log(two1); //{age:20,nameArr:[ 'tom', 'lucky','hehe' ],sex:'male'}
//console.log(two1.say()); //报错 two1.say is not a function
/*
    可知 虽然在子类函数中借用关键字 call 调用了父类构造函数 继承了父类的一些属性
    但是我们可以看到,当子类创建的实例去访问定义在父类原型上的方法时是会报错的,访问不到
    在前面提到的组合继承中,通过修改和修正子类的prototype 可以有效解决这个问题,
    在这里,我们通过组合原型继承和寄生继承同样也可以实现 过程如下

*/
function createObj(obj) {
    function makeNewObj() {}
    makeNewObj.prototype = obj;
    return new makeNewObj();
}
Two.prototype = createObj(One.prototype);
Two.prototype.constructor = Two;
const three = new Two(23); //新创建一个实例 three 访问say属性
console.log(three); //{age:23,nameArr:[ 'tom', 'lucky' ],sex:'male'}
console.log(three.say()); //hello

以下是对以上寄生组合继承方案进行进一步封装

/*寄生继承的形态二  可以看作是对形态一的进一步封装*/

function Father(age) {
    this.age = age || 40;
    this.nameArr = [ 100, 200, 300 ];
}
Father.prototype.say = function() {
    return 'hello world';
};
function Child(age) {
    Father.call(this, age);
}

function Create_Child(o) {
    function makeChild() {}
    makeChild.prototype = o;
    return new makeChild();
}
function inheritFn(child, father) {
    child.prototype = Create_Child(father.prototype);
    child.prototype.constructor = child;
}
inheritFn(Child, Father);
const child1 = new Child(20);
console.log(child1.say()); //hello world

进一步可以简化为

/*寄生组合继承的最终形态*/
function Person(age) {
    this.age = age || age;
    this.arr = [ 1, 2, 3 ];
}
Person.prototype.say = function() {
    return 'human';
};
function Male(age) {
    Person.call(this, age);
}
Male.prototype = Object.create(Person.prototype);
Male.prototype.constructor = Male;
const male1 = new Male(30);
console.log(male1); //{age:30,arr:[1,2,3]}
console.log(male1.say()); // human

寄生组合继承与组合继承相比,它的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了
在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能
保持不变;寄生组合式继承是最理想的继承范式。

7.最优的继承方案 es6的extends

class Person {
    constructor(name) {
        this.name = name;
        this.hobby = [ 1, 2 ];
    }
    sayHi() {
        console.log('hello');
    }
    play() {
        console.log('basketball');
    }
}
class Man extends Person {
    constructor(name, age) {
        super(name);
        this.age = age;
    }
    sayHi() {
        console.log('I am from Man');
    }
}
let man1 = new Man('tom', 20);
console.log(man1);
man1.sayHi(); //I am from Man
man1.play(); //basketball

优点:简洁明了,不用手动设置原型。
缺点:新语法,部分浏览器支持,需要转为 ES5 代码。

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

推荐阅读更多精彩内容