类、原型和继承

本文详细探讨了JavaScript中的类、原型和继承机制,包括构造函数、原型对象、原型链、组合继承以及ES6中的类和继承。解释了如何通过原型链和借用构造函数实现继承,同时分析了ES6类的特性和继承方式,如静态方法、静态属性以及super关键字的使用。此外,还讨论了不同继承模式的优缺点和潜在问题。
摘要由CSDN通过智能技术生成

js类和继承

JS设计模式

类、原型和继承

ES5中类的继承

类(构造函数)

构造函数的名字通常用作类名,构造函数是类的公有标识

// Person类
function Person(name) {
  // 实例属性
  this.name = name
  // 实例方法
  this.getName = function() {
    return this.name
  }
}
// 类创建的对象叫类的实例对象
var person = new Person('ziyi2')

类的实例对象都有一个不可枚举的属性constructor属性指向类(构造函数)

// true
console.log(person.constructor === Person)

构造函数是类的公有标识,但是原型是类的唯一标识

// 检测实例对象所在的类 true
// 实际上instanceof运算符并不会检测person是否由Person()构造函数初始化而来
// 而是会检查person是否继承自Person.prototype
console.log(person instanceof Person)
// 继承至原型链顶端的Object类 true
console.log(person instanceof Object)

所有实例的实例方法并不是同一个

var person1 = new Person('ziyi1')
// 创建一个实例对象就会创建一个新的对象物理空间 false
console.log(person1.getName === person.getName)

如果不用new关键字,Person是一个普通的全局作用域中的函数

Person('ziyi2')
// 全局作用域中调用函数的this指向window全局对象 ziyi2
console.log(window.name)
// ziyi2
console.log(window.getName())

注意类的静态属性和静态方法和实例的属性和方法的区别

// 类属性
Person.age = 23
// 类方法
Person.getAge = function() {
  return this.age
}

构造函数创建实例对象的过程和工厂模式类似

function createPerson(name) {
  var person = new Object()
  person.name = name
  person.getName = function() {
    return this.name
  }
  return person
}
let person1 = createPerson('ziyi1')
let person2 = createPerson('ziyi2')
// false
console.log(person1.getName === person2.getName)

工厂模式虽然抽象了创建具体对象的过程,解决了创建多个相似对象的问题,但是没有解决对象的识别问题,即如何知道对象的类型,而类(构造函数)创建的实例对象可以识别实例对象对应哪个原型对象(需要注意原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象,才属于同一个类的实例,而构造函数并不能作为类的唯一标识)。
构造函数的创建过程

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象(this新对象)
  • 执行构造函数中的代码
  • 返回新对象(最终返回的就是new出来的实例对象,因此this指向实例对象)
原型
原型特性
// Person类(构造函数、Function类的实例对象、函数)
function Person(name) {
  this.name = name
}
// Person类是一个Function类的实例 true
// Person继承自Function
console.log(Person instanceof Function)

只要创建一个类(构造函数、Function类的实例对象、函数),就会为类创建一个prototype属性,这个属性指向类的原型对象,在默认情况下,原型对象会自动获得一个constructor(构造函数)属性,这个属性对应类本身(构造函数)

console.log(Person.prototype.constructor === Person)

类的所有实例共享一个原型对象,如果实例对象的方法可以通用,可以通过原型对象共享方法,而不需要为每一个实例对象开辟一个需要使用的实例方法。

// 原型对象的方法
Person.prototype.getName = function() {
  return this.name
}
// Person类的实例对象
var person = new Person('ziyi2')
var person1 = new Person('ziyi1')
// 两个实例对象引用的是同一个原型方法 true
console.log(person1.getName === person.getName)

当调用构造函数创建新实例后,该实例内部将包含一个指向构造函数原型对象的[[Prototype]] 内部属性,脚本中没有标准的方式访问[[Prototype]],但在一些浏览器诸如Firefox、Safari、Chrome在每个对象上都支持属性__proto__,这个引用存在于实例与构造函数的原型对象之间,调用构造函数创建的实例都有[[Prototype]]属性,但是无法访问,可以通过isPrototypeOf()方法来确定原型对象是否对应当前实例对象

