es6 class

1,基本语法

之前:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);
es6 class

ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用ES6的“类”改写,就是下面这样

//定义类
class Point{
    constructor(x,y){
        this.x = x;
        this.y = y;
    }
    toString(){
        return '(' + this.x + ', ' + this.y + ')';
    }
}
//上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。
//也就是说,ES5的构造函数Point,对应ES6的Point类的构造方法。

//Point类除了构造方法,还定义了一个toString方法。
//注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。
//另外,方法之间不需要逗号分隔,加了会报错

console.log(typeof Point);// function
console.log(Point === Point.prototype.constructor);//true
//上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

//使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致
var p = new Point(1,2);
console.log(p.toString());//(1,2)
构造函数的 prototype 属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的 prototype 属性上面。

class Point{
    constructor(){

    }
    toString(){

    }
    createNum(){

    }
}
//等同于
Point.prototype = {
    toString(){},
    createNum(){}
}
//由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。
//Object.assign方法可以很方便地一次向类添加多个方法。

class Point{
    constructor(){

    }
}
Object.assign(Point.prototype,{
    toString(){},
    createNum(){}
});
//prototype对象的constructor属性,直接指向“类”的本身,这与ES5的行为是一致的
console.log(Point.prototype.constructor === Point);
另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。

class Point{
    constructor(){

    }
    toString(){

    }
}
console.log(Object.keys(Point.prototype));//[]
console.log(Object.getOwnPropertyNames(Point.prototype));//["constructor", "toString"]

//上面代码中,toString方法是Point类内部定义的方法,它是不可枚举的。这一点与ES5的行为不一致
var Foo = function(x,y){

};//
Foo.prototype.toString = function(){
    //
};
console.log(Object.keys(Foo.prototype));//["toString"]
console.log(Object.getOwnPropertyNames(Foo.prototype));//["constructor", "toString"]
//上面代码采用ES5的写法,toString方法就是可枚举的

//类的属性名,可以采用表达式
let meName = 'getNum';
class Creat{
    constructor(len){

    }
    [meName](){
        console.log(11)
    }
}
//上面代码中,Square类的方法名getNum,是从表达式得到的
var create = new Creat();
create.getNum();//11

constructor方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加

//constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象
class Foo{
    constructor(){
        return Object.create(null);
    }
}
console.log(new Foo() instanceof Foo);//false
//上面代码中,constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
类的实例对象

//生成类的实例对象的写法,与ES5完全一样,也是使用new命令。
//如果忘记加上new,像函数那样调用Class,将会报错。
//报错
var point = Point();
//正确
var point = new Point();

//与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),
//否则都是定义在原型上(即定义在class上)
//定义类
class Foo{
    constructor(x,y){
        this.x = x;
        this.y = y;
    }
    toString(){
        return '(' + this.x + ', ' + this.y + ')';
    }
}
var f = new Foo(1,2);
console.log(f.toString());//(1,2)
console.log(f.hasOwnProperty('x'));//true
console.log(f.hasOwnProperty('y'));//true
console.log(f.hasOwnProperty('toString'));//false
console.log(f.__proto__.hasOwnProperty('toString'));//true
//x和y都是实例对象f自身的属性(因为定义在this变量上),
//所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Foo类上),
//所以hasOwnProperty方法返回false。这些都与ES5的行为保持一致。

//与ES5一样,类的所有实例共享一个原型对象
var f1 = new Foo(1,2);
var f2 = new Foo(2,3);
console.log(f1.__proto__ === f2.__proto__);//true
//p1和p2都是Point的实例,它们的原型都是Point.prototype,所以__proto__属性是相等的

//这也意味着,可以通过实例的__proto__属性为Class添加方法
f1.__proto__.conName = function(){
    return 'andy';
};
console.log(f1.conName());//andy
console.log(f2.conName());//andy
//由于f1的原型就是f2的原型,因此f2也可以掉用这个方法,以及以后实例出来的对象,也可以调用;
//这就意味着,使用实例的__proto__属性修改原型,必须谨慎使用,另,不推荐使用;
不存在变量提升

//Class不存在变量提升(hoist),这一点与ES5完全不同
new Foo();//Uncaught ReferenceError: Foo is not defined
class Foo{

}
//上面代码中,Foo类使用在前,定义在后,这样会报错,因为ES6不会把类的声明提升到代码头部。
//这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。

{
    let Foo = class {};
    class Bar extends Foo {
    }
}
//上面的代码不会报错,因为Bar继承Foo的时候,Foo已经有定义了。
//但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,
//而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。
class表达式

//与函数一样,类也可以使用表达式的形式定义
const CreatNum = class Num{
    getName(){
        return Num.name;
    }
};
//上面代码使用表达式定义了一个类。
//需要注意的是,这个类的名字是CreatNum而不是Num,Num只在Class的内部代码可用,指代当前类。

