前言

说到面向对象,可能第一想到的是C++或者Java这样的语言。这些语言有都一个标志,那就是引入了类的概念。我们可以通过类创建任意数量的具有相同属性和方法的对象。ECMAScript(JavaScript分为ECMAScript、DOM和BOM)中没有类的概念,所以它的对象相比较基于类的语言还是有所不同的。

说说对象的属性

 var person={
        name:'张三',
        age:23,
        sex:'男',
        sayName:function () {
            alert(this.name);
        }
    }

上面我们用了对象字面量的方式建了一个非常简单的person对象,他拥有name、age、sex、sayName这些属性,而这些属性在创建时都带有一些特征值,JavaScript通过这些特征值就可以定义这些属性的行为。在ECMAScript中,属性分为两种:数据属性和访问器属性。下面我们一一学习下。

数据属性

数据属性有四个特征值,分别为如下四个:

  1. configurable
    表示能否delete删除属性,能否修改属性的特性,默认值是false

  2. enumerable
    表示能否通过for-in循环返回属性,默认值是false

  3. writable
    表示能否修改属性的值,默认值是false

  4. value
    表示该属性的值,我们读取和修改都是在这个位置,默认值是undefined

接下来我们一一理解这四个特征值。要修改属性默认的特性,必须使用ECMAScript 5的Object.defineProperty()方法

configurable

1.delete无效
    var person={};
    Object.defineProperty(person,"name",{
        configurable:false,
        value:"张三"
    });
    console.log(person.name);//张三
    delete person.name;
    console.log(person.name);//张三
2.不能修改属性的特性
    var person={};
    Object.defineProperty(person,"name",{
        configurable:true,
        value:"张三"
    });
    Object.defineProperty(person,"name",{
        value:"李四"
    });
    console.log(person.name);//李四
    var person={};
    Object.defineProperty(person,"name",{
        configurable:false,
        value:"张三"
    });
    Object.defineProperty(person,"name",{
        value:"李四"
    });
    console.log(person.name);
    
    //控制台报错 Uncaught TypeError: Cannot redefine property: name

enumerable

    var person={};
    Object.defineProperty(person,"name",{
        enumerable:true,
        value:"张三"
    });
    Object.defineProperty(person,"age",{
        value:"23"
    });
    Object.defineProperty(person,"sayName",{
        enumerable:true,
        value:function () {
            alert(this.name);
        }
    });
    for( var prop in person){
        console.log(prop);
    }
    //控制台输出 name和sayName

writable

    var person={};
    Object.defineProperty(person,"name",{
        writable:false,
        value:"张三"
    });
    console.log(person.name);//张三
    person.name="李四";
    console.log(person.name);//张三

value

    var person={};
    Object.defineProperty(person,"name",{
        writable:true,
        value:"张三"
    });
    console.log(person.name);//张三
    person.name="李四";
    console.log(person.name);//李四

访问器属性

访问器属性有四个特征值,分别为如下四个:

  1. configurable
    表示能否delete删除属性,能否修改属性的特性,默认值是false

  2. enumerable
    表示能否通过for-in循环返回属性,默认值是false

  3. get
    在读取属性调用的函数,默认值是undefined

  4. set
    在设置属性调用的函数,默认值是undefined

下面我们一一了解一下访问器属性的特征值,其中configurable和enumerable与上面数据类型一样,这里我们就不多做介绍,主要我们说一下get和set。

 var person={
        name:"张三",
        age:32
    };
    Object.defineProperty(person,"sayAge",{
        get:function () {
            return this.name+":"+this.age+"岁";
        },
        set:function (newAge) {
            console.log("想要重返"+newAge+"岁?不存在的!");
        }
    });
    console.log(person.sayAge);//张三:32岁
    person.sayAge=18;//想要重返18岁?不存在的!
    console.log(person.sayAge);//张三:32岁

get和set并非需要同时都要指定。如果只指定get,那么这个属性就是不可写的;如果只指定set,那么这个属性就是不可读的。

 var person1={
        name:"张三",
        age:32
    };
    Object.defineProperty(person1,"sayAge",{
        get:function () {
            return this.name+":"+this.age+"岁";
        }
    });
    console.log(person1.sayAge);//张三:32岁
    person1.sayAge=18;
    console.log(person1.sayAge);//张三:32岁

    var person2={
        name:"李四",
        age:46
    };
    Object.defineProperty(person2,"sayAge",{
        set:function () {
            console.log("想要重返18岁?不存在的!");
        }
    });
    console.log(person2.sayAge);//undefined
    person2.sayAge=18;//想要重返18岁?不存在的!
    console.log(person2.sayAge);//undefined

