是时候写一写 “继承”了,为何加引号,由于当你阅读完这篇文章,你会知道,说是 继承 实际上是不许确的。javascript
1、类
一、传统的面向类的语言中的类:
类/继承 描述了一种代码的组织结构形式。举个例子:
“汽车”能够被看做是“交通工具”的一种特例。
咱们能够定义一个 Vehicle 类和一个 Car 类来对这种关系进行描述。
Vehicle 的定义可能包含引擎、载人能力等,也就是 全部交通工具,好比飞机、火车和汽车等都有的通用的功能描述。
在对 Car 类进行定义的时候,重复定义“载人能力”是没有意义的,咱们只须要声明 Car 类继承了 Vehicle 的这个基础类就能够了。 Car 其实是对通用 Vehicle 定义的特殊化。
Car 如今只是个类,当咱们把它更加形象化,好比说是 保时捷、奔驰的时候,就是一个实例化的过程。
咱们也有可能 会在 Car 中定义一个 和 Vehicle 中相同的方法,这是子类对父类针对特定方法的重写,为了能够更加特殊化,更加符合对子类的描述,这个被称做是 多态。html
以上就是 类、继承、实例化和多态。java
再举个例子:好比房屋的构建
建筑师设计出来建筑蓝图,而后由建筑工人按照建筑蓝图建造出真正的建筑。建筑就是蓝图的物理实例,本质上是对建筑蓝图的复制。以后建筑工人就能够到下一个地方,把全部的工做重复一遍,再建立一份副本。
建筑和蓝图之间的关系是间接的。你能够经过蓝图了解建筑的结构,只观察建筑自己是没法得到这些信息的。可是若是你想打开一扇门,那就必须接触真实的建筑才行--蓝图只能表示门应该在哪,但并非真正的门。
一个类就是一张蓝图。为了得到真正能够交互的对象,咱们必须按照类来建造(实例化)一个东西,这个东西一般被称为实例。这个对象就是类中描述的全部特性的一份副本。设计模式
在传统的面向类的语言中,类的继承,实例化其实就是复制,用一张图:jsp
箭头表示复制操做。ide
子类 Bar 相对于 父类 Foo 来讲是一个独立并彻底不一样的类。子类会包含父类行为的副本,也能够经过在子类中定义于父类中相同的方法名来改写某个继承的行为,这种改写不会影响父类中的方法,这两个方法互不影响。对于 类 Bar 和 实例 b1 、b2 之间也一样是 类经过复制操做被实例化为对象形式。函数
二、javascript 中的类
然而 javascript 其实并无类的概念,可是 咱们早已经习惯用类来思考,因此 javascript 也提供了一些近似类的语法,咱们用它模拟出了类,然而这种 “类”仍是与传统面向类语言中的类有不一样。
在继承和实例化的过程当中,javascript的对象机制并不会自动执行复制行为。简单来讲,javascript中只有对象,并不存在能够被实例化的“类”。一个对象并不会被复制到其它对象,他们只会被关联起来,也就是复制的是实际上是引用。(对于复制引用,具体的能够看【 js 基础 】 深浅拷贝 一文,第一部分)工具
2、javascript 原型链、“类”和 继承
一、 [[prototype]]:javascript 中的对象都有一个特殊的 [[prototype]] 内置属性,其实就是对于其余对象的引用。他的做用是什么呢?
当你试图访问对象的属性的时候,就会触发对象的内置操做 [[Get]],[[Get]] 操做就是从对象中找到你要的属性。然而他是怎么找的呢?
例子:post
1 var testObject = { 2 a:2 3 }; 4 console.log(testObject.a) // 2
在上面的代码中,当你 console 的时候,会触发 [[Get]] 操做,查找 testObject 中的 a 属性。对于默认的 [[Get]]操做,第一步是检查对象自己是否有这个属性,若是有的话就直接使用它。第二步,若是 a 不在 testObject 中,也就是 没法在对象自己中找到须要的属性,就会继续访问对象的 [[prototype]] 链。学习
例子:
1 var anotherObject = { 2 a : 2 3 }; 4 5 var testObject = Object.create(anotherObject); 6 7 console.log(testObject.a); // 2
Object.create() 方法会建立一个对象并把这个对象的 [[prototype]] 关联 到指定的对象(anotherObject)。
例子中,testObject 对象的 [[prototype]] 关联到了 anotherObject。 testObject 自己并无 a 属性,然而仍是能够 console 出testObject.a 为 2,这是 [[Get]] 从 testObject 的 [[prototype]] 链中找到的,即 anotherObject 中的属性a。可是假若 anotherObject中也没有属性 a,而且 [[prototype]]不为空,就会继续查找下去。这个过程会持续到找到匹配的属性名,或者查找完整条 [[prototype]] 链未找到,[[Get]] 操做的返回值是 undefined 。
那么哪里是原型链的尽头呢?
全部的普通的 [[prototype]] 链最终都会指向 内置的 Object.prototype 。
二、“类”:在上一部分的内容中,咱们已经说到,javascript 中实际上是没有传统意义上的“类”的,但咱们一直在试图模仿类,主要是利用了 函数的一种特殊特性:全部的函数默认都会有一个名为 prototype 的公有而且不可枚举的属性,它会指向另外一个对象。
例子:
1 function foo(){ 2 //… 3 } 4 5 foo.prototype; // {} 6 7 var a = new foo(); 8 Object.getPrototypeOf(a) === foo.prototype;//true
这个对象是在调用 new foo () 时建立的,最后会被关联到 foo.prototype 上。就像例子中的调用 new foo () 时会建立 a ,而后 将 a 内部的 [[prototype]] 连接到 foo.prototype 所指向的对象。
在传统的面向类的语言中,类能够被复制屡次,每次实例化的过程都是一次复制。但在 javascript 中没有相似的复制机制。你不能建立一个类的多个实例,只能建立多个对象,他们的 [[prototype]] 关联的是同一个对象。由于在默认状况下,并不会进行复制,因此这些对象之间并不会彻底失去联系,他们是互相关联的。
就像上面的例子 new foo () 会生成一个新的对象,称为 a,这个新的对象的内部的 [[prototype]] 关联的是 foo.prototype 对象。最后咱们获得两个对象,他们之间互相关联。咱们并无真正意义上初始化一个类,实际上咱们并无从 “类” 中复制任何行为到一个对象中,只是让两个对象互相关联着。
再强调一下: 在 javascript 中,并不会将一个对象(“类”)复制到另外一个对象(“实例”),只是将它们关联起来。看一个图:
箭头表示 关联。
这个图就表达了 [[prototype]] 机制,即 原型继承。
可是说是 继承实际上是不许确的,由于传统面向类的语言中 继承 意味着复制操做,而 javascript (默认)并不会复制对象属性,而是在两个对象之间建立一个关联,这样一个对象能够 委托 访问另外一个对象的属性和函数。委托 能够更加准确的描述 javascript 中对象的关联机制。
三、 (原型)继承
来看个例子:
1 function foo(name){ 2 this.name = name; 3 } 4 5 foo.prototype.myName = function(){ 6 return this.name; 7 } 8 9 function bar(name,label){ 10 foo.call(this,name); 11 this.label = label; 12 } 13 14 // 建立了一个新的 bar.prototype 对象并把它关联到了 foo.prototype。 15 bar.prototype = Object.create(foo.prototype); 16 17 bar.prototype.myLabel = function(){ 18 return this.label; 19 } 20 21 var a = new Bar(“a”,”obj a”); 22 console.log(a.name) // “a" 23 console.log(a.label) // "obj a"
声明 function bar(){} 时,bar 会有一个默认的 .prototype 关联到默认的对象,可是这个对象不是咱们想要的 foo.prototype 。所以 咱们经过 Object.create() 建立了一个新的对象并把它关联到咱们但愿的对象上,即 foo.prototype,直接把原始的关联对象抛弃掉。
若是你说为何不用下面这种方式关联?
bar.prototype = foo.prototype
由于 这种方式并不会建立一个关联到 foo.prototype 的新对象,它只是让 bar.prototype 直接引用 foo.prototype 。所以当你执行 bar.prototype.myLabel 的赋值语句时会直接修改 foo.prototype 对象自己。
或者说为何不用new?
bar.prototype = new foo();
这样的确会建立一个关联到 foo.prototype 的新对象。 可是它同时 也执行了对 foo 函数的调用,若是 foo 函数中有给this添加属性、修改状态、写日志等,就会影响到 bar() 的 “后代” 。
这里补充两点关于 new ,方便理解:
function foo(){ console.log(“test”); } var a = new foo(); // test
当你执行 var a = new foo(); 也就是使用 new 来调用函数,会执行下面四步操做:
一、建立一个全新的对象
二、这个新对象会被执行 [[prtotype]] 链接
三、这个新对象会绑定到函数调用的 this 上
四、若是函数没有返回值,那么 new 表达式中的函数调用会自动返回这个新对象。
另外一点,当你执行 var a = new foo(); 时 ,console 打出 test。foo 只是个普通的函数,当使用 new 调用时,它就会创造一个新对象并赋值给 a,固然也会调用自身。
综上,要建立一个合适的关联对象,最好的方式就是用 Object.create(),这样作也有缺点:就是建立了新对象,而后把旧对象抛弃掉,不能直接修改默认的已有对象了。
Object.create() 会建立一个 拥有空 [[prototype]] 链接的对象。它是 es5 新增的方法,让咱们来看看在老的环境中如何实现它:
if(!Object.create){ Object.create = function(o){ function F(){} F.prototype = o; return new F(); } }
咱们使用了一个空函数 F,经过改写它的 .prototype 属性使其指向想要关联的对象,而后再使用 new F() 来构造一个新对象来进行关联。
3、类式继承设计模式 和 委托设计模式
这两种模式都是用来实现继承,本质上也就是 关联。
一、类式继承设计模式:
这个应该是你们最熟悉的,主要就是运用构造函数和原型链实现继承,也就是所谓的面向对象风格。
1 function Foo(who){ 2 this.me = who; 3 } 4 5 Foo.prototype.identify = function(){ 6 return "i am “ + this.me; 7 } 8 9 function Bar(who){ 10 Foo.call(this,who); 11 } 12 13 Bar.prototype = object.create(Foo.prototype); 14 15 Bar.prototype.speak = function(){ 16 alert(“Hello,”+ this.identify()+”.”); 17 } 18 19 var b1 = new Bar(“b1”); 20 var b2 = new Bar(“b2”); 21 22 b1.speak(); 23 b2.speak();
子类 Bar 继承了 父类 Foo,而后生成了 b1 和 b2 两个实例。 b1 继承了 Bar. prototype , Bar.prototype 继承了 Foo.prototype。
二、 委托设计模式
对象关联风格:
1 Foo = { 2 init:function(who){ 3 this.me = who; 4 }, 5 identify:function(){ 6 return “i am” +this.me; 7 } 8 }; 9 10 Bar = Object.create(Foo); 11 Bar.speak = function(){ 12 alert(“hello,” + this.identify()) 13 }; 14 15 var b1 = Object.create(Bar); 16 b1.init(“b1”); 17 var b2 = Object.create(Bar); 18 b2.init(“b2”); 19 20 b1.speak(); 21 b2.speak();
这段代码一样 利用 [[prototype]] 把 b1 委托给 Bar 并把 Bar 委托给 Foo,和上一段代码一摸同样,一样实现了三个对象的关联。
三、以上两种模式都实现了三个对象的关联,那么它们的区别是什么呢?
首先是思惟方式的不一样:
类式继承设计模式:定义一个通用的父类,能够将其命名为 Task,在 Task 中定义全部任务都有的行为。接着定义子类 A 和 B,他们都继承子 Task,而且会添加一些特殊的行为来处理对应的人物。而后你实例化子类,这些实例拥有 父类 Task 的通用方法,也拥有 子类 A 的特殊行为。
委托设计模式:首先定义一个名为Task 的对象,它包含全部任务均可以使用的行为。接着对于每一个任务 A 和 B,都会定义一个对象来存储对应的数据和行为。执行 任务 A 须要两个兄弟对象(Task 和 A)协做完成,只是在须要某些通用行为的时候 能够容许 A 对象委托给 Task。在上面的例子中,也就是 Bar 经过 Object.create(Foo); 建立,它的 [[prototype]] 委托给了 Foo 对象。这就是一种对象关联的风格。委托行为意味着某些对象(Bar)在找不到属性或者方法引用时会把这个请求委托给另外一个对象(Foo)。