美文网首页
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