《JavaScript高级程序设计》笔记:面向对象的程序设计(六)

面向对象的语言有一个标志,那就是它们都有类的概念,而经过类能够建立任意多个具备相同属性和方法的对象。javascript

理解对象

建立自定义对象的最简单的方法就是建立一个Object的实例,而后再为它添加属性和方法。例如:java

var person = new Object();
    person.name="Nicholas";
    person.age=29;
    person.job="Software Engineer";
    person.SayName=function(){
        alert(this.name);
    }

一样上面的例子能够经过对象字面量语法写成以下:chrome

var person ={
        name:"Nicholas",
        age:29,
        person.job:"Software Engineer",
        SayName:function(){
            alert(this.name);
        }
    }

属性类型

ECMAScript中有两种属性:数据属性和访问器属性。数组

1.数据属性

数据属性包含一个数据值的位置。在这个位置能够读取和写入值。数据属性有四个描述其行为的特性。浏览器

Configurable:表示可否通delete删除属性从而从新定义属性,可否修改属性的特性,或者可否把属性修改成访问器属性。像前面的例子中那样直接在对象上定义属性,它们的这个特性默认值为true。app

Enumerable:表示可否经过for-in循环返回属性。像前面的例子中那样直接在对象上定义属性,它们的这个特性的默认值为true。函数

Writable:表示可否修改属性的值。前面例子直接在对象上定义的属性,它们的这个特性默认值为true。this

Value:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存到这个位置。这个特性默认值为undefined。spa

对于前面的例子,value特性被设置为特定的值。例如:firefox

var person={
    name="Niceholas"
}

这里建立一个名为name的属性,为它指定的值是"Niceholas"。也就是说value特性将被设置为"Niceholas",而对这个值的任何修改都将反映在这个位置。

要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性名字和一个描述符对象。其中,描述符对象的属性必须是Configurable、Enumerable、Writable、Value。设置其中的一或多个值。能够修改对应的特性值。例如:

var person={};
    Object.defineProperty(person,"name",{
        writable:false,
        value:'Nich'
    });
    
    alert(person.name);//Nich
    person.name="Greg";
    alert(person.name);//Nich

这个例子建立了一个名为name的属性,它的值为Nich是只读的。这个属性的值是不能够修改的,若是尝试为它指定新值,则在非严格模式下,赋值操做将被忽略;在严格模式下,赋值操做将会抛出错误。
相似的规则也适用与不可配置的属性。例如:

var person={};
    Object.defineProperty(person,"name",{
        configurable:false,
        value:'Nich'
    });
    
    alert(person.name);//Nich
    delete person.name;
    alert(person.name);//Nich
    
    

注意:一旦把属性定义为不可配置的,就不能再把它变回可配置了。此时,再调用Object.defineProperty()方法修改除了writable以外的特性,都会致使错误。

var person={};
    Object.defineProperty(person,"name",{
        configurable:false,
        value:'Nich'
    });

    //抛出错误
    Object.defineProperty(person,"name",{
        configurable:true,
        value:'Nich'
    });
    

也就是说,屡次调用Object.defineProperty()方法修改同一个属性,可是把configurable特性设置为false以后就会有限制了。
在调用Object.defineProperty()方法时,若是不指定,configurable、Enumerable和writable特性的默认值为false。多数状况下,可能都没有必要利用Object.defineProperty()方法提供的这些高级功能。不过,理解这些概念对于理解javascript对象却很是有用。

注:IE8是第一个实现Object.defineProperty()方法的浏览器版本。然而,这个版本的实现存在诸多的限制:只能在DOM对象上使用这个方法,并且只能建立访问器属性。因为实现不完全,建议不要在IE8中使用Object.defineProperty()方法。

2.访问器属性
访问器属性不包含数据值;它们包含一对儿getter和setter函数(不过,这两个函数都不是必需的)。

在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器属性有以下4个特性。

  • [Configurable]:表示可否经过delete删除属性从而从新定义属性,可否修改属性的特性,或者可否把属性修改成数据属性。对于直接在对象上定义的属性,这个特性的默认值为true。
  • [Enumerable]:表示可否经过for-in循环返回属性。对于直接在对象上定义的属性,这个特性默认值为true。
  • [Get]:在读取属性时调用的函数。默认值为undefined。
  • [Set]:在写入属性时调用的函数。默认值为undefined。

访问器属性不能直接定义,必须使用Object.defineProperty()来定义。下面例子:

