类的初识
-
class是ES6中新的基础性语法糖,表面上可以支持正式的面向对象编程,但实际背后仍使用的是原型和构造函数的概念
-
类的首字母大写,区别其创造的实例:通过class Student{}创建实例student
-
类实质上就是一个函数。类自身指向的就是构造函数。所以可以认为ES6中的类其实就是构造函数的另外一种写法
class Person {}
console.log(typeof Person) // function
console.log(Person === Person.prototype.constructor) // true
复制代码
类的构造函数
constructor关键字
- constructor关键字告诉解释器,使用new操作符创建类实例时,应该调用该函数。即使类中无constructor,也会自动生成该构造函数
- 类的属性的定义位置是有区别的
- constructor中定义的属性可以称为实例属性(即定义在this对象上)。 每一个实例都对应一个唯一的成员对象,这意味着实例属性不会在原型上共享
- constructor外声明的属性,包括函数都是定义在原型上的,可以称为原型属性或者原型方法(即定义在class上,可以在实例中的__proto__中看到)
class Person {
constructor(name,age){
this.name = name
this.age = age
this.read = function read(){
console.log(`阅读`);
}
}
speak(){
console.log(`我叫${this.name}`);
}
}
let person = new Person('Lee',22)
console.log(person.hasOwnProperty("speak")) // false
console.log(person.hasOwnProperty("read")) // true
console.log(person.__proto__.read) // undefined
console.log(person)
复制代码
构造函数
- 使用new调用Person的constructor后发生了什么?
内存中创建一个新对象 ——> person.__ proto __ = Person.prototype ——> 构造函数内部this指向新对象,然后执行构造函数的代码,给新对象添加属性 ——> 返回新对象
- 类的__ proto __ 属性 (上述过程中的第二步与此有关)
- 实际上,类的所有实例共享一个原型对象,它们的原型都是Person.prototype,所以proto属性是相等的
class Person {
constructor(name,age){
this.name = name
this.age = age
this.read = function read(){
console.log(`阅读`);
}
}
speak(){
console.log(`我叫${this.name}`);
}
}
let person = new Person('Lee',22)
// person.__proto__就是Person.prototype
// 里面有constructor: class Person和speak: ƒ speak()
console.log(person.__proto__)
// Person {name: "LeeCopy", age: 23, read: ƒ}
let person2 = new person.__proto__.constructor('LeeCopy',23)
console.log(person2)
// true
console.log(person.__proto__ === person2.__proto__)
复制代码
涉及到原型链的知识,如下图所示(借用红宝书)
- 使用实例的proto属性改写原型,会改变Class的原始定义,影响到所有实例
class Person {
constructor(name,age){
this.name = name
this.age = age
this.read = function read(){
console.log(`阅读`);
}
}
speak(){
console.log(`我叫${this.name}`);
}
}
let person = new Person('Lee',22)
let person2 = new Person('LeeCopy',23)
// 不能用箭头函数,因为这样的话this指向的是window
person.__proto__.sayAge = function () {console.log(this.age)}
person.__proto__.speak = null
person2.sayAge() // 23
person2.speak() // Uncaught TypeError: person1.speak is not a function
复制代码
类的继承
类继承虽然使用的是新语法,但是背后仍使用的是原型链。 使用extends关键字就可以继承任何拥有constructor和prototype的对象,这意味着不仅可以继承类,也可以继承构造函数
类继承的语法
- 子类必须在constructor方法中调用super方法之后,才能使用this关键字
这是因为子类没有自己的this对象,而是继承父类的this对象。如果不调用super方法,子类就得不到this对象。由此,子类在constructor函数中需要先添加super(),再定义自己的属性
// 报错:Must call super constructor in derived class before accessing
// 没有super()
class Son extends Father {
constructor(sonName, sonAge) {
this.sonName = sonName;
this.sonAge = sonAge
}
}
// 报错: Must call super constructor in derived class before accessing
// this比super()早,此时子类还没有自己的this对象
class Son extends Father{
constructor(sonName,sonAge,school) {
this.school = school
super(sonName,sonAge);
}
}
复制代码
在这一点上ES5的继承与ES6正好相反,ES5先创建自己的this对象然后再将父类的属性方法添加到自己的this当中
- 如果子类没有显式地定义constructor,代码将被默认添加。换言之,如果constructor函数中只有super的话,该constructor函数可以省略
// 如果不想给子类添加新属性,下面两个写法效果一样
class Son extends Father{}
class Son extends Father{
constructor(sonName,sonAge) {
super(sonName,sonAge);
}
}
复制代码
继承中的就近原则(优先使用子类方法)
class Father {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayMe() {
console.log(`我是老爸,我叫${this.name},年龄${this.age}`)
}
}
class Son extends Father{
constructor(sonName,sonAge,school) {
super(sonName,sonAge);
this.school = school
}
sayMe() {
console.log(`我覆盖了老爸的方法,我叫${this.name},年龄${this.age}`)
super.sayMe()
}
}
let son = new Son('Lee的son', 23)
// 我覆盖了老爸的方法,我叫Lee的son,年龄23
// 我是老爸,我叫Lee的son,年龄23
son.sayMe()
复制代码
类继承的本质
- ES6的class继承
我们先来看一下用ES6的class实现继承后的情况
class Father {
constructor(name, age) {
this.name = name;
this.age = age;
}
fatherSay() {
console.log(`father`)
}
}
class Son extends Father{
constructor(sonName,sonAge,school) {
super(sonName,sonAge);
this.school = school
}
sonSay() {
console.log(`son`)
}
}
let son1 = new Son('Lee的son', 23, '南邮')
console.log(son1)
// son1.__proto__指向Father实例,相当于Son.prototype = new Father()
// 但是son1.__proto__的constructor指回了Son
console.log(son1.__proto__)
复制代码
我们可以很清楚地看到,ES6的class实现继承依旧是使用的是ES5原型链实现继承,细节上略有不同,后文会补充不同之处
- ES5构造函数实现继承
我们再来看一下ES5实现继承的方式
function Father(name,age){
this.name = name;
this.age = age;
this.fatherSay = function (){
console.log(`father`)
}
}
function Son(sonName,sonAge,school) {
Father.call(this,sonName,sonAge)
this.school = school
this.sonSay = function (){
console.log(`son`)
}
}
Son.prototype = Object.create(Father.prototype)
Son.prototype.constructor = Son
let son1 = new Son('Lee的son', 23, '南邮')
console.log(son1)
console.log(son1.__proto__)
console.log(son1.__proto__.constructor) // 指回构造函数
复制代码
可以看到类继承和构造函数继承有异曲同工之处
不同之处红线部分已标出 —— 构造函数继承中的函数都为实例属性或实例函数,本来实例属性不会在原型上共享的,但是在继承中,父类实例属性摇身一变变成了原型属性,以此给子类复用,变成了子类的实例属性
- 补充: 继承中的原型链
原型链的基本思想就是通过原型继承多个引用类型的属性和方法,子的原型是父实例,意味着子原型本身有一个内部指针指向另一个原型,相应的父原型也有一个指针指向另一个构造函数,这样实例和原型之间就构造了一条原型链。
类继承的应用
- 继承内置引用类型,扩展内置类型
// randomArr可以创建一个数组,该数组内置了洗牌算法
class randomArr extends Array {
shuffle() {
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 2));
[this[i], this[j]] = [this[j], this[i]];
}
}
}
let arr = new randomArr(1, 2, 3, 4, 5)
console.log(arr) // [1, 2, 3, 4, 5]
arr.shuffle()
console.log(arr) // [5, 4, 2, 3, 1]
console.log(arr instanceof randomArr) // true
console.log(arr instanceof Array) // true
复制代码
类的静态方法
静态方法:不需要实例化类,即可直接通过该类来调用的方法,在原型方法前加上static关键字即成静态方法
特点
- 可直接通过该类来调用的方法,且不会被实例继承
class Father {
constructor(name, age) {
this.name = name;
this.age = age;
}
static fatherSay() {
console.log(`father`)
}
}
Father.fatherSay() // father
let father = new Father('Lee', 23)
// Uncaught TypeError: father.fatherSay is not a function
father.fatherSay()
复制代码
2. 静态方法只能被 静态方法/类 调用
class Father {
constructor(name, age) {
this.name = name;
this.age = age;
}
static fatherSay() {
console.log(`father`)
}
static useStatic () {
this.fatherSay()
}
failUseStatic (){
this.fatherSay()
}
}
Father.useStatic() // father
let father = new Father('Lee', 23)
// Uncaught TypeError: this.fatherSay is not a function
father.failUseStatic()
复制代码
3. 父类的静态方法可以被子类继承,子类使用该静态方法和父类一样。 类的静态方法调用父类的静态方法,需要从super对象上调用
class Father {
static fatherSay() {
console.log(`father`)
}
}
class Son extends Father {
static sonSay() {
super.fatherSay()
}
}
Son.sonSay() // father
复制代码
应用
- 静态方法非常适合作为实例工厂
class Person {
constructor(age) {
this.age = age
}
static create () {
return new Person(Math.floor(Math.random()*100))
}
}
person1 = Person.create()
复制代码