原型和原型链
与大部分面向对象语言不同,JavaScript中并没有引入类(class)的概念,但JavaScript仍然大量地使用了对象,如何将所有对象联系起来就成了一个问题,于是引入了原型(prototype)与原型链(prototype chain)的概念。
原型与构造函数
prototype是所有函数都有的一个属性,且只有函数有, 与创建函数的方式无关
字面量方式常规方式
构造函数方式
我们用不同方式创建了函数, 它们都带有一个默认属性prototype,其值为Object类型
prototype的使用方式?
function Dog(name, color) {
this.name = name
this.color = color
this.bark = function(){
console.log('wang,wang')
}
}
var dog1 = new Dog('dog1', 'black'); console.dir(dog1);
var dog2 = new Dog('dog2', 'white'); console.dir(dog2);
console.log(dog1.bark === dog2.bark);
上述代码就是声明一个构造函数并通过构造函数创建实例的过程,这样看起来似乎有点面向对象的样子了,但实际上这种方法还存在一个很大的问题。(构造函数与普通函数的区别?点击这里)
如图所示,有两个实例被创建,它们有自己的名字、颜色,但它们的bark方法是一样的,而通过构造函数创建实例的时候,每创建一个实例,都需要重新创建这个方法,再把它添加到新的实例中。这无疑造成了很大的浪费,既然实例的方法都是一样的,为什么不把这个方法单独放到一个地方,并让所有的实例都可以访问到呢。
这里就需要用到原型(prototype):
将上述代码稍微做些修改,把bark方法放入Dog构造函数的原型中:
function Dog(name, color) {
this.name = name
this.color = color
}
Dog.prototype.bark = function(){
console.log('wang,wang')
}
var dog1 = new Dog('dog1', 'black'); console.log(dog1);
var dog2 = new Dog('dog2', 'white'); console.log(dog2);
console.log(dog1.bark === dog2.bark); //true
console.log(dog1.bark()); //wang,wang
console.log(dog2.bark()); //wang,wang
可以看到bark方法依然能正常被调用。
prototype的作用是啥?(节省内存开支)
先看下使用prototype后产生的影响
对构造函数本身的影响:
原型对象添加属性
对派生实例的影响:
实例共享原型对象属性
如图所示,两个实例的bark内存地址是相等的,因此我们可以将实例中共有的属性放到原型对象中,让所有实例共享这部分属性,节省内存开支,非共有属性放在构造函数中;
原型链与继承
不管我们给对象定义什么内容,它们总是有一些相同的方法和属性。比如说hasOwnProperty(),toString()等; 这说明一个对象所拥有的属性不仅仅是它本身拥有的属性,它还会从其他对象中继承一些属性。当js在一个对象中找不到需要的属性时,它会到这个对象的父对象上去找,以此类推,这就构成了对象的原型链
所有原型链的终点都是Object函数的prototype属性,因为在JavaScript中的对象都默认由Object()构造。Objec.prototype指向的原型对象同样拥有原型,不过它的原型是null,而null则没有原型
proto是所有对象都有的一个属性,且只有对象有
proto和函数的prototype指向的是同一对象(即:原型对象)
JavaScript中的继承相当灵活,有多种继承的实现方法,这里只介绍一种最常用的继承方法也就是组合继承
function Dog(name, color) {
this.name = name
this.color = color
}
Dog.prototype.bark = () => {
console.log('wangwang~')
}
function Husky(name, color, weight) {
Dog.call(this, name, color)
this.weight = weight
}
Husky.prototype = new Dog()
这里声明了一个新的构造函数Husky,通过call方法继承Dog中的属性,并添加了一个weight属性。然后用Dog函数创建了一个实例作为Husky的原型对象赋值给Husky.prototype以继承方法。这样,通过Husky函数创建的实例就拥有了Dog中的属性和方法。
总结
- 每一个构造函数都拥有一个prototype属性,这个属性指向一个对象,也就是原型对象。当使用这个构造函数创建实例的时候,prototype属性指向的原型对象就成为实例的原型对象。
- 原型对象默认拥有一个constructor属性,指向它的那个构造函数(也就是说构造函数和原型对象是互相指向的关系)。
- 每个对象都拥有一个隐藏的属性[[prototype]],指向它的原型对象,这个属性可以通过
Object.getPrototypeOf(obj) 或 obj.__ proto __ 来访问。 - 实际上,构造函数的prototype属性与它创建的实例对象的[[prototype]]属性指向的是同一个对象,即 对象.__ proto __ === 函数.prototype 。
- 如上文所述,原型对象就是用来存放实例中共有的那部分属性。
- 在JavaScript中,所有的对象都是由它的原型对象继承而来,反之,所有的对象都可以作为原型对象存在。
- 访问对象的属性时,JavaScript会首先在对象自身的属性内查找,若没有找到,则会跳转到该对象的原型对象中查找。
如果想要深入了解关于JavaScript中的对象和原型链的话,无脑推荐红宝书(《JavaScript高级程序设计(第3版)》)吧,第六章关于原型链有相当详细的讲解。