前端知识总结系列笔记三:原型与原型链

453 阅读5分钟

前言

原型与原型链是面试的常考点之一,因此很有必要理解并掌握,本文尝试去弄清原型与原型链的关系,并通过图解的方式去帮助自身建立起原型与原型链的知识体系,使自己能在面试中能与面试官侃侃而谈,嘻嘻~

关于原型

JavaScript常被描述为一种基于原型的语言(prototype-based language)---每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。因此如果往原型对象上添加属性和方法,那么所有由该对象实例化的实例对象都可以共享该原型上的属性和方法。

// 代码块1
function SuperType() {}; // 构造函数
SuperType.prototype; // 原型对象

SuperType.prototype.sayHello = function() {
    console.log('Hello World!');
}

const s1 = new SuperType(); // 实例对象1
const s2 = new SuperType(); // 实例对象2

// 实例s1和实例s2都可以共享该原型上的方法sayHello()
s1.sayHello();
s2.sayHello();

每个构造函数都有一个指向原型对象的指针,通过.prototype属性访问,而原型对象都包含一个指向构造函数的指针,通过.constructor访问,如下图所示,其实就是一个循环引用。

// 接上述代码块1
SuperType.prototype.constructor === SuperType; // true
s1.__proto__ === SuperType.prototype; // true

// 接上述代码块1
s1.constructor === SuperType; // true

从上述代码我们可以猜想:通过构造函数new出来的实例对象,是否也有一个指向实例化自身的构造函数的指针,可通过.constructor访问呢? 我们尝试把实例对象s1打印出来看看!

从打印结果我们可以看出,实例对象s1本身并没有constructor属性,而是通过原型向上查找__proto__,共享原型上的constructor属性,该属性最终指向SuperType。

因此构造函数、实例对象、原型对象三者的关系如下:

关于原型链

我们来回顾一下构造函数、原型和实例的关系:

每个构造函数都有一个原型对象,而原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针(proto)。

而什么是原型链呢? 我们知道,每个对象都拥有一个原型对象,通过__proto__指针指向上一个原型,并从中继承方法和属性,同时原型对象也有可能拥有原型,这样一层一层,最终指向null。这就是原型链(prototype chain),通过原型链一个对象会拥有定义在其他对象中的属性和方法。

接下来简单展示下原型链的运作机制:

function Person(name) {
    this.name = name;
}
const p = new Person('jiaxin');

p; // Person {name: 'jiaxin'}
p.__proto__ === Person.prototype; // true
p.__proto__.__proto__ === Object.prototype; // true
p.__proto__.__proto__.__proto__ === null; // true

图解原型链

上面只是简单地介绍了原型链的概念以及运作机制,而完整的原型链远远没有那么简单,来看一张图:

从上图可以看出,对象除了普通的对象(有__proto__属性,没有prototype属性)、原型对象(有constru属性指向构造函数,也有__proto__指向原型)外,还有函数对象,通过new Function()创建的都是函数对象,也有__proto__属性指向Function.prototype,Function.prototype也有原型,__proto__指向Object.prototype,最终指向null。

const f = new Function();
f; // 函数对象
f.__proto__ === Function.prototype; // true
f.__proto__.__proto__ === Object.prototype; // true
f.__proto__.__proto__.__proto__ === null; // true

看下函数对象f的打印结果

因此可以得到另外一条原型链,如下图所示:

原型上的属性和方法是定义在prototype对象上的,而非对象实例本身。当访问一个对象的属性/方法时,它不仅仅在该对象上查找,还会查找对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性/方法或到达原型链的末尾(null)。

function Person(name) {
    this.name = name;
}

function Girl(name) {
    Person.call(this, name);
}

const girl = new Girl('jiaxin');
girl.call(); // Uncaught TypeError: girl.call is not a function

如上述代码所示,Person函数里面没有定义call()方法,为什么Person可以访问call()方法呢?我们把Person打印出来看一下,发现原来是在Person的原型上即Function.prototype上有call()方法可以访问。

而Girl函数里也没有定义call()方法,那么为什么执行girl.call()却会报错呢?根据原型链的思想,我们girl首先会看自身是否有call()方法,没有则会一直沿着原型链即__proto__向上查找,直到到达原型链的末端(null)还找不到,则会报错。我们把girl打印出来看看,发现girl实例对象的原型链上的确没有call方法(其实是因为girl是一个对象,所以没有函数原型上的call方法)。

总结

1、每个构造函数都有一个原型对象,而原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针(proto)。

2、每个对象拥有一个原型对象,通过 proto 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null,这种关系被称为原型链。

3、当访问一个对象的属性 / 方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性 / 方法或到达原型链的末尾(null)。