众所周知,JS并没有类(class)的概念,虽然说ES6开始有了类的概念,但是,这并不是说JS有了像Ruby、Java这些基于类的面向对象语言一样,有了全新的继承模型。ES6中的类,仅仅只是基于现有的原型继承的一种语法糖,下面我们好好分析一下,具体是如何实现的

面向对象思想

在讲正题之前,我们先来讨论一下各种面试题都可能出现的一个问题,什么是面向对象编程(OOP)?

  • 类:定义某一事物的抽象特点,包含属性和方法,举个栗子,这个类包含狗的一些基础特征,如毛皮颜色,吠叫等能力。

  • 对象:类的一个实例,还是举个栗子,小明家的白色的狗和小红家红色的狗。

  • 属性:对象的特征,比如刚提到的狗皮毛的颜色。

  • 方法:对象的行为,比如刚才提到的狗的吠叫能力。

  • 封装性:通过限制只有特定类的对象可以访问特定类的成员,一般包含public protected private 三种,不同语言的实现不同。

  • 继承性:一个类会有子类,这个子类是更具体化的一个抽象,它包含父类的一些属性和方法,并且有可能有不同于父类的属性和方法。

  • 多态性:多意为‘许多’,态意为‘形态’。不同类可以定义相同的方法或属性。

  • 抽象性:复杂现实问题转化为类定义的途径,包括以上所有内容。

如何实现对象(类)的定义

由于JS并没有类(class)的概念,更多的时候我们把它叫做对象(function),然后把对象叫做实例(instance),跟团队里面的人讨论OOP的时候,经常会有概念上的一些误解,特此说明一下。

构造函数:一个指明了对象类型的函数,通常我们可以通过构造函数类创建

在js里面,我们通常都是通过构造函数来创建对象(class),然后通过new这个关键字来实例化一个对象,如:

function Dog(name){
  this.name = name;
}
var d1 = new Dog("dodo");
d1.constructor
// Dog(name){
//  this.name = name;
// }

var d2 = new Dog('do2do');

为什么通过构造函数可以实现对象(class)属性的定义呢?首先,我们必须理解这个语法new constructor[([arguments])]

我们来具体看看当new Dog('name')时,具体做了哪些事情

  1. 一个新实例被创建。它继承自Dog.prototype

  2. 构造函数被执行,相应的参数会被传入,同时上下文(this)会指向这个新的实例

  3. 除非明确返回值,否则返回新的实例

至此,我们实现了OOP里面的类(Dog)、对象(d1,d2)、和属性(name)的概念,d1d2有相同的name属性,但是值并不相同,即属性是私有的。

注: 新创建的实例,都包含一个constructor属性,该属性指向他们的构造函数Dog

原型对象(prototype)

接下来,我们即将讨论如何定义方法,其实,我们完全可以这样定义我们的方法,如:

function Dog(name){
  this.name = name;
  this.bark = function(){
    console.log(this.name + " bark");
  };
}
var d1 = new Dog("dodo");
d1.bark();
// dodo bark

但是,一般我们不推荐这么做,正如我们所知Dog是一个构造函数,每次实例化时,都会执行这个函数,也就是说,bark 这个方法每次都会被定义, 比较浪费内存。但是我们通常可以用constructor和闭包的方式来实现私有属性,如:

function Dog(name){
  this.name = name;
  
  // barkCount 是私有属性,因为实例并不知道这个属性
  var barkCount = 0;
  this.bark = function(){
    barkCount ++;
    console.log(this.name + " bark");
  };
  this.getBarkCount = function(){
    console.log(this.name + " has barked " + barkCount + " times");
  };
}
var d1 = new Dog("dodo");
d1.bark();
d1.bark();
d1.getBarkCount();
// dodo has barked 2 times

好像扯得有点远,我们回归我们的主角prototype,函数Dog有一个特殊的属性,这个属性就叫原型,如上所述,当用new运算符创建实例时,会把Dog的原型对象的引用复制到新的实例内部的[[Prototype]]属性,即d1.[[Prototype]] = Dog.prototype,因为所有的实例的[[Prototype]]都指向Dog的原型对象,那么,我们就可以很方便的定义我们的方法了,如:

function Dog(name){
  this.name = name;
}

Dog.prototype = {
  bark: function(){
    console.log(this.name + " bark");
  }
};

var d1 = new Dog("dodo");
d1.bark();
// dodo bark

我们可以通过d1.__proto__ == Dog.prototype,来验证我们的想法。用原型对象还有一个好处,由于实例化的对象的[[Prototype]]指向Dog的原型对象,那么我们可以通过添加Dog的原型对象的方法,来添加已经实例化后的实例d1的方法。如:

Dog.prototype.run = function(){
  console.log(this.name + " is running!");
}
d1.run();
// dodo is running!

注:所有对象的__proto__都指向其构造器的prototype

原型链

上面已经描述如何定义一个,接下来我们将要了解,如何实现类的继承。在此之前,我们先了解js里一个老生常谈的概念:原型链:每个对象都有一个指向它的原型(prototype)对象的内部链接。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止(也就是不再有原型指向),组成这条链的最后一环。这种一级一级的链结构就称为原型链

mozilla给出一个挺好的例子:

