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除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型”继承”属性。

让我们用一张图来表示构造函数和实例原型之间的关系:

那么我们该怎么表示实例与实例原型,也就是 dogDog.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里面直接的属性名称,而是在objproto里面找到的属性名称(即,obj.proto.prop2,但我们不需要这样打)。

obj.prop3:同样地,每一个对象里面都包含一个prototype,包括对象proto本身也不例外,所以,如果输入obj.prop3时,JavaScript会先在obj这个对象里去寻找有没有prop3这个属性名称,找不到时会再往objproto去寻找,如果还是找不到时,就再往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博客点赞和关注,感激不尽!



JavaScript      JavaScript 原型 原型链

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!