深入了解class类并使用

 

前情提要:

JavaScript 语言中,在使用类之前,生成实例对象的传统方法是通过使用构造函数。



一、构造函数:

定义:通过  new 函数名  来实例化对象的函数叫构造函数。

主要功能:为初始化对象,特点是和new 一起使用。new就是在创建对象,从无到有,构造函数就是在为初始化的对象添加属性和方法。

注意:任何的函数都可以作为构造函数存在,构造函数定义时首字母大写(规范)。

对new的理解:new 申请内存, 创建对象,当调用new时,后台会隐式执行new Object()创建对象。所以,通过new创建的字符串、数字是引用类型,而是非值类型。

1、原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。

大致有:

    Boolean()

    Number()

    String()

    Array()

    Date()

    Function()

    RegExp()

    Error()

    Object()

在之前,这些原生构造函数是无法继承的,比如,不能自己定义一个Array的子类。
例子:
function MyArray() {
  Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
});
var colors = new MyArray();
colors[0] = "red";
colors.length  // 0


colors.length = 0;
colors[0]  // "red"
之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。

这就说明了原生构造函数的this无法绑定,导致拿不到内部属性。

2、执行一个构造函数:

function A(name,age){   this.name = name;   this.age = age; }
A.prototype.info = function(){
  return "姓名"+ "" + this.name + "年龄" + this.age 
}
let a = new A("张三",22)//实例化a
//打印 a结果 
A{
  name:"张三",
  age:22
}
//打印 a.info() 结果为 "姓名张三年龄22"

二、class 类

由来:因为上面构造函数的写法跟传统的面向对象语言差异很大,给很多程序员造成很多困惑,所以ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。

通过class关键字,可以定义类。

1、class类基本语法的使用

class A{
  constructor(){
    //成员属性
    this.name = name
    this.age = age
  }
  //静态方法 如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。A.nihao()
  static nihao(){
    console.log("你好")
  }
  //成员方法
  info(){
    return "姓名"+ "" + this.name + "年龄" + this.age 
  }
}
与上面的构造函数相比之言,新的class写法让对象原型的写法更加清晰、更像面向对象编程的语法。
注意:定义info()方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了,方法与方法之间不需要逗号分隔,加了会报错。


添加静态属性:静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。
**老式写法:**
class A{}
A.props=1 //A.props= 1   props就是A的静态属性 


**新式写法:**
class A{
   static props = 1
}
新写法是显式声明(declarative),而不是赋值处理,语义更好。


**私有方法和私有属性:**
私有方法两种写法:    
function bar(name){
    return this.name = name
}
class A{


    foo(name){


        bar.call(this,name)
    }
}
这样写的原因是类内部的所有方法都是对外可见的。foo是公开方法,内部调用了bar.call(this, baz)。这使得bar()实际上成为了当前类的私有方法。


**还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。**
const bar = Symbol('bar')
const  name = Symbol('name')
class A{
    //公有方法
    foo(){
    this[bra](name)
  }
  //私有方法
    [bar](name){
        return this[name] = name
  }
}


**私有属性**
第一种方法是在属性名之前,使用#表示。
class A{
  #count = 0
}
注意:#count就是私有属性,只能在类的内部使用(this.#count)。如果在类的外部使用,就会报错。


这种写法不仅可以写私有属性,还可以用来写私有方法。
私有属性也可以设置 getter 和 setter 方法。


class Foo { 
    #a; 
    #b;
    #xVal = 0; constructor(a, b) { this.#a = a; this.#b = b; } #sum() { return this.#a + this.#b; } printSum() { console.log(this.#sum()); }
    get #x() { return #xValue; }
    set #x(value) {
        this.#xValue = value;
    }
}
私有属性不限于从this引用,只要是在类的内部,实例也可以引用私有属性。
私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法。
```
### 2、深入介绍class类
```
<1>
ES6 的类,完全可以看作构造函数的另一种写法。
class A{}  typeof A //function  A === A.prototype.constructor // ture 可以看出,类的数据类型就是函数,类本身就指向构造函数。
使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
<2>
class A{
  constructor(){}
  info(){}
  toString(){}
  toVal(){}
}
等同于
A.prototype={
    info(){},
    toString(){},
    toVal(){},
};
构造函数的prototype属性,在类里面也存在,类的所有方法都定义在类的prototype属性上面.
因此,在类的实例上面调用方法,其实就是调用原型上的方法。
<3>
Object.assign()  方法可以很方便地一次向类添加多个方法。如下:
Object.assign(A.prototype,{
    toString(){},
    toVal(){},
})
<4>
类的内部所有定义的方法,都是不可枚举的,如下:
class A{ constructor(x, y) { // ... } toString() { // ... } } Object.keys(A.prototype) // []
<5>
类内部可以忽略不写constructor,因为JavaScript 引擎会自动为它添加一个空的constructor()方法。如下:
class A{} === class A{constructor(){}} //true
<6>
constructor()方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
<7>
类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。

