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
因为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() 通过原型链这就链成链了
右侧是一个小的原型链,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; }
还是原型指向的问题,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就为了过渡一下,没有实际的用途,
所以把它放的闭包里当做私有化变量,看起来更好些,写法上语义上也更好,
无关的隐式的附加的东西,就放的私有化变量里面去,这是一个高端的写法。