课程内容
构造函数和原型与原型链
课程目标
- 能够使用构造函数创建对象
- 能够说出原型的作用
- 能够说出原型链的原理
构造函数
概述
在面向对象语言中,都存在类的概念,类就是对象的模板,对象就是类的实例。
通俗来讲:类可以理解成做房子的图纸,对象就是用图纸造出来的房子。
在 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 关键字一起使用
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
, 有了 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
概述
每个对象都有一个 __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__
对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条线路,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性。
原型 constructor 构造函数
概述
构造函数 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
原型上有没有这个成员,如果再没有就返回空,没有找到这个成员。所以这个原型链就好比一条线路一样,让我们去查找的时候按照这条路一层层去往上找,这就是我们所说的原型链。