美文网首页
JavaScript中的类

JavaScript中的类

作者: 大喵爱读书 | 来源:发表于2018-09-26 20:54 被阅读0次

类是面向对象的基础,是我们组织代码的一大利器,学好使用类会使我们可以更好的理解面向对象编程的思想,下边就整理一些JavaScript中类的知识点,看看类在JavaScript语言中是个怎么样的存在。

es5中的类

JavaScript语言早期是没有class这个概念,只能通过一些手段去模拟类。

    function PersonType(name) {
        this.name = name;
    }
    PersonType.prototype.sayName = function () {
        console.log(this.name);
    };
    const pt = new PersonType('lilei');
    pt.sayName();

es6中的类

考虑到类实在使用的很多,es6中加入了类(class),使用class来工作比es5中的方式要方便很多,代码如下所示:

   class PersonType {
       constructor(name) {
           this.name = name;
       }
       sayName() {
           console.log(this.name);
       }
   }
   const pt = new PersonType('lilei');
   pt.sayName();

es6中类的底层实现机制

es6中的类其实底层还是使用es5中的原型链在工作,只是在其上面封装了一层语法糖。
上面的代码如果使用es5重写就是如下的样子

let PersonType = (function () {
       'use strict';
       const PersonType = function (name) {
           if (typeof new.target === 'undefined') {
               throw  new Error('函数只能new');
           }
           this.name = name;
       };
       Object.defineProperty(PersonType.prototype, 'sayName', {
           value: function () {
               if (typeof new.target !== 'undefined') {
                   throw new Error('函数不能new');
               }
               console.log(this.name);
           },
           enumerable: false,
           configurable: true,
           writable: true
       });
        return PersonType;
    })();
    const pt = new PersonType('lilei');
    pt.sayName();

从上面的代码可以看出es6的类存在如下特性:
1.类中的代码都是严格模式
2.类变量不会变量提升(因为考虑会影响到类的继承,父类一定要在子类面前先声明
3.类的构造函数如果用不是new的形式调用会报错。
4.类中定义的方法都是不可枚举的(typescript中类的方法都是可枚举的)
5.类中非构造函数使用new调用会报错
6.类名在类的内部不能修改(在类的外部是可以修改的)

类表达式

类除了基本声明方式定义类之外还有表达式形式,表达式有两种表达方式:

基本表达式方式

     const PersonType = class{
        constructor(name) {
            this.name = name;
        }
        sayName() {
            console.log(this.name);
        }
    };

命名表达式方式

     const PersonType = class PersonType2{
        constructor(name) {
            this.name = name;
        }
        sayName() {
            console.log('inner',PersonType2)
            console.log(this.name);
        }
    };
    console.log('outer',PersonType2)

命名表达式定义类时的名字(PersonType2)只能在类内部使用,外部无法识别。代码中outer处会报错,inner处就可以正确读取到类。
:使用类表达式的方式有一大好处就是,可以使用立即调用函数的方式直接生成对象如下所示:

     const pt = class PersonType2{
        constructor(name) {
            this.name = name;
        }
        sayName() {
            console.log('inner',PersonType2)
            console.log(this.name);
        }
    }();
    console.log('outer',PersonType2)

这样,pt就直接是PersonType对象的实例,这种方式可以直接实现单例模式

可计算成员变量

es6中的类和对象字面量一样,属性和方法名可以动态计算。例如:

let methodName = 'sayName';
   class PersonType {
       constructor(name) {
           this.name = name;
       }
       [methodName]() {
           console.log(this.name);
       }
   }
   const pt = new PersonType('lilei');
   pt.sayName();

类中的属性或者方法加上[]后会先解析变量,最终结果和之前的es6类例子结果是一样的。

生成器方法

es6中引入了生成器的概念,在类中可以直接使用生成器方法,定义的方法也很简单在方法名前加上*就可以,代码如下:

class PersonType {
    name;
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
    *createIterator() {
        yield 1;
        yield 2;
        yield 3;
    }
}

除了生成器方法,也可以给类定义默认迭代器,形式如下:

class PersonType {
    name;
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
    }
}

