《JavaScript的那些事》之原型与原型链(上篇)

前言

原型和原型链在JavaScript的一个核心内容,它用于对象之间的属性继承,在面试的过程当中也会常常会问到这部分的知识,若是接触过像Java这类的语言,并且只是对这个概念只知其一;不知其二的话,估计只能全靠猜,因此掌握原型和原型链是进阶前端的一个重要关键点。这里小编将从函数对象构造函数实例newprototype__proto__contructorclass 这八个知识点来探索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 建立的 teacher1teacher2 两个实例,在建立后访问内部属性 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
复制代码

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

相关文章
相关标签/搜索