构造函数和原型与原型链
在面向对象语言中,都存在类的概念,类就是对象的模板,对象就是类的实例。
通俗来讲:类可以理解成做房子的图纸,对象就是用图纸造出来的房子。
在 ES6 之前,并没有类的概念,面向对象是通过构造函数来实现的,所以我们今天讲如何利用构造函数来实现面向对象。
doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>原型与原型链title>
head>
<body>
<script>
// 使用构造函数Teacher来抽象一个模板,里面接收name 和 age 两个参数,并给它定义一些公共属性和方法
function Teacher(name, age) {
this.name = name
this.age = age
this.teach = function () {
console.log('我会讲课')
}
}
// 通过 new 关键字实例化构造函数,得到对象
const hls = new Teacher('黄老师', 18)
// const zls = new Teacher('张老师', 19)
console.log(hls)
hls.teach()
// zls.teach()
script>
body>
html>

这里的
Teacher就是我们所说的图纸,hls就是造出来的房子。一个构造函数可以new出来多个实例对象
总结:
构造函数是一种特殊的函数,主要用来初始化对象,给对象的成员变量赋初始值,它总与 new 一起使用。我们可以把对象中的一些公共的属性和方法抽取出来,然后封装到这个函数里面。
在 js 中,使用构造函数时要注意以下两点:
new 在执行实例化是时候会做四件事:
this 指向这个新的对象这个构造函数虽然好用,但是存在浪费内存的问题。来看下面的两张图


根据上图,当我们在创建第一个对象 hls 的时候,里面的 name,age都是简单数据类型,要用的话直接赋值就行,
关键问题是里面还有一个 teach 方法,它是一个函数类型,在创建数据对象的时候会单独的开辟一个内存空间来存储。
以此类推,当我们创建第二个对象 zls 的时候,里面又有了一个 teach 方法,它又开辟了一个内存空间,两个内存空间都用来存放同一个函数。
这样的话,如果我们创建 100 个对象,那么就需要开辟 100 个内存空间,大大浪费了内存。
但是有一点可以肯定的是,这些函数存放在不同的内存空间里面,它们的内存地址肯定是不一样的。
我们来测试一下
doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>原型与原型链title>
head>
<body>
<script>
function Teacher(name, age) {
this.name = name
this.age = age
this.teach = function () {
console.log('我会讲课')
}
}
const hls = new Teacher('黄老师', 18)
const zls = new Teacher('张老师', 19)
console.log(hls.teach === zls.teach)
script>
body>
html>
得到的结果是 false,说明他们的内存地址是不一样的。这样就不太科学了,方法里面的代码是一样的,还要给我开辟不同的内存空间去存储同一个函数。
所以我们希望所有的对象使用同一个函数,这样就比较节省内存,那我们要怎么做呢?这就是接下来我们要讲的原型。
原型翻译成中文就是 prototype, 有了 prototype 就能实现我们这个函数的共享。构造函数通过原型分配的函数是所有对象所共享的。
js 规定,每个构造函数都有一个 prototype 属性,指向另一个对象。注意这个 prototype 就是一个对象,这个对象的所有属性和方法都会被构造函数所拥有。
接下来我们打印一下这个构造函数,修改代码
doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>原型与原型链title>
head>
<body>
<script>
function Teacher(name, age) {
this.name = name
this.age = age
this.teach = function () {
console.log('我会讲课')
}
}
const hls = new Teacher('黄老师', 18)
const zls = new Teacher('张老师', 19)
// console.log(hls.teach === zls.teach)
console.dir(Teacher)
script>
body>
html>
先看一下我们这个构造函数里面有没有 prototype 这个属性。