// true
console.log(person.__proto__ === Person.prototype)
// true
console.log(Person.prototype.isPrototypeOf(person))

读取原型链的方法和属性时,会向上遍历搜索,首先搜索实例对象本身有没有同名属性和方法,有则返回,如果没有,则继续搜索实例对象对应的原型对象的方法和属性。

// Person类的原型对象的属性
// 需要注意原型对象具有动态性
// 先创造实例对象后修改原型对象也能够立即生效
Person.prototype.age = 28
// 获取原型对象的属性 28
console.log(person.age)
// 检测属性是否存在于实例对象中 false
console.log(person.hasOwnProperty('age'))
// 给实例对象赋值属性
person.age = 11
// 获取了实例对象的属性 11
console.log(person.age)
// 检测属性是否存在于实例对象中 true
console.log(person.hasOwnProperty('age'))
// 删除实例对象的属性
delete person.age
// 仍然可以重新获取原型对象的属性 28
console.log(person.age)
// 检测属性是否存在于实例对象中 false
console.log(person.hasOwnProperty('age'))
// 判断属性存在于实例对象还是原型对象(也可能属性不存在)
function hasPrototypePrpperty(obj,property){
  // in可以检测到实例对象和原型对象中的属性
  return !obj.hasOwnProperty(property) && (property in obj)
}
// for...in循环 
// 返回所有能够通过对象访问、可枚举的(enumerated)属性,包括实例和原型对象的中的属性
for(let key in person) {
  // name、age
  // name是实例对象的属性
  // age是原型对象的属性
  console.log(key)
}
// Object.keys() 
// 返回所有可枚举的实例对象的属性
// ['name']
console.log(Object.keys(person))

创建类的时候默认会给类创建一个prototype属性,是类的原型对象的引用,也可以重写改类的原型对象

function Person(name) {
  this.name = name
}
Person.prototype = {
  getName: function() {
    return this.name
  }
}
let person = new Person('ziyi2')
// 此时Person类原型对象是一个新的对象
// 注意和Person.prototype.getName的区别
// 这个新的对象的constructor属性对应Object类
console.log(Person.prototype.constructor === Object)
Person.prototype = {
  // 使新的原型对象的constructor属性对应Person类
  constructor: Person,
  getName: function() {
    return this.name
  }
}
// true
console.log(Person.prototype.constructor === Person)
// 注意未重写原型对象之前的实例仍然指向未重写前的原型对象
for(let key in person) {
  // name,getName
  console.log(key)
}
let person1 = new Person('ziyi1')
// 重写的原型对象的constructor属性变成了可枚举属性
for(let key in person1) {
  // name,constructor,getName
  console.log(key)
}
// 将constructor属性配置成不可枚举属性
Object.defineProperty(Person.prototype,"constructor",{
  enumerable:false,
  value:Person
})
for(let key in person1) {
  // name,getName
  console.log(key)
}
原型的弊端

原型对象的基本类型数据的属性(存放的是具体的值,因此每个实例对象的该属性值的改变互不影响)的共享对于实例对象而言非常便捷有效,但是原型对象的引用类型属性不同,原型对象的引用类型的属性存放的是一个指针(C语言中的指针的意思,指针存放的是一个地址,并不是存放一个具体的值,因为类似数组等值在一个32bit的物理块中是放不下的,肯定是放在一个连续的物理块中,因此需要一个地址去读取这些连续的物理块),指针最终指向的是一个连续的物理块,因此通过原型对象的引用类型修改的值都是改变这个物理块中的值,因此所有实例对象的该指向都会发生变化。

