面向对象编程语言(如C++、Java)存在“类”这个概念,“类”是对象的模板,对象是“类”的实例。但在javascript语言体系中不存在“类”,存在构造函数和原型链,构造函数是对象的模板,原型链给对象提供方法和属性。
理解一门语言,必须理解这门语言的底层以及原理。
1、构造函数
1.1、工厂模式
早期考虑ECMAScript无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象,这类特定接口包括Object、Aarray等原生构造函数。
function turbosun(name, sex){
var o = new Object();
o.name = name;
o.sex = sex;
o.info = function(){
console.log("姓名:" + this.name +" 性别:" + this.sex);
}
return o
}
var miniturbosun = turbosun("孙俪","female");
miniturbosun.info();复制代码
工厂模式解决了创建多个相似对象的问题,但没有解决对象识别问题,即我们不知道这是一个怎么样的对象类型,没有辨别度。
随着发展,新的模式出现了。
1.2、构造函数模式
除了原生构造函数之外,我们可以自定义创造构造函数。这里我构造了一个“Turbosun”类的函数,和Object的用法一样,通过new Turbosun创建实例对象。“turbosun”和“miniturbosun”都是基于Turbosun的实例对象。(这里解决了辨别度的问题)
function Turbosun(name,sex,job){
this.name = name;
this.sex = sex;
this.job = job;
this.info = function() {
console.log("姓名:" + this.name + " 性别: " + this.sex + " 职业:" + this.job);
}
}
var turbosun = new Turbosun("孙俪", "female" ,"演员");
turbosun.info();
console.log(turbosun.name);
var miniturbosun = new Turbosun("小花","female","无业");
miniturbosun.info();复制代码
构造函数很好用,但是它也有缺点。使用构造函数的主要问题,就是每次实例化构造函数时,每个方法都要在每个实例上重新创建一遍。
this.info = new Function(){
...
}
//本质是实例化了一个Function的实例。复制代码
所以turbosun的info方法和miniturbosun的info方法是独立分配在内存中,如果我们创造了很多实例,那么内存中是不是出现了很多info方法?岂不是很浪费内存?有没有什么方法只创造一个info,让实例话对象指针都指向这个info?
这些问题的出现,又迎来了原型模式。
1.3、原型模式
我们创建的每个函数都有一个原型属性prototype,这个属性是一个指针,指向一个原型对象,这个原型对象的用途是包含可以以特定类型的所有实例共享的属性和方法。下面我们在Turbosun的prototype中添加属性和方法,这些属性和方法将被实例共享。
function Turbosun(){
}
Turbosun.prototype.name = "孙俪";
Turbosun.prototype.sex = "female";
Turbosun.prototype.job = "演员";
Turbosun.prototype.hobbies = ["看电影"];
Turbosun.prototype.info = function(){
console.log("姓名:" + this.name + " 性别: " + this.sex + " 职业:" + this.job);
}
var turbosun = new Turbosun();
turbosun.info();
turbosun.hobbies.push("运动");
var miniturbosun = new Turbosun();
miniturbosun.info();
console.log(miniturbosun.hobbies); 复制代码
turbosun是实例化对象,turbosun有一个prototype属性,这个属性指向Turbosun.prototype,所以我们可以访问到name/sex/job/info。同理,miniturbosun也能访问到这些属性和方法。
但是这里有个问题,如果要自定义属性怎么办呢?如果存在引用类型,任意一个实例修改引用类型,会导致所有实例的数据改变。
1.4、组合使用构造函数和原型模式
有了之前的实践,使用构造函数模式定义实例属性,原型模式定义共享属性和方法。
function Turbosun (name, sex, job){
this.name = name;
this.sex = sex;
this.job = job;
}
Turbosun.prototype = {
constructor: Turbosun,
info:function(){
console.log("姓名:" + this.name + " 性别: " + this.sex + " 职业:" + this.job);
}
}
var turbosun = new Turbosun("孙俪","sex","演员");
turbosun.info();复制代码
Turbosun.prototype使用字面量的方式相当于重写了整个原型对象,它的constructor对象指向了Object,所以要将其重新指回Turbosun。
1.5、寄生构造函数模式
这个 模式咋一看跟工厂模式很像,其实是差不多,只不过现在是构造函数。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。
function Turbosun(name, sex, job){
var o = new Object();
o.name = name;
o.sex = sex;
o.job = job;
o.info = function(){
console.log("姓名:" + this.name + " 性别: " + this.sex + " 职业:" + this.job);
}
return o
}
var turbosun = new Turbosun("孙俪", "female", "演员");
turbosun.info();复制代码
1.6、稳妥构造函数模式
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的 实例方法不引用 this;二是不使用 new 操作符调用构造函数。下面函数只有info方法能够获取到数据,没有别的方式可 以访问其数据成员。相对于其他模式,它比较安全。
function Turbosun(name,sex,job){
var o = new Object();
o.info = function(){
console.log("姓名:" + name + " 性别: " + sex + " 职业:" + job);
}
return o
}
var turbosun = Turbosun("孙俪","female","演员");
turbosun.info();复制代码
2、继承
继承是 面向对象编程语言中的一个最为人津津乐道的概念。许多编程语言都支持两种继承方式:接口继承和
实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。如前所述,由于函数没有签名,
在 ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链
来实现的。
2.1、原型链
原型链的的继承是实例化一个上级构造函数,然后将实例对象赋值给下一级构造函数的原型对象。
function Turbosun(sex){
this.sex = sex;
this.hobbies = ["看书","画油画","运动"]
}
Turbosun.prototype.info = function(){
console.log(this.sex)
}
function MiniTurbosun(name){
this.name = name;
}
MiniTurbosun.prototype = new Turbosun("female");
MiniTurbosun.prototype.hobby = function(){
console.log(this.name)
}
var miniTurbosun = new MiniTurbosun("小花妹妹");
miniTurbosun.hobby()
miniTurbosun.info()
console.log(miniTurbosun.sex)
miniTurbosun.hobbies.push("旅游");
var hans = new MiniTurbosun("dengdeng");
console.log(hans.hobbies)复制代码
我们知道,构造函数有一个prototype原型属性指向原型对象,构造函数的实例对象也有一个原型属性指向原型对象。
MiniTurbosun.prototype 现在是Turbosun的实例化对象,所以MiniTurbosun.prototype有一个[[prototype]]属性指向Turbosun的原型对象。当然,MiniTurbosun.prototype中还有Turbosun的sex和hobbies这两个实例属性。
miniTurbosun是MiniTurbosun的实例对象,它的[[prototype]]属性指向MiniTurbosun的原型对象。
这里用一副图来展示一下他们之间的关系。
图形很清晰地展示了这条原型链,但是原型链继承有缺点么?
第一个问题:如果miniTurbosun往hobbies里push数据,那么是不是坏了一锅“粥”呀?之后的实例对象的hobbies里面就不是原始数据了。图上我们可以看到实例对象的[[prototype]]只是一个指向原型对象的指针,hobbies始终是共享的。
第二个问题:在创建子类型的实例时,不能向超类型的构造函数中传递参数。这句话的意思是在创建MiniTurbosun的实例时,不能向Turbosun传递参数。
2.2、借用构造函数
利用call()或apply()修改this指向,在之后创建子类型(MiniTurbosun)的实例时,调用超类型(Turbosun)的构造函数。
function Turbosun(sex){
this.sex = sex;
this.hobbies = ["看书","画油画","运动"]
}
Turbosun.prototype.info = function(){
console.log(this.sex)
}
function MiniTurbosun(name,sex){
Turbosun.call(this,sex)
this.name = name;
}
var miniTurbosun = new MiniTurbosun("小花妹妹","female");
console.log(miniTurbosun.sex);
miniTurbosun.hobbies.push("旅游");
console.log(miniTurbosun.hobbies);
var hans = new MiniTurbosun("dengdeng","male");
console.log(hans.hobbies);
复制代码
2.3、组合继承
组合继承就是将原型链和借用构造函数的技术组合到一块,其思路是使用原型链实现对原型属性和方 法的继承,而通过借用构造函数来实现对实例属性的继承。这样一来,就可以让两个不同的 MiniTurbosun 实例既分别拥有自己属性——包括 hobbies属性,又可以使用相同的方法了。
function Turbosun(sex){
this.sex = sex;
this.hobbies = ["看书","画油画","运动"]
}
Turbosun.prototype.info = function(){
console.log(this.sex)
}
function MiniTurbosun(name){
Turbosun.call(this,"female")
this.name = name;
}
MiniTurbosun.prototype = new Turbosun();
MiniTurbosun.prototype.constructor = MiniTurbosun;
MiniTurbosun.prototype.hobby = function(){
console.log(this.name)
}
var miniTurbosun = new MiniTurbosun("小花妹妹");
miniTurbosun.hobby()
console.log(miniTurbosun.sex)
miniTurbosun.hobbies.push("旅游");
var hans = new MiniTurbosun("dengdeng");
console.log(hans.hobbies)复制代码
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。但是仔细发现,子类型虽然继承了超类型的实例属性,但是子类型的原型对象中也继承了超类型的实例属性,但这不影响函数执行。
2.4、原型式继承
在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的
原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var turbosun = {
name:"孙俪",
hobbies:["看书","旅行","画画"]
}
var hannan = object(turbosun);
hannan.hobbies.push("跑步");
console.log(turbosun.hobbies)
console.log(hannan.hobbies)复制代码
2.5、寄生式继承
寄生式(parasitic)继承是与原型式继承紧密相关的一种思路,寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数, 函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function Turbosun(o){
var clone = object(o);
clone.act = function(){
console.log("movies")
}
return clone
}
var turbosun = {
name:"孙俪",
hobbies:["看书","旅行","画画"]
}
var miniTurbosun = Turbosun(turbosun);
console.log(miniTurbosun.hobbies)
miniTurbosun.hobbies.push("看电影");
console.log(miniTurbosun.hobbies);
console.log(turbosun.hobbies)复制代码
2.6、寄生组合式继承
组合继承是 JavaScript 最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是
在子类型构造函数内部。寄生组合式继承改善了调用两次超类型构造函数的现状。
function Turbosun(sex){
this.sex = sex;
this.hobbies = ["看书","画油画","运动"]
}
Turbosun.prototype.info = function(){
console.log(this.sex)
}
function MiniTurbosun(name){
Turbosun.call(this,"female")
this.name = name;
}
function inheritPrototype(subType, superType){
var prototype = Object.create(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}
inheritPrototype(MiniTurbosun,Turbosun)
var miniturbosun = new MiniTurbosun("小花妹妹");
miniturbosun.info();复制代码
寄生组合式继承的好处是:子类型利用借用构造类型继承实现了构造函数实例属性的继承,通过inheritPrototype子类型实现了对超类型的原型对象的继承,这时子类型的原型对象中没有超类型的实例属性了(这一点算是在组合继承上的优化)。
本文学习资料:高级程序设计(第三版) 红色