原文作者:CaelumTian
原文地址:http://www.w3cfuns.com/blog-5465813-5405580.html
一、类的创建
在面向对象语言中,类是面向对象的基础,并且具有棉线的层次概念和继承关系。Javascript中并没有类的概念,Javascript是基于对象的弱类型语言,以对象为基础,以函数为模型,以原形为继承机制的一种模式。创建一个简单的对象,就是创建一个Object类的实例。虽然Object构造函数或对象字面量可以用来创建单个对象,但是这些对象都是基于Object这一个类。为此我们需要创建自己的类,Javascript中创建类的方式有很多种:工厂模式,构造函数模式,原形模式,动态原形模式,寄生构造模式,稳妥构造模式。
工厂模式:通过函数把一个类型实例包装起来,这样可以通过函数来实现类型的实例化;但是这只是一种伪装的构造函数,而且instanceof判断会发现创建的对象并不属于自己定义的类而是Object不推荐使用
function Person(name,age) {
var o = new Object(); //Object创建对象
o.name = name;
o.age = age;
return o;
}
var per1 = Person("tian",11);
per1 instanceof Person; //false
per1 instanceof Object; //true
构造模式:自定义构造函数,从而自定义对象类型的属性和方法,使用new操作符,创建一个对象;但是缺点很明显,类方法要为每个函数重新创建一遍,不能共享。
function Person(name,age) {
this.name = name;
this.age = age;
this.run = function() { //该方法并不能共享,而是每个实例都要有一个
console.log(this.name + "在走路");
}
}
var per1 = new Person("tian", 12);
原形模式:声明一个构造函数,利用构造函数的prototype属性为该构造函数定义原形属性(原形prototype是Javascript核心特性之一,设计的目的就是用来实现继承的,从予以角度分析,prototype就是构造类拥有的原始成员。注意对象是没有原形的,只有构造函数拥有原形);
function Person() {
}
Person.prototype = {
name : "tian",
age : 21,
run : function() {
console.log(this.name + "在跑步");
}
}
var per1 = new Person("tian", 12);
一般的我们把定义在原形上的方法叫做原形方法,他们被所有对象共享,也就是只有一份。这样就解决了构造模式的缺点,但是又没有特权方法||属性。于是,我们参考以上这几种方法,就可以得到目前最常用的类的创建方法:
组合模式(构造原形模式): 可以看出这是原型模式和构造模式的组合,构造函数中我们放入特权属性和特权方法,他们每一个实例就是一个副本,互不影响。在内部还可以放入var 声明的变量作为私有属性。把公共的方法给原形,这样就可以通用。
function Person(name, age) {
this.name = name; //特权方法
this.age = age;
var _idNum = 0; //该属性无法被访问到
}
Person.prototype = {
run : function() {
conosole.log(this.name + "在跑步"); //公共方法
}
}
var per1 = new Person("tian", 12);
原形方法和特权方法都属于实例方法,还有一种类方法或者类属性,我们直接在函数上定义就可以:Person.method = function(){}
。遵循对象设计原则,类的所有成员都应该封装在类的结构体内,因此优化该模式,产生动态原形模式。
动态原形模式:把所有信息放在构造函数中,并且动态的判断是否具有某方法并创建
function Person(name, age) {
this.name = name;
this.age = age;
if(typeof this.run === 'undefined') {
Person.prototype.run = function() {
console.log(this.name + "在跑步");
}
}
}
寄生构造函数:类似工厂模式,将工厂中的对象实例o,换成指定构造器生成,常用来扩展一些JS本身构造函数,又不希望直接修改构造函数时候使用。比如扩展Array构造函数
稳妥构造函数模式:一般用于安全的环境中。最后这两种方法直接十分类似,也不太常用,具体代码就不贴了都和工厂模式长得很像,有兴趣的自行查找。
二、Prototype原形与new操作符
在讨论继承之前,我们先探索一下原形链和new操作符在创建对象的时候的步骤。
首先:我们知道在当我们访问对象的一个属性或方法的时候,那么他会先找特权成员,如果有同名的就返回,没有就查找原形,在没有查找父类原形。我们通过组合模式创建的对象都有父类:Object。这种原形链的查找方式我们看看在修改prototype的时候会发生什么:
function Person() {}
Person.prototype = {
name : "tian"
}
var per1 = new Person();
console.log(per1.name) //tian
Person.prototype = {
name : "hyang" //原形链断开,重写
}
console.log(per1.name) //tian 不受影响
function Teacher(){}
Teacher.prototype = {
name : "qiqi"
}
per1.constructor = Teacher;
console.log(per1.name) //tian 依然不受影响
通过上述代码我们发现,当重写类的原形链的时候,已经生成的实例并不受任何影响,修改construct属性(该属性表示对象的构造函数),也没有任何效果。那么究竟什么才是对象回溯的依据呢?其实每个对象都有一个 __proto__
属性。该属性保存着对象指向的原形。
console.log(per1.__proto__) //Object {name: "tian"}
per1.__proto__ = Person.prototype; //修改__proto__属性
console.log(per1.name) //hyang
per1.__proto__ = Teacher.prototype
console.log(per1.name) //qiqi
在IE11以后和标准浏览器中该属性可以修改访问,之前的该属性不暴露。于是我们得出new操作符在执行的时候过程(参考Person类):
1. 创建一个空对象obj
2. `obj.__proto__ = Person.prototype` (引用)
3. 将构造器中`this = obj`
4. 执行构造器里面的代码
5. 判断有没有返回值,没有返回值默认undefined,有返回值且为复合类型则返回该类型,否则返回this如果通过new生成对象的时候,忘记加上new了,那么属性会保存在哪儿里呢?
function Person(name) {
this.name = name;
}
var per = Person("tian");
per //'undefined'
console.log(window.name) //'tian'
可以看到没有加new操作符,Person中的this会被解释称window对象,全局变量就很容易受到污染,谨慎使用new操作符。
三、继承
Javascript实现继承,通过上面我们知道只要prototype有什么,那么实例就有什么;如果我们将类prototype置换为另一类的prototype,那么该类就可以轻易得到类的原型成员。但是由于对象是引用类型,所以不能直接替换:
function Person(){} //父类
Person.prototype = {
"name" : "tian"
}
function Teacher(){} //子类
Teacher.prototype = Person.prototype;
这样Teacher的原形保存的是对Person的引用,修改Teacher会同时修改Person。解决的方法有两个,一个是通过for in把父类原型逐一赋给子类的原形(拷贝继承);第二种现将父类的原形赋给一个函数,然后将该函数的实例作为子类的原形。
方法1:
function extend(child, super) {
for(var property in super) {
child[property] = super[property];
}
return child;
}
该方法有一个缺陷,就是无法通过instanceof验证。
方法2:原型继承
function Person(name){
this.name = name;
}
Person.prototype = {
"run" : function(){
console.log(this.name + "跑步")
}
}
function birdge(){}
birdge.prototype = Person.prototype;
function Teacher(name, wage){
this.name = name;
this.wage = wage;
}
Teacher.prototype = new birdge();
var per = new Person("tian");
var tea = new Teacher("hyang",10);
Person.prototype === Teacher.prototype; // false 说明原型已经分离
Person.prototype.say = function(){
console.log(this.name)
} //为父类添加一个方法
tea.say() //hyang 说明子类得到了父类新添加的方法
Teacher.prototype.getWage = function(){
console.log(this.wage);
}
per.getWage; //'undefined' 说明子类添加的方法并没有影响到父类
这样我们就完成了类的原型继承,我们给子类原形添加的新方法,其实是保存在了生成的new bridge()那个对象中(Teacher的原型保存的是对new bridge这个对象的引用)
per.__proto__ //Object {run: function, say: function}
tea.__proto__ //birdge {getWage: function, run: function, say: function}
tea.__proto__.__proto__ //Object {run: function, say: function}
这里我们可以清除的看到对象各自的原型是什么,中介函数bridge的使用在ES5中有更加简单的方法 Object.create(原型)这样就可以创建出一个具有指定原形的对象。对于不支持Object.create我们可以自己定义该函数
if(!Object.create){
Object.create= (function(){
function F(){} //创建中介函数(bridge)
return function(obj) {
if(arguments.length !== 1) {
throw new Error("仅支持一个参数");
}
F.prototype = obj; //原形绑定
return new F(); //返回实例
}
})()
}
这样上述继承就可以改写成,Teacher.prototype = Object.create(Person.prototype)`。 对于特权属性和函并没有在原型链中,我们可以采用借用构造函数来继承,于是上面Teacher子类修改为
function Teacher(name, wage) {
Person.call(this, name); //调用父类的构造方法,实现特权函数继承;
this.wage = wage;
}
四、类工厂
为了简化类的生成和继承问题,主流框架都引入了一个专门的方法,只要用户传入相应的参数或者按一定的简单格式就能创建一个类,或者子类。随着ES5新增Object方法的出现,类工厂也在改变着。下一篇,对一些类工厂的源码进行分析。