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. 判断原型的几个方法
Object.prototype.isPrototypeOf(obj)
用于测试一个对象 obj 是否存在于另一个对象的原型链上。Object.prototype.hasOwnProperty()
判断某一个属性到底是本地属性,还是继承自 prototype 对象的属性- 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 的“继承”机制,不见不散~