es6中定义默认迭代器就是使用Symbol.iterator,Symbol是es6新引入的类型,Symbol.iterator是es6设定好的表示对象默认迭代器的标志。

静态成员

静态成员表示可以直接通过类来调用,不一定必须通过实例来调用。

es5中的静态成员

在es5中静态成员通过直接给函数上设置属性来模拟,代码如下:

    function PersonType(name) {
        this.name = name;
    }
    PersonType.prototype.sayName = function () {
        console.log(this.name);
    };
    PersonType.create = function (name) {
        return new PersonType(name);
    };
    // const pt = new PersonType('lilei');
    const pt = PersonType.create('lilei');
    pt.sayName();

es6中的静态成员

es6中定义静态成员就非常简单了,在属性或者方法前加上static就可以了。

class PersonType {
    name;
    constructor(name) {
        this.name = name;
    }
    static create(name) {
        return new PersonType(name);
    }
    sayName() {
        console.log(this.name);
    }
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
    }
}

注意:不可以在实例中访问静态成员,只能通过类来访问。

继承

es5中的继承

 function Rectangle(length, width) {
        this.length = length;
        this.width = width;
    }

    Rectangle.prototype.getAreas = function () {
        return this.length * this.width;
    };

    function Square(length) {
        Rectangle.call(this, length,length);
    }
    Square.prototype = Object.create(
        Rectangle.prototype,{
            constructor:{
                value: Square,
                enumerable: true,
                writable: true,
                configurable: true
            }
        }
    );
    const square = new Square(3);
    const result = square.getAreas();
    console.log(square,result);

es6中的继承

对比es5的方式,使用es6的方式实现继承就非常简单了。

class Rectangle {
    length;
    width;
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }

    getAreas () {
        return this.length * this.width;
    }
}

export class Square extends Rectangle {
    constructor(length) {
        super(length, length);
    }
}

和java语言一样,es6中继承一样使用extends操作,使用子类extends父类的方式来完成继承。
这里需要注意的地方是super方法,必须要在子类构造方法中通过super方法来调用父类的构造方法来完成初始化,尤其是手动写了构造方法,那必须手动添加super方法,如果你在派生类中没有添加构造函数,那么类会自动在构造函数中添加super方法。这里还有一点需要注意的是,派生类中的super方法必须要在访问this之前调用super方法,如果在super方法之前访问了this就会报错
如果当真不想调用super方法,只有让构造函数直接返回一个对象,一般不要这么干。

类方法遮蔽

继承中派生类会遮蔽掉父类的方法,这个和原型链继承的表现是一样的。
假如在Square类中也有个getAreas方法,那么Rectangle中的getAreas方法在Square类的实例中将无法调用。
如果有特殊需求希望直接调用父类的方法,那么也可以在子类中直接通过super调用父类的方法。

class Rectangle {
   length;
   width;
   constructor(length, width) {
       this.length = length;
       this.width = width;
   }

   getAreas () {
       return this.length * this.width;
   }
}

export class Square extends Rectangle {
   constructor(length) {
       super(length, length);
   }
   getAreas () {
       return super.getAreas();
   }
}

静态成员的继承

父类的静态成员也同样继承到派生类中,如下例子

export class Rectangle {
    length;
    width;

    static create(length, width) {
        return new Rectangle(length, width);
    }

    constructor(length, width) {
        this.length = length;
        this.width = width;
    }

    getAreas () {
        return this.length * this.width;
    }

}

export class Square extends Rectangle {
    constructor(length) {
        super(length, length);
    }
    getAreas () {
        return super.getAreas();
    }
}

const rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); //true
console.log(rect instanceof Square); //false
console.log(rect.getAreas());  //12

