JavaScript继承
默认的继承方法:通过原型来实现继承关系链
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()函数来接收父对象,并返回一个以对象为原型的新对象