我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象, 而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以 让所有对象实例共享它所包含的属性和方法。
首先来了解一下原型对象
原型对象
在创建任何一个函数f()
的时候,都会自动生成创建一个对象,并将这个对象的指针赋值给函数的prototype
属性。同时也会给这个对象添加一个属性叫constructor
,内容是指向f()
的指针。我们称这个对象为函数f()
的原型对象。原型对象的属性默认只有一个constructor
,其余的属性或方法都继承自Object
。
原型对象的constructor
指向f()
而f()
的prototype
又指向原型对象。也就是说我们可以得到下面的表达式
f.prototype.constructor === f
这个时候,如果我们使用f()
来构造一个对象o
时,o
会默认获得一个[[prototype]]
属性,内容为一个指向构造函数原型对象的指针。也就是说
o.[[prototype]] === f.prototype
实际上,我们并不能直接通过o.[[prototype]]
来访问o
的[[prototype]]
属性,因为它是一个内部属性,这里只是为了方便理解。那么我们怎么能够知道某个对象与构造函数间的原型关系呢?我们可以通过isPrototypeOf()
方法来判断对象的[[prototype]
是不是指向目标构造函数的原型对象。
f.prototype.isPrototypeOf(o); // true
我们还可以通过__proto__
以及ES5以后的getPrototypeOf()
方法来直接获取[[prototype]]
的值。
总结一下原型对象与实例、构造函数的关系,可以得到下面这张经典的关系图。
其中Person
是构造函数,person1
和person2
则是实例化出来的对象。
原型对象的妙用
第一,原型一个非常大的用处就是实现了JavaScript的继承,以一种内存占用最小的方式。第二,在使用构造函数创建对象的时候,不同对象的相同方法却需要重复new Function()
,重复占用内存。而原型则可以避免这一问题。
还是先来一个构造函数JOJO()
,但是这一次,我们通过修改JOJO的原型来为其添加属性与方法。
function JOJO(_name) {
this.name = _name;
}
JOJO.prototype.age = 18;
JOJO.prototype.say = function(msg){
console.log(msg);
};
实例化一个对象,并打印一下jotaro
和jotaro.age
:
let jotaro = new JOJO('jotaro');
console.log(jotaro);
console.log(jotaro.age);
得到结果:
很合理,却又很奇怪。打印jotaro.age
得到18很合理,因为这是我们定义的。而打印jotaro
的时候,却只有一个name
属性,很奇怪。
这里就体现了原型的继承属特性。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到, 则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。
在这里,我们读取jotaro.age
的时候,首先查找jotaro
这个对象本身有没有age
属性,一看,没有。就沿着[[prototype]]
来找到其原型对象,发现他的原型对象是有age
属性的,于是就返回jotaro.__proto__.age
。
所以我们上面打印jotaro.age
的语句改为:
console.log(jotaro.__proto__.age);
效果也是一样的。不过我们并不需要这么做,因为JS会自动帮我们寻找,这就是原型的一大好处。
这里我提出一个小问题:
function JOJO(_name) {
this.name = _name;
}
JOJO.prototype.age = 18;
JOJO.prototype.say = function(msg){
console.log(msg);
};
let jotaro = new JOJO('jotaro');
let jostar = new JOJO('jostar');
jostar.age = 68;
console.log(jotaro.age);
请问这个时候的控制台会打印什么呢?
答案是
怎会如此?!我的jostar
对象应该是没有age
属性的,按照继承规则,我应该修改的是原型对象的age
属性才对呀。如果我修改了原型对象的age
,那么同样没有age
属性的jotaro
对象应该输出的也是他们共同的原型对象的age
也就是68才对。
然而真实情况其实是
jostar
对象确实没有age
属性。那么JS在执行jostar.age = 68
时,实际上是在给jostar
对象定义一个age
属性并赋值18,此时我们分别打印jostar
和jotaro
应该可以看出来,jostar
对象会比jotaro
多出一个age
属性来。
所以,我们直接修改jostar.age
是并不能影响到原型对象的。现在我们将代码修改为
function JOJO(_name) {
this.name = _name;
}
JOJO.prototype.age = 18;
JOJO.prototype.say = function(msg){
console.log(msg);
};
let jotaro = new JOJO('jotaro');
let jostar = new JOJO('jostar');
jostar.__proto__.age = 68;
console.log(jotaro.age);
这个时候会得到什么呢?答案留给你们自己去探索。
再回到第二个问题就显得轻而易举了。我们通过定义原型对象的方法,而不直接定义对象方法,避免重复生成函数对象。调用时直接寻找原型中的方法来实现。
现在你应该可以很确定得说出下面这段代码的结果了吧
function JOJO(_name) {
this.name = _name;
}
JOJO.prototype.age = 18;
JOJO.prototype.say = function(msg){
console.log(msg);
};
let jotaro = new JOJO('jotaro');
let jostar = new JOJO('jostar');
console.log(jotaro.say === jostar.say);
console.log(jotaro.say === jostar.__proto__.say);
console.log(jostar.say === jotaro.__proto__.say);
没错,答案就是三个true
。
小结
感觉已经找不到比JS还基于对象的语言了。Array是对象,Number是对象,就连对象也是对象函数也是对象。
JS真是太美妙了(确信
貌似工作中很少用到这底层的方法了,上vue基本不使用到
yes! vue真的是很香,不过学习讲究个知其然知其所以然嘛,还是要研究研究一些基础
确实,不错