原型继承

JavaScript不是真正意义上的面向对象语言,没有提供传统的继承方式,它提供的是一种叫做原型继承的方式。但是它拥有面向对象和函数式的编程特点,理解原型继承,对我们使用JS来实现面向对象很有帮助。

原型

在Javascript中,每个函数都有一个原型属性prototype指向函数自身的原型,而由这个函数创建的对象也有一个proto属性指向这个原型,而函数的原型是一个对象,所以这个对象也会有一个proto指向自己的原型,这样逐层深入直到Object对象的原型(null),这样就形成了原型链。

结合一下图,认真思考下:

prototype-lauout

理解几点:

  • prototype: 在函数身上,指向原型对象
  • proto: 在对象身上(包括函数创建的对象, 函数本身和原型对象),指向自身的原型
  • constructor: 在原型对象上,指向构造函数, 在多级继承的时候,指明构造函数方便在对象上扩展原型属性
  • Object.protp为null: 原型的顶端

    原型使用方式

方式一

通过给Calculator对象的prototype属性赋值对象字面量来设定Calculator对象的原型。

  1. var Calculator = function (decimalDigits, tax) {
  2. this.decimalDigits = decimalDigits;
  3. this.tax = tax;
  4. };
  5. Calculator.prototype = {
  6. add: function (x, y) {
  7. return x + y;
  8. },
  9. subtract: function (x, y) {
  10. return x - y;
  11. }
  12. };
  13. //alert((new Calculator()).add(1, 3));

方式二

赋值原型prototype的时候使用function立即执行的表达式来赋值,即如下格式:Calculator.prototype = function () { } ();, 可以封装私有的function,通过return的形式暴露出简单的使用名称,以达到public/private的效果。

  1. Calculator.prototype = function () {
  2. add = function (x, y) {
  3. return x + y;
  4. },
  5. subtract = function (x, y) {
  6. return x - y;
  7. }
  8. return {
  9. add: add,
  10. subtract: subtract
  11. }
  12. } ();
  13. //alert((new Calculator()).add(11, 3));

方式三

分别设置原型对象:

  1. var Calculator = function (decimalDigits, tax) {
  2. this.decimalDigits = decimalDigits;
  3. this.tax = tax;
  4. };
  5. Calculator.prototype.add = function (x, y) {
  6. return x + y;
  7. };
  8. Calculator.prototype.subtract = function (x, y) {
  9. return x - y;
  10. };

原型的作用

最主要的一点是数据共享,创建对象的时候,我们会把公共的方法和属性挂载到原型上,避免资源浪费。

原型链

原型对象也有自己的原型,直到对象的原型为 null 为止(也就是没有原型)。这种一级一级的链结构就称为原型链。原型继承的模型就是JavaScript实现继承的原理。真正形成原型链的是每个对象的proto属性,而不是函数的prototype属性,这是很重要的。

  1. function Foo() {
  2. this.value = 42;
  3. }
  4. Foo.prototype = {
  5. method: function() {}
  6. };
  7. function Bar() {}
  8. // 设置Bar的prototype属性为Foo的实例对象
  9. Bar.prototype = new Foo();
  10. Bar.prototype.foo = 'Hello World';
  11. // 修正Bar.prototype.constructor为Bar本身
  12. Bar.prototype.constructor = Bar;
  13. var test = new Bar() // 创建Bar的一个新实例
  14. // 原型链
  15. test [Bar的实例]
  16. Bar.prototype [Foo的实例]
  17. { foo: 'Hello World' }
  18. Foo.prototype
  19. {method: ...};
  20. Object.prototype
  21. {toString: ... /* etc. */};

上面的例子中,test 对象从 Bar.prototypeFoo.prototype 继承下来;因此, 它能访问 Foo 的原型方法 method。同时,它也能够访问那个定义在原型上的 Foo 实例属性 value。 需要注意的是 new Bar() 不会创造出一个新的 Foo 实例,而是 重复使用它原型上的那个实例;因此,所有的 Bar 实例都会共享相同的 value 属性。

