深入理解ES6中的类

407 阅读7分钟
(转载请注明出处)

什么是类

我们都知道,类其实就是一个可以动态批量生产一类对象的封装,在没有使用类之前,我们想要获得一个对象就得这样:

// 实例化对象方式
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类及继承实现,如有疏漏之处还请不吝指正。