默认的继承方法:通过原型来实现继承关系链
function Shape() {
this.name = 'Shape';
this.toString = function () {
return this.name;
};
}
function TwoDShape() {
this.name = '2D Shape';
}
function Triangle(side, height) {
this.name = 'Triangle';
this.side = side;
this.height = height;
this.getArea = function () {
return this.side * this.height / 2;
};
}
继承的代码:
TwoDShape.prototype = new Shape();
Triangle.prototype = new TwoDShape();
对对象的prototype属性进行完全替换时(不同于向prototype指向的对象添加属性,有可能会对对象的constructor属性产生一定的副作用),
所以对这些对象的constructor属性进行相应的重置:
TwoDShape.prototype.constructor = TwoDShape;
Triangle.prototype.constructor = Triangle;
测试一下实现的内容:
var my = new Triangle(5, 10);
my.getArea();
--25
my.toString();
--"Triangle"
在JavaScript引擎在my.toString()被调用时发生的事情。
1.会遍历my对象中的所有属性,没找到一个叫toString()的方法。
2.再去查看my.__proto__所指向的对象,该对象应该是在继承关系构建中由new ToDShape()所创建的实体
3.JS在遍历ToDShape实体的过程中依然不会找到toString方法,然后又继续检查该实体的__proto__属性,该__proto__属性所指向的实体由new Shape()所创建
4.在new Shape()所创建的实体中找到了toString()方法
5.该方法就会在my对象中被调用,并且其this指向了my
my.constructor === Triangle; --true
通过instanceof,可以验证my对象同时是上述三个构造器的实例:
my instanceof Shape;
--true
my instanceof TwoDShape;
--true
my instanceof Triangle;
--true
my instanceof Array; --false
以my参数调用这些构造器原型的isPropertypeOf()方法时,结果也是如此:
Shape.prototype.isPrototypeOf(my); --true TwoDShape.prototype.isPrototypeOf(my); --true Triangle.prototype.isPrototypeOf(my); --true String.prototype.isPrototypeOf(my); --false
用其他两个构造器来创建对象,用new TwoDShape()所创建的对象也可以获得继承自Shape()的toString()的方法。
var td = new TwoDShape();
td.constructor === TwoDShape;
--true
td.toString(); "2D Shape"
var s = new Shape();
s.constructor === Shape;
--true
2.将共享属性迁移到原型中去:
用某一个构造器创建对象时,其属性就会被添加到this中去,被添加的属性实际上不会随着实体改变,这种做法没有什么效率。
function Shape() {
this.name = 'Shape';
}
用new Shape()创建的每个实体都会拥有一个全新的name属性,并在内存中拥有自己的独立存储空间,可以将name属性添加到原型上去,所有实体就可以共享这个属性
function Shape() { }
Shape.prototype.name = 'Shape';
将所有的方法和符合条件的属性添加到原型对象中,Shape()和TwoDShape()而言,所有东西都是可以共享的
function Shape() {
}
Shape.prototype.name = 'Shape';
Shape.prototype.toString = function () {
return this.name;
};
function TwoDShape() { }
TwoDShape.prototype = new Shape();
TwoDShape.prototype.constructor = TwoDShape;
TwoDShape.prototype.name = '2D shape';
function Triangle(side, height) {
this.side = side;
this.height = height;
}
Triangle.prototype = new TwoDShape();
Triangle.prototype.constructor = Triangle;
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function () {
return this.side * this.height / 2;
}
var my = new Triangle(5, 10);
my.getArea(); --25 my.toString(); --"Triangle"
也可以通过hasOwnPrototype()属性来明确对象的自身属性和原型属性
my.hasOwnProperty('side');
--true
my.hasOwnProperty('name');
false
TwoDShape.prototype.isPrototypeOf(my); --true
my instanceof Shape; --true
3.只继承与原型:
1.不要单独为继承关系创建新对象
2.尽量减少运行时的方法搜索
function Shape() { }
Shape.prototype.name = 'shape';
Shape.prototype.toString = function () {
return this.name;
};
function TwoDShape() { }
TwoDShape.prototype = Shape.prototype;
TwoDShape.prototype.constructor = TwoDShape;
TwoDShape.prototype.name = '2D Shape';
function Triangle(side, height) {
this.side = side;
this.height = height;
}
Triangle.prototype = TwoDShape.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);
my.getArea(); - -25 my.toString(); --"Triangle"
以上代码采用了引用传递而不是值传递。
简单的拷贝原型在效率上来说固然好一些,但有他的副作用,子对象和父对象指向同一个对象,一旦子对象对其原型就行修改,父对象也会随即被改变,如:
Triangle.prototype.name = 'Triangle';
var s = new Shape();
s.name;
--"Triangle"
效率高,应用场景中并不适用
二:临时构造器——new F()
解决上述问题就必须利用中介来打破这种连锁关系,可以用一个临时构造器函数来充当中介,创建一个空函数F(),将其原型设置为父级构造器。
function Shape() {}
Shape.prototype.name = 'Shape';
Shape.prototype.toString = function () {
return this.name;
};
function TwoDShape() { }
var F=function(){};
F.prototype = Shape.prototype;
TwoDShape.prototype = new F();
TwoDShape.prototype.constructor = TwoDShape;
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.prototype.name = 'Triangle';
Triangle.prototype.getArea = function () {
return this.side * this.height / 2;
}
var my = new Triangle(5, 10);
my.getArea();
--25
my.toString(); --"Triangle"
通过这种方法,我们就可以保持住原型链:
my.__proto__ === Triangle.prototype; --true my.__proto__.constructor == Triangle; --true my.__proto__.__proto__ === TwoDShape.prototype; --true my.__proto__.__proto__.__proto__.constructor === Shape; --true
并且父对象的属性不会被子对象所覆盖:
var s = new Shape();
s.name;
--"Shape"
"I am a " + new TwoDShape(); --"I am a 2D shape"
将所有要共享的属性与方法添加到原型中,然后只围绕原型构建继承关系。
3.uber--子对象访问父对象的方式(指向父级原型对象)
function Shape() { }
Shape.prototype.name = 'shape';
Shape.prototype.toString = function () {
//var const1 = this.constructor;
return this.constructor.uber
? this.constructor.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 thi.side * this.height / 2;
};
var my = new Triangle(5, 10);
my.toString();
--"shape, 2D shape, Triangle"
增加以下内容:
1.将uber属性设置成指向其父级原型的引用
2.对toString()方法进行了更新
检查对象中是否存在this.constructor.uber属性,如果存在,就先调用该属性的toString方法,由于this.constructor本身是一个函数,而this.constructor.uber则是指向当前对象父级原型的引用。
当调用Triangle实体的toString()方法时,其原型链上所用的toString()都会被调用。
4.将继承部分封装成函数:
function extend(Child, Perent) {
var F = function () { };
F.prototype = Perent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Perent.prototype;
}
function Shape() { };
Shape.prototype.name = 'Shape';
Shape.prototype.toString = function () {
return this.constructor.uber
? this.constructor.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;
}
new Triangle().toString();
--"Shape, 2D shape, Triangle"
6.属性拷贝
将父对象的属性拷贝给子对象,
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
这种方法仅适用于只包含基本数据类型的对象,所有的对象类型(包括函数与数组)都是不可复制的,他们只支持引用传递。
Shape的原型中包含了一个基本类型属性name,和一个非基本类型属性---toString()方法
var Shape = function () { };
var TwoDShape = function () { };
Shape.prototype.name = 'shape';
Shape.prototype.toString = function () {
return this.uber
? this.uber.toString() + ', ' + this.name
: this.name;
};
通过extend()方法实现继承,name属性既不会是TwoDShape()实例的属性,也不会成为其原型对象的属性,但是子对象依然可以通过继承方式来访问该属性
extend(TwoDShape, Shape);
var td = new TwoDShape();
td.name;
--"shape"
TwoDShape.prototype.name; --"shape"
td.__proto__.name; --"shape"
td.hasOwnProperty('name');
--false
td.__proto__.hasOwnProperty('name');
--false
继承通过extend2()方法来实现,TwoDShape()的原型中就会拷贝获得属于自己的name属性,同样也会拷贝toString()方法,但这只是一个函数引用,函数本身并没有被再次创建
extend2(TwoDShape, Shape);
var td = new TwoDShape();
td.__proto__.hasOwnProperty('name');
--true
td.__proto__.hasOwnProperty('toString');
--true
td.__proto__.toString === Shape.prototype.toString; --true
extend2()方法的效率要低于extend()方法,主要是前者对部分原型属性进行了重建
td.toString(); --"shape, shape"
TwoDShape并没有重新定义name属性,所以打印了两个Shape,可以在任何时候重新定义name属性,
TwoDShape.prototype.name = "2D shape";
td.toString();
--"shape, 2D shape"
6.小心处理引用拷贝
对象类型(包括函数与数组)通常都是以引用形式来进行拷贝的,会导致一些预期不同的结果:
function Papa() { }
function Wee() { }
Papa.prototype.name = 'Bear';
Papa.prototype.owns = ["porridge", "chair", "bed"];
让Wee继承Papa(通过extend()或extend2()来实现):
extend2(Wee, Papa);
即Wee的原型继承了Papa的原型属性,并将其变成了自身属性
Wee.prototype.hasOwnProperty('name');
---true
Wee.prototype.hasOwnProperty('owns');
---true
name属于基本类型属性,创建的是一份全新的拷贝,owns属性是一个数组对象,它执行的引用拷贝。
Wee.prototype.owns; -- ["porridge", "chair", "bed"] Wee.prototype.owns === Papa.prototype.owns; --true
改变Wee中的name属性,不会对Papa产生影响:
Wee.prototype.name += ', Little Bear'; --"Bear, Little Bear"
Papa.prototype.name; --"Bear"
如果改变的是Wee的owns属性,Papa就会受到影响,这两个属性在内存中引用的是同一个数组:
--pop() 方法用于删除并返回数组的最后一个元素。
Wee.prototype.owns.pop(); --"bed"
Papa.prototype.owns; --["porridge", "chair"]
用另一个对象对Wee的owns属性进行完全重写(不是修改现有属性),这种情况下,Papa的owns属性将会继续引用原有对象,而Wee的owns属性指向了新对象。
Wee.prototype.owns = ["empty bowl", "broken chair"];
Papa.prototype.owns.push('bed');
Papa.prototype.owns;
-- ["porridge", "chair", "bed"]
7.对象之间的继承
在对象之间进行直接属性拷贝
用var o={}语句创建一个没有任何私有属性的“空”对象作为画板,逐步为其添加属性,将现有对象的属性全部拷贝过来
function extendCopy(p) {
var c = {};
for (var i in p ) {
c[i] = p[i];
}
var twoDee = extendCopy(shape);
twoDee.name = '2D shape';
twoDee.toString = function () {
return this.uber.toString() + ', ' + this.name;
};
c.uber = p; return c; }
创建一个基本对象:
var shape = {
name: 'Shape',
toString: function () {
return this.name;
}
};
根据就对象来创建一个新对象,调用extendCopy()函数,返回一个新对象,继续对这个新对象进行扩展,添加额外的功能
var twoDee = extendCopy(shape);
twoDee.name = '2D shape';
twoDee.toString = function () {
return this.uber.toString() + ', ' + this.name;
};
让triangle对象继承一个2D图形对象。
var triangle = extendCopy(twoDee);
triangle.name = 'Triangle';
triangle.getArea = function () {
return this.side * this.height / 2;
};
使用triangle:
triangle.side = 5;
triangle.height = 10;
triangle.getArea();
--25
triangle.toString();
---"Shape, 2D shape, Triangle"
8.深拷贝
深拷贝的实现方式与浅拷贝基本相同,需要通过遍历对象的属性来进行拷贝操作,在遇到一个对象引用性的属性时,需要再次对其调用深拷贝函数。
当对象被拷贝时,实际上拷贝的只是该对象在内存中的位置指针----浅拷贝(如果我们修改了拷贝对象,就等于修改了原对象)
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;
}
//创建一个对象,包含数组和子对象
var parent = {
numbers: [1, 2, 3],
letters: ['a', 'b', 'c'],
obj: {
prop: 1
},
bool: true
};
在深拷贝中,对拷贝对象的numbers属性进行更改不会对原对象产生影响
var mydeep = deepCopy(parent);
var myshallow = extendCopy(parent);
mydeep.numbers.push(4, 5, 6);
mydeep.numbers; ---- [1, 2, 3, 4, 5, 6]
parent.numbers; ---- [1, 2, 3]
myshallow.numbers.push(10); --4
myshallow.numbers; -- [1, 2, 3, 10]
parent.numbers; -- [1, 2, 3, 10]
mydeep.numbers; --- [1, 2, 3, 4, 5, 6]
push:方法将一个或多个元素添加到数组的末尾,并返回新数组的长度
使用deepCopy()函数注意的地方:
1.在拷贝每个属性之前,使用hasOwnProperty()来确认不会误拷贝不需要的继承属性
2.区分Array对象和普通Object对象相当繁琐,ES5实现了Array.isArray()函数。
if (Array.isArray != "function") {
Array.isArray = function (candidate) {
return Object.prototype.toString.call(candidate) === '[Object Array]';
};
}
9.Object()
基于这种在对象之间直接构建构建继承关系的理念,即可以用Object()函数来接收父对象,并返回一个以对象为原型的新对象