JS原型链探究

先从构造函数说起

任意普通函数用于创建一个对象时,它就被称为构造函数

但我更愿意将其称为自定义构造函数,因为这个函数之所以能够作为构造函数,是因为我赋予了它这个功能,是我定义出来的。

1
2
3
4
5
function Person(name) {
this.name = name;
} //构造函数
var person1 = new Person('张三') // 实例对象
var person2 = new Person('李四') // 实例对象

上述代码就把Person()函数作为自定义构造函数,生成了两个实例对象

既然有自定义构造函数,难不成还有非自定义构造函数?

带着这个问题,反问一句,通过function关键词就能创造出来一个函数,那么function这个功能是谁来实现呢?

其实JS内部还有一个底层设计好的的Function()函数,作为创建其他函数的构造函数,它不用我们自己去设置,只需要通过关键词function调用即可。

为什么要有prototype?

现在要为张三和李四添加一个相同效果的sayHello方法,可以用以下代码

1
2
3
4
5
6
person1.sayHello = function () {
console.log("Hello!");
};
person2.sayHello = function () {
console.log("Hello!");
};

代码执行后,会分别为两个实例对象分别添加相应的sayHello属性。里面存放着相应的方法(就叫sayHello()方法吧)。

现在只是两个人,如果要创建50个人,500个,5000个甚至更多,每个人也都要有该方法,为每个对象单独使用一段代码添加方法显然就不现实了,此时可以修改构造函数如下:

1
2
3
4
5
6
7
8
9
function Person(name) {
this.name = name;
this.sayHello = function () {
console.log("Hello!");
};
} //构造函数
var person1 = new Person('张三') // 实例对象
var person2 = new Person('李四') // 实例对象
... // 更多实例对象

这样,就能够在创建实例对象时直接添加上该方法。并且在创建实例对象时,会为每个实例对象的属性或方法创建单独的内存空间存放,即使代码及实现效果相同也互不影响。验证如下:

1
2
3
4
5
6
console.log(person1.sayHello === person2.sayHello); // false,它们不是同一个方法,各自占有内存
person1.sayHello = function () {
console.log("Hello!!!!");
};
console.log(person1.sayHello()); // Hello!!!!
console.log(person2.sayHello()); // Hello!

但这样做有一个问题,每个sayHello()方法都占据单独的一块空间,现在只是两个人,如果要创建50个人,500个,5000个甚至更多,每个人也都要有该方法,那么内存占用就会爆炸高,那么该如何解决这种问题?

对此,JS的方法就是:既然效果相同,干脆只留一份,大家共享这一个方法,把它单独放在一个地方,谁用谁去调。

那么放到哪里?这就要考虑这些实例对象有什么共同的联系——无疑就是它们都是由同一个构造函数创建的,那就把这段代码交给构造函数保管吧。

为此构造函数Person()特意开辟了一块空间,将这些需要进行共享的方法或属性进行存放,之后把这个空间的地址告诉构造函数,构造函数用prototype这个属性值存放这个地址。这样构造函数就可以通过prototype访问到这块空间并对其进行修改。这块空间就被称为显式原型(为了避免这些名词造成理解上的困难,后面就叫prototype空间了)

在JS中,所有函数都有相对应的显示原型(也就是prototype空间)

1
2
3
4
5
6
7
8
9
10
11
//构造函数
function Person(name) {
this.name = name;
}
// 向构造函数的共享空间中添加方法
Person.prototype.sayHello = function () {
console.log("Hello!");
};
console.log(Person.prototype); // {sayHello: ƒ}

... // 创建实例对象

constructor是什么,在哪里?

constructor在JS中的作用很简单,通过对象.constructor指向创建该对象的构造函数。

1
2
console.log(person1.constructor); // Person(name) {this.name = name;}
console.log(person2.constructor); // Person(name) {this.name = name;}