let init = new CreatNum();
console.log(init.getName());//Num
console.log(Num.name);//Uncaught ReferenceError: Num is not defined

//如果类的内部没用到的话,可以省略Num,也就是可以写成下面的形式
const CreatNums = class{
    //
};

//采用Class表达式,可以写出立即执行的Class
let person = new class{
    constructor(name){
        this.name = name;
    }
    sayName(){
        console.log(this.name);
    }
}('andy');
person.sayName();//andy
上面代码中, person 是一个立即执行的类的实例

私有方法

私有方法是常见需求,但 ES6 不提供,只能通过变通方法模拟实现

//一种做法是在命名上加以区别
class Wig{
    //公有方法
    foo(bar){
        this._baz(bar);
    }

    //私有方法
    _baz(bar){
        return this.sn = bar;
    }
}
//_baz方法前面的下划线,表示这是一个只限于内部使用的私有方法。
//但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法

//另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的
class Widget {
    foo (baz) {
        bar.call(this, baz);
    }

    // ...
}

function bar(baz) {
    return this.snaf = baz;
}
//上面代码中,foo是公有方法,内部调用了bar.call(this, baz)。这使得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;
    }
}
//上面代码中,bar和snaf都是Symbol值,导致第三方无法获取到它们,因此达到了私有方法和私有属性的效果
this的指向

//类的方法内部如果含有this,它默认指向类的实例。
//但是,必须非常小心,一旦单独使用该方法,很可能报错
class Logger{
    printName(name = 'there'){
        this.print(`hello ${name}`);
    }
    print(txt){
        console.log(txt);
    }
}
const logger = new Logger();
const {printName} = logger;
printName();//Uncaught TypeError: Cannot read property 'print' of undefined
//上面代码中,printName方法中的this,默认指向Logger类的实例。
//但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境,因为找不到print方法而导致报错。

//一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了
class Logger {
    constructor() {
        this.printName = this.printName.bind(this);
    }
    // ...
}

//另一种解决方法是使用箭头函数。
class Logger {
    constructor() {
        this.printName = (name = 'there') => {
            this.print(`Hello ${name}`);
        };
    }

    // ...
}

//还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this

严格模式

类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。

考虑到未来所有的代码,其实都是运行在模块之中,所以ES6实际上把整个语言升级到了严格模式。

name属性

由于本质上,ES6的类只是ES5的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性

class Point {}
console.log(Point.name);
//name属性总是返回紧跟在class关键字后面的类名。

class的继承

Class之间可以通过 extends 关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多
class ColorPoint extends Point{

}
//上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。
//但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类
class ColorP extends P{
    constructor(x,y,color){
        super(x,y);//调用父类的constructor(x,y)
        this.color = color;
    }
    toString(){
        return this.color + ' ' + super.toString();//调用父类的toString()
    }
}
//上面代码中,constructor方法和toString方法之中,都出现了super关键字,
//它在这里表示父类的构造函数,用来新建父类的this对象

//子类必须在constructor方法中调用super方法,否则新建实例时会报错。
//这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
class Point{

}
class ColorPoint extends Point{
    constructor(){

    }
}
let p = new ColorPoint();//编译报错
//上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。

//ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。
//ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

//如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。
//也就是说,不管有没有显式定义,任何一个子类都有constructor方法
constructor(...args){
    super(...args);
}

//另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。
//这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}

class ColorPoint extends Point {
    constructor(x, y, color) {
        this.color = color; // ReferenceError
        super(x, y);
        this.color = color; // 正确
    }
}
//上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的

类的prototype属性和__proto__属性
大多数浏览器的ES5实现之中,每一个对象都有 __proto__ 属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和 __proto__ 属性,因此同时存在两条继承链。

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性

class F{

}
class M extends F{

}
console.log(M.__proto__ === F);//true
console.log(M.prototype.__proto__ == F.prototype);//true


Object.getPrototypeOf()

//Object.getPrototypeOf方法可以用来从子类上获取父类
console.log(Object.getPrototypeOf(B) === A);//true
//因此,可以使用这个方法判断,一个类是否继承了另一个类


super关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同

//第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数
//class A {}
//
//class B extends A {
//    constructor() {
//        super();
//    }
//}
上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错
//
注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,
因此super()在这里相当于A.prototype.constructor.call(this)

class A {
    constructor() {
        console.log(new.target.name);
    }
}
class B extends A {
    constructor() {
        super();
    }
}
new A(); // A
new B(); // B
//上面代码中,new.target指向当前正在执行的函数。
//可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。
//也就是说,super()内部的this指向的是B

//作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错
class A {}

class B extends A {
    m() {
        super(); // 报错
    }
}
//上面代码中,super()用在B类的m方法之中,就会造成句法错误


