Go to comments

JavaScript 继承模式

继承发展史

1. 传统形式

    过多的继承了没用的属性

2. 借用构造函数

    不能继承借用构造函数的原型

    每次构造函数都要多走一个函数

3. 共享原型

    不能随便改动自己的原型

4. 圣杯模式


上节课讲的是原型,原型(prototype)是function的一个属性,非得是构造函数才有原型吗?

不是,是个函数都有这个prototype属性,构造函数只是一个特殊表示,每个函数都有原型,原型是这个构造函数构造出对象的公有祖先。


原型是构造函数上面的东西,原型是这个构造函数构造出对象的公有祖先。

接着上节课收尾,继承(extend)有很多种方式,继承发展了很长时间,从技术一开始萌生到最后走向成熟,发展了一系列东西。


昨天讲的由于原型产生的继承关系,原型只是实现继承的一种方法,但提升不到工业化的一个程度,那个太low了而且会发生很多问题,这节课没有什么新的语法,只是把上节课的语法进行汇总,包括一个"变形"好好的用一下,看看怎么来用这个东西,产生的继承才更加丰满更加的完善。

一、传统形式

最开始想实现一种继承关系,A继承B或一个对象继承一个原型,

第一种方式就是上阶课讲的传统方式原型链,连成一个链之后谁继承自谁,谁再继承自谁,从原型链上逐步去继承

Grand.prototype.lastName = "明";
function Grand(){
	
}
var grand = new Grand();


Father.prototype = grand;
function Father(){
	this.name = "老明";
}
var father = new Father();


Son.prototype = father;
function Son(){
	
}
var son = new Son();

原型链虽然好但有个问题,它过多的继承了没用的属性,

比如通过原型链的继承产生了三个原型连成的链,现在主要想继承原型链顶端的"lastName"属性,但是由于形成了原型链,一系列的都连成一个继承下来了。


对象son既能继承fasher的东西,又能继承Father原型的东西,又能继承grand的东西,又能继承Grand原型的东西,一系列都继承了,

这样一系列从头继承到尾就会发生一个矛盾,想继承的继承下来了,不想继承的也继承下来了,造成了效率和用法上的不好,所以第一种方法很快就被废弃了。

二、借用构造函数

后来又发展一段时间,既然原型链的方法不好就换一种方法,就是call、apply的一个实际应用,叫借用构造函数,

第二种方法要强说是继承,好像也不太是继承,就是借别人的方法来用一下。


现在构造一个Student构造函数,之前已经有Person函数已经形成了部分功能,之后就没必要把所有的功能都写到Student自己身上,用Person函数的就可以了

function Person(name ,age ,sex){
    this.name = name;
    this.age = age;
    this.sex = sex;
}

function Student(name ,age ,sex ,grade){
    Person.call(this, name, age, sex); // 把this放进去有个前提,构造函数Student本身必须new操作 
    this.grade = grade;
}

var glee = new Student('Glee', 37, 'female', 2000);

从这个工业化的层面上,用别人的方法也叫做一种继承,然而并不是,实质上它没有继承的关系,它只是用你的东西做我的事,但是也勉强把它算到继承发展史里面其中一个环节了。


第二种方法,借用构造函数的方式,实现初步函数的借用,但也有不太好的地方

1). 只能借用Person构造函数的方法,不能用人家的原型,

2). Student构造出的glee对象的原型还是自己的Student.prototype

3). 每调用一次构造函数实际上执行了两个方法,每构造一个对象都要执行一次Student,还要执行一次Person,

     从视觉上省了代码量,但从运行上根本就没省,而且还更加复杂了,还增加了一个函数的调用浪费了效率。


所以不把它算成一个标准的继承模式,后来再发展,发展出了第三种模式,基本就是现在用的一个标准的模式了,

如果想实现一个A继承B,B继承C就用第三种模式叫做共有原型


这里说一下,第二种形式虽然在继承发展史里面是一个环节,但在是在工业开发上,但凡有需求还是建议用第二种方式,

就是别人写一个构造函数,我又写了一个构造函数,我的方法完全囊括了他的方法,那就把他的拽过来。


但是从继承的角度想实现A继承B,下面第三种是最好的,然后由第三种能导出第四种,我们先看第三种公有原型

