JavaScript 语言中,生成实例对象的传统方法是通过构造函数;
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。
基本上,ES6 的class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
注意:
- 类和模块的内部,默认就是严格模式,所以不需要使用
use strict
指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。 - ES6里面class没有提升功能,在ES5,用函数模拟可以,默认函数提升
- ES6里面this 比之前轻松多了,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
- 类必须使用
new
调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new
也可以执行。
特点:
es5构造函数;
class Person{};
构造函数的prototype
属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype
属性上面。
let wjp = "toString";
class Person {
constructor() {
// ...
console.log("new 实例对象时自动执行")//=------构造方法(函数), 调用new,自动执行
}
[wjp]() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Person.prototype = {
constructor() {},
toString() {},
toValue() {},
};
类的数据类型就是函数,类本身就指向构造函数。
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
const point = new Point();
point.constructor = Point.prototype.constructor
与 ES5 一样,类的所有实例共享一个原型对象。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__ //true
静态方法:
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
取值函数(getter)和存值函数(setter)
与 ES5 一样,在“类”的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
实例属性; 静态属性的新写法
// 老写法
class Foo {
// ...
constructor(){
this._count = 0
}
}
Foo.prop = 1;
// 新写法
class Foo {
_count = 0;
static prop = 1;因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上static关键字。
}
继承
子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this
对象。
如果子类没有定义constructor
方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor
方法。
在子类的构造函数中,只有调用super
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super
方法才能调用父类实例。
最后,父类的静态方法,也会被子类继承
super 关键字
super
这个关键字,既可以当作函数使用,也可以当作对象使用。有两种情况。
第一种情况,super
作为函数调用时,代表父类的构造函数。 作为函数时,super()
只能用在子类的构造函数之中( constructor )
class A {
constructor() {
console.log(this);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
第二种情况,super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
由于super
指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super
调用的。如果属性定义在父类的原型对象上,super
就可以取到
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();//子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。
在子类普通方法中通过super
调用父类的方法时,方法内部的this
指向当前的子类实例
super.print() 实际上执行 super.print.call(this)
下面代码中,super.x
赋值为3
,这时等同于对this.x
赋值为3
。而当读取super.x
的时候,读的是A.prototype.x
,所以返回undefined
。
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
作为对象,用在静态方法之中,这时super
将指向父类,而不是父类的原型对象。
同样,在子类的静态方法中通过super
调用父类的方法时,方法内部的this
指向当前的子类,而不是子类的实例。
原型链的概念
__proto__ 属性,它是对象所独有的,可以看到__proto__属性都是由一个对象指向一个对象,即指向它们的原型对象(也可以理解为父对象),那么这个属性的作用是什么呢?它的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(可以理解为父对象)里找,如果父对象也不存在这个属性,则继续往父对象的__proto__属性所指向的那个对象(可以理解为爷爷对象)里找,如果还没找到,则继续往上找…直到原型链顶端null(可以理解为原始人。。。),再往上找就相当于在null上取值,会报错(null为原型链的终点),由以上这种通过__proto__属性来连接对象直到null的一条链即为我们所谓的原型链。
其实我们平时调用的字符串方法、数组方法、对象方法、函数方法等都是靠__proto__继承而来的。