var book={
        _year:2004,
        edition:1
    }
    Object.defineProperty(book,"year",{
        get:function(){
            return this._year;
        },
        set:function(newValue){
            console.log(newValue);
            if(newValue>2004){
                this._year=newValue;
                this.edition+=newValue-2004;
            }
        }
    });
    book.year=2005;
    console.log(book.edition);//2

//上面代码建立了一个book对象,并给它定义两个默认的属性:_year和edition。_year前面的下划线是一种经常使用的记号,用于表示只能经过对象方法访问的属性。


//支持ECMAScript5的这个方法的浏览器有IE9+、Firefox4+、SaFari5+、Opera12+和Chrome。在这个方法以前,要建立访问器属性,通常都使用两个非标准的方法:__defineGetter__()和__defineSetter__()。这2个方法最初是由Firefox引入的,后来SaFari三、Chrome一、opera9.5也给出了相同的实现。使用这2个遗留的方法,能够实现上面的例子以下:
var book={
    _year:2004,
    edition:1
}
//定义访问器的旧有方法
book.__defineGetter__('year',function(){
    return this._year;
});
book.__defineSetter__('year',function(newValue){
    if(newValue>2004){
        this._year=newValue;
        this.edition+=newValue-2004;
    }
});
book.year=2005;
alert(book.edition);//2

在不支持Object.defineProperty()方法的浏览器中不能修改[Configurable] 和[Enumerable]。

定义多个属性

ECMAScript5又定义了一个Object.defineProperties()方法。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象;第二个对象的属性与第一个对象中添加或修改的属性一一对应。例如:

var book={}

Object.defineProperties(book,{

    _year:{
        value:2004
    },
    edition:{
        value:1
    },
    year:{

        get:function(){
            return this._year;
        },
        set:function(newValue){
            if(newValue>2004){
                this._year=newValue;
                this.edition+=newValue-2004;
            }
        }
    }
})

读取属性的特性

使用Object.getOwnPropertyDescriptor()方法,这个方法接收两个参数:属性所在的对象和要读取的属性名称。

var book={};
Object.defineProperties(book,{

    _year:{
        value:2004
    },
    edition:{
        value:1
    },
    year:{

        get:function(){
            return this._year;
        },
        set:function(newValue){
            if(newValue>2004){
                this._year=newValue;
                this.edition+=newValue-2004;
            }
        }
    }
})

var descriptor=Object.getOwnPropertyDescriptor(book,'_year');
alert(descriptor.value);//2004
alert(descriptor.configurable);//false
alert(typeof descriptor.get);//undefined

var descriptor=Object.getOwnPropertyDescriptor(book,'year');
alert(descriptor.value);//undefined
alert(descriptor.configurable);//false
alert(typeof descriptor.get);//'function'

建立对象

虽然object构造函数或对象字面量均可以用来建立单个对象。但这些方式有个明显的缺点:使用同一个接口建立不少对象,会产生大量重复代码。

工厂模式

function createPerson(name, age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    }

    return o;
}

var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

工厂模式虽然解决了建立多个类似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

构造函数模式

function Person(name, age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    }
   
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

从上面的例子能够看出,要建立一个Person实例,须要使用new操做符。以这种方式调用构造函数要经历下面四个步骤:

  • 建立一个新对象;
  • 将构造函数的做用域赋给新对象(所以this就指向了这个新对象);
  • 执行构造函数中的代码(为这个新对象添加属性);
  • 返回新对象。

前面生成的两个person1和person1对象实例,这两个对象都一constructor属性,该属性指向Person,以下代码:

console.log(person1.constructor == Person); //true
console.log(person2.constructor == Person); //true

对象的constructor属性最初是用来标识对象类型的。可是,提到检测对象类型,使用instanceof操做符更可靠些。咱们在上面建立的person1,person2对象既是Object的实例,同时也是Person的实例。

console.log(person1 instanceof Person); //true
console.log(person1 instanceof Object); //true
console.log(person2 instanceof Person); //true
console.log(person2 instanceof Object); //true

1.将构造函数当函数
例如前面例子中的Person函数能够用下面任何一种方式调用:

//当成构造函数使用
var person1 = new Person("Nicholas", 29, "Software Engineer");
person1.sayName();//Nicholas
//做为普通函数调用
Person("Greg", 27, "Doctor");
window.sayName();//Greg 


//在另外一个对象的做用域中调用
var o=new Object();
Person.call(o,"Kristen",25,"Nurse");
o.sayName();
    

2.构造函数的问题

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("console.log(this.name)"); // 与声明函数在逻辑上是等价的
}

