Class 的基本语法
ECMAScript 的原生构造函数大致有下面这些:
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
|
|
类的所有实例共享一个原型对象,这也意味着,可以通过实例的 __proto__ 属性为“类”添加方法。__proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。使用实例的 __proto__ 属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。
与函数一样,类也可以使用表达式的形式定义,如下。需要注意的是,这个类的名字是 Me,但是 Me 只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用 MyClass 引用。如果类的内部没用到的话,可以省略 Me。
|
|
注意:
- 类和模块的内部,默认就是严格模式,考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式
- 类不存在变量提升(hoist),这一点与 ES5 完全不同
name属性总是返回紧跟在class关键字后面的类名- 如果某个方法之前加上星号(*),就表示该方法是一个
Generator函数 - 类的方法内部如果含有
this,它默认指向类的实例。但是,一旦单独使用该方法,很可能报错,可以在构造方法中绑定this或者使用箭头函数来解决
|
|
如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
|
|
私有方法和私有属性, ES6 不提供,只能通过变通方法模拟实现。私有属性和私有方法前面,也可以加上 static 关键字,表示这是一个静态的私有属性或私有方法,只能在类的内部调用。
|
|
ES6 为 new 命令引入了一个 new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数。如果构造函数不是通过 new 命令或 Reflect.construct() 调用的,new.target 会返回 undefined,因此这个属性可以用来确定构造函数是怎么调用的。
|
|
Class 的继承
Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。父类的静态方法,也会被子类继承。
|
|
子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面 (Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用super 方法),然后再用子类的构造函数修改 this。
如果子类没有定义 constructor 方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有 constructor 方法。
|
|
super 这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。第一种情况,super 作为函数调用时,代表父类的构造函数,super() 只能用在子类的构造函数之中。ES6 要求,子类的构造函数必须执行一次 super 函数,super 虽然代表了父类的构造函数,但是返回的是子类的实例。
|
|
第二种情况,super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。ES6 规定,在子类普通方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类实例。由于 this 指向子类实例,所以如果通过 super 对某个属性赋值,这时 super 就是 this,赋值的属性会变成子类实例的属性。在子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例。
|
|
大多数浏览器的 ES5 实现之中,每一个对象都有 __proto__ 属性,指向对应的构造函数的 prototype 属性。Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链:
- 子类的
__proto__属性,表示构造函数的继承,总是指向父类 - 子类
prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性
这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__ 属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype 属性)是父类的原型对象(prototype 属性)的实例。
|
|
extends 关键字后面可以跟多种类型的值,只要是一个有 prototype 属性的函数,就能被继承.由于函数都有 prototype 属性(除了 Function.prototype 函数),因此可以继承任意函数,还可以用来继承原生的构造函数。
|
|
子类实例的 __proto__ 属性的 __proto__ 属性,指向父类实例的 __proto__ 属性。也就是说,子类的原型的原型,是父类的原型。
|
|
以前,这些原生构造函数是无法继承的,比如,不能自己定义一个 Array 的子类,因为这个类的行为与 Array 完全不一致,子类无法获得原生构造函数的内部属性,通过 Array.apply() 或者分配给原型对象都不行。原生构造函数会忽略 apply 方法传入的 this,也就是说,原生构造函数的 this 无法绑定,导致拿不到内部属性。ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象 this,然后再用子类的构造函数修饰 this,使得父类的所有行为都可以继承,这是 ES5 无法做到的。
注意,继承 Object 的子类,有一个行为差异。下面代码中,NewObj 继承了 Object,但是无法通过 super 方法向父类 Object 传参。这是因为 ES6 改变了 Object 构造函数的行为,一旦发现 Object 方法不是通过 new Object() 这种形式调用,ES6 规定 Object 构造函数会忽略参数。