一、原型继承与属性拷贝
对于继承应用来说,主要目标是将一些现有的功能归为己有。也就是说,我们在新建一个对象时,通常首先应该继承于现有对象,然后在为其添加额外的方法和属性。对此,我们可以通过一个函数调用来完成,并且在其中混合使用我们刚才所讨论的两种方式:
function objectPlus(o, stuff){
var n;
var F = function(){};
F.prototype = o;
n = new F();
n.uber = o;
for(var i in stuff){
n[i] = stuff[i];
}
return n;
}
这个函数用于接受两个参数,o
用于继承,而另一个对象stuff
则用于拷贝方法和属性.
var shape = {
name: 'shape',
toString: function(){return this.name;}
}
var twoDee = objectPlus(shape,{
name: '2D shape',
toString: function(){return this.uber.toString() + ", " + this.name; }
})
var triangle = objectPlus(twoDee,{
name: 'triangle',
getArea: function(){return this.side * this.height / 2 ;},
side: 0,
height: 0
})
var my = objectPlus(triangle,{
side: 4,
height: 4
})
console.log(my.getArea()); //8
console.log(my.toString()); //shape, 2D shape, triangle, triangle。这里有两个triangle,是因为我们在具体实例化时是
//继承于triangle对象的,所以多了一层继承关系,我们也可以给实例一个新的名字。例如下面
var my = objectPlus(triangle,{
side: 4,
height: 4,
name: 'My Ext'
})
console.log(my.toString()); //shape, 2D shape, triangle, My Ext, 这里就变成了新的名字了
二、多重继承
所谓多重继承,通常是指一个子类对象中不止一个父对象的继承模式。对于JavaScript
这样的动态语言来说,实现多重继承是很简单的,尽管语言本身没有提供特殊的语法单元多重继承实现是极其简单的,我们只需要延续属性拷贝的继承思路依次扩展对象,不对其所继承的对象数量参数进行输入限制即可实现。
下面,我们创建一个multi()
函数,它可以接受任意数量的输入性对象。然后,我们在其中实现了一个双重循环,内层循环用于拷贝属性,而外层循环用于遍历函数参数中所传递进来的所有对象。
function multi(){ //这里是按照参数传递的顺序循环的,而且是浅拷贝
var n = {}, stuff, j = 0, len = arguments.length;
for(var j = 0; j < len; j++){
stuff = arguments[j];
for(var i in stuff){
n[i] = stuff[i];
}
}
return n;
}
需要注意的是:
multi()
函数中循环是按照对象输入的顺序来进行遍历的。如果两个对象拥有相同的属性,以最后一个对象为准。
var shape = {
name: 'shape',
toString: function(){return this.name;}
}
var twoDee = {
name: '2D shape',
dimensions: 2,
numbers: [1,2,3]
}
var triangle = multi(shape, twoDee, {
name: 'triangle',
getArea: function(){return this.side * this.height / 2; },
side: 5,
height: 10
})
console.log(triangle.getArea()); //25
console.log(triangle.dimensions); //2
console.log(triangle.toString()); //triangle
triangle.numbers.push(4);
console.log(triangle.numbers); //[1,2,3,4]
console.log(twoDee.numbers); //[1,2,3,4] 显然这个是浅拷贝的属性
三、寄生式继承
基本思想是:我们可以在创建对象的函数中,直接吸收其他对象的功能,然后对其进行扩展并返回。就好像所有的工作都是自己做的一样。
代码示例如下:
function object(o){
var n;
var F = function(){};
F.prototype = o;
n = new F();
n.uber = o;
return n;
}
var twoD = {
name: "2D shape",
dimensions: 2
}
function triangle(s, h){
var that = object(twoD);
that.name = 'triangle';
that.getArea = function () {
return this.side * this.height / 2;
}
that.side = s;
that.height = h;
return that;
}
//由于这里triangle是一个普通函数,不属于构造函数,所以调用它是不需要要new操作符的,但是
//该函数返回的是要给对象,所以在我们错误的使用了new操作符,也会正常工作
var t = triangle(5,6);
console.log(t.dimensions); //2
console.log(t.getArea()); //15
var t2 = new triangle(5,5); //照样正常工作
console.log(t2.getArea()); //12.5
四、构造器借用
子对象构造器可以通过call()
或apply()
方法来调用父对象的构造器。因而,它通常被称为构造器盗用法,或者构造器借用法。
在这里新的Triangle
对象继承了父对象的id
属性,但它并没有继承父对象原型中的其他任何东西。之所以对象中不包含Shape
的原型属性,是因为我们从来没有调用new Shape()
创建任何一个实例,
function Shape(id){
this.id = id;
}
Shape.prototype.name = "Shape";
Shape.prototype.toString = function () {return this.name;}
function Triangle(){
Shape.apply(this,arguments);
}
Triangle.prototype.name = "triangle";
var t = new Triangle(101);
console.log(t.name); //triangle
//在这里新的Triangle对象继承了父对象的id属性,但它并没有继承父对象原型中的其他任何东西
//之所以对象中不包含Shape的原型属性,是因为我们从来没有调用new Shape()创建任何一个实例,
//自然其原型也从来没有被用到。所以底下打印的是[object object]
console.log(t.toString()); //[object object],
那么如使其包含Shape
中的原型呢?我们可以对Triangle
构造器进行如下重定义:
function Triangle(){
Shape.apply(this,arguments);
}
Triangle.prototype = new Shape();
Triangle.prototype.constructor = Triangle;
在这种继承模式中,父对象的属性是以子对象自身的属性的身份来重建的(这与原型链模式中的子对象属性正好相反)。这也体现了构造器借用法的一大优势:当我们创建一个继承于数组或者其他对象类型的子对象时,将获得一个完完全全的新值(不是一个引用),对它做任何修改都不会影响父对象。
但这种模式也是有缺点的,因为这种情况下父对象的构造器往往会被调用两次:一次发生在通过apply
方法继承其自身属性时,而另一次则发生在通过 new
操作符继承其原型时。这样一来,父对象的自身属性事实上被继承了两次。
下面,我们来做一个简单的演示:
function Shape(id){
this.id = id;
}
Shape.prototype.name = "Shape";
Shape.prototype.toString = function () {return this.name;}
function Triangle(){
Shape.apply(this,arguments);
}
Triangle.prototype = new Shape(101); //为了演示加上了101,如果不加的话底下打印的就是undefined,虽然是,为什么是undefined,因为你没有传入参数,在执行Shape构造器的时候给id就是没有定义的值,所以this。id就赋值成了undefined,但是原型中还有有这个属性的,还是重复定义了两次
Triangle.prototype.constructor = Triangle;
Triangle.prototype.name = "triangle";
var t = new Triangle(202);
console.log(t.id); //202
console.log(t.__proto__.id); //101
delete t.id; //删除之后
console.log(t.id); //101
五、借用构造器和原型复制
对于这种由于构造器的双重调用而带来的重复执行问题,实际上是很容易修正的。我们可以在父类构造器上调用apply()
方法,以获得其全部的自身属性,然后在用一个简单的迭代器对其原型属性执行逐项拷贝。
function extend2(Child, Parent){
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p){
c[i] = p[i];
}
c.uber = p; //扩展子类原型,增加一个uber属性
}
function Shape(id){
this.id = id;
}
Shape.prototype.name = "Shape";
Shape.prototype.toString = function () {return this.name;}
function Triangle(){
Shape.apply(this,arguments);
}
extend2(Triangle,Shape);
Triangle.prototype.name = "triangle";
var t = new Triangle(202);
console.log(t.toString()); //triangle
console.log(t.id); //202
console.log(t.__proto__.id); //undefined
六 综合案例
我们利用所学的知识来写一个综合案例,结合canvas
绘制如下图形:
DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>canvas+prototypetitle>
head>
<body>
<canvas id="canvas" height="600" width="800">canvas>
body>
html>
//定义点类
function Point(x, y) {
this.x = x;
this.y = y;
}
//计算两点之间的直线距离
function Line(p1, p2){
this.p1 = p1;
this.p2 = p2;
this.length = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
}
//定义父类形状
function Shape(){
this.points = [];
this.lines = [];
this.init();
}
Shape.prototype = {
constructor: Shape,
init: function(){
if(typeof this.context === 'undefined'){
var canvas = document.getElementById('canvas');
Shape.prototype.context = canvas.getContext("2d");
}
},
draw: function(){
var ctx = this.context;
ctx.strokeStyle = this.getColor();
ctx.beginPath();
ctx.moveTo(this.points[0].x, this.points[0].y);
for(var i = 1; i<this.points.length; i++){
ctx.lineTo(this.points[i].x, this.points[i].y);
}
ctx.closePath();
ctx.stroke();
},
getColor: function(){
var rgb = [];
for(var i = 0; i < 3; i++){
rgb[i] = Math.round(255 * Math.random());
}
return 'rgb(' + rgb.join(',') + ')';
},
getLines: function(){
if(this.lines.length > 0){
return this.lines;
}
var lines = [];
for(var i = 0; i < this.points.length; i++){
lines[i] = new Line(this.points[i],(this.points[i+1]) ? this.points[i+1] : this.points[0]);
}
this.lines = lines;
return lines;
},
getArea: function () { //面积
},
getPerimeter: function(){ //z周长
var lines = this.getLines();
var perim = 0;
for(var i = 0; i < lines.length; i++){
perim += lines[i].length;
}
return perim;
}
}
//定义三角形
function Triangle(a, b, c){
this.points = [a, b, c];
this.getArea = function () {
var p = this.getPerimeter();
var s = p / 2;
return Math.sqrt(
s * (s - this.lines[0].length)
* (s - this.lines[1].length)
* (s - this.lines[2].length)
);
}
}
//定义矩形
function Rectangle(p, side_a, side_b){
this.points = [
p,
new Point(p.x + side_a, p.y),
new Point(p.x + side_a, p.y + side_b),
new Point(p.x, p.y + side_b)
]
this.getArea = function () {
return side_a * side_b;
}
}
//定义正方形
function Square(p, side){
Rectangle.call(this, p, side, side);
}
//修改原型链
var s = new Shape();
Triangle.prototype = s;
Rectangle.prototype = s;
Square.prototype = s;
//测试
var p1 = new Point(100, 100);
var p2 = new Point(300, 100);
var p3 = new Point(200, 0);
//三角形
var t = new Triangle(p1, p2, p3);
t.draw();
console.log(t.getPerimeter());
console.log(t.getArea());
//矩形
var r = new Rectangle(new Point(200, 200), 50, 100);
r.draw();
console.log(r.getPerimeter());
console.log(r.getArea());
//正方形
var s = new Square(new Point(130, 130), 50);
s.draw();
console.log(s.getPerimeter());
console.log(s.getArea());
new Square(p1, 200).draw();