对于大多数人来,原型链与继承都是学习 JavaScript 过程中的一座大山。还记得当时自己学习的时候,是强忍着把书扔掉的冲动,硬着头皮在那看。

但一旦弄懂之后,无论是成就感还是自信心,都会得到很大的提升。

不知道大家有没有想过这样一个问题,当我们创建在一个对象后,总是可以调用一些这个对象本身没有的方法,如:

toString()hasOwnProperty()valueOf() 等等,一开始我以为这是语言默认给对象赋予的方法,后来才明白这一切,都是因为原型与原型链。

1. 什么是原型?

在 JavaScript 中,每个对象,都会有一个原型属性,在 chrome 中,我们可以通过 obj.__proto__ 方式来查看这个属性值。

而函数作为一种特殊的对象,除了原型属性,还有一个 prototype 属性,我们称为原型对象。

而无论是普通对象的原型,还是函数的原型对象,依然是一个对象,通常这个对象默认会有一个 constructor 属性,值为指向它的构造函数。

我们先以一个简单的例子来说明下大体情况。

  1. function Person(name, age) {
  2. this.name = name;
  3. this.age = age;
  4. }
  5. const 小明 = new Person('小明', 23)

Person 是一个构造函数,小明是我通过 Person 创建的对象实例。那我们怎么能够看出小明是 Person 创建的,而不是其他构造函数呢?

答案其实就在于,小明的 proto 属性与 Person 的 prototype 属性,都指向同一个对象。

原型与原型链 - 图1

所以,我们说的原型,就是图中的 Person.prototype 这个对象,它链接着对象实例与构造函数。

2. 什么是原型链

有了上面的知识我们不禁要问,普通的对象是不是也有自己的构造函数? Person.prototype 是一个对象,是不是也有自己的 proto 对象?而函数也是对象啊,那 Peson 是不是也有自己的 proto 对象?

答案都是肯定的,我们一个一个来看。

1.普通对象的的构造函数

  1. const obj = {type: 'nomalObj'}
  2. console.log(obj.__proto__.constructor === Object); // true

用普通方式(字面量语法或new Object()方式)创建了一个对象 obj , 我们通过查询它的 proto 原型得知它的构造函数就是 Object 。同样用更直观的图来看下。

原型与原型链 - 图2

可以看到,原生的构造函数 Object 和普通对象的关系与构造函数 Person 和它实例对象’小明’之间的关系是一样的。

2.Person.prototypeproto 原型是谁?

首先, Person.prototype 并不是我们手动使用构造函数创建的,它是 JavaScript 创建的。不过我们可以通过查询它的 proto 属性来查看它的值,发现这个值是 Object.prototype

也就说,Person.prototype 也是一个普通的对象。

  1. console.log(Person.prototype.__proto__ === obj.__proto__); // ture

新问题来了, Object.prototype 也是对象,那 Object.prototype.__proto__ 的值是谁?难道是它自己?

  1. console.log(Object.prototype.__proto__); // null

原来是 null

原型与原型链 - 图3

3.构造函数 Person 也是对象,是不是也有自己的 proto 属性,会是谁呢?

在这里,要向大家介绍一个新朋友,那就是 Function 构造函数。

其实除了通过 funtion name() {} 的方式声明一个函数外,使用 new Function() 也是可以的。此时,假如只传入一个参数,那这个参数将被当做函数体,如果有多个参数,最后一个参数会被当做函数体,前面的参数为函数的参数。

  1. const aa = new Function('a', 'b','console.log(a + b)');
  2. console.log(aa, aa(1,4)); // function (a,b) {console.log(a + b)} 5

而我们通过普通方式声明的函数,与通过 new Function 这种式创建的函数,都来源于同一个构造函数 Function ,除此之外,所有内置函数如 ArrayDate 也是如此。

因此,所有的函数,包括构造函数 Function 自己,它们的 proto 属性,都是指向 Function.prototype

  1. console.log(Person.__proto__ === Function.prototype); // true
  2. console.log(aa.__proto__ === Function.prototype); // true
  3. console.log(Function.__proto__ === Function.prototype); // true
  4. console.log(Array.__proto__ === Function.prototype); // true
  5. console.log(Date.__proto__ === Function.prototype); // true

此时需要注意的是,其它函数的原型对象,都是普通的对象,而 Funtion.prototype 是一个函数,不过这个函数的 proto 属性却是指向 Object.prototype 对象。 看起来,Function.prototype 就像是Object 的实例。

关于这一点,确实是很奇怪,根据文档信息,这么做是为了兼容之前的 ECMAScript 代码。所以这一点,大家知道就行,这是一个特例。

至此,我们将所有的对象(包括函数)与它们的原型和构造函数之间的情况都知道了,最终形成了下面这张图。

原型与原型链 - 图4

又图我们可以看出,对象小明的原型是 Person.prototype ,而 Person.prototype 的原型是 Object.prototype ,由此,根据 proto属性,形成了一条从小明到 Object.prototype 的链条,而这,就是原型链

3. 原型链的作用

一句话,原型链的作用,就是使得对象之间的属性或方法可以共享。

文章一开始我们提到的普通对象之所以能使用 toString() 等方法,是因为 Object.prototype 对象中有这个方法。

当我们访问一个对象的属性或方法时,会首先检测当前对象中是否存在,如果不存在,则会沿着原型链一层一层向上查找,如果在某一层找到了,便会使用这个值且停止查找,否则继续向上找,直到 Object.prototype ,如果此时仍旧没有找到,才会返回 undefined。

举个例子。

  1. // 通过构造函数 Person 创建一个对象
  2. const xiaohua = new Person('小华', 23);
  3. // 在 Person.prototype 上添加一个属性
  4. Person.prototype.father = '张三'
  5. console.log(xiaohua.name, xiaohua.father, xiaohua.mother); // 小华 张三 undefined

此时,对象 xiaohua 的原型链长这样。

原型与原型链 - 图5

当我们查找 xiaohua 里的属性时,就是按照图中的原型链从左至右依次进行。

这样一来,我们可以将许多对象都具有的相同属性,放到他们的原型中而不用每次都各自分别创建。

就拿构造函数 Person 来说,我们每次通过 new Person() 的方式可以创建一个具有 name 与 age 属性的对象,但每个对象之间,还有很多相同的属性或方法,比如,每个人都可以说话奔跑,都拥有鼻子眼睛与耳朵….., 这些特性,其实都可以放到 Person.prototype 中,这样,每个实例都无须重复定义这些相同的属性或方法却可以直接使用。

  1. Person.prototype.sayName = function() {
  2. console.log(`你好,我的名字叫 ${this.name}`)
  3. }
  4. Person.prototype.ear = 'ear'
  5. const per1 = new Person('per1', 23);
  6. const per2 = new Person('per2', 34);
  7. console.log(per1.name,per2.name); // per1 per2
  8. console.log(per1.ear, per2.ear); // ear ear
  9. per1.sayName(); // 你好,我的名字叫 per1
  10. per2.sayName(); // 你好,我的名字叫 per2

好了,原型与原型链的知识,我知道的就这么多了。总结起来,原型与原型链就是普通对象构造函数构造函数的 prototype 对象Object.prototype 对象这四个概念的关系。

在阅读的时候,试着用笔去画一画,一遍不行再读一遍。这种东西,真经不住你去死磕。

文章最后感谢谢「糖汐儿」同学提出关于 Function.prototype 类型的提示,也希望大家有问题的话,可以在下方评论区提出来,互相学习。