Javascript 面向对象程序设计

1、理解对象

两个属性类型

一、数据属性

数据属性有四个描述其行为的特性:bash

  • configurable:表示可否用delete关键字删除;可否修改属性的特性,或者修改成访问器属性。经过对象字面量定义的属性,该特性默认为true。
  • enumerable:可否经过for...in遍历到该属性。经过对象字面量定义的属性,该特性默认为true。
  • writable:可否修改属性的值。经过对象字面量定义的属性,该特性默认为true。
  • value:该属性的值。默认值为undefined。

二、访问器属性

访问器属性有四个描述其行为的特性:app

  • configurable:表示可否用delete关键字删除;可否修改属性的特性,或者修改成访问器属性。经过对象字面量定义的属性,该特性默认为true。
  • enumerable:可否经过for...in遍历到该属性。经过对象字面量定义的属性,该特性默认为true。
  • get:读取属性时调用该函数,默认为undefined。
  • set:写入属性时调用该函数,默认为undefined。

定义属性以及属性类型

一、定义单个属性

var person = {};
Object.defineProperty(person,'name',{
    configurable:true,
    enumerable:true,
    writable:true,
    value:'Eallon',
})
复制代码

值得注意的是,若是将configurable的值设置为false,那么该属性将永远不能再设置成true了。函数

二、定义多个属性

var hero = {};
Object.defineProperties(person,{
    //不可修改的属性
    name:{
        writable:false, 
        value:'VN'
    },
    // 能够修改的属性
    position:{
        writable:true,
        value:'ADC'
    },
    // 前面加"_"的属性,咱们通常约定为内部访问的属性
    _attackSpeed:{
      writable:true,
      value:0.8
    },
    // 访问器属性
    attackSpeed:{
        get:function(){
            return this._attackSpeed;
        },
        set:function(newValue){
            this._attackSpeed = newValue > 2.5 ? 2.5 : newValue;
        }
    }
})
复制代码

读取属性的特性

var descriptor = Object.getOwnPropertyDescriptor(hero,'attackSpeed');
 console.log(descriptor);
复制代码

2、建立对象

工厂模式

工厂模式,将建立对象的代码封装在一个简单的函数中,而后返回新建立的对象。ui

function createHero(name,position){
    var hero = new Object();
    hero.name = name;
    hero.position = position;
    hero.say = function(){
        console.log("I am " + this.name);
    };
    return hero;
}
var VN = createHero('VN','ADC');
var EZ = createHero('EZ','Mid');
复制代码

优势:一套代码能够建立多个类似的对象。this

缺点:建立的对象都是属于Object类,没法标识其具体类型。spa

构造函数模式

function Hero(name,position){
        this.name = name;
        this.position = position;
        this.say = function () {
            console.log("I am " + this.name);
        }
    }
    var VN = new Hero("VN","ADC");
    var EZ = new Hero("EZ","Mid");
复制代码

优势:使用构造函数建立的对象已经能区分对象类型了,如VN和EZ都是Hero的实例。prototype

缺点:相同的方法,在多个实例上要重复建立,如say方法。 指针

原型模式

function Hero(){

    }
    Hero.prototype.name = "VN";
    Hero.prototype.position = "ADC";
    Hero.prototype.sup = ['蕾欧娜','布隆'];
    Hero.prototype.say = function () {
        console.log("我叫" + this.name + ",个人辅助们: "+ this.sup.toString() );
    };
    var VN = new Hero();
    var EZ = new Hero();

    EZ.sup.push('拉克丝');

    EZ.name = "EZ";

    VN.say();   // 我叫VN,个人辅助们: 蕾欧娜,布隆,拉克丝
    EZ.say();   // 我叫EZ,个人辅助们: 蕾欧娜,布隆,拉克丝
复制代码

在构造函数的prototype上定义的属性是全部实例共用的,每一个实例都有一个内部指针(__proto__)来指向构造函数的prototype对象。也就是说,构造函数的prototype与全部实例的__proto__是指向同一个内存地址的。code

将共用的方法定义到prototype上是再合适不过了,这样就解决了构造函数中定义的方法在实例初始化时重复建立的问题,如今全部的实例都统一使用原型上的say方法,你们都不须要再本身单首创建say方法了。cdn

可是,若是将一个引用类型的属性定义到prototype上会存在隐患,好比EZ将“拉克丝”加入到了辅助列表,结果VN获取辅助列表时发现本身也有“拉克丝”了,这是由于EZ将“拉克丝”加入到prototype的sup中了,全部的实例都会受到影响。

固然,若是只是基本类型的属性,却是问题也不大,好比EZ定义了name="EZ",其实EZ只是在本身的实例上定义了一个新的name属性,而并无更改prototype中的name属性,prototype的name属性仍是VN。

组合使用构造函数 + 原型模式(最经常使用)

单独使用构造函数或原型模式都会有一些问题,可是将二者结合使用取长补短,则能够完美解决对象的建立问题,咱们把上面的例子用组合方式再写一遍。

