原型继承
原型链
JavaScript 中每个函数中都有一个指向某一个对象的 prototype 属性,该函数被 new 操作符调用时会创建并返回一个对象,并且该对象中会有一个指向其原型对象的秘密链接,通过该链接,我们就可以在新建的对象中调用相关原型对象的方法和属性。
而原型对象自身也具有对象固有的普遍特征,因此本身也包含了指向其原型的链接,由此形成一条链,我们称之为原型链。
在对象 A 的一系列属性中,有一个叫做 __proto__
的隐藏属性,它指向另一个对象 B,而 B 的 __proto__
又指向了对象 C,依次类推,直至链条末端的 Object 对象,该对象是 JavaScript 中的最高级父对象,语言中所有对象都必须继承它。
继承的作用,它能使每个对象都能访问其原型链上的任何一个属性。
原型链示例
function Shape() {
this.name = "shape";
this.toString = function () {
return this.name;
};
}
function TwoShape() {
this.name = "two shape";
}
function Triangle(side, height) {
this.name = "triangle";
this.side = side;
this.height = height;
this.getArea = function () {
return (this.side * this.height) / 2;
};
}
TwoShape.prototype = new Shape();
Triangle.prototype = new TwoShape();
// constructor 包含在 prototype 中
TwoShape.prototype.constructor = TwoShape;
Triangle.prototype.constructor = Triangle;
var my = new Triangle(5, 10);
console.log(my.getArea()); // 25
console.log(my.toString()); // 'triangle'
console.log(my.constructor === Triangle); // true
console.log(my.__proto__.constructor === Triangle); // true
console.log(my instanceof Shape); // true
console.log(my instanceof TwoShape); // true
console.log(my instanceof Triangle); // true
console.log(my instanceof Object); // true
console.log(Shape.prototype.isPrototypeOf(my)); // true
console.log(TwoShape.prototype.isPrototypeOf(my)); // true
console.log(Triangle.prototype.isPrototypeOf(my)); // true
console.log(String.prototype.isPrototypeOf(my)); // false
将共享属性迁移到原型中去
function Shape() {}
Shape.prototype.name = "Shape";
Shape.prototype.toString = function () {
return this.name;
};
function TwoShape() {}
TwoShape.prototype = new Shape();
TwoShape.prototype.constructor = TwoShape;
TwoShape.prototype.name = "2D Shape";
function Triangle(side, height) {
this.side = side;
this.height = height;
}
Triangle.prototype = new TwoShape();
Triangle.prototype.constructor = Triangle;
Triangle.prototype.name = "Triangle";
Triangle.prototype.getArea = function () {
return (this.side * this.height) / 2;
};
var my = new Triangle(5, 10);
console.log(my.getArea()); // 25
console.log(my.hasOwnProperty("side")); // true
console.log(my.hasOwnProperty("name")); // false
只继承于原型
处于效率考虑,我们应该尽可能的将一些可重用的属性和方法添加到原型中去。
- 不要单独为继承关系创建新对象
- 尽量减少运行时方法搜索
function Shape() {}
Shape.prototype.name = "Shape";
Shape.prototype.toString = function () {
return this.name;
};
function TwoShape() {}
TwoShape.prototype = Shape.prototype;
TwoShape.prototype.constructor = TwoShape;
TwoShape.prototype.name = "2D shape";
function Triangle(side, height) {
this.side = side;
this.height = height;
}
Triangle.prototype = TwoShape.prototype;
Triangle.prototype.constructor = Triangle;
Triangle.prototype.name = "Triangle";
Triangle.prototype.getArea = function () {
return (this.side * this.height) / 2;
};
var my = new Triangle(5, 10);
console.log(my.getArea()); // 25
console.log(my.toString()); // 'Triangle'
// 弊端
// 子对象与父对象指向的是同一个对象,所以一旦子对象对其原型进行了修改,父对象也会被改变,甚至所有的继承关系也是如此
console.log(new TwoShape().name); // Triangle
console.log(new Shape().name); // Triangle
这种方法效率虽然更高,但是很多场景中并不适用。
临时构造器 - new F()
如果所有的 prototype 属性都指向相同的对象,父对象就会受到子对象的影响。
要解决这个问题,就必须利用某种中介打破这种连锁关系,可以利用一个临时构造器函数来充当中介。
function Shape() {}
Shape.prototype.name = "Shape";
Shape.prototype.toString = function () {
return this.name;
};
function TwoShape() {}
var F = function () {};
F.prototype = Shape.prototype;
TwoShape.prototype = new F();
TwoShape.prototype.constructor = TwoShape;
TwoShape.prototype.name = "2D shape";
function Triangle(side, height) {
this.side = side;
this.height = height;
}
var F = function () {};
F.prototype = TwoShape.prototype;
Triangle.prototype = new F(); // new F() 创建了一个新的原型对象,用来继承
Triangle.prototype.constructor = Triangle;
Triangle.prototype.name = "Triangle";
Triangle.prototype.getArea = function () {
return (this.side * this.height) / 2;
};
var my = new Triangle(5, 10);
console.log(my.getArea()); // 25
console.log(my.toString()); // 'Triangle'
console.log(new TwoShape().name); // 2D shape
console.log(new Shape().name); // Shape
console.log(Triangle.prototype);
uber - 子对象访问父对象的方式
function Shape() {}
Shape.prototype.name = "shape";
Shape.prototype.toString = function () {
var constr = this.constructor;
return constr.uber ? constr.uber.toString() + ", " + this.name : this.name;
};
function TwoDShape() {}
var F = function () {};
F.prototype = Shape.prototype;
TwoDShape.prototype = new F();
TwoDShape.prototype.constructor = TwoDShape;
TwoDShape.uber = Shape.prototype;
TwoDShape.prototype.name = "2D Shape";
function Triangle(side, height) {
this.side = side;
this.height = height;
}
var F = function () {};
F.prototype = TwoDShape.prototype;
Triangle.prototype = new F();
Triangle.prototype.constructor = Triangle;
Triangle.uber = TwoDShape.prototype;
Triangle.prototype.name = "Triangle";
Triangle.prototype.getArea = function () {
return (this.side * this.height) / 2;
};
var two = new TwoDShape();
console.log(two.constructor); // TwoDShape
console.log(two.constructor.uber);
console.log(two.toString());
- 将 uber 属性设置成指向其父级原型的引用
- 对 toString() 方法进行更新
此前,toString() 所做的仅仅是返回 this.name 的内容而已,现在我们为它新增了一项额外任务,即检查对象中是否存在 this.constructor.uber 属性,如果存在就先调用该属性的 toString() 方法,由于 this.constructor 本身就是一个函数,而 this.constructor.uber 则是指向了当前对象父级原型的引用,所以当我们调用 Triangle 实体的 toString() 方法时,其原型链上所有的 toString() 都会被调用。
var triangle = new Triangle(5, 10);
console.log(triangle.toString()); // shape, 2D Shape, Triangle
将继承部分封装成函数
function extend(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
// 改造上面实例
function extend(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
function Shape() {}
Shape.prototype.name = "shape";
Shape.prototype.toString = function () {
var constr = this.constructor;
return constr.uber ? constr.uber.toString() + ", " + this.name : this.name;
};
function TwoDShape() {}
extend(TwoDShape, Shape);
TwoDShape.prototype.name = "2D Shape";
function Triangle(side, height) {
this.side = side;
this.height = height;
}
extend(Triangle, TwoDShape);
Triangle.prototype.name = "Triangle";
Triangle.prototype.getArea = function () {
return (this.side * this.height) / 2;
};
var triangle = new Triangle(5, 10);
console.log(triangle.toString()); // shape, 2D Shape, Triangle
属性拷贝
在构建可重用的继承代码时,我们可以简单的将父对象属性拷贝给子对象,
function extend(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
function Shape() {}
function TwoDShape() {}
Shape.prototype.name = "shape";
Shape.prototype.toString = function () {
var constr = this.constructor;
return constr.uber ? constr.uber.toString() + ", " + this.name : this.name;
};
extend(TwoDShape, Shape);
var td = new TwoDShape();
console.log(td.name); // 'shape'
console.log(TwoDShape.prototype.name); // 'shape'
console.log(td.__proto__.name); // 'shape'
console.log(td.hasOwnProperty("name")); // false
如果继承是通过 extend2() 方法来实现的,TwoDShape() 的原型就会拷贝获得属于自己的 name 属性,同样的,其中也会拷贝属于自己的 toString() 方法,但这只是一个函数引用,函数本身没有本再次创造。
/**
Child: 子对象构造函数
Parent: 父对象构造函数
*/
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
// constructor、length 等属性不可枚举
// 不需要在重新执行 child 的 constructor 指向
// 这个方法相率更高
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
function Shape() {}
function TwoDShape() {}
Shape.prototype.name = "shape";
Shape.prototype.toString = function () {
var constr = this.constructor;
return constr.uber ? constr.uber.toString() + ", " + this.name : this.name;
};
extend2(TwoDShape, Shape);
var td = new TwoDShape();
console.log(td);
console.log(td.name); // 'shape'
console.log(TwoDShape.prototype.name); // 'shape'
console.log(td.__proto__.name); // 'shape'
console.log(td.hasOwnProperty("name")); // false
小心处理引用拷贝
事实上,对象类型(包括函数与数组)通常都是以引用形式来进行拷贝的,这有时候会造成与预期不同的结果。
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
function Papa() {}
function Wee() {}
Papa.prototype.name = "Bear";
Papa.prototype.owns = ["porridge", "chair", "bed"];
extend2(Wee, Papa);
// name 属于基本类型属性,创建的是一份全新的拷贝,而 owns 属性是一个数组对象,它所执行的引用拷贝。
// 问题:子类修改引用类型属性,父类会同步修改
new Wee().owns.push("sss");
console.log(new Papa().owns); // [ "porridge", "chair", "bed", "sss" ]
// 对引用类型进行重写
Wee.prototype.owns = ["test1", "test2", "test3"]; // 改变引用类型指针
Papa.prototype.owns.push("bed2");
console.log(Wee.prototype.owns); // ['test1', 'test2', 'test3']
对象之间的继承
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
var shape = {
name: "Shape",
toString: function () {
return this.name;
},
};
var twoDee = extendCopy(shape);
console.log(twoDee);
twoDee.name = "2D shape";
twoDee.toString = function () {
return this.uber.toString() + ", " + this.name;
};
console.log(twoDee.toString()); // Shape, 2D shape
var triangle = extendCopy(twoDee);
triangle.name = "Triangle";
triangle.getArea = function () {
return (this.side * this.height) / 2;
};
triangle.side = 5;
triangle.height = 10;
console.log(triangle.getArea()); // 25
console.log(triangle.toString()); // 'Shape, 2D shape, Triangle'
对于这种方法,可能的问题就在于初始化一个新的 triangle 对象的过程过于繁琐。
因为我们必须要对该对象 side 和 height 值进行手动设置,
深拷贝
function deepCopy(p, c) {
c = c || {};
for (var i in p) {
if (p.hasOwnProperty(i)) {
if (typeof p[i] === "object") {
c[i] = Array.isArray(p[i]) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
}
return c;
}
// 保险起见
if (Array.isArray !== "function") {
Array.isArray = function (candidate) {
return Object.prototype.toString.call(candidate) === "[object Array]";
};
}
var parent = {
numbers: [1, 2, 3],
letters: ["a", "b", "c"],
obj: {
prop: 1,
},
bool: true,
};
var mydeep = deepCopy(parent);
mydeep.numbers.push(4, 5, 6);
console.log(mydeep.numbers); // [ 1, 2, 3, 4, 5, 6 ]
console.log(parent.numbers); // [1, 2, 3]
object()
可以使用 object() 函数来接收父对象,并返回一个以该对象为原型的新对象。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
如果我们需要访问 uber 属性,可以继续 object() 函数
function object(o) {
var n;
function F() {}
F.prototype = o;
n = new F();
n.uber = o; // uber 代表的是父类属性
return n;
}
// 代码演示
function object(o) {
var n;
function F() {}
F.prototype = o;
n = new F();
n.uber = o; // uber 代表的是父类属性
return n;
}
var shape = {
name: "shape",
toString: function () {
return this.name;
},
};
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
var twoDee = extendCopy(shape);
twoDee.name = "2D shape";
twoDee.toString = function () {
return this.uber.toString() + ", " + this.name;
};
var triangle = object(twoDee);
triangle.name = "triangle";
triangle.getArea = function () {
return (this.side * this.height) / 2;
};
console.log(triangle.toString()); // shape, 2D shape, triangle
这种模式也称为原型继承,因为这里,我们将父对象设置成子对象的原型,这个 object() 函数被 ES5 采纳,并且更名为 object.cteate()
console.log(Object.create(triangle));
原型继承与属性拷贝的混合应用
/*
* o: 用于继承
* stuff: 用于拷贝方法与属性
**/
function objectPlus(o, stuff) {
var n;
function F() {}
F.prototype = o;
n = new F();
n.uber = o;
for (var i in stuff) {
n[i] = stuff[i];
}
return n;
}
/*
* o: 用于继承
* stuff: 用于拷贝方法与属性
**/
function objectPlus(o, stuff) {
var n;
function F() {}
F.prototype = o;
n = new F();
n.uber = o;
for (var i in stuff) {
n[i] = stuff[i];
}
return n;
}
var shape = {
name: "shape",
toString: function () {
return this.name;
},
};
var twoDee = objectPlus(shape, {
name: "2D shape",
toString: function () {
return this.uber.toString() + ", " + this.name;
},
});
console.log(twoDee);
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
多重继承
我们来创建一个 multi() 函数,它可以接受任意数量的输入性对象,然后,我们在其中实现了一个双重循环,内层循环用于拷贝属性,外层循环则用于遍历函数参数中所传递进来的所有对象。
function multi() {
var n = {},
stuff,
j = 0,
len = arguments.length;
for (j = 0; j < len; j++) {
stuff = arguments[i];
for (var i in stuff) {
if (stuff.hasOwnProperty(i)) {
n[i] = stuff[i];
}
}
}
return n;
}
// 演示
function multi() {
var n = {},
stuff,
j = 0,
len = arguments.length;
for (j = 0; j < len; j++) {
stuff = arguments[j];
for (var i in stuff) {
if (stuff.hasOwnProperty(i)) {
n[i] = stuff[i];
}
}
}
return n;
}
var shape = {
name: "shape",
toString: function () {
return this.name;
},
};
var twoDee = {
name: "2D shape",
dimesions: 2,
};
var triangle = multi(shape, twoDee, {
name: "Triangle",
getArea: function () {
return (this.side * this.height) / 2;
},
side: 5,
height: 10,
});
console.log(triangle);
console.log(triangle.getArea()); // 25
console.log(triangle.dimesions); // 2
console.log(triangle.toString()); // 'Triangle'
寄生式继承
基本思路是:我们可以在创建对象的函数中直接吸收其他对象的功能,然后对其进行扩展并返回。
function object(o) {
var n;
function F() {}
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;
}
var t = triangle(5, 10);
console.log(t.dimensions); // 2
var t2 = new triangle(5, 5);
console.log(t2.getArea()); // 12.5
构造器借用
子对象构造器可以通过 call 或 apply 方法来调用父对象的构造器,
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);
console.log(t.name); // 'Triangle'
console.log(t.id); // 101
console.log(t.toString()); // [object Object]
Triangle 对象中不包含 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 = new Shape();
Triangle.prototype.name = "Triangle";
var t = new Triangle(101);
console.log(t);
console.log(t.name); // 'Triangle'
console.log(t.id); // 101
console.log(t.toString()); // Triangle
这种继承模式中,父对象的属性是以子对象自身属性的身份来重建的,这也体现了构造器借用法的一大优势,当我们创建一个继承于数组或其他对象类型的子对象时,将获得一个完完全全的新值(不是引用),对它做任何修改都不会影响其父对象。
这种模式也有缺点,父对象的构造器会被调用两次。
借用构造器与原型复制
对于这种由于构造器的双重调用带来的重复执行问题,实际上是很容易更正的。
我们可以在父对象构造器上调用 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;
}
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(101);
console.log(t.toString()); // 'Triangle'
console.log(t.id); // 101