Javascript 继承篇
来源:互联网 发布:国泰安数据库 登陆 编辑:程序博客网 时间:2024/05/22 12:52
1. Prototype 链(Prototype chaining)
Javascript 是一种动态语言,实现一个目标通常有多种方式,继承也不例外。首先我们介绍下实现继承最普遍的方式 :利用 Prototype 链。
这里假设你已经对 prototype 以及 __proto__ 有了一定的了解,否则请先参考 Javascript 之 Prototype
prototype 链的示意图:
接下来的例子我们都采用类似示意图中的3层继承结构:最顶层为一个 Sharp 类,它有一个 名为 TwoDSharp 的子类,最后还有一个名为 Triangle 的类继承自 TwoDSharp。
1.1 Prototype 链示例
首先定义这3个类:
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();// 修正 constructor 信息TwoDShape.prototype.constructor = TwoDShape;Triangle.prototype.constructor = Triangle;
这里需要指出: Javascript 操作的是对象而不是类,所以 prototype 属性需要一个实例化对象来实现继承,而不像其他 OO 语言直接声明继承自某一个类。
另外在你通过这种方法实现继承后,你再去修改甚至删除父类的函数,已经没有什么关系了,因为我们的继承关系是建立在实例对象上的。
var my = new Triangle(5, 10);// 自属性my.getArea();// 25// 父属性my.toString();// "Triangle"// 修正 constructor 的语句让我们能获得正确的信息my.constructor === Triangle;// true// 类型确认my instanceof Shape;// truemy instanceofTwoDShape;// truemy instanceof Triangle;// trueShape.prototype.isPrototypeOf(my);// trueTwoDShape.prototype.isPrototypeOf(my);// trueTriangle.prototype.isPrototypeOf(my);// true
1.2 把共有属性转到 prototype 上
当你用构造函数创建实例对象时,你可以通过 this 关键字来为其追加属性。当这些属性是只读时,这种做法值得商榷:
function Shape(){ this.name = 'Shape';}
这意味着你每一个 Shape 实例对象在内存中都保存着一份 name 属性的值,这显然是不必要的,可以尝试把属性转移到 prototype 上:
function Shape() { ... }Shape.prototype.name = 'Shape';
利用这种方法,我们可以改进一下前面的函数定义:
// 构造函数function Shape() {}// 扩展 prototypeShape.prototype.name = 'Shape';Shape.prototype.toString = function () { return this.name;};// 构造函数function TwoDShape() {}// 继承实现TwoDShape.prototype = new Shape();TwoDShape.prototype.constructor = TwoDShape;// 扩展 prototypeTwoDShape.prototype.name = '2D shape';function Triangle(side, height) { this.side = side; this.height = height;}// 继承实现Triangle.prototype = new TwoDShape();Triangle.prototype.constructor = Triangle;// 扩展 prototypeTriangle.prototype.name = 'Triangle';Triangle.prototype.getArea = function () { return this.side * this.height / 2;};
验证一下:
var my = new Triangle(5, 10);my.getArea();// 25my.toString();// "Triangle"TwoDShape.prototype.isPrototypeOf(my);// truemy instanceof Shape;// true
输出结果都满足预期,这里要指出一个细节:my 上的 toString() 方法要比改进前多一步查找,因为原先这个函数就在 Shape 类上,而现在它在 Shape.prototype 上。
2. 仅继承 prototype
前面的例子可以看到在 prototype 上扩展可重用的属性和方法的优势,那么直接从 prototype 继承显然是个不错的方法,至少它带来两个好处:
- 不用再实例化对象来实现继承
- 运行时定位属性/方法的效率高了
function Shape() {}// 扩展 prototypeShape.prototype.name = 'Shape';Shape.prototype.toString = function () { return this.name;};function TwoDShape() {}// 实现继承TwoDShape.prototype = Shape.prototype;TwoDShape.prototype.constructor = TwoDShape;// 扩展 prototypeTwoDShape.prototype.name = '2D shape';function Triangle(side, height) { this.side = side; this.height = height;}// 实现继承Triangle.prototype = TwoDShape.prototype;Triangle.prototype.constructor = Triangle;// 扩展 prototypeTriangle.prototype.name = 'Triangle';Triangle.prototype.getArea = function () { return this.side * this.height / 2;};// 测试var my = new Triangle(5, 10);my.getArea();// 25my.toString();// "Triangle"
如果你细究一下,你会发现现在 my 的 toString() 函数定位高效了不少。
这里要提一下这个方法的缺点:由于父子的 prototype 都指向同一个对象,当其中一方修改时就会影响到另一方:
Triangle.prototype.name = 'Triangle';var s = new Shape();s.name;// "Triangle"
这显然不可接受啊!
3. 临时构造函数 – new F()
前面介绍的继承方法非常高效,却有个致命的缺点。我们引入一个临时的构造函数来解决:
function Shape() {}// 扩展 prototypeShape.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;// 扩展 prototypeTwoDShape.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;// 扩展 prototypeTriangle.prototype.name = 'Triangle';Triangle.prototype.getArea = function () { return this.side * this.height / 2;};// 测试var s = new Shape();s.name;// "Shape""I am a " + new TwoDShape(); // 调用 toString()// "I am a 2D shape"
3.1 子类中访问父类
传统的 OO 语言通常都有一个关键字(super/base等)来代表父类,这种方式下子类能非常方便的访问父类成员。
在 Javascript 中没有类似的语法,但我们依然能巧妙的实现类似的功能,下例中我们建立了一个 uber 属性来指向父的 prototype 对象。
function Shape() {}// 扩展 prototypeShape.prototype.name = 'Shape';Shape.prototype.toString = function () { var const = this.constructor; return const.uber ? this.const.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;// 扩展 prototypeTwoDShape.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;// 扩展 prototypeTriangle.prototype.name = 'Triangle';Triangle.prototype.getArea = function () { return this.side * this.height / 2;};
这里修改了两个内容:
- 建立了一个 uber 属性来指向父类的 prototype 对象
- 一个新的 toString() 函数,用来帮助我们验证这套运行机制
var my = new Triangle(5, 10);my.toString();// "Shape, 2D shape, Triangle"
这里的属性名 uber 可以任意命名,但并不建议采用以下的常用名称。
“superclass”:这暗示了 Javascript 中有类的概念,而实际却不是这样
“super”:这是 Java 中的父对象关键字,在 Javascript 是保留字(虽然没作用)
3.2 把继承逻辑合到一个 function 中
现在我们把继承的实现逻辑提取出来做成一个 fucntion,方便调用:
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 () { 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"
4. 复制属性
现在换一下思路,既然我们继承的目的是重用代码,那么把一个对象的成员简单地复制到另一个对象不也可以吗?我们这就新建一个 extend2() 函数来实验一下这个方法:
function extend2(Child, Parent) { var p = Parent.prototype; var c = Child.prototype; for (vari in p) { c[i] = p[i]; } c.uber = p;}
这里没有设置 Child.prototype.constructor 的原因是:我们没有整个替换掉 Child.prototype 对象,而是在其本身上进行了扩展。
这个方法比起前面的 extend() 来说有一个缺点:每个子类成员都被做了一个副本,而前者是通过 prototype 链。另外需要注意的是这个复制操作仅仅对原始(primitive)类型有效,对于 object 类型(包括 function 及 array)来说因为它们是基于引用传递的,你复制的仅仅是它们的地址!
var Shape = function () {};varTwoDShape = function () {};Shape.prototype.name = 'Shape';Shape.prototype.toString = function () { return this.uber ? this.uber.toString() + ', ' + this.name : this.name;};// extend()extend(TwoDShape, Shape);var td = new TwoDShape();td.name;// "Shape"TwoDShape.prototype.name;// "Shape"td.__proto__.name;// "Shape"td.hasOwnProperty('name');// falsetd.__proto__.hasOwnProperty('name');// false// extend2()extend2(TwoDShape, Shape);var td = new TwoDShape();td.__proto__.hasOwnProperty('name');// truetd.__proto__.hasOwnProperty('toString');// truetd.__proto__.toString === Shape.prototype.toString;// true
子成员都需要做一个副本的话 extend2() 似乎并不高效,但这没有想象的那么糟糕因为实际上我们仅仅对原始(primitive)类型做了复制;另外当 Javascript 引擎在 prototype 链上查找成员时查找的路径变短了。
4.1 注意引用拷贝
有时候引用拷贝的结果并不是你期待的,比如下面的例子:
function Papa() {}function Wee() {}Papa.prototype.name = 'Bear';Papa.prototype.owns = ["porridge", "chair", "bed"];extend2(Wee, Papa);Wee.prototype.hasOwnProperty('name');// trueWee.prototype.hasOwnProperty('owns');// trueWee.prototype.owns;// ["porridge", "chair", "bed"]Wee.prototype.owns=== Papa.prototype.owns;// trueWee.prototype.name += ', Little Bear';// "Bear, Little Bear"Papa.prototype.name;// "Bear"// 对 Wee 的 owns 属性的修改会作用到 Papa 上Wee.prototype.owns.pop();// "bed"Papa.prototype.owns;// ["porridge", "chair"]
但是当你整个替换 Wee 的 owns 属性时,又是另外一番情形了:
Wee.prototype.owns= ["empty bowl", "broken chair"];Papa.prototype.owns.push('bed');Papa.prototype.owns;// ["porridge", "chair", "bed"]
这里用一张示意图来分析一下这个过程:
- 生成一个新 object对象,A 中存放指向它的地址
- 声明一个新变量 B,把 A 中的地址复制一份给 B
- 修改 B.code 意味着修改 B 的地址指向内容的 code,由于 A 的指向和 B 的一致,所以修改也会作用到 A 上。
- 一个新对象被创建,B 的地址被改为指向这个新的对象。A 和 B 指向了不同的内容,不再互相影响了。
5. 从实例对象继承
前面的篇章我们都在构造函数上做文章,通过设置这些函数的 prototype 来实现继承。之前提到过 Javascript 中并没有类的概念,所以它的继承实际上就是代码的重用,前面的 extend2() 就是个比较直接的方式,但它只是在构造函数本身上动手脚。让我们更简单粗暴一点,跳过构造函数直接生成子类的实例对象岂不更好?
function extendCopy(p) { var c = {}; for (vari in p) { c[i] = p[i]; } c.uber = p; return c;}
利用新的 extendCopy() 来实现一下前面的继承结构:
var shape = { name: 'Shape', toString: function () { return this.name; }};var twoDee = extendCopy(shape);twoDee.name = '2D shape';twoDee.toString = function () { return this.uber.toString() + ', ' + this.name;};var triangle = extendCopy(twoDee);triangle.name = 'Triangle';triangle.getArea = function () { return this.side * this.height / 2;};// 测试triangle.side = 5;triangle.height = 10;triangle.getArea();// 25triangle.toString();// "Shape, 2D shape, Triangle"
新的实现让我们不再需要定义子类的构造函数,但某些情形下你可能需要通过构造函数传入一些参数(譬如:创建 triangle 实例的同时传入 side,height),这要解决起来也是很简单的:
- 增加一个 init() 来传参数
- 在 extendCopy() 上添加第二个参数,用来传递这些信息
6. 深拷贝
你已经理解 Javascript 中的引用拷贝的内部原理:它们只是指针的拷贝,这被称之为浅拷贝。那么复制指针指向的内容就称之为深拷贝,它们的实现代码非常相似,深拷贝只是浅拷贝的一个“递归”版本:
function deepCopy(p, c) { c = c || {}; for (vari 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};var mydeep = deepCopy(parent);var myshallow = extendCopy(parent);mydeep.numbers.push(4,5,6);// 6mydeep.numbers;// [1, 2, 3, 4, 5, 6]parent.numbers;// [1, 2, 3]myshallow.numbers.push(10);// 4myshallow.numbers;// [1, 2, 3, 10]parent.numbers;// [1, 2, 3, 10]mydeep.numbers;// [1, 2, 3, 4, 5, 6]
最后补充两点:
- 使用 hasOwnProperty() 来排除非自有属性,以此保证不会附加多余的属性
- Array.isArray() 是 ES5 的规范,如果你的 Javascript 环境中没有别惊慌,使用下面的 Polyfill:
if (Array.isArray !== "function") { Array.isArray = function (candidate) { return Object.prototype.toString.call(candidate) === '[object Array]'; };}
7. object()
基于从对象继承的模式,有人提出使用一个 object() 函数来实现:
function object(o) { function F() {} F.prototype = o; return new F();}
如果需要对父操作,可以改为:
// 虽然是从实例对象继承,但这种模式内部使用 prototype 继承的方式function object(o) { var n; function F() {} F.prototype = o; n = new F(); n.uber = o; return n;}
使用起来与 extendCopy() 一样,也是传入一个实例对象:
var triangle = object(twoDee);triangle.name = 'Triangle';triangle.getArea = function () { return this.side * this.height / 2;};triangle.toString();// "Shape, 2D shape, Triangle"// 在 ES5 中内置了类似的函数 Object.create()var square = Object.create(triangle);
8. prototype 继承 与 成员拷贝的混合使用
继承的时候,你总是想能够利用既有的功能,以此为基础进行扩展,同时利用前面的两种继承方式可以让我们很容易达成该目的。
- 使用 prototype 继承来获取既有对象的功能
- 使用成员拷贝来扩展新的实例对象
function objectPlus(o, stuff) { var n; function F() {} F.prototype = o; n = new F(); n.uber = o; for (vari in stuff) { n[i] = stuff[i]; } return n;}
objectPlus() 函数的第一个参数用来 prototype 继承,第二个参数用来扩展,来看看具体的使用:
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});my.getArea();// 8my.toString();// "Shape, 2D shape, Triangle, Triangle"
最后的输出出现了两个 Triangle,这是因为实例对象 my 继承自 Triangle,导致继承层数多了一层,如果创建时给 name 赋个值就能分辨出这个原由了:
objectPlus(triangle, { side: 4, height: 4, name: 'My 4x4'}).toString();// "Shape, 2D shape, Triangle, My 4x4"
objectPlus() 与 ES5 中的 Object.create() 非常相似,唯一不同的是第二个参数。详细可参考相关的文档。
9. 多重继承
多重继承就是子类有多个父类,在 OO 语言中许多都不支持,当你遇到是否引入多重继承时需要考虑再三,因为它可能给你带来便利的同时增加程序的复杂度,同时打断直观的继承链。
采用成员拷贝的做法能够很容易地实现多重继承,你甚至可以继承自无限多个父对象。下面的 multi() 函数接受一个父对象数组,内部使用 arguments 关键字来遍历这个数组,并逐一复制:
function multi() { var n = {}, stuff, j = 0, len = arguments.length; for (j = 0; j <len; j++) { stuff = arguments[j]; for (vari in stuff) { if (stuff.hasOwnProperty(i)) { n[i] = stuff[i]; } } } return n;}// 使用var shape = { name: 'Shape', toString: function () { return this.name; }};vartwoDee = { name: '2D shape', dimensions: 2};var triangle = multi(shape, twoDee, { name: 'Triangle', getArea: function () { return this.side * this.height / 2;}, side: 5, height: 10});// 测试triangle.getArea();// 25triangle.dimensions;// 2triangle.toString();// "Triangle"
multi() 采用顺序遍历的方式复制类成员,所以如果父对象的成员有相同的名字,最后出现的作用在子对象上
混合体(Mixins)
混合体是一种多个对象的合体,它通常具有所有这些合体对象的功能(成员),但本质上并不是这些对象的子对象。(前文的 multi() 生成的结果就是一个混合体)
10. 寄生式继承(Parasitic inheritance)
寄生式继承:把一个对象上的所有功能移到新的实例对象上,并对新的实例对象进行扩展。
var twoD = { name: '2D shape', dimensions: 2};// 寄生式继承function triangle(s, h) { // 使用 object() 来拷贝对象上的所有成员 var that = object(twoD); // 修改/扩展 that.name ='Triangle'; that.getArea = function () { return this.side * this.height / 2; }; that.side = s; that.height = h; // 返回新实体 return that;}
使用时注意不用 new 关键字:
var t = triangle(5, 10);t.dimensions;// 2vart2 = new triangle(5,5);t2.getArea();// 12.5
11. 构造函数的借用
这是本文介绍的最后一种实现继承的方式:子对象使用 call() 或 apply() 来调用父对象的构造函数。
如果你对 call() 或 apply() 不是非常了解,这里简短的介绍一下:
这两个方法让你调用函数的时候传入一个对象,并把该函数中的 this 与传入的这个对象进行绑定。
// 父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);t.name;// "Triangle"t.id;// 101t.toString();// "[object Object]"
实例 Triangle 对象从 Shape 上继承了 id 属性,但是并没有得到父的 prototype 上的内容。根据前文的介绍,我们知道可以这样修改来解决这个问题:
// 子function Triangle() { Shape.apply(this, arguments);}Triangle.prototype = new Shape();Triangle.prototype.name = 'Triangle';
这里有个小问题问题:父的构造函数被调用了两次,一次是 apply() 发起的,一次是设置 prototype 的时候。
- Javascript 继承篇
- javascript内部原理篇[javascript实现继承]
- javascript继承
- javascript继承
- javascript 继承
- Javascript继承
- Javascript继承
- javascript继承
- Javascript继承
- javascript继承
- JavaScript 继承
- javascript 继承
- JavaScript 继承
- javascript 继承
- javascript 继承
- Javascript继承
- Javascript继承
- Javascript 继承
- C++之派生类的构造函数和析构函数
- 欢迎使用CSDN-markdown编辑器
- 四招让你赢得高薪
- jQuery UI draggable+droppable+resizable+selectable+sortable
- 【hadoop Hbase】hbase的安装
- Javascript 继承篇
- Git提交到多个远程仓库
- Alcatraz使用
- iOS的多线程Core Data
- 观察者模式 Python版--第14章
- JavaIO流详解——Java语言I/O输入输出流read()readFully()
- uva 10954 add all
- PHPExcel 运用
- sql优化工具