function Hero(name,position){
        this.name = name;
        this.position = position;
        this.sup = ['蕾欧娜','布隆'];
    }

    Hero.prototype.say = function () {
        console.log("我叫" + this.name + ",个人辅助们: "+ this.sup.toString() );
    };
    var VN = new Hero('VN','ADC');
    var EZ = new Hero('EZ','Mid');

    EZ.sup.push('拉克丝');

    VN.say();   // 我叫VN,个人辅助们: 蕾欧娜,布隆
    EZ.say();   // 我叫EZ,个人辅助们: 蕾欧娜,布隆,拉克丝
复制代码

如今,咱们在构造函数中定义实例特有的属性(name、position、sup),在构造函数的prototype上定义全部实例共用的方法(say)。VN和EZ本身维护本身的辅助们,互不干涉,可是他们可使用共同的say方法。

动态原型模式

function Hero(name,position){
        this.name = name;
        this.position = position;
        this.sup = ['蕾欧娜','布隆'];
        if(typeof this.say !== "function"){
            Hero.prototype.say = function () {
                console.log("我叫" + this.name + ",个人辅助们: "+ this.sup.toString() );
            };
        }
    }
    
    var VN = new Hero('VN','ADC');
    var EZ = new Hero('EZ','Mid');

    EZ.sup.push('拉克丝');

    VN.say();   // 我叫VN,个人辅助们: 蕾欧娜,布隆
    EZ.say();   // 我叫EZ,个人辅助们: 蕾欧娜,布隆,拉克丝
复制代码

动态原型模式,将关于prototype的定义写到了构造函数中,这样的好处是看起来封装性和总体性更好一点。

在构造函数中,先判断say函数是否已经存在,也就是说say函数是在VN实例化的时候建立到prototype中的,等到EZ实例化的时候,prototype中已经存在say函数了,就不会再次建立了。

寄生构造函数模式

function Hero(name,position){
        var hero = new Object();
        hero.name = name;
        hero.position = position;
        hero.sup = ['蕾欧娜','布隆'];
        hero.say = function () {
            console.log("我叫" + this.name + ",个人辅助们: "+ this.sup.toString() );
        };
        return hero;
    }

    var VN = new Hero('VN','ADC');
    var EZ = new Hero('EZ','Mid');
复制代码

咱们发现Hero函数里面的代码跟工厂模式差很少,惟一不一样的是调用方式不一样,该模式是用new关键字来调用Hero函数的。虽然是用构造函数的方式初始化的实例,可是获得的对象却与构造函数以及构造函数的原型没有任何关系,跟普通对象没有什么区别。因此,尽可能不要用这种方式建立对象,由于彻底没有必要。

稳妥构造函数模式

function Hero(name,position){
        var hero = new Object();
        hero.say = function () {
            console.log("我叫" + name + ",我打" + position);
        };
        return hero;
    }

    var VN = Hero('VN','ADC');
    var EZ = Hero('EZ','Mid');
    VN.say();   // 我叫VN,我打ADC
    EZ.say();   // 我叫EZ,我打Mid
复制代码

稳妥构造函数模式,不使用new和this关键字,直接调用Hero函数。调用Hero时传进去的参数,在返回的hero对象中没法直接访问,只能经过say方法来访问内部的成员变量。


3、继承

原型链

原型链是什么?
每一个实例对象都有一个指向构造函数原型的内部指针(__proto__),实例可使用原型中的属性和方法,若是咱们把子类构造函数的prototype指向父类的一个实例,那么子类实例的__proto__的__proto__就是父类构造函数的prototype,那么子类实例也可使用父类构造函数的prototype中定义的属性和方法了。像这样,实例与构造函数原型之间造成的这个链条就是原型链。

function Hero(name,gender) {
        this.name = name;
        this.gender = gender;
        if(typeof this.say != 'function'){
            Hero.prototype.say = function () {
                console.log("I am " + this.name + ", I am " + this.gender);
            }
        }
    }
    function ADC(name,gender) {
        this.name = name;
        this.gender = gender;
        this.position = "ADC";  // 子类扩展的属性
    }
    ADC.prototype = new Hero();
    var VN = new ADC("VN",'female');
复制代码

使用原型链的继承方式,咱们实现了子类能够复用父类原型上的属性和方法。
可是,咱们仍是会面临如下几个问题:

  • 父类实例上的属性和方法被放到了子类原型上,ADC的原型上有父类的实例属性name和gender。
  • 初始化父类实例给子类原型赋值时,不知道怎么传参数。也所以,ADC原型的name和gender都是undefined。
  • 子类构造函数中重复写了一遍父类构造函数的代码

借用构造函数

function Hero(name,gender) {
        this.name = name;
        this.gender = gender;
        if(typeof this.say != 'function'){
            Hero.prototype.say = function () {
                console.log("I am " + this.name + ", I am " + this.gender);
            }
        }
    }
    function ADC(name,gender) {
        Hero.apply(this,arguments);
        this.position = 'ADC';
    }
    ADC.prototype = new Hero();
    var VN = new ADC("VN",'female');