function Person(name) {
  this.name = name
}
Person.prototype = {
  constructor: Person,
  getName: function() {
    return this.name
  },
  age: 28,
  names: ['ziyi', 'ziyi1', 'ziyi2']
}
var person = new Person()
person.names[0] = 'ziyi_modify'
person.age = 11
var person1 = new Person()
// ["ziyi_modify", "ziyi1", "ziyi2"]
console.log(person1.names)
// 28
console.log(person1.age)
组合构造函数和原型模式

构造函数定义实例属性,原型对象定义共享的方法和基本数据类型的属性

function Person(name) {
  this.name = name
  this.names = ['ziyi', 'ziyi1', 'ziyi2']
}
Person.prototype = {
  constructor: Person,
  getName: function() {
    return this.name
  }
}
var person = new Person()
person.names[0] = 'ziyi_modify'
var person1 = new Person()
// ['ziyi', 'ziyi1', 'ziyi2']
console.log(person1.names)
继承

继承分为接口继承和实现继承,ECMAScript只支持实现继承,实现继承主要依靠原型链。

原型链

假设有两个类(Son类和Father类),Son类对应一个原型对象,通过Son类创建的实例对象都包含一个指向Son类原型对象的内部指针[Prototype]。假如让Son类的原型对象引用Father类的实例对象,则Son类的原型对象将包含一个指向Father类的原型对象的内部指针[[Prototype]],从而使Son类的原型对象可以共享Father类原型对象和实例对象(注意这里也包括实例对象)的方法和属性,从而又使Son类的实例对象可以共享Father类原型对象的方法和属性,这就是原型链。

原型链实现了方法和属性的继承,此时Son类是子类,继承了Father类这个父类。

function Father() {
  this.names = ['ziyi','ziyi1', 'ziyi2']
}
function Son() {}
Father.prototype.getName = function() {
  return Father.name
}
// Son类的原型对象包含了Father类原型对象的方法和属性
// 同时也包含了Father类实例对象的实例方法和实例属性
Son.prototype = new Father()
// 重写了Son类的原型对象
// Son.prototype.constructor !== Son
// true
console.log(Son.prototype.constructor === Father)
let son = new Son()
// 继承Father类实例对象的引用类型属性 
// ["ziyi", "ziyi1", "ziyi2"]
console.log(son.names)
// 继承Father类原型对象的方法
console.log(son.getName())

读取对象的属性和方法时,会执行搜索,首先搜索实例对象本身有没有同名的属性和方法,有则返回, 如果没有找到,那么继续搜索原型对象,在原型对象中查找具有给定名字的属性和方法。
实现原型链,本质上就是扩展了原型搜索机制

  • 搜索实例
  • 搜索实例的原型(该原型同时也是另一个类的实例)
  • 搜索实例的原型的原型

一直向上搜索,直到第一次搜索到属性或者方法为止,搜索不到,到原型链的末端停止。

该继承方法有一个缺陷,具体可以查看原型的弊端,在原型中使用引用类型的属性,在所有的实例对象中的该属性都引用了同一个物理空间,一旦空间的值发生了变化,那么所有实例对象的该属性值就发生了变化。

// son实例对象修改Father类实例对象(Son类的原型对象)的引用类型属性
son.names.push('ziyi3')
let son1 = new Son()
// son1中仍然引用的是Father类实例对象(Son类的原型对象)的引用类型属性
// ["ziyi", "ziyi1", "ziyi2", "ziyi3"]
console.log(son1.names)
借用构造函数(伪造对象或经典继承)

为了避开原型中有引用类型数据的问题,做到子类继承(这里的继承只是创建和父类相同的实例对象的属性和方法)父类的实例对象的实例方法和实例属性,在子类的构造函数中调用父类的构造函数,从而使子类的this对象在父类构造函数中执行,并最终返回的是子类的this对象(我们知道子类的this对象在构造函数的执行过程中都是开辟新的对象空间,因此引用类型的实例属性都是不同的指针地址)。

