Javascript基础——Javascript语言核心(6):类和模块;面向对象的程序设计

类和模块

在Javascript中,的实现是基于其原型继承机制。如果两个实例都从同一个原型对象上继承了属性,它们是同一个类的实例。

9.1 类和原型

在Javascript中,类的所有实例对象都从同一个原型对象上继承属性。因此,原型对象是类的核心

9.2 类和构造函数

使用关键字new来调用构造函数。构造函数的prototype属性被用作新对象的原型。

定义构造函数既是定义类,并且类名首字母要大写

function Range(from,to){
    this.from=from;
    this.to=to;
}
Range.prototype={
    includes:function(x){
        return this.from <=x && x <= this.to;
    },
    foreach: function(f){
        for(var x=Math.ceil(this.from);x<=this.to;x++){
            f(x);
        }
    }
}
var r = new Range(1,3); //创建对象

r.includes(2);          //=>true: 2在范围内
r.foreach(console.log); //输出1 2 3

9.2.1 构造函数和类的标识

原型对象唯一标识,而初始化对象的状态的构造函数不能作为类的标识

举例:两个构造函数的prototype属性可能指向同一个原型对象,那么这两个构造函数创建的实例属于同一个类。

构造函数名字通常用做类名

9.2.2 constructor属性

在9.2的例子中,将Range.prototype定义为一个新对象,这个对象包含类所需要的方法。其实没有必要新创建一个对象,用单个对象直接量的属性就可以定义原型上的方法。

每个Javascript函数(ECMAScript 5中的Function.bind()方法返回的函数除外)都自动拥有一个prototype属性,这个属性值是一个对象,包含唯一一个不可枚举属性constructor

var F = function(){};
var p = F.prototype;
var c = p.constructor;
c===F //=> true: 对于任意函数F.prototype.constructor == F

构造函数的原型中存在预先定义好的constructor属性,意味着对象通常继承的constructor均指代它们的构造函数。constructor属性为对象提供了类。

9.3 Javascript中Java式的类继承

定义类的步骤:

  1. 定义构造函数,初始化新对象的实例属性
  2. 给构造函数的prototype对象定义实例的方法
  3. 给构造函数定义类字段和类属性

9.7 子类

在面向对象编程中,类B可以继承自另外一个类A。A称为父类(superclass),B称为子类(subclass)。B的实例从A继承了所有的实例方法。

9.7.1 定义子类

Javascript的对象可以从类的原型对象中继承属性(通常继承的是方法)。

如果O是类B的实例,B是A的子类,那么O也一定从A中继承了属性。为此,确保B的原型对象继承自A的原型对象。

//通过原型继承创建一个新对象
function inherit(p){
    //p是一个对象,但不能是null
    if(p == null){
        throw TypeError();
    }

    //ES5的方式:Object.create()
    if(Object.create){
        return Object.create(p);
    }

    //ES3的方式
    var t=typeof p;
    if(t!=="object" && t!=="function"){
        throw TypeError();
    }
    function f(){}; //定义一个空构造函数
    f.prototype = p;
    return new f();
}

function A(){};
function B(){};

B.prototype = inherit(A.prototype);
B.prototype.constructor = B;

犀牛书这个章节的内容其实是很多的,但是讲得不是很浅显易懂,很多概念还是很模糊。所以整理了《JavaScript高级程序设计(第3版)》第6章的如下内容进行补充(内容框架和书中相同,但进行了删减,并加入了自己的理解):

面向对象的程序设计

6.2 创建对象

虽然Object构造函数或对象字面量都可以用来创建单个对象,但是有明显缺点:使用同一个接口创建很多对象,会产生大量重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。

注意:下面是模式的发展过程

6.2.1 工厂模式

考虑到在ECMAScript中无法创建类,开发人员发明了一种函数,用函数来封装以特定接口创建对象的细节。

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

var person1 = createPerson('Jenny',18);
var person2 = createPerson('Sam',28);

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

随着发展,新模式出现了。

6.2.2 构造函数模式

重写上面的例子:

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

var person1 = new Person('Jenny',18);
var person2 = new Person('Sam',28);
(1) 构造函数模式与工厂模式的区别和优势

与上例的不同之处:

  • 没有显示创建对象
  • 直接将属性和方法赋值给了this对象
  • 没有return语句

