面向对象和继承

145 阅读30分钟

面向对象编程

什么是对象

Everything is object (万物皆对象)

对象到底是什么,我们可以从两次层次来理解。

(1) 对象是单个事物的抽象。

一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实 物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

(2) 对象是一个容器,封装了属性(property)和方法(method)。

属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属 性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。

在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集或功能集。

ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。 严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都 映射到一个值。

提示:每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义 的类型。

什么是面向对象

面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性。

mxdx01.png

面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。

它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。

在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息 等任务。

因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成 的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。

面向对象与面向过程:

  • 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
  • 面向对象就是找一个对象,指挥得结果
  • 面向对象将执行者转变成指挥者
  • 面向对象不是面向过程的替代,而是面向过程的封装

面向对象的特性:

  • 封装性

封装,也就是把客观事物封装成抽象的类,并且类可以把自己的 数据和方法只让可信的类或者对象操作,对不可信 的进行信息隐藏。

  • 继承性

    继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩 展。

  • 多态性

基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。

程序中面向对象的基本体现

创建对象

简单方式

我们可以直接通过 new Object() 创建:

const person = new Object() 
person.name = 'Jack' 
person.age = 18 
person.sayName = function () { console.log(this.name) }

每次创建通过 new Object() 比较麻烦,所以可以通过它的简写形式对象字面量来创建:

const person = { 
    name: 'Jack', 
    age: 18, 
    sayName: function () { 
        console.log(this.name) } 
 }

对于上面的写法固然没有问题,但是假如我们要生成两个 person 实例对象呢?

const person1 = { 
    name: 'Jack', 
    age: 18, 
    sayName: function () { 
        console.log(this.name) 
    } 
 } 
 const person2 = { 
     name: 'Mike', 
     age: 16, 
     sayName: function () { 
         console.log(this.name) 
      } 
 }

简单方式的改进:工厂函数

我们可以写一个函数,解决代码重复问题:

function createPerson (name, age) {
return {
name: name,
age: age,
sayName: function () {
console.log(this.name)
}
}
}
​

然后生成实例对象:

let p1 = createPerson('Jack', 18)
let p2 = createPerson('Mike', 18)

这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题,

但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

构造函数

构造函数也可以添加属性 函数也是对象

  • 内容引导:

  • 构造函数语法

  • 分析构造函数

  • 构造函数和实例对象的关系

    • 实例的 constructor
    • 属性 instanceof 操作符
  • 普通函数调用和构造函数调用的区别

  • 构造函数的返回值

  • 构造函数的静态成员和实例成员

    • 函数也是对象
    • 实例成员
    • 静态成员
  • 构造函数的问题

更优雅的工厂函数:构造函数

一种更优雅的工厂函数就是下面这样,构造函数:

function Person (name, age) {
    this.name = name
    this.age = age
    this.sayName = function () {
        console.log(this.name)
    }
}
let p1 = new Person('Jack', 18)
p1.sayName() // => Jack
let p2 = new Person('Mike', 23)
p2.sayName() // => Mike

解析构造函数代码的执行

在上面的示例中, Person() 函数取代了 createPerson() 函数,但是实现效果是一样的。 这是为什么呢?

我们注意到, Person() 中的代码与 createPerson() 有以下几点不同之处

  • 没有显示的创建对象
  • 直接将属性和方法赋给了 this 对象
  • 没有 return 语句
  • 函数名使用的是大写的 Person

而要创建 Person 实例,则必须使用 new 操作符。

以这种方式调用构造函数会经历以下 4 个步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
  3. 执行构造函数中的代码
  4. 返回新对象

下面是具体的伪代码:

function Person (name, age) {
 // 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象
 // var instance = {}
 // 然后让内部的 this 指向 instance 对象
 // this = instance
 // 接下来所有针对 this 的操作实际上操作的就是 instance
 this.name = name
 this.age = age
 this.sayName = function () {
  console.log(this.name)
 }
 // 在函数的结尾处会将 this 返回,也就是 instance
 // return this
}
构造函数和实例对象的

构造函数和实例对象的关系

使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。 在每一个实例对象中的proto中同时有一个 constructor 属性,该属性指向创建该实例的构造函数:

