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

1,654 阅读7分钟

前言

原型和原型链在JavaScript的一个核心内容,它用于对象之间的属性继承,在面试的过程中也会经常会问到这部分的知识,如果接触过像Java这类的语言,而且只是对这个概念一知半解的话,估计只能全靠猜,所以掌握原型和原型链是进阶前端的一个重要关键点。这里小编将从函数对象构造函数实例newprototype__proto__contructorclass 这八个知识点来探索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 创建的 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

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