构造函数与实例

借用new 关键词与构造函数是常用的创建对象的方式之一

  1. function Person(name) {
  2. this.name = name;
  3. }
  4. const man = new Person('man');
  5. man;
  6. // Person {name: 'man'}

prototype

  • prototype 是函数的一个对象属性,其指向调用该构造函数而创建的实例的原型
  • prototype 上定义的属性和方法可以被对象实例共享

在上面例子中,我们通过调用 new Person() 创建了对象 man,那么构造函数 Personprototype 属性,便指向了实例对象 man 的原型

  1. Person.prototype;
  2. // { constructor: f }
  3. // {
  4. // constructor: Person(name),
  5. // __proto__: Object
  6. // }

proto

  • __proto__ 是对象的一个属性,其指向该对象的原型
    1. man.__proto__ === Person.prototype; // true
    ES5中新增的创建对象的方法 Object.create() 就是经典的例子
    Object.create() 接收一个对象作为参数,以该对象作为原型,创建一个新的对象 ```javascript const child = Object.create(man); child.proto === man;

// true

  1. <a name="jLy1F"></a>
  2. ### Object.getPrototype()
  3. 新的web标准删除了 `__proto__` 属性,但未正式弃用。而ES5亦提供了替代方法 `Object.getPrototypeOf()`
  4. ```javascript
  5. Object.getPrototypeOf(child) === man; // true

constructor

constructor原型对象指向其构造函数的一个属性

一般是原型对象对其构造函数的引用属性

  1. Super.prototype.constructor === Super; // true

原型链

在了解 __proto__ 时,我们以 man 为原型创建了新的对象 child ,若我们输出 child.name 的值

  1. child.name; // man

会发现我们为并对 child 添加 name 属性,而输出的值却不是 undefined
可以通过打印出 child 来一探究竟
image.png
如你所见, 当输出 child.name 时,实际上是输出了 child.__proto__ 所指向的原型上的属性,即 man.name

同时我们知道,在JavaScript中,函数也是对象的一种,而所有的对象都是由基类 Object 继承而来

  1. Person instanceof Object; // true

通过 prototype 共享属性与方法的特性可以了解 js 继承的原理,因此可以得出

  1. Person.prototype.__proto__ === Object.prototype; // true

而当我们进一步打印出基类 Object__proto__ 属性

  1. Object.prototype.__proto__; // null

实际上,在JavaScript中,当读取对象属性时,浏览器会查找当前对象中是否有该属性,如果找不到,则通过__proto__ 向上查找原型中是否存在该属性并以此类推,直到最顶层的原型对象为止。
Object 基类不存在再往上的原型对象,即为 null

我们可以通过关系图进行总结得出原型链如下:

从原型到继承 - 图2

JS中的继承实现

在JavaScript中,实际上并没有真正的类,其对象继承是基于原型,与经典模型的OOP不同,JS中的继承实际上是源于构造函数中的prototype 属性。
该属性上定义的属性和方法可以被对象实例共享,而每个对象都有隐式属性__proto__ 并指向其构造函数的prototype,并可通过此原型链进行关联访问,从而呈现出实例对象继承原型prototype的样子

原型链继承

  1. function Person() {
  2. this.value = 1;
  3. this.arr = [1];
  4. }
  5. Person.prototype.log = function() { console.log('JavaScript') };
  6. function Child() {}
  7. Child.prototype = new Child();
  8. const boy = new Child();
  9. const girl = new Child();
  10. boy.value; // 1
  11. girl.value; // 1
  12. boy.log(); // JavaScript
  13. girl.log(); // JavaScript
  14. boy.arr.push(2); // [1, 2]
  15. girl.arr; // [1, 2]

通过 prototype 属性/方法可以被共享的性质实现继承,便是原型链继承,也是ECMAScript的主要继承方式。
当访问实例属性/方法时,将通过原型链机制读取到父级的属性/方法。
但这种继承方式存在两个问题:

  • 实例化子类时无法给父类构造函数传参
  • 子类 prototype 指向同一个原型,原型中包含的引用值会在所有实例间共享

借用构造函数

  1. function Person(sex) {
  2. this.sex = sex;
  3. this.arr = [1];
  4. }
  5. Person.prototype.log = function() { console.log(this.sex) };
  6. function Child(sex) {
  7. Person.call(this, sex);
  8. }
  9. const boy = new Child('boy');
  10. const girl = new Child('girl');
  11. boy; // { sex: 'boy', arr: [1] }
  12. girl; // { sex: 'girl', arr: [1] }
  13. boy.arr.push(1); // [1, 2]
  14. girl.arr; // [1]
  15. boy.log; // undefined
  16. girl.log; // undefined

在子类构造函数中调用父类构造函数,妙用 Function.prototype.call/apply ,以新的对象为上下文执行构造函数。
相比原型链继承,这种继承方式可以向父类传参,也不存在共享同个原型上的引用属性。
但是可以发现,子类实例没有继承父类原型上定义的方法

注:仅原型方法无法继承

