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
构造函数会忽略参数。