对于大多数人来,原型链与继承都是学习 JavaScript 过程中的一座大山。还记得当时自己学习的时候,是强忍着把书扔掉的冲动,硬着头皮在那看。
但一旦弄懂之后,无论是成就感还是自信心,都会得到很大的提升。
不知道大家有没有想过这样一个问题,当我们创建在一个对象后,总是可以调用一些这个对象本身没有的方法,如:
toString()
、hasOwnProperty()
、 valueOf()
等等,一开始我以为这是语言默认给对象赋予的方法,后来才明白这一切,都是因为原型与原型链。
1. 什么是原型?
在 JavaScript 中,每个对象,都会有一个原型属性,在 chrome 中,我们可以通过 obj.__proto__
方式来查看这个属性值。
而函数作为一种特殊的对象,除了原型属性,还有一个 prototype
属性,我们称为原型对象。
而无论是普通对象的原型,还是函数的原型对象,依然是一个对象,通常这个对象默认会有一个 constructor
属性,值为指向它的构造函数。
我们先以一个简单的例子来说明下大体情况。
function Person(name, age) {
this.name = name;
this.age = age;
}
const 小明 = new Person('小明', 23)
Person
是一个构造函数,小明是我通过 Person
创建的对象实例。那我们怎么能够看出小明是 Person
创建的,而不是其他构造函数呢?
答案其实就在于,小明的 proto 属性与 Person
的 prototype 属性,都指向同一个对象。
所以,我们说的原型,就是图中的 Person.prototype
这个对象,它链接着对象实例与构造函数。
2. 什么是原型链
有了上面的知识我们不禁要问,普通的对象是不是也有自己的构造函数? Person.prototype
是一个对象,是不是也有自己的 proto 对象?而函数也是对象啊,那 Peson
是不是也有自己的 proto 对象?
答案都是肯定的,我们一个一个来看。
1.普通对象的的构造函数
const obj = {type: 'nomalObj'}
console.log(obj.__proto__.constructor === Object); // true
用普通方式(字面量语法或new Object()
方式)创建了一个对象 obj
, 我们通过查询它的 proto 原型得知它的构造函数就是 Object
。同样用更直观的图来看下。
可以看到,原生的构造函数 Object
和普通对象的关系与构造函数 Person
和它实例对象’小明’之间的关系是一样的。
2.Person.prototype
的 proto 原型是谁?
首先, Person.prototype
并不是我们手动使用构造函数创建的,它是 JavaScript 创建的。不过我们可以通过查询它的 proto 属性来查看它的值,发现这个值是 Object.prototype
。
也就说,Person.prototype
也是一个普通的对象。
console.log(Person.prototype.__proto__ === obj.__proto__); // ture
新问题来了, Object.prototype
也是对象,那 Object.prototype.__proto__
的值是谁?难道是它自己?
console.log(Object.prototype.__proto__); // null
原来是 null
。
3.构造函数 Person
也是对象,是不是也有自己的 proto 属性,会是谁呢?
在这里,要向大家介绍一个新朋友,那就是 Function
构造函数。
其实除了通过 funtion name() {}
的方式声明一个函数外,使用 new Function()
也是可以的。此时,假如只传入一个参数,那这个参数将被当做函数体,如果有多个参数,最后一个参数会被当做函数体,前面的参数为函数的参数。
const aa = new Function('a', 'b','console.log(a + b)');
console.log(aa, aa(1,4)); // function (a,b) {console.log(a + b)} 5
而我们通过普通方式声明的函数,与通过 new Function
这种式创建的函数,都来源于同一个构造函数 Function
,除此之外,所有内置函数如 Array
、 Date
也是如此。
因此,所有的函数,包括构造函数 Function
自己,它们的 proto 属性,都是指向 Function.prototype
。
console.log(Person.__proto__ === Function.prototype); // true
console.log(aa.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true
console.log(Array.__proto__ === Function.prototype); // true
console.log(Date.__proto__ === Function.prototype); // true
此时需要注意的是,其它函数的原型对象,都是普通的对象,而 Funtion.prototype 是一个函数,不过这个函数的 proto 属性却是指向 Object.prototype 对象。 看起来,Function.prototype 就像是Object
的实例。
关于这一点,确实是很奇怪,根据文档信息,这么做是为了兼容之前的 ECMAScript 代码。所以这一点,大家知道就行,这是一个特例。
至此,我们将所有的对象(包括函数)与它们的原型和构造函数之间的情况都知道了,最终形成了下面这张图。
又图我们可以看出,对象小明的原型是 Person.prototype
,而 Person.prototype
的原型是 Object.prototype
,由此,根据 proto属性,形成了一条从小明到 Object.prototype
的链条,而这,就是原型链。
3. 原型链的作用
一句话,原型链的作用,就是使得对象之间的属性或方法可以共享。
文章一开始我们提到的普通对象之所以能使用 toString()
等方法,是因为 Object.prototype
对象中有这个方法。
当我们访问一个对象的属性或方法时,会首先检测当前对象中是否存在,如果不存在,则会沿着原型链一层一层向上查找,如果在某一层找到了,便会使用这个值且停止查找,否则继续向上找,直到 Object.prototype
,如果此时仍旧没有找到,才会返回 undefined。
举个例子。
// 通过构造函数 Person 创建一个对象
const xiaohua = new Person('小华', 23);
// 在 Person.prototype 上添加一个属性
Person.prototype.father = '张三'
console.log(xiaohua.name, xiaohua.father, xiaohua.mother); // 小华 张三 undefined
此时,对象 xiaohua
的原型链长这样。
当我们查找 xiaohua
里的属性时,就是按照图中的原型链从左至右依次进行。
这样一来,我们可以将许多对象都具有的相同属性,放到他们的原型中而不用每次都各自分别创建。
就拿构造函数 Person
来说,我们每次通过 new Person()
的方式可以创建一个具有 name 与 age 属性的对象,但每个对象之间,还有很多相同的属性或方法,比如,每个人都可以说话奔跑,都拥有鼻子眼睛与耳朵….., 这些特性,其实都可以放到 Person.prototype
中,这样,每个实例都无须重复定义这些相同的属性或方法却可以直接使用。
Person.prototype.sayName = function() {
console.log(`你好,我的名字叫 ${this.name}`)
}
Person.prototype.ear = 'ear'
const per1 = new Person('per1', 23);
const per2 = new Person('per2', 34);
console.log(per1.name,per2.name); // per1 per2
console.log(per1.ear, per2.ear); // ear ear
per1.sayName(); // 你好,我的名字叫 per1
per2.sayName(); // 你好,我的名字叫 per2
好了,原型与原型链的知识,我知道的就这么多了。总结起来,原型与原型链就是普通对象、构造函数、构造函数的 prototype 对象, Object.prototype
对象这四个概念的关系。
在阅读的时候,试着用笔去画一画,一遍不行再读一遍。这种东西,真经不住你去死磕。
文章最后感谢谢「糖汐儿」同学提出关于 Function.prototype 类型的提示,也希望大家有问题的话,可以在下方评论区提出来,互相学习。