发现在 Teacher 这个构造函数里面确实有个 prototype 属性,但是这个属性里面存放的是一个花括号,
那我们知道花括号里面肯定是一个对象,所以 prototype 也称为对象,那 prototype 翻译成中文是 原型,所以我们可以称 prototype 为原型对象。
所以说我们每个构造函数里面都有一个原型对象,那这个原型对象是用来干什么的呢?
我们可以把那些不变的方法直接定义在
prototype对象上,这样所有对象的实例就可以共享这些方法,不需要再开辟内存空间了。
修改代码
doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>原型与原型链title>
head>
<body>
<script>
function Teacher(name, age) {
this.name = name
this.age = age
// this.teach = function () {
// console.log('我会讲课')
// }
}
// 1、在构造函数的原型对象里面添加一个 teach 方法
Teacher.prototype.teach = function() {
console.log('我会讲课')
}
const hls = new Teacher('黄老师', 18)
const zls = new Teacher('张老师', 19)
// console.log(hls.teach === zls.teach)
// console.dir(Teacher)
// 2、那么这两个实例能不能使用这个方法呢?如果能使用,说明实现了方法共享
hls.teach() // 我会讲课
zls.teach() // 我会讲课
script>
body>
html>
提问:
原型就是一个对象,我们称 prototype 为原型对象
共享方法
最后我们再次打印
console.log(hls.teach === zls.teach) // true
一般情况下,我们的公共属性定义到构造函数里面,公共方法我们放到原型对象身上,这样就不会另外开辟内存空间了
总结:
在 ES6 之前我们面向对象是通过构造函数来实现的,但是构造函数有个缺点,当我们把方法放在构造函数里面,在我们创建实例的时候都会开辟一个新的内存空间来单独存放同一个函数,这样就比较浪费内存。
这个时候我们想到了一个解决方案,我们把这个公共方法定义到构造函数的原型对象上,这样就可以实现这个方法的共享,所有的实例都可以使用这个方法了。
提问:
我们 teach 方法是定义在 Teacher 这个构造函数的原型对象上的,那为什么 hls 这个对象就能使用这个方法呢?
接下来就是我们要讲的 对象原型 __proto__
每个对象都有一个 __proto__ 指向构造函数的 prototype 原型对象,之所以对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 __proto__ 原型的存在。
接下来,我们打印一下,看对象里面是否有 __proto__ 这个属性
doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>原型与原型链title>
head>
<body>
<script>
function Teacher(name, age) {
this.name = name
this.age = age
}
Teacher.prototype.teach = function() {
console.log('我会讲课')
}
const hls = new Teacher('黄老师', 18)
const zls = new Teacher('张老师', 19)
console.log(hls) // 对象身上系统自己添加一个 __proto__ ,指向构造函数的原型对象 prototype
script>
body>
html>

虽然这个对象里面没有 teach 方法,但是这个对象里面有个 __proto__ ,它指向这个对象的原型对象,那原型对象上有这个方法,所以我们就能拿来使用了。
所以我们可以得出:
__proto__对象原型和构造函数的原型对象 prototype 是等价的
我们来打印一下看看
console.log(hls.__proto__ === Teacher.prototype) // true
通过上述案例,我们可以得到一个方法的查找规则:
hls 对象身上是否有 teach 方法,若有,则执行这个对象上的 teach 方法。teach 方法,因为有 __proto__ 的存在,就会去构造函数原型对象 prototype 身上去查找 teach 这个方法。所以我们可以得出:
__proto__对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条线路,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性。
构造函数 prototype 原型对象和对象原型 __proto__ 里面都有一个 constructor 属性,这个 constructor 我们称为构造函数,因为它指向构造函数本身。
doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>原型与原型链title>
head>
<body>
<script>
function Teacher(name, age) {
this.name = name
this.age = age
}
Teacher.prototype.teach = function() {
console.log('我会讲课')
}
const hls = new Teacher('黄老师', 18)
const zls = new Teacher('张老师', 19)
console.log(Teacher.prototype) // 打印构造函数上的原型
console.log(hls.__proto__) // 打印hls这个对象上的原型
// console.log(Teacher.prototype.constructor)
// console.log(hls.__proto__.constructor)
script>
body>
html>


展开后,我们可以看到两个对象里面都有 constructor 属性,都是指向原来的构造函数。
constructor它的目的很简单,主要用来记录该对象引用了哪个构造函数,它可以让原型对象重新指向原来的构造函数。
很多情况下,我们需要手动的利用 constructor 这个属性指回原来的构造函数,我们可以把多个方法放到这个构造函数的原型对象上,所以我们可以再添加一个方法
doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>原型与原型链title>
head>
<body>
<script>
function Teacher(name, age) {
this.name = name
this.age = age
}
Teacher.prototype.teach = function() {
console.log('我会讲课')
}
// 添加一个我会编程的方法
Teacher.prototype.program = function() {
console.log('我会编程')
}
const hls = new Teacher('黄老师', 18)
const zls = new Teacher('张老师', 19)
console.log(Teacher.prototype) // 打印构造函数上的原型
console.log(hls.__proto__) // 打印hls这个对象上的原型
script>
body>
html>
刷新浏览器,上面的输出还是指向原来的构造函数,这是没有问题的。但是如果我们有很多个方法呢,能不能换一种对象的方式添加方法呢?
doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>原型与原型链title>
head>
<body>
<script>
function Teacher(name, age) {
this.name = name
this.age = age
}
// Teacher.prototype.teach = function() {
// console.log('我会讲课')
// }
//
// Teacher.prototype.program = function() {
// console.log('我会编程')
// }
Teacher.prototype = {
teach: function () {
console.log('我会讲课')
},
program: function () {
console.log('我会编程')
}
}
const hls = new Teacher('黄老师', 18)
const zls = new Teacher('张老师', 19)
// console.log(Teacher.prototype) // 打印构造函数上的原型
// console.log(hls.__proto__) // 打印hls这个对象上的原型
console.log(Teacher.prototype.constructor)
console.log(hls.__proto__.constructor)
script>
body>
html>