另外,函数名首字母大写。按照惯例,构造函数以大写字母开头,非构造函数以小写字母开头

创建Person的新实例,必须使用new操作符,会经历以下4个步骤

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
  • 执行构造函数中的代码(为这个新对象添加属性)
  • 返回新对象

这个例子中创建的对象既是Object的实例,也是Person的实例:

person1 instanceof Object //=> true
person1 instanceof Person //=> true

自定义构造函数意味着可以将它的实例标识为一种特定的类型,这正是构造函数模式胜过工厂模式的地方

(2) 构造函数的问题

使用构造函数主要问题,就是每个方法都要在每个实例上重新创建一遍

上例中,person1和person2都有sayName()的方法,但两个方法不是同一个Function的实例(函数是对象,因此每次定义一个函数,就是实例化了一个对象):

person1.sayName == person2.sayName //=> false

因此,可以像如下代码一样,把函数定义转移到构造函数外部来解决这个问题:

function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayName = sayName; //<---看这里
}

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

var person1 = new Person('Jenny',18);
var person2 = new Person('Sam',28);

person1.sayName == person2.sayName //=>true

person1和person2共享了全局作用域中的同一个函数,但也产生了新的问题:如果对象需要定义很多方法,那么需要定义多个全局函数,于是这个自定义的引用类型就丝毫没有封装性可言了。

这个问题可以通过使用原型模式来解决。

6.2.3 原型模式

创建的每个函数都有一个prototype属性,这个对象的用途是包含可以有特定类型的所有实例共享的属性和方法

所以,不必在构造函数中定义对象实例的信息,而是将这个信息直接添加到原型对象中。

function Person(){}

Person.prototype.name = "Sam";
Person.prototype.age = 16;
Person.prototype.sayName = function(){
    console.log(this.name);
};

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

person1.sayName == person2.sayName //=>true
(1) 理解原型对象

创建一个新函数,就会为其创建一个prototype属性。函数对象有prototype属性,普通对象没有prototype,但有__proto__属性

根据本例的代码,在下图中展示了各个对象之间的关系:

这里写图片描述

关系不再进行文字赘述,图上的蓝色箭头为指针,可以通过代码来验证这些指向关系:

person1.__proto__ == Person.prototype          //=> true
Person.prototype.constructor == Person         //=> true
Person.prototype.__proto__ == Object.prototype //=> true
Object.prototype.__proto__ == null             //=> true
(2) 更简单的原型语法

为了减少不必要的输入,以及从视觉上更改好封装原型的功能,更常见的做法是用一个包含所有属性和方法的字面量重写整个原型对象

function Person(){}

Person.prototype = {
    name: "Sam",
    age: 16,
    sayName: function(){
        console.log(this.name);
    }
}

Person.prototype.constructor == Person //=> false
Person.prototype.constructor == Object //=> true

将Person.prototype设置为以对象字面量形式创建的新对象,constructor属性不再指向Person了,不能再通过这个属性确定对象的类型。所以,应当进行指定:

function Person(){}

Person.prototype = {
    constructor: Person, //<----看这里
    name: "Sam",
    age: 16,
    sayName: function(){
        console.log(this.name);
    }
}

注意,原生的constructor属性是不可枚举的,以上方式会改变其enumerable的值,所以对于ES5可以像以下示例一样书写:

function Person(){}

Person.prototype = {
    name: "Sam",
    age: 16,
    sayName: function(){
        console.log(this.name);
    }
}

Object.defineProperty(Person.prototype,"constructor",{
    enumerable: false,
    value: Person
});
(3) 原型对象的问题

原型模式的缺点在于省略了后遭函数传递初始化参数的环节,所有实例在默认情况下取得相同的属性值。而最大的问题是由其共享的本性所致的。

看下面的例子:

function Person(){}

Person.prototype = {
    constructor: Person,
    name: "Sam",
    age: 16,
    friends: [],  //<----看这里
    sayName: function(){
        console.log(this.name);
    }
}

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

person1.friends.push('Lily');
person2.friends.push('Joyce');

person1.friends //=> ["Lily", "Joyce"]
person2.friends //=> ["Lily", "Joyce"]

显然,两个实例共享同一个数组是不合理的。实例一般都有属于自己的全部属性。

为了解决这个问题,可以组合使用模式。

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