console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true

对象的 constructor 属性最初是用来标识对象类型的,

但是,如果要检测对象的类型,还是使用 instanceof 操作符更可靠一些:

console.log(p1 instanceof Person) // => true
console.log(p2 instanceof Person) // => true

总结:

  • 构造函数是根据具体的事物抽象出来的抽象模板

  • 实例对象是根据抽象的构造函数模板得到的具体实例对象

  • 每一个实例对象都具有一个 constructor 属性,指向创建该实例的构造函数

    • 注意: constructor 是实例的属性的说法不严谨,具体后面的原型会讲到
  • 可以通过实例的 constructor 属性判断实例和构造函数之间的关系

    • 注意:这种方式不严谨,推荐使用 instanceof 操作符,后面学原型会解释为什么

构造函数的问题

使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:

function Person (name, age) {
  this.name = name
  this.age = age
  this.type = 'human'
  this.sayHello = function () {
    console.log('hello ' + this.name)
  }
}
let p1 = new Person('lpz', 18)
let p2 = new Person('Jack', 16)
​

在该示例中,从表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。

那就是对于每一个实例对象, type 和 sayHello 都是一模一样的内容,

每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费

console.log(p1.sayHello === p2.sayHello) // => false

对于这种问题我们可以把需要共享的函数定义到构造函数外部

function sayHello = function () {
    console.log('hello ' + this.name)
}
function Person (name, age) {
    this.name = name
    this.age = age
    this.type = 'human'
    this.sayHello = sayHello
}
let p1 = new Person('lpz', 18)
let p2 = new Person('Jack', 16)
console.log(p1.sayHello === p2.sayHello) // => true

这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突的问题。

你肯定想到了可以把多个函数放到一个对象中用来避免全局命名空间冲突的问题:

const fns = {
sayHello: function () {
console.log('hello ' + this.name)
},
sayAge: function () {
console.log(this.age)
}
}
function Person (name, age) {
this.name = name
this.age = age
this.type = 'human'
this.sayHello = fns.sayHello
this.sayAge = fns.sayAge
}
let p1 = new Person('lpz', 18)
let p2 = new Person('Jack', 16)
console.log(p1.sayHello === p2.sayHello) // => true
console.log(p1.sayAge === p2.sayAge) // => true

至此,我们利用自己的方式基本上解决了构造函数的内存浪费问题。 但是代码看起来还是那么的格格不入,那有没有更好的方式呢?

小结

  • 构造函数语法

  • 分析构造函数

  • 构造函数和实例对象的关系

    • 实例的 constructor
    • 属性 instanceof 操作符
  • 构造函数的问题

构造函数总结

1.构造函数的函数名首字母大写(建议,非强制) 目的就是为了和普通函数区分

2.构造函数内 不要写 ruturn

如果return 的是一个 基本数据类型 写了也没用

如果return 的是一个 引用数据类型 写了就会导致构造函数没用

3.构造函数使用时,必须和new关键字连用

如果不练用,也可以调用,但构造函数就没用了

4.构造函数内部的this

当一个函数和new关键字连用的时候,那么我们说的这个函数是构造函数,然后这个函数内部的this指向本次调用被自动创建出来的那个对象

5.构造函数不能使用 箭头函数

因为箭头函数没有this

原型

内容引导:

  • 使用 prototype 原型对象解决构造函数的问题

  • 分析 构造函数、prototype 原型对象、实例对象 三者之间的关系

  • 属性成员搜索原则:原型链

  • 实例对象读写原型对象中的成员

  • 原型对象的简写形式

  • 原生对象的原型

    • Object
    • Array
    • String
    • ...
  • 原型对象的问题

  • 构造的函数和原型对象使用建议

更好的解决方案: prototype

Javascript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。 这个对象的所有属性和方法,都会被构造函数的实例继承。

这也就意味着,我们可以把所有对象实例需要共享的属性方法直接定义在 prototype 对象上。

prototype原型上的属性

1.方法 函数 公用性最高

2.具体的非引用类型的值

3.公共集和 引用类型

