这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战
原型和原型链的有关知识在前端的面试题中会经常被问到,这个问题其实有非常多细节点,所以笔者想通过一篇文章整理其中的知识点,分享给大家。
在JavaScript语言中,类使用基于原型的继承,如果两个对象从同一个原型继承属性,那么我们就说这些对象是同一个类的实例。在ES6以前,JavaScript创建类通常是使用工厂函数或者构造函数的方式。ES6新增了相关语法(包括class关键字),才让创建类的方式变得简单,但其原理是不变的,可以把ES6中的类当作是构造函数加原型继承的语法糖,了解这些创建类的方式,可以让我们更加了解类的底层工作原理
定义类
-
工厂函数
在JavaScript中,类意味着一组对象从同一个原型对象中继承属性,这些属性在所有对象中是共享的。JavaScript提供了Object.create() 函数用于创建一个新的对象,这个新对象继承指定的原型对象。通常我们定义一个类的时候,都需要初始化一些状态,而这些状态是每个对象独有的,所以常见的做法便是定义一个工厂函数,用于创建和初始化一个类的新实例
const personMethod={
getName(){
return `${ this.firstName} ${this.lastName}`
}
}
function person(firstName,lastName,gender){
const p=Object.create(personMethod)
p.firstName=firstName
p.lastName=lastName
p.gender=gender
return p
}
const p1=person('Li','lumy','female')
console.log(p1.getName())//Li lumy
在上面的例子中,我们创建了一个类person,并且通过调用这个函数,传入三个参数,得到了一个person类的实例p1,这个实例继承了personMethod对象的属性,因此拥有getName的方法。
-
构造函数
构造函数是一种专门用于初始化新对象的函数。构造函数需要使用new关键字调用。使用new调用构造函数会自动创建新对象,并将构造函数作为该对象的方法调用,最后返回该对象。因此,构造函数本身只需要通过this关键字访问新对象,初始化新对象的状态。那么通过构造函数创建的对象又是如何继承属性的呢?其实构造函数调用的关键在于构造函数的prototype属性将被用作新对象的原型,这意味着用同一构造函数所创建的对象都继承同一个原型对象,下面我们来改造一下上面的例子
function Person(firstName,lastName,gender){
this.firstName=firstName
this.lastName=lastName
this.gender=gender
}
Person.prototype={
getName(){
return `${ this.firstName} ${this.lastName}`
}
}
const p1=new Person('Li','lumy','female')
console.log(p1.getName())//Li lumy
可以看到,使用构造函数创建得到的对象与工厂函数创建对象的效果是一样的。
比较一下两者的区别:
- 命名区别,构造函数某种意义上是定义类,而类名按照习惯是以大写字母开头,普通函数和方法则以小写字母开头;
- 调用方式不同,
Person构造函数需要以new关键字调用,而person工厂函数则不需要; - 指定原型对象的方式不同,在工厂函数中,对象的原型使用自定义命名的方式,并通过
Object.create指定原型来创建对象,而通过构造函数创建的对象原型是构造函数的prototype属性,是强制性的
原型
刚刚我们了解了如何去定义一个类,类又是如何从一个原型中继承属性,下面我们来聊一下什么是原型。
几乎每一个JavaScript对象都有另一个与之关联的对象,而这另一个对象被称为原型(prototype),第一个对象从这个原型中继承属性。
我们通常会通过字面量的方式去创建一个对象,事实上,这种方式与通过new Object() 方法创建对象是一样的,我们可以看到,这是一种通过构造函数创建对象的形式,所以我们通过字面量创建的对象会自动继承Object.prototype 的属性。类似的,new Array()创建的对象以Array.prototype为原型 ,new Date() 创建的对象以Date.prototype为原型......几乎所有对象都有原型,但只有少数对象有prototype属性。
那么,哪些对象有prototype属性呢?答案其实很明显,只有函数对象才有prototype属性,我们前面讲的,构造函数的prototype属性将被用作新对象的原型。
只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象),默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数,在上面的例子中,Person.prototype.constructor指向Person。
在自定义构造函数时,原型对象默认只会获得constructor属性,其他的所有属性都会继承自Object。每次调用构造函数创建的对象时,这个对象内部会有一个**[[Prototype]]**指针指向构造函数的原型,在脚本中没有直接访问这个指针的方式,但在FireFox、Safari、Chrome浏览器中会给每个对象上面暴露一个__proto__属性来访问,在其他实现中则会被完全隐藏。至此我们可以看出:实例对象与构造函数之间没有直接联系,但实例对象与构造函数的原型对象通过[[Prototype]]指针联系起来,我们可以通过简单的例子了解它们之间的关系
function Person(){}
const p=new Person()
console.log(typeof Person.prototype)//object
console.log(Person.prototype)//{constructor: ƒ Person(),__proto__: Object}
console.log(p.__proto__===Person.prototype)//true
console.log(Person.prototype.constructor===Person)//true
从上面的例子可以看出,Person构造函数在声明之后就有了一个与之关联的原型对象Person.prototype,p是Person的实例对象,p的__proto__指针指向Person.prototype原型对象,而原型对象的constructor属性指回构造函数Person
我们再来观察一下
console.log(Person.prototype.__proto__===Object.prototype)//true
console.log(Person.prototype.__proto__)//{constructor: ƒ Object(),hasOwnProperty: ƒ hasOwnProperty(),isPrototypeOf: ƒ isPrototypeOf(),.....}
console.log(Person.prototype.__proto__.__proto__===null)//true
Person的原型对象的__proto__指针又指向Object.prototype,说明Person.prototyp是Object的实例对象,因此我们可以把Person.prototype看作是通过new Object(constructor,__proto__)创建而来,其中constructor,__proto__是这个实例的特定属性,分别指向Person和Object.prototype
从上面的例子中还可以看到,Person.prototype.__proto__.__proto__===null为true,我们知道Person.prototype.__proto__指向的是Object.prototype,因此可以看作Object.prototype.__proto__===null,这说明Object.prototype是没有原型的对象,它不继承任何属性。
JavaScript中所有的对象都会从Object.prototype中继承属性,它是所有对象的原型。其他原型对象都是常规对象,都有自己的原型。例如Array.prototype是数组的原型对象,它是一个常规的对象,而常规的对象的原型是从Object.prototype继承的,因此,通过new Array() 创建的对象可以从Array.prototype和Object.prototype继承属性。这种原型对象链接起来的序列被称为原型链
原型链
JavaScript中的对象是通过原型链继承的,其基本思想是通过原型继承多个引用类型的属性和方法。我们来梳理一下构造函数、原型、和实例的关系:每个构造函数都有一个原型对象,原型有一个属性(constructor)指回构造函数,而实例有一个内部指针([[Prototype]])指向原型。
那如果原型又是另一个类型的实例呢?那其实原理还是一样的,这个原型的一个内部指针会指向另一个原型,另一个原型的constructor属性指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。
我们可以通过一段代码实现这个原理
function AniAmal(){
this.property='animal'
}
Animal.prototype.getProperty=function(){
return this.property
}
function Cat(){
this.catProperty='cat'
}
Cat.prototype=new Animal()
Cat.prototype.getCatProperty=function(){
return this.catProperty
}
const cat=new Cat()
console.log(cat.getProperty())//animal
console.log(cat.getCatProperty())//cat
console.log(cat.hasOwnProperty('catProperty'))//true
从上面的例子我们可以看到cat是Cat类型的实例,因此它继承了Cat.prototype的属性,它可以调用getCatProperty方法,因为Cat.prototype又是Animal的实例,cat又可以继承了Animal实例的属性,所以cat也可以调用getProperty方法。
那为什么cat实例又拥有hasOwnProperty方法呢?因为任何函数的默认原型是Object的实例,AniAmal的实例会默认继承Object.prototype的属性,因此,cat实例会拥有包括tostring、valueOf、hasOwnProperty等所有默认方法。
为什么是AniAmal的实例继承Object.prototype的属性呢?因为Cat.prototype被改写成为AniAmal的实例,cat能够拥有这些默认方法是因为从AniAmal的实例中继承。
下面我们通过一张图来总结原型链的基本原理
每一个JavaScript对象都有自己的原型对象,其内部通过一个[[Prototype]]指针指向原型对象,而原型对象通过constrctor属性指向其构造函数,构造函数的prototype属性指向原型对象,原型链就是相互连接关系的不断循环,最终到达Object.prototype,它是原型链的终点