Javascript之类的定义和继承

225 阅读5分钟

1、类的定义

本文主要针对ES5类的定义做简单的介绍,而不会介绍ES6使用class关键字来定义类(class关键字只是一种语法糖,与ES5定义类的最大区别是,使用class关键字不存在变量提升)。

在介绍JavaScript定义类的方法之前,对以下三种对象要有基本的认识。

1.1、构造函数对象

顾名思义,该对象指的就是构造函数的函数对象,它能为JavaScript类提供类名。也就是说,构造函数就是类的“外在表现”。而添加到构造函数对象上的属性和方法,都是类的方法(类似于Java中的静态方法)。

注意

如果说构造函数是类的“外在表现”,那么构造函数的prototype属性上的原型对象才是类的真正唯一标示。因为两个构造函数可能拥有相同的原型对象,那么这两个构造函数创建的实例属于相同类的实例。在使用instanceof操作符判断某个对象是否为某个类的实例时,并不是判断对象的构造函数是否和当前构造函数一致,而是在检查实例的原型链上,是否存在当前构造函数的prototype原型对象。

a instanceof A;

在上述代码中,执行instanceof操作符时,实际是在查询对象a的原型链上是否存在A.prototype对象。

1.2、原型对象

原型对象上的属性和方法被所有该类的实例所继承。也就是说,该类的每个实例都会共享原型对象上的方法和属性。

1.3、实例对象

实例对象就是通过构造函数构造出来的对象。实例对象上可能有自己的私有属性和方法,它们都不会与该类的其他实例共享这些属性和方法。

1.4、定义类的步骤

  • 创建一个构造函数,在构造函数中完成实例对象的属性和方法的初始化。
  • 在构造函数的原型对象上,定义原型对象的属性和方法。如上所述,它们被指定类的所有实例共享。
  • 在构造函数对象上,定义类的方法。
function definedClass(constructor, methods, statics) {
    if(methods) {
        for(var m in methods) {
            constructor.prototype[m] = methods[m];
        }
    }
    if(statics) {
        for(var s in statics) {
            constructor[s] = statics[s];
        }
    }
    return constructor;
}
// 调用
var clz = definedClass(function() {
    this.name = 'a class';
    this.say = function() {
        console.log(this.name);
    }
}, {
    toString: function () {
        return this.name;
    }
}, {
    className: function() {
        return 'No Name Class';
    }
});
var instance = new clz();

2、类的继承

常见的继承方式有以下两种:

2.1、方法一

如果构造函数Bprototype指向A的实例对象,那么B的实例对象将可以访问A的实例对象上的属性和方法。这样也就实现了类的继承。

function A() {
    this.name = 'A';
    this.say = function() {
        console.log(this.name);
    }
}
function B() {}
B.prototype = new A();
B.prototype.constructor = B;
new B().say(); 
// 输出"A"

对于这种方法,B的实例对象可以访问A中定义的属性和方法,这些属性和方法存在于A的实例对象中,而B.prototype又指向A的实例对象。也就是说,每个B的实例对象访问的其实都是原型对象上的属性。如果对B的原型对象上的属性进行修改时,所有B的实例对象都将受到影响。

2.2、方法二

如果构造函数Bprototype间接指向A.prototype对象,那么B的实例对象可以访问A.prototype上的所有属性和方法,但无法访问构造函数A中初始化的属性和方法。

function A() {
    this.name = 'A';
    this.say = function() {
        console.log(this.name);
    }
}
function B() {}
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;
new B().say();
// 抛出异常"Uncaught TypeError: (intermediate value).say is not a function"

那么,如何让B的实例对象也拥有构造函数A中初始化的属性和方法呢?很简单,只需要修改构造函数B即可。

function B() {
    A.apply(this, arguments); // 对于父类构造函数所需的参数,需要根据具体情况具体判断
}
...
new B().say();
// 输出"A"

对于这种方法,在构造函数B调用了构造函数A(这种行为可以称为构造函数链)。相当于在执行new B()时,在B的实例对象中初始化了A的属性和方法。于是,B的每个实例上都存在A的属性和方法,在修改B的某个实例对象上的属性时,其他实例对象不会受到影响。

2.3、常见问题

既然在方法二执行A.apply(this, arguments)调用A的构造函数,为什么不在方法一执行相同的调用呢?

function A() {
    this.name = 'A';
    this.say = function() {
        console.log(this.name);
    }
}
function B() {
    A.apply(this, arguments);
}
B.prototype = new A();
B.prototype.constructor = B;
new B().say(); 

其实是因为这种情况下,父类构造函数将会执行两次。如上代码所示,构造函数A执行了两次。

假设让B.prototype直接指向A.prototype,这样就能够解决构造函数A执行两次的问题,但是这种方式又会带来新的问题:子类构造函数和父类构造函数的原型对象完全一致,从而无法判断某个实例是父类还是子类构造出来的。

2.4、子类的定义

参考上面的definedClass函数,这里也可以实现通用的定义子类的函数。

function definedSubClass(superClass, constructor, methods, statics) {
    constructor.prototype = Object.create(superClass.prototype);
    constructor.prototype.constructor = constructor;
    return definedClass(constructor, methods, statics);
}

或者,将它定义到Function.prototype上。于是,父类的构造函数就可以直接调用该方法了。

Function.prototype.definedSubClass = function(constructor, methods, statics) {
    return definedSubClass(this, constructor, methods, statics);
}