function Person (name, age) {
this.name = name
this.age = age
}
console.log(Person.prototype)
Person.prototype.type = 'human'
Person.prototype.sayName = function () {
console.log(this.name)
}
let p1 = new Person(...)
let p2 = new Person(...)
console.log(p1.sayName === p2.sayName) // => true

这时所有实例的 type 属性和 sayName() 方法, 其实都是同一个内存地址,指向 prototype 对象,因此就提高了运行效率

构造函数、实例、原型三者之间的关系

原型1.jpg

任何函数都具有一个 prototype 属性,该属性是一个对象

function F () {}
console.log(F.prototype) // => object
F.prototype.sayHi = function () {
    console.log('hi!')
}

构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数。

console.log(F.constructor === F) // => true

通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 proto

var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true

proto 是非标准属性。

proto 指向实例化对象构造函数的原型

实例对象可以直接访问原型对象成员。

instance.sayHi() // => hi!

总结:

  • 任何函数都具有一个 prototype 属性,该属性是一个对象
  • 构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数
  • 通过构造函数得到的实例对象内部会包含一个指向构造函数的prototype 对象的指针 proto
  • 所有实例都直接或间接继承了原型对象的成员

属性成员的搜索原则:原型链

了解了 构造函数-实例-原型对象 三者之间的关系后,接下来我们来解释一下为什么实例对象可以访问原型对象中的 成员

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性

  • 搜索首先从对象实例本身开始
  • 如果在实例中找到了具有给定名字的属性,则返回该属性的值
  • 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
  • 如果在原型对象中找到了这个属性,则返回该属性的值

也就是说,在我们调用 person1.sayName() 的时候,会先后执行两次搜索:

  • 首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。
  • ”然后,它继续搜索,再问:“ person1 的构造函数的原型有 sayName 属性吗(自身的proto意思一样 自身proto指向构造函数原型)?”答:“有。
  • ”于是,它就读取那个保存在原型对象中的函数。
  • 当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果

而这正是多个对象实例共享原型所保存的属性和方法的基本原理。

总结:

  • 先在自己身上找,找到即返回
  • 自己身上找不到,则沿着原型链向上查找,找到即返回
  • 如果一直到原型链的末端还没有找到,则返回 undefined

实例对象读写原型对象成员

读取:

  • 先在自己身上找,找到即返回
  • 自己身上找不到,则沿着原型链向上查找,找到即返回
  • 如果一直到原型链的末端还没有找到,则返回 undefined

值类型成员写入( 实例对象.值类型成员 = xx ):

  • 当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上
  • 也就是说该行为实际上会屏蔽掉对原型对象成员的访问

引用类型成员写入( 实例对象.引用类型成员 = xx ):

  • 同上

复杂类型修改( 实例对象.成员.xx = xx ):

  • 同样会先在自己身上找该成员,
  • 如果自己身上找到则直接修改 如果自己身上找不到,则沿着原型链继续查找,
  • 如果找到则修改 如果一直到原型链的末端还没有找到该成员,则报错( 实例对象.undefined.xx = xx )

更简单的原型语法

我们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype 。

为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:

function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
}
}

在该示例中,我们将 Person.prototype 重置到了一个新的对象。

这样做的好处就是为 Person.prototype 添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor 成员。

所以,我们为了保持 constructor 的指向正确,建议的写法是

function Person (name, age) {
  this.name = name
  this.age = age
}
Person.prototype = {
  constructor: Person, // => 手动将 constructor 指向正确的构造函数
  type: 'human',
  sayHello: function () {
    console.log('我叫' + this.name + ',我今年' +     this.age + '岁了')
  }
}

更简单的原型语法

我们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype 。 为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:

function Person (name, age) {
  this.name = name
  this.age = age
}

Person.prototype = {
  type: 'human',
  sayHello: function () {
    console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
  }
}

在该示例中,我们将 Person.prototype 重置到了一个新的对象。

这样做的好处就是为 Person.prototype 添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor 成员。

所以,我们为了保持 constructor 的指向正确,建议的写法是:


function Person (name, age) {
  this.name = name
  this.age = age
}

