ES6 中Class创建对象与继承实现

1 Class in ES6

ES6提出了类(Class)的概念,让对象的原型的写法更像面向对象语言写法。 ES6中通过class定义对象,默认具有constructor方法和自定义方法,但是包含在class中的方法不可枚举。

        class Point{
            constructor(){
            this.x=x;
            this.y=y;
            }
        toString(){
            return this.x+this.y;
            }
        }

注意:constructor方法对应ES5的构造函数;创建类的方法不需要使用function关键字,方法之间不需要逗号分隔。

ES5中定义对象时组合使用构造函数模式和原型模式,构造函数模式用于定义实例属性,原型模式用于定义共享方法和共享属性,节省内存,而且支持向构造函数传递参数。

        function Point (x,y){
            this.x=x;
        this.y=y;
        }
        Point.prototype.toString= function(){
            return this.x+this.y;
        }
        

2 ES5和ES6创建实例和对象对比

1)ES5中的Person构造函数和Person原型对象以及实例Person1、Person2的关系:构造函数的prototype属性以及实例的__proto__属性指向原型对象,原型对象的constructor属性指向构造函数。
ES6 中Class创建对象与继承实现

构造函数的原型(prototype)属性在ES6中依旧存在,这一点和ES5一样:类的方法都定义在类的原型(prototype)上,在类的实例上面调用方法,其实就是调用原型上的方法。

    //实例的constructor方法就是类的原型(prototype)的constructor方法
    class B{
      constructor(){}
    }
    let b = new B();
    console.log(b.constructor === B.prototype.constructor);//true

2)ES6的类可以看做是构造函数的另一种写法(和ES5中构造函数等同的并不是constructor方法):类是function类型,且类本身指向类的prototype对象的constructor属性,这与ES5一样。

        class Point{
          // ...
        }
        console.log(typeof Point) // "function"
        console.log(Point === Point.prototype.constructor) // true

3)使用类创建实例对象也是直接对类使用new命令,跟ES5中构造函数的用法一致。

4)类内部定义的方法都是不可枚举的,这和ES5不同。
ES6 中Class创建对象与继承实现ES6 中Class创建对象与继承实现

5)constructor方法
一个类必须有constructor方法,如果没有就默认添加constructor(){}。虽然类是函数,但是和ES5不同,不通过new而直接调用类会导致类型错误,也就是说这个函数仅当在和new一起使用时才有意义。
6)类的实例
与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

            class Point {
              constructor(x, y) {
                this.x = x;
                this.y = y;
              }
              toString() {
                return '(' + this.x + ', ' + this.y + ')';
              }
            }
            var point = new Point(2, 3);
            point.toString() // (2, 3)
            point.hasOwnProperty('x') // true
            point.hasOwnProperty('y') // true
            point.hasOwnProperty('toString') // false
            point.__proto__.hasOwnProperty('toString') // true

以上代码中,x和y都是实例对象point自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Point类上),所以hasOwnProperty方法返回false。这些都与ES5的行为保持一致。

与ES5一样,类的所有实例共享一个原型对象。

        var p1 = new Point(2,3);
        var p2 = new Point(3,2);
        p1.__proto__ === p2.__proto__
        //true

可以通过实例的__proto__属性为Class原型添加方法,增加后所有的原型都具有这个方法,这和ES5一样。

        var p1 = new Point(2,3);
        var p2 = new Point(3,2);
        p1.__proto__.printName = function () { return 'Oops' };
        p1.printName() // "Oops"
        p2.printName() // "Oops"

7)Class不存在变量提升(hoist),这一点与ES5完全不同。

        new Foo(); // ReferenceError
        class Foo {}

上面代码中,Foo类使用在前,定义在后,这样会报错,因为ES6不会把类的声明提升到代码头部。这种规定的原因与继承有关,必须保证子类在父类之后定义。

8)严格模式
类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以ES6实际上把整个语言升级到了严格模式。

3 ES6 Class的继承

Class通过extends关键字实现继承,这和ES5通过修改原型链实现继承不同(巧合的是,SASS也通过@extend实现样式继承):

        class ColorPoint extends Point{}

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

    class ColorPoint extends Point {
      constructor(x, y, color) {
        super(x, y); // 调用父类的constructor(x, y)
        this.color = color;
      }
      toString() {
        return this.color + ' ' + super.toString(); // super代表父类原型,调用父类的toString()
      }
    }

上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
super这个关键字,既可以当作函数使用,也可以当作对象使用。第一种情况,super作为函数调用时,代表父类的构造函数,只能用在子类的构造函数中。ES6 要求,子类的构造函数必须执行一次super函数。第二种情况,super作为对象时,指代父类的原型对象。

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。(ES5通过原型模式的继承:创建子类,然后将父类的实例赋值给子类原型,也就是重写子类原型,代之以一个新类型的实例。)ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

如果子类没有定义constructor方法,这个方法会被默认添加。在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。

        class Point {
          constructor(x, y) {
            this.x = x;
            this.y = y;
          }
        }
        
        class ColorPoint extends Point {
          constructor(x, y, color) {
            this.color = color; // ReferenceError
            super(x, y);
            this.color = color; // 正确
          }
        }

和ES5一样,通过子类创建的实例是父类以及子类的实例:

        let cp = new ColorPoint(25, 8, 'green');
        
        cp instanceof ColorPoint // true
        cp instanceof Point // true

4 补充

1)类的prototype属性和__proto__属性(这段还没看明白,太绕了。)

ES5中每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。ES6中没有构造函数,Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
这样的结果是因为,类的继承是按照下面的模式实现的。

        class A {
        }
        class B {
        }
        // B的实例继承A的实例
        Object.setPrototypeOf(B.prototype, A.prototype);
        const b = new B();
        // B的实例继承A的静态属性
        Object.setPrototypeOf(B, A);
        const b = new B();

这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例。

2)Object.getPrototypeOf

Object.getPrototypeOf方法可以用来从子类上获取父类。

        Object.getPrototypeOf(ColorPoint) === Point
        // true

因此,可以使用这个方法判断,一个类是否继承了另一个类。

3)实例的__proto__属性

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类实例的原型的原型,是父类实例的原型。

            
            var p1 = new Point(2, 3);
            var p2 = new ColorPoint(2, 3, 'red');
            p2.__proto__ === p1.__proto__ // false
            p2.__proto__.__proto__ === p1.__proto__ // true

总的来说,ES6是对ES5的形式上的改变,真实内容依旧不变,本质上依旧是通过原型链实现继承。

上一篇:js中类型判断


下一篇:RVO优化