 三、class继承

1、extends 关键字

class 通过extends关键字实现继承,如下代码:
 
class A{
    constructor(){
        this.p = 1
    }
    static hello(){
        console.log("Hello World")
    }
    p(){
        return 2
    }
    getP(){
        console.log(this.p);
    }
}
A.prototype.num = 2
class B extends A{
    constructor(){
        super() //关键字,super作为函数调用时,代表父类的构造函数
        this.p = 2
        super.p = 3
        console.log(super.num) //2
        console.log(super.p()) //2


        console.log(super.p) //undefined
        console.log(this.p) //3
        //super.p赋值为3,这时等同于对this.p赋值为3。而当读取super.p的时候,读的是A.prototype.p,所以返回undefined。
    }


    get n(){
        return super.p
    }
    m(){
        super.getP()
        //super()写在这里报错
    }
} //B类继承了A类的所有属性和方法


let b = new B() //实列化
B.hello() //Hello World //这就反映出,父类的静态方法,也会被子类继承。
b.n //undefined
b.m() //2 这说明实际上执行的是super.getP.call(this)。
 
优点:这样的继承方式非常清晰和方便
实质:先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
注意:
    <1><span style="color:red;">构造函数如果没有调用super方法,就会导致新建实例时报错。</span>
    <2><span style="color:green;">在子类A的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。</span>


2、Object.getPrototypeof()
作用:从子类上获取父类.
例子:
Object.getPrototypeOf(B) === A 
//true
因此从上面的例子看出,可以使用这个方法判断,一个类是否继承了另一个类。
3、super 关键字
作用:可以当作函数使用,也可以当作对象使用 注意:<span style="color:red;">在这两种不同的情况下,它的用法也完全不相同。</span> 第一种情况 super作为函数调用时,代表父类的构造函数。 ES6 要求,子类的构造函数必须执行一次super函数 注意: <span style="color:red;">super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。</span> <span style="color:red;">作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。</span> 代码可以看上面对extends的介绍** 第二种情况 super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。 普通方法中 子类B当中的super.p(),就是将super当作一个对象使用。 super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。 因为由于super指向父类的原型对象,所以定义在父类的原型对象上,super是可以取到。 注意:<span style="color:red;">定义在父类实例上的方法或属性,是无法通过super调用的。</span> 在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。
代码可以看上面对extends的介绍 静态方法中 class Parent { static myMethod(msg) { console.log('static', msg); } myMethod(msg) { console.log('instance', msg); } } class Child extends Parent { static myMethod(msg) { super.myMethod(msg); } myMethod(msg) { super.myMethod(msg); } } Child.myMethod(1); // static 1 var child = new Child(); child.myMethod(2); // instance 2 super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。 在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。 注意:<span style="color:red;">我们使用super时,一定要表明他的使用类型是作为函数使用还是作为对象使用,不能直接打印super会报错</span>  最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。 4、类的prototype属性和__proto__属性 Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。 接下来上代码解释: class A { } class B extends A { } B.__proto__ === A // true B.prototype.__proto__ === A.prototype // true //由此看出,子类的__proto__属性,表示构造函数的继承,总是指向父类; //子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。 // B 的实例继承 A 的实例 Object.setPrototypeOf(B.prototype, A.prototype); // 等同于 B.prototype.__proto__ = A.prototype; // B 继承 A 的静态属性 Object.setPrototypeOf(B, A); // 等同于 B.__proto__ = A; const b = new B(); 这两条继承链,可以这样理解:**作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。** 实例的__proto__属性 子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。 const a1 = new A() const a2 = new B() a2.__proto__.__proto__ = a1.__proto__ //true 因为B继承了A,所以说B的原型的原型是A的原型 因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为。 a2.__proto__.__proto__.name = function(){ console.log("张三") } a1.name()//"张三" 上面代码在B的实例a2上向A类添加方法,结果影响到了A的实例a1。

  

原文地址:https://www.cnblogs.com/LcxWeb/p/14203630.html