Person.prototype = {
  constructor: Person, // => 手动将 constructor 指向正确的构造函数
  type: 'human',
  sayHello: function () {
    console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
  }
}

原生对象的原型

所有函数都有 prototype 属性对象。

  • Object.prototype
  • Function.prototype
  • Array.prototype
  • String.prototype
  • Number.prototype
  • Date.prototype
  • ...

原型对象的问题

  • 共享数组
  • 共享对象

如果真的希望可以被实例对象之间共享和修改这些共享数据那就不是问题。但是如果不希望实例之间共享和修改这 些共享数据则就是问题。

一个更好的建议是,最好不要让实例之间互相共享这些数组或者对象成员,一旦修改的话会导致数据的走向很不明 确而且难以维护。

原型对象使用建议

  • 私有成员(一般就是非函数成员)放到构造函数中
  • 共享成员(一般就是函数)放到原型对象中
  • 如果重置了 prototype 记得修正 constructor 的指向
  • 构造函数 也可以添加方法 或 属性 属于共享的(就这么一个) 构造函数也是对象的一种

prototype与__ptoto__

prototype

每个函数都有一个prototype属性,该属性是一个指针,指向一个对象(构造函数的原型对象) ,这个对象包含所 有实例共享的属性和方法。原型对象都有一个 constructor 属性,这个属性指向所关联的构造函数。使用这个对象 的好处就是可以让所有实例对象共享它所拥有的属性和方法。这个属性只用js中的类(或者说能够作为构造函数的对 象)才会有。

proto

proto 是非标准属性

实例化对象,标准属性是[Prototype] 但属性无法访问 proto 是为了能访问实例化对象的原型而设置的

每个实例对象都有一个proto属性,用于指向构造函数的原型对象( protitype )。__proto__属性是在调用构造函 数创建实例对象时产生的。该属性存在于实例和构造函数的原型对象之间,而不是存在于实例与构造函数之间。

function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = function(){
  console.log(this.name);
 }; // 与声明函数在逻辑上是等价的
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
console.log(person1);
console.log(Person);
console.log(person1.prototype);//undefined
console.log(person1.__proto__);
console.log(Person.prototype);
console.log(person1.__proto__ === Person.prototype);//true1、调用构造函数创建的实例对象的[prototype]属性为"undefined",构造函数的prototype是一个对象。
2、__proto__属性是在调用构造函数创建实例对象时产生的。
3、调用构造函数创建的实例对象的__proto__属性指向构造函数的prototype,本质上就是继承构造函数的原型属性。
4、在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所
在函数的指针。

prtotype.png

  • proto :是 对象 就会有这个属性(强调是对象); 函数 也是对象,那么函数也有这个属性咯,它指向 构造函数 的 原型对象;
  • prototype :是 函数 都会有这个属性(强调是函数), 普通对象 是没有这个属性的(JS 里面,一切皆为对象,所 以这里的 普通对象 不包括 函数对象 ).它是构造函数的原型对象;
  • constructor :这是 原型对象 上的一个指向 构造函数 的属性。

总结:

  • 每一个对象都有_ proto ==> Object.prototype (Object 构造函数的原型对象);
  • 每个函数都 proto 和 prototype 属性;
  • 每个 原型对象 都有 constructor 和 **proto **属性,其中 constructor 指回'构造函数', 而 proto 指向 Object.prototype ;
  • object 是有对象的祖先,所有对象都可以通过 proto 属性找到它;
  • Function 是所有函数的祖先,所有函数都可以通过 proto 属性找到它;
  • 每个函数都有一个 prototype ,由于 prototype 是一个对象,指向了构造函数的原型对象
  • 对象的 proto 属性指向 原型 , **proto **将对象和原型链接起来组成了原型链

原型链与继承

对于使用过基于类的语言 (如 Java 或 C++) 的开发人员来说,JavaScript 有点令人困惑,因为它是动态的,并 且本身不提供一个 class 实现。(在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍 然是基于原型的)。 当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层 层向上直到一个对象的原型对象为 null 。根据定义, null 没有原型,并作为这个原型链中的最后一个环节。 几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。 尽管这种原型继承通常被认为是 JavaScript 的弱点之一,但是原型继承模型本身实际上比经典模型更强大。 例如,在原型模型的基础上构建经典模型相当简单。

