Javascript的面向对象入门实例(三)

内容摘要
这篇文章主要为大家详细介绍了Javascript的面向对象入门实例(三),具有一定的参考价值,可以用来参考一下。

在Javascript中,虽然借助原型链就可以实现继承,但这里面还是有很多细节
文章正文

这篇文章主要为大家详细介绍了Javascript的面向对象入门实例(三),具有一定的参考价值,可以用来参考一下。

在Javascript中,虽然借助原型链就可以实现继承,但这里面还是有很多细节问题的要处理的。分析并解决这些问题后,就可以把创建类的过程写成一个通用函数了。

constructor属性

Javascript中的对象都有一个constructor的属性指向其构造函数。例如:

/**
 * 面向对象入门实例
 *
 * @param 
 * @arrange (www.idcnote.com)
 **/
function A() { }
var a = new A();
a.constructor; // A
确切地说,constructor属性是位于构造函数的prototype上。下面的代码可以证实这一规则:

/**
 * 面向对象入门实例
 *
 * @param 
 * @arrange (www.idcnote.com)
 **/
function A() { }

var a = new A();
console.log(a.constructor); // A

delete A.prototype.constructor; // 删除原型上的constructor属性
console.log(a.constructor); // Object
由于删除了A.prototype下的constructor属性,所以访问a.constructor的时候,在原型链中的查找就得查到Object.prototype上去,而Object.prototype.constructor自然就是Object。现在看一下简单的原型链继承方案带来的问题:

/**
 * 面向对象入门实例
 *
 * @param 
 * @arrange (www.idcnote.com)
 **/
function A() { }
function B() { }
B.prototype = new A();

var a = new A();
a.constructor; // A
var b = new B();
b.constructor; // A
可见,b的constructor应为B,但却成了A。原因是:b.constructor即B.prototype.constructor,而此时B.prototype是一个A对象,A对象的constructor即A.prototype.constructor,而A.prototype.constructor正是A。幸好constructor是一个可写的属性,所以只要重新设定这个值,问题就解决了:

/**
 * 面向对象入门实例
 *
 * @param 
 * @arrange (www.idcnote.com)
 **/
function A() { }
function B() { }
B.prototype = new A();
B.prototype.constructor = B; // important

var a = new A();
a.constructor; // A
var b = new B();
b.constructor; // B

instanceof操作符

从字面意思来看,instanceof用于判断某个对象是否某个类的实例,但准确地说,它是用于检测某个对象的原型链中是否包含某个构造函数的prototype。举个例子:

/**
 * 面向对象入门实例
 *
 * @param 
 * @arrange (www.idcnote.com)
 **/
var arr = new Array();
arr instanceof Array; // true
arr instanceof Object; // true
由于Array.prototype和Object.prototype都在arr的原型链中,所以上面的测试结果均为true。另外还要注意,instanceof的检测只跟原型链有关,跟constructor属性没有任何关系。所以,基于原型链的继承不会影响到instanceof的检测。

带参数的构造函数

前面通过原型链实现继承的例子中,构造函数都是不带参数的,一旦有参数,这个问题就复杂很多了。先看看下面的例子:

/**
 * 面向对象入门实例
 *
 * @param 
 * @arrange (www.idcnote.com)
 **/
function A(data) {
    this.name = data.name;
}
A.prototype.sayName = function() {
    console.log(this.name);
};
function B() { }
B.prototype = new A();
B.prototype.constructor = B;

var b = new B();
这段代码运行的时候会产生异常:
Cannot read property 'name' of undefined
出现异常的代码就是A构造函数内的那一行。出现异常的原因是:要访问data.name,就得保证data不为null或undefined,但是执行B.prototype=new A()时,却没有传参数进去,此时data为undefined,访问data.name就会出现异常。仅解决这个问题并不难,只要在new A()时传入一个不为null且不为undefined的参数就行:

/**
 * 面向对象入门实例
 *
 * @param 
 * @arrange (www.idcnote.com)
 **/
function A(data) {
    this.name = data.name;
}
A.prototype.sayName = function() {
    console.log(this.name);
};
function B() { }
B.prototype = new A({ });
B.prototype.constructor = B;

var b = new B();
b.sayName(); // undefined
然而,实际情况远没有这么简单。其一,A的参数可能不止一个,其内部逻辑也可能更为复杂,随便传参数进去很有可能导致异常。要彻底解决这个问题,可以借助一个空函数:

/**
 * 面向对象入门实例
 *
 * @param 
 * @arrange (www.idcnote.com)
 **/
function A(data) {
    this.name = data.name;
}
A.prototype.sayName = function() {
    console.log(this.name);
};
function B() { }

function Empty() { }
Empty.prototype = A.prototype; // important
B.prototype = new Empty();
B.prototype.constructor = B;