定义多个属性

这个里我们就要说一个Object.defineProperties()方法,具体用下看如下示例:

    var person = {};
    Object.defineProperties(person, {
        name: {
            writable: true,
            value: "张三"
        },
        age: {
            enumerable: true,
            value: 23,
        },
        sayName: {
            get: function () {
                return this.name;
            },
            set: function (newName) {
                console.log("名字修改完成");
                this.name=newName+"(修改)";
            }
        }
    });

读取属性的特性

这里我们可以正好验证我们前面所有默认值。

    var person={};
    Object.defineProperty(person,'name',{
        value:"张三"
    });
    var descriptor=Object.getOwnPropertyDescriptor(person,"name");
    console.log("configurable:"+descriptor.configurable);
    //configurable:false
    console.log("enumerable:"+descriptor.enumerable);
    //enumerable:false
    console.log("writable:"+descriptor.writable);
    //writable:false
    console.log("value:"+descriptor.value);
    //张三

创建对象

字面量模式

    var person={};
    person.name="张三";
    person.age=22;
    person.sex="男";
    person.sayName=function () {
        alert(this.name);
    }

优点:创建单个对象简单方便
缺点:创建多个相似对象会产生大量代码

工厂模式

    function createPerson(name, age, sex) {
        var person = new Object();
        person.name = name;
        person.age = age;
        person.sex = sex;
        person.sayName = function () {
            alert(this.name);
        };
        return person;
    }
    var person=createPerson("张三",22,"男");

优点:可以快速创建多个相似对象
缺点:无法进行对象的识别

构造函数模式

     function Person(name, age, sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.sayName = function () {
            alert(this.name);
        };
    }
    var person=new Person("张三",22,"男");

以构造函数的方式创建对象要经历下面四个步骤:

  1. 创建一个新对象

  2. 将构造函数的作用域赋给新对象(因此this指向这个新对象)

  3. 执行构造函数中的代码,为这个新对象添加属性

  4. 返回新对象

这里person有一个constructor(构造函数)属性指向Person,我们可以验证一下。

    alert(person.constructor===Person);//true

鉴于这个特性我们可以用constructor来验证对象的类型。除了这个,我们还可以利用instanceof。

    alert(person instanceof Person);//true

虽然我们使用构造函数模式可以进行对象的识别,但是构造函数模式却有一个缺点,就是每个方法都要在每个实例上重新创建一遍。下面我们举个例子说明一下。

    function Person(name, age, sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.sayName = function () {
            alert(this.name);
        };
    }
    var person1=new Person("张三",22,"男");
    var person2=new Person("李四",25,"男");
    alert(person1.sayName===person2.sayName);//false

从上面的例子我们看出来,person1和person2的sayName函数并非共用同一个。

优点:可以进行对象的识别
缺点:构造函数里面的函数在实例化的时候都需要每次都创建一遍,导致不同作用域链和标识符解析。

原型模式

   function Person() {

   }
   Person.prototype.name="张三";
   Person.prototype.age=22;
   Person.prototype.sex="男";
   Person.prototype.sayName=function () {
       alert(this.name);
   };
   var person=new Person();

任何时候我们只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性这个属性指向函数的原型对象。默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性会执行prototype属性所在函数。以上面这个例子为例,即:

Person.prototype.constructor===Person//true

另外我们的实例对象都会有一个__proto__属性指向构造函数的原型对象。即:

person.__proto__===Person.prototype //true

接下来我们要说的一点是我们可以通过对象实例访问保存在原型中的值,但是不能通过对象实例重写原型中的值。下面我们看个例子:

   function Person() {

   }
   Person.prototype.name="张三";
   Person.prototype.age=22;
   Person.prototype.sex="男";
   Person.prototype.sayName=function () {
       alert(this.name);
   };
   var person1=new Person();
   var person2=new Person();
   person1.name='李四';
   alert(person1.name);//李四
   alert(person2.name);//张三