以这种方法建立函数,会致使不一样的做用域链和标示符解析。不一样实例上的同名函数是不相等的。

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
console.log(person1.sayName == person2.sayName); // false  

而后,建立两个完成一样任务的Function实例的确没有必要;何况有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。所以,大可像下面这样,经过把函数定义转移到构造函数外部来解决这个问题。

function Person(name, age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}

function sayName(){
    alert(this.name);
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

但是新问题又来了:在全局做用域中定义的函数实际上只能被某个对象调用,这让全局做用域有点名存实亡。而更让人没法接受的是:若是对象须要定义不少方法,那么就要定义不少多个全局函数,因而咱们这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题能够经过使用原型模式来解决。

原型模式

咱们建立的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含能够由特定类型的全部实例共享的属性和方法。也就是说prototype就是经过调用构造函数而建立的那个实例对象的原型对象。

function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software  Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
}

var person1 = new Person();
person1.sayName(); // Nicholas

var person2 = new Person();
person2.sayName(); // Nicholas
alert(person1.sayName == person2.sayName);

从图上能够看到构造函数Person的属性prototype指向了函数的原型对象,这个原型对象刚开始只有一个constructor属性,后面给它添加了原型对象的属性和方法。person1和person2的实例对象没有标准的方式访问[[prototype]],但firefox,safari,chrome在每一个对象上有一个内部属性__proto__;这个内部属性__proto__指向了Person.prototype。这儿说明下,Person.prototype.constructor == Person为true,说明Person.prototype.constructor指回了Person。

isPrototypeOf()

console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true

hasOwnProperty()

function Person(){}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software  Engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
}

var person1 = new Person();
var person2 = new Person();

console.log(person1.hasOwnProperty("name")); // false

person1.name = "Greg";
console.log(person1.name); // Greg
console.log(person1.hasOwnProperty("name")); // true

console.log(person2.name); // Nicholas
console.log(person2.hasOwnProperty("name")); // false

delete person1.name;
console.log(person1.name); // Nicholas
console.log(person1.hasOwnProperty("name")); // false

原型与in操做符

function Person(){}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software  Engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
}

var person1 = new Person();
var person2 = new Person();

console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1); // true

person1.name = "Greg";
console.log(person1.name); // Greg
console.log(person1.hasOwnProperty('name')); // true
console.log("name" in person1); // true


console.log(person2.name); // Nicholas
console.log(person2.hasOwnProperty('name')); // false
console.log("name" in person2); // true

delete person1.name;
console.log(person1.name); // Nicholas
console.log(person1.hasOwnProperty('name')); // false
console.log("name" in person1); // true

同时使用hasOwnProperty()方法和in操做符,就能够肯定该属性究竟是存在于对象中,仍是存在于原型中,以下:

function hasPrototypeProperty(object,name){
    return !object.hasOwnProperty(name)&&(name in object);
}

只要in操做符返回true而hasOwnProperty()返回false,就能够肯定属性是原型中的属性。

更简单的原型语法

function Person(){}

Person.prototype = {
    name: "Nicholas", 
    age:29,
    job: "Software Engineer",
    sayName: function(){
        console.log(this.name);
    }
}

var friend = new Person();
console.log(friend instanceof Object); // true
console.log(friend instanceof Person); // true
console.log(friend.constructor == Person); // false
console.log(friend.constructor == Object); // true

若是constructor的值真的很重要,能够像下面这样特地将它设置回适当的值。

function Person(){}

Person.prototype = {
    constructor: Person,
    name: "Nicholas", 
    age:29,
    job: "Software Engineer",
    sayName: function(){
        console.log(this.name);
    }
}

原型对象的问题

function Person(){}

Person.prototype = {
    constructor: Person,
    name: "Nicholas", 
    age:29,
    job: "Software Engineer",
    friends: ['Shelby', "Court"],
    sayName: function(){
        console.log(this.name);
    }
}

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Van");

console.log(person1.friends); //Shelby,Court,Van
console.log(person2.friends); //Shelby,Court,Van
console.log(person1.friends===person2.friends); // true

假如咱们的初衷就是像这样在全部实例中共享一个数组,那么对这个结果无话可说。但是,实例通常都是要有属于本身的所有属性的。而这个问题正是咱们不多看到有人单独使用原型模式的缘由所在。

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

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}