QQ截图20230203115654.png

1. 原型链继承

QQ截图20230203120520.png

核心:原型链对象 变成 父类实例,子类就可以调用父类方法和属性。

子类.prtotype.__prto__ = 父类.prototype
function Parent() {}
Parent.prototype.age = 18
Parent.prototype.getName = function () {
   return this.name
}
function Child(name) {
      this.name = name
}

Child.prototype = new Parent()
var child = new Child('leo')
// 这样子类就可以调用父类的属性和方法
console.log(child.getName()) // leo
console.log(child.age) // 18

优点:** 实现简单。

缺点:

  1. 引用类型值的原型属性会被所有实例共享。
  2. 不能向父类传递参数。
  3. 子类自己的方法不可以调用了 因为原型对象被改变了
function Parent() {
  this.likeFood = ['水果', '鸡', '烤肉']
}

Parent.prototype.age = 18
Parent.prototype.getName = function () {
  return this.name
}

function Child(name) {
  this.name = name
}

Child.prototype = new Parent()
var chongqiChild = new Child('重庆孩子')
var guangdongChild = new Child('广东孩子')
// 重庆孩子还喜欢吃花椒。。。
chongqiChild.likeFood.push('花椒')
console.log(chongqiChild.likeFood) // ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "鸡", "烤肉", "花椒"]

这时,会发现明明只是 重庆孩子 爱吃花椒,广东孩子 莫名奇妙得也变得爱吃了????这个共享是存在问题的, 不科学的。(可能重庆孩子和广东孩子一起黑脸问号。。。)

至于第二个问题,其实也显而易见了,没有传递参数的途径。因此,第二种继承方式出来啦。

2. 借用构造函数继承

遗留问题:

  1. 父类引用属性共享。
    1. 不能传参数到父类。

核心:

  • 子类构造函数内部调用父类构造函数,并传入 this指针。
  • 将父类的属性 添加到了子类中
function Parent(name) {
  this.name = name
  this.likeFood = ["水果", "鸡", "烤肉"]
}

function Child(name) {
  Parent.call(this, name)
}

Parent.prototype.getName = function() {
  return this.name
}

var chongqingChild = new Child('重庆孩子')
var guangdongChild = new Child('广东孩子')

chongqingChild.likeFood.push('花椒')

console.log(chongqingChild.likeFood) // ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "鸡", "烤肉"]
console.log(chongqingChild.name) // "重庆孩子"

console.log(chongqingChild.getName()) // Uncaught TypeError: chongqingChild.getName is not a function

值得庆幸的是,这次只有我们 重庆孩子 喜欢吃花椒,广东孩子 没被标记爱吃花椒啦。并且,我们通过 call 方法将 我们的参数也传入到了父类,解决了之前的遗留问题啦。

但是,原型链继承 是可以调用父类方法的,但是借用构造函数却不可以了,这是因为 当前子类的原型链并不指向 父类了。因此,结合 第一,第二种继承方式,第三种继承方式应运而生啦。

3. 组合继承

核心: 前两者结合,进化更高级。

function Parent(name) {
  this.name = name
  this.likeFood = ["水果", "鸡", "烤肉"]
}

function Child(name, age) {
    //第二次调用父类Parent
  Parent.call(this, name)
  this.age = age
}

Parent.prototype.getName = function() {
  return this.name
}
//第一次调用父类Parent
Child.prototype = new Parent()
//手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child

Child.prototype.getAge = function() {
  return this.age
}

var chongqingChild = new Child('重庆孩子', 18)
var guangdongChild = new Child('广东孩子', 19)
chongqingChild.likeFood.push('花椒')

console.log(chongqingChild.likeFood) // ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "鸡", "烤肉"]
console.log(chongqingChild.name) // "重庆孩子"
console.log(chongqingChild.getName()) // "重庆孩子"
console.log(chongqingChild.getAge()) // 18