function Father() {
  this.names = ['ziyi','ziyi1', 'ziyi2']
}
function Son() {
  // 使用new关键字执行的构造函数this指向实例对象
  // 注意如果不用new关键字执行this指向全局对象window
  // 这里的Father类当做一个普通的执行函数
  // 此时只是让Son类的实例对象创建了和Father类实例对象一样的实例属性和实例方法
  Father.call(this)
  // Father.call(this)类似于在Son构造函数中执行
  // this.names = ['ziyi','ziyi1', 'ziyi2']
}
let son = new Son()
son.names.push('ziyi3')
// ["ziyi", "ziyi1", "ziyi2", "ziyi3"]
console.log(son.names)
let son1 = new Son()
// ['ziyi','ziyi1', 'ziyi2']
console.log(son1.names)

如果此时父类有自己的实例属性,而子类也有自己的实例属性

function Father(name,age) {
  this.name = name
  this.age = age
}
function Son() {
  Father.apply(this, arguments)
  this.job = arguments[2]
}
let son = new Son('ziyi2', 28, 'web')
// {name: "ziyi2", age: 28, job: "web"}
console.log(son)
组合继承

为了防止原型对象引用类型在实例对象中是同一个指针的问题,在原型链的实现中可以混合借用构造函数实现组合继承

function Father(name,age) {
  this.name = name
  this.age = age
  // 引用类型的实例属性
  this.names = []
}
Father.prototype.getNames = function() {
  return this.names
}
function Son(name, age, job) {
  // 借用构造函数
  // this执行的过程中也会创建this.names实例属性
  Father.call(this, name, age)
  this.job = job
}
// 创建原型链
// 需要注意此时Son类的原型对象中还是有Father类的实例对象的属性和方法
Son.prototype = new Father()
// 调整Son原型对象的constructor指向
Son.prototype.constructor = Son
let son = new Son('ziyi2', 28, 'web')
son.names.push('ziyi2')
// { age:28 job:"web" name:"ziyi2" names:["ziyi2"] }
console.log(son)
let son1 = new Son('ziyi2', 28, 'web')
// [] son.name和son1.names是不同的指针,指向不同的物理空间
console.log(son1.names)

需要注意的是Son类的实例对象中有name、age和names属性,Son类的原型对象中也有这些属性,只是根绝原型链的搜索机制,在使用实例对象访问这些属性时,首先搜索了实例对象中的该同名属性,因此原型对象中的该属性被屏蔽。

// undefined
console.log(Son.prototype.age)
// undefined
console.log(Son.prototype.name)
// []
console.log(Son.prototype.names)
寄生组合式继承
类型优缺点
构造函数模式可以创建不同实例属性的副本,包括引用类型的实例属性,但是不能共享方法
原型模式引用类型的属性对于实例对象而言共享同一个物理空间,因此可以共享方法
原型链对父类实现方法和属性继承的过程中,父类实例对象的引用类型属性在子类的实例中共享同一个物理空间,因为父类的实例对象指向了子类的原型对象
借用构造函数解决了继承中的引用值类型共享物理空间的问题,但是没法实现方法的共享
组合继承属性的继承使用借用构造函数方法,方法的继承使用原型链技术,即解决了引用值类型共享的问题,又实现了方法的共享,但是子类的原型对象中还存在父类实例对象的实例属性

目前而言,组合继承已经可以解决大部分问题,但是也有缺陷,就是会调用两次父类的构造函数,一次是实现原型时使子类的原型等于父类的实例对象调用了父类构造函数(同时在子类的原型对象中还存在了父类实例对象的实例属性),一次是使用子类构造函数时调用了一次父类构造函数。

寄生组合式继承可以解决在继承的过程中子类的原型对象中还存在父类实例对象的实例属性的问题。

