构造函数和原型与原型链

课程内容

构造函数和原型与原型链

课程目标

  • 能够使用构造函数创建对象
  • 能够说出原型的作用
  • 能够说出原型链的原理

构造函数

概述

在面向对象语言中,都存在类的概念,类就是对象的模板,对象就是类的实例。
通俗来讲:类可以理解成做房子的图纸,对象就是用图纸造出来的房子。
在 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 原型上有没有这个成员,如果再没有就返回空,没有找到这个成员。所以这个原型链就好比一条线路一样,让我们去查找的时候按照这条路一层层去往上找,这就是我们所说的原型链。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值