1、准备工作-->搭建babel转码环境
本节不作为重点(如果觉得本节阅读有问题的同学,请查阅此处),长话短说。
npm init
全部回车,将会生成一个package.json文件,编辑内容,如下:
{
"name": "class-explain",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/preset-env": "^7.12.11"
},
"scripts": {
"build": "./node_modules/.bin/babel src --out-dir lib",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
新建.babelrc文件,内容如下:
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}
至此,我们的babel编译环境已经准备就绪。
2、编写用于测试的class文件
为了揭开class的神秘面纱,我们将尽可能的涵盖class中的语法(此时我们暂且不考虑继承,关键语法有属性(方法)、静态属性(方法)、私有属性(方法),构造函数),我们编写一个如下的class:
class Point {
//静态属性
static getName() {
return "Rectangle";
}
/**
* 私有属性
*/
#background = 'red';
/**
* 普通成员属性
*/
width = 100;
height = 100;
/**
* 构造函数
* @param {Number} x
* @param {Number} y
*/
constructor(x, y) {
this.x = x;
this.y = y;
console.log(this.#background);
}
/**
* 普通成员函数
*/
toString() {
return "(" + this.x + ", " + this.y + ")";
}
}
使用
npm run build
如果命令行成功执行的话,我们将会得到如下内容:
"use strict";
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a 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);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true,
});
} else {
obj[key] = value;
}
return obj;
}
var _background = new WeakMap();
var Point = /*#__PURE__*/ (function () {
_createClass(Point, null, [
{
key: "getName",
value: function getName() {
return "Rectangle";
},
},
]);
function Point(x, y) {
_classCallCheck(this, Point);
_background.set(this, {
writable: true,
value: 'red',
});
_defineProperty(this, "width", 100);
_defineProperty(this, "height", 100);
this.x = x;
this.y = y;
}
_createClass(Point, [
{
key: "toString",
value: function toString() {
return "(" + this.x + ", " + this.y + ")";
},
},
]);
return Point;
})();
我们对代码进行逐一分析:
2.1 限定类的调用形式
因为class需要使用new调用,因此,此处代码为了防止我们直接调用class(在使用new调用函数的时候,this指向构造器,这是强绑定this的一种方式,也是优先级最高的一种强绑定形式(参考自《你不知道的JavaScript(上)》))
//函数定义
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
// 函数调用
function Point(x, y) {
_classCallCheck(this, Point);
//此处省略无关代码
}
2.2 生成类的属性和方法,静态属性和方法
接着为class生成属性、方法,静态属性、方法,
//定义一系列属性的方法
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);
}
}
function _createClass(Constructor, protoProps, staticProps) {
//转义方法或者属性
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
//转义静态方法或者属性
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
//定义属性的方法
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true,
});
} else {
obj[key] = value;
}
return obj;
}
//为class添加静态属性、方法和属性、方法
var Point = /*#__PURE__*/ (function () {
//添加静态属性或者方法
_createClass(Point, null, [
{
key: "getName",
//静态属性
value: function getName() {
return "Rectangle";
},
},
]);
function Point(x, y) {
_classCallCheck(this, Point);
_background.set(this, {
writable: true,
value: 'red',
});
//为类的实例生成属性
_defineProperty(this, "width", 100);
//为类的实例生成属性
_defineProperty(this, "height", 100);
this.x = x;
this.y = y;
}
/**
* 添加方法
*/
_createClass(Point, [
{
key: "toString",
value: function toString() {
return "(" + this.x + ", " + this.y + ")";
},
},
]);
return Point;
})();
这儿出现了一个非常关键的语法:Object.defineProperty(target:Object, prop: string, descriptor:PropertyDescriptor),请大家划重点。这个方法,相信使用Vue的朋友们非常属性,Vue便是利用这个方法递归的设置双向绑定,这个方法接收3个参数,第一个参数是要设置属性的源对象,第二个参数是这个属性的键值,第三个属性是一个比较关键的属性,叫PropertyDescriptor,这个属性有6个属性(这6个属性都非常重要,尤其是与装饰器的结合,将会出现化腐朽为神奇的作用,笔者此处只做语法介绍),分别是
1、enumerable,属性是否可以被枚举,如果设置为true,for-in或Object.keys()将会遍历到该属性;
2、configurable,属性是否可以使用delete操作符将其删除,如果设置为true,将可以执行delete删除;
3、value,属性的值;
4、writable,属性的值是否是可写,如果是false的话,属性的值将会是不可改写的;
5、get,取值器函数,如果定义了取值函数的话,当我们获取属性值的时候,将会自动调用该函数;
6、set,赋值器函数,如果定义了赋值函数的话,当我们设置属性值的时候,将会自动调用该函数;
Vue的双向绑定便是动态的劫持了data函数返回的内容,建立侦听,从而实现数据变化,视图即可更新的效果。 Object.defineProperty还有个兄弟,叫Object.defineProperties,此处不再赘述。 从babel转码的结果来看,当如果class的属性或者方法重复定义,后面的定义将会覆盖前面。 并且从这一部分的代码,我们可以看出,class的静态属性或者方法可以直接通过类名调用,而class的方法,是定义在构造器的原形上,而class的属性定义在类的实例上的;
2.3 私有属性
刚才我们暂且没有讨论私有属性,我们单独来看babel的转码结果来学习私有属性
function _classPrivateFieldGet(receiver, privateMap) {
var descriptor = privateMap.get(receiver);
if (!descriptor) {
throw new TypeError("attempted to get private field on non-instance");
}
if (descriptor.get) {
return descriptor.get.call(receiver);
}
return descriptor.value;
}
var _background = new WeakMap();
var Point = /*#__PURE__*/ (function () {
/**
* 构造函数
* @param {Number} x
* @param {Number} y
*/
function Point(x, y) {
_classCallCheck(this, Point);
_background.set(this, {
writable: true,
value: 200,
});
console.log(_classPrivateFieldGet(this, _background));
}
return Point;
})();
由于私有属性只能在类的内部访问,我们可以看到,babel在这儿使用的是是WeakMap,这也是ES6中引入的新的方法,可以通过WeakMap建立对象到值的映射(WeakMap的键只能是对象)。在这儿通过this设置了值,因此我们也只能通过this才能访问到这个值,这便印证了私有属性只能在类的内部访问的语法规则。不得不称赞这个设计思路,妙啊,🤓,同时也学到了WeakMap的一个使用场景(笔者在之前在掘金论坛有过和开发者对于私有属性设计的讨论)。
2.4 构造函数
看完了上面比较复杂的代码,此处就比较简单了,跟我们写的ES5也没有任何区别。
function Point(x, y) {
//无关代码已忽略
_classCallCheck(this, Point);
this.x = x;
this.y = y;
}
我们可以看到,在构造器内部定义属性或者方法则是直接定义在类的实例上的。
2.5 小结
1、 静态属性或方法直接通过类调用(静态属性或方法定义在构造器上);
2、 属性或访问直接通过类的实例调用(方法定义在构造器的原形上,属性定义在类的实例上);
3、 构造器内部,通过this设置的属性或者方法直接定义在类的实例上;
4、 私有属性只能类内部访问(WeakMap设置this的引用,只能通过这个作用域下的this访问, 外界无法访问);
3、class的继承
如果说我为什么要用class,那便是class的继承语法,使得JS在的继承语法优雅程度上面有了质的飞越。 我们继续编写用于测试的class文件:
import Point from './index.js';
class Rectangle extends Point {
/**
* 私有属性
*/
#alpha = 0;
width = 100;
static height = 100;
constructor() {
super();
}
toString() {
console.log("Hello World");
super.toString();
}
static draw() {}
dispose() {}
}
运行命令,得到babel转码之后的代码,
"use strict";
// 辅助函数 惰性加载
function _typeof(obj) {
"@babel/helpers - typeof";
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function _typeof(obj) {
return typeof obj;
};
} else {
_typeof = function _typeof(obj) {
return obj &&
typeof Symbol === "function" &&
obj.constructor === Symbol &&
obj !== Symbol.prototype
? "symbol"
: typeof obj;
};
}
return _typeof(obj);
}
var _index = _interopRequireDefault(require("./index"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a 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);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
function _get(target, property, receiver) {
if (typeof Reflect !== "undefined" && Reflect.get) {
_get = Reflect.get;
} else {
_get = function _get(target, property, receiver) {
var base = _superPropBase(target, property);
if (!base) return;
var desc = Object.getOwnPropertyDescriptor(base, property);
if (desc.get) {
return desc.get.call(receiver);
}
return desc.value;
};
}
return _get(target, property, receiver || target);
}
function _superPropBase(object, property) {
while (!Object.prototype.hasOwnProperty.call(object, property)) {
object = _getPrototypeOf(object);
if (object === null) break;
}
return object;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true },
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf =
Object.setPrototypeOf ||
function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Date.prototype.toString.call(Reflect.construct(Date, [], function () {}));
return true;
} catch (e) {
return false;
}
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
//此处不再赘述
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true,
});
} else {
obj[key] = value;
}
return obj;
}
var _alpha = new WeakMap();
var Rectangle = /*#__PURE__*/ (function (_Point) {
_inherits(Rectangle, _Point);
var _super = _createSuper(Rectangle);
/**
* 私有属性
*/
function Rectangle() {
var _this;
_classCallCheck(this, Rectangle);
_this = _super.call(this);
_alpha.set(_assertThisInitialized(_this), {
writable: true,
value: 0,
});
_defineProperty(_assertThisInitialized(_this), "width", 100);
return _this;
}
_createClass(
Rectangle,
[
{
key: "toString",
value: function toString() {
console.log("Hello World");
_get(_getPrototypeOf(Rectangle.prototype), "toString", this).call(this);
},
},
{
key: "dispose",
value: function dispose() {},
},
],
[
{
key: "draw",
value: function draw() {},
},
]
);
return Rectangle;
})(_index["default"]);
_defineProperty(Rectangle, "height", 100);
除去我们之前已经讨论的代码,我们对上述代码进行分析:
3.1 惰性加载
此处,有一些辅助函数用到了一个比较重要的编码技巧,叫作“惰性加载”。 例如:
function _get(target, property, receiver) {
if (typeof Reflect !== "undefined" && Reflect.get) {
_get = Reflect.get;
} else {
_get = function _get(target, property, receiver) {
var base = _superPropBase(target, property);
if (!base) return;
var desc = Object.getOwnPropertyDescriptor(base, property);
if (desc.get) {
return desc.get.call(receiver);
}
return desc.value;
};
}
return _get(target, property, receiver || target);
}
惰性加载的目的是减少不必要的if-else分支语句,从而提升程序的执行效率。我们直接拿《JavaScript高级程序设计》这本书上的示例举例子,假设我们现在需要封装一个Ajax的函数(说用axios的朋友直接给我联系方式,我帮你挂华西最好的精神病门诊医生🤣),由于我们需要支持IE6,因此我们会进行兼容性判断,但是目前市面上绝大部分浏览器已经不再是IE6了,而这些浏览器还需要去走这个兼容性if-else分支判断吗,答案肯定是不需要的。因此,我们直接用当前宿主环境可支持的分支路径复写函数,以后我们的函数将不再进行兼容性的判断,这样,函数已经执行过一次之后,我们的性能将会大大提升,而我们仅仅只损失的是在函数第一次执行时候的这么一丁点儿性能。
这儿我们先从构造器分析,逐步的分析babel所定义的辅助函数。
3.2 关联原型链
首先
_inherits(Rectangle, _Point);
Rectangle是子类,Point是父类。
//核心继承函数
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true },
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
//设置原形的方法
function _setPrototypeOf(o, p) {
//如果不存在Object.setPrototypeOf则手动修改原型链
_setPrototypeOf =
Object.setPrototypeOf ||
function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
首先,我们先介绍一下Object.create方法,笔者借花献佛,直接贴出MDN的解释
Object.create(proto,[propertiesObject])
proto
新创建对象的原型对象。
propertiesObject
可选。需要传入一个对象,该对象的属性类型参照Object.defineProperties()的第二个参数。
如果该参数被指定且不为 undefined,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。
所以核心继承方法里面的这段代码
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true },
});
意思大概就是: 通过Object.create得到一个对象,这个对象的__proto__指向,superClassd的原形对象,但这个对象的构造器指向的是subClass本身,同时,让subClass的原形对象指向这个对象(这儿比较绕,如果读者不太清楚,可以打开调试工具尝试一下)。 我们用代码来阐述这段文字所描述的意思 temp = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true }, }) subClass.prototype = temp; 由于temp.__proto__ === superClass.prototype(但temp的constructor是subClass) 由此关系,得出:subClass.prototype.__proto__ === superClass.prototype;
从_inherits方法中,我们得出2个非常重要的结论,
1、SubClass.prototype.__proto__ === SuperClass.prototype;
2、SubClass.__proto__ === SuperClass;
这正是阮一峰老师著《ES6入门》所述的class的原形指向关系。(笔者在此赘述一下我个人记忆这个原形链的指向关系,由于静态属性可以继承,静态属性直接通过类名调用,因此直接得出结论2;由于构造器以外的非私有非静态方法或属性可继承,而这些方法或属性是定义在原形上的,由此可得结论1;)
3.3 对构造器及构造器super的处理
接着:
var _super = _createSuper(Rectangle);
首先,在class中有一个重要的关键字:super,通过super可以调用父类的属性或方法(这是我们在面向对象的前端开发过程中实现多态的一个重要手段)。同时,在如果子类编写了constructor方法,必须在其开头调用super()(如果不写,则程序自动生成默认的),其目的是为了将构造器里面定义的的属性或者方法继承到子类; 我们先看构造器里面调用super的实现:
//辅助函数
function _typeof(obj) {
"@babel/helpers - typeof";
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function _typeof(obj) {
return typeof obj;
};
} else {
_typeof = function _typeof(obj) {
return obj &&
typeof Symbol === "function" &&
obj.constructor === Symbol &&
obj !== Symbol.prototype
? "symbol"
: typeof obj;
};
}
return _typeof(obj);
}
//辅助函数
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Date.prototype.toString.call(Reflect.construct(Date, [], function () {}));
return true;
} catch (e) {
return false;
}
}
//辅助函数
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
//处理构造函数如果有return的情况
// 这儿 有个有趣的现象
// 当我们使用new调用函数的时候,如果构造函数有返回值,若返回的是基础类型,则自动忽略,并返回new的对象,如果函数返回的是引用类型,则返回这个引用类型
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
//断言是否在构造器开头调用了super();
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
//定义super()
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
//执行构造器,目的是为了将构造器里面定义的的属性或者方法继承到子类,
//如果存在反射的API,则用反射的API,否则,调用父类的构造函数
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
var Rectangle = /*#__PURE__*/ (function (_Point) {
_inherits(Rectangle, _Point);
var _super = _createSuper(Rectangle);
/**
* 私有属性
*/
function Rectangle() {
var _this;
_classCallCheck(this, Rectangle);
_this = _super.call(this);
//如果还没有调用super的话,错误断言;
_alpha.set(_assertThisInitialized(_this), {
writable: true,
value: 0,
});
//如果还没有调用super的话,错误断言;
_defineProperty(_assertThisInitialized(_this), "width", 100);
return _this;
}
return Rectangle;
})(_index["default"]);
3.3 对使用super调用的属性或方法处理
接着,我们看一看,当我们调用super的属性或者函数的时候,babel所做的处理:
//babel转码前:
super.toString();
//babel转码后,从当前类的原形对象上尝试获取该方法
_get(_getPrototypeOf(Rectangle.prototype), "toString", this).call(this);
//获取对象属性的辅助方法
function _get(target, property, receiver) {
// 如果浏览器支持反射,则使用反射API
if (typeof Reflect !== "undefined" && Reflect.get) {
_get = Reflect.get;
} else {
// 否则,尝试从原形链向上查找对象
_get = function _get(target, property, receiver) {
var base = _superPropBase(target, property);
// 如果找到原形链顶端都还没有找到,则终止查找
if (!base) return;
//如果找得到,获取属性的描述符
var desc = Object.getOwnPropertyDescriptor(base, property);
//如果PropertyDescriptor定义了getter,则调用getter方法
if (desc.get) {
return desc.get.call(receiver);
}
//否则,返回真正的属性值
return desc.value;
};
}
return _get(target, property, receiver || target);
}
//在对象及对象的原形链查找对应的属性,如果找的到,返回这个对象,如果找到了原形链的顶端,还找不到,则返回null
function _superPropBase(object, property) {
while (!Object.prototype.hasOwnProperty.call(object, property)) {
object = _getPrototypeOf(object);
if (object === null) break;
}
return object;
}
4、总结
4.1 执行流程
我们现在来回顾一下class的执行流程:
1、首先判断是否有继承,如果有的话,先确定原形的指向关系;
2、然后紧接着,调用构造函数,如果没有构造函数,先生成默认构造,否则直接调用构造函数,在构造函数里判断是否有返回,如果返回的是基础类型,无视,并且返回这个对象,如果返回的是对象,则返回这个对象,否则,返回构造的对象;
3、随后,再初始化自己构造内部的属性(包括外部的属性),方法还有类的私有属性。
4、最后再初始化自己的方法,静态属性或方法。
4.2 结论
1、class的本质仍然是ES5的构造函数;
2、class的静态属性或方法定义在类上;
3、class的方法定义在类的原形上,属性定义在类的实例上;
4、在class的构造器内部,通过this设置的属性或者方法直接定义在类的实例上;
5、class的私有属性只能类内部访问,外界无法访问;
6、若对于两个类,SuperClass和SubClass,且SubClass继承自SuperClass,则有如下关系: SubClass.prototype.__proto__ === SuperClass.prototype, SubClass.__proto__ === SuperClass;
7、若对于两个类,SuperClass和SubClass,且SubClass继承自SuperClass,若不写contructor,则JS解析器自动生成,如若编写,则必须在constructor的开头调用父类的构造器super();
由于笔者水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,你们的意见将会帮助我更好的进步。本文乃笔者原创,若转载,请联系作者本人,邮箱404189928@qq.com🥰