likes
comments
collection
share

干货分享(三)——深入理解JS原型和原型链

作者站长头像
站长
· 阅读数 32

引言

在JavaScript中,原型(prototype)则是一个让人爱-hate的话题。许多初学者在接触原型时都会感到头疼,有时甚至会觉得原型就是JavaScript中的一颗定时炸弹!但实际上,原型是JavaScript中非常强大且有趣的概念。

本文将会分为以下几个方面来进行介绍:

  1. 什么是原型

  2. 显式原型

  3. 隐式原型

  4. 隐式原型的隐式原型

  5. 构造函数的隐式原型

  6. 原型链

  7. 所有对象都有原型?

1. 什么是原型

作为JS中的一个方便方式,使用原型所定义的函数和功能会自动应用到对象的实例上。一旦进行了定义,原型的属性就会变成实例化对象的属。

如果你有其它编程语言的学习经历肯定会对以上的描述感到熟悉。其实,原型类似于经典面向对象语言中的类(Class)。

实际上,JS原型的主要用途就是使用一种类风格的面向对象和继承技术进行编码。

在对原型有了一个大致的了解后,让我们来具体了解一下原型。通常我们将JS中的原型分为两类:显式原型(prototype)和隐式原型(__prot__ /[[prototype]])

2. 显式原型(prototype)

所有函数在初始化时都会具有一个prototype属性,该属性的初值是一个空对象

干货分享(三)——深入理解JS原型和原型链

既然是对象,我们就可以向这个对象中进行方法和属性的增删改查。

干货分享(三)——深入理解JS原型和原型链

那么prototype对象有什么用呢?

首先让我们使用我们创建的构造函数Person,实例化一个对象p1,并打印p1。

干货分享(三)——深入理解JS原型和原型链

可以看到怕p1是由Person创建的一个空对象。那么如果我们试图打印p1中的lastName属性会发生什么?按照我们以往的学习它应该会输出undefined才对。

干货分享(三)——深入理解JS原型和原型链

令人意外的是它打印出了 To ,这是为什么呢?还记不记得在介绍什么的原型时,有过一句话使用原型所定义的函数和功能会自动应用到对象的实例上。也就是说实例化对象会自动继承构造函数的显式原型。

你是否注意到我们在上面打印p1时,最左边由一个可打开的箭头标识,下面让我打开这个箭头:

干货分享(三)——深入理解JS原型和原型链

constructor 表示对应的构造函数

我们可以看到在看似为空的p1对象中有一个[[prototype]]对象,也就是我们下面要讲的隐式原型,在它的里面包含了lastName属性,所以我们在打印p1中的lastName属性才会打印出 To

当然你也会疑惑为什么这里使用p1.lastName会直接输出To,而无需使用p1.[[prototype]].lastName ,这个问题你将在第四部分原型链中得到解答。

3.隐式原型(__proto__ /[[prototype]])

通过上面的介绍我们知道了p1具有一个隐式原型[[prototype]],当然这是它现在的写法,在之前隐式原型的写法为__proto__(前后各两个下划线 _)。

以p1为例,如果我们打使用p1.[[Prototype]]进行打印,会出现报错。

干货分享(三)——深入理解JS原型和原型链

而如果使用p1.__proto__进行打印,它将会打印我们预期的结果

干货分享(三)——深入理解JS原型和原型链

所以,下面我们仍然使用 __proto__ 来表示对象的隐式原型。

当你看到p1.__proto__是否感到有点熟悉,我们之前打印了构造函数Person的显式原型(prototype)

干货分享(三)——深入理解JS原型和原型链

发现它们长得一模一样,那么它们是否真的一样呢?

干货分享(三)——深入理解JS原型和原型链

从这我们可以得到一个重要结论对象的隐式原型 === 创建它的构造函数的显式原型,而它是原型链能形成的基础

4. 隐式原型的隐式原型

我们再打印一下p1

干货分享(三)——深入理解JS原型和原型链

我们知道第一个[[prototype]] 是p1的隐式原型,也就是Person的显式原型;那么最后一行的[[prototype]]是什么呢?让我们继续展开

