JS本身不是面向对象语言,并没有类的支持。这不妨碍很多人对面向对象的热爱。因此,在ES2015之前,我们经常看到这样创建实例:
function Pet(species) {
this.species = species;
}
Pet.prototype.getSpecies = function() { return this.species; }
var cat1 = new Pet('cat');
console.log(cat1.getSpecies()); // 结果:"cat"
再用prototype
(原型)模拟继承:
function Cat(color) {
this.color = color;
}
Cat.prototype = new Pet('cat');
Cat.prototype.getColor = function() { return this.color; }
var blackCat = new Cat('black');
console.log(blackCat.getSpecies()); // 结果:"cat"
console.log(blackCat.getColor()); // 结果:"black"
除此之外还有好几种方式可以模拟继承的操作,详情参见这篇有意思的博文:JavaScript常用八种继承方案。此外,MDN上也有4种方式以及它们之间的比较:继承与原型链。
prototype原型
我们可以根据以上简单的栗子看出,所谓的原型
也就是个对象。在浏览器控制台中打印一下Pet.prototype
可以看到如下内容:
{getSpecies: ƒ, constructor: ƒ}
再打印一下Pet.prototype.constructor
:
ƒ Pet(species) {
this.species = species;
}
原来在声明函数但没有指定继承对象的时候,Javascript创建了一个原型对象,并且把该函数充作了构造函数。
这个隐式属性是非实例对象才有的,不信我们打印一下cat1
:
Pet {species: "cat"}
species: "cat"
__proto__:
getSpecies: ƒ ()
constructor: ƒ Pet(species)
__proto__: Object
另外除了函数(包括Object()
、Array()
这些构造函数)、以及特殊值如undefined
, null
等其他值对象都可以考虑是某个构造函数的实例。
如NaN
是number
类型的,它实际上是Number()
的实例。因此没有prototype
属性:
console.log(NaN.prototype) // 打印:undefined
__proto__
属性
cat1
实例里我们看到了在Pet.prototype
的构造函数中添加的自身属性species
,因为表明实例化过程中调用了构造函数。
此外,还有一个陌生的__proto__
属性,而这个属性的值正是我们刚刚打印过的Pet.prototype
。也就是说**实例对象有个__proto__
属性,它指向实例对象的构造函数constructor
的原型prototype
对象。**不过注意这个__proto__
其实已经被标准弃用多年,但许多浏览器仍在沿用的属性。注意现在标准推荐的是Object.getPrototypeOf()
方法。
由此可以推断出子类blackCat
的__proto__
属性值是Cat.prototype
,即一个Pet
实例。
然而这并不适用于所有对象,比如{}
:
console.log(({}).__proto__ === ({}).__proto__.constructor.prototype); // 打印:true
console.log(({}).__proto__ === ({}).__proto__.__proto__); //打印:false
console.log(({}).__proto__.__proto__); // 打印:null
关于这个null
是怎么回事,让我们在下一部分“原型链”中解释。
原型链
你大概已经能猜到原型链是什么了——一个可以追溯对象原型的链表。
通过__proto__
可以追溯构造函数的原型。浏览器便是利用__proto__
追溯寻找一个属性的。让我们试试:
console.log(cat1.__proto__);
/*
{getSpecies: ƒ, constructor: ƒ}
getSpecies: ƒ ()
constructor: ƒ Pet(species)
__proto__: Object
*/
console.log(cat1.__proto__.__proto__);
/*
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
*/
console.log(cat1.__proto__.__proto__.__proto__);
/*
null
*/
console.log(cat1.__proto__.__proto__.__proto__);
/*
VM2565:1 Uncaught TypeError: Cannot read property '__proto__' of null
at <anonymous>:1:36
*/
这就到头了。
首先我们可以看到从函数new
出来的实例变成了Object
类型,而Object()
构造函数的原型是null
,原型链到null
为止。
这__proto__
为null
的对象,称为原子对象,也就是最小单位——Object
。
像Array
和Number
这些都是由Function
创建而来,而Function
从Object
创建而来。这里不得不提到另外一个有趣的发现:
console.log(Function.__proto__.__proto__.__proto__); // 打印: null
console.log(Function.__proto__.constructor); // Chrome, Safari打印:ƒ Function() { [native code] }
console.log(Pet.__proto__.__proto__.__proto__); // 打印: null
这跟预想的不同。。。看起来我们直接能调用的Function
已经被浏览器“调包”了。