原型和原型链在JavaScript的一个核心内容,它用于对象之间的属性继承,在面试的过程当中也会常常会问到这部分的知识,若是接触过像Java这类的语言,并且只是对这个概念只知其一;不知其二的话,估计只能全靠猜,因此掌握原型和原型链是进阶前端的一个重要关键点。这里小编将从函数对象
、构造函数
、实例
、new
、prototype
、__proto__
、contructor
、class
这八个知识点来探索JavaScript的 原型 和 原型链。javascript
函数对象:使用
function
关键字或使用Function
构造函数建立的对象即为函数对象。前端
“万物皆对象”,在JavaScript中,函数是一个特殊的对象,它能够像普通对象那样子设置以及访问自身的属性,例如:java
// 普通对象
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
的,看下面的例子:ui
var a = {}
var b = function () {}
function c () {}
console.log(a.prototype) // undefined
console.log(b.prototype) // { constructor: ƒ }
console.log(c.prototype) // { constructor: ƒ }
复制代码
prototype
是用来干吗的?经过上文咱们了解到什么是实例,可是每次经过构造函数建立的实例都是不同的,若是想让多个实例之间具备共享属性的话,仅靠构造函数是不够的。this
在 ECMAScript 设计的时候,并无像Java那样子设计成类的概念,而是经过构造函数的 prototype
来实现对象之间的共享属性,看下面的例子:spa
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
属性。prototype
__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
复制代码
经过本文能够了解到了原型的基本概念,为了避免形成阅读疲劳(懒得继续码字了),关于原型和原型链的相关内容分开两篇,下篇将在这周内更新。