干货分享(三)——深入理解JS原型和原型链

也就是说p1的隐式原型还有一个隐式原型,它的constructor对应的是Object,也就是说p1的隐式原型的隐式原型是Object的显式原型

而如果我们将它不断打开,最终会通向null。

5. 构造函数的隐式原型

在 JavaScript 中,构造函数也是函数对象,因此它也是通过函数构造而来的对象。对于函数对象来说,它也有隐式原型。那么构造函数的隐式原型是什么呢?

由于构造函数本身也是一个函数对象,因此当我们使用构造函数创建对象时,构造函数本身也拥有一个 __proto__ 属性,指向Function.prototypeFunction.prototype 是对应于函数对象的内置原型对象,包含所有函数对象共享的方法,如 callapplybind 等。

下面是一个简单的示例来演示构造函数作为对象的隐式原型:

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

// 构造函数作为对象的隐式原型指向Function.prototype
console.log(Person.__proto__ === Function.prototype); // true

在这个例子中,Person.__proto__ 指向的是函数对象 Person 的隐式原型,该隐式原型就是Function.prototype。因此,构造函数作为函数对象时,其隐式原型指向的是 Function.prototype

6. 原型链

js引擎在查找属性时,会顺着对象的隐式原型向上查找,找不到,则查找隐式原型的隐式原型,直到找到Object.prototype(null),这种查找关系,称之为原型链。

让我们通过一个例子来理解这句话:

GrandFather.prototype.say = function() {
  console.log('haha');
}


function GrandFather() {
  this.age = 60
  this.like = 'drink'
}


Father.prototype = new GrandFather()

function Father() {
  this.age = 40
  this.fortune = {
    card: 'visa'
  }
}


Son.prototype = new Father() // {age: 40, fortune: {xxx}}

function Son() {
  this.age = 18
}


let son = new Son()
son.say()

那么son.say() 能正常执行吗?答案是可以的。

1. son 会现在自己自身寻找say方法。

干货分享(三)——深入理解JS原型和原型链

找不到 2. 之后son 会在自身的隐式原型,也就是Son.prototype(son构造函数的显式模型)中继续寻找:

干货分享(三)——深入理解JS原型和原型链

依然没有找到

3. 它会继续寻找,查找son隐式原型的隐式原型,也就是Father.prototype(father构造函数的显式模型)中继续寻找:

干货分享(三)——深入理解JS原型和原型链

依然没有找到

4. 它会继续寻找,查找son隐式原型的隐式原型的隐式原型,也就是GrandFather.prototype(grandFather构造函数的显式模型)中继续寻找:

干货分享(三)——深入理解JS原型和原型链

你会发现找到了,所以son.say() 能正常运行。

如果你将son的隐式原型一直展开,也能找到say函数。

干货分享(三)——深入理解JS原型和原型链

上方案例完整原型链如下:

干货分享(三)——深入理解JS原型和原型链

7. 所有对象都有原型?(大坑)

答案是否定的。

let a = {
    name: 'b'
}
let obj = Object.create(a); // 创建一个新对象,这个对象的原型是a,隐式继承了a的属性
console.log(obj.__proto__);

干货分享(三)——深入理解JS原型和原型链

可以发现obj的隐式原型是我们传入的 a

而当我们传入一个null

let obj1 = Object.create(null);
console.log(obj1.__proto__);

干货分享(三)——深入理解JS原型和原型链

你会发现这种情况obj1没有原型,这也证明了所有对象都有原型 这句话是错误的。

结语

总的来说,虽然原型可能会让人感到困惑,但一旦掌握了它的奇妙之处,你会发现原型是JavaScript中最具魔力和魅力的部分之一。所以,让我们不要害怕原型,而是拥抱它,让它成为我们编程世界中的好朋友!今天的内容就到这里了。如果你觉得这篇文章有帮助或有所启发,别忘了给我一个鼓励的赞哦!

转载自:https://juejin.cn/post/7366166521798295615
评论
请登录