原型
JavaScript 只有对象,而没有类,但是又需要支持面向对象编程,那么该怎么办呢?我们可以使用构造函数来模拟类。
function Human(name) {
this.name = name;
this.speakName = function(){
console.log(this.name);
}
}
var humanA = new Human('A');
var humanB = new Human('B');
其实 new
指令相当于执行了以下操作
function createNewObject(constructor, agruments){
var obj = {};
constructor.apply(obj, agruments);
return obj;
}
也就是执行了以下三个步骤:
- 先创建一个空对象
- 将构造函数的作用域赋给该空对象,并且执行构造函数
- 返回该对象
但是构造函数有一个问题,那就是每一次创建的对象的属性都是私有的。
function Human(name) {
this.name = name;
this.speakName = function(){
console.log(this.name);
}
}
var humanA = new Human('A');
var humanB = new Human('B');
humanA.speakName === humanB.speakName // false
这样会导致内存的浪费,所以我们可以通过设置一个公有对象的方式,将所有 Human 对象的共有成员放进这个共有对象中,而这个共有对象,就是原型。
- 每个函数对象都有一个
prototype
引用指向该对象。 - 而每个
prototype
对象会有一个constructor
引用指回其的构造函数对象 - 每个对象都有
_proto_
属性指向 它们的构造函数中的prototype
对象
function Human(name) {
this.name = name;
}
Human.prototype.speakName = function () {
console.log(this.name);
}
var humanA = new Human('A');
var humanB = new Human('B');
humanA.__proto__ === Human.prototype // true
humanB.__proto__ === Human.prototype // true
humanA.speakName(); // A
humanB.speakName(); // B
humanA.speakName === humanB.speakName // true
当执行 humanA.speakName()
时,JS会先从 humanA
自己的属性里去寻找 speakName
, 找不到就去它的原型对象,也就是 humanA.__proto__
里寻找。另外, Object.keys
是无法获取到原型里的属性的。
Obeject.keys(humanA) // ["name"]
经过测试,在chrome中,用 for in
还是能够遍历出原型中的属性,所以可以加个条件判断
var keys = [];
for (var key in humanA) {
if (humanA.hasOwnProperty(key)) {
keys.push(key);
}
}
keys // ['name']
当然,你可以通过 Object.defineProperty
来设置原型中的属性禁止被枚举
function Human(name) {
this.name = name;
}
Object.defineProperty(Human.prototype, 'speakName', {
value:function () {
console.log(this.name);
},
enumerable: false
});
var humanA = new Human('A');
humanA.speakName(); // A
const keys = [];
for (var key in humanA) {
keys.push(key);
}
keys // ['name']
解决了共有属性的问题,在面向对象的语言中,我们还需要考虑继承的问题。这种时候,原型链就派上用场了。
原型链
如何实现一个继承呢?其实很急单,那就是让子类的原型等于父类的一个实例, 同时再加上子类原型所需要的一些特性就可以了。
function Animal() {
this.size = ['1','2','3'];
}
Animal.prototype.eat = function (){
console.log('eat');
}
function Human(name) {
this.name = name;
}
Human.prototype = new Animal();
Human.prototype.speakName = function () {
console.log(this.name);
}
var humanA = new Human('A');
humanA.eat(); // eat
humanA.eat === humanA.__proto__.__proto__.eat // true
正像上面的代码所显示的,当执行 humanA.eat()
时,JS在 humanA
自己的属性中没找到 eat
函数,那么就会在它的原型中找,如果还没找到,那么就会往上,去继承的原型链里找。
但是这样有一个问题,那就是父类的非共有属性也包含再子类的原型中了。
var humanA = new Human('A');
var humanB = new Human('B');
humanA.size === humanB.size // true
humanA.__proto__.size === humanB.__proto__.size // true
humanA.size.push('4');
humanB.size // ['1','2','3','4'];
其实也很简单,我们只需要在子类的构造函数中,调用一下父类的构造函数(将作用域指向子类代表的作用域)。
...
function Human(name) {
Animal.call(this);
this.name = name;
}
Human.prototype = new Animal();
...
var humanA = new Human('A');
var humanB = new Human('B');
humanA.size === humanB.size // false
但是这样还有一个问题,Human 生成的实例,不仅在它的属性里有 size 属性,而且在它的原型中还有一个 size属性(因为其的原型 是 Animal 的一个实例),只不过先在它的属性里找到了size,所以就不会再去它的原型里找了。也就是说,父类的构造函数被调用了两次,一次在子类的构造函数中,一次在生成子类的原型时。
humanA.__proto__.size === humanB.__proto__.size // true
humanA.__proto__.size.push('4')
humanB.__proto__.size // ['1','2','3','4']
所以我们要找到一个方法,让子类的原型的原型可以指向父类的原型,但不包括父类的私有属性。
function object(o){
function F(){};
F.prototype = o;
return new F();
}
function inhertPrototype(father, child){
var prototype = object(father.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
function Animal() {
this.size = ['1','2','3'];
}
Animal.prototype.eat = function (){
console.log('eat');
}
function Human(name) {
this.name = name;
}
inhertPrototype(Animal, Human);
Human.prototype.speakName = function () {
console.log(this.name);
}
其实我们在这里做的事也非常简单。
首先, object
函数创建了一个临时的构造函数,并且让该构造函数的原型等于传入的构造函数的原型,并且返回该临时的构造函数的一个实例。
在 inhertPrototype
函数中,我们先生成一个临时构造函数的实例,让子类的原型等于该临时对象就可以了。同时我们还修改了该原型指向的 contructor
指向子类构造函数,否则其的 contructor
就会指向 object
中生成的临时构造函数了。经过这么一通操作,我们就不会在生成子类的原型的时候,再去调用父类的构造函数了,也就不会在子类的构造函数中多出了父类构造函数生成的属性。
而以上这种方式被叫做寄生组合式继承,至于如何去解释这个名词,这里就不展开了,这种继承模式也是现在最普遍的继承模式。
但,为什么我们不直接让子类原型的 __proto__
引用指向 父类的原型对象呢?
像这样:
function Animal() {
this.size = ['1','2','3'];
}
Animal.prototype.eat = function (){
console.log('eat');
}
function Human(name) {
this.name = name;
}
Human.prototype.__proto__ = Animal.prototype;
Human.prototype.speakName = function () {
console.log(this.name);
}
我在 chrome
这样尝试是可以的, 而且这种做法的好处是还不需要去设置子类的原型的 contructor
,因为它没有变化过。
但有一个问题是,Firefox、Safari和Chrome等浏览器在每个对象上都显示 __proto__
;而在其他实现中,这个属性对脚本则是完全不可见的。所以在其它地方就完全不能用这种方法。
如果还有其它原因的话,希望某位大神可以打在下面的评论中。
instanceof
此外,我们在这里讲一下 instanceof
的原理。
例如,
humanA instanceof Animal // true
instanceof
的原理很简单,一句话就能讲明白,即:如果在 humanA
的原型链中找到了 Animal
的原型,就返回 true
,否则就是 false
。