(转载请注明出处)
什么是类
我们都知道,类其实就是一个可以动态批量生产一类对象的封装,在没有使用类之前,我们想要获得一个对象就得这样:
// 实例化对象方式
const people = new Object();
people.name = "小明";
people.age = 18;
或者这样:
// 字面量方式
const people = {
name: "小明",
age: 18
}
这种方式虽然可以实现对象,但是如果我们要批量创造一类对象,就需要重复写上面的代码,非常麻烦,所以我们引入了“类”这个方式来减轻我们的工作。
ES5中的仿类结构(以下称构造类)
在ES5及之前的时代,我们的js是没有类的,于是我们聪明的工程师就找到了一个使用函数来模拟类的方式:
function People(name, age) {
this.name = name;
this.age = age;
}
const people = new People("小明", 18);
const people2 = new People("小红", 16);
console.log(people); // People { name: '小明', age: 18 }
console.log(people2); // People { name: '小红', age: 16 }
这样我们就可以动态创建一类对象了。
实例属性、原型属性及静态属性
我们上面写的是一个简化的构造类,其中只包含了类中的实例属性,事实上一个类除了包含实例属性,还可以包含原型属性、静态属性,其中原型属性在构造类实例化之后就可以在实例化对象上访问,而静态属性只能在构造类上访问:
function People(name, age) {
this.name = name; // 这是实例属性
this.age = age;
}
People.prototype.getName = function () { // 这是构造类上的原型属性(方法)
return this.name;
}
People.getPeopleInstance = function (name, age) { // 这是构造类上的静态属性(方法)
return new People(); // 返回构造类的实例化对象
}
const people = new People("小红", 16); // 实例化对象
console.log(people.name) // 直接访问实例化对象上的实例属性
people.getName(); // 使用实例化对象访问构造类的原型方法
People.getPeopleInstance("小明", 18); // 在构造类上访问静态方法
ES6中类的结构
在说完ES5中构造类的内部结构之后,我们再看看ES6中类的结构:
/**
* ES6中的类
*/
class People {
constructor (name, age) { // 这是构造器, 接收实例化时传入的参数
this.name = name; // 这是实例属性
this.age = age;
}
static getPeopleInstance (name, age) { // 这是构造类上的静态属性(方法)
return new People(name, age); // 返回构造类的实例化对象
}
getName () { // 这是构造类上的原型属性(方法)
return this.name;
}
}
const people = new People("小红", 16); // 实例化对象
console.log(people.name) // 访问实例化对象上的实例属性
people.getName(); // 使用实例化对象访问构造类的原型方法
People.getPeopleInstance("小明", 18); // 在构造类上访问静态方法
ES6中类的新特性
1.在ES6的外部可以给类名重新赋值,但在内部赋值会抛出错误
class Foo {
constructor () {
}
}
Foo = "这是个字符串"; // 成功赋值
console.log(Foo); // "这是个字符串"
class Foo {
constructor () {
Foo = "这是个字符串"; // TypeError: Assignment to constant variable.
}
}
new Foo();
2.ES6的类不能以普通函数形式调用
class Foo { }
Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'
3.ES6类的原型方法不能实例化
class Foo {
print () {
console.log("这是一个原型方法")
}
}
const foo = new Foo();
new foo.print(); // TypeError: foo.print is not a constructor
4.ES6类的原型属性不可枚举
class People {
constructor (name, age) {
this.name = name;
this.age = age;
}
getName () {
return this.name;
}
}
const people = new People("小明", 18);
for (let p in people) {
console.log(p); // name age 没有 getName
}
5.ES6中基类的静态属性(方法)可以被继承
class People {
constructor (name, age) { // 这是构造器, 接收实例化时传入的参数
this.name = name; // 这是实例属性
this.age = age;
}
static getPeopleInstance (name, age) { // 这是构造类上的静态属性(方法)
return new People(name, age); // 返回构造类的实例化对象
}
getName () { // 这是构造类上的原型属性(方法)
return this.name;
}
}
class AnotherPeople extends People {
constructor (name, age) {
super(name, age); // 映射当前类构造器到基类,并向基类传参
}
}
const anotherPeople = AnotherPeople.getPeopleInstance("小王", 22); // 访问从基类继承的静态属性(方法)
console.log(anotherPeople); // People { name: '小王', age: 22 }
如何实现ES6中的类
我们根据以上ES6的新特性,来手动实现一个功能与ES6相似的类
// 长方形类
let Rect = (function () { // 用变量接收一个立即执行函数,这样可以使得变量名可以在外部重新赋值
"use strict" // 使用严格模式,显式抛出错误
const Rect = function (length, width) { // 内部使用常量接收一个函数,这样可以使得内部无法对类名重新赋值
if (new.target === undefined) { // new.target是用来判断实例化对象指向的构造函数,如果对象不以实例化方式调用,则new.target指向undefined
throw new Error(`Class constructor Rect cannot be invoked without 'new'`); // 如果对象被用作常规函数执行,则显式抛出错误
}
this.length = length;
this.width = width;
}
Rect.staticM = function () { // 这是类上的一个静态方法
console.log("this is a static method")
}
Object.defineProperty(Rect.prototype, "getArea",{
value () { // 使用新的箭头函数简化写法, 由于箭头函数没有构造器,所以实例化时会抛出错误
return this.length * this.width;
},
enumerable: false, // 类的原型方法不可枚举,所以需要将对象内的枚举属性设为false
writable: true,
configurable: true
})
return Rect; // 将立即执行函数内的类返回
})()
如何实现ES6中的类继承
上面一个示例我们使用了new.target, 我们ES6中继承时子类会改变基类new.target指向。 但是如果我们使用ES5的结构类经典继承法,则不能改变基类new.target指向,从而使得基类的new.target指向undefined,导致执行失败。所以在这里我们要采用ES6中代理方法,也就是以映射的方式改变基类new.target指向,这样我们才能ES6中的类一样的表现方式, 以下是完整代码:
// 长方形基类
let Rect = (function () { // 用变量接收一个立即执行函数,这样可以使得变量名可以在外部重新赋值
"use strict" // 使用严格模式,显式抛出错误
const Rect = function (length, width) { // 内部使用常量接收一个函数,这样可以使得内部无法对类名重新赋值
if (new.target === undefined) { // new.target是用来判断实例化对象指向的构造函数,如果对象不以实例化方式调用,则new.target指向undefined
throw new Error(`Class constructor Rect cannot be invoked without 'new'`); // 如果对象被用作常规函数执行,则显式抛出错误
}
this.length = length;
this.width = width;
}
Rect.staticM = function () { // 这是类上的一个静态方法
console.log("this is a static method")
}
Object.defineProperty(Rect.prototype, "getArea",{
value () {
return this.length * this.width;
},
enumerable: false, // 类的原型方法不可枚举,所以需要将对象内的枚举属性设为false
writable: true,
configurable: true
})
return Rect; // 将立即执行函数内的类返回
})()
// Square 子类
let Square = (function (_Rect) { // 将基类以参数的形式传给立即执行函数
"use strict"
const Square = function (length) {
var NewTarget = Object.getPrototypeOf(this).constructor; // 获取当前类的构造器
let result = Reflect.construct(_Rect, [length, length], NewTarget); // 实例化基类, 将子类参数传入,并将子类构造器映射到基类上,以修改基类new.target指向 (目标类, [参数],映射)
result.length = length; // 在实例化对象上添加子类实例属性
return result; // 显式返回实例化对象
}
Square.prototype = Object.create(_Rect.prototype, {constructor: { // 继承基类原型
value: Square, // 修正构造器指向
writable: true,
configurable: true
}});
// Object.setPrototypeOf(Square, _Rect); // 将基类设为子类的原型
Object.defineProperties(Square.prototype, { // 添加子类原型方法
getArea: {
value () {
const superValue = Reflect.get(_Rect.prototype, "getArea")
// Reflect.get(Object.getPrototypeOf(Square.prototype), "getArea"); // // 获取基类原型方法(实现ES6中的super.getArea)
return superValue.call(this); // 调用基类原型方法并修改this上下文指向
},
enumerable: false
}
})
for (let staticMethod in _Rect) { // 继承基类静态方法
Square[staticMethod] = _Rect[staticMethod];
}
return Square;
})(Rect);
const square = new Square(6); // 实例化子类
square.getArea() // 调用子类原型方法(测试在子类原型方法中重载基类原型方法)
Square.staticM();
以上就是ES6类及继承实现,如有疏漏之处还请不吝指正。