创建自定义类型最常见的方式就是组合使用构造函数模式与原型模式。可以说是用来定义引用类型的默认模式。

模式 作用
构造函数模式 定义实例属性
原型模式 定义方法和共享的属性

每个实例都会有自己的一份实例属性的副本,同时又共享对方法的引用,最大限度地节省了内存。

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

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

var person1 = new Person('Sam',16);
var person2 = new Person('Amy',18);

person1.friends.push('Lily');
person2.friends.push('Joyce');

person1.friends //=> ["Lily"]
person2.friends //=> ["Joyce"]

6.2.5 动态原型模式

动态原型模式致力于把所有信息封装在构造函数中

只有在sayName()方法不存在的情况下,才会将它添加到原型中。且只有在初次调用构造函数时才会执行。

function Person(name,age){
    this.name =name;
    this.age = age;
    this.friends =[];
    if(typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            console.log(this.name);
        }
    }
}

Person.prototype //=> {constructor: ƒ}

var person = new Person('Sam',16);

Person.prototype //=> {sayName: ƒ, constructor: ƒ}

注意,使用动态原型模式时,不能使用对象字面量重写原型。

6.3 继承

ECMAScript只支持实现继承,主要依靠原型链。

6.3.1 原型链(实践中很少单独使用)

实现原型链有一种基本模式,如下:

//父类
function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
}

//子类
function SubType(){}

//实现继承
SubType.prototype = new SuperType(); //<---看这里

//子类实例
var instance = new SubType();
instance.getSuperValue(); //=> true

SubType继承了SuperType。没有使用SubType默认的原型,而是换了一个新原型——SuperType的实例。新原型不仅具有作为一个SuperType的实例所拥有的全部属性和方法,而且其内部的指针指向了SuperType的原型。

(1) 原型链的问题

在通过原型来实现继承时,原型实际上会变成另一个类型的实例。原先的实例属性也变成了现在的原型属性。

用下面的代码来说明问题:

//父类
function SuperType(){
    this.colors = ['red'];
}

//子类
function SubType(){}

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


var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); //=> ["red", "black"]

var instance2 = new SubType();
console.log(instance2.colors);//=> ["red", "black"]

SubType的所有实例都会共享colors属性。对实例1的修改会反映到实例2上。

问题二:创建子类型实例时,不能像超类型的构造函数中传递参数。

综上,实践中很少单独使用。

6.3.2 借用构造函数(实践中很少单独使用)

为了解决原型中包含引用类型值所带来的问题,使用叫做借用构造函数(constructor stealing)的技术(也叫伪造对象或经典继承)。

基本思想:在子类型构造函数的内部调用超类型构造函数。通过使用apply()和call()方法在新创建对象上执行构造函数。

//父类
function SuperType(){
    this.colors = ['red'];
}

//子类
function SubType(){
    //继承了SuperType
    SuperType.call(this); //<---看这里
}

var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); //=> ["red", "black"]

var instance2 = new SubType();
console.log(instance2.colors);//=> ["red"]

“借调”了超类型的构造函数。会在新SubType对象上执行SuperType()中定义的所有对象初始化代码,这样,SubType的每个实例都会有自己的colors属性副本了。

(1) 传递参数

可以在子类型构造函数中向超类型构造函数传递参数。

//父类
function SuperType(name){
    this.name = name;
}

//子类
function SubType(){
    //继承了SuperType,传递参数
    SuperType.call(this,"Sam"); //<---看这里
    //实例属性
    this.age=28;
}


var instance = new SubType();
console.log(instance.name); //=> Sam
console.log(instance.age);  //=> 28
(2) 借用构造函数的问题

仅仅借用构造函数,无法避免构造函数模式存在的问题——方法都在构造函数中定义,函数不能复用。且,在超类型的原型中定义的方法,对子类型不可见,所有类型都只能使用构造函数模式。

综上,实践中很少单独使用。

6.3.3 组合继承

组合继承(combination inheritance),也叫做伪经典继承,指的是将原型链和借用构造函数的技术结合在一起。

思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。

//--父类--
function SuperType(name){
    this.name = name;
    this.colors = ['red'];
}

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("Sam",28);
instance1.colors.push('black');
console.log(instance1.name);   //=> Sam
console.log(instance1.age);    //=> 28
console.log(instance1.colors); //=> ["red", "black"]