// 继承o原型对象的方法和属性
// 需要注意o的引用类型属性在F实例对象中共享同一个物理空间(需要避免使用引用类型属性值)
// 当然o的方法在F实例对象中共享
function objectCreate(o) {
  // F类没有实例方法和实例属性
  function F() {}
  F.prototype = o
  // o是F类的原型对象
  // 返回F类的实例对象
  // new F().__proto__ = o
  // F类实例对象共享o的属性和方法
  // true
  console.log(new F().__proto__ === o)
  return new F()
}
// SubClass实现对于SuperClass原型对象的方法和属性的继承
function inheritPrototype(SubClass, SuperClass) {
  // prototype是一个实例对象
  // prototype不是SuperClass的实例对象而是另一个类F类的实例对象
  // SuperClass类和objectCreate函数中的F类的原型对象都是SuperClass.prototype
  // 这里没有调用SuperClass构造函数
  // prototype继承了SuperClass类的原型对象的方法和属性
  var prototype = objectCreate(SuperClass.prototype)
  // 使prototype实例对象的constructor属性指向SubClass子类
  // 因为重写SuperClass的原型对象时会修改constructor属性
  // SubClass.prototype.constructor = SubClass
  prototype.constructor = SubClass
  // 使SubClass类的原型对象指向prototype实例对象
  // 类似于SubClass.prototype = new SuperClass()
  // 只是这里是SubClass.prototype = new F()
  // F类本身没有实例方法和实例属性
  SubClass.prototype = prototype
}
function Father(name, age) {
  this.name = name
  this.age = age
}
Father.prototype.getName = function() {
  return this.name
}
function Son1(name, age, job) {
  // 借用构造函数
  Father.apply(this, arguments)
  this.job = job
}
function Son2(name, age, job) {
  // 借用构造函数
  Father.apply(this, arguments)
  this.job = job
}
// 组合继承的写法
Son1.prototype = new Father()
Son1.prototype.constructor = Son1
// age : undefined
// constructor : ƒ Son1(name, age, job)
// name : undefined
// __proto__ : { getName:ƒ () constructor : ƒ Father(name, age) }
console.log(Son1.prototype)
// 寄生组合式继承的写法
// 借用构造函数实现构造函数的方法和属性的继承
// inheritPrototype函数实现原型对象的方法和属性的继承
inheritPrototype(Son2, Father)
// constructor : ƒ Son2(name, age, job)
// __proto__ : { getName:ƒ () constructor : ƒ Father(name, age) }
console.log(Son2.prototype)
var son1 = new Son1('ziyi2', 28, 'web')
var son2 = new Son2('ziyi3', 28, 'hardware')
son1.getName()
son2.getName()

ES6中类的继承

ES6中的类只是ES5封装后的语法糖而已

// ES6中实现的Person类
class Es6Person {
  constructor(name, age) {
    // 实例对象的属性
    this.name = name
    this.age = age
    // 实例对象的方法
    this.getName = () => {
      return this.name
    }
  }
  // Person类原型对象的方法
  getAge() {
    return this.age
  }
}
// ES5中实现的Person类
function Es5Person (name, age) {
  // 实例对象的属性
  this.name = name
  this.age = age
  // 实例对象的方法
  this.getName = () => {
    return this.name
  }
}
// Person类原型对象的方法
Es5Person.prototype.getAge = function() {
  return this.age
}

在ES5中类的原型对象的方法是可枚举的,但是ES6中不可枚举

// []
console.log(Object.keys(Es6Person.prototype))
// ["getAge"]
console.log(Object.keys(Es5Person.prototype))

在ES5中如果不用new,this指向windows全局变量,在ES6如果不用new关键字则会报错处理

// Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
let person2 = Es6Person('ziyi2', 111)

ES6中的类是不会声明提升的, ES5可以

// Es5Person {}
console.log(Es5Person)
// Uncaught ReferenceError: Es6Person is not defined
let es6 = new Es6Person()
console.log(es6)
class Es6Person {}
function Es5Person {}

在ES6中如果不写构造方法

class Es6Person {}
// 等同于
class Es6Person {
  constructor() {}
}

在ES6中类的属性名可以采用表达式