//第二种情况,super作为对象时,指向父类的原型对象
class F{
    p(){
        return 1;
    }
}
class M extends F{
    constructor(){
        super();
        console.log(super.p());//1
    }
}
let m = new M();
//上面代码中,子类M当中的super.p(),就是将super当作一个对象使用。
//这时,super指向F.prototype,所以super.p()就相当于F.prototype.p()

//由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的
class X{
    constructor(){
        this.p = 1;
    }
}
class Y extends X{
    get m(){
        return super.p;
    }
}
let y = new Y();
console.log(y.m);//undefined
//p是父类X实例的属性,super.p就引用不到它

//如果属性定义在父类的原型对象上,super就可以取到
class A{

}
A.prototype.x = 1;
class B extends A{
    constructor(){
        super();
        console.log(super.x);//1
    }
}
let b = new B();
//上面代码中,属性x是定义在A.prototype上面的,所以super.x可以取到它的值

ES6 规定,通过 super 调用父类的方法时, super 会绑定子类的 this

class X{
    constructor(){
        this.x = 1;
    }
    print(){
        console.log(this.x);
    }
}
class Y extends X{
    constructor(){
        super();
        this.x = 2;
    }
    m(){
        super.print();
    }
}
let y = new Y();
y.m();//2
//上面代码中,super.print()虽然调用的是X.prototype.print(),
//但是X.prototype.print()会绑定子类Y的this,导致输出的是2,而不是1。
//也就是说,实际上执行的是super.print.call(this)

//由于绑定子类的this,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性
class A{
    constructor(){
        this.x = 1;
    }
}
class B extends A{
    constructor(){
        super();
        this.x = 2;
        super.x = 3;
        console.log(super.x);//undefined
        console.log(this.x);//3
    }
}
let b = new B();
//上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。
//而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。

注意,使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错


实例的__proto__属性

//子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。
//也就是说,子类的原型的原型,是父类的原型

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

//上面代码中,ColorPoint继承了Point,导致前者原型的原型是后者的原型

//因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为
p2.__proto__.__proto__.printName = function () {
    console.log('Ha');
};

p1.printName() // "Ha"
//上面代码在ColorPoint的实例p2上向Point类添加方法,结果影响到了Point的实例p1


原型构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript的原生构造函数大致有下面这些。

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()
ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。

class的取值函数(getter)和存值函数(setter)

与ES5一样,在Class内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为
class X{
    constructor(){

    }
    get prop(){
        return 'getter';
    }
    set prop(val){
        console.log('setter:'+ val);
    }
}
let inst = new X();
inst.prop = 123;// setter:123
console.log(inst.prop);//getter
//上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了

//存值函数和取值函数是设置在属性的descriptor对象上的

class的Generator方法
//如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数
class F{
    constructor(...args){
        this.args  =args;
    }
    *[Symbol.iterator](){
        for(let arg of this.args){
            yield arg;
        }
    }
}
for(let x of new F('hello','andy')){
    console.log(x);
}
//hello
//andy
//F类的Symbol.iterator方法前有一个星号,表示该方法是一个 Generator 函数。
//Symbol.iterator方法返回一个F类的默认遍历器,for...of循环会自动调用这个遍历器。

class的静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”
class F{
    static className(){
        return 'hello';
    }
}
console.log(F.className());//hello

var f  = new F();
//console.log(f.className());//Uncaught TypeError: f.className is not a function
//上面代码中,F类的classMethod方法前有static关键字,表明该方法是一个静态方法,
//可以直接在F类上调用(Foo.classMethod()),而不是在F类的实例上调用。
//如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法

//父类的静态方法,可以被子类继承
class M{
    static className(){
        return 'hello';
    }
}
class B extends M{

}
console.log(B.className());//hello

//静态方法也是可以从super对象上调用的
class A{
    static className(){
        return 'hello';
    }
}
class C extends A{
    static className(){
        return super.className() + ',andy'
    }
}
console.log(C.className());//hello,andy

class的静态属性和实例属性

//静态属性指的是Class本身的属性,即Class.propname,而不是定义在实例对象(this)上的属性
class F{

}
F.prop= 1;
console.log(F.prop);//1
//上面的写法为F类定义了一个静态属性prop

//目前,只有这种写法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性
//以下两种写法无效
class M{
    //1
    prop:1
    //2
    static prop:1
}
console.log(M.prop);//undefined

//ES7有一个静态属性的提案,目前Babel转码器支持
//这个提案对实例属性和静态属性,都规定了新的写法

//(1)类的实例属性
//类的实例属性可以用等式,写入类的定义之中

//(2)类的静态属性
//类的静态属性只要在上面的实例属性写法前面,加上static关键字就可以了。

new.target属性

new是从构造函数生成实例的命令。ES6为new命令引入了一个new.target属性,(在构造函数中)返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值