重学 ES6 之 Class 原理

Class 诞生背景

Javascript 是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP:Object-Oriented JavaScript)语言,ES6 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

那 ES6 Class 解决了哪些问题呢?我们可以从两点入手,一个是解决代码重复问题,另一个是解决实例之间需要存在联系,即基于同一个原型对象的问题。

1. 从函数封装入手

首先我们封装一个通用函数

function Cat(name, color) {
  return {
    name: name,
    color: color,
  };
}
let cat1 = Cat("大毛", "黄色");
let cat2 = Cat("二毛", "黑色");
复制代码

但 cat1 和 cat2 之间没有内在的联系,不能反映出它们是同一个原型对象的实例。

为了解决从原型对象生成实例的问题,Javascript 提供了一个构造函数(Constructor)模式。

所谓 构造函数 ,其实就是一个普通函数,但是内部使用了 this 变量。对构造函数使用 new 运算符,就能生成实例,并且 this 变量会绑定在实例对象上。

function Cat(name, color) {
  this.name = name;
  this.color = color;
}
let cat1 = new Cat("大毛", "黄色");
let cat2 = new Cat("二毛", "黑色");

cat1.constructor === cat2.constructor; // T 说明实例都指向同一个构造函数
cat1 instanceof Cat; // T
cat2 instanceof Cat; // T
复制代码

但针对于对象上一些公共的属性和方法,我们希望可以复用,也就是每一个实例都指向那个内存地址。

JS 规定每个构造函数都有一个 prototype 属性,指向另一个对象(原型对象)。这个对象的所有属性和方法,都会被构造函数的实例继承。

所以我们把一些不变的属性和方法,定义在原型对象上,

function Cat(name, color) {
  this.name = name;
  this.color = color;
}
Cat.prototype.type = "猫科动物";
Cat.prototype.eat = function () {
  console.log("吃老鼠");
};
let cat1 = new Cat("大毛", "黄色");
let cat2 = new Cat("二毛", "黑色");
cat1.eat === cat2.eat; // T 该方法指向同一个内存地址,提高了运行效率
复制代码

2. 判断原型的几个方法

  1. Object.prototype.isPrototypeOf(obj) 用于测试一个对象 obj 是否存在于另一个对象的原型链上。
  2. Object.prototype.hasOwnProperty() 判断某一个属性到底是本地属性,还是继承自 prototype 对象的属性
  3. in 运算符,prop in object,判断某个实例是否含有某个属性(包含原型链上)
function Cat(name, color) {
  this.name = name;
  this.color = color;
}
Cat.prototype.type = "猫科动物";
Cat.prototype.eat = function () {
  console.log("吃老鼠");
};
let cat1 = new Cat("大毛", "黄色");
Cat.prototype.isPrototypeOf(cat1); // T

cat1.hasOwnProperty("name"); // T
cat1.hasOwnProperty("type"); // F

"name" in cat1; // T
"type" in cat1; // T
复制代码

3. Class 语法糖

上面的写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。所以 ES6 的 class 可以看作只是一个语法糖。

下面用 Class 改写上面的例子

class Cat {
  constructor(name, color) {
    this.name = name;
    this.color = color;
  }
  eat() {
    console.log("吃老鼠");
  }
  // 可以将type写成原型上的getter 方法
  get type() {
    return "猫科动物";
  }
}

// type属性当然也可能这样定义,但总感觉方式不是很好,可能Class也不鼓励我们直接在原型上定义共享属性吧
// Cat.prototype.type = "猫科动物";

Cat.prototype; // {constructor,eat,type..}
复制代码

那么具体 Class 的基础用法 可以参见上一篇文章哦~

Class 编译解析

Babel 编译

下面我们来看看 ES6 的 class 经过 Babel 编译后是什么样的呢,比如下面这个例子 ⬇️

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  static run() {
    console.log("run");
  }
  say() {
    console.log("hello!");
  }
}

Person.run();

let p = new Person("张三", 18);
复制代码

Babel 编译后 ⬇️(有部分代码省略)

"use strict";

/**
 * 定义属性
 * @param {*} target
 * @param {array} props
 */
function _defineProperties(target, props) {
  for (let i = 0; i < props.length; i++) {
    let descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

/**
 * 给构造函数添加属性/方法
 * @param {*} Constructor 构造函数
 * @param {array} protoProps 原型属性 - 添加到原型对象上
 * @param {array} staticProps 静态属性 - 直接添加到构造函数本身上
 * @returns
 */
function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

let Person = (function () {
  function Person(name, age) {
    // 1. 判断构造函数是否是通过new操作符调用,是的话this为Person实例,不是的话this为undefined,
    if (!(this instanceof Person)) {
      throw new TypeError("Cannot call a class as a function");
    }
    this.name = name;
    this.age = age;
  }

  // 2. 给构造函数添加属性/方法,第2个参数添加原型上的属性/方法,第3个参数添加形态属性/方法
  _createClass(
    Person,
    [
      {
        key: "say",
        value: function say() {
          console.log("hello!");
        },
      },
    ],
    [
      {
        key: "run",
        value: function run() {
          console.log("run!");
        },
      },
    ]
  );

  // 3. 返回新的构造函数
  return Person;
})();

Person.prototype.say(); // hello!
Person.run(); // run!
复制代码

可以看到主要分为以下几步:

Class 的 constructor 编译后本质还是一个构造函数

  • 首先判断了 Class 的调用方式,要求必须使用 new 调用
  • 然后通过 _createClass 方法区分了原型上的方法和静态方法
  • 最后通过 _defineProperties 方法进行属性/方法的添加

了解了 Class 的由来以及 Class 语法糖编译后的样子,在下篇文章中我们将继续进行 Class 进阶,来看看 Class 的“继承”机制,不见不散~

参考:www.ruanyifeng.com/blog/2010/0…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享