Javascript 类工厂Ⅰ - 类与继承

原文作者: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方法的出现,类工厂也在改变着。下一篇,对一些类工厂的源码进行分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值