constructor属性
Javascript中的对象都有一个constructor的属性指向其构造函数。例如:
/**
* 面向对象入门实例
*
* @param
* @arrange (512.笔记) jb51.cc
**/
function A() { }
var a = new A();
a.constructor; // A
确切地说,constructor属性是位于构造函数的prototype上。下面的代码可以证实这一规则:
/**
* 面向对象入门实例
*
* @param
* @arrange (512.笔记) jb51.cc
**/
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 (512.笔记) jb51.cc
**/
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 (512.笔记) jb51.cc
**/
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 (512.笔记) jb51.cc
**/
var arr = new Array();
arr instanceof Array; // true
arr instanceof Object; // true
由于Array.prototype和Object.prototype都在arr的原型链中,所以上面的测试结果均为true。另外还要注意,instanceof的检测只跟原型链有关,跟constructor属性没有任何关系。所以,基于原型链的继承不会影响到instanceof的检测。
带参数的构造函数
前面通过原型链实现继承的例子中,构造函数都是不带参数的,一旦有参数,这个问题就复杂很多了。先看看下面的例子:
/**
* 面向对象入门实例
*
* @param
* @arrange (512.笔记) jb51.cc
**/
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 (512.笔记) jb51.cc
**/
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 (512.笔记) jb51.cc
**/
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 (512.笔记) jb51.cc
**/
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 (512.笔记) jb51.cc
**/
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 (512.笔记) jb51.cc
**/
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 (512.笔记) jb51.cc
**/
// 形状类
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('正方形');
},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命名法,给普通函数命名时使用驼峰命名法。