var instance2 = new SubType("Amy",16);
console.log(instance2.colors); //=> ["red"]

这种继承方式是Javascript中最常用的继承模式。且instaceofisPrototypeOf()也能够用于识别基于组合继承创建的对象。

6.3.4 原型式继承

道格拉斯·克罗克福德在2006年题为Prototypal Inheritance in JavaScript(JavaScript中的原型继承)中介绍了一种实现继承的方法。

思路:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。借助如下函数:

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

object()对传入其中的对象执行了一次浅复制。例子(沿用上面的函数):

var person={
    name: "sam",
    friends:[]
}

var person1 = object(person);
person1.name; //=> "sam"
person1.friends.push('A');

var person2 = object(person);
person2.friends; //=> ["A"]

这种原型式继承要求必须有一个对象可以作为另一个对象的基础。但是,如果包含引用类型的属性都会共享相应的值,就像使用原型模式一样。

ECMASciprt 5通过新增Object.create()方法规范了原型式继承。接受两个参数:

  • 参数1:用作新对象原型的对象
  • 参数2(可选):为新对象定义额外属性的对象(与Object.defineProperties()方法的第二个参数格式相同,每个属性都是通过自己的描述符定义的)
var person={
    name: "sam",
    friends:[]
}

var person1 = Object.create(person); //<---看这里
person1.name; //=> "sam"
person1.friends.push('A');

var person2 = Object.create(person); //<---看这里
person2.friends; //=> ["A"]

var person3 = Object.create(person,{ //<---看这里
    name : {
        value: 'Jack'
    }
});
person3.name //=> "Jack"

在没有必要兴师动众创建构造函数,只是想让一个对象与另一个对象保持类似的情况下,原型式继承完全可以胜任。

6.3.5 寄生式继承

寄生式(parasitic)继承的思路:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。

寄生式继承模式示例:

//--调用了6.3.4的object()函数--
function createAnother(original){
    var clone = object(original); //通过调用函数穿件一个新对象
    clone.sayHi = function(){ //以某种方式来增强这个对象
        console.log('Hi');
    }
    return clone;
}

//--使用这个函数--
var person = {
    name: "Sam",
    friends:[]
}

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //=> Hi

这个例子中anotherPerson不仅基友person的所有属性和方法,还有自己的sayHi方法。

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类似。

6.3.6 寄生组合式继承

(1) 组合继承存在问题

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

这里可以回顾下6.3.3的组合继承中的代码示例:

SubType.prototype = new SuperType(); //第一次调用

function SubType(name,age){
    SuperType.call(this,name);       //第二次调用
}   

第一次调用SuperType构造函数时,SubType.prototype会得到name和colors两个属性(是SuperType的实例属性,位于SubType的原型中)。

当调用SubType构造函数时,又一次调用SuperType构造函数。这一次在新对象创建实例属性name和colors。于是,这两个属性屏蔽了原型中的两个同名属性。

为了解决这个问题,可以使用寄生组合式继承

(2) 寄生组合式继承

基本思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需的无非是超类型原型的副本。本质上,就是使用寄生式继承来继承超类型的原型,然后将结果制定给子类型的原型。

基本模式如下,这个函数实现了寄生组合式继承最简单的形式:

function inheritPrototype(subType,superType){
    var prototype = object(superType.prototype); //创建对象
    prototype.constructor = subType;             //增强对象
    subType.prototype = prototype;               //指定对象
}

这个函数接受两个参数:子类型构造函数和超类型构造函数。

function SuperType(name){
    this.name = name;
    this.colors = [];
}

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

这个例子的高效率体现在它只调用了一次SuperType构造函数,避免了在SubType.prototype上创建不必要的、多余的属性。同事,原型链保持不变,可以正常使用instanceof和isPrototypeOf()。

寄生组合式继承是引用类型最理想的继承范式

6.4 小结

继承模式 说明
组合继承 使用最多的继承模式,使用原型链继承共享的属性和方法,通过借用构造函数继承实例属性
原型式继承 在不必预先定义构造函数的情况下实现继承,本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造
寄生式继承 与原型继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用
寄生组合式继承 集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式

猜你喜欢

转载自blog.csdn.net/joyce_lcy/article/details/80831404