简单读懂JS原型链

182 阅读4分钟

JS 的原型链是一个很神奇的操作,它采用了一套巧妙的方式,解决了 JS 中的继承问题,同时也是面试中的必考点,本文会简单讲一下我的理解,不对的地方请各位指正。

一般来说,原型链可以拆分成四点:

  • 构造函数
  • prototype:原型
  • _ proto_:链
  • constructor

构造函数

构造函数,主要用来生成实例。也是我们平时使用最多的地方。

function Person(name, age) {
	this.name = name
  this.age = age
}
var xiaoming = new Person('xiaoming', 18)
var lihua = new Person('lihua', 20)

Person 就是构造函数
xiaoming,lihua 就是实例对象,也就是原型链的最末端。

原型(prototype)

在解释原型(prototype)之前,我们先来看个小问题

function Person(name) {
	this.sayName = function() {
    console.log(name)
  }
}
var person1 = new Person('xiaoming')
var person2 = new Person('lihua')
person1.sayName()
person2.sayName()

提问:sayName 函数在内存中有几个?

正确答案是两个。因为 person1 和 person2 都是从 Person 构造出来的,因此内存会为这两个实例各自开辟一个空间,它们的 sayName 函数也是独立的。

虽然功能相同,但是 sayName 在内存中却有两份。如果我们创建了 1000 个实例,sayName 在内存中岂不是会占据 1000 份?这是多大的性能浪费,产品经理知道了分分钟拿刀砍你。

这就是原型(prototype)存在的意义。

所谓原型,其实就是一个对象,它为构造函数共享了属性和方法,所有的实例引用到的原型都是这个对象。

我们可以把原型理解成一个大仓库,这个仓库存放着实例通用的属性和方法,而实例可以直接调用。

function Person() {}
Person.prototype.sayName = function (name) {
	console.log(name)
}
var person1 = new Person()
var person2 = new Person()
person1.sayName('xiaoming')	// xiaoming
person2.sayName('lihua')	// lihua

在这里,我们通过 Person.prototype.sayName 设置了公用的 sayName 函数,而实例则像属性一样执行即可。

这样的好处是,内存中只有一个 sayName 函数,并且只有 Person 函数创建出来的实例才能用,实现了高性能隐私性

注意:每个函数都有 prototype,也只有函数,才会有 prototype。

链(_ proto_)

问题又来了,为什么实例 person1 和 person2 能够访问到 Person.prototype 里的属性呢?答案就是 _ proto_。

链(_ porto_)可以理解为一个指针,是实例对象上的一个属性,能够指向该对象的原型。

看个例子:

function Person() {}
var person = new Person()
console.log(person.__proto__ === Person.prototype)	// true

通过属性 _ proto_,实例就和构造函数联系起来了。

constructor


我们刚才说到,原型就像构造函数的仓库,存放着通用的属性和方法,那么这个仓库是怎么与构造函数绑定在一起的呢?我们怎么才能从原型里找到构造函数呢?

答案是 constructor。
每个原型都有一个 constructor 属性指向关联的构造函数。

举例:

function Person() {}
console.log(Person === Person.prototype.constructor)	// true

可以看到,原型 Person.prototype 通过 constructor 就找到了构造函数 Person。

原型链

在上面的例子中

person1.sayName('xiaoming')	// xiaoming
person2.sayName('lihua')	// lihua

你有没有思考过这样一个问题:为什么调用 person1.sayName,就可以访问到 Person.prototype,sayName 呢?

答案在原型链的中。

原型链的定义是:一个实例对象,在调用属性或方法时,会依次从实例本身、构造函数原型、构造函数原型的原型... 不断向上查找,查看是否有对应的属性或方法。这样的寻找方式就像一个链条一样,从实例对象,一直查找到 Object.prototype,中途查找到目标属性或方法则终止。

举个例子:

function Person() {}
Person.prototype.sayName = function(name) {
	console.log(name)
}
var xiaoming = new Person()
xiaoming.sayName('xiaoming')
// 在实例中没有找到,在原型中找到了
// 实际调用的是 xiaoming.__proto__.sayName,即Person.prototype.sayName

xiaoming.toString()
// 在实例没有找到,在原型没有找到,在原型的原型中找到了
// 实际调用的是 xiaoming.__proto__.__proto__.toString,即Object.prototype.toString

可以看到,_ proto_ 像链条一样把实例,原型,构造函数之间的关系连起来,就称为原型链。

原型链的常见问题

实例怎么找构造函数?

我们来捋一下思路:

  1. 实例可以通过 _ proto_ 找到原型
  2. 原型可以通过 constructor 找到构造函数

因此我们可以得到这样的式子

function Person() {}
var xiaoming = new Person()
console.log(xiaoming.__proto__.constructor === Person)	// true

但是这个写法太长了,有没有更剪短点的呢?

当然有,我们可以利用原型链的特点,直接写成 xiaoming.constructor,就可以找到构造函数 Person。

function Person() {}
var xiaoming = new Person()
console.log(xiaoming.constructor === Person)	// true

当获取 xiaoming 对象上的 constructor 属性时,发现并没有,于是沿着原型链找到原型 Person.prototype,发现 constructor 在这里,就调用该属性,因此 xiaoming.constructor 的效果等同于 xiaoming._ proto_.constructor。

_ proto_ 到底是什么

绝大部分浏览器都支持使用 _ proto_ 访问原型,然而实际上它并不存在于 Person.prototype 上。实际上,它来自于 Object.prototype,与其说是属性,不如说是 getter/setter,当使用 obj._ proto_ 时,可以理解为返回了 Object.getPrototypeOf(obj)。

Function._ proto_ 是什么

首先找到 Function 的构造函数

console.log(Function.constructor)	// [Function: Function]

所以 Function 的构造函数是 Function。

因此 Function._ proto_ === Function.prototype。

Object.prototype._ proto_ 是什么

按照正常逻辑,Object.prototype._ proto_ 的结果应该是 Object.prototype,但是这样会有一个问题:原型链不就在 Object 这里死循环了吗?

为了禁止套娃,JS 规定:Object.prototype._ proto_ 为 null。

这样原型链就能够正常连接下去了。