三、共享原型

想让构造函数Son生产出的对象,继承自构造函数Father的原型(Father.prototype),按照原来的方法用原型链,

现在不用原型链了,现在简单粗暴Son.prototype = Father.prototype 让一个原型给了两个函数

Father.prototype.lastName = "明";

function Father(){

}

function Son(){

}

Son.prototype = Father.prototype; // 让一个原型链给了两个函数

也就是说现在不论是构造函数Father,还是构造函数Son,它们的原型都是Father.prototype

image.png

因为Father.prototype是一个对象,对象是引用值,引用值之间的赋值是传地址过去,现在让Father.prototype直接扔到Son.prototype里面就ok了,这种继的承方式叫公有原型


让构造函数Son也有Father的原型就这样来,以后Son构造出来的对象,就继承了Father.prototype上的属性,它两共用一个原型了

Father.prototype.lastName = "明";

function Father(){

}

function Son(){

}

Son.prototype = Father.prototype; // 让一个原型链给了两个函数


// 两个构造函数有同一个原型,所以这两个构造函数生产出的对象就继承自同一个原型


var son = new Son();

console.log(son.lastName); // 明

var father = new Father();

console.log(father.lastName); // 明


一开始学习原型时,原型是这个构造函数构成出对象的公有原型(公有祖先),现在多个构造函数还可以共用同一个原型,这是一个新的用法

所以想让构造函数Son继承构造函数Father的原型,这种是A继承自b,就用公有原型的方式,不用原型链的方式这种方式更好,


这种方式虽然简单明了,我们可以封装成一个inherit函数

/**
 * 继承 公有原型
 * 让Target继承Origin,Target的prototype等于Origin的prototype,就这么一步
 *
 * Target: 构造函数  原型源头 Father
 * Origin: 构造函数  目标 Son
 */ 

function inherit(Target, Origin){

	Target.prototype = Origin.prototype; 
	
}


PS:

要学会这种技能,抽象出一个功能,封装出一个函数,函数就代表功能,函数代表功能的一个复用,那块能复用,那块能定义化,就是通过参数来实现的。

功能是继承,继承有两种写法一种是extend、一种inherit也是继承的意思。


inherit还是CSS属性的一个值,这个值是继承值的意思,

比如css属性里面但凡是文字类的属性,包括文字类的颜色color、font-size、font-weigth、line-height都是文字类属性,文字类属性有一个传递的特性,就是如果子元素没设置文字类属性,就默认继承父元素的文字类属性。


给父级加一个font-size:20px,子元素也是20像素,除非单独设置,所以这个子元素font-size的默认值就是inherit(font-size:inherit),这是一个css值,代表我没有就继承父级的。

这里沿用inherit的命名这个继承函数。


两个参数(Target, Origin)传进来的是构造函数,我们不是想让一个对象去继承某一个东西,我们最终要的是一个构造函数去继承某一个东西,这样这个构造函数生成出的对象就全继承这个东西了,这是根上的问题,继承最好是从根上来。


想让一个对象去继承一个东西,直接去改__proto__就可以了,我们想让一个构造函数,构造出的所有对象都改变继承的指向,所以我们让构造函数的prototype更换一下,让构造函数它们互相实现继承。


Father是一个原始的构造函数,这个构造函数上面有一个原型,这个原型是希望被用到构造函数Son身上的,现在改变一下Son的原型,让它变成Father的原型,实现两个构造函数公有一个原型的特点。

function inherit(Target, Origin){
	Target.prototype = Origin.prototype; 
}


Father.prototype.lastName = "明";

function Father(){

}

function Son(){

}

inherit(Son, Father); // Son、Father这两个函数实现了继承,此时这两个函数生成出的对象共用一个原型了 


var son = new Son();

var father = new Father();

console.log(son.lastName); // 明

console.log(father.lastName); // 明


注意一个问题,让继承 inherit(Son, Father) 放到new操作的下面,还能实现继承吗?

function inherit(Target, Origin){
	Target.prototype = Origin.prototype; 
}

Father.prototype.lastName = "明";
function Father(){

}

function Son(){

}

var son = new Son();

var father = new Father();