const getAge = Symbol('getAge')
class Es6Person {
  constructor(name, age) {
    this.name = name
    this.age = age
    this.getName = () => {
      return this.name
    }
  }
  
  // 表达式
  [getAge]() {
    return this.age
  }
}
let es6Person = new Es6Person('ziyi2', 28)
es6Person[getAge]()
类的私有方法和私有属性

在ES6的类中目前并没有私有方法和私有属性的标准语法,但是可以通过其他方式模拟实现,例如属性名采用表达式,而ES5中很难实现

// 利用Symbol值的唯一性
const getAge = Symbol('getAge')
const job = Symbol('job')
class Es6Person {
  constructor(name, age) {
    this.name = name
    this.age = age
    this.getName = () => {
      return this.name
    }
    // 私有属性
    this[job] = 'web'
  }
  
  // 私有方法
  [getAge]() {
    return this.age
  }
}
let es6Person = new Es6Person('ziyi2', 28)
es6Person[getAge]()
es6Person[job]

此时如果Es6Person类处在一个文件中,那么getAge和job变量是当前文件的局部变量,外部文件无法访问,从而在外部文件调用Es6Person类的时候无法访问具有唯一性的getAge和job变量,从而使之成为私有方法和私有属性。目前私有属性有一个3阶段的提案

类的getter和setter

ES6利用get和set关键字对某属性设置存值函数和取值函数拦截属性的存取行为和ES5一样。

class Es6Person {
  constructor(name,age) {
    this.name = name
    this.age = age
  }
  get name() {
    return 'ziyi2'
  }
  set name(value) {
    console.log('setter: ', value)
  }
}
// setter: ziyi3
let person = new Es6Person('ziyi3', 11)
// ziyi2
console.log(person.name)
// setter: ziyi2
person.name = 'ziyi2'
类的Generator方法

给类增加一个遍历器,从而使类可以进行for…of操作

class Es6Person {
  constructor(...args) {
    this.args = args
  }
  * [Symbol.iterator]() {
    for(let arg of this.args) {
      yield arg
    }
  }
}
let person = new Es6Person('ziyi3', 11)
for(let value of person) {
  // ziyi3
  // 11
  console.log(value)
}
类的静态方法
class Es6Person {
  // 类的静态方法
  static getClassName() {
    // this指向类,而不是实例对象
    return 'Es6Person'
  }
  // 类的静态方法
  static getName() {
    // this指向类,而不是实例对象
    return this.getClassName()
  }
}
// Es6Person
console.log(Es6Person.getName())
类的静态属性提案

ES6明确规定,Class内部只有静态方法,没有静态属性,目前有一个静态属性的提案,对静态属性规定了新的写法

class Es6Person {
 static className = 'Es6Person'
}
new.target

new.target返回new命令作用于的那个构造函数

class Es6Person {
  constructor(name) {
    // new.target = class Es6Person {}
    if(new.target === Es6Person) {
      this.name = name
    } else {
      throw new Error('没有用new命令生成实例对象')
    }
  }
}
let person = new Es6Person('ziyi2')

new.target可以用于指定哪些子类可以继承父类。

类的继承

ES5 的继承使用借助构造函数实现,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面。ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象

class Es6Person {
  constructor(name) {
    this.name = name
  }
}
class Es6WebDeveloper extends Es6Person {
  constructor(name, age) {
    // 表示父类构造函数
    // 子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例
    super(name)
    this.age = age
  }
}

例如以下写法会报错

class Es6Person {
  constructor(name) {
    this.name = name
  }
}
class Es6WebDeveloper extends Es6Person {
  constructor(name, age) {
    // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
    console.log(this)
    super(name)
  }
}
let developer = new Es6WebDeveloper('ziyi2', 11)

此时的继承类似于ES5的寄生组合式继承,子类的原型对象中没有父类的实例对象和实例方法

// Es6Person {constructor: ƒ}
console.log(Es6WebDeveloper.prototype)

