JS ES6 Class 类详解
Class详解
零,ES6转ES5代码
下面代码是我们使用babel
转换器来实现es6
转es5
(如果想进行代码转换,那么可以使用官网转换器):
代码如下:
//ES6
class Foo{
constructor(name){
this.name = name
}
show(){
console.log(this.name)
}
}
var foo = new Foo('xz')
//ES5
"use strict";
//该函数的主要作用是判断left是不是right的实例对象,这里做了类型处理
function _instanceof(left, right) {
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
return !!right[Symbol.hasInstance](left);
}
else {
return left instanceof right;
}
}
//如果this不是我们构造函数的实例,那么就抛出错误
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, 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;
}
var Foo = /*#__PURE__*/function () {
function Foo(name) {
//这里的this指的是我们创建的实例对象的地址引用,
//通过_classCallCheck()函数来判断是否this是Foo的实例,
//不是就抛出错误,这样做的目的就是让我们必须用new来操作类
_classCallCheck(this, Foo);
this.name = name;
}
_createClass(Foo, [{
key: "show",
value: function show() {
console.log(this.name);
}
}]);
return Foo;
}();
var foo = new Foo('xz');
从上面转换的代码中我们可以初步得出以下几个结论:
-
类操作是传统实例创建的语法糖
-
我们必须使用
new
来操作类,否则报错 -
constructor
中的this
指向的是创建的实例对象 -
除了
constructor
方法,其他方法均被挂载在类的原型上。 -
constructor
方法的参数是类构造函数的参数,语句为类构造函数中的语句。也就是说constructor
相当于传统意义上的构造函数。
一,Class的基本语法
1.1基本使用方法
在传统的js
语法当中,如果我们想创建一个实例对象的时候一般会先定义一个构造函数,然后通过new
运算符来创建一个实例对象,例如:
function Person(name,age){
this.name = name
this.age = age
}
Person.prototype.showName = function(){
console.log(this.name)
}
Person.prototype.showAge = function(){
console.log(this.age)
}
var person = new Person('xz',21)
person.showName()//'xz'
person.showAge()//21
我们定义一个Person
的构造函数,然后通过new Person()
来创建一个实例对象。一般我们向构造函数的原型上添加方法,表示实例要继承的方法,这个方法是公共的,不是某个实例自己特有的方法。这样我们就可以完成一个具体的实例了。
上的方法固然好,但是不利于新手的学习,所以在ES6
中,为我们提供了一个叫Class
的一个方法,让我们能够更容易理解和创建一个实例对象。接下来我们看用Class
如何来创建一个person
实例对象。
class Person {
constructor(name,age){
this.name = name
this.age = age
}
showName(){//为我们的实例添加继承的方法
console.log(this.name)
}
showAge(){//为我们的实例添加继承的方法
console.log(this.age)
}
}
var person = new Person('xz',21)
person.showName()//'xz'
person.showAge()//21
1.2constructor方法
constructor
方法是类默认方法,通过new
命令生成对象实例时自动调用该方法。一个类必须有一个constructor
方法,如果没有显示定义,那么一个空的constructor
方法会被默认添加。例如:
class Person{
showName(){
...
}
}
//等价于:
class Person{
constructor(){}//自动添加一个空的constructor方法
showName(){
....
}
}
constructor
函数返回的值就是我们new Person
最后拿到的值。默认情况下返回的是我们的新建实例对象的地址引用,但是我们还可以让他返回其他的东西,例如:
class Person {
constructor(name,age){
this.name = name
this.age = age
return {
h:'100px',
w:'100px'
}
}
showName(){
console.log(this.name)
}
showAge(){
console.log(this.age)
}
}
var person = new Person('xz',21)
此时我们返回的并不是我们新建的实例的地址引用,而是另一个对象,那么我们最终拿到的就是另一个对象。该对象不会继承任何我们定义的方法。只是一个‘’过路人‘’。
当我们定义了Person
的时候,虽然它是函数类型的值,但是必须使用new
操作符,如果直接调用会报错。
1.3表达式形式的类
我们可以通过一个表达式形式来声明一个类,例如:
var myClass = class Me{
constructor(){
......
}
}
上面的代码使用表达式定义了一个类。需要注意的是,这个类的名称是myClass
而不是Me
。所以我们使用的时候应该是这样:new myClass()
。而不是new Me()
。Me
只在Class
的内部代码中可用,指代的是当前类。
1.4不存在变量提升
类的声明不存在变量提升,例如:
new Person()//报错,因为类的声明不存在变量提升
class Person{
constructor(){
this.name = 'xz'
this.age = 21
}
}
1.5私有方法
ES6
并不支持私有方法,所以有时候我们还是需要自己通过某种手段去实现私有方法。
1.6this的指向
在contructor
函数当中,this
永远指向的是我们新建的实例对象,一般情况在,其他的方法其实也是指向实例对象的。但是有一种情况特殊,那就是我们单独使用类中的方法而不是通过实例调用的方法来使用类中的方法,例如:
class Person {
constructor(name,age){
this.name = name
this.age = age
}
showName(){
console.log(this.name)
}
showAge(){
console.log(this.age)
}
}
var person = new Person('xz',21)
var {constructor,showName,showAge} = person//通过解构赋值来提取各方法
person.showName()//'xz'
showName()//报错
我们person.showName()
打印xz
。这是毋庸置疑的,但是我们直接showName()
。因为此时的this
并不指向我们的实例对象。
1.7name
本质上,由于ES6
的类只是ES5
的构造函数的一层包装,所以函数的许多特性都被class
继承,包括name
属性。
class Person{
}
console.log(Person.name)//'Person'
name
属性总是返回,紧跟在class
关键字后面的类名。例如:
var myClass = class Me{
....
}
console.log(myClass.name)//Me
最然类的名称是myClass
,但是当我们打印类的name
的值的时候,然会的是紧跟在class
关键字后面的类名。其实当我们:
var myClass = class Me{
....
}
的时候,其实真正意义上是只定义了一个类,但是它有两个名称,当我们想创建实例对象的时候使用的是myClass
,而在内存中给这个类做标记的是Me
这个名称。
1.8Class的取值函数(get)和存值函数(set)
在类中,我们可以定义属性,该属性可以是每个类都可以使用的,例如:
class Person{
constructor(){}
}
Person.name = 'xz'//我们定义了一个类的属性,至于为什么要这样写,后面1.10小节会有解释
Person
类中我们定义了一个name
的属性值,并赋值为xz
,此时我们来创建几个实例:
var student = new Person()
var teacher = new Person()
console.log(student.name)
console.log(teacher.name)
student.name = 'stu'
console.log(student.name)
console.log(teacher.name)
结果:
xz
xz
stu
xz
每个实例都会有类中的属性,并且互不影响,不属于共享类型的值。对于类中的属性,我们可以为其设置读写函数,例如:
class Person{
constructor(){}
get name(){
return 'xz'
}
set name(value){
console.log(value)
}
}
var person = new Person()
console.log(person.name)//'xz'
person.name = 123//123
其实和我们传统的get/set
函数类似,当我们去用实例对象去试图读或者写的时候都会触发相应的函数。
1.9Class的静态方法
所有在类中定义的方法都会被实例继承。如果在一个方法前面添加static
关键字,就表示该方法不会被实例继承,而是直接通过类调用,这些方法被称为静态方法
。例如:
class Person{
static show(){
console.log(123)
}
}
var person = new Person()
console.log(person.show)//undefine
原因在于我们在show
方法前面添加了static
关键字,此时该方法不会被实例继承。
我们来进行静态方法的代码转换:
class Person{
static show(){}
}
//es5
"use strict";
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, 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; }
var Person = /*#__PURE__*/function () {
function Person() {
_classCallCheck(this, Person);
}
_createClass(Person, null, [{
key: "show",
value: function show() {}
}]);
return Person;
}();
从代码中我们很清楚的看到当我们使用static
对方法进行静态化的时候,在内部其实是挂载在了Person.show = function(){}
身上,而不是Person.prototype
上。
1.10类的静态属性和实例属性
类的静态属性指的是Class
本身的属性,即Class.propname
,而不是定义在实例对象this
上的属性,例如:
class Person{}
Person.prop = 1//为类添加一个属性值,只属于这个类
Person.prop//1
上面的写法可以读/写Person
类的静态属性prop
。目前,只有上面这种写法可行,因为ES6
明确规定,Class
内部只有静态方法,没有静态属性。以下两种写法都无效:
class Person{
//写法一
prop:1
//写法二
static prop:1
}
Person.prop//undefine
类的实例属性:
Class
的实例属性可以用等式写入类的定义当中,例如:
class Person{
age = 21
}
var person = new Person()
console.log(person.age)//21
我们通过等式在类中定义的属性是类的实例属性,我们只能在实例当中读取到它,而使用类去读取Person.prop
会返回undefine
。以前我们只能通过constructor
函数来为实例添加属性,现在我们有了一种新的为实例添加属性的方法,那就是我们直接在类中用等式为实例添加属性。这种写法其实在内部是这样的:
_defineProperty(this, "age", 21);
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 Person{
static prop = 'xz'
}
此时我们就为类添加了一个静态属性。这种写法等价于:Person.prop = 'xz'
。
二,Class的继承
2.1Reflect.construct()
在讲类的继承之前我们不得不讲一个函数Reflect.construct()
这个函数,只有知道这个函数的用处,我们才能真正的知道ES6
中类的继承是什么一回事。
语法
Reflect.construct(target, argumentsList[, newTarget])
参数
-
target
被运行的目标构造函数
-
argumentsList
类数组,目标构造函数调用时的参数。
-
newTarget
可选作为新创建对象的原型对象的
constructor
属性, 参考new.target
操作符,默认值为target。
实例:
function OneClass() {
this.name = 'one';
}
function OtherClass() {
this.name = 'other';
}
// 创建一个对象:
var obj1 = Reflect.construct(OneClass, args, OtherClass);
// 与上述方法等效:
var obj2 = Object.create(OtherClass.prototype);
OneClass.apply(obj2, args);
console.log(obj1.name); // 'one'
console.log(obj2.name); // 'one'
console.log(obj1 instanceof OneClass); // false
console.log(obj2 instanceof OneClass); // false
console.log(obj1 instanceof OtherClass); // true
console.log(obj2 instanceof OtherClass); // true
上面的代码来自MDN
官网的一段代码实例,从这个代码实例中我们可以看到Reflect.construct()
函数的本质。当我们在调用该函数的时候,并且三个参数都齐全的时候,例如:Reflect.construct(sub,'xz',sup)
。这段代码等价于:
var obj = new sup()
sub.call(obj,'xz')
也就是说,创建的实例对象的obj.__proto__ === sup.prototype
,然后sub.call(obj,...arguments)
。我们只是利用sub
构造函数来初始化一下数据而已。而它继承sup
的原型对象。
2.2代码的转换:
ES6
:
class Car{
constructor(name){
this.name = name
}
show(){
console.log(this.name)
}
}
class BMW extends Car{
constructor(width,name){
super(name)
this.width = width
}
showinfo(){
console.log(this.width)
}
}
var obj = new BMW(2,'baoma')
ES5
:
"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);
}
//_inherits(BMW,_Car),该函数的作用就是让父类的实例作为子类的原型,并将父类的构造函数设置为子类的隐式原型对象
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
//我们创建一个新的对象,该对象是子类的原型,该新对象继承了父类的原型,并且我们还添加了constructor属性,该属性就是Object.create函数的第二个参数
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);
}
//_createSuper(BMW),该函数返回_createSuperInternal函数
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
console.log(hasNativeReflectConstruct)
//_super.apply(this, name) this是子类的实例对象
return function _createSuperInternal() {
//这里的Derived指向子类的构造函数,这里值BMW
var Super = _getPrototypeOf(Derived),result;//获取Derived的隐式原型,也就是父类构造函数。这里的var Super = _getPrototypeOf(Derived),result
//等价于:var Super = _getPrototypeOf(Derived);var result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;//NewTarget是子类的构造函数
console.log(NewTarget === Derived)//true
/**方法的行为有点像 new 操作符 构造函数 , 相当于运行 new target(...args).
* Reflect.construct(target, argumentsList[, newTarget])
* target:被运行的目标构造函数
* argumentsList:类数组,目标构造函数调用时的参数。
* newTarget(可选):作为新创建对象的原型对象的constructor属性的属性值。
*/
result = Reflect.construct(Super, arguments, NewTarget);
console.log(result.__proto__ === NewTarget.prototype)
//result虽然是有父类创建的,但是 result.__proto__ === BMW.prototype
} else {
//这里的this指向的是子类的实例对象,argumnets在宏观上是在'new 子类'的时候传给constructor的参数。在转es5代码时转移到了子类的构造函数身上
//这里的Super是父类的构造函数,这里是使用原型式继承来间接的让子类实例继承父类的属性。result是已经初始化完成的子类实例对象。
result = Super.apply(this, arguments);
// console.log(result)
}
return _possibleConstructorReturn(this, result);//这里的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;
}
//一般情况下这个函数返回true
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 _instanceof(left, right) {
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
return !! right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}
//判断构造函数(类)是否是通过new来操作的
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, 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;
}
var Car = /*#__PURE__*/function () {
function Car(name) {
_classCallCheck(this, Car);
this.name = name;
}
_createClass(Car, [{
key: "show",
value: function show() {
console.log(this.name);
}
}]);
return Car;
}();
var BMW = /*#__PURE__*/function (_Car) {
_inherits(BMW, _Car);//将父类的实例变为子类的原型,并将父类的构造函数设为子类的__proto__
var _super = _createSuper(BMW);
function BMW(width, name) {
var _this;
//判断是否是通过new运算符来操作子类的
_classCallCheck(this, BMW);
//我们将子类实例对象传入到_super中,顺便将其余参数传入 _super等价于:_createSuperInternal函数
_this = _super.call(this, name);//这里的_this其实是父类的实例对象
_this.width = width;
return _this;//返回的是父类的实例对象
}
_createClass(BMW, [{//这个地方一定要注意,此时的BMW的原型是父类的实例对象,也就是说,此时我们向原型上添加方法其实是向父类的实例对象添加方法
key: "showinfo",
value: function showinfo() {
console.log(this.width);
}
}]);
return BMW;
}(Car);
var obj = new BMW(2, 'baoma');
通过阅读上面的代码我们会获得下面的这个图:
图中的实例2
是result
的值,也是我们最后返回的_this
的值。有几个地方需要注意:第一,BMW
原型的变更在向外暴露BMW
构造函数之前就已经更改为父类的实例对象。第二,返回的实例2其实是父类的实例对象,但是我们让它的隐式原型直接更改为了实例1
了。其实继承的思路也很简单,主要是通过这个语句完成继承的关键部分:
result = Reflect.construct(Super, arguments, NewTarget);
首先执行父类构造函数,那么创建的实例对象的数据就会被初始化一次,又因为该实例对象的原型为Car
的实例对象,那么就很容易的去实现了对父类原型上的方法的继承。接着我们在前面就知道了,添加给子类的方法也同样会被添加到子类构造函数的原型上,而该原型又是实例2
的隐式原型,所以也会继承子类的方法,至于子类内部的属性,会在调用子类构造函数的时候被初始化。这就是继承的大体原理,当然这还有很多值得我们注意的地方。我们下面将会讲到我们应该注意的地方。
2.3extends
我们可以通过extends
关键字来实现子类对父类的继承。子类必须在constructor
方法中调用super
方法,否则新建的实例会报错。原因在于子类没有自己的this
对象,而是继承父类的this
对象,然后进行加工,如果不调用super
关键字,那么子类就拿不到this
对象。
var _this
_this = _super.call(this, name)
如果不写super
,那么就没有上面的几行语句。那么我们就根本获取不了其返回的_this
实例对象。
2.4super关键字
super
关键字既可以当作函数来调用,也可以当作对象来访问属性或者方法。这两种使用方法所代表的含义完全不一样。
-
super
当函数调用:当
super
当做函数来调用时,它表示的其实是父类的构造函数,其实super
的调用是一个比较复杂的事情,这里可能概括不准确,有兴趣的同学可以去看源码分析。super
函数返回的是一个父类实例对象。我们只能在子类的constructor函数内部使用该函数,在其他地方使用会报错。 -
super
当做对象使用:**当
super
当做对象使用的时候,它指向的是父类的原型对象。在静态方法中指向父类。**例如:class sup{ constructor(){} show(){//向父类的原型上挂载show()方法 console.log('sup') } } class sub extends sup{ constructor(){ super() } showinfo(){ console.log(super.show()) } } var obj = new sub() obj.showinfo()//sup
我们从结果可以看到,子类原型上的
show
函数内部调用了super.show()
,此时打印的结果为sup
,说明super
此时指向的就是父类的原型对象。注意:
由于super指向的是父类的原型对象,所以定义在父类实例上的方法或者属性是无法通过super调用的。
其实上面的这段话也非常的好理解,那就是我们挂载在父类实例对象上的属性或者方法我们是无法通过
super
获取到的,例如:class Person{ constructor(){ this.name = 'xz'//我们向父类实例对象添加name属性 } } class Student extends Person{ constructor(){ super() this.age = 21 } showName(){ console.log(super.name)//因为super指向的是父类的原型,所以我们不能获取到该属性值 } } var obj = new Student() obj.showName()//undefine
ES6规定,通过super调用父类的方法时,super会绑定子类的this。这句话是什么意思呢?我们来看一个实例:
class A{ constructor(){ this.x = 1 } print(){ console.log(this.x) } } class B extends A{ constructor(){ super() this.x = 2 } m(){ super.print() } } var obj = new B()
其实上面的称述是正确的,但是有一点要说的是,其实上面说的子类的
this
就是我们返回的父类的实例。如果你从源码的角度去看上面的代码,其实this.x = 1
会先执行,在我们的父类实例对象中添加该属性,然后继续执行this.x = 2
。那么此时this
指向的仍是父类实例对象,也就是说,它们必然会发生覆盖现象,那么此时的父类实例对象中的x
属性的值是2
。然后返回我们的实例对象。我们再来分析ES6规定,通过super调用父类的方法时,super会绑定子类的this
这句话,其实与其说super
会绑定子类到的this
,倒不如换句话说,它会绑定我们返回的父类实例对象。因为在ES6
的继承上面,是不存在子类实例这个说法的。如果super作为对象在静态方法中使用,这时super将指向父类,而不是父类的原型对象。其实这句话也很好理解,例如:
class A{ constructor(){ this.x = 1 } print(){ console.log(234) } static print(){ console.log(123) } } class B extends A{ constructor(){ super() this.x = 2 } static m(){ super.print() } } var obj = new B() obj.__proto__.constructor.m()//123
首先我们在父类中定义了两个
print
,一个是静态的,另一个是非静态的。然后我们通过obj.__proto__.constructor.m
来调用子类的静态方法m
。此时我们使用了super
对象,此时的super
是在静态方法中调用的。所以会指向父类,而非父类原型,所以会打印出123
。这里的constructor
不是指类中定义的函数,而是我们传统对象中的constructor
,指向的是生成该实例的构造函数。
2.5类的prototype属性和__proto__属性
其实这里的类,是指代码转换为es5
后的构造函数,如果我们阅读了前面的代码后一定会得出以下的结论:
- 子类的
__proto__
指向的是父类 - 子类的
prototype
属性的__proto__
属性总是指向父类的prototype
属性
其实这些结论在前面的那张图里体现的有。
2.6实例的__proto__属性
**子类实例的__proto__
属性的__proto__
指向的是父类实例的__proto__
**其实这里的子类实例指的是我们最后返回的实例对象。
上一篇: ES6(7):class类
下一篇: ES6中的类(Class)