从上面的例子我们可以看出,我们修改了person1的name属性实际是实例对象person1中的属性,而不是Person.prototype原型对象。如果我们想要person1.name指向Person.prototype.name则需要删除实例对象person1中name属性,如下所示:

   delete person1.name;
   alert(person1.name);//张三

说到这里我们遇到一个一个问题,就是如何判断一个属性在原型上还是在实例对象上?这个是有方法可以做到的,那就是hasOwnProperty()方法,接着上面的代码,我们可以用这个hasOwnProperty()方法去验证一下。

   alert(person1.hasOwnProperty("name"));//false

上面我们删除实例对象person1中name属性之后,name应该不属于实例对象person1的属性,所以hasOwnProperty()返回false.
如果只是想知道person1能否访问name属性,不论在实例对象上还是原型上的话,我们可以用in操作符。如下所示:

   alert("name" in person1);//true

相对上面的原型语法,我们有一个相对简单的原型语法。

    function Person() {

    }
    Person.prototype = {
        constructor:Person,
        name: '张三',
        age: 22,
        sex:"男",
        sayName: function () {
            alert(this.name);
        }
    };

这里注意的是,需要重新设置constructor为Person,否则constructor指向Object而不是Person。但是这样有一个缺点,就是constructor的enumerable特性被设为true。导致constructor属性由原本不可枚举变成可枚举。如果想解决这个问题可以尝试这种写法:

    function Person() {

    }
    Person.prototype = {
        name: '张三',
        age: 22,
        sex:"男",
        sayName: function () {
            alert(this.name);
        }
    };
    Object.defineProperty(Person.prototype,"constructor",{
        enumerable:false,
        value:Person
    });

说完这个之后,我们来说一下原型的动态性。由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来。我们看一下下面的例子:

   function Person() {

   }
   var person=new Person();
   person.name="张三";
   person.sayName=function () {
     alert(this.name);
   };
   person.sayName();//张三

但是在重写整个原型对象的时候情况就不一样了,我们看一下下面这个例子:

    function Person() {

    }
    var person = new Person();
    Person.prototype = {
        constructor:Person,
        name: '张三',
        age: 22,
        sayName: function () {
            alert(this.name);
        }
    };
    person.sayName();
    //Uncaught TypeError: person.sayName is not a function

重写原型对象会切断现有原型与任何之前已经存在的对象实例之间的联系,引用的仍然是最初的原型,上面的例子由于最初的原型的没有sayName()方法,所以会报错。

优点:可以进行对象的识别,以及实例对象的函数不会被重复创建,从而不会导致不同的作用域链。
缺点:省略了为构造函数传递初始化参数这一环节,所有实例在默认情况都取相同的值。

组合使用构造函数模式和原型模式

    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    Person.prototype = {
        constructor:Person,
        sayName: function () {
            alert(this.name);
        }
    };
    var person1 = new Person('张三', 22);
    var person2 = new Person("李四", 23);
    alert(person1.sayName===person2.sayName);//true

优点:结合构造函数模式和原型模式的优点,是目前使用最广泛、认同度最高的一种创建自定义类型的方法。

动态原型模式

    function Person(name,age,sex) {
        this.name=name;
        this.age=age;
        this.sex=sex;
        if(typeof this.sayName!="function"){
            Person.prototype.sayName=function () {
              alert(this.name);
            };
        }
    }
    var person=new Person("张三",22,"男");
    person.sayName();//张三

上面代码中if语句只有在初次调用构造函数时才会执行。此后,原型已经初始化,不需要再做什么修改了。
优点:保留了构造函数模式和原型模式的优点,又将所有信息封装信息封装在了构造函数中。

寄生构造函数模式(了解即可)

    function Person(name,age,sex) {
        var object=new Object();
        object.name=name;
        object.age=age;
        object.sex=sex;
        object.sayName=function () {
            alert(this.name);
        };
        return object;
    }
    var person=new Person("张三",22,"男");
    person.sayName();//张三

由于我们可以重写调用构造函数时的返回值,所以我们可以在特殊情况下为对象创建构造函数。例如我们想创建一个具有特殊方法的数组,由于我们不能修改Array构造函数,因此可以使用这种方式。

    function SpecialArray() {
        var values=new Array();
        values.push.apply(values,arguments);
        values.toPipedString=function () {
            return this.join("|");
        };
        return values;
    }
    var colors=new SpecialArray("red","yellow","white");
    alert(colors.toPipedString());//"red|yellow|white"

