详解JS类概念的实现

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

面向对象思想

在讲正题以前,咱们先来讨论一下各类面试题均可能出现的一个问题,什么是面向对象编程(OOP)?java

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

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

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

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

  • 封装性:经过限制只有特定类的对象能够访问特定类的成员,通常包含public protected private 三种,不一样语言的实现不一样。闭包

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

  • 多态性:多意为‘许多’,态意为‘形态’。不一样类能够定义相同的方法或属性。this

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

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

因为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);

教是最好的学,我正在尝试把我本身理解的内容分享出来,但愿我能讲清楚,若是描述有误,欢迎指正。

参考文献

相关文章
相关标签/搜索