现在的位置: 首页 > 综合 > 正文

原型链

2018年04月01日 ⁄ 综合 ⁄ 共 6217字 ⁄ 字号 评论关闭

前言

虽然接触了很久的javascript,但是总感觉自己对js的prototype理解不到位。刚好今天在公司的所有活都做完了,就抽空把自己整理的笔记记录下来(有很多是从网上的例子,自己动手操作加深理解)。

首先需要了解js的几个概念:

  1. js没有类这回事。虽然它保留了class的关键字,但是至今并未派上用场。在js中,所有的东西都是对象,包括函数,因此函数可以为变量赋值。
  2. 整个原型链就是一个链表,prototype,_proto_就是一些指针而已。

伪类

javascript并不像C++/Java直接从其他对象继承,反而插入了一个多余的间接层,从而使得JS的继承机制难以理解。

当一个函数对象被创建时,Function构造器产生的函数对象会运行类似这样的一些代码:

  1.     this.prototype = {constructor: this}

我们可以做个实验对其佐证。

  1. function F(){}
  2. console.log(F.prototype);
  3. //F {}

函数对象F被赋予一个prototype属性,它的值包含一个constructor属性且属性值为该新函数的对象。对于本例来说,当创建一个函数,它会发生以下几件事情:

  • 创建一个函数对象,即F本身。
  • 创建一个原型对象@F(用@F来表示)。
  • 函数对象会有一个prototype指针,它指向了对应的原型对象,这里就指向了@F。
  • @F对象中有一个constructor指针,指向它的构造函数,这里就指向了F。

该prototype对象是存放继承特征的地方。因为js语言没有提供一种方法来确定哪个函数是打算用来作构造器的,所以每个函数都会得到一个prototype对象。

