javascript高级程序设计第三版 第六章 面向对象的程序设计

6 面向对象的程序设计

6.1 理解对象

6.1.1 属性类型

分两种:数据属性和访问器属性
js引擎使用,js不能直接访问。
4个描述其行为的特性。
[[Configurable]] 能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认true
[[Enumerable]] 能否通过for-in循环返回属性,默认true
[[Writable]] 能否修改属性的值 默认true
[[Value]] 包含这个属性的数据值。默认undefined

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

采用Object.defineProperty()时,如果不指定,configurable,enumerable和writable默认false

访问器属性
[[Get]] 读取属性时调用的函数,默认undefined
[[Set]] 设置属性时调用的函数,默认undefined

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

6.1.2 定义多个属性

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

6.1.3 读取属性的特性

var descriptor=Object.getOwnPropertyDescriptor(book,"_year");
alert(descriptor.value);//2004
alert(descriptor.configurable);//false

6.2 创建对象

6.2.1 工厂模式

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("a",10,"engineer");
var person2=createPerson("b",20,"teacher");

优点:解决了创建多个相似对象的问题
缺点:没有解决对象识别的问题,即怎样知道一个对象的类型。只有person1 instanceof Object为true

6.2.2 构造函数模式

//没有显示创建对象
//直接将属性和方法赋值给this对象
//没有return语句
function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.sayName=function(){
        alert(this.name);
    }
}

var person1=new Person("a",10,"engineer");
var person2=new Person("b",20,"teacher");

alert(person1.constructor == Person);//true
alert(person1 instanceof Object);//true
alert(person1 instanceof Person);//true

必须使用new操作符调用构造函数,经历4个步骤:
1、创建一个新对象
2、将构造函数的作用域赋值给新对象,因此this指向新对象
3、执行构造函数的代码
4、返回新对象

如果把构造函数当做函数调用,跟普通函数一样

//构造函数
var person=new Person("a",10,"engineer");
person.sayName();//"a"

//普通函数
Person("a",10,"engineer");
window.sayName();//"a"

//在另一个对象的作用域中调用
var o=new Object();
Person.call(o,"a",10,"engineer");
o.sayName();//"a"

构造函数的问题
person1和person2都有一个名为sayName()的方法,但不是同一个Function的实例,会浪费内存。ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。

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

alert(person1.sayName == person2.sayName);//false

解决方法

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

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

新问题:在全局作用域中定义的函数实际上只能被某个对象调用,如果对象需要定义很多方法,就需要定义很多全局函数,不合理。

6.2.3 原型模式

每个函数都有一个prototype(原型)属性,是一个指针,指向一个对象,用途是包含可以由特定类型的所有实例共享的属性和方法。好处是让所有对象实例共享原型对象所包含的属性和方法。

function Person(){
}
Person.prototype.name="a";
Person.prototype.sayName=function(){
    alert(this.name);
}
var person1=new Person();
person1.sayName();
var person2=new Person();
person2.sayName();
alert(person1.name == person2.name);//true
1、理解原型对象

默认情况,所有原型对象都会自动获得一个constructor(构造函数)属性,指向prototype属性所在函数的指针。

//isPrototypeOf()
alert(Person.prototype.isPrototypeOf(person1));//true

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

//先查找实例是否有name属性,然后查找原型,即使实例属性设置为null,也不会恢复指向原型属性,只能通过delete删除实例属性
alert(person1.name);

//hasOwnProperty()检查是否有实例属性
person1.name="a";
alert(person1.hasOwnProperty("name"));//true

delete person1.name;
alert(person1.hasOwnProperty("name"));//false
2、原型与in操作符

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

//检查原型是否有该属性
function hasPrototypeProperty(object,name){
    return !object.hasOwnProperty(name) && (name in object);
}

//获取对象上所有可枚举实例属性
var keys = Object.keys(Person.prototype);
alert(keys);//"name,obj,job,sayName"

//获取对象上所有实例属性,无论是否可枚举
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys);//"constructor,name,obj,job,sayName"
3、更简单原型语法
function Person(){
}
//这种语法本质上重写了默认的prototype对象,所以constructor属性不再指向Person
Person.prototype = {
    //可手动添加constructor:Person,
    name:"a",
    sayName:function(){
        alert(this.name);
    }
};
var person1=new Person();
alert(person1 instanceof Person);//true
alert(person1.constructor == Person);//false
4、原型的动态性
//对原型对象所做的任何修改能够立即从实例上反应出来,即使是先创建实例后修改原型
var p=new Person();
Person.prototype.sayHi=function(){
    alert("hi");
}
p.sayHi();//没问题

重写修改了构造函数的原型属性的指针指向的对象

//但是重写就不行
var p=new Person();
Person.prototype={
    constructor:Person,
    name:"a",
    sayName:function(){
        alert(this.name);
    }
};

p.sayName();//error
5、原生对象的原型

如Object,Array,String等的方法都是在其构造函数的原型上定义方法。

6、原型对象的问题

对象的属性一般定义在构造函数,因为定义在原型上会被共享。

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

//构造函数模式用于定义实例属性,原型模式用于定义方法和共享属性。使用最广泛
function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.friends=["a","b"];
}

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