var b = new B();
b.sayName(); // undefined
Empty即为该空函数,它的prototype被更改为A.prototype,即Empty与A共享同一个prototype。因此,在忽略构造函数内部逻辑的前提下,把B.prototype设成Empty的实例跟设成A的实例效果是一样的。但因为Empty内部没有逻辑,所以new Empty()肯定不会产生异常。此外,ES5中的Object.create也可以解决这个问题:

/**
 * 面向对象入门实例
 *
 * @param 
 * @arrange (www.idcnote.com)
 **/
function A(data) {
    this.name = data.name;
}
A.prototype.sayName = function() {
    console.log(this.name);
};

function B() { }
B.prototype = Object.create(A.prototype); // important
B.prototype.constructor = B;

var b = new B();
b.sayName(); // undefined
其二,很多时候我们需要把子类构造函数的参数传给父类构造函数。比如说达到这样的效果:

var b = new B({ name: 'b1' });
b.name; // 'b1'
这就需要在子类构造函数中调用父类构造函数:

/**
 * 面向对象入门实例
 *
 * @param 
 * @arrange (www.idcnote.com)
 **/
function A(data) {
    this.name = data.name;
}
function B() {
    A.apply(this, arguments); //important
}

function Temp() { }
Temp.prototype = A.prototype;
B.prototype = new Temp();
B.prototype.constructor = B;

var b = new B({ name: 'b1' });
console.log(b.name);
通过A.apply(this, arguments)就可以确保操作的对象为当前对象(this),且把所有参数(arguments)传到A。

createClass函数

总算是完全解决了这些细节问题,为了不在每次创建类的时候都要写这么一大堆代码,我们把这个过程写成一个函数:

/**
 * 面向对象入门实例
 *
 * @param 
 * @arrange (www.idcnote.com)
 **/
function createClass(constructor, methods, Parent) {
    var $Class = function() {
        // 有父类的时候,需要调用父类构造函数
        if (Parent) {
            Parent.apply(this, arguments);
        }
        constructor.apply(this, arguments);
    };

    if (Parent) {
        // 处理原型链
        var $Parent = function() { };
        $Parent.prototype = Parent.prototype;
        $Class.prototype = new $Parent();
        // 重设constructor
        $Class.prototype.constructor = $Class;
    }

    if (methods) {
        // 复制方法到原型
        for (var m in methods) {
            if ( methods.hasOwnProperty(m) ) {
                $Class.prototype[m] = methods[m];    
            }
        }
    }

    return $Class;
}
在这个函数的基础上,把计算周长的问题解决掉:

/**
 * 面向对象入门实例
 *
 * @param 
 * @arrange (www.idcnote.com)
 **/
// 形状类
var Shape = createClass(function() {
    this.setName('形状');
}, {
    getName: function() { return this._name; },
    setName: function(name) { this._name = name; },
    perimeter: function() { }
});

// 矩形类
var Rectangle = createClass(function() {
    this.setLength(0);
    this.setWidth(0);
    this.setName('矩形');
}, {
    setLength: function(length) {
        if (length < 0) {
            throw new Error('...');
        }
        this.__length = length;
    },
    getLength: function() { return this.__length; },
    setWidth: function(width) {
        if (width < 0) {
            throw new Error('...');
        }
        this.__width = width;
    },
    getWidth: function() { return this.__width; },
    perimeter: function() {
        return (this.__length + this.__width) * 2;
    }
}, Shape);

// 正方形类
var Square = createClass(function() {
    this.setLength(0);
    this.setName('正方形');
}, {
    setLength: function(length) {
        if (length < 0) {
            throw new Error('...');
        }
        this.__length = length;
    },
    getLength: function() { return this.__length; },
    perimeter: function() {
        return this.__length * 4;
    }
}, Shape);

// 圆形
var Circle = createClass(function() {
    this.setRadius(0);
    this.setName('圆形');
}, {
    setRadius: function(radius) {
        if (radius < 0) {
            throw new Error('...');
        }
        this.__radius = radius;
    },
    getRadius: function() { return this.__radius; },
    perimeter: function() {
        return 2 * Math.PI * this.__radius;
    }
}, Shape);


function computePerimeter(shape) {
    console.log( shape.getName() + '的周长是' + shape.perimeter() );
}

var rectangle = new Rectangle();
rectangle.setWidth(10);
rectangle.setLength(20);
computePerimeter(rectangle);

var square = new Square();
square.setLength(10);
computePerimeter(square);

var circle = new Circle();
circle.setRadius(10);
computePerimeter(circle);

最后

最后总结一下在Javascript中模拟面向对象的要点:new的是构造函数,而不是类;属性写在构造函数内,方法写到原型链上;继承可以通过原型链实现;封装难以实现,可通过代码规范来约束。此外,鉴于构造函数是函数,普通函数也是函数,建议通过不同的命名规则区分它们:给构造函数命名时使用Pascal命名法,给普通函数命名时使用驼峰命名法。

注:关于Javascript的面向对象入门实例(三)的内容就先介绍到这里,更多相关文章的可以留意

代码注释

作者:喵哥笔记

IDC笔记

学的不仅是技术,更是梦想!