inherit(Son, Father); // 放到new操作符的下面,就不能实现继承了

console.log(father.lastName); // 明

console.log(son.lastName); // undefined

已经生成了对象,再改原型已经晚了,所以现在son访问lastName输出nudefined,因为对象son继承的还是原来原型的那个空间,指向已经是原来原型的那个空间了,一定要先继承再使用new操作生成对象。


这种方法有没有不足呢?

还是有点不足的,比如构造函数Son虽然用的是Father.prototype,但Son想给自己的原型上多加一个属性sex,方便Son生成出的对象使用


Son多加一个属性 Son.prototype.sex = male 生产出的对象都是男的,这样理论上的可以了

function inherit(Target, Origin){
	Target.prototype = Origin.prototype; 
}

Father.prototype.lastName = "明";
function Father(){

}

function Son(){

}

inherit(Son, Father); // 继承

Son.prototype.sex = 'male'; // 在继承的下面,Son多加一个sex属性,生成出的对象都是男的male

var son = new Son();

var father = new Father();

console.log(son.sex); // son访问male属性值是male

console.log(father.sex); // 对象father也有sex属性值是male

但是有一点不好,Father生成出的对象也有sex属性,Son.prototype和Father.prototype都指向的是同一个空间,构造函数Son给这个空间加属性,构造函数Father的空间也变了,所以Son想实现个性化的,自己属性加到原型上是不行。


我们希望的是,构造函数Son用的是构造函数Father的原型,但是Son往自己原型上加属性,根本不影响Father的原型。我又继承自你,我又不想影响你,这才是完美的形式,


我有自己定义的一个原型,我又有你给我提供的一个原型,有自定义的,有咱俩公有的,这就引申出最后一个丰满的形式圣杯模式

四、圣杯模式

想给构造函数Son在自己原型上,加一个的独特的属性,但是影响了构造函数Father的原型,怎么办呢?


方法还是公有原型,但是要有点小的区别

1). 单独加一个构造函数F,做一个中间层

2). F.prototype = Fater.prototype 现在F跟Father它两共用一个原型了,那跟Son还有什么关系呢?

3). Son.prototype = new F()  通过原型链这就链成链了

image.png

右侧是一个小的原型链,F.prototype继承了Father.prototype,F.prototype就是Father.prototype了,然后new F()当做Son.prototype的原型,

这样的好处是Son.prototype原型是new F(),new F()是干干净净的,要给Son.prototype加属性,根本不影响Father.prototype,然而Father.prototype上的东西还依然被继承,现在我又能改自己的又能继承别人的,这是最终的圣杯模式。


现在Son想给自己的Son.prototype上加属性,不会影响Father.prototype,因为Son的原型是new F(),new F()的原型才是Father.prototype,

我们把这个提取出来封装成一个inherit函数,再次实现a继承b的功能。

/**
 * 最终的继承"圣杯模式"
 *
 * Target: 构造函数 原型源头 Father
 * Origin: 构造函数 目标   Son
  */ 

function inherit(Target, Origin){
	function F(){};
	F.prototype = Origin.prototype;
	Target.prototype =  new F();
}


试一下这个函数

function inherit(Target, Origin){
	function F(){};
	F.prototype = Origin.prototype;
	Target.prototype =  new F();
}

Father.prototype.lastName = "明";
function Father(){

}

function Son(){

}

inherit(Son, Father); // 实现继承

var son = new Son();
var father = new Father();

console.log(son.lastName); // 明
console.log(father.lastName); // 明


// -------------------------------------------------------------------

Son.prototype.sex = 'male'; // 给构造函数Son自己加一个sex属性

console.log(son.sex); // 对象son有male属性打印male

console.log(father.sex); // 对象father没有sex打印undefined

console.log(Father.prototype); // 没有改变Father的原型 {lastName: "明", constructor: ƒ}


还可以在丰满,忽略了一个问题,原型上都有一个系统自带的constructor属性,默认值指向它的构造函数

function inherit(Target, Origin){
	function F(){};
	F.prototype = Origin.prototype;
	Target.prototype =  new F();
}

Father.prototype.lastName = "明";
function Father(){

}

function Son(){

}

inherit(Son, Father);