当采用构造器调用模式,即使用new前缀去调用一个函数时,这将修改函数执行的方式。如果new运算符是一个方法而不是一个运算符,它可能如下执行:

  1. Function.method('new', function(){
  2. //创建一个新对象,它继承自构造函数的原型对象。
  3. var that = Object.beget(this.prototype)
  4. //调用构造器函数,绑定-this-到该对象上。
  5. var other = this.apply(that, arguments);
  6. //如果它的返回值不是一个对象,就返回该新对象
  7. return (typeof other === 'object' && other) || that

我们可以定义一个构造器并扩充它的原型:

  1. var Mammal = function(name) {
  2. this.name = name;}
  3. Mammal.prototype.get_name = function() {
  4.     return this.name;
  5.     }

现在我们创建一个实例,它继承了Mammal的原型:

  1. var myMammal = new Mammal("hello world");
  2. var name = myMammal.get_name(); //输出hello world

常用原型写法和用法

  1. function Animal(name, style) {this.name = name; this.style = style; this.common = function() {alert('动物')}}
  2. Animal.prototype.getName = function() {alert(this.name);}
  3. var cat = new Animal('小白', 'cat');
  4. cat.common(); //动物
  5. cat.getName(); //小白

这种写法是工厂模式,刚开始接触JS的时候,我使用的就是这种写法。不过这种写法是一种很危险的写法,因为函数写法一种全局方法,执行顺序高于直接量。因此修改以上的代码:

  1. var Animal = function(name, style) {this.name = name; this.style = style; this.common = function() {alert('动物')}}
  2. Animal.prototype.getName = function() {alert(this.name);}
  3. var cat = new Animal('小白', 'cat');
  4. cat.common(); //动物
  5. cat.getName();//小白

为什么要使用prototype?

使用原型来拓展方法,是为了提高函数的使用效率,可从以下的案例进行分析。

  1. var Animal = function(name, style){this.name = name; this.style = style; this.common = function() {console.log(this.style)}}
  2. Animal.prototype.getName = function() {console.log(this.name);}
  3. Animal.prototype.getStyle = function() {console.log(this.style)}
  4. var cat = new Animal('小白', 'cat');
  5. var dog = new Animal('小黑', 'dog');
  6. console.log(cat.common === dog.common); //false
  7. console.log(cat.getName === dog.getName); //true

从结果中我们可以看到,Animal拥有方法common,getName,但是dog实例和cat实例将两种方法进行对比就出现不一样的结果。在伪类中我们已经介绍了,实例的新建首先是创建一个对象,它继承自构造函数的原型对象,然后再调用构造器函数。
因此cat和dog的getName均来自于Animal的原型对象。即他们共享Animal的prototype资源。而构造函数Animal自身的属性和方法,每次跟着实例化。即每个实例均有自身的common对象,当新建多个实例时,会产生大量的common对象,占用大量的内存。

上面的两种原型写法都是标准的原型写法。每个原型都有一个构造函数,每个原型的实例也都有一个构造函数。每个原型构造函数都是唯一的,我们不能随意更改它们。

  1. var Animal = function(name, style){this.name = name; this.style = style; this.common = function() {console.log(this.style)}}

  2. Animal.prototype.getName = function() {console.log(this.name);}
  3. Animal.prototype.getStyle = function() {console.log(this.style)}
  4. var cat = new Animal('小白', 'cat');

  5. console.log(Animal.prototype.constructor == Animal); //true
  6. console.log(cat.constructor == Animal); //true

Animal为cat的构造函数,cat为Animal的实例,所有的构造函数的实例共享该构造函数。

原型继承

要真正理解原型链,需要先了解继承,如下面的代码,就是一个简单的原型继承:

  1. var Animal = function(){
  2. this.name = "animal"; this.style = "小型"; this.common = function() {console.log(this.style)}}
  3. Animal.prototype.getName = function() {console.log("名称");}
  4. Animal.prototype.getStyle = function() {console.log("体型")}
  5. var cat = function() {
  6. this.weight = "20";
  7. }
  8. cat.prototype = Animal.prototype;
  9. console.log(cat.prototype.constructor == Animal);//true
  10. var cat1 = new cat();
  11. cat1.getName();//名称

上面的代码中,新建了cat对象继承了Animal,它们两者的继承通过prototype来实现。但是这样写有个很大的问题就是,Animal的prototype对象覆盖了cat的prototype对象,因此改变了cat的prototype对象的constructor,变成了指向Animal。另一个弊端就是,由于cat的原型和Animal的原型指向同一个内存,如果修改了cat的prototype对象,也会同时修改Animal的prototype,不符合继承的隔离性质。因此,仅仅修改cat.prototype.constructor=cat会影响Animal.prototype.constructor,因此需要修改以上的代码来修正以上两个问题。

有人提出利用空对象作为中介来解决继承问题。

  1. var Animal = function(){this.name = "animal"; this.style = "小型"; this.common = function() {console.log(this.style)}}
  2. Animal.prototype.getName = function() {console.log("名称");}
  3. Animal.prototype.getStyle = function() {console.log("体型")}
  4. var cat = function () {
  5. this.weight = "20";
  6. }
  7. var empty = function(){}
  8. empty.prototype = Animal.prototype;
  9. cat.prototype = new empty();
  10. cat.prototype.constructor = cat;
  11. console.log(cat.prototype.constructor == cat); //true
  12. console.log(Animal.prototype.constructor == Animal);//true
  13. var cat1 = new cat();
  14. cat1.getName(); //名称

这下能解决继承的问题。cat1继承了Animal的prototype方法,并且cat的prototype的指向了自己,同时未篡改Animal的prototype的constructor。

但是这种继承方式另有一个弊端,即cat1只能继承Animal的prototype的属性和方法,无法继承Animal对象本身的属性。即cat1只有方法getName和getStyle而没有方法common。因此再对上面的方法进行修改:

  1. var Animal = function(){
  2. this.name = "animal"; this.style = "小型"; this.common = function() {console.log(this.style)}}
  3. Animal.prototype.getName = function() {console.log("名称");}
  4. Animal.prototype.getStyle = function() {console.log("体型")}
  5. var cat = function () {
  6. this.weight = "20";
  7. }
  8. cat.prototype = new Animal();
  9. cat.prototype.constructor = cat;
  10. console.log(cat.prototype.constructor == cat); //true
  11. console.log(Animal.prototype.constructor == Animal); //true
  12. var cat1 = new cat();
  13. cat1.getName();//名称

这样就可以是实现了完美继承了。即cat1不但继承了来自Animal的prototype,还继承了Animal本身自带的属性和方法。所以我们直接继承实例,这样可以获得Animal中的所有的方法和属性。安全有效,不用直接去操作原型本身,只是操作原型实例。

到这里貌似就可以告一段落了。但是深究下去会发现,继承者cat1的prototype存在着方法,我们想保留这些方法,然后继续继承来自Animal的所有属性和方法,应该怎么办?

通过深拷贝实现完美继承

通过以下代码我们来重现上面所提到的缺陷:

  1. var Animal = function(){this.name = "animal"; this.style = "小型"; this.common = function() {console.log(this.style)}}
  2. Animal.prototype.getName = function() {console.log("名称");}
  3. Animal.prototype.getStyle = function() {console.log("体型")}
  4. var cat = function () {
  5. this.weight = "20";
  6. }
  7. cat.prototype.testCat = function(){console.log("猫")}
  8. cat.prototype = new Animal();
  9. cat.prototype.constructor = cat;
  10. console.log(cat.prototype.constructor == cat); //true
  11. console.log(Animal.prototype.constructor == Animal);//true
  12. var cat1 = new cat();
  13. cat1.getName(); //名称
  14. cat1.testCat(); //undefine function

在这段代码中,cat1继承了Animal的方法和属性,但是它的prototype对象的testCat被覆盖了,所以需要重新修改代码。

  1. var Animal = function(){this.name = "animal"; this.style = "小型"; this.common = function() {console.log(this.style)}}
  2. Animal.prototype.getName = function() {console.log("名称");}
  3. Animal.prototype.getStyle = function() {console.log("体型")}
  4. var cat = function () {
  5.     this.weight = "20";
  6. }
  7. cat.prototype.testCat = function(){console.log("猫")}
  8. var extend = function(child, parent) {var p=new parent();var c = child.prototype; for(var i in p) {c[i]=p[i]}}
  9. extend(cat, Animal);
  10. var cat1 = new cat();
  11. cat1.getName(); //名称
  12. cat1.testCat(); //猫

其实这样做的原理挺简单的,遍历Animal的实例的所有属性和方法,将其添加到cat1的prototype对象中,既可以保留本身prototype的属性和方法,又可以继承来自Animal的属性和方法。

抱歉!评论已关闭.