JavaScript 也是一门面向对象的语言,ES6之前并没有引入类(class)的概念,像c++ 这种典型的面向对象语言都是通过类来创建实例对象,而JavaScript是直接通过构造函数来创建实例。
所以理解两种继承模式的差异是需要一定时间的,今天我们就来了解一下原型和原型链,在介绍原型和原型链之前,我们有必要先了解一下构造函数的知识。
构造函数
构造函数模式的目的就是为了创建一个自定义类,并且创建这个类的实例。
构造函数就是一个普通的函数,创建方式和普通函数没有区别,不同的是构造函数习惯上首字母大写。另外就是调用方式的不同,普通函数是直接调用,而构造函数需要使用new关键字来调用。我们先使用构造函数创建一个对象:
function Dog() {
this.name = '阿黄'
}
var dog = new Dog()
console.log(dog.name) // 阿黄
复制代码
上面例子中,Dog 就是一个构造函数,我们使用 new 创建了一个实例对象 dog。
原型
prototype
JavaScript是一种基于原型的语言(prototype-based language),每个对象拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的prototype
属性上,而非对象实例本身。看以下代码:
function Dog() {
this.name = '阿黄'
}
console.log(Dog.prototype)
复制代码
那这个构造函数的 prototype
属性指向的是什么呢?是这个函数的原型吗?
打开 chrome 浏览器的开发者工具,在 console 栏输入上面的代码,你可以看到 Dog.prototype
的值:
其实,函数的 prototype
属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型。
那什么是原型呢?你可以这样理解:每一个JavaScript对象(null
除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。
让我们用一张图来表示构造函数和实例原型之间的关系:
那么我们该怎么表示实例与实例原型,也就是 dog
和 Dog.prototype
之间的关系呢,接下来就应该讲到第二个属性:
proto
上面可以看到 Dog 原型(Dog.prototype
)上有__proto__
属性,这是一个访问器属性(即 getter 函数和 setter 函数),通过它可以访问到对象的内部[[Prototype]]
(一个对象或null
)。
为了证明这一点,我们可以在chrome中输入:
function Dog() {
this.name = '阿黄'
}
var dog = new Dog()
console.log(Object.getPrototypeOf(dog) === dog.__proto__) // true
console.log(dog.__proto__ === Dog.prototype) // true
复制代码
这里用dog.__proto__
获取对象的原型,__proto__
是每个实例上都有的属性,prototype
是构造函数的属性,这两个并不一样,但dog.__proto__
和Dog.prototype
指向同一个对象。于是我们更新下关系图:
既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?
constructor
指向实例对象倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性:constructor
,每个原型都有一个 constructor
属性指向关联的构造函数。
为了验证这一点,我们在chrome中输入:
function Dog() {
this.name = '阿黄'
}
console.log(Dog.prototype.constructor === Dog) // true
复制代码
所以再更新下关系图:
综上我们已经得出:
function Dog() {
this.name = '阿黄'
}
var dog = new Dog()
console.log(dog.__proto__ == Dog.prototype) // true
console.log(Dog.prototype.constructor == Dog) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(dog) === Dog.prototype) // true
复制代码
原型链
在上文我们理解了原型,从字面意思看原型链肯定是与原型有关了,是一个个原型链接起来的么?我们先通过下面的图来观察一下。
解析:
obj.prop1:假设我们现在有一个对象,就称作obj
,而这个对象包含一个属性(property)
,我们称作prop1
,现在我们可以使用obj.prop1
来读取这个属性的值,就可以直接读取到prop1
的属性值了。
obj.prop2:JavaScript中会有一些预设的属性和方法,所有的对象和函数都包含prototype
这个属性,假设我们把prototype
叫做proto
,这时候如果我们使用obj.prop2
的时候,JavaScript引擎会先在obj
这个对象的属性里去寻找有没有叫作prop2
的属性,如果它找不到,这时候它就会再进一步往该对象的proto
里面去寻找。所以,虽然我们输入obj.prop2
的时候会得到回传值,但实际上这不是obj
里面直接的属性名称,而是在obj
的proto
里面找到的属性名称(即,obj.proto.prop2
,但我们不需要这样打)。
obj.prop3:同样地,每一个对象里面都包含一个prototype
,包括对象proto
本身也不例外,所以,如果输入obj.prop3
时,JavaScript会先在obj
这个对象里去寻找有没有prop3
这个属性名称,找不到时会再往obj
的proto
去寻找,如果还是找不到时,就再往proto
这个对象里面的proto
找下去,最后找到后回传属性值给我们(obj.proto.proto.prop3
)。
虽然乍看之下,prop3
很像是在对象obj
里面的属性,但实际上它是在obj → prop → prop
的对象里面,而这样从对象本身往proto
寻找下去的链我们就称作「原型链(prototype chain)」。这样一直往下找会找到什么时候呢?它会直到某个对象的原型为null
为止(也就是不再有原型指向)。
官方解释是:每个对象拥有一个原型对象,通过__proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向null
。这种关系被称为原型链 (prototype chain),通过原型链一个对象会拥有定义在其他对象中的属性和方法。
举个例子来帮助理解原型链
让我们实际来看个例子帮助我们了解prototype chain这个概念,这个例子只是单纯为了用来说明prototype chain的概念,实际上千万不要使用这样的方式编程!
首先,我们先建立一个对象person
和一个对象jay
:
var person = {
firstName : 'Default' ,
lastName : 'Default' ,
getFullName : function ( ) {
return this . firstName + ' ' + this . lastName ;
} ,
} ;
var jay = {
firstName : 'Jay' ,
lastName : 'Chou' ,
} ;
复制代码
接着,我们知道所有的对象里面都会包含原型(prototype)这个对象,在JavaScript中这个对象的名称为__proto__。如同上述原型链(prototype chain)的概念,如果在原本的对象中找不到指定的属性名称或方法时,就会进一步到__proto__
这里面来找。
为了示范,我们来对__proto__
做一些事:
//千万不要照着下面这样做,这么做只是为了示范
jay . __proto__ = person ;
复制代码
如此,jay
这个对象就继承了person
对象。在这种情况下,如果我们想要呼叫某个属性或方法,但在原本jay
这个对象中找不到这个属性名称或方法时,JavaScript引擎就会到__proto__
里面去找,所以当接着执行如下的代码时,并不会报错:
console . log ( jay . getFullName ( ) ) // Jay Chou;
复制代码
我们可以得到"Jay Chou"的结果。原本在jay
的这个对象中,是没有getFullName()
这个方法的,但由于我让__proto__
里面继承了person
这个对象,所以当JavaScript引擎在jay
对象里面找不到getFullName()
这个方法时,它便会到__proto__
里面去找,最后它找到了,于是它回传"Jay Chou"的结果。
如果我是执行:
console . log ( jay . firstName ) ; // Jay
复制代码
我们会得到的是John而不是'Default',因为JavaScript引擎在寻找jay.firstName
这个属性时,在jay
这个对象里就可以找到了,因此它不会在往__proto__
里面找。这也就是刚刚在上面所的原型链(prototype chain)的概念,一旦它在上层的部分找到该属性或方法时,就不会在往下层的prototype去寻找。
在了解了prototype chain这样的概念后,让我们接着看下面这段代码:
var jane = {
firstName : 'Jane'
}
jane . __proto__ = person ;
console . log ( jane . getFullName ( ) ) ;
复制代码
现在,你可以理解到会输出什么结果吗?
答案是"Jane Default" 。
因为在jane
这个对象里只有firstName
这个属性,所以当JavaScript引擎要寻找getFullName()
这个方法和lastName
这个属性时,它都会去找__proto__
里面,而这里面找到的就是一开始建立的person
这个对象的内容。
全代码如下:
var person = {
firstName : 'Default' ,
lastName : 'Default' ,
getFullName : function ( ) {
return this . firstName + ' ' + this . lastName ;
}
}
var jay = {
firstName : 'Jay' ,
lastName : 'Chou'
}
//千万不要照着下面这样做,这么做只是为了示范
jay . __proto__ = person ;
console . log ( jay . getFullName ( ) ) ; // Jay Chou
console . log ( jay . firstName ) ; // Jay
var jane = {
firstName : 'Jane'
}
jane . __proto__ = person ;
console . log ( jane . getFullName ( ) ) ;
复制代码
以上就是目前能总结的全部了,肯定还是有缺陷的地方,后续还会修改完善的。最后再看底下这张图,是否有了更深入的理解呢?
如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!