前情提要:
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。