前言
原型和原型链在JavaScript的一个核心内容,它用于对象之间的属性继承,在面试的过程中也会经常会问到这部分的知识,如果接触过像Java这类的语言,而且只是对这个概念一知半解的话,估计只能全靠猜,所以掌握原型和原型链是进阶前端的一个重要关键点。这里小编将从函数对象
、构造函数
、实例
、new
、prototype
、__proto__
、contructor
、class
这八个知识点来探索JavaScript的 原型 和 原型链。
函数对象 & 构造函数 & 实例
函数对象:使用
function
关键字或使用Function
构造函数创建的对象即为函数对象。
“万物皆对象”,在JavaScript中,函数是一个特殊的对象,它可以像普通对象那样子设置以及访问自身的属性,例如:
// 普通对象
var stutent = {}
stutent.age = 18
console.log(stutent.age) // 18
// 函数对象
function teacher () {}
teacher.age = 50
console.log(teacher.age) // 50
构造函数:即是函数的本身,是函数的一个用法,可以通过
new
关键字来创建对象。
实例:通过
new
和构造函数创建的对象就是实例。通过__proto__
指向原型prototype
,通过constructor
指向构造函数。
function Student (name, age, school) {
this.name = name
this.age = age
this.school = school
}
var student1 = new Student('啊俊俊', 23, '华软')
在上述例子中,Student
方法即是 构造函数,使用 new
关键字加 Student
构造函数创建 student1
对象,student1
就是一个 实例。
prototype
& __proto__
原型和原型链的概念中,prototype
就是原型,可以把它理解为制作月饼的模子。prototype
是函数特有的属性,普通的对象是没有 prototype
的,看下面的例子:
var a = {}
var b = function () {}
function c () {}
console.log(a.prototype) // undefined
console.log(b.prototype) // { constructor: ƒ }
console.log(c.prototype) // { constructor: ƒ }
prototype
是用来干嘛的?
通过上文我们了解到什么是实例,但是每次通过构造函数创建的实例都是不一样的,如果想让多个实例之间具有共享属性的话,仅靠构造函数是不够的。
在 ECMAScript 设计的时候,并没有像Java那样子设计成类的概念,而是通过构造函数的 prototype
来实现对象之间的共享属性,看下面的例子:
function Student () {}
Student.prototype.school = '华软'
var student1 = new Student()
var student2 = new Student()
console.log(student1.school) // 华软
console.log(student2.school) // 华软
在上面的代码中可以看到,定义了一个 Student
的构造函数,是一个空函数同时设置了该构造函数的原型 prototype
属性 school
,通过该构造函数创造的两个实例中,都继承了原型中的 school
属性。
__proto__
又是什么?
在上文中我们了解到如何让多个实例之间具有共享属性,但它共享的原理又是什么呢?
原理就是通过构造函数创建出来的实例中,该实例的内部具有一个 __proto__
指针来指向构造函数的原型 prototype
。
我们都知道,在JavaScript中,对象是在堆内存中保存的,像 var o = { name: 'a' }
中,变量 o
是一个指针并指向了 { name: 'a' }
的内存地址,判断两个对象变量是否相等实际上是判断这两个变量指针是否指向同一个内存空间。
而在实例和原型之间的关系则是实例的 __proto__
指向了原型 prototype
,即 __proto__
和 prototype
指向了同一个内存空间,看下面的例子就可以看出它们两的关系:
function Student () {}
Student.prototype.school = '华软'
var student1 = new Student()
console.log(student1.__proto__) // { school: "华软", constructor: ƒ }
console.log(Student.prototype) // { school: "华软", constructor: ƒ }
console.log(student1.__proto__ === Student.prototype) // true
那么 new
实际上是做了什么呢,可以用下面的代码来理解 __proto__
的赋值过程:
function Student (name) {
this.name = name
}
Student.prototype.school = '华软'
// var student1 = new Student('小明')
var student1 = {}
student1.__proto__ = Student.prototype
Student.call(student1, '小明')
修改原型属性
首先用一张关系图来表示 __proto__
、prototype
和对象存储关系:

