类的初识
-
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 后发生了什么?
只要 new 生成实例时,就会调用 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
实例属性和原型属性
- constructor中定义的属性可以称为实例属性(即定义在this上)
- constructor外声明的属性都是定义在原型上的,可以称为原型属性(即定义在class上)。
- hasOwnProperty()函数用于判断属性是否是实例属性。其结果是一个布尔值, true说明是实例属性,false说明不是实例属性。
- in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中
class Box {
constructor(num1,num2) {
this.num1=num1;
this.num2=num2;
}
sum(){
return this.num1+this.num2
}
}
const box = new Box(12,88)
console.log(box.sum()) //100
console.log(box.hasOwnProperty('num1')) //true
console.log(box.hasOwnProperty('sum')) // false
console.log('num1' in box) // true
console.log('sum' in box) //true
console.log('say' in box) //false
构造函数中的this指向问题
constructor中的this指向实例对象,类中非constructor中定义的方法中的this指向这个方法的调用者
class Person {
constructor(_name, _age) {
//this指向实例对象
this.name = _name
this.age = _age
this.btn = document.querySelector('button')
this.btn.onclick = this.sing //给btn元素的onclick事件属性赋值
}
sing() {
//sing方法里面的this指向的是btn,因为是这个btn按钮调用了该函数
console.log(this)
}
}
类的继承
类继承虽然使用的是新语法,但是背后仍使用的是原型链。 使用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当中
//子构造函数使用自己的this来调用父构造函数的constructor
function Son(sonName,sonAge,school) {
Father.call(this,sonName,sonAge)
this.school = school
this.sonSay = function (){
console.log(`son`)
}
}
- 如果子类没有显式地定义constructor,代码将被默认添加。换言之,如果constructor函数中只有super的话,该constructor函数可以省略
// 如果不想给子类添加新属性,下面两个写法效果一样
class Son extends Father{}
class Son extends Father{
constructor(sonName,sonAge) {
super(sonName,sonAge);
}
}
继承中的就近原则(优先使用子类方法)
首先要知道的是,super关键字既可以调用父类的构造函数,也可以调用父类的普通函数(甚至是静态函数)
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) // 指回构造函数
//寄生组合式继承相对组合继承的优点:
//1、只调用了父类构造函数一次,节约了性能。
//2、避免生成了不必要的属性。
//3、使用原型式继承保证了原型链上下文不变,子类的prototype只有子类通过prototype声明的属性和方法,父类的prototype只有父类通过prototype声明的属性和方法
可以看到类继承和构造函数继承有异曲同工之处
不同之处红线部分已标出 —— 构造函数继承中的函数都为实例属性或实例函数,本来实例属性不会在原型上共享的,但是在继承中,父类实例属性摇身一变变成了原型属性,以此给子类复用,变成了子类的实例属性
- 补充: 继承中的原型链 原型链的基本思想就是通过原型继承多个引用类型的属性和方法,子的原型是父实例,意味着子原型本身有一个内部指针指向另一个原型,相应的父原型也有一个指针指向另一个构造函数,这样实例和原型之间就构造了一条原型链。
类继承的应用
- 继承内置引用类型,扩展内置类型
// 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()