6.2.5 动态原型模式

有其他OO语言经验的开发人员看到独立的构造函数和原型时,可能会非常困惑。动态原型模式正是致力于解决这个问题,即把所有信息封装在构造函数中。

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.friends=["a","b"];

    //初次调用构造函数时才会执行
    if(typeof this.sayName != "function"){
        Person.prototype.sayName=function(){
            alert(this.name);
        };
    }
}

6.2.6 寄生构造函数模式

//仅仅封装创建对象的代码,然后返回新对象
function SpecialArray(){
    var values = new Array();
    values.push.apply(values,arguments);
    //不修改Array构造函数,增加额外方法,但是会每创建一次,就创建一个函数对象,浪费内存?
    values.toPipedString = function(){
        return this.join("|");
    };
    //构造函数在不返回值的情况下,默认会返回新对象实例,而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值
    return values;
}

var colors = new SpecialArray("reb","green","blue");
var colors2 = new SpecialArray("reb","green","blue");

alert(colors.toPipedString());
alert(colors instanceof SpecialArray);//false
alert(colors instanceof Array);//true
alert(colors instanceof Object);//true
alert(colors.toPipedString ==  colors2.toPipedString);//false

此模式下,返回的对象与构造函数或与构造函数原型属性之间没有关系,也就是说和在构造函数外部创建的对象没什么不同,不能使用instanceof确定对象类型,可以用其他模式就不要使用这种模式。

6.2.7 稳妥构造函数模式

稳妥对象指的是没有公共属性,其方法也不引用this对象,也不使用new操作符调用构造函数。
参考链接:https://www.zhihu.com/question/25101735/answer/36695742

function Person(name,age,job){
    var o = new Object();

    //可以在这里定义私有变量和函数
    //凡是想设为 private 的成员都不要挂到 Person 返回的对象 o 的属性上面,挂上了就是 public 的了。当然,这里的 private 和 public 都是从形式上类比其他 OO 语言来说的,其实现原理还是 js 中作用域、闭包和对象那一套。感觉实现得挺巧妙的。
    var name2=name;

    o.sayName=function(){
        alert(name);
    };

    o.sayName2=function(){
        alert(name2);
    };

    return o;
}

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

这样保存的是一个稳妥对象,除了调用sayName()方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。

6.3 继承

ECMAScript中只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

6.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
alert(instance.constructor);//function SuperType()....

通过实现原型链,本质上扩展了原型搜索机制。
搜索属性时:
1,搜索实例
2,搜索SubType.prototype
3,搜索SuperType.prototype
4、搜索Object.prototype
直到找到为止,在找不到属性或方法时,搜索过程是要一环一环地前行到原型链末端才会停下来。

1、别忘记默认的原型

所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。
所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这是所有自定义类型都会继承toString等默认方法的根本原因。

2、确定原型和实例的关系
alert(instance instanceof Object);//true
alert(instance instanceof SuperType);//true
alert(instance instanceof SubType);//true

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

只要原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。

3、谨慎地定义方法

给原型添加方法的代码一定要放在替换原型的语句之后。

4、原型链的问题

1、在通过原型来实现继承时,原型实际上会变成另一个类型的实例。原先的实例属性变成现在的原型属性。如果是包含引用类型值的原型,一个实例修改会影响另外一个实例。
2、在创建子类型的实例时,不能向超类型的构造函数中传递参数。
因此,实践中很少会单独使用原型链。

6.3.2 借用构造函数

有时也叫伪造对象或经典继承

function SuperType(){
    this.color = ["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"
1、传递参数
function SuperType(name){
    this.name=name;
}

function SubType(){
    SuperType.call(this,"a");
    this.age=29;
}

var instance = new SubType();
alert(instance.name);
alert(instance.age);
console.log(instance);

在SubType构造函数内部调用SuperType构造函数时,实际上是为SubType的实例设置了name属性。为了确保SuperType构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。

2、借用构造函数的问题

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

6.3.3 组合继承

将原型链和借用构造函数的技术组合在一块,其背后思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

function SuperType(name){
    this.name = name;
    this.color = ["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.sayAge = function(){
    alert(this.age);
}

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

6.3.4 原型式继承

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

本质上讲,object()对传入其中的对象执行了一次浅复制。
ECMAScript5通过新增Object.create()方法规范化了原型式继承。包含引用类型值的属性始终都会共享相应的值,和原型链有同样的问题。

6.3.5 寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似。

function createAnother(original){
    var clone = object(original);//通过调用函数创建一个新对象
    clone.sayHi = function(){//以某种方式来增强这个对象
        alert("hi");
    };
    return clone;//返回这个对象
}

问题是不能做到函数复用而降低效率。

6.3.6 寄生组合式继承

组合继承是Javascript最常用的继承模式,问题是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。

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();//第一次调用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
}

最终,有两组name和age属性,一组在实例上,一组在Subtype原型中。解决方法是使用寄生组合式继承。
寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

//两个参数:子类型构造函数,父类型构造函数
function inheritPrototype(subType,superType){
    var prototype = object(superType.prototype);//创建对象
    prototype.constructor = subType;//增强对象,弥补重写原型而失去的默认constructor属性
    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);
}

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值