复制代码

为了解决子类构造函数中重复写父类构造函数代码的问题,咱们在子类ADC的构造函数中用apply或者call方法调用父类构造函数Hero,而后再声明子类ADC本身的属性position。
此处应注意,为了防止调用父类构造函数时覆盖子类本身的属性,咱们应该像上面同样,先调用父类构造函数,再声明子类本身的属性。

组合继承

上面的例子其实就已是组合继承的例子了。咱们在子类的构造函数中调用父类的构造函数来使子类也有父类同样的实例属性,而后将父类的一个实例赋值给子类的原型,从而使子类实例与父类原型之间产生了一条原型链。这样就使得子类既继承了父类原型的属性,也继承了父类实例的属性。
这种组合继承的方式是使用最普遍的,但也仍是没有解决子类原型中出现父类实例属性的问题,虽然这个问题不影响正常使用,但总以为不太完美,下面会有更加完美的解决方案。O(∩_∩)O

原型式继承

function object(o) {
        function F() {}
        F.prototype = o;
        return new F();
    }

    var ADC = {
        position:'ADC',
        say:function () {
            console.log("I am " + this.position);
        }
    };
    var VN = object(ADC);
    VN.name = 'VN';
复制代码

原型式继承的思路是,以一个已知的对象为原型建立一个新的对象,而后在新的对象上能够扩展新的属性。

ECMAscript5 新增的Object.create()方法规范化了原型化继承。该方法接受两个参数,第二个参数是扩展在实例上的属性,第二个参数的格式与Object.defineProperties的第二个参数的格式同样。若是只传第一个参数,则与上面的object函数的行为同样。
上面的object例子,能够这样写:

var ADC = {
        position: 'ADC',
        say: function () {
            console.log("I am " + this.position);
        }
    };
    var VN = Object.create(ADC, {
        name: {
            writable: false,
            value: 'VN'
        }
    });
复制代码

寄生式继承

function createHero(obj) {
    var clone = Object.create(obj);
    clone.attack = function () {
        console.log("走位,A")
    };
    return clone;
}
var hero = {
    name:'hero',
    say: function () {
        console.log(this.name);
    }
};
var vn = createHero(hero);
复制代码

寄生式继承与原型式继承的区别是,在函数内部还扩展了实例属性。

寄生组合式继承

重头戏来了!!!寄生组合式继承是目前最理想的继承方式。我的认为,该方式也是最完美的继承方式,由于该方式基本上解决了以前咱们遇到的全部问题。

// 继承prototype
    function inheritPrototype(Sub, Super) {
        var prototype = Object.create(Super.prototype);
        prototype.constructor = Sub;
        Sub.prototype = prototype;
    }
    
    function Hero(name, gender) {
        this.name = name;
        this.gender = gender;
        if (typeof this.say != 'function') {
            Hero.prototype.say = function () {
                console.log(this.name);
            }
        }
    }
    
    function ADC(name, gender) {
        Hero.apply(this, arguments);
        this.position = "ADC";
    }

    inheritPrototype(ADC, Hero);
    
    ADC.prototype.attack = function () {
        console.log("ADC要走A");
    };
    
    var vn = new ADC("vn", 25);
复制代码

该模式与组合式继承相比的精髓之处体如今inheritPrototype函数中,此处咱们再也不是实例化一个Hero实例直接赋值给ADC的prototype,由于这样作的话,ADC的prototype中会出现Hero实例的属性,这不是咱们想要的。咱们是这样作的,咱们以寄生的方式获得一个以Hero的prototype对象为原型的实例对象,该对象没有实例方法,内部指针指向Hero的prototype。而后咱们将该实例对象的constructor属性指向ADC,再而后赋值给ADC的prototype,这样就完美实现了原型的继承。

上面代码中的Object.create方法的调用是关键一环,那它作了什么?
首先它以Hero的prototype对象为原型建立了一个构造函数,而后用新建立的构造函数实例化一个对象,那么该实例对象的内部指针__proto__是指向Hero的prototype的(这是咱们的目的),而后把该对象返回。咱们拿到该对象后,要把该对象的constructor指向子类ADC,由于此时该对象的constructor是指向父类Hero的。再而后,就是把该实例对象赋值给ADC的prototype即实现了原型的继承。(此处很重要,因此啰嗦了好几遍O(∩_∩)O)

总结下寄生组合继承的思路:

  1. 经过在子类构造函数中调用父类构造函数来继承实例属性,使用apply或者call方法实现。
  2. 获取一个以父类prototype对象为原型的实例对象(使用Object.create方法实现),将该对象的constructor属性指向子类构造函数(由于此时该属性的constructor仍是指向父类构造函数的)。
  3. 将第2步获得的实例对象赋值给子类的prototype,实现原型的继承。
相关文章
相关标签/搜索