遨游 JavaScript 对象星际:探索面向对象编程的深邃世界

本文主要是介绍遨游 JavaScript 对象星际:探索面向对象编程的深邃世界,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!


请添加图片描述



个人主页:学习前端的小z

个人专栏:JavaScript 精粹

本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结,欢迎大家在评论区交流讨论!
在这里插入图片描述

文章目录

  • 💯面向对象编程
    • 🔗1 什么是对象
    • 🔗2 什么是面向对象
    • 🔗3 程序中面向对象的基本体现
    • 🔗4 创建对象
      • 🚀4.1 简单方式
      • 🚀4.2 简单方式的改进:工厂函数
    • 🔗5 构造函数
      • 🚀5.1 更优雅的工厂函数:构造函数
      • 🚀5.2 解析构造函数代码的执行
      • 🚀5.3 构造函数和实例对象的关系
      • 🚀5.4 构造函数的问题
      • 🚀5.5 小结
    • 🔗6 原型
      • 🚀6.1 更好的解决方案: `prototype`
      • 🚀6.2 构造函数、实例、原型三者之间的关系
      • 🚀6.3 属性成员的搜索原则:原型链
      • 🚀6.4 实例对象读写原型对象成员
      • 🚀6.5 更简单的原型语法
      • 🚀6.6 原生对象的原型
      • 🚀6.7 原型对象的问题
      • 🚀6.8 原型对象使用建议
    • 🔗7 prototype 与 \__proto\_\_
      • 🚀7.1 prototype
      • 🚀7.2 \__proto\_\_
      • 🚀7.3 isPrototypeOf()
      • 🚀7.4 Object.getPrototypeOf()
      • 🚀7.5 hasOwnProperty()
      • 🚀7.6 in 操作符
    • 🔗8 原型链与继承
      • 🚀8.1 原型链继承
      • 🚀8.2 借用构造函数继承
      • 🚀8.3 组合继承
      • 🚀8.4 原型式继承
      • 🚀8.5 寄生式继承
      • 🚀8.6 寄生组合式继承
      • 🚀8.7 总结
      • 🚀8.8 Object.create()
      • 🚀8.9 用于原型继承
      • 🚀8.10 多重继承


在这里插入图片描述


💯面向对象编程

🔗1 什么是对象

Everything is object (万物皆对象)

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

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

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

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

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

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

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

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

🔗2 什么是面向对象

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

面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。
它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。

在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。
因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。

面向对象与面向过程:

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

面向对象的特性:

  • 封装性
    • 封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
  • 继承性
    • 继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
  • 多态性
    • 基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。

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

在 JavaScript 中,所有数据类型都可以视为对象,当然也可以自定义对象。
自定义的对象数据类型就是面向对象中的类( Class )的概念。

我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。

假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个对象表示:

let student1 = { name: 'Michael', score: 98 }
let student2 = { name: 'Bob', score: 81 }

而处理学生成绩可以通过函数实现,比如打印学生的成绩:

function printScore (student) {console.log(`姓名:${student.name}   成绩:${student.score}`);
}

如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,
而是 Student 这种数据类型应该被视为一个对象,这个对象拥有 namescore 这两个属性(Property)。
如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个 printScore 消息,让对象自己把自己的数据打印出来。

抽象数据行为模板(Class):

function Student (name, score) {this.name = namethis.score = score
}Student.prototype.printScore = function () {console.log(`姓名:${this.name}   成绩:${this.score}`);
}

根据模板创建具体实例对象(Instance):

var std1 = new Student('Michael', 98);
var std2 = new Student('Bob', 81);

实例对象具有自己的具体行为(给对象发消息):

std1.printScore() // => 姓名:Michael  成绩:98
std2.printScore() // => 姓名:Bob  成绩 81

面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。
Class 是一种抽象概念,比如我们定义的 Class——Student ,是指学生这个概念,
而实例(Instance)则是一个个具体的 Student ,比如, Michael 和 Bob 是两个具体的 Student 。

所以,面向对象的设计思想是:

  • 抽象出 Class
  • 根据 Class 创建 Instance
  • 指挥 Instance 得结果

面向对象的抽象程度又比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。

🔗4 创建对象

🚀4.1 简单方式

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

const person = new Object()
person.name = 'Jack'
person.age = 18person.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)}
}

通过上面的代码我们不难看出,这样写的代码太过冗余,重复性太高。

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

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

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)

这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题,
但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

🔗5 构造函数

内容引导:

  • 构造函数语法
  • 分析构造函数
  • 构造函数和实例对象的关系
    • 实例的 constructor 属性
    • instanceof 操作符
  • 普通函数调用和构造函数调用的区别
  • 构造函数的返回值
  • 构造函数的静态成员和实例成员
    • 函数也是对象
    • 实例成员
    • 静态成员
  • 构造函数的问题

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

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

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

🚀5.2 解析构造函数代码的执行

在上面的示例中,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 的操作实际上操作的就是 instancethis.name = namethis.age = agethis.sayName = function () {console.log(this.name)}// 在函数的结尾处会将 this 返回,也就是 instance// return this
}

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

使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。
在每一个实例对象中的__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 操作符,后面学原型会解释为什么

🚀5.4 构造函数的问题

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

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

在该示例中,从表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。
那就是对于每一个实例对象,typesayHello 都是一模一样的内容,
每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。

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

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

function sayHello = function () {console.log('hello ' + this.name)
}function Person (name, age) {this.name = namethis.age = agethis.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 = namethis.age = agethis.type = 'human'this.sayHello = fns.sayHellothis.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

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

🚀5.5 小结

  • 构造函数语法
  • 分析构造函数
  • 构造函数和实例对象的关系
    • 实例的 constructor 属性
    • instanceof 操作符
  • 构造函数的问题

🔗6 原型

内容引导:

  • 使用 prototype 原型对象解决构造函数的问题
  • 分析 构造函数、prototype 原型对象、实例对象 三者之间的关系
  • 属性成员搜索原则:原型链
  • 实例对象读写原型对象中的成员
  • 原型对象的简写形式
  • 原生对象的原型
    • Object
    • Array
    • String
  • 原型对象的问题
  • 构造的函数和原型对象使用建议

🚀6.1 更好的解决方案: prototype

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

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

function Person (name, age) {this.name = namethis.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 对象,因此就提高了运行效率。

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

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

function F () {}
console.log(F.prototype) // => objectF.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__` 是非标准属性。

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

instance.sayHi() // => hi!

总结:

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

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

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

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

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

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

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

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

总结:

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

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

读取:

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

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

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

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

  • 同上

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

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

🚀6.5 更简单的原型语法

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

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

🚀6.6 原生对象的原型

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

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

🚀6.7 原型对象的问题

  • 共享数组
  • 共享对象

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

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

🚀6.8 原型对象使用建议

  • 私有成员(一般就是非函数成员)放到构造函数中
  • 共享成员(一般就是函数)放到原型对象中
  • 如果重置了 prototype 记得修正 constructor 的指向

🔗7 prototype 与 __proto__

🚀7.1 prototype

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

🚀7.2 __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属性所在函数的指针。

在这里插入图片描述

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

总结:

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

🚀7.3 isPrototypeOf()

虽然在所有实现中都无法访问到proto,但可以通过 isPrototypeOf()方法来确定对象之间是否存在这种关系。

alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true

🚀7.4 Object.getPrototypeOf()

在所有支持的实现中,这个方法返回proto的值。例如:

//这里的person1是下面实例
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"  person1

注意: 虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。

🚀7.5 hasOwnProperty()

可以检测一个属性是存在于实例中,还是存在于原型中。返回值为true表示该属性存在实例对象中,其他情况都为false。

🚀7.6 in 操作符

无论该属性存在于实例中还是原型中。只要存在对象中,都会返回true。但是可以同时使用 hasOwnProperty()方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。

var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
person1.name = "Greg";
alert(person1.name); //"Greg" —— 来自实例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true
alert(person2.name); //"Nicholas" —— 来自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true
delete person1.name;
alert(person1.name); //"Nicholas" —— 来自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true

🔗8 原型链与继承

对于使用过基于类的语言 (如 Java 或 C++) 的开发人员来说,JavaScript 有点令人困惑,因为它是动态的,并且本身不提供一个 class 实现。(在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的)。

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

尽管这种原型继承通常被认为是 JavaScript 的弱点之一,但是原型继承模型本身实际上比经典模型更强大。例如,在原型模型的基础上构建经典模型相当简单。

在这里插入图片描述

🚀8.1 原型链继承

基本思想: 利用原型让一个引用类型继承另一个引用类型的属性和方法

在这里插入图片描述

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

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. 不能向父类传递参数。
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)    // ["水果", "鸡", "烤肉", "花椒"]

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

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

🚀8.2 借用构造函数继承

遗留问题

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

核心:子类构造函数内部调用父类构造函数,并传入 this指针。

// 2. 借用构造函数
function Parent(name) {this.name = namethis.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 方法将我们的参数也传入到了父类,解决了之前的遗留问题啦。

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

🚀8.3 组合继承

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

function Parent(name) {this.name = namethis.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
}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构造函数。所以,无论什么情况下都会调用两次父类构造函数

🚀8.4 原型式继承

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

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

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

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

/*@function 实现继承 函数@param parent 充当父类的对象
*/
function realizeInheritance(parent) {// 临时函数function tempFunc() {}tempFunc.prototype = parentreturn 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) //    ["水果", "鸡", "烤肉", "花椒"]

效果和上面相同~

🚀8.5 寄生式继承

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

function realizeInheritance(parent) {// 临时函数function tempFunc() {}tempFunc.prototype = parentreturn 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() // 我是儿子

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

🚀8.6 寄生组合式继承

我们先回顾之前的 组合继承

function Parent(name) {this.name = namethis.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 = namethis.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 = parentreturn 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 构造函数的情况(这个对象本来就没有自定义的函数),这样就避免了执行构造函数,这就是高效率的体现。并且,在中间对象继承过程中,父类构造器也没有执行。所以,没有在子类原型上绑定属性。

这种继承方式也被开发人员普遍认为是引用类型最理想的继承范式。

🚀8.7 总结

  • 模式(简述):
    • 工厂模式:创建中间对象,给中间对象赋添加属性和方法,再返回出去。
    • 构造函数模式:就是自定义函数,并用过 new 关键子创建实例对象。缺点也就是无法复用。
    • 原型模式: 使用 prototype 来规定哪一些属性和方法能被共享。
  • 继承
    • 原型链继承:
      • 优点:只调用一次父类构造函数,能复用原型链属性
      • 缺点:部分不想共享属性也被共享,无法传参。
    • 构造函数继承:
      • 优点:可以传参,同属性可以不被共享。
      • 缺点:无法使用原型链上的属性
    • 组合继承
      • 优点:可以传参,同属性可以不被共享,能使用原型链上的属性。
      • 缺点:父类构造函数被调用2次,子类原型有冗余属性。
    • 原型式继承:(用于对象与对象之间)
      • 优点:在对象与对象之间无需给每个对象单独创建自定义函数即可实现对象与对象的继承,无需调用构造函数。
      • 缺点:父类属性被完全共享。
    • 寄生式继承:
      • 优点:基于原型式继承仅仅可以为子类单独提供一些功能(属性),无需调用构造函数。
      • 缺点:父类属性被完全共享。
    • 寄生组合继承:
      • 优点:组合继承+寄生式继承,组合继承缺点在于调用两次父类构造函数,子类原型有冗余属性,寄生式继承的特性规避了这类情况,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。

🚀8.8 Object.create()

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

const person = {isHuman: false,printIntroduction: function() {console.log(`我的名字是 ${this.name}. 我是人吗? ${this.isHuman}`);}
};const me = Object.create(person);me.name = 'Matthew'; 
me.isHuman = true; me.printIntroduction();
// "我的名字是 Matthew. 我是人吗? true"

🚀8.9 用于原型继承

 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;}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(); //1pig.showSex(); //母

🚀8.10 多重继承

 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();//3child.showSomething(); //something

在这里插入图片描述


参考 : http://bclary.com/log/2004/11/07/#a-11.9.3

请添加图片描述


这篇关于遨游 JavaScript 对象星际:探索面向对象编程的深邃世界的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/985436

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听