类的使用
类是ES6为了更接近传统语言的写法而加入的新的概念,作为对象的模板,通过class关键字,可以定义类。要注意的是,类其实可以说只是一个语法糖,因为它能实现的都能在ES5中实现,它本身的数据结构是函数。它虽然叫类,但它实际上并非是类机制,而是委托机制,其继承还是基于[[Prototype]]的。
class c{}
typeof c
// "function"
类的构建
类在使用的时候,使用new运算符构建,跟构造函数的使用一样。
class C{
constructor(){
console.log('class');
}
}
var c=new C()
// class
要注意的是,类一定要有constructor方法,如果没有自己定义该方法,则会默认添加一个空的constructor方法。而子类如果没有构造方法,则会默认调用父类的构造方法并传入所有参数。
class c{}
//等同于
class c{
constructor(){}
}
class child extends parent{}
//等同于 关于super和extends的用法在下文提及
class child extends parent{
constructor(...args){
super(...args);
}
}
Class表达式
与函数一样,类也可以使用表达式的形式来定义。
var myclass=class C{
constructor(){}
}
要注意的是,上面代码声明的类,类名是myclass而非C,C的作用在于类内部的调用。若类的内部没有调用自身的话,可以省略该名称。
var myclass=class{
constructor(){}
}
而通过Class表达式,就可以写出立即执行的类的实例。
var person=new class{
constructor(name){
this.name=name;
}
getName(){
return this.name;
}
}('jack')
person.getName()
// "jack"
上面的代码中类在声明后立即执行赋给了变量person,所以person的name属性即刻就被赋了值。
类的方法
类的方法都在类的prototype属性上,且类上的方法都是不可枚举的,这与ES5中的行为是不一致的
class C{
toString(){
//...
}
}
Object.keys(C.prototype)
// []
Object.getOwnPropertyNames(C)
// ["length", "prototype", "name"]
Object.getOwnPropertyNames(C.prototype)
// ["constructor", "toString"]
上面代码中,在类C中定义的toString方法没法用keys方法获得,就是因为其不能枚举。而定义的toString方法在类上找不到,但在其prototype属性上就找得到该方法。
一般方法
取值函数(getter)和存值函数(setter)
在类的内部也可以使用get和set关键字来对某个属性设置存值函数和取值函数,这样也就可以对存值和取值设置拦截。
class C {
constructor(x) {
this.x = x;
}
get a() {
return 'get';
}
set a(val) {
console.log('set');
}
}
var c1 = new C(1);
c1.a = 1
console.log(c1.a);
上面代码中,通过在取值函数和存值函数中进行拦截,改变了原来的方法。
和普通对象一样,类的取值函数和存值函数是定义在其属性的描述对象(descriptor对象)上的。
class C {
constructor(x) {
this.x = x;
}
get a() {
return 'get';
}
set a(val) {
console.log('set');
}
}
var c1 = new C(1);
var d = Object.getOwnPropertyDescriptor(C.prototype, 'a');
"get" in d;
// true
"set" in d;
// true
静态方法
若在方法前面添加关键字static,则该方法变为静态方法,只能通过类来调用,没法被实例继承。
class myclass{
constructor(){}
static s(){
console.log('static');
}
}
var c=new myclass()
c.s()
// Uncaught TypeError: c.s is not a function
myclass.s()
// static
上面代码可以看到,使用myclass的实例调用静态方法时报错,而使用类直接调用静态方法时则没有问题。
静态方法既然不能使用实例调用,那this肯定也不是指向实例了,静态方法中的this指向类本身。
class myclass{
constructor(){}
static out(){
this.greet();
}
static greet(){
console.log('hello');
}
greet(){
console.log('world');
}
}
myclass.out()
// hello
上面的代码可以看到,out方法调用了myclass自己的greet方法,还有一点,类中的静态方法可以和一般方法有相同的名字,上面的代码就可以看出来了。
父类的静态方法,子类可以继承,使用super就可以调用父类的静态方法。
class parent{
static greet(){
return 'hello';
}
}
class child extends parent{
static greet(){
return super.greet()+' world';
}
}
child.greet()
// "hello world"
私有方法
私有方法只能在类的内部访问,但ES6并不提供这类方法,所以要自己变通实现。
常见的操作是在正常的函数名前面添加下划线来区分私有方法,但这种私有方法实际上还是能被外部访问到,并不是真正的私有方法。
class Widget {
// 公有方法
foo (baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf = baz;
}
// ...
}
也可以通过将方法移到整个类外,类内部都是可以被外部访问到的,但如果将方法移到外部,再从内部调用外部的方法,就不能在外部访问该方法了。
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
上述代码中,foo调用了外部的bar函数,这使得bar函数实际上变为该类的私有方法了。
当然,也可以利用Symbol值的唯一性,将方法名改为Symbol值。
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
因为Symbol值的唯一性,所以一般方法是没法访问它们的,这样就达到了私有方法的效果。(事实上Reflect.ownKeys()仍然可以访问到)
类的属性
实例属性
类的实例属性可以定义在constructor方法里面,也可以定义在类的顶层。
// 第一种写法
class myclass{
constructor(){
this.count=0;
}
}
// 第二种写法
class myclass{
count=0;
}
第二种写法要使用babel转码后才能使用,在浏览器中直接使用会报错。
静态属性
静态方法指类自身的方法,那么静态属性就指类自身的属性了。即Class.propName,而非定义在实例上的属性。
有两个方法用于定义类的静态属性,一个是声明类后用点运算符直接定义类的属性,二是在类的顶层直接定义属性,在定义属性前面增加static关键字。
// 第一种写法
class Foo {}
Foo.prop = 1;
// 第二种写法
class Foo {
static prop = 1;
}
同属性的第二种写法一样,静态属性当前的第二种写法还不能直接在浏览器中使用,需要使用babel转码后才能使用。
new.target属性
该属性用在构造函数中,若构造函数不是通过new来调用的话,该属性就会返回undefined,若是的话,则返回该构造函数。(在类中即返回该类)因此该属性可以用来确定构造函数是不是由new运算符调用的。
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}}
var obj = new Rectangle(3, 4); // 输出 true
要注意的是,子类继承父类时,new.target属性会返回子类。(归根结底是因为new,target指向new实际上直接调用的构造器)
class Rectangle {
constructor(length, width) {
console.log(new.target === Square);
// ...
}}
class Square extends Rectangle {
constructor(length) {
super(length, length);
}}
var obj = new Square(3);
// true
上面代码中,Square继承了Rectangle,最后new.target属性返回了子类。
类的实例
生成类的实例的方法,就是使用new命令,如果忘记加new命令,则会报错。
var c=C()
// Uncaught TypeError: Class constructor C cannot be invoked without 'new'
与ES5一样,实例的属性除非显式定义在其本身(即this对象),否则都是在其原型上(即class)
class C {
constructor(x) {
this.x = x;
}
toString() {}
}
var c = new C(2);
console.log(c.hasOwnProperty('x'));
// true
console.log(c.hasOwnProperty('toString'));
// false
上面代码中,x是显式定义的,所以它在其实例本身,而toString方法是在原型上才能得到。从这里也能看出,所有实例都会返回相同的toString方法,这是因为所有实例共享一个原型。
class C {
constructor(x) {
this.x = x;
}
toString() {}
}
var c1 = new C(1);
var c2 = new C(2);
c1._proto_ === c2._proto_;
//true
类的继承
extends继承
类的“继承”是通过extends关键字来实现的(这里的继承实际上是在两个函数原型之间建立[[Prototype]]委托链接)
class parent {}// 父类
class child extends parent {}// 子类
上面代码就是一个最简单的继承。因为没有在类里面添加任何代码,所以这两个类此时是相同的。
在extends后面的不一定是类,只要是一个带有prototype属性的函数就可以了,而除了Function.prototype函数,其他函数都有prototype属性,即其他函数可以被extends关键字继承。
super关键字
若要在子类中调用父类的方法和属性,则需要用到super关键字了,而super关键字有两种用法,可以当函数使用,也可以当对象使用。通过super,我们可以实现相对多态。
当函数使用
当super关键字当函数使用时,有三个重要的点。
1.在子类中需要使用super继承父类的构造方法
若子类没有省略构造方法,又没有使用super调用父类的构造方法,那么创建类的实例时会报错。
class parent {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class child extends parent {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
var c=new child(1,2)
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor at new child
上面代码中,正是因为在子类的构造方法中没有调用父类的构造方法,所以在创建实例时报错了。
class parent {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class child extends parent {
constructor(x, y) {
super(x, y)
}
}
var c = new child(1, 2);
c.x // 1
c.y // 2
若是省略了构造方法,又继承了父类,则默认新建的构造方法会默认调用父类的构造方法。
class child extends parent {
}
// 等同
class child extends parent {
constructor(...args) {
super(...args)
}
}
2.不能在super前使用this
如果在使用super前调用this对象,则会报错。
class parent {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class child extends parent {
constructor(x, y, a) {
this.a = a;
super(x, y);
}
}
var c = new child(1, 2, 3);
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor at new child
上面代码中,子类在使用super调用父类的构造函数之前使用了this对象,所以报错了。
class parent {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class child extends parent {
constructor(x, y, a) {
super(x, y);
this.a = a;
}
}
var c = new child(1, 2, 3);
c.x // 1
c.y // 2
c.a // 3
3.super()只能用在子类的构造函数中
super()只能用在子类的构造函数中以调用父类的构造函数,若用在其他方法中,则会报错。
class parent {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class child extends parent {
constructor(x, y) {
super(x, y);
}
toString() {
super(1, 2);
}
}
var c = new child(1, 2);
c.toString();
// Uncaught SyntaxError: 'super' keyword unexpected here
上面的代码在普通方法toString中使用super要调用父类的构造函数,但是报错了。因为不允许在子类的普通方法中将super当函数使用。
当对象使用
在普通方法中指向父类的原型对象,其this指向当前子类的实例,在静态方法中指向父类,其this指向当前子类。
普通方法中
class parent {
print() {
console.log(this.name);
}
}
class child extends parent {
judge() {
console.log(super.print === parent.prototype.print);
}
print() {
super.print();
}
}
var c = new child();
c.judge();
// true
c.name = 'jack';
c.print();
// jack
如上代码,普通方法中使用super获取方法print,正是其父类原型对象的方法print。而print方法中的this,指向的是实例,所以在用this找name时才回找到实例中的’jack’。
静态方法中
class parent {}
class child extends parent {
static print() {
console.log("super: " + super.name);
console.log("this: " + this.name);
}
}
child.print();
// super: parent
// this: child
上面代码中,在子类的静态方法中使用了super和this,因为方法的name属性默认指向class关键字后的名字,从上面打印出的情况可以看出,super指向了父类parent,this指向了子类child。
要注意的是,使用super时必须显示指定是作为函数还是作为对象,不然会报错。不管是什么方法中都会报错。
//构造函数中
class parent {}
class child extends parent {
constructor(){
super
}
}
// Uncaught SyntaxError: 'super' keyword unexpected here
//普通方法中
class parent {}
class child extends parent {
constructor(){
super();
}
toString(){
super
}
}
// Uncaught SyntaxError: 'super' keyword unexpected here
//静态方法中
class parent {}
class child extends parent {
constructor(){
super();
}
static toString(){
super
}
}
// Uncaught SyntaxError: 'super' keyword unexpected here
与this绑定的区别
我们知道,this绑定的对象是由它被调用的位置来决定的,super虽然和this很类似,但它绑定的对象是在声明的时候决定的。并非根据当前调用位置确定对象后调用上一层。
class parent {
fn() {
console.log('p fn');
}
}
class child extends parent {
fn() {
super.fn();
}
}
var c1 = new child();
c1.fn();//p fn
var child2 = {
fn: child.prototype.fn
}
var parent2 = {
fn() {
console.log('p2 fn');
}
}
Object.setPrototypeOf(child2, parent2);
console.log(Object.getPrototypeOf(child2) === parent2);//true
child2.fn();//p fn
上面代码中,child继承了parent,其fn方法打印出“p fn”,是因为其fn方法中的super指向了parent,所以调用了parent中的fn方法,而这里指向parent正是在声明时绑定的。可以看到,在后面child2的原型是parent2,即child2继承了parent2,但是其调用了child的fn方法后,还是使用了parent的fn方法。可以看到此时的super还是指向parent而非parent2。总的来说,super的绑定发生在创建的时候,在[[HomeObject]].[[Prototype]]上,而[[HomeObject]]会在创建时静态绑定,所以与调用的环境无关。
Object.getPrototypeOf()
可以使用该方法从子类获取父类。
class parent {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class child extends parent {
constructor(x, y) {
super(x, y);
}
}
Object.getPrototypeOf(child) === parent;
// true
上面代码中,child继承parent,使用Object.getPrototypeOf方法获取child子类时返回的正是parent,这个方法可以用来确定继承的父类。
类的继承链属性
子类的prototype属性和__proto__属性
对于子类的这两个属性,可以这么理解,子类的prototype属性是父类原型对象的实例,子类的__proto__属性是父类
class parent {}
class child extends parent {}
console.log(child.prototype.__proto__ === parent.prototype);
// true
console.log(child.__proto__ === parent);
// true
如上代码中,子类的prototype属性指向了父类原型对象的实例,所以其__proto__属性就恰好指向了父类的原型对象,而其__proto__恰好就是父类parent
实例的__proto__属性
子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性
class parent {}
class child extends parent {}
var c = new child();
var p = new parent();
console.log(c.__proto__.__proto__ === p.__proto__);
//true
类的应用
原生构造函数的继承
在ES5中,原生构造函数没法被继承,因为ES5中是先构建“子类”的this对象,再为其添加“父类”的属性方法。但是在ES6中,是先构建父类实例对象this,然后再用子类的构造函数来修饰该对象。这种继承可以直接使用extends关键字来实现。
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
上面代码中类MyArray继承了Array类,所以可以使用MyArray生成数组实例。
在继承Object类时有一个行为差异,那就是ES6改变了Object构造函数的行为,如果Object实例不是由new Object构造的,那其中的参数会被忽略掉。
class myObj extends Object {};
var obj = new myObj({ attr: 1 })
console.log(obj.attr);
如上面代码,在构造函数中设置的属性attr,在获取时却为undefined,这就是因为该参数被忽略掉了。
类的注意点
1.严格模式
类中的代码默认都是严格模式,不需要使用’use strict’来指定执行模式。
class C {
constructor(x) {
a=1;
this.x = x;
}
}
var c=new C(1)
// a is not defined
上面代码可以看到,类的内部默认为严格模式,因为严格模式下不允许“a=1”这样的写法,所以报错了。
2.不存在变量提升
这与函数的声明不一样,在使用函数时,函数使用可以写在函数声明之前,只要函数声明在同一个作用域即可,但是类的声明必须写在类的使用之前,否则会报错。
var c1 = new C(1);
class C {
constructor(x) {
a=1;
this.x = x;
}
}
// Uncaught ReferenceError: C is not defined
如上代码,在类C声明之前new了一个C的实例,结果报错了,报错原因是C没被声明,说明类并不存在变量提升。
3.name属性
类的name属性即class关键字后面的名字。
class myclass{}
myclass.name
// "myclass"
4.使用Generator方法
在方法名前加*即表示为Generator方法
class myclass{
constructor(){}
*gen(){
yield 1;
yield 2;
}
}
var c=new myclass()
var g=c.gen()
for (let i of g)
console.log(i)
// 1
// 2
5.this的指向
this在类中默认指向类的实例
class myclass {
print() {
console.log(this.x);
}
}
var c = new myclass();
c.x = 1;
c.print();
// 1
上面代码中,print方法输出this.x,正是实例上的属性x。
6.不会创建同名的全局对象属性
使用function来创建一个函数时,会在当前作用域创建一个对象属性,而class不会
function fn(){};
window.fn;
// ƒ fn(){}
class c{};
window.c;
// undefined
参考自阮一峰的《ECMAScript6入门》
Kyle Simpson的《你不知道的JavaScript 下卷》
ES6学习笔记目录(持续更新中)