但这种方式缺点也是很明显,由于构造函数返回的对象与构造函数外部创建的对象没有什么不同。所以,instanceof操作符不能确定对象类型。因此这种模式优先级很低,不推荐优先使用。
优点:可以重写构造函数函数的返回值,特殊情况下比较好用。
缺点:instanceof操作符不能确定对象类型。

稳妥构造函数模式

    function Person(name) {
        var object=new Object();
        var name=name;
        object.sayName=function () {
            alert(name);
        };
        return object;
    }
    var person=new Person("张三",22,"男");
    person.sayName();//张三
    alert(person.name);//undefined

在这种模式下,想访问name这个数据成员时,除了调用sayName()方法,没有其他方法可以访问传入构造函数中的原始数据。这种模式的安全性就很高。
优点:安全性高。
缺点:instanceof操作符不能确定对象类型。

继承

原型链

    function SuperType() {
        this.property=true;
    }
    SuperType.prototype.getSuperValue=function () {
        return this.property;
    };
    function SubType() {
        this.subproperty=false;
    }
    //继承SuperType
    SubType.prototype=new SuperType();
    SubType.prototype.getSubValue=function () {
        return this.subproperty;
    };
    var instance=new SubType();
    alert(instance.getSuperValue());

上面代码中,我们没有使用SubType默认提供的原型,而是给它换了一个新原型(SuperType实例)。于是,新原型不仅具有作为SuperType的实例所拥有的全部属性和方法,而且其内部还拥有一个指针,指向了SuperType的原型。最终结果如下:

图片描述

根据上面,需要说明一点的是所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针指向Object.prototype。

在我们使用原型链的过程会有一个问题就是确定原型和实例之间的关系。这里我们有两种方式,我们接着上面代码继续看。

第一种:instanceof操作符,测试实例与原型中出现过的构造函数

    alert(instance instanceof Object);//true
    alert(instance instanceof SuperType);//true
    alert(instance instanceof SubType);//true

第二种:方法isPrototypeOf(),测试原型链中出现过的原型

   alert(Object.prototype.isPrototypeOf(instance));//true
   alert(SuperType.prototype.isPrototypeOf(instance));//true
   alert(SubType.prototype.isPrototypeOf(instance));//true

如果子类型需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法,我们要遵循一个原则,给原型添加方法的代码一定要放在替换原型的语句之后。如下所示:

    function SuperType() {
        this.property=true;
    }
    SuperType.prototype.getSuperValue=function () {
        return this.property;
    };
    function SubType() {
        this.subproperty=false;
    }
    //继承SuperType
    SubType.prototype=new SuperType();
    //添加新方法
    SubType.prototype.getSubValue=function () {
        return this.subproperty;
    };
    //重写超类型中的方法
    SubType.prototype.getSuperValue=function () {
        return false;
    };
    var instance=new SubType();
    alert(instance.getSuperValue());

另外我们还需要注意在添加方法时候,不能使用对象字面量创建原型方法。如下所示:

    function SuperType() {
        this.property=true;
    }
    SuperType.prototype.getSuperValue=function () {
        return this.property;
    };
    function SubType() {
        this.subproperty=false;
    }
    //继承SuperType
    SubType.prototype=new SuperType();
    //使用字面量添加新方法,会导致上一行代码无效
    SubType.prototype={
        getSubValue:function () {
            return this.subproperty;
        }
    };
    var instance=new SubType();
    alert(instance.getSuperValue());
    //Uncaught TypeError: instance.getSuperValue is not a function

说到这里我们,我来总结一下原型链的优缺点:
优点:功能很强大,可以连续继承多个原型的全部属性和方法。
缺点:

1.原型的通用问题就是属性被共用,修改原型的属性将会动态映射到所有指向该原型的实例。
2.鉴于属性是共用的,我们无法给超类型的构造函数传递参数。

