灏天阁

原型继承

· Yin灏

原型链

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

- Book Lists -