Person.prototype = {
    constructor: Person,
    sayName: function(){ console.log(this.name);}
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");
console.log(person1.friends); // Shelby, Count, Van
console.log(person2.friends); // Shelby, Count
console.log(person1.friends === person2.friends); // false
console.log(person1.sayName === person2.sayName); // true

在这个例子中,实例属性都是在构造函数中定义的,而由全部实例共享的属性constructor和方法sayName()则是在原型中定义的。这种构造函数与原型混成的模式,是目前认同度最高的一种建立自定义类型的方法。

动态原型模式

function Person(name, age,job){
    this.name = name;
    this.age = age;
    this.job = job;
        
    if (typeof this.sayName!='function'){
            Person.prototype.sayName = function(){
                    console.log(this.name);
         }
    }
}

var friend = new Person("Nicholas",29,"Software Engineer");
friend.sayName(); //Nicholas

寄生构造函数模式

这种模式的基本思想是建立一个函数,这个函数的做用仅仅是封装建立对象的代码,而后返回新建立的对象。

function Person(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        console.log(this.name);
    };
    return o;
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); // Nicholas

关于寄生构造函数模式,返回的对象与构造函数或者构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部建立的对象没有什么不一样。

 function SpecialArray(){
        var values=new Array();
        values.push.apply(values,arguments);
        values.toPipedString=function(){
            return this.join("|");
        }
        return values;
    }
    var colors=new SpecialArray("red","blue","green");
    console.log(colors.toPipedString()); //red|blue|green
    

稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,并且其方法也不引用this的对象。

其特色是:一是新建立的对象实例方法不引用this;二是不使用new操做符调用构造函数。

把前面的Person构造函数从新以下:

function Person(name,age,job){
    var o = new Object();
    
   //能够在这儿建立私有变量和函数
   //方法
    o.sayName = function(){
        console.log(name);
    };
    
    //返回对象
    return o;
}

var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); // Nicholas

继承

原型链

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();
console.log(instance.getSuperValue()); // 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;
    },
    someOtherMethod: function(){
        return false;
    }
};

var instance = new Subtype();
console.log(instance.getSuperValue()); // error

原型链的问题

包含引用类型值的原型属性会被全部实例共享;而这也正是为何要在构造函数中,而不是在原型对象中定义属性的缘由。
在建立子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响全部对象实例的状况下,给超类型的构造函数传递参数。

function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function Subtype(){
    
}
Subtype.prototype= new SuperType();
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, black

借用构造函数

1.传递参数

function SuperType(name){
    this.name = name;
}

function Subtype(){
    SuperType.call(this,"Nicholas");
    this.age = 29;
}

var instance = new Subtype();
console.log(instance.name); //Nicholas
console.log(instance.age); // 29

2.借用构造函数的问题

方法都在构造函数中定义,所以函数复用就无从谈起。并且在超类型的原型中定义的方法,在子类型中就没法调用了。

组合继承

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function(){
    console.log(this.name);
};

function Subtype(name,age){
    SuperType.call(this,name);
    this.age = age;
}

Subtype.prototype = new SuperType();
Subtype.prototype.sayAge = function(){
   console.log(this.age);
};

var instance1 = new Subtype("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // red, blue, green, black
instance1.sayName(); // Nicholas
instance1.sayAge(); //29

var instance2 = new Subtype("Greg", 2);
console.log(instance2.colors); // red, blue, green
instance2.sayName(); // Greg
instance2.sayAge(); //2

组合继承避免了原型链和借用函数的缺陷,融合了它们的优势,成为Javascript中最经常使用的继承模式。

原型式继承

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}
var person = {
    name:"Nicholas",
    friends:["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends); // Shelby, Court, Van, Rob, Barbie

Object.create()

Object.create()方法规范了原型式继承。

var person = {
    name:"Nicholas",
    friends:["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends); // Shelby, Court, Van, Rob, Barbie

寄生式继承

就是一个封装继承过程的函数,该函数内部以某种方式来加强对象,最后返回这个加强的对象。

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}
function createAnother(original){
    var clone = object(original);
    clone.sayHi = function(){
        alert('hi')
    };
    return clone;
}
var person = {
    name:"Nicholas",
    friends:["Shelby", "Court", "Van"]
}

var anotherPerson = createAnother(person);
anotherPerson.sayHi();

寄生组合式继承

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

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(){
    console.log(this.name);
}

function Subtype(name,age){
    SuperType.call(this,name);
    this.age = age;
}

inheritPrototype(Subtype, SuperType);

Subtype.prototype.sayAge = function(){
    console.log(this.age);
}
相关文章
相关标签/搜索