欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-07-12 18:16:29
...

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()。寄生组合式继承是引用类型最理想的继承范式。