ES6在继承的语法上显然也考虑的更加周到,不仅继承了类的原型对象,还继承了类的静态属性和静态方法

class Es6Person {
  static getClassName() {
    return Es6Person.name
  }
  constructor(name) {
    this.name = name
  }
}
class Es6WebDeveloper extends Es6Person {
  constructor(name, age) {
    super(name)
    this.age = age
  }
}
// Es6Person
Es6WebDeveloper.getClassName()
super关键字

super作为函数调用时,代表父类的构造函数,只能在子类的构造函数中使用

class Es6Person {
  constructor(name) {
    // Es6WebDeveloper
    console.log(new.target.name)
    // Es6WebDeveloper {}
    console.log(this)
    this.name = name
  }
}
class Es6WebDeveloper extends Es6Person {
  constructor(name, age) {
    // 类似于A.prototype.constructor.call(this) 
    // super内部的this指向Es6WebDeveloper
    super(name)
    this.age = age
  }
}
let developer = new Es6WebDeveloper('ziyi2')

除此之外,super当做一个对象使用,在子类普通方法(原型对象)中,指向父类的原型对象,因此可以调用父类的原型方法,需要注意的是执行父类的原型方法时,在方法中执行时this指向的是子类的实例对象而不是父类的实例对象

class Es6Person {
  constructor(name) {
    this.name = name
  }
  getName() {
    // Es6WebDeveloper {name: "ziyi2", age: undefined}
    // 子类的whoIsSuper中调用的super.getName在这里执行的this指向子类实例对象
    console.log(this)
    return this.name
  }
}
Es6Person.prototype.className = Es6Person.name
class Es6WebDeveloper extends Es6Person {
  constructor(name, age) {
    super(name)
    this.age = age
  }
  whoIsSuper() {
    // true
    console.log(super.getName === Es6Person.prototype.getName)
    // super无法获取父类的实例属性
    // 因为super指向的是父类的原型对象
    // undefined
    console.log(super.name)
    // Es6Person
    console.log(super.className)
    return super.getName()
  }
}
let developer = new Es6WebDeveloper('ziyi2')
// ziyi2
console.log(developer.whoIsSuper())

如果super用作对象且不在子类的原型对象中调用,而是在子类的静态方法中调用,那么super指代父类而不是父类的原型对象,同理调用父类静态方法时this指向子类而不是父类

class Es6Person {
  static getClassName() {
    // class Es6WebDeveloper extends Es6Person {
    // ...
    // 在子类中使用super调用时,this指向子类
    console.log(this)
    return this.name
  }
  constructor(name) {
    this.name = name
  }
}
class Es6WebDeveloper extends Es6Person {
  constructor(name, age) {
    super(name)
    this.age = age
  }
  static getClassName() {
    return super.getClassName()
  }
}
// Es6WebDeveloper
console.log(Es6WebDeveloper.getClassName())
ES6的继承原理

__proto__存在于实例与构造函数的原型对象之间

class Es6Person {
  constructor(name) {
    this.name = name
  }
}
let es6Person = new Es6Person('ziyi2')
// true
console.log(es6Person.__proto__ === Es6Person.prototype)

ES6的继承原理

class Es6Person {}
class Es6WebDeveloper extends Es6Person {}
// true
// Es6WebDeveloper看做一个实例对象
// Es6Person看做一个原型对象
// 因此Es6WebDeveloper继承了Es6Person的所有属性和方法
// 实现了类的静态属性和方法的继承
// 子类的原型是父类
console.log(Es6WebDeveloper.__proto__ === Es6Person)
// true
// 这里类似于 Es6WebDeveloper.prototype = new Es6Person()
// 和ES5的原型链一样
// 子类的原型对象是父类的原型对象的实例
// 子类的实例继承父类的实例
console.log(Es6WebDeveloper.prototype.__proto__ === Es6Person.prototype)

因此以下继承都是可理解的

class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

本文转载于 子弈 js类和继承

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值