ES6学习笔记(十七)类class

类的使用


类是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学习笔记目录(持续更新中)

 

ES6学习笔记(一)let和const

ES6学习笔记(二)参数的默认值和rest参数

ES6学习笔记(三)箭头函数简化数组函数的使用

ES6学习笔记(四)解构赋值

ES6学习笔记(五)Set结构和Map结构

ES6学习笔记(六)Iterator接口

ES6学习笔记(七)数组的扩展

ES6学习笔记(八)字符串的扩展

ES6学习笔记(九)数值的扩展

ES6学习笔记(十)对象的扩展

ES6学习笔记(十一)Symbol

ES6学习笔记(十二)Proxy代理

ES6学习笔记(十三)Reflect对象

ES6学习笔记(十四)Promise对象

ES6学习笔记(十五)Generator函数

ES6学习笔记(十六)asnyc函数

ES6学习笔记(十七)类class

ES6学习笔记(十八)尾调用优化

ES6学习笔记(十九)正则的扩展

ES6学习笔记(二十)ES Module的语法

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值