既然任意JS对象都可以通过对象.constructor指向创建该对象的构造函数,这就意味着上述每个person实例对象都有该方法,既然大家都有,何不把该方法也交给Person.prototype保管呢?

因此构造函数的prototype空间中也保存着constructor方法,该方法的作用就是指回构造函数本身,就这样,prototype和constructor形成了闭环,能够实现构造函数和它的prototype空间相互访问。

__proto__是什么?如何使用的?

上面提到通过将共享的方法放到构造函数的prototype空间中,以方便实例对象进行调用,那么实例对象如何才能调用这些方法呢?

在JS中,每个实例对象在创建的时候都会被告知其构造函数的prototype空间的地址,通过__proto__属性进行标记(在有些浏览器中显示为[[prototype]])。因此,通过 对象.__proto__方法就能够访问该空间并使用存在在其中的方法。

在JS中,当对象身上没有要使用的方法时,会自动通过__proto__进入到共享空间去查找有没有相应的方法。

因此person1.constructor真正的执行过程是:首先在person1身上查找constructor方法,发现没有,就根据__proto__进入共享空间去查找,发现此处存在该属性且为一个方法,因此去执行该方法,就找到了构造函数那里。

⚠️需要注意的是:

(1)__proto__实例对象身上的属性,用于访问其构造函数的prototype空间,被称为隐式原型。

(2)在JS中,任意自定义的(构造)函数本身也是由Function()构造函数实例化出来的,所以自定义的(构造)函数本身也是一个实例对象,也有__proto__属性。

也是因为所有的自定义函数都是由Function()构造函数实例化出来的,所有的自定义函数的__proto__其实都指向同一个空间,即Function()的prototype空间。

而Function()构造函数则是JS底层实现的,所以现在先不用纠结谁生成的它。

(3)单纯作为实例对象(如person1、person2),身上是不会有prototype属性的,因为它不需要也不会用来为其他对象提供共享的内容!(它只需要利用别人提供的服务就好,本身不再对外提供服务)

Object()和Object.prototype

从这里开始,所提到的东西就涉及到了JS底层的实现,所有的自定义函数、变量,数组等等最终都会回到这里!

前面提到,所有的构造函数都有一个prototype空间,那么这个空间是以什么样的形式存在的?

1
2
console.log(Person.prototype); // {sayHello: ƒ, constructor: ƒ}
console.log(typeof Person.prototype); // object

从上面代码中可以看出,prototype空间本质上是一个对象(Object),那么这个对象是怎么生成的,由谁生成的?下面我们继续探究。

既然是一个对象,我们可以把这个对象看做是某个构造函数实例化之后的结果,那么这个函数是谁?

1
console.log(Person.prototype.__proto__.constructor); // ƒ Object() { [native code] }

通过__proto__我们来到了一个共享空间,又通过constructor找到了该空间所对应的构造函数。也正是这个函数创造了Person.prototype

这里可以看到是一个名为Object()的函数作为构造函数创建了Person.prototype空间,因此我们为内存空间中添加上与Object()相关的内容。

可以看到,Object()函数创造了自定义函数Person()的prototype空间,其实所有自定义函数__proto__都指向Object.prototype,因为所有自定义函数的prototype空间都是一个纯粹的对象,JS中凡是纯粹的对象都是由Object()函数创建的。

JS万物皆对象

说到这里其实有一句话——JS中万物皆对象——就要进行解释了。

抛去底层代码不谈,所有的自定义构造函数是怎么来的?其本质是Function()函数的实例化对象。

而所有的自定义实例对象怎么来的?是自定义构造函数的实例化对象!

甚至每个自定义构造函数本身的prototype空间是怎么来的?是Object()函数的实例化对象!!

可以说,通过Object()和Function()两个底层函数,实例化出了所有的JS其他内容,因此JS中“万物皆对象”,这个对象指的就是实例化对象。

所以JS中所有能看到的东西身上都有__proto__属性,其指向无外乎三处:Function()的prototype空间、Object()的prototype空间、自定义构造函数的prototype空间。(null的问题下面再谈)

