JavaScript原型、原型链和继承

原型对象

无论我们什么时候创建一个函数,它都会有一个属性prototype,该属性是一个指针,它指向函数的原型对象,该原型对象所拥有的属性和方法都可被函数的实例所共享。举个🌰:

var fn = function(){
alert(0)
};
fn.prototype.name = 'a';
fn.prototype.todo = function(){
alert('todo')
};
var newFn = new fn();
console.dir(fn)//查看fn所有的属性和方法
console.log(newFn.name);//'a'
newFn.todo();

所有的原型对象都会自动拥有一个属性constructor(构造函数),这个属性包含一个指向构造函数的指针。即:

fn.prototype.constructor = fn;

当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个属性__proto__;而在其他实现中,这个属性对脚本则是完全不可见的。——JavaScript高级程序设计

该属性是实例和构造函数的原型对象之间的联系,和构造函数并无直接关联,其实实例之所以可以调用构造函数的原型对象上面的属性和方法,也是通过该属性实现的。

小结:

  • 所有的引用类型都可以自由的扩展其属性(除null)
  • 所有的引用类型都有_proto_属性(除null)
  • 所有的对象都有prototype属性
  • 所有的引用类型的_proto_都指向它们的构造函数的prototype
  • 当寻找一个引用类型的属性时,如果在当前对象找不到该属性的定义,就会沿着_proro_一直向上寻找,直到找到为止或者找到Object.prototype(即null)为止。

注意:大家可能已经注意到,函数既有prototype属性,也有_proto_属性。下面就针对函数做一个解释:

var Fn = function(){};
Fn.prototype.colors = ['red','yellow'];
Fn.prototype.todo = function(){
alert('todo')
};

var fn1 = new Fn();
console.log(Object.getPrototypeOf(fn1))//Fn.prototype
console.log(Object.getPrototypeOf(Object.getPrototypeOf(fn1)))//Object.prototye
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(fn1))))//null

请记住:一切引用类型都有一个隐式原型Object.prototype。

原型链

原型对象的问题

原型对象最大的问题就是因其共享性所导致的。看个🌰:

//原型
var Fn = function(){};
Fn.prototype.colors = ['red','yellow'];
Fn.prototype.todo = function(){
alert('todo')
};

var fn1 = new Fn();
fn1.colors.push('blue');

var fn2 = new Fn();
console.log(fn2.colors);// ["red", "yellow", "blue"]

为什么要使用原型对象

首先,看一段代码:

//新建一个构造函数 
var Person = function (){ 
drink(){ 
console.log('drink') 
} 
}; 
Person.prototype.eat = function(){ 
console.log('eat') 
} 
var person = new Person();
person.eat();

在这段代码中,我们分别在Person和Person.prototye上挂载了 eat 和 drink 函数,然后使用 new 关键字对构造函数 Person 进行了实例化。

对于 drink 函数:每进行一次实例化,都要重新在内存中占用一些资源。

对于 eat 函数:我们将 eat 函数挂载在 Person 的原型上,Person 的实例每次只需要调用原型上的方法即可,节约了内存占用。

继承

ECMAScript中实现继承的主要就是依靠原型链来实现的。

原型链继承

最为ECMAScript最主要的继承方法,其基本思想就是让一个引用类型继承另一个引用类型的属性和方法。在上面我们说过,每一个构造函数都有一个原型对象,原型对象都有一个指向构造函数的指针constructor,每一个构造函数都可以生成一个实例,每一个实例都有一个指向原型对象的内部指针_proto_。简单来说就是:

var Fn = function(){};
var Test = function(){}
var fn = new Fn();
Fn.prototype.constructor = Fn;
fn._proto_ => Fn.prototype;

如果此时我们让一个Fn的原型对象指向另一个类型的实例呢?

Fn.prototype = new Test();

那么此时,原型对象就会有一个指向另一个原型的指针,另一个原型也会包含一个指向另一个构造函数的指针。如此层层递进,即构成了原型链。

function SuperType(){
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.Fun = function(){

};
function SubType(){
}
//继承了SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"

优点:可以通过 instanceOf 和 isPrototypeOf 检测

缺点:

1、父类型中的私有属性会变为子类型中的公有属性

2、创建子类型的时候,不能像父类型的构造函数中传递参数。

构造函数继承

JavaScript规定,每一个构造函数都有一个prototype属性,它指向一个对象,这个对象中的所有的属性和方法都会被构造函数的实例所继承。

构造函数的继承是通过new关键字,生成实例完成的。使用new关键字生成实例的过程中,就会把this绑定到实例上,具体过程如下:

  1. 在内存中先生成一个object的实例对象,

  2. 将实例对象的_proto_指向构造函数的prototype(即构造函数的原型),

  3. 运行构造函数,

  4. 检查返回值,如果返回值为基本数据类型,则无视该返回值,而将生成的对象返回。如果为引用类型,则将该返回值返回。

function SuperType(){
this.colors = ["red", "blue", "green"];
}

function SubType(name){
SuperType.call(this)
}

var s = new SubType('ykx');

s.colors.push('yellow');

console.log(s.colors);

var s2 = new Subtype('lhd');

console.log(s2.colors)

优点:

1、实例化子类型的时候可以传参

2、父类型中的属性不会变为公共的属性

缺点:虽然构造函数实现继承的方式比较好用,但是并不推荐这种方式。构造函数继承存在内存浪费的情况,每生成一个实例,都会占用一些内存。

寄生式继承

寄生式继承有点类似与工厂模式,即仅创建一个封装继承过程的函数,该函数在函数内部使用某种方式增强对象,最后再返回一个对象。

function inhertprototype(original){
var child = Object.creata(original);
child.say = function(){
alert(0)
}
return child;
}
var Person = {
age:10
}
var boy = inhertprototype(Person);
boy.say()

使用寄生式继承来为对象添加函数,不能够使函数得到更多的复用,降低了效率,类似于构造函数。

寄生组合式继承

function Man(age,name){
this.eat = function(something){
console.log(this.age + '岁的' + this.name + '正在吃: ' + something)
}
}

Man.prototype.drink = function(something){
console.log(this.name + '正在喝:' + something)
}
function Boy(age,name){
Man.call(this,age,name);//组合继承
this.age = age;
this.name = name;
}

/**

不推荐使用 Boy.prototype = new Man();//组合继承
因为在JavaScript中没有显式的constructor,
所以使用new关键字实例化的时候 该函数会被调用一次

*/

function inhertprototype (child,parent){
var prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}

//Boy.prototype = Object.create(Man.prototype);

//Boy.prototype.constructor = Boy;//避免原型错乱

inhertprototype(Boy,Man)

var boy = new Boy(12,'ykx');

boy.eat('apple');

boy.drink('water')

原型错乱:

function SuperType(){
}

function Sub(){
SuperType.call(this)
}

Sub.prototype = Object.create(SuperType.prototype);

console.dir(Sub);

结果如下:

原型错乱

正确的继承结果应该如下:

原型正常

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!