JavaScript在编程语言界是个异类,它和其他编程语言很不一样,JavaScript可以在运行的时候动态地改变某个变量的类型。
比如,你永远也没法想到像isTimeout这样一个变量可以存在多少类型;除了布尔值true和false,它还可能是undefined、1和0、一个时间戳、甚至一个对象。
除了变量可以在运行时被赋值为任何类型外,JavaScript也能实现继承,但它不像JAVA、C++等编程语言一样依靠类来实现继承,而已依靠原型来实现继承。
我们不禁想问为什么?因为JavaScript中有个特殊的存在:对象。每个对象还有一个原型对象,并可以从中继承方法和属性。
提到对象和原型,你是否曾经有过以下这些疑惑:
1.JavaScript的函数怎么也是个对象?
2._proto_和prototype到底是什么关系?
3.JavaScript中对象是怎么实现继承的?
4.JavaScript是怎么访问对象的方法和属性的?
我们依据这些问题,探究JavaScript对象和继承
首先,原型对象和对象是什么关系?
在JavaScript中,对象由一组或多组的属性或值组成;对象的用途很是广泛,因为它的值既可以是原始类型(number、string、boolean、null、undefined、bigint和symbol)还可以是对象和函数。
不管对象、函数还是数组,它们都是Object的实例,除了原始类型以外,其余都是对象。
因此,在JavaScript中,函数也是一种特殊的对象,它同样拥有属性和值,所有的函数会有一个特别的属性prototype,该属性的值是一个对象,这个对象便是我们常说的"原型对象"。
我们在控制台中打印下这个属性,打印结果如下:
可以看到,该原型对象有两个属性,constructor和Prototype;到这里,我们仿佛看到了疑惑二,_proto_和prototype到底是什么关系?
_proto_属性指向对象的原型对象;对于函数来说,它的原型对象便是prototype。函数的原型对象有以下特点
1.默认情况下,所有函数的原型对象prototype都拥有constructor属性,该属性指向与之关联的构造函数,在这里构造函数便是Person函数
2.Person函数的原型对象prototype同样拥有自己的原型对象,用_proto_属性表示
因此Person.prototype的原型对象为Object.prototype
从图中,我们可以知道:
1.对于函数来说,每个函数都有一个prototype属性,该属性为该函数的原型对象
使用prototype和_proto_实现继承
通过将对象A的_proto_属性赋值为对象B
即A._proto_=B
此时使用A._proto_便可以访问B的属性和方法
JavaScript可以在两个对象之间创建一个关联
使得一个对象可以访问另一个对象的属性和方法,从而实现了继承
到此,疑惑3也基本差不多了解了,那么JavaScript又是怎么使用prototype和_proto_实现继承的呢?
继续以Person为例,
var lily = new Person("Lily");
//这条语句其实在JavaScript内部引擎实行了以下代码
var lily = {}
lily._proto_ = Person.prototype
Person.call(lily,"Lily")
根据这张图,我们可以得出:
1.每个函数的原型对象(Person.prototype)都拥有constructor属性,指向该原型对象的构造函数(Person)
2.使用构造函数(new Person())可以创建对象,创建的对象称为实例对象(lily)
3.实例对象通过将_proto_属性指向构造函数的原型对象(Person.prototype),实现了该原型对象的继承
现在关于疑惑2的问题,我们可以得出以下答案:
1.每个对象都有_proto_属性来表示自己所继承的原型对象,但只有函数才有prototype属性
2.对于函数来说,每个函数都有一个prototype属性,该属性为该函数的原型对象
3.通过将实例对象的_proto_属性赋值为其构造函数的原型对象prototype
JavaScript可以使用构造函数创建对象的方式,来实现继承
最后,我们来看问题四,JavaScript是怎么访问对象的方法和属性的?
JavaScript是通过原型链访问对象的方法和属性
JavaScript首先会优先在该对象上搜寻
如果找不到,还会依次层层向上搜索该对象的原型对象、
该对象的原型对象的原型对象等(套娃告警)
接下来,
JavaScript中的所有对象都来自Object
Object.prototype._proto_ = null
null没有原型,并作为这个原型链中的最后一个环节
最后,JavaScript会便利对象的整个原型链
如果最终依然找不到,此时会认为该对象的属性值为undefined
由于通过原型链层层遍历可能带来性能问题,当试图访问不存在的属性时,会遍历整个原型链。在原型链上查找比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。因此,我们在设计对象的时候,要注意原型链的长度。
除了通过原型链的方式实现JavaScript的继承,JavaScript中实现继承的方式还有:
其中,原型链继承方式中引用类型的属性被所有实例共享,无法做到实例私有;
经典继承方式可以实现实例属性私有,但要求类型只能通过构造函数来定义
虽然继承的方式有很多种,但实际上都离不开原型对象与原型链的内容。
随之ES6\ES7等新语法糖的出现,在日常开发中可能更倾向于使用class\extends等语法来编写代码,原型继承等概念逐渐变淡;但不管语法糖如何先进,JavaScript的设计在本质上依然没有变化,依然是基于原型来实现继承的。
原创不易,点赞关注,支持一下吧