var son = new Son();

console.log(son.constructor) // 对象son的constructor属性指向 ƒ Father(){}

对象son的constructor属性,理应指向它默认的构造函数Son,现在是ƒ Father(){}


分析一下怎么来

1). 对象son的原型是new F(),son.__proto__ --> new F(),对象身上都没有constructor原型上有

2). 向上找对象new F().__proto__的原型是谁?是Father.prototype

3). Father.prototype上面有constructor属性,指向的是ƒ Father(){}

所以对象son访问constructor指向的是Father,指向紊乱了,因为是继承过来的


现在把构造函数Son的constructor归位,让对象son访问constructor时指向就是它自己构造函数

function inherit(Target, Origin){

	function F(){};
	F.prototype = Origin.prototype;
	Target.prototype =  new F();
	Target.prototype.constructor = Target; // 给归位constructor的指向

}


最后还可以再多加一步,这步可有可无,我们希望构造出的对象能找到自己的超类,超类的意思就是超级父级,

也就是真正继承自谁,对象son继承自new F()是我们给加的中间层,它真正继承的还是Father.prototype,希望把Father.prototype挂到对象身上保存起来,如果有需要知道自己真正继承自谁

function inherit(Target, Origin){
	function F(){};
	F.prototype = Origin.prototype;
	Target.prototype =  new F();
	Target.prototype.constructor = Target;
	Target.prototype.uber =  Origin.prototype; // 这里正常应该用super,但这是(super)关键保留字,所以我们用uber
}

如果有一天真正想知道继承自谁,可以通过这个方法把它调用出来,为信息的一个储存,这样的方法就形成了最完美的方法我们叫"圣杯模式",

这个方法是必须得会的,但凡有人问能实现一个继承吗就默写这个。


但要注意一个小问题,考考大家,如果按照下面这样换一个位置,还能好使吗?

function inherit(Target, Origin){

	function F(){};

	Target.prototype =  new F();    // 2. 到上面这

	F.prototype = Origin.prototype;

	// Target.prototype =  new F(); // 1. 把这行放的上面

	Target.prototype.constructor = Target;
	Target.prototype.uber =  Origin.prototype;

}

image.png

还是原型指向的问题,new F()的时候用的是构造函数F()原来的原型,后面改就晚了,一定要new之前改原型,这点一定要注意


把圣杯模式再换一种形式来写,我们的写法是一种最通俗的写法并不高大上,雅虎提供一个"YUI3库",封装了无数种功能,直接调用函数用就可以了(现在不用YUI3库了,现在用jQuery),

YUI3库里面有一个inherit继承圣杯模式,和我们通俗的写法极其类似,但是又有点小的不同,非常高大上

var inherit = (function(){

	var F = function () {};

	return function (Target, Origin){
		F.prototype = Origin.prototype;
		Target.prototype =  new F();
		Target.prototype.constructor = Target;
		Target.prototype.uber =  Origin.prototype;
	}

}());


// 先来一个立即指向函数,立即指向函数有返回值,在立即执行函数里面定义一个构造函数函数F。
// return 一个function,里面写了四行
// 最后立即执行函数执行完会把,return返回出来,抛给变量inherit

// ps: return function(){}的是一个函数引用,就相当于return一个函数名,return一个函数就相当于return一个函数的引用
// var inherit = (function(){
//     var F = function () {};
//     function demo(Target, Origin){
//     	F.prototype = Origin.prototype;
//     	Target.prototype =  new F();
//     	Target.prototype.constructor = Target;
//     	Target.prototype.uber =  Origin.prototype;
//     }
//     return  demo;
// }());

暂时看不太懂没关系,正好把前面闭包没讲的一个知识点学一下,闭包的第三点应用

五、闭包的第三点应用“可以实现封装,属性私有化”

这个构造工厂就构造"老邓",老邓是一个非常精明的人!终于在三十多年后的一天活明白了!!!攒了很多财富以及社会阅历,以及个人魅力!!!然后学会了生活上的小技巧!!!


老邓找了一个小媳妇,也有自己的功能,比如divorce功能(离婚),让wife换成小媳妇。

