Javascript prototype 原型链,看这一篇就够了

    科技2022-07-12  146

    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已经被浏览器“调包”了。

    一图总结

    Processed: 0.015, SQL: 8