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的语法

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值