lampkid / blog

the Source of My blog generated by hexo
1 stars 0 forks source link

JS中类的实现 #2

Open lampkid opened 5 years ago

lampkid commented 5 years ago

引子

开始之前,先说下为什么会研究这个问题? 在分析别人的node代码,比如express、koa等时,发现了两种不同定义路由方法或者我们称之为action的方案。

静态方法

function MyController() {
}
MyController.doAction = function() {
}

对象方法

class MyController {
doAction() {
}
}
const ctrl = new MyController()

两种方案都在实际生产中有实践。要分析两种方案的优劣和区别,得先了解class方案实际上是怎么实现的。 然后从内存、性能、写法等多方面可以考量。

本文不讨论何种方案优劣,只作为本篇文章的引子!

正文

先写一个最简单的空类

源码

class MyClass {
}

babel编译

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var MyClass = function MyClass() {
  _classCallCheck(this, MyClass);
};

分析

定义一个类,实际上定义了一个函数,只是比普通函数多了一个实例检查,判断该实例是否是通过构造函数创建的。一般我们创建一个类的实例使用new MyClass()关键字。

定义一个类的方法

源码

定义一个doAction方法

class MyClass {
  doAction() {
    console.log('do something...');
  }
}

babel编译

"use strict";

var _createClass = function() {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || false; // 是否可枚举,如for ... in , Object.keys
      descriptor.configurable = true;   // 是否可配置
      if ("value" in descriptor) descriptor.writable = true; // 是否可修改
      Object.defineProperty(target, descriptor.key, descriptor); // 通过defineProperty定义类的方法
    }
  }
  return function(Constructor, protoProps, staticProps) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps); // 原型属性定义
    if (staticProps) defineProperties(Constructor, staticProps);    // 静态属性定义,下面再讨论。
    return Constructor;
  };
} ();

function _classCallCheck(instance, Constructor) {
  if (! (instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var MyClass = function() { // 类通过一个立即执行函数包裹并返回
  function MyClass() { // 定义构造函数,并验证实例是否正确。
    _classCallCheck(this, MyClass);
  }

  _createClass(MyClass, [{ // 通过_createClass方法定义类的方法
    key: "doAction",
    value: function doAction() {
      console.log('do something...');
    }
  }]);

  return MyClass;
} (); 

分析

  1. 类通过一个立即执行函数包裹并返回
  2. 定义构造函数MyClass,并调用_classCallCheck函数验证实例是否正确
  3. 通过_createClass函数定义类的方法

定义一个类的静态方法

源码

class MyClass {
  doAction() {
    console.log('do something...');
  }
  // 静态方法
  static doStaticAction() {
    console.log('do something static');
  }
}

babel编译

'use strict';

var _createClass = function() {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || false;
      descriptor.configurable = true;
      if ("value" in descriptor) descriptor.writable = true;
      Object.defineProperty(target, descriptor.key, descriptor);
    }
  }
  return function(Constructor, protoProps, staticProps) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    if (staticProps) defineProperties(Constructor, staticProps); // here
    return Constructor;
  };
} ();

function _classCallCheck(instance, Constructor) {
  if (! (instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var MyClass = function() {
  function MyClass() {
    _classCallCheck(this, MyClass);
  }

  _createClass(MyClass, [{
    key: 'doAction',
    value: function doAction() {
      console.log('do something...');
    }
  }], [{
    key: 'doStaticAction', // here,_createClass的第三个参数
    value: function doStaticAction() {
      console.log('do something static');
    }
  }]);

  return MyClass;
} ();

解释

我们可以在_createClass函数中发现:

    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    if (staticProps) defineProperties(Constructor, staticProps); // here

普通方法和静态方法唯一的区别在于:方法是定一个在了构造函数的原型上还是构造函数本身。 我们知道。我们调用一个类的普通方法时,我们需要实例化。而调用静态方法时,可直接用类名直接引用调用。如MyClass.doStaticAction()

定义一个类的属性和静态属性

源码

class MyClass {

  prop = 1;  // 定义一个实例属性
  static staticProp = 2; // 定义一个静态属性

  doAction() {
    console.log('do something...');
  }
  static doStaticAction() {
    console.log('do something static');
  }
}

babel编译

'use strict';

var _createClass = function() {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || false;
      descriptor.configurable = true;
      if ("value" in descriptor) descriptor.writable = true;
      Object.defineProperty(target, descriptor.key, descriptor);
    }
  }
  return function(Constructor, protoProps, staticProps) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    if (staticProps) defineProperties(Constructor, staticProps);
    return Constructor;
  };
} ();

