Javascript的面向对象入门实例(三)
内容摘要
这篇文章主要为大家详细介绍了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的面向对象入门实例(三)的内容就先介绍到这里,更多相关文章的可以留意
代码注释