借用构造函数

    function SuperType() {
        this.colors=["red","blue","green"];
    }
    function SubType() {
        //继承了SuperType
        SuperType.call(this);
    }
    var instance1=new SubType();
    instance1.colors.push("black");
    console.log(instance1.colors);//["red", "blue", "green", "black"]
    var instance2=new SubType();
    console.log(instance2.colors);//["red", "blue", "green"]

通过使用call()方法(或apply()方法),在新建的SubType实例的环境下条用了SuperType构造函数。这样一来,就会在新的SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的colors属性的副本了。

相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。如下所示:

    function SuperType(name) {
        this.name=name;
    }
    function SubType() {
        //继承了SuperType,同时还传递了参数
        SuperType.call(this,"张三");
        //实例属性
        this.age=22;
    }
    var instance=new SubType();
    alert(instance.name);//张三
    alert(instance.age);//22

优点:弥补原型链的共用属性和不能传递参数的缺点。
缺点:函数不能复用,超类型的原型中定义的方法在子类型中是不可见的。

组合继承

    function SuperType(name) {
        this.name=name;
        this.colors=["red","blue","green"];
    }
    SuperType.prototype.sayName=function () {
        alert(this.name);
    };
    function SubType(name,age) {
        SuperType.call(this,name);//第二次调用SuperType()
        this.age=age;
    }
    //继承方法
    SubType.prototype=new SuperType();
    SubType.prototype.constructor=SubType;
    SubType.prototype.sayAge=function () {
      alert(this.age);
    };
    var instance1=new SubType("张三",22);//第一次调用SuperType()
    instance1.colors.push("black");
    console.log(instance1.colors);//["red", "blue", "green", "black"]
    instance1.sayName();//张三
    instance1.sayAge();//22


    var instance2=new SubType("李四",25);
    console.log(instance2.colors);//["red", "blue", "green"]
    instance2.sayName();//李四
    instance2.sayAge();//25
    

缺点:创建对象时都会调用两次超类型构造函数。
优点:融合了原型链和借助构造函数的优点,避免了他们的缺陷。Javascript中最常用的继承模式。

原型式继承

    var person={
        name:"张三",
        friends:["李四","王五"]
    };
    var person1=Object(person);//或者Object.create(person)
    person1.name="赵六";
    person1.friends.push("孙七");
    var person2=Object.create(person);
    person2.name="周八";
    person2.friends.push("吴九");
    console.log(person.friends);//["李四", "王五", "孙七", "吴九"]

原型式继承实际上是把实例的__proto__属性指向了person。

优点:只想让一个对象跟另一个对象保持相似的情况下,代码变得很简单。
缺点:共享了相应的值,原型的通病。

寄生式继承

    function createPerson(obj) {
        var clone=Object(obj);
        clone.sayMyfriends=function () {
            console.log(this.friends);
        };
        return clone;
    }
    var person={
        name:"张三",
        friends:["李四","王五","赵六"]
    };
    var anotherPerson= createPerson(person);
    anotherPerson.sayMyfriends();//["李四", "王五", "赵六"]

优点:可以为任意对象添加指定属性,代码量很少。
缺点: 在为对象添加函数,由于函数不能复用。每次添加都会新建一个函数对象,降低了效率。这一点与构造函数模式类似。

寄生组合式继承


    function inheritPrototype(subType,superType) {
        var prototype=Object(superType.prototype);//创建对象
        prototype.constructor=subType;//增强对象
        subType.prototype=prototype;//指定对象
    }
    function SuperType(name) {
        this.name=name;
        this.colors=["red","blue","green"];
    }
    SuperType.prototype.sayName=function () {
        alert(this.name);
    };
    function SubType(name,age) {
        SuperType.call(this,name);
        this.age=age;
    }
    inheritPrototype(SubType,SuperType);
    SubType.prototype.sayAge=function () {
        alert(this.age);
    };
    var instance1=new SubType("张三",22);
    instance1.colors.push("yellow");
    instance1.sayName();//张三
    instance1.sayAge();//22

    var instance2=new SubType("李四",25);
    console.log(instance2.colors);// ["red", "blue", "green"]
    instance2.sayName();//李四
    instance2.sayAge();//25

上面的inheritPrototype()函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一部是创建超类型原型的一个副本。第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以用调用inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句。
优点:集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方式。


wuming
3.2k 声望788 粉丝

Success is not final, failure is not fatal, it is the courage to continue that counts.