上一段时间为了面试一直在看这个“创建对象”和“继承方法”,但是面试的时候并没有问到(可能是面试官觉得我太水了,也不想问)。不过,网上大部分资料都说,JavaScript里的设计方法和继承是很重要的,还是想打算重新梳理一下~万一以后用到了呢?
(下面是直接参考《JavaScript高级程序设计(第三版)》里的内容,有部分个人的见解,也省略了一部分)
创建对象
创建对象 有4种常用方法:1、工厂模式;2、构造函数模式;3、原型模式;4、组合使用构造函数模式与原型模式
1、工厂模式
(个人理解就是,有一个专门“闭门造车”的函数,成为工厂,在工厂内部自己搞出一个对象,然后负责return给调用他的人)
代码如下:
function createPerson(name , age , job){
var o = new Object();//在里面创建一个Object对象
o.name = name; //把传进来的参数给他赋值给对应属性字段
o.age = age;
o.job = job;
o.sayName() = function(){ //创建一个新sayName方法
alert(this.name);
};
return o;
}
var person1 = createPerson("carl",21,"worker"); //通过最简单的函数传参方法去调用这个“工厂函数”,然后person1这个变量将会获得一个Object对象,也就是o
缺点:因为这个“工厂函数”返回来的这个对象o,全部都是Object类型的(不信你person1 instanceof Object试试)
为了解决上面的自定义对象类型问题,所以有了下面的构造函数模式↓↓↓
2、构造函数模式
(个人理解就是,和java里面的有参构造函数差不多)
function Person(name , age , job){ this.name = name; this.age = age; this.job = job;
this.sayName = function(){
alert(this.name);
}; }
var person2 = new Person("carl",21,"worker");
优点:可以自定义对象类型了(不信你person2 instanceof Person试试)
缺点:我每次要给对象定义函数方法,就要写进构造函数,那么,我定义的这个方法,就成为了每个实例的“实例方法”!也就是说,person2有一个sayName,person3也有一个sayName,每个方法都要在每个实例上重新创建一遍,更要命的是,一个函数就是一个对象,每重新创建一遍这个方法,那就是重新实例化了一个对象~又因为不同实例上的同名方法是不一样的,而且他们都只是调用this(功能一样),所以!得出结论:没有必要重新创建一遍功能一模一样的方法
为了解决上面这个“方法复用”的问题,有了原型模式
3、原型模式
(个人理解就是,设置这个构造函数的原型对象,这个原型对象就是所有通过这个构造函数实例出来的对象都共享的属性or方法)
function Person(){ }//将构造函数写成了空函数
Person.prototype.name = "carl";
Person.prototype.age = 29;
Person.prototype.job = "worker";
Person.prototype.sayName = function(){
alert(this.name);
}
var person3 = new Person();
person3.sayName();//"carl"
var person4 = new Person();
person4.sayName();//"carl"
alert(person3.sayName == person4.sayName)//true
新问题1:但是,虽然person3和person4是两次通过new 构造函数得来的两个不同实例,但是你看,构造函数是空的!也就是没有实例属性,我们prototype的是原型属性,而原型属性(包括原型方法),是要被每个实例对象共享的!也就是这些实例的每个人名字name都叫“carl”!
解答1 :直接在实例化了person3后,使用下图代码,那么此时person3用的就是实例属性,而不是原型属性。
这个可以通过 alert(person3.hasOwnProperty("name")); 来检测,当前person3的name属性,用的是实例属性(true),还是原型属性(false)了
也可以通过 alert(hasPrototypeProperty(person , "name"));来检测 ,这个方法是专门检测是否用的是原型属性,和上面的相反。
person3.name = "michael"
优点:解决了构造函数模式的“方法复用”问题,你看,最后一句都说,两个方法是一样的了。
缺点:那万一我想每个实例都有自己的一个数组呢?(比如person3有一个朋友数组,person4也有一个朋友数组,两个人的朋友交际圈不一样,那么继续用原型模式,只会使每个实例对象都是同一个“交际圈”)怎么办呢?
4、组合使用构造函数模式和原型模式
上面说了,构造函数模式可以解决给每个实例对象都拥有自己的实例属性这个能力,而原型模式可以解决给每个实例对象都有共享原型属性(or方法)这个能力,那么,把这两种模式结合起来,不就可以解决上面原型模式带来的“交际圈”不同的问题了吗?
function Person(name , age , job){ this.name = name;
this.age = age ;
this.job = job ;
this.friends = ["a","b"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person5 = new Person("carl",21,"worker");
var person6 = new Person("pinky",24,"nurse");
person5.friends.push("David");
alert(person5.friends);//"a,b,David"
alert(person6.friends);//"a,b"
alert(person5.friends == person6.friends);//false
alert(person5.sayName == person6.sayName);//true
到此为止,已经解决了我们的心头大恨了,“交际圈”这个数组是每个实例的实例属性(不会遭共享)了,而sayName这样的共享方法也只在原型里创建一次。
所以,这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。也可以说,这是用来定义引用类型的一种默认模式啦!
继承
继承 的4种常用方法:1、原型链;2、借用构造函数继承;3、组合继承;4、寄生组合式继承
首先想说一下,许多OO语言都支持两种继承,一种是接口继承(只继承方法签名,相当于java的implements),一种是实现继承(继承实际的方法,相当于java的extends)。因为JavaScript的函数都是没有签名的,所以JavaScript只能通过“实现继承”,其实现继承主要依靠的是“原型链”来实现的
1、原型链
因为“ 构造函数 ← 原型 ← 实例” ,所以,让“子原型” 变成 “父实例”,那么,以后创建的子实例,也就拥有了父类的所有属性和方法了~
function Sup(){
this.pro = true; }
Sup.prototype.getSupVal = function(){
return this.pro;
};
function Sub(){
this.subPro = false;
}
//继承了Sup
Sub.prototype = new Sup();
Sub.prototype.getSubVal = function(){
return this.subPro;
}
var instance = new Sub();
alert(instance.getSupVal());//true
alert(instance.getSubVal());//false
可以看出,子类不仅继承了父类的属性(pro)和方法(getSupVal),还自己写了一个方法(getSubVal)
注意!如果要和他一样,在子类写一个新的方法, 那么用上面那种方式就行了,切记不能重写对象字面量,否则会切断旧实例和现有原型的联系
缺点:①这样父类的实例属性,就变成了子类的原型属性,就会在子类的实例中共享了!
②创建子实例的时候,不能向父类的构造函数传递参数
所以就有了下面的“借用构造函数”
2、借用构造函数(call)
为了解决“让父类的实例属性,在子类的实例中不要共享”,那就用call()吧!
先科普一下,一个函数是在特定环境中执行的代码对象,通过call可以改变这个函数的执行环境。
用法:A.call(B);那么函数A就在B的环境下执行(B也是个函数),从而B可以获得函数A的所有变量和方法
function Sup(){
this.colors = ["red","orange","blue"];
}
function Sub(){
Sup.call(this);//将Sup()函数的执行环境改为Sub()内触发,换句话说,也就是继承了Sup
}
var instance1 = new Sub();
instance1.colors.push("black");
alert(instance1.colors);//"red,orange,blue,black"
var instance2 = new Sub();
alert(instance2.colors);//"red,orange,blue"
像上面那样,在子类构造函数里边,利用call来调用父类的构造方法,同时也省略了“子原型 = 父实例”,这样不仅可以达到继承,也可以让父类的实例属性不会变成共享属性。
对了!call()的第二个参数开始,就是被调用的函数的参数。apply()功能和call()一样,但是apply()的第二个参数是个参数数组,也可以是arguments
缺点:①构造函数的确不会共享了,那么如果我要写的是共享的属性或者方法呢!
②父类里面的原型定义的方法对于子类来说是看不见的
为了解决这些问题,也就有了下面的组合继承:
3、组合继承
和前边的“创建对象里面的组合构造函数模式和原型模式”一样,各取所长,即通过构造函数来实现对实例属性的继承,再通过原型链来实现对原型属性和方法的继承.
function Sup(name){
this.name = name;
this.colors = ["red","orange"];
}
Sup.prototype.sayName = function(){
alert(this.name);
};
function Sub(name,age){
//继承属性
Sup.call(this,name);//第一个参数放被调用函数的执行环境,第二个参数放被调用函数的传参
this.age = age;//这里还定义了一个子类的实例属性age
}
//继承方法
Sub.prototype = new Sup();//子原型 = 父实例
Sub.prototype.constructor = Sub;//这个“constructor”是一个原型属性来的,方便检查出这个子实例的构造方法是Sub
Sub.prototype.sayAge = function(){
aleert(this.age);
};
var instance1 = new Sub("carl",29);//这里将两个参数传进了子构造Sub,其中“carl”是传给父构造Sup用的,“29”是自己用的
instance1.colors.push("black");
alert(instance1.colors);//"red,orange,black"
instance1.sayName();//sayName是一个父类原型的方法,"carl"
instance1.sayAge();//sayAge是子类原型的方法,"29"
var instance2 = new Sub("pinky",31);;
alert(instance2.colors);//"red,orange"
instance2.sayName();//"pinky"
instance2.sayAge();//"31"
从上面的代码可以看出,组合继承融合了原型链和借用构造函数的优点,而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。
组合继承成为了JavaScript中最常用的集成模式。
然而,聪明的开发人员感觉还是不够简单,因为上面他调用了两次父类构造函数:一次是在“子原型=父实例”的时候,一次是在call的时候。这是因为,子类最终会包含父类的全部实例属性,但是我们不得不在调用子类构造的时候重写这些属性啊!
还是不懂吗?那我说详细一点吧:
第一次在“子原型=父实例”调用到new Sub();的时候,子原型也就是Sub.prototype会得到两个属性:name和colors,而且它们都是Sup的实例属性,同时也是Sub的原型属性;
第二次在call的时候,也调用了new Sub();,只不过这次是在新对象上,创建了实例属性name和colors。
于是,第二次的这两个属性,就屏蔽了子原型中的两个同名属性啦!聪明的开发人员发明了下面这种方法,可以省略一次调用父类构造函数哦!
4、寄生组合式继承
function inherit(sub,sup){ var proto = new Object(sup.prototype);//创建父类原型对象副本
proto.constructor = sub;//把这个对象副本的constructor原型属性改成sub构造函数
sub.prototype = proto;//再把这个“父类原型对象副本”赋值给子类原型
}
用的时候这样写:
function Sup(name){ this.name = name; this.colors = ["red","orange"]; } Sup.prototype.sayName = function(){ alert(this.name); }; function Sub(name,age){ //继承属性 Sup.call(this,name); this.age = age; } //继承方法 //Sub.prototype = new Sup();//子原型 = 父实例 //Sub.prototype.constructor = Sub;
//把上面两句省略,改成:调用inherit:
inherit(Sub,Sup);
Sub.prototype.sayAge = function(){ aleert(this.age); };
这样可以避免在子原型上面创建不必要、多余的属性。所以开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式~!