在上图中可以清晰的了解到两者之间的关系,虽然可以通过实例访问原型中的属性,但不能通过实例直接修改或重写原型的属性,看下面的例子:
function Teacher () {}
Teacher.prototype.system = '软件系'
var teacher1 = new Teacher()
var teacher2 = new Teacher()
console.log(teacher1.system) // 软件系
console.log(teacher2.system) // 软件系
teacher1.system = '外语系'
console.log(teacher1.system) // 外语系
console.log(teacher2.system) // 软件系
在上面的例子中,通过构造函数 Teacher
创建的 teacher1
和 teacher2
两个实例,在创建后访问内部属性 system
,JavaScript在执行时会先搜索实例中是否存在该属性,如果有则立刻获取并终止搜索,如果没有则往实例的原型中继续搜索。
所以上面代码中前两个 console
中打印的都是来自 Teacher
原型中的 system
,而后面执行了 teacher1.system = '外语系'
,此时 teacher1
实例修改的并不是原型中的属性,而是自身的system属性,所以后面两个打印的 system
分别来自实例自身和原型。如果想同时修改两个实例的共享属性的话就应该从原型上修改,如下:
function Teacher () {}
Teacher.prototype.system = '软件系'
var teacher1 = new Teacher()
var teacher2 = new Teacher()
console.log(teacher1.system) // 软件系
console.log(teacher2.system) // 软件系
Teacher.prototype.system = '外语系'
console.log(teacher1.system) // 外语系
console.log(teacher2.system) // 外语系
修改原型属性的特别情况!!
什么了解到修改原型修改多个实例中的共享属性,但由于对象是使用堆内存进行存储的,变量指针指向对象所属的内存空间,所以下面的这种情况是不会修改实例的共享属性:
function Teacher () {}
Teacher.prototype.system = '软件系'
Teacher.prototype.saySystem = function () { console.log(this.system) }
var teacher1 = new Teacher()
teacher1.saySystem() // 软件系
Teacher.prototype = {
system: '外语系',
studentNumber: 50,
saySystem: function () { console.log(this.system) },
sayStudentNumber: function () { console.log(this.studentNumber) }
}
teacher1.saySystem() // 软件系
teacher1.sayStudentNumber() // TypeError: teacher1.sayStudentNumber is not a function
在上面的代码中,重写了 Teacher
构造函数的 prototype
原型,实际上是让 prorotype
指向了新的内存空间,但创建出来的实例的 __proto__
并不会一起指向该内存空间,这并不只是在原型里是这样子的机制,在普通对象中也是一样。
原型内部修改属性
话不多说,先看两个例子:
// 例子一
function GirlFriend (name) {
this.name = name
}
GirlFriend.prototype = {
features: ['美', '长头发', '皮肤白'],
addFeatures: function (feature) {
this.features.push(feature)
}
}
var girlfriend1 = new GirlFriend('小花')
var girlfriend2 = new GirlFriend('小白')
girlfriend1.addFeatures('腿长')
console.log(girlfriend1.features === girlfriend2.features) // true
console.log(girlfriend1.features) // ["美", "长头发", "皮肤白", "腿长"]
console.log(girlfriend2.features) // ["美", "长头发", "皮肤白", "腿长"]
// 例子二
function GirlFriend (name) {
this.name = name
this.features = ['美', '长头发', '皮肤白']
}
GirlFriend.prototype = {
addFeatures: function (feature) {
this.features.push(feature)
}
}
var girlfriend1 = new GirlFriend('小花')
var girlfriend2 = new GirlFriend('小白')
girlfriend1.addFeatures('腿长')
console.log(girlfriend1.features === girlfriend2.features) // false
console.log(girlfriend1.features) // ["美", "长头发", "皮肤白", "腿长"]
console.log(girlfriend2.features) // ["美", "长头发", "皮肤白"]
上面两个例子中,不同的地方就是 features
的位置,例子一是在原型上定义的,例子二是在构造函数中定义的,定义的位置不同,打印的结果就完全不同。
例子一中,在构造函数的原型定义中就开辟了内存空间存储了数组并用 features
指向该内存,new
的时候只是将两个实例的 features
指向了那个内存,所以调用 girlfriend1
的addFeatures会将 girlfriend2
的也一起修改。
例子二中,内存空间是在调用时才创建的,并让 this.features
指向该内存,两次调用就会创建两次不同的内存,所以调用 girlfriend1
的addFeatures不会修改 girlfriend2
的。

constructor
在上述中,我们了解完了 prototype
和 __proto__
,还有一个关键点就是 constructor
了,constructor
的概念比较简单,它就是原型中的 constructor
指向构造函数,谁创造这个实例的,那么这个实例的 constructor
就是谁,一张图和一段代码了解它们之间的关系。
实例.__proto__ === 构造函数.prototype
prototype.constructor = 构造函数
原型 === 构造函数.prototype
// 实例有__proto__,没有prototype
// 构造函数有prototype
// 构造函数也有__proto__(Object构造函数除外)
function a () {}
var b = new a()
console.log(b.constructor) // ƒ a () {}
console.log(b.__proto__.constructor) // ƒ a () {}
console.log(b.constructor === a) // true

通过本文可以了解到了原型的基本概念,为了不造成阅读疲劳(懒得继续码字了),关于原型和原型链的相关内容分开两篇,下篇将在这周内更新。
