举例理解JS的原型和原型链

500 阅读5分钟

前言

文章部分图片来自 用自己的方式(图)理解constructor、prototype、__proto__和原型链

1.函数即对象

先牢记一句话:所有引用类型的constructor(构造函数)属性(是一个指针)指向它的构造函数 注意:其实constructor属性是在原型对象中

// 创建一个函数
function Person(name) {
    this.name = name
}

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

上面的代码创建了一个Person函数,从这段代码我们可以得知:
(1).Person被声明为一个函数,通过Person.constructor输出的内容可知,Function函数是Person函数的构造函数
(2).Function函数是自己的构造函数
(3).Function函数同样是Object这类内置对象的构造函数

根据这三点我们可以总结为:在js里,函数就是Function函数的实例对象,也就是我们说的函数即对象。几乎可以理解为:

// 使用Function构造器创建Function对象
// 为什么用几乎呢?因为这种方式生成的函数是匿名函数[anonymous],并且只在真正调用时才生成对象模型。
var Person = new Function('...')

2.什么是contructor

前一节说,所有引用类型都有一个constructor(构造函数)属性,该属性(是一个指针)指向它的构造函数

举个例子:

// 创建一个Person构造函数
function Person() {}
// 实例化对象
let person1 = new Person()
let person2 = new Person()

下图表示constructor的指向:

contructor的指向

我们已经知道对象的constructor属性指向它的构造函数,因此可以这么理解:
(1).person1与person2是Person对象的实例,他们的constructor指向创建它们的构造函数,即Person函数
(2).Person是函数,但同时也是Function实例对象,它的constructor指向创建它的构造函数,即Function函数
(3).至于Function函数,它是JS的内置对象,在第一节我们就已经知道它的构造函数是它自身,所以它的constructor属性指向自己

举个例子来看看:

// 创建一个构造函数
function Person(name) {
    this.name = name
}

// 实例化对象
let uzi = new Person('uzi') 

Person.a = 1
console.log(uzi.constructor.a) // 1

Function.b = 2
console.log(Person.constructor.b) // 2

所以constructor属性其实就是拿来保存自己构造函数引用的属性.

3.理解原型和原型链

首先记住这几点:
(1).所有的引用类型都有一个 __proto__ (隐式原型)属性,属性值是一个普通的对象
(2).所有的函数都有一个prototype(显示原型)属性,属性值是一个普通的对象
(3).所有引用类型都有一个constructor(构造函数)属性,该属性(是一个指针)指向它的构造函数
(4).所有引用类型的 __proto__ 属性指向它构造函数的prototype

接下来我们来分别验证,示例:

// 创建一个构造函数
function Person(name) {
    this.name = name
}

// 实例化对象
let uzi = new Person('uzi')

(1).所有的引用类型都有一个__proto__属性,所以Person和uzi都有_proto_属性,打印uzi来看:

uzi的打印结果

(2).只有函数对象才有prototype属性,所以只有Person才有prototype属性,打印Person来看:

Person的打印结果

(3).uzi对象的constructor(构造函数)属性指向它的构造函数

uzi.constructor

(4).打印一下 uzi.__proto__ === Person.prototype

uzi.__proto__

到此为止,上面的4个结论全都被验证。继续往下看。

什么是原型对象?
Person.prototype就是原型对象,划重点。我们给原型对象(Person.prototype)添加一个属性

Person.prototype.job = 'ADC'

在默认情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性,这个属性(是一个指针)指向prototype属性所在的函数(Person)。如图,原型对象中有我们添加的属性和默认的constructor属性:

Person

现在再回头看这段代码:

// 创建一个构造函数
function Person(name) {
    this.name = name
}

// 实例化对象
let uzi = new Person('uzi') 

当创建Person函数的时候,js会自动为Person添加prototype属性,值默认是一个有constructor属性的对象。当我们把Person当做构造函数调用(通过new关键字调用),创建实例化对象uzi的时候,uzi这个实例会通过设置自己的_proto_指向构造函数的prototype来继承构造函数prototype的所有属性和方法。

打印Person:

Person

打印uzi,可以看到uzi从Person.prototype中将job属性继承了下来:

uzi

因为uzi.__proto__ === Person.prototype,现在我们知道了一个简单的关系,就是当一个对象调用自身不存在的属性或者方法的时候,会先去它的__proto__上查找,也就是它的构造函数的prototype,就比如这里,我们需要调用uzi.job的时候,找到的其实是Person.prototype设置的job,如图:

uzi.job

我们试试能否找到sex属性,如图:

uzi.sex

那么现在问题来了,如果在Person.prototype也找不到呢?

我们先看看此时Person的结构,可以看到Person的prototype下面也有一个__proto__属性(前面说过所有的引用类型都有一个__proto__属性,Person.prototype是一个对象自然也是引用类型,所以也会有)。既然在Person.prototype上面找不到,那会不会去Person.prototype.__proto__上面去找呢?如果是,那Person.prototype.__proto__指向的又是什么?

Person

前面我们说,uzi(实例对象)的__proto__的constructor(构造函数属性)指向Person(构造函数),uzi.__proto__ === Person.prototype。 在这张图上我们看到Person.prototype.__proto__的constructor指向的是Object(因为函数是一个对象),那我们是否可以大胆推论Person.prototype.__proto__ 会指向Object.prototype,可以验证一下:

假设成立,因此当Person.prototype也找不到对应的属性或者方法的时候,会去Person.prototype.__proto__上面找,也就是Object.prototype

为了验证这种结论,我们设置Object.prototype.sex = '男'

设置Object.prototype.sex = '男'

再来打印uzi.sex:

uzi.sex

关系图

现在可以得出结论:当一个对象调用自身不存在的属性或者方法的时候,会先去它的__proto__上查找,也就是它的构造函数的prototype,如果没有找到,就会去它构造函数的prototype的__proto__指向的上一级函数的prototype去找(这里就是Object.prototype),如果还找不到,最后为null,这种一层一层向上查找的关系形成了一种链式结构,就叫原型链。