派生自表达式的类

es6中继承不一定只能继承类,只要表达式可以被解析为一个函数并且具有construct属性和原型,那么就可以用extends进行继承。

例如下边两种情况

 function Rectangle(length, width) {
        this.length = length;
        this.width = width;
    }

    Rectangle.prototype.getAreas = function () {
        return this.length * this.width;
    };
   class Square extends Rectangle {
    constructor(length) {
        super(length, length);
    }
    getAreas () {
        return super.getAreas();
    }
}

Rectangle是一个函数可以被解析为构造函数,所以Square可以继承它。

 function Rectangle(length, width) {
        this.length = length;
        this.width = width;
    }

 Rectangle.prototype.getAreas = function () {
        return this.length * this.width;
    };
function getBase(){
  return Rectangle
}
   class Square extends getBase() {
    constructor(length) {
        super(length, length);
    }
    getAreas () {
        return super.getAreas();
    }
}

getBase是类声明的一部分,可以执行函数然后取得返回值来继承。

这种特性可以让我们写出很漂亮的代码,可以根据需要动态设定类的继承关系,活用可以玩出很多令人眼前一亮的代码,比如下边这样。

const SerializableMixin = {
    serialize() {
        return JSON.stringify(this);
    }
};

const AreaMixin = {
    getArea() {
        return this.length * this.width;
    }
};

function mix(...mixins) {
    const base = function () {};
    Object.assign(base.prototype, ...mixins);
    return base;
}
export class Square extends mix(AreaMixin, SerializableMixin) {
    constructor(length) {
        super(length, length);
    }
}

内建对象的继承

活用继承可以改写很多JavaScript固有的对象,让它可以完成更多满足我们要求的功能,比如数组。
使用es5的方法继承重写数组几乎是不可能

 export function MyArray() {
    Array.call(this, ...arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
    constructor: {
        value: MyArray,
        enumerable: true,
        writable: true,
        configurable: true,
    }
});
const arr = new MyArray();
arr[0] = 1;
arr.length = 0 ;
console.log(arr[0]);//1

上面的代码 MyArray并没有表现出Array该有的效果,至于原因本人目前也不是很清楚,大体解释是this初始化时指向的问题。
使用es6的继承方式就可以很容易在Array类基础上进行增强。

export class MyArray extends Array{

}
const arr = new MyArray();
arr[0] = 1;
arr.length = 0 ;
console.log(arr[0]);//undefined

Symbol.spcies属性

返回函数的静态访问器属性,默认返回this

通过实例来生成类新的实例默认调用[Symbol.species]接口

export class MyArray extends Array {
    constructor(...args) {
        super(...args);
    }
    static get [Symbol.species]() {
        return Array;
    }
}

const arr = new MyArray(1, 2, 3, 4);
// arr[0] = 1;
// arr.length = 0 ;
const sli = arr.slice(2);
console.log(sli);
console.log(sli instanceof MyArray);
console.log(sli instanceof Array);

如果上边的代码不加Symbol.species接口slice方法返回的实例就是MyArray实例,加了后返回的就是Array,实例中生成类新的实例默认调用的接口都是Symbol.species接口

new target

我们都知道通过new.target属性可以知道当前对象是如何调用的,因此可以通过new.target属性模拟抽象类。

export class Shape {
    constructor() {
        if (new.target === Shape) {
            throw Error('该类不能直接实例化');
        }
    }
}
export class Rectangle extends Shape {
    length;
    width;

    static create(length, width) {
        return new Rectangle(length, width);
    }

    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }

    getAreas () {
        return this.length * this.width;
    }

}

上边的例子,如果直接实例化Shape就会报错,只能通过继承的方式来调用,间接的模拟了抽象类。

相关文章

网友评论

      本文标题:JavaScript中的类

      本文链接:https://www.haomeiwen.com/subject/ovoloftx.html