这样:

  1. 原型引用类型传参共享问题
  2. 传参问题
  3. 调用父类问题都解决啦。
  • Javascript 的经典继承。

  • 但是有一个小缺点:在给 Child 原型赋值会执行一次Parent构造函数。所以,无论什么情况下都会调用两次父 类构造函数 -还有一个比较细节的问题是第二次调用的Parent,出现了属性在不同层级重复,Parent的age也会在实例第一层对象上面,拥有这个“多余的”属性也按照原型链的规则,没什么问题。但在某些情况下会造成错误,例如删除实例上的age属性后,实际上还能访问到,此时获取到的是原型上的属性。

4.原型式继承

这是在2006年一个叫 道格拉斯·克罗克福德 的人,介绍的一种方法,这种方法并没有使用严格意义上的构造函数。

他的想法是 借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

这之前的三种继承方式,我们都需要自己写自定义函数(例如,Parent和Child)。假如,现在已经有一个对象了,并 且,我也只是想用你的属性,不想搞得那么麻烦的自定义很多函数。那怎么办呢?

核心: 我们需要创建一个临时的构造函数,并将作为父类的对象作为构造函数的原型,并返回一个新对象。

/*
  @function 实现继承 函数
  @param parent 充当父类的对象
*/

function realizeInheritance(parent) {
// 临时函数
    function tempFunc() {}
    tempFunc.prototype = parent
    return new tempFunc()
}

核心点说了,我们来尝试一下。

// 这个就是已有的对象
var baba = {
  name: "爸爸",
  likeFoods: ["水果", "鸡", "烤肉"]
}

/*
var newChild = {} <==> baba 这两个对象建立关系就是这种继承的核心了。
*/

var child1 = realizeInheritance(baba)
var child2 = realizeInheritance(baba)

child1.likeFoods.push('花椒')
console.log(child1.likeFoods) // ["水果", "鸡", "烤肉", "花椒"]
console.log(child2.likeFoods) // ["水果", "鸡", "烤肉", "花椒"]

我们可以发现,父类的属性对于子类来说都是共享的。所以,如果我们只是想一个对象和另一个对象保持一致,这 将是不二之选。

ES5 新增了个 Object.create(parentObject) 函数来更加便捷的实现上述继承

var baba = { 
  name: "爸爸", 
  likeFoods: ["水果", "鸡", "烤肉"] 
} 
var child1 = Object.create(baba) 
var child2 = Object.create(baba) 
child1.likeFoods.push('花椒')
console.log(child1.likeFoods) // ["水果", "鸡", "烤肉", "花椒"] 
console.log(child2.likeFoods) // ["水果", "鸡", "烤肉", "花椒"]

5. 寄生式继承

这种继承是基于原型式继承,是同一个人想出来的,作者觉得,这样不能有子类的特有方法,似乎不妥。就用来一 个种工厂模式的方式来给予子类一些独特的属性。

function realizeInheritance(parent) {
// 临时函数
  function tempFunc() {}
  tempFunc.prototype = parent
  return new tempFunc()
}

// Parasitic: 寄生的 inheritance: 继承 一个最简单的工厂函数。
function parasiticInheritance(object) {
  var clone = realizeInheritance(object) // 这是用了原型式继承,但是只要是任何可以返回对象的方法都可以。
  clone.sayName = function() {
    console.log('我是'+this.name)
  }
  return clone
}

var baba = {
  name: "爸爸",
  likeFoods: ["水果", "鸡", "烤肉"]
}

var child = parasiticInheritance(baba)
child.name = '儿子'
child.sayName() // 我是儿子

缺点:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率(每一个函数都是新的);这一点 与构造函数继承类似。

6.寄生组合式继承

function Parent(name) {
  this.name = name
  this.likeFood = ["水果", "鸡", "烤肉"]
}

function Child(name, age) {
  Parent.call(this, name) // 第二次调用
  this.age = age
}

Parent.prototype.getName = function() {
  return this.name
}

Child.prototype = new Parent() // 第一次调用
Child.prototype.constructor = Child

Child.prototype.getAge = function() {
  return this.age
}

这个两次调用的问题之前有提及过。过程大致:

  • 第一次调用,Child 的原型被赋值了 name 和 likeFood 属性
  • 第二次调用,注入this,会在Child 的实例对象上注入 name 和 likeFood 属性,这样就屏蔽了原型上的特性。 只要了问题,我们就来解决这个问题~