原型链使用方式

通常情况下,我们在实现继承的时候,会将一个函数的原型指向另一个函数的实例对象,而不是函数的原型本身。因为这将会导致两个对象共享相同的原型。 因此,改变任意一个对象的原型都会影响到另一个对象的原型,在大多数情况下这不是希望的结果。

也不要使用 Bar.prototype = Foo,因为这不会执行 Foo 的原型,而是指向函数 Foo。 因此原型链将会回溯到 Function.prototype 而不是 Foo.prototype,因此 method 将不会在 Bar 的原型链上。

重写原型方法

在使用第三方JS类库的时候,往往有时候他们定义的原型方法是不能满足我们的需要,但是又离不开这个类库,所以这时候我们就需要重写他们的原型中的一个或者多个属性或function,我们可以通过继续声明的同样的add代码的形式来达到覆盖重写前面的add功能,代码如下:

  1. Calculator.prototype.add = function (x, y) {
  2. return x + y + this.tax;
  3. };

原理就是让在原型链上的查找能够就近解决。需要注意的是重写的代码需要放在最后,这样才能覆盖前面的代码。

属性查找

当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止。

到查找到达原型链的顶部, 也就是 Object.prototype, (因为Object的原型的proto是null) 但是仍然没有找到指定的属性,就会返回 undefined。

hasOwnProperty函数

为了判断一个对象是否包含自定义属性而不是原型链上的属性, 我们需要使用继承自 Object.prototypehasOwnProperty方法。它是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。

在使用for…in遍历属性的时候,可以方便过滤是否是自身属性。

原型继承与类继承的区别

基于类的继承 原型继承
类是不可变的。在运行时,你无法修改或者添加新的方法 原型是灵活的。它们可以是不可变的也可以是可变的
类可能会不支持多重继承 对象可以继承多个原型对象
基于类的继承比较复杂。你需要使用抽象类,接口和final类等等 原型继承比较简洁。你只有对象,你只需要对对象进行扩展就可以了

创建方式

new 运算符是如何工作的

造物者 Brendan Eich 想让JS和传统的面向对象的编程语言差不太多,如Java和C++。在这些语言里,我们采用 new 运算符来给类实例化一个新的对象。所以他在JS里写了一个 new 运算符:

  • C++里有用来初始化实例属性的构造函数概念,因此 new 运算符必须针对函数。
  • 我们需要将对象的方法放到一个地方去,既然我们在用原型语言,我们就把它放到函数的原型属性中去。
    new 运算符接受一个函数 F 及其参数:new F(arguments…)。这一过程分为三步:

  • 创建类的实例。这步是把一个空的对象的 proto 属性设置为 F.prototype 。

  • 初始化实例。函数 F 被传入参数并调用,关键字 this 被设定为该实例。
  • 返回实例。
    注意构造函数中的this关键字,它就代表了新创建的实例对象。

看个实例:

  1. function Point(x, y) {
  2. this.x = x;
  3. this.y = y;
  4. }
  5. Point.prototype = {
  6. print: function () { console.log(this.x, this.y); }
  7. };
  8. var p1 = new Point(10, 20);
  9. p1.print(); // 10 20
  10. console.log(p1 instanceof Point); // true
  11. var p2 = New (Point)(10, 20);
  12. p2.print(); // 10 20
  13. console.log(p2 instanceof Point); // true

构造模式和原型模式对比

构造模式 原型模式
函数式特点无法与new关键字一起使用 函数式特点可以与create结合使用
忘记使用new会导致无法预期的bug并且会污染全局变量 由于create是一个函数,所以程序总是会按照预期工作
使用构造函数的原型继承比较复杂并且混乱 使用原型的原型继承简洁易懂

参考资料

原文: https://leohxj.gitbooks.io/front-end-database/content/javascript-basic/prototypal-inheritance.html