《JavaScript高级程序设计》读书笔记(三):面向对象

1.属性的特征

用对象字面量来创建对象:

var person = {

    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function(){
        alert(this.name);

}}; 

1. 数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的

特性。

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

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

  •  [[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的 这个特性默认值为 true。

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

要修改属性默认的特性,必须使用 ECMAScript 5 的 Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable 和 value。设置其中的一或多个值,可以修改对应的特性值。例如: 

var person = {};
Object.defineProperty(person, "name", {
    writable: false,
    value: "Nicholas"
});

alert(person.name); //"Nicholas"

person.name = "Greg";

alert(person.name); //"Nicholas" 


可以多次调用 Object.defineProperty()方法修改同一个属性,但在把 configurable特性设置为 false 之后就会有限制了。 

2.访问器类型

访问器属性不包含数据值;它们包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下 4 个特性。

 [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为true。 

  [[Enumerable]]:表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,这 5个特性的默认值为 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){
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;

}}

});

book.year = 2005;alert(book.edition); //2 

这是使用访问器属性的常见方式,即设置一个属性的值会导致其他属性发生变化。 

3.读取属性的特性 

使用 ECMAScript 5Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurableenumerablegetset;如果是数据属性,这个对象的属性有configurableenumerablewritablevalue。 

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 

2.创建对象

1.构造函数

   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");

构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1person2都有一个名为 sayName()的方法,但那两个方法不是同一个Function 的实例。 

2.原型模式

我们创建的每个函数都有一个 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);  //true

理解原型:

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。就拿前面的例子来说,Person.prototype.constructor 指向 Person。 




alert(Object.getPrototypeOf(person1) == Person.prototype); //true 


每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。


使用 delete操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性 :

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();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name);
alert(person2.name);
delete person1.name;
alert(person1.name);

//"Greg"——来自实例//"Nicholas"——来自原型

//"Nicholas"——来自原型 


in 操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。 

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


或者可以用对象字面量来定义原型:
function Person(){
}
Person.prototype = {
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }

}; 

:constructor属性不再指向 Person 了。前面曾经介绍过,每创建一个函数,就会同时创建它的prototype 对象,这个对象也会自动获得 constructor 属性。而我们在这里使用的语法,本质上完全重写了默认的prototype 对象,因此 constructor属性也就变成了新对象的constructor 属性(指向 Object构造函数),不再指向Person 函数。此时,尽管instanceof操作符还能返回正确的结果,但通过constructor 已经无法确定对象的类型了,如下所示。

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

所以需要显示设置:
Person.prototype = {
constructor : Person,

             
             

name : "Nicholas", 

age : 29, job: "Software Engineer", sayName : function () { 

        alert(this.name);
    }

}; 

以这种方式重设 constructor 属性会导致它的[[Enumerable]]特性被设置为true。默认情况下,原生的constructor 属性是不可枚举的,因此如果你使用兼容ECMAScript 5JavaScript引擎,可以试一试 Object.defineProperty()。 

//重设构造函数,只适用于ECMAScript 5兼容的浏览器Object.defineProperty(Person.prototype, "constructor", {

        enumerable: false,
        value: Person
    });



function Person(){
}
var friend = new Person();
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};
friend.sayName();   //error




原型对象的问题

原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。

原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟(如前面的例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。来看下面的例子:

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

}};

var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court,Van"
alert(person1.friends === person2.friends);  //true

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

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。 

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

}

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

}

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

4.动态原型模式

可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。来看一个例子。 

function Person(name, age, job){

//属性
this.name = name;

this.age = age;

this.job = job; 

//方法

if (typeof this.sayName != "function"){
    Person.prototype.sayName = function(){
        alert(this.name);

};} 

}

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

3.继承

1.原型链

简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。 


实现原型链有一种基本模式,其代码大致如下。
    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());//true 


调用instance.getSuperValue()会经历三个搜索步骤:1)搜索实例;2)搜索 SubType.prototype;3)搜索 SuperType.prototype,最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。 

原型链的问题

原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型。想必大家还记得,我们前面介绍过包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了:

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

}

function SubType(){
}

//继承了SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();

instance1.colors.push("black");

alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();

alert(instance2.colors); //"red,blue,green,black" 


原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。 


2.借用构造函数10

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

//继承了SuperType

    SuperType.call(this);

}

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);    //"red,blue,green,black"
var instance2 = new SubType();
alert(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, "Nicholas");

//实例属性

    this.age = 29;
}
var instance = new SubType();
alert(instance.name);    //"Nicholas";
alert(instance.age);     //29

借用构造函数的问题

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。 


3.组合继承

思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。 

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;
}

//继承方法
SubType.prototype = new SuperType();SubType.prototype.constructor = SubType;SubType.prototype.sayAge = function(){

    alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
var instance2 = new SubType("Greg", 27);

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceofisPrototypeOf()也能够用于识别基于组合继承创建的对象。 


4.寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。 

如:

function createAnother(original){varclone=object(original); //通过调用函数创建一个新对象

}

clone.sayHi = function(){
    alert("hi");//以某种方式来增强这个对象
};
return clone;//返回这个对象 
}


5.寄生组合继承

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;		 //指定对象

}

在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加constructor 属性,从而弥补因重写原型而失去的默认的constructor 属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。 


例子:

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);

这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceofisPrototypeOf()开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。 







  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值