Skip to content

class 的实现

一谈到编程,我们首先想到的就是 O(Object)O(Oriented)P(Programming)编程,也就是面向对象编程。面向对象编程是一种设计思想。如果把程序当做一个人,那么对象就是各个器官,对象里面的各种操作函数就是细胞。

很多语言中面向对象的蓝图都是基于类,比如 Pytho、C++、Java。js 在 es6 语法中也引入了类的概念,不过还是基于 es5 的原型链实现的语法糖。而为什么 js 一开始并没有用类的概念呢?这都是因为 Brendan Eich 一开始设计 js 的时候只是想设计出一门运行在浏览器上能解决他手头上的需求的语言,所以他没有引入类的概念,而是用原型模式来实现 js 的继承。

既然谈到原型,那肯定大家都对 prototype 不陌生,这个属性实际是一个指针,指向一个对象,而这个对象包含有特定类型的所有实例共享的属性和方法。讲人话就是:“这个家伙的一些东西,他的儿子孙子也都能同时拥有。这些东西是共享的。”而 class 就是基于这个原型实现的,至于怎么实现的,我们可以看看代码:

javascript
//class
class Hello {
  constructor(x) {
    this.x = x;
  }
  greet() {
    console.log("Hello, " + this.x);
  }
}

let world = new Hello("world");
world.greet();

//es5
var Hello = (function() {
  function Hello(x) {
    this.x = x;
  }
  Hello.prototype.greet = function() {
    console.log("Hello, " + this.x);
  };
  return Hello;
})();
var world = new Hello("world");
world.greet();

上述的例子很简单地诠释了 class 的语法在 es5 中是怎么实现的,有意思的是 class 的 constructor 其实就是扮演了构造函数的角色,本质上还是利用构造函数和原型模式来实现 class 的继承。类的实例调用方法,实际上就是调用原型上的方法。(这里要注意,类的内部默认就是严格模式,所以不需要使用 use strict 指定运行模式)


原型对象

es5 语法

让我们先打印出 es5 语法实现的 world 来看看:

javascript
var Hello = (function() {
  function Hello(x) {
    this.x = x;
  }
  Hello.prototype.greet = function() {
    console.log("Hello, " + this.x);
  };
  return Hello;
})();
var world = new Hello("world");

console.log(world);

prototype

对js原型有所了解的朋友都知道world.__proto__指向的是world继承的原型对象,有意思的是world.__proto__.constructor指回了Hello构造函数,然而根据原型模式Hello.prototype === world.__proto__,也就是说Hello.prototype.constructor也是指回了Hello构造函数。让我们用来代码来展示:
javascript
var Hello = (function() {
  function Hello(x) {
    this.x = x;
  }
  Hello.prototype.greet = function() {
    console.log("Hello, " + this.x);
  };
  return Hello;
})();

var world = new Hello("world");

world.__proto__.constructor === Hello; //true
world.__proto__ === Hello.prototype; //true
Hello.prototype.constructor === Hello; //true

class 语法

再让我们来打印出 class 实现的 world 看看:

javascript
class Hello {
  constructor(x) {
    this.x = x;
  }
  greet() {
    console.log("Hello, " + this.x);
  }
}

let world = new Hello("world");

console.log(world);

hello

上图和es5语法打印出来的基本一样,只不过constructor指向的不是Hello构造函数而是Hello类。也就是说:

javascript
class Hello {
  constructor(x) {
    this.x = x;
  }
  greet() {
    console.log("Hello, " + this.x);
  }
}

let world = new Hello("world");

world.__proto__.constructor === Hello; //true
world.__proto__ === Hello.prototype; //true
Hello.prototype.constructor === Hello; //true

看完上面的解析,充分证明 class 确实是 es5 的语法糖,class 的继承还是基于原型链,所以还是建议大家认真去了解 js 的原型模式和原型链,才能充分理解 js 的 class,不过在实际工作中还是建议使用 class,这样会更直观,代码也方便维护。



静态方法

类就是实例的原型,所有在类中定义的方法,都会被实例继承,而类的静态方法不会被实例继承,那么是怎么实现的呢?

javascript
// class
class Hello {
  constructor(x) {
    this.x = x;
  }
  static greet() {
    console.log("Hello, world");
  }
}

// es5
var Hello = (function() {
  function Hello(x) {
    this.x = x;
  }
  Hello.greet = function() {
    console.log("Hello, world");
  };
  return Hello;
})();

我相信有些人看到这的反应就是:

yiwen

就这样来实现静态方法?这么简单粗暴?其实我觉得还好,很直观明了。

继承

class 通过关键字 extends 来实现继承,这也是比 es5 通过原型链实现继承的一个优势:清晰方便。那么我们来看看 es5 语法如何实现继承:

javascript
//class
class Hello {
  constructor(x) {
    this.x = x;
  }
  static greet() {
    console.log("Hello, world");
  }
}

class Hi extends Hello {
  constructor(x) {
    super(x);
  }
}

// es5
var __extends =
  (this && this.__extends) ||
  (function() {
    var extendStatics = function(d, b) {
      extendStatics =
        Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array &&
          function(d, b) {
            d.__proto__ = b;
          }) ||
        function(d, b) {
          for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
        };
      return extendStatics(d, b);
    };
    return function(d, b) {
      extendStatics(d, b);
      function __() {
        this.constructor = d;
      }
      d.prototype =
        b === null
          ? Object.create(b)
          : ((__.prototype = b.prototype), new __());
    };
  })();
var Hello = (function() {
  function Hello(x) {
    this.x = x;
  }
  Hello.greet = function() {
    console.log("Hello, world");
  };
  return Hello;
})();
var Hi = (function(_super) {
  __extends(Hi, _super);
  function Hi(x) {
    return _super.call(this, x) || this;
  }
  return Hi;
})(Hello);

还记得阮一峰的《ECMAScript 6 入门》的有关 super 的介绍吗?

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。

上面的代码可以充分诠释阮老师的这几句话,其实最后还是利用 call 函数来完成实例 this 的绑定。



结语

以上只是大概讲述一下 class 是怎么实现的,其实还有很多属性和方法是更骚地操作,以后有时间我会写出跟大家分享。建议大家还是深入理解 js 的原型模式和原型链,毕竟不论出 es6、es7 语法,这些都是基于 js 的基本设计模式实现的。