function Parent(name) {
  this.name = name
  this.likeFood = ["水果", "鸡", "烤肉"]
}

function Child(name, age) {
  Parent.call(this, name)
  this.age = age
}

Parent.prototype.getName = function() {
  return this.name
}

// Child.prototype = new Parent() 使用新方法解决
// Child.prototype.constructor = Child

inheritPrototype(Child, Parent)

function inheritPrototype(childFunc, parentFunc) {
  var prototype = realizeInheritance(parentFunc.prototype) //创建对象,我们继续是用原型式继承的创建
  prototype.constructor = childFunc //增强对象
  childFunc.prototype = prototype //指定对象
}

function realizeInheritance(parent) {
// 临时函数
  function tempFunc() {}
  tempFunc.prototype = parent
  return new tempFunc()
}

Child.prototype.getAge = function() {
  return this.age
}

var chongqingChild = new Child('重庆孩子', 18)
var guangdongChild = new Child('广东孩子', 19)
chongqingChild.likeFood.push('花椒')
console.log(chongqingChild.likeFood) // ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "鸡", "烤肉"]
console.log(chongqingChild.name) // "重庆孩子"
console.log(chongqingChild.getName()) // "重庆孩子"
console.log(chongqingChild.getAge()) // 18

这种方法的核心思想:

  • 首先,用一个空对象建立和父类关系。
  • 然后,再用这个空对象作为子类的原型对象。

这样,中间的对象就不存在new 构造函数的情况(这个对象本来就没有自定义的函数),这样就避免了执行构造函 数,这就是高效率的体现。并且,在中间对象继承过程中,父类构造器也没有执行。所以,没有在子类原型上绑定 属性。 这种继承方式也被开发人员普遍认为是引用类型最理想的继承范式。

总结

  • 模式(简述):
    • 工厂模式:创建中间对象,给中间对象赋添加属性和方法,再返回出去。
    • 构造函数模式:就是自定义函数,并用过 new 关键子创建实例对象。缺点也就是无法复用。
    • 原型模式: 使用 prototype 来规定哪一些属性和方法能被共享。
  • 继承
    • 原型链继承

      • 优点:只调用一次父类构造函数,能复用原型链属性
      • 缺点:部分不想共享属性也被共享,无法传参。
    • 构造函数继承:

      • 优点:可以传参,同属性可以不被共享。
      • 缺点:无法使用原型链上的属性。
    • 组合继承:

      • 优点:可以传参,同属性可以不被共享,能使用原型链上的属性
      • 缺点:父类构造函数被调用2次,子类原型有沉余属性
    • 原型式继承:(用于对象与对象之间)

      • 优点:可以传参,同属性可以不被共享。
      • 缺点:无法使用原型链上的属性。
    • 构造函数继承:

      • 优点::在对象与对象之间无需给每个对象单独创建自定义函数即可实现对象与对象的继承,无需调 用构造函数。
      • 缺点:父类属性被完全共享。
    • 寄生式继承:

      • 优点:基于原型式继承仅仅可以为子类单独提供一些功能(属性),无需调用构造函数。
      • 缺点:父类属性被完全共享。
    • 寄生组合继承:

      • 优点:组合继承+寄生式继承,组合继承缺点在于调用两次父类构造函数,子类原型有冗余属性, 寄生式继承的特性规避了这类情况,集寄生式继承和组合继承的优点与一身,是实现基于类型继承 的最有效方式。

Object.create()原型式继承

ES5 新增了个 Object.create(parentObject) 原型式继承方法

Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。

用于原型继承(最优解)

function Animal(name, age) {
  this.name = name;
  this.age = age;
}

Animal.prototype.showName = function () {
  console.log(this.name, `我是${this.constructor.name}类`);
}

Animal.prototype.showAge = function () {
  console.log(this.age, `我是${this.constructor.name}类`);
}

function Pig(name, age, sex = "公") {
  Animal.call(this, name, age);
  this.sex = sex;
}

//原型式继承 可以自己写 但e5封装了快捷方法
Pig.prototype = Object.create(Animal.prototype);

