前言
在之前的[文章](解析JavaScript原型(一) - 掘金 (juejin.cn))我们知道了原型的一些基本作用,在上篇文章的结尾,我曾留下一张有关JavaScript原型的经典图,今天就让我们一起解开JavaScript原型的神秘面纱。
先看图片:
第一次看这张图可能会一脸懵,没关系,下面我将会和大家一起去逐步过关斩将,相信看完这篇文章,这张图对你来说将会变得十分亲切、熟悉。想要看懂该图,我们不得不去了解另一个知识点--原型链。
什么是原型链?
在JavaScript中,每个对象都有一个原型。对象的原型可以通过
__proto__属性访问。当我们访问一个对象的属性或方法时,如果对象本身没有这个属性或方法,JavaScript引擎会沿着原型链向上查找,直到找到对应的属性或方法或者到达原型链的顶端。
下面看一个案例:
const obj = {}; // 创建一个空对象
console.log(obj.__proto__ === Object.prototype); // 输出: true
console.log(Object.prototype.__proto__ === null); // 输出: true
上述代码中,obj是我们创建的一个对象字面量,该对象字面量obj可以通过__proto__(隐式原型)访问到Object的原型(显示原型),(有点类似指针可以访问下一个元素一样)。从中我们可以看出,obj继承了Object构造函数的原型。而Object构造函数在往上找就只能找到null,你可以理解为Object就是最大的,已经到头了(把Object比作链表的最后一个元素,在往后找只能找到null)。
要了解什么是原型链,我们上述括号中曾提到的隐式原型和显示原型我们也得了解他们是什么。
在JavaScript中,每个函数都有一个特殊的属性叫做prototype,而每个实例对象都有一个特殊的属性叫做__proto__,这也就是隐式原型是显示原型叫法的来源。
隐式原型
- 概念: 我们每实例化一个对象时,这个对象都会有自己的隐式原型,该实例对象会通过它自己的隐式原型去继承构造函数的显示原型上的属性。
- 作用: 隐式原型是为了实现对象间的继承关系。当我们创建一个函数时,它会自动获得一个隐式原型对象,并且所有通过该函数创建的实例对象将共享这个隐式原型。
Person.prototype.money = 100;
function Person(name) {
this.name = name;
}
// Person函数的显示原型
console.log(Person.prototype); // 输出:{ money: 100 }
const person1 = new Person('小明');
console.log(person1.__proto__); // 输出:{ money: 100 }
// person1的隐式原型指向Person函数的显示原型
console.log(person1.__proto__ === Person.prototype); // 输出: true
在上面的例子中,Person.prototype是Person函数的显式原型,而person1.__proto__指向了Person.prototype,形成了原型链。
显示原型
看了上面隐式原型的例子,我们大概也知道了显示原型是什么东西了。这里还是讲一下它的概念和作用。
- 概念: 每个函数都有一个
prototype属性,这个属性是函数自己的一个属性,被称为函数的显式原型。这个显式原型是一个对象,用于存储共享的属性和方法,比如上述案例中Person.prototype中的money属性。 - 作用: 显式原型是为了将属性和方法共享给该函数创建的所有实例对象。
function Person(name) {
this.name = name;
}
// 添加一个方法到Person函数的显示原型
Person.prototype.sayHello = function() {
console.log(`Hello,我是${this.name}.`);
};
const person1 = new Person('小美');
const person2 = new Person('小明')
// person1、person2都继承了Person函数的显示原型上的方法
person1.sayHello(); // 输出: Hello,我是小美.
person2.sayHello(); // 输出: Hello,我是小明.
在这个例子中,Person.prototype是Person函数的显式原型,它包含了sayHello方法。当我们通过new Person('小美')创建person1对象以及new Person('小明')创建person2对象,person1和person2都继承了Person.prototype上的sayHello方法。
在了解了隐式原型和显示原型之后,下面我们再来分析最开始的那张图。
如上图:
一. 实例对象f1、f2的隐式原型会指向构造函数Foo()的显示原型。
二. 构造函数Foo()的显示原型的隐式原型又会指向构造函数Object()的显示原型
解释:
- 当你创建一个函数时,JavaScript 自动为这个函数创建一个
prototype对象。对于Foo来说,这个prototype对象就是Foo.prototype。 - 这个
prototype对象有一个属性叫做constructor,它指向函数本身。因此,Foo.prototype.constructor将指向Foo构造函数。 Object.prototype是 JavaScript 中所有对象的顶层原型,包括函数对象。所以,Foo.prototype作为一个对象,也有一个隐式原型,指向Object.prototype。
三.实例对象o1、o2的隐式原型指向构造函数Object()的显示原型,Object()的显示原型的隐式原型指向null
四.构造函数Object()的隐式原型指向Function()构造函数的显示原型,Function()构造函数的显示原型的隐式原型又指向构造函数Object()的显示原型Object.prototype.并且,构造函数Function()的显示原型和隐式原型都指向Function.prototype
这里需要特别注意,连GPT懵了,他一开始认为构造函数Function()的隐式原型指向Object.prototype,这是错的。它并没搞清这层关系。
GPT更正前:
GPT更正后:
五. 最后,构造函数Foo()的隐式原型当然会指向Function.prototype(构造函数Function的显示/隐式原型)
- 图示可以见上面那种大图,这里就不截出来了。
留言
看完本文之后,相信你一定对JavaScript中原型链已经有了深刻的认识,最开始的那张图也已经难不到你了,如何喜欢博主写的文章的话,还请给博主点个赞哦,关注我,我将和大家一起踏上探索JavaScript的路途。♥(ˆ◡ˆԅ)