部分资料来源 Javascript继承机制的设计思想-阮一峰、《JavaScript权威指南》
笔者试图用自己的话来阐述JS中的原型和继承,彻底理解这部分概念。
首先回顾一下构造函数的用法
function Cat(name) {
this.name = name;
}
let tom = new Cat('tom')
现在如果要给两个cat都加上一个共有的属性species,最简单的做法当然是在构造函数内增加这个属性。
function Cat(name) {
this.name = name;
this.species = '猫科'
}
let tomA = new Cat('tom')
let tomB = new Cat('tom')
这样当然可以实现,但是如果我们修改tomA上的specie,tomB上的species会被修改吗?答案当然是不会的,所以我们需要一个设计,能够放置共享的属性和方法。考虑到这一点,JS的设计者引入了prototype。
prototype
用prototype实现共有的属性
function Cat(name) {
this.name = name;
}
Cat.prototype.species = '猫科'
let tomA = new Cat('tom')
let tomB = new Cat('tom')
根据《JavaScript权威指南》中的释义:
每一个JavaScript对象(null除外)都和另一个对象有关联。”另一个“对象就是我们所熟知的原型,每一个对象都从原型继承属性。
结合上面的代码来理解的话,tomA和tomB是构造函数产生的实例对象,实例对象就会和原型相关联,这个原型就是Cat.prototype
所以对象中的属性就会包括两种类型,一种是本地的,例如tomB.name, 一种是引用的,也就是来自原型上的对象,例如tomB.species。
这里要注意的是,prototype是构造函数上的属性,而不是创建的实例对象上的属性,那么实例对象上是怎么引用原型的呢?
__proto__
__proto__是每个JavaScript对象上的一个属性,指向该对象的原型对象。
console.log(cat.__proto__ === Cat.prototype); // true
console.log(Cat.prototype.__proto__ === Object.prototype); // true
实例对象就是通过 __proto__ 这个属性指向构造函数的prototype。
原型链
从上述来看,实例对象上的属性继承自构造函数的原型,也就是prototype上,而构造函数的prototype也是一个对象,这个对象的属性是继承自Object.prototype
所以在寻找对象上的属性时,会按照这个顺序进行查找,直到找到这个属性。
Object.prototype是少数没有原型的对象,它不继承任何属性,所以原型链到这里就终止了。
结合上面的例子,可以认为,tomA继承了Cat.prototype 和 Object.prototype,这一系列链接的原型对象就是所谓的 ”原型链“。
new 关键字
结合上述,可以认识到,new一个对象的步骤即为:
- 创建一个新的空对象;
- 将新对象的 __proto__ 属性指向构造函数的 prototype 属性;
- 将构造函数的 this 指向新对象;
- 执行构造函数内部的代码,并给新对象添加属性和方法;
- 如果构造函数有返回值,并且返回值是对象类型,则返回该对象;否则返回新对象。
注意
最后需要注意的一点是,虽然我们经常使用 __proto__ 来访问对象的原型,但是它并不是标准的 JavaScript 属性。实际上,它是通过浏览器厂商所提供的非标准扩展实现的。在标准中,我们应该使用 Object.getPrototypeOf() 方法来访问对象的原型。而 prototype 是标准的 JavaScript 属性,它指向构造函数的原型对象,用于定义构造函数所创建的对象的共享属性和方法。