Pig.prototype.constructor = Pig;

Pig.prototype.showSex = function () {
  console.log(this.sex, `我是${this.constructor.name}类`);
}

let pig = new Pig('佩奇', 1, '母');
console.log(pig);//Pig {name: "佩奇", age: 1, sex: "母"}
pig.showName(); //佩奇
pig.showAge(); //1
pig.showSex(); //母

多重继承**

一个子类继承多个父类

function Parent1(name) {
  this.name = name;
}

Parent1.prototype.showName = function () {
  console.log(this.name)
}

Parent1.prototype.showAge = function () {
  console.log(this.age)
}

function Parent2(age) {
  this.age = age;
}

Parent2.prototype.showSomething = function () {
  console.log('something')
}

function Child(name, age, address) {
  Parent1.call(this, name);
  Parent2.call(this, age);
  this.address = address;
}

function mixProto(targetClass, parentClass, otherParent) {
  targetClass.prototype = Object.create(parentClass.prototype);
  Object.assign(targetClass.prototype, otherParent.prototype);
}

mixProto(Child, Parent1, Parent2)
var child = new Child('佩奇', 3, '火星');
console.log(child); //Child {name: "佩奇", age: 3, address: "火星"}
child.showName();//佩奇
child.showAge();//3
child.showSomething(); //something

Es6继承 开发实用

super 关键字

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

上面代码中,对象 obj.find() 方法之中,通过 super.foo 引用了原型对象 proto 的 foo 属性。 注意, super 关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错

Class extends 继承

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Point { } 
class ColorPoint extends Point { }

上面代码定义了一个 ColorPoint 类,该类通过 extends 关键字,继承了 Point 类的所有属性和方法。但是由于没有 部署任何代码,所以这两个类完全一样,等于复制了一个 Point 类。下面,我们在 ColorPoint 内部加上代码。

class ColorPoint extends Point { 
  constructor(x, y, color) { 
    super(x, y); // 调用父类的constructor(x, y)   
    this.color = color; 
  } 
  toString() { return this.color + ' ' + super.toString(); // 调用父类的toString() 
  } 
}

上面代码中, constructor 方法和 toString 方法之中,都出现了 super 关键字,它在这里表示父类的构造函数,用来 新建父类的 this 对象。

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

ss Point { /* ... */ } 
class ColorPoint extends Point { 
  constructor() { 
  } 
} 
let cp = new ColorPoint(); // ReferenceError

上面代码中, ColorPoint 继承了父类 Point ,但是它的构造函数没有调用 super 方法,导致新建实例时报错。

ES5 的继承,实质是先创造子类的实例对象 this ,然后再将父类的方法添加到 this 上面( Parent.apply(this) )。

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

如果子类没有定义 constructor 方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何 一个子类都有 constructor 方法。

class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
  constructor(...args) {
  super(...args);
  }
}

另一个需要注意的地方是,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这 是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。

class Point {
  constructor(x, y) {
  this.x = x;
  this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正确
  }
}

上面代码中,子类的 constructor 方法没有调用 super 之前,就使用 this 关键字,结果报错,而放在 super 方法之后 就是正确的。

下面是生成子类实例的代码

let cp = new ColorPoint(25, 8, 'green'); 
cp instanceof ColorPoint // true 
cp instanceof Point // true

上面代码中,实例对象 cp 同时是 ColorPoint 和 Point 两个类的实例,这与 ES5 的行为完全一致。

最后,父类的静态方法,也会被子类继承。

class A {
  static hello() {
    console.log('hello world');
  }
}
class B extends A {
}
B.hello() // hello world

上面代码中, hello() 是 A 类的静态方法, B 继承 A ,也继承了 A 的静态方法。

Object.getPrototypeOf()

Object.getPrototypeOf 方法可以用来从子类上获取父类

Object.getPrototypeOf(ColorPoint) === Point // true

因此,可以使用这个方法判断,一个类是否继承了另一个类。

super 关键字

注意点:

  • super() 当做函数用的时候只能在子类的构造函数中(constructor)
  • super super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

super 这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。 第一种情况, super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super 函 数。