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种方式以及它们之间的比较:继承与原型链。
我们可以根据以上简单的栗子看出,所谓的原型也就是个对象。在浏览器控制台中打印一下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) // 打印:undefinedcat1实例里我们看到了在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已经被浏览器“调包”了。