function Deng(name, wife){
	
    var prepareWife = "小媳妇小章";
    
    this.name = name;
    this.wife = wife;
	
    this.divorce = function(){
        this.wife = prepareWife; // 看好这里,变量prepareWife没有用this
    }
	
    this.changePrepareWife = function(target){
        prepareWife = target;
    }
	
    this.sayPrepareWife = function(){
        console.log(prepareWife);
    }

}

var deng = new Deng("虚明" ,"第一任妻子"); // 生成对象"老邓"

console.log(deng); // 看一下老邓有什么Deng {name: "虚明", wife: "第一任妻子", divorce: ƒ, changePrepareWife: ƒ, sayPrepareWife: ƒ}

deng.divorce(); // 然后老邓操作divorce方法离婚了

console.log(deng.wife); // 离婚后再访问deng.wife妻子换人了"小媳妇小章"

console.log(deng.prepareWife); // 访问不到变量prepareWife返回undefined

deng.sayPrepareWife();// 除非老邓自己的方法能看到小媳妇

为什么能换成小媳妇"小章"呢?

1). 构造出一个对象this点属性,this点方法,然后把this这个对象保存出来,这个变量prepareWife是函数里面,由于这个函数执行,产生的执行期上下文里面的一个变量。

2). 函数执行完被销毁了,为什么变量prepareWife能用呢?divorce()方法在对象上,由于对象被返回divorce()方法也被返回了,所以divorce()方法用变量prepareWife是可以的,为什么能用这个变量?

3). divorce()方法在外部执行的,外部执行的怎么能用内部的prepareWife变量呢?因为闭包。函数divorce被保存到外部,所以它储存了函数Deng的执行期上下文,就可以用这个闭包了。

4). 变量prepareWife是被这三个函数divorce、changePrepareWife、sayPrepareWife共同共用的。

5). 这三个函数divorce、changePrepareWife、sayPrepareWife分别和函数Deng形成了三对一的闭包,所以它们共同用函数Deng的AO,三个函数可以在外面可以随意存取。


Dong是一个构造函数,构造出的对象可以随意操作prepareWife,这是一个闭包的应用,这个变量prepareWife对于对象很微妙。

比如现在问老邓,你在是不是外面有小媳妇?访问deng.prepareWife返回undefined,并没有是清白的。然而老邓自己所有的方法都能操作这个prepareWife变量,但是我们问老邓说没有。

所以变量prepareWife老邓能用,但表面上看prepareWife变量不是他自己的。

这个闭包相当于隐藏的区域永远跟着他,所以变量prepareWife像一个私有化的变量似的,只有老邓自己能看到,别人看都看不到。

这样一个闭包的应用叫做"私有化变量",就像问老邓永远不会承认有小媳妇,但真有没有他自己知道。

想看到这个prepareWife变量,只能通过老邓的方法deng.sayPrepareWife()(还的是他确实设置了这个方法)才能看到,要想直接通过老邓deng.prepareWife看不到。

这样一个变量对于对象来说就变成了私有化变量了,只有通过对象自己设置的方法可以去操作它,外部用户想通过对象访问不到。

因为它不是对象身的东西,但是它对象和原有空间形成闭包里面的东西。这是闭包的第三点应用私有化变量。


再看这个

var inherit = (function(){
	var F = function () {};
	return function (Target, Origin){
		F.prototype = Origin.prototype;
		Target.prototype =  new F();
		Target.prototype.constructor = Target;
		Target.prototype.uber =  Origin.prototype;
	}
}());

1. 这个过程执行完,会把function放到变量inherit上

    var inherit = function (Target, Origin){

                        F.prototype = Origin.prototype;

                        Target.prototype =  new F();

                        Target.prototype.constructor = Target;

                        Target.prototype.uber =  Origin.prototype;

                  }

2. 问题是,构造函数F形成闭包了,成了return到外面函数的私有化变量了

3. 构造函数F变成私有化变量是一个非常好的写法,本身F就为了过渡一下,没有实际的用途,

    所以把它放的闭包里当做私有化变量,看起来更好些,写法上语义上也更好,

    无关的隐式的附加的东西,就放的私有化变量里面去,这是一个高端的写法。



Leave a comment 0 Comments.

Leave a Reply

换一张