组合继承

  1. function Person(sex) {
  2. this.sex = sex;
  3. this.arr = [1];
  4. }
  5. Person.prototype.log = function() { console.log(this.sex) };
  6. function Child(sex) {
  7. Person.call(this, sex); // 继承实例属性
  8. }
  9. Child.prototype = new Person(); // 继承属性与方法
  10. const boy = new Child('boy');
  11. const girl = new Child('girl');
  12. boy; // { sex: 'boy', arr: [1] }
  13. girl; // { sex: 'girl', arr: [1] }
  14. boy.arr.push(1); // [1, 2]
  15. girl.arr; // [1]

组合使用 原型链继承借用构造函数 两种方式,综合其优点:

  • 通过原型链继承父类属性与方法
  • 借用构造函数继承实例属性

但很明显,我们调用了两次父类构造函数,这是需要优化的。

原型式继承

由JavaScript布道者 Douglas Crockford《Prototypal Inheritance in JavaScript》提出:

  1. function object(o) {
  2. function F() {};
  3. F.prototype = o;
  4. return new F();
  5. }

借用临时构造函数,将传入的对象作为其原型对象并返回其实例。
这种继承方式更贴近JavaScript原型性质,新对象通过共享基础对象的 prototype 原型属性,实现原型继承。
ES5将其规范化实现,增加了 Object.create() 方法

  1. if (typeof Object.create !== 'function') {
  2. Object.create = function (o) {
  3. function F() {}
  4. F.prototype = o;
  5. return new F();
  6. };
  7. }
  8. const newObject = Object.create(oldObject);

但借用 prototype 共享属性也就意味着这种方式与 原型链继承 一样存在引用数据会在所有实例间共享的问题

寄生式继承

  1. function createObject(o) {
  2. const obj = object(o); // 以原型式继承为基础
  3. obj.log = function() { console.log('new object') } ; // 增强对象
  4. return obj;
  5. }

在原型式继承的基础上,借用工厂函数并以自定义方式增强对象。
这种继承方式就是原型式继承的扩展,但并没有解决实例共享引用数据的问题,且通过这种方式增强对象难以复用。
相同的是:原型式继承与寄生式继承都是把重点放在对象上,而不用关注构造函数与类型

组合寄生式继承

回看 组合继承 方式,我们在其基础上优化调用两次父类构造函数的问题,就可以得到一个目前最佳的继承方式。
而上述多种继承方式也明显指出直接指定子类原型对象避免不了引用数据会在所有实例间共享的问题:

  1. Child.prototype = new Person();
  2. // 不用多次调用父类构造函数,但引用数据仍共享
  3. Child.prototype = Person.prototype;

因此需要借用寄生式继承来继承父类原型

  1. // 以父类原型为基础创建一个新的对象,并赋值给子类原型
  2. Child.prototype = Object.create(Person.prototype);

但由于此时子类原型被重写为以父类原型为基础创建的对象,那么其原型对象所指向的也就是父类构造函数,即

  1. Child.prototype.constructor === Person;
  2. // true

因此需要修正子类原型对象的正确指向

  1. Child.prototype.constructor = Child;

到此,糅合多种继承方式的优点而成的最佳继承方式就实现了

  1. function Person(sex) {
  2. this.sex = sex;
  3. this.arr = [1];
  4. }
  5. Person.prototype.log = function() { console.log(this.sex) };
  6. function Child(sex) {
  7. Person.call(this, sex); // 继承实例属性
  8. }
  9. // 以父类原型为基础创建一个新的对象,并赋值给子类原型
  10. Child.prototype = Object.create(Person.prototype);
  11. // 修正重写子类原因导致的constructor错误指向
  12. Child.prototype.constructor = Child;

ES6 Class

  1. class Person {
  2. static isSuper = true;
  3. constructor(sex) {
  4. this.sex = sex;
  5. }
  6. }
  7. class Child extends Person {
  8. constructor() {
  9. super();
  10. }
  11. }

通过babel编译来了解其背后的实现原理:(具体结果看 babel转译结果

  1. // 实现继承
  2. function _inherits(subClass, superClass) {
  3. if (typeof superClass !== "function" && superClass !== null) {
  4. throw new TypeError("Super expression must either be null or a function");
  5. }
  6. subClass.prototype = Object.create(superClass && superClass.prototype, {
  7. constructor: { value: subClass, writable: true, configurable: true },
  8. });
  9. if (superClass) {
  10. subClass.__proto__ = superClass;
  11. }
  12. }
  13. // 执行构造函数
  14. function _createSuper(Derived) {
  15. ...
  16. return Super.apply(this, arguments);
  17. }
  18. // 避免把构造函数当成普通函数执行的验证,即需要通过 new 调用
  19. function _classCallCheck() {
  20. if (!instance instanceof Constructor) {
  21. throw new TypeError("Cannot call a class as a function");
  22. }
  23. }
  24. // 父类构造函数
  25. var Person = function Person(sex) {
  26. _classCallCheck(this, Person);
  27. this.sex = sex;
  28. };
  29. Object.defineProperty(Person, "isSuper", true);
  30. // 子类构造函数
  31. var Child = /*#__PURE__*/ (function (_Person) {
  32. _inherits(Child, _Person);
  33. var _super = _createSuper(Child);
  34. function Child() {
  35. _classCallCheck(this, Child);
  36. return _super.call(this);
  37. }
  38. return Child;
  39. })(Person);

为了方便浏览我删除并修改了辅助函数相关的代码,可以看出ES6 Class本质也是组合寄生式继承的实现

参考文献