原型(prototype)的来源:
虽然JavaScript和Java并没什么关系,但是JavaScript在开始创建的时候想要模仿Java 通过new 操作符实现对一些方法和属性的共享。但是在es6 之前JavaScript并没有真正类 (class)的概念,所以JavaScript借鉴了其他语言(self , io, lua等)的原型机制。 简单点说就是在JavaScript中每个函数都可以是一个构造器,每个函数被创建的时候都会有一个prototype ,而函数通过new 操作符可以产生自己的实例对象,这个实例对象会继承父类prototype 上的所有方法 。
原型的构成:
原型的本质其实是个对象字面量,主要由三部分构成 : constructor (构造器) , 原型方法 ,_ proto_ (原型链)
上图是以数组为例,因为所有的数组都是Array的实例产出,所以所有的数组都能用上边那些原型方法。
constructor :构造器,指向的是当前拥有这个原型的那个类(函数)。
_ proto_ : 原型链索引,指向自己的父类,也就是说指向的是当前这个原型的爸爸。 一个被new 出来的实例对象到底能使用什么方法不只是取决是它的爸爸,还有他的爷爷,太爷爷….. , 也就是说如果只要是老祖宗给它继承下来的东西它都是能用的 。_ proto_这个东西就是用来翻族谱的,儿子的 _ proto_会指向爸爸,爸爸的_ proto_会指向爷爷,以此类推。下边来看个例子。如果实例对象某个自定义的方法或属性跟prototype 上重名的话,会以这个实例对象自定义的方法或属性返回。所以这也是一种非常灵活的改写。
在页面创建了一个div dom实例对象
看一下这个实例对象的 _ proto_ ,找到了它的爸爸 HTMLDivElement
展开HTMLDivElement这个对象的_ proto_,找到了它的爷爷 HTMLElement ,
往下翻翻发现它爷爷居然也有个 _ proto_ 指向了Element
接着往下找下去你会发现还有 Node 、 EventTarget 不过最后指向的是Object 到这里才算结束了。 这个原型上的方法我们的 box 实例对象都能使用,因为是继承关系。咱们这个往上级找爸爸的过程形成了一条链路就是原型链。总结来说,当访问一个对象的属性的时候,JavaScript会从对象本身开始往上遍历整个原型链,如果到达了原型链的顶部,也就是Object.prototype仍然未发现需要查找的属性,那么JavaScript就会返回undefined .
JavaScript 面向对象:
不管是任何语言中面向对象有三个要素: 封装、继承 、多态。 作为一个以函数作为一等对象的语言,封装在JavaScript中随处可见,看一个封装的例子,顺便了解几个概念:
var Book = function(id,name){
// 私有属性
var num=1;
//私有方法
function() checkId{
}
// 特权方法
this.setName = fucntion(name){}
//特权属性
this.type = "book"
}
Book.method2 = function(){} // 类方法
Book.name = "hello" //类属性
上边的私有属性和方法就是一个简单的封装,只有内部可以访问到。
多态 :就是同一种方法多种调用方式,简单的例子是根据参数不同执行不同代码,或者根据this类型不同,执行不同代码,不展开了。
继承是大家问到比较多的地方,这里要敲一下黑板的:
上边已经说过了JavaScript开始创建的时候就考虑到了一些方法和属性共享的情景,并且创建了原型。所以我们实现继承的方式必然和原型脱不开联系。
现在有两个函数 A 和B ,如果B想继承A的原型方法应该怎么做?
情景一 :简单暴力的
function A(){}
A.prototype = {
aaa:1
}
function B(){}
B.prototype = A.prototype ;
var b = new B();
b.aaa // 1
A.prototype.bbb = 2;
b.bbb // 2
这个例子暴露出来最大的问题是不灵活,因为上边说过prototype是个对象字面量,直接赋值会传递引用地址。好比两个人共用了一个u盘,其中一个人对里边的文件进行了操作另一个使用时也会发现变化。所以这是不可取的
情景二 : 模仿一下
既然直接赋值原型不行。那我们只把别人原型上的方法和属性拷贝过来不就可以了吗?就是说我们再拿一个u盘,把里边的文件拷贝一份出来,大家自己用自己的这样就能互不干扰了。
function A(){}
A.prototype= {aaa:1 }
fucntion B(){}
for (var property in A.prototype){
B.prototype[property] = A.prototype[property]
}
var b = new B();
A.prototype.aaa = 2;
b.aaa; // 1
这样看起来似乎就比较灵活了,但是这样似乎只继承了A的原型方法,假如A拥有特权方法和属性怎么办? 例如下面这种的
function A (name){
this.name =name;
this.sayHey =fucntion(){
console.log("hello");
}
}
所以还得把特权方法和特权属性继承过来; 这就需要用到两个方法 call 和apply。
这两个方法功能相似,调用的对象都是个函数,第一个参数都代表执行调用函数的对象,后边传入的参数代表调用函数的参数;区别主要在于call 方法参数是单个传入,apply 方法可以直接传入一个数组 。
要在B函数内执行A函数里的代码。
function A(name,age){
this.name= name;
this.age = age;
}
function B(name,age){
A.call(this,name,age) 或 A.apply(this,arguments)
// 执行上边这行代码相当于
// this.name = name;
// this.age = age;
}
所以要想实现特权属性、方法和原型属性、方法都继承就得结合一下
function B(){
A.apply(this,arguments);
}
for (var property in A.prototype){
B.prototype[property] = A.prototype[property]
}
情景三 :并不完美
似乎一切看起来都妥了,我们说过原型是由三部分构成的,其中有个比较关键的部分叫constructor (构造器), 它会指向当前这个原型的所属函数。还有个_ proto_会指向自己的父类原型。 采用直接复制的方式会把constructor和_ proto_属性也复制过来,虽然并不影响功能使用,但是会使得_ proto_在追溯自己的爸爸的时候迷失方向的,有可能会认贼作父。所以采用上边那种方式,在复制完后还需要将constructor和_ protp_重新调整一下。
好在有个牛人(道格拉斯. 克罗克福)发明了更简单的方法- 原型链继承 (上边那种叫拷贝继承)
// 父类
function Parent (name,age){
this.name = name;
this.age = age;
}
//子类
fuction Children(){}
//中间类
function F(){}
F.prototype = Parent.prototype ;
Children.prototype = new F();
在进行new 操作的时候实际是将 Children的 _ proto_指向F.prototype,而F.prototype又和Parent.prototype相同。所以Children的实例在使用方法和属性的时候能使用到Parent类的方法和属性。 你可能会疑问为什么不直接使用 Children.prototype =new Parent() 呢? 这是因为在 调用new Parent()的时候会产生一个Parent类的实例,这可能并不是我们想要的。而F作为中间传递者,没有任何的私有方法、属性或特权方法、属性。所以在实例化的时候产生的负面影响几乎没有。
当然,最后不能忘了我们还有特权方法和属性要继承,所有完美的继承方式应该是这样的:
// 父类
function Parent (name,age){
// 特权属性
this.name = name;
this.age = age;
this.show =fucntion(){ }
}
//子类
fuction Children(name,age){
Parent.apply(this,arguments)
}
//中间类
function F(){}
F.prototype = Parent.prototype ;
Children.prototype = new F();