Object()和Function()

前面所谈内容都是有逻辑、有推理的,可以一环一环扣下来进行推导,到了这里,就进入JS底层的设计了。这里就是人为规定的一些规则,没有什么必然逻辑。

在JS中,既然“万物皆对象”那么Object()和Function()是谁生成的实例对象呢?

按照我自己的理解。答案是:没有人生成他们它们。

🔸Object()和Function()本身其实就是JS设计出来的纯粹的函数,只不过为了体现“万物皆对象”的思想,使其看起来像是和对象一样,分别为他们两个添加了__proto__属性。

🔸于是设计了Object()的__proto__指向Function()的prototype,毕竟Object()是个函数;

🔸设计了Function.prototype的__proto__指向Object.prototype,毕竟它是个prototype空间,可以按照对象去理解,而对象的__proto__就是要指向Object.prototype的;

🔸设计了Function()的__proto__指向Function.prototype,也就是自己的prototype空间,毕竟Function()是个函数,所有函数的__proto__最终会指向Function.prototype

🔸为了让原型链能够有尽头,设计了Object.prototype的__proto__指向null(而不是像Function.prototype那样指向自身)。(原型链下一节介绍)

就这样,底层的Object()和Function()以及它们的prototype空间之间的关系被规定了下来。

结合前面的内容,JS中对象、函数等之间的关系就可以用下面这张图来表示:

原型链是什么?

介绍原型链是什么之前,先回顾一下显式原型(prototype)和隐式原型(__proto__)。

这两个概念通常与构造函数、实例对象同时出现,其关系如下:

显示原型和隐式原型有时指一个东西,就是构造函数的prototype空间。

显示原型是构造函数对prototype空间的称呼,通过prototype访问。

隐式原型是实例对象对prototype空间的称呼,通过__proto__访问。

通过__proto__,实例对象能够使用其构造函数身上的方法,从而实现了**“继承”**。

又因为构造函数的prototype空间(被看做对象时)也有__proto__属性,于是可以继续共享向上一级的方法,因此通过两次甚至更多次的“继承”,其实例对象也就能够使用既不在自身,也不再构造函数身上的一些方法。这种通过原型(prototype空间)实现的多级之间的继承关系,就被称为原型链。

通过原型链的继承,使得JS中一些基础的方法在封装于底层的同时还能提供给自定义函数、构造函数、对象去使用。

原型链实例

下面通过实例进行介绍:

比如在Object.prototype空间中定义了a()方法,虽然Person()构造函数和person1实例对象中都没有定义该方法,但两者均可以使用a方法。其流程如下:

(1)对于person1.a()

查找自身是否存在a(),不存在;

通过__proto__进入Person.prototype空间,查找是否存在a(),不存在;

继续通过__proto__进入Object.prototype空间,查找是否存在a(),存在;

返回a的执行结果

(2)对于Person.a()

查找自身是否存在a(),不存在;

通过__proto__进入Function.prototype空间,查找是否存在a(),不存在;

继续通过__proto__进入Object.prototype空间,查找是否存在a(),存在;

返回a的执行结果

(3)对于person2.b()

查找自身是否存在b(),不存在;

通过__proto__进入Person.prototype空间,查找是否存在b(),不存在;

继续通过__proto__进入Object.prototype空间,查找是否存在b(),不存在;

继续通过__proto__来到null,,不存在b()且不能继续查找下去,则返回person1.b is not a function


参考文章

JS 究竟是先有鸡还是有蛋,Object与Function究竟谁出现的更早,Function算不算Function的实例等问题杂谈 - 听风是风 - 博客园 (cnblogs.com)

帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)_码飞_CC的博客-CSDN博客_js prototype

用自己的方式(图)理解constructor、prototype、__proto__和原型链_飞歌Fly的博客-CSDN博客

谈谈对prototype和__proto__的理解 - 简书 (jianshu.com)