// 假定有一个对象 o, 其自身的属性(own properties)有 a 和 b:
// {a: 1, b: 2}
// o 的原型 o.[[Prototype]]有属性 b 和 c:
// {b: 3, c: 4}
// 最后, o.[[Prototype]].[[Prototype]] 是 null.
// 这就是原型链的末尾,即 null,
// 根据定义,null 没有[[Prototype]].
// 综上,整个原型链如下: 
// {a:1, b:2} ---> {b:3, c:4} ---> null

console.log(o.a); // 1
// a是o的自身属性吗?是的,该属性的值为1

console.log(o.b); // 2
// b是o的自身属性吗?是的,该属性的值为2
// o.[[Prototype]]上还有一个'b'属性,但是它不会被访问到.这种情况称为"属性遮蔽 (property shadowing)".

console.log(o.c); // 4
// c是o的自身属性吗?不是,那看看o.[[Prototype]]上有没有.
// c是o.[[Prototype]]的自身属性吗?是的,该属性的值为4

console.log(o.d); // undefined
// d是o的自身属性吗?不是,那看看o.[[Prototype]]上有没有.
// d是o.[[Prototype]]的自身属性吗?不是,那看看o.[[Prototype]].[[Prototype]]上有没有.
// o.[[Prototype]].[[Prototype]]为null,停止搜索,
// 没有d属性,返回undefined

现在我们可以通过我们理解的构造函数和原型对象来实现继承的概念了,代码如下:

function Dog(name){
  this.name = name;
}

// 这种写法会修改dog实例的constructor,可以通过Dog.prototype.constructor = Dog来重置
Dog.prototype = {
  bark: function(){
    console.log(this.name + " bark");
  }
};

// 重置Dog实例的构造函数为本身
Dog.prototype.constructor = Dog;

// Haski 的构造函数
function Haski(name){
  // 继承Dog的构造函数
  Dog.call(this, name);
  // 可以补充更多Haski的属性
  this.type = "Haski";
};

// 1. 设置Haski的prototype为Dog的实例对象
// 2. 此时Haski的原型链是 Haski -> Dog的实例 -> Dog -> Object
// 3. 此时,Haski包含了Dog的所有属性和方法,而且还有一个指针,指向Dog的原型对象
// 4. 这种做法是不推荐的,下面会改进
Haski.prototype = new Dog();

// 重置Haski实例的构造函数为本身
Haski.prototype.constructor = Haski;

// 可以为子类添加更多的方法
Haski.prototype.say = function(){
  console.log("I'm " + this.name);
}

var ha = new Haski("Ha");
// Ha bark
ha.bark();
// Ha bark
ha.say();
// I'm Ha

注: 子类在定义prototype时,不可直接使用Haski.prototype = {}定义,这样会重写Haski的原型链,把Haski的原型当做Object的实例,而非Dog的实例

但是,当我想找一下ha的原型链时,会发现ha的原型对象指向的是Dog的实例,而且还有一个值为undefinedname属性,在实例化时,name是没必要的, 如下图:

所以,我们需要修改一下我们的实现,代码如下:

// 修改前
Haski.prototype = new Dog();

// 修改后
Haski.prototype = Object.create(Dog.prototype);

注: __proto__ 方法已弃用,从 ECMAScript 6 开始, [[Prototype]] 可以用Object.getPrototypeOf()和Object.setPrototypeOf()访问器来访问

自此,我们已经实现继承的概念,父类有自己的方法,子类继承了父类的属性和方法,而且还可以定义自己的属性和方法。

ES6 如何实现

'use strict';
// 声明 Dog 类
class Dog {
  // 构造函数
  constructor(name){
    this.name = name;
  }
 
  // 普通方法
  dark(){
    console.log(this.name + "bark");
  }
 
  // 静态方法,也叫类方法
  static staticMethod(){
    console.log("I'm static method!");
  }
}

// 通过`extends`关键字来实现继承
class Haski extends Dog {
  constructor(name){
    // 调用父类的构造函数
    super(name);
    this.type = "Haski";
  }
  
  // 定义子类方法
  say(){
    console.log("I'm" + this.name);
  }
}

在ES6中,我们只需通过class extends super constructor 即可比较方便的完成原来使用JS比较难理解的实现,我们可以通过babel的解析器,来看看babel是怎么把这些语法糖转成JS的实现的。具体代码可以参考

"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"); } }

// 声明 Dog 类

var Dog = function () {
  // 构造函数

  function Dog(name) {
    _classCallCheck(this, Dog);

    this.name = name;
  }

  // 普通方法


  _createClass(Dog, [{
    key: "dark",
    value: function dark() {
      console.log(this.name + "bark");
    }

    // 静态方法,也叫类方法

  }], [{
    key: "staticMethod",
    value: function staticMethod() {
      console.log("I'm static method!");
    }
  }]);

  return Dog;
}();

// 通过`extends`关键字来实现继承


var Haski = function (_Dog) {
  _inherits(Haski, _Dog);

  function Haski(name) {
    _classCallCheck(this, Haski);

    var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Haski).call(this, name));
    // 调用父类的构造函数


    _this.type = "Haski";
    return _this;
  }

  _createClass(Haski, [{
    key: "say",
    value: function say() {
      console.log("I'm" + this.name);
    }
  }]);

  return Haski;
}(Dog);

教是最好的学,我正在尝试把我自己理解的内容分享出来,希望我能讲清楚,如果描述有误,欢迎指正。

参考文献