function _classCallCheck(instance, Constructor) {
  if (! (instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var MyClass = function() {
  function MyClass() {
    _classCallCheck(this, MyClass);

    this.prop = 1;  // 定义的实例属性
  }

  _createClass(MyClass, [{
    key: 'doAction',
    value: function doAction() {
      console.log('do something...');
    }
  }], [{
    key: 'doStaticAction',
    value: function doStaticAction() {
      console.log('do something static');
    }
  }]);

  return MyClass;
} ();

MyClass.staticProp = 2; // 定义的静态属性

解释

是不是发现了什么不一样的东东。 为什么方法和静态方法通过Object.defineProperty定义的,而属性和静态属性不一样了? 为什么没有把prop属性定义到构造函数的prototype上? 为什么没有通过Object.defineProperty将staticProp定义到构造函数constructor上? 如果采用和定义方法同样的方法Object.deifneProperty定义属性和静态属性会发生什么?

要解答这些问题,其实我们可以从《Javascript权威指南》中找到答案:

Javascript类相关的对象有三个: 构造函数对象:如在引文中提到的MyController.doAction,定义在构造函数对象中的属性都是类字段和类方法。即定义在了Constructor上。 原型对象:原型对象上的属性被类的所有实例所共享,Object.defineProperty(Constructor.prototype, doAction),即定义在了prototype上。 实例对象: 在实例对象上的属性只在实例上生效,如其中的属性MyClass中的prop,不会为所有实例所共享,即定义在了this上。

理解了上面三个对象,上面的问题自然迎刃而解。

写一个空类继承MyClass

源码

class MyClass {

  prop = 1;
  static staticProp = 2;

  doAction() {
    console.log('do something...');
  }
  static doStaticAction() {
    console.log('do something static');
  }
}

class MyChildClass extends MyClass {
}

babel编译

'use strict';

var _createClass = function() {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || false;
      descriptor.configurable = true;
      if ("value" in descriptor) descriptor.writable = true;
      Object.defineProperty(target, descriptor.key, descriptor);
    }
  }
  return function(Constructor, protoProps, staticProps) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    if (staticProps) defineProperties(Constructor, staticProps);
    return Constructor;
  };
} ();

function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  }
  return call && (typeof call === "object" || typeof call === "function") ? call: self;
}

function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

function _classCallCheck(instance, Constructor) {
  if (! (instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var MyClass = function() {
  function MyClass() {
    _classCallCheck(this, MyClass);

    this.prop = 1;
  }

  _createClass(MyClass, [{
    key: 'doAction',
    value: function doAction() {
      console.log('do something...');
    }
  }], [{
    key: 'doStaticAction',
    value: function doStaticAction() {
      console.log('do something static');
    }
  }]);

  return MyClass;
} ();

MyClass.staticProp = 2;

var MyChildClass = function(_MyClass) {
  _inherits(MyChildClass, _MyClass); // 调用_inherits实现继承

  function MyChildClass() {
    _classCallCheck(this, MyChildClass);

    return _possibleConstructorReturn(this, (MyChildClass.__proto__ || Object.getPrototypeOf(MyChildClass)).apply(this, arguments));  // 构造函数比父类MyClass多了一个返回
  }

  return MyChildClass;
} (MyClass);

解释

从上面可以看到,引入了继承,一切看起来不再那么简单。 不过总体来说,子类继承父类做了两个工作:

  1. 调用_inherits继承父类
  2. 构造函数调用 _possibleConstructorReturn返回原型实例

下面主要研究这两个函数具体是怎么做的? 当然还有一个很明显的问题,为什么父类在构造函数里不需要返回,而子类构造函数需要返回? 下面逐一分析。

  1. _inherits
    • 父类校验,一个类的父类要么是一个函数,要么这个类没有父类。
    • 调用Object.create用父类的原型重写子类的原型prototype
    • 将子类的proto属性指向父类
  2. _possibleConstructorReture 这个待讨论 从以上分析,我们可以归纳出,继承需要的几个要素:
    • 原型重写
    • proto重写
    • 构造函数重写

说到这里,可能还是不太明白? 为了彻底搞明白继承这个事情,我们必须先了解下面几个问题:

  1. 理解prototype与proto的区别
  2. 什么是原型链
  3. 原型继承怎么做?

待续?

这些我们可以另开文章注意研究讨论。

其实这个问题我们还可以继续讨论,如: 如果给子类添加新的属性或新的方法,是如何编译的? 如果有一个孙子类继承子类,又是如何编译,和父类MyClass的关系又如何?

总结

上面我们可以说基本上都是以研究的思路一点一点逐步加深的思路去做的? 我会在另一篇文章里以总结的思路去重新梳理这篇文章? 比如:

lampkid commented 5 years ago

理解prototype与proto的区别

lampkid commented 5 years ago

为什么调用Math或JSON的方法不用new 一个实例?而Date需要去new一个实例?

lampkid commented 5 years ago

如何解释“JS里一切皆对象”?

lampkid commented 5 years ago

理解原型链

lampkid commented 5 years ago

理解Object.create与Object.defineProperty

lampkid commented 5 years ago

JS中如何实现私有化属性和方法?