前言
虽然接触了很久的javascript,但是总感觉自己对js的prototype理解不到位。刚好今天在公司的所有活都做完了,就抽空把自己整理的笔记记录下来(有很多是从网上的例子,自己动手操作加深理解)。
首先需要了解js的几个概念:
- js没有类这回事。虽然它保留了class的关键字,但是至今并未派上用场。在js中,所有的东西都是对象,包括函数,因此函数可以为变量赋值。
- 整个原型链就是一个链表,prototype,_proto_就是一些指针而已。
伪类
javascript并不像C++/Java直接从其他对象继承,反而插入了一个多余的间接层,从而使得JS的继承机制难以理解。
当一个函数对象被创建时,Function构造器产生的函数对象会运行类似这样的一些代码:
- this.prototype = {constructor: this}
- function F(){}
- console.log(F.prototype);
- //F {}
函数对象F被赋予一个prototype属性,它的值包含一个constructor属性且属性值为该新函数的对象。对于本例来说,当创建一个函数,它会发生以下几件事情:
- 创建一个函数对象,即F本身。
- 创建一个原型对象@F(用@F来表示)。
- 函数对象会有一个prototype指针,它指向了对应的原型对象,这里就指向了@F。
- @F对象中有一个constructor指针,指向它的构造函数,这里就指向了F。
- Function.method('new', function(){
- //创建一个新对象,它继承自构造函数的原型对象。
- var that = Object.beget(this.prototype)
- //调用构造器函数,绑定-this-到该对象上。
- var other = this.apply(that, arguments);
- //如果它的返回值不是一个对象,就返回该新对象
- return (typeof other === 'object' && other) || that
- var Mammal = function(name) {
- this.name = name;}
- Mammal.prototype.get_name = function() {
- return this.name;
- }
- var myMammal = new Mammal("hello world");
- var name = myMammal.get_name(); //输出hello world
常用原型写法和用法
- function Animal(name, style) {this.name = name; this.style = style; this.common = function() {alert('动物')}}
- Animal.prototype.getName = function() {alert(this.name);}
- var cat = new Animal('小白', 'cat');
- cat.common(); //动物
- cat.getName(); //小白
这种写法是工厂模式,刚开始接触JS的时候,我使用的就是这种写法。不过这种写法是一种很危险的写法,因为函数写法一种全局方法,执行顺序高于直接量。因此修改以上的代码:
- var Animal = function(name, style) {this.name = name; this.style = style; this.common = function() {alert('动物')}}
- Animal.prototype.getName = function() {alert(this.name);}
- var cat = new Animal('小白', 'cat');
- cat.common(); //动物
- cat.getName();//小白
为什么要使用prototype?
使用原型来拓展方法,是为了提高函数的使用效率,可从以下的案例进行分析。
- var Animal = function(name, style){this.name = name; this.style = style; this.common = function() {console.log(this.style)}}
- Animal.prototype.getName = function() {console.log(this.name);}
- Animal.prototype.getStyle = function() {console.log(this.style)}
- var cat = new Animal('小白', 'cat');
- var dog = new Animal('小黑', 'dog');
- console.log(cat.common === dog.common); //false
- console.log(cat.getName === dog.getName); //true
从结果中我们可以看到,Animal拥有方法common,getName,但是dog实例和cat实例将两种方法进行对比就出现不一样的结果。在伪类中我们已经介绍了,实例的新建首先是创建一个对象,它继承自构造函数的原型对象,然后再调用构造器函数。 因此cat和dog的getName均来自于Animal的原型对象。即他们共享Animal的prototype资源。而构造函数Animal自身的属性和方法,每次跟着实例化。即每个实例均有自身的common对象,当新建多个实例时,会产生大量的common对象,占用大量的内存。
上面的两种原型写法都是标准的原型写法。每个原型都有一个构造函数,每个原型的实例也都有一个构造函数。每个原型构造函数都是唯一的,我们不能随意更改它们。
- var Animal = function(name, style){this.name = name; this.style = style; this.common = function() {console.log(this.style)}}
- Animal.prototype.getName = function() {console.log(this.name);}
- Animal.prototype.getStyle = function() {console.log(this.style)}
- var cat = new Animal('小白', 'cat');
- console.log(Animal.prototype.constructor == Animal); //true
- console.log(cat.constructor == Animal); //true
Animal为cat的构造函数,cat为Animal的实例,所有的构造函数的实例共享该构造函数。
原型继承
要真正理解原型链,需要先了解继承,如下面的代码,就是一个简单的原型继承:
- var Animal = function(){
- this.name = "animal"; this.style = "小型"; this.common = function() {console.log(this.style)}}
- Animal.prototype.getName = function() {console.log("名称");}
- Animal.prototype.getStyle = function() {console.log("体型")}
- var cat = function() {
- this.weight = "20";
- }
- cat.prototype = Animal.prototype;
- console.log(cat.prototype.constructor == Animal);//true
- var cat1 = new cat();
- 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,因此需要修改以上的代码来修正以上两个问题。
有人提出利用空对象作为中介来解决继承问题。
- var Animal = function(){this.name = "animal"; this.style = "小型"; this.common = function() {console.log(this.style)}}
- Animal.prototype.getName = function() {console.log("名称");}
- Animal.prototype.getStyle = function() {console.log("体型")}
- var cat = function () {
- this.weight = "20";
- }
- var empty = function(){}
- empty.prototype = Animal.prototype;
- cat.prototype = new empty();
- cat.prototype.constructor = cat;
- console.log(cat.prototype.constructor == cat); //true
- console.log(Animal.prototype.constructor == Animal);//true
- var cat1 = new cat();
- cat1.getName(); //名称
这下能解决继承的问题。cat1继承了Animal的prototype方法,并且cat的prototype的指向了自己,同时未篡改Animal的prototype的constructor。
但是这种继承方式另有一个弊端,即cat1只能继承Animal的prototype的属性和方法,无法继承Animal对象本身的属性。即cat1只有方法getName和getStyle而没有方法common。因此再对上面的方法进行修改:
- var Animal = function(){
- this.name = "animal"; this.style = "小型"; this.common = function() {console.log(this.style)}}
- Animal.prototype.getName = function() {console.log("名称");}
- Animal.prototype.getStyle = function() {console.log("体型")}
- var cat = function () {
- this.weight = "20";
- }
- cat.prototype = new Animal();
- cat.prototype.constructor = cat;
- console.log(cat.prototype.constructor == cat); //true
- console.log(Animal.prototype.constructor == Animal); //true
- var cat1 = new cat();
- cat1.getName();//名称
这样就可以是实现了完美继承了。即cat1不但继承了来自Animal的prototype,还继承了Animal本身自带的属性和方法。所以我们直接继承实例,这样可以获得Animal中的所有的方法和属性。安全有效,不用直接去操作原型本身,只是操作原型实例。
到这里貌似就可以告一段落了。但是深究下去会发现,继承者cat1的prototype存在着方法,我们想保留这些方法,然后继续继承来自Animal的所有属性和方法,应该怎么办?
通过深拷贝实现完美继承
通过以下代码我们来重现上面所提到的缺陷:
- var Animal = function(){this.name = "animal"; this.style = "小型"; this.common = function() {console.log(this.style)}}
- Animal.prototype.getName = function() {console.log("名称");}
- Animal.prototype.getStyle = function() {console.log("体型")}
- var cat = function () {
- this.weight = "20";
- }
- cat.prototype.testCat = function(){console.log("猫")}
- cat.prototype = new Animal();
- cat.prototype.constructor = cat;
- console.log(cat.prototype.constructor == cat); //true
- console.log(Animal.prototype.constructor == Animal);//true
- var cat1 = new cat();
- cat1.getName(); //名称
- cat1.testCat(); //undefine function
在这段代码中,cat1继承了Animal的方法和属性,但是它的prototype对象的testCat被覆盖了,所以需要重新修改代码。
- var Animal = function(){this.name = "animal"; this.style = "小型"; this.common = function() {console.log(this.style)}}
- Animal.prototype.getName = function() {console.log("名称");}
- Animal.prototype.getStyle = function() {console.log("体型")}
- var cat = function () {
- this.weight = "20";
- }
- cat.prototype.testCat = function(){console.log("猫")}
- var extend = function(child, parent) {var p=new parent();var c = child.prototype; for(var i in p) {c[i]=p[i]}}
- extend(cat, Animal);
- var cat1 = new cat();
- cat1.getName(); //名称
- cat1.testCat(); //猫
其实这样做的原理挺简单的,遍历Animal的实例的所有属性和方法,将其添加到cat1的prototype对象中,既可以保留本身prototype的属性和方法,又可以继承来自Animal的属性和方法。