当我们给 Teacher 的原型对象来了一个赋值后,它不再指向原来的构造函数了,而指向了 Object。
上面的第一种方式是直接在 Teacher 的原型对象里面添加了方法,而下面的这种方式是直接把一个对象赋值给了原型对象,这样就把之前的原型对象里面的方法全部覆盖了,所以我们的 Teacher.prototype 就没有了 constructor 属性了。
那么此时就有问题了,这两个原型到底是属于哪个构造函数的,不清楚了。这时我们需要手动的利用 constructor 这个属性指回原来的构造函数,那么我们该怎么办呢?
修改代码
doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>原型与原型链title>
head>
<body>
<script>
function Teacher(name, age) {
this.name = name
this.age = age
}
// Teacher.prototype.teach = function() {
// console.log('我会讲课')
// }
//
// Teacher.prototype.program = function() {
// console.log('我会编程')
// }
Teacher.prototype = {
constructor: Teacher,
teach: function () {
console.log('我会讲课')
},
program: function () {
console.log('我会编程')
}
}
const hls = new Teacher('黄老师', 18)
const zls = new Teacher('张老师', 19)
console.log(Teacher.prototype) // 打印构造函数上的原型
console.log(hls.__proto__) // 打印hls这个对象上的原型
console.log(Teacher.prototype.constructor)
console.log(hls.__proto__.constructor)
script>
body>
html>
刷新浏览器,发现 constructor 又指回我们原来的构造函数了。
总结:
如果我们修改了原来的原型对象,并且修改的是以对象的形式赋值的,则必须手动的利用 constructor 指回原来的构造函数,这样我们就知道我们的对象是通过哪个构造函数创建出来的。
通过前面的学习,我们学了三个知识点,Teacher构造函数、hls我们通过构造函数创建的实例、Teacher.prototype构造函数的原型对象。我们来看一下这三者之间的关系是什么?

分析:
Teacher 构造函数,每一个构造函数里面都有一个原型对象 prototype,它是通过构造函数的 prototype 指向这个原型对象的,constructor,它又指回了这个 Teacher 构造函数。hls,那我们知道在这个对象实例里面也有一个原型叫做 __proto__,这个 __proto__ 是指向的是这个原型对象。只要是对象就有 __proto__ 原型, 指向原型对象
构造函数原型对象里面的 __proto__ 原型指向的是大写的 Object.prototype
Object.prototype原型对象里面的 __proto__ 原型指向的是 null
接下来,我们打印一下 Teacher 的原型对象里面是否有个 __proto__ 原型
doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>原型与原型链title>
head>
<body>
<script>
function Teacher(name, age) {
this.name = name
this.age = age
}
Teacher.prototype.teach = function() {
console.log('我会讲课')
}
const hls = new Teacher('黄老师', 18)
// 只要是对象就有__proto__ 原型, 指向原型对象
console.log(Teacher.prototype)
script>
body>
html>

我们可以看出这个原型对象里面也有一个 __proto__ 原型,那它到底是指向谁的呢?展开后你会发现它指向的是大写的 Object 里面的原型对象,那到底是不是呢?我们可以验证一下
console.log(Teacher.prototype.__proto__ === Object.prototype) // true
返回的结果是 true,那说明我们的指向是没有错误的。所以我们可以得出:
Teacher 原型对象里面的 __proto__ 原型指向的是大写的 Object.prototype。Object.prototype是谁创建出来的呢?毫无疑问就是大写的 Object 构造函数创建出来的,那么 Object.prototype 就指回 Object 原型对象,Object 原型对象里面也有一个 constructor 指回 Object 构造函数Object 原型对象里面的 __proto__ 又指向的是谁呢?console.log(Object.prototype.__proto__) // null
Object 原型对象里面的 __proto__ 指向的是 null
最后我们来总结一下原型链:
hls 是一个对象,对象里面有一个__proto__原型,它指向的是 Teacher原型对象,那么 Teacher 也是一个对象啊,它里面也有一个 __proto__,那么这个原型是指向的 Object原型对象,那么 Object原型对象也有一个原型,它指向的是空。我们发现这里有许许多多的原型,这些原型组合成了一条链子,
这种现象呢我们称为原型链。
有了这个原型链,后面我们在访问对象成员的时候,给我们提供了非常好的一条路,我们可以先看对象的实例上有没有这个成员,如果没有的话,就到 Teacher原型对象上看看有没有这个成员,如果还没有呢,那我们再往上一层,看 Object 原型上有没有这个成员,如果再没有就返回空,没有找到这个成员。所以这个原型链就好比一条线路一样,让我们去查找的时候按照这条路一层层去往上找,这就是我们所说的原型链。