目录javascript
面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。它将真实世界各类复杂的关系,抽象为一个个对象,而后由对象之间的分工与合做,完成对真实世界的模拟。java
每个对象都是功能中心,具备明确分工,能够完成接受信息、处理数据、发出信息等任务。对象能够复用,经过继承机制还能够定制。所以,面向对象编程具备灵活、代码可复用、高度模块化等特色,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合做的大型软件项目。es6
那么,“对象”(object)究竟是什么?咱们从两个层次来理解。数据库
(1)对象是单个实物的抽象。编程
一本书、一辆汽车、一我的均可以是对象,一个数据库、一张网页、一个与远程服务器的链接也能够是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就能够模拟现实状况,针对对象进行编程。浏览器
(2)对象是一个容器,封装了属性(property)和方法(method)。服务器
属性是对象的状态,方法是对象的行为(完成某种任务)。好比,咱们能够把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。app
想要了解对象,咱们先来学习如何建立一个对象,首先是经过构造函数的形式来建立一个对象。编程语言
通常状况下,咱们能够将现实生活当中的实物抽象成对象。而想要抽象成对象,咱们一般状况下须要一个模板,这个模板当中具有这类
实物的公有特性,而后咱们就能够经过这个模板来实现该类对象的建立。模块化
在js中,咱们就能够经过构造函数来建立这类模板。
构造函数是用new建立对象时调用的函数,与普通惟一的区别是构造函数名应该首字母大写。
function Person(){ this.age = 30; } var person1 = new Person(); console.log(person1.age);//30
根据须要,构造函数能够接受参数:
function Person(age){ this.age = age; } var person1 = new Person(30); console.log(person1.age);//30
若是没有参数,能够省略括号
function Person(){ this.age = 30; } //等价于var person1 = new Person() var person1 = new Person; console.log(person1.age);//30
若是忘记使用new操做符,则this将表明全局对象window。一般这种状况下会容易发生不少错误。
必定要当心。
function Person(){ this.age = 30; } var person1 = Person(); //Uncaught TypeError: Cannot read property 'age' of undefined console.log(person1.age);
当咱们经过构造函数建立了一个对象以后,咱们就能够经过instanceof来判断对象的类型以及当前对象是不是经过指定构造函数构建而成。
function Person(){ // } var person1 = new Person; console.log(person1 instanceof Person);//true
每一个对象在建立时都自动拥有一个构造函数属性constructor,其中包含了一个指向其构造函数的引用。而这个constructor属性实际上继承自原型对象,而constructor也是原型对象惟一的自有属性
function Person(){ // } var person1 = new Person; console.log(person1.constructor === Person);//true console.log(person1.__proto__.constructor === Person);//true
经过打印person1,你会发现,constructor 是一个继承的属性。
虽然对象实例及其构造函数之间存在这样的关系,可是仍是建议使用instanceof来检查对象类型。这是由于构造函数属性能够被覆盖,并不必定彻底准确
function Person(){ // } var person1 = new Person; Person.prototype.constructor = 123; console.log(person1.constructor);//123 console.log(person1.__proto__.constructor);//123
函数中的return语句用来返回函数调用后的返回值,而new构造函数的返回值有点特殊
若是构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象做为调用结果
function fn(){ this.a = 2; return; } var test = new fn(); console.log(test);//{a:2}
若是构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象
var obj = {a:1}; function fn(){ this.a = 2; return obj; } var test = new fn(); console.log(test);//{a:1}
因此,针对丢失new的构造函数的解决办法是在构造函数内部使用instanceof判断是否使用new命令,若是发现没有使用,则直接使用return语句返回一个实例对象
function Person(){ if(!(this instanceof Person)){ return new Person(); } this.age = 30; } var person1 = Person(); console.log(person1.age);//30 var person2 = new Person(); console.log(person2.age);//30
使用构造函数的好处在于全部用同一个构造函数建立的对象都具备一样的属性和方法
function Person(name){ this.name = name; this.sayName = function(){ console.log(this.name); } } var person1 = new Person('bai'); var person2 = new Person('hu'); person1.sayName();//'bai'
构造函数容许给对象配置一样的属性,可是构造函数并无消除代码冗余。使用构造函数的主要问题是每一个方法都要在每一个实例上从新建立一遍。在上面的例子中,每个对象都有本身的sayName()方法。这意味着若是有100个对象实例,就有100个函数作相同的事情,只是使用的数据不一样。
function Person(name){ this.name = name; this.sayName = function(){ console.log(this.name); } } var person1 = new Person('bai'); var person2 = new Person('hu'); console.log(person1.sayName === person2.sayName);//false
能够经过把函数定义转换到构造函数外部来解决问题
function Person(name){ this.name = name; this.sayName = sayName; } function sayName(){ console.log(this.name); } var person1 = new Person('bai'); var person2 = new Person('hu'); console.log(person1.sayName === person2.sayName);//true
可是,在全局做用域中定义的函数实际上只能被某个对象调用,这让全局做用域有点名存实亡。并且,若是对象须要定义不少方法,就要定义不少全局函数,严重污染全局空间,这个自定义的引用类型没有封装性可言了
若是全部的对象实例共享同一个方法会更有效率,这就须要用到下面所说的原型对象 。
提及原型对象,就要说到原型对象、实例对象和构造函数的三角关系 。
例如:
function Foo(){}; var f1 = new Foo;
构造函数
用来初始化新建立的对象的函数是构造函数。在例子中,Foo()函数是构造函数
实例对象
经过构造函数的new操做建立的对象是实例对象,又经常被称为对象实例。能够用一个构造函数,构造多个实例对象。下面的f1和f2就是实例对象
function Foo(){}; var f1 = new Foo; var f2 = new Foo; console.log(f1 === f2);//false
原型对象及prototype
经过构造函数的new操做建立实例对象后,会自动为构造函数建立prototype属性,该属性指向实例对象的原型对象。经过同一个构造函数实例化的多个对象具备相同的原型对象。下面的例子中,Foo.prototype是原型对象
function Foo(){}; Foo.prototype.a = 1; var f1 = new Foo; var f2 = new Foo; console.log(Foo.prototype.a);//1 console.log(f1.a);//1 console.log(f2.a);//1
proto
实例对象内部包含一个proto属性(IE10-浏览器不支持该属性),指向该实例对象对应的原型对象
function Foo(){}; var f1 = new Foo; console.log(f1.__proto__ === Foo.prototype);//true
isPrototypeOf
通常地,能够经过isPrototypeOf()方法来肯定对象之间是不是实例对象和原型对象的关系
function Foo(){}; var f1 = new Foo; console.log(f1.__proto__ === Foo.prototype);//true console.log(Foo.prototype.isPrototypeOf(f1));//true
Object.getPrototypeOf()
ES5新增了Object.getPrototypeOf()方法,该方法返回实例对象对应的原型对象
function Foo(){}; var f1 = new Foo; console.log(Object.getPrototypeOf(f1) === Foo.prototype);//true
实际上,Object.getPrototypeOf()方法和proto属性是一回事,都指向原型对象
function Foo(){}; var f1 = new Foo; console.log(Object.getPrototypeOf(f1) === f1.__proto__ );//true
当读取一个对象的属性时,javascript引擎首先在该对象的自有属性中查找属性名字。若是找到则返回。若是自有属性不包含该名字,则javascript会搜索proto中的对象。若是找到则返回。若是找不到,则返回undefined
var o = {}; console.log(o.toString());//'[object Object]' o.toString = function(){ return 'o'; } console.log(o.toString());//'o' delete o.toString; console.log(o.toString());//'[objet Object]'
in操做符能够判断属性在不在该对象上,但没法区别自有仍是继承属性
var o = {a:1}; var obj = Object.create(o); obj.b = 2; console.log('a' in obj);//true console.log('b' in obj);//true console.log('b' in o);//false
//Object.create()是建立对象的一种方法,等价于 function Test(){}; var obj = new Test; Test.prototype.a = 1; obj.b = 2; console.log('a' in obj);//true console.log('b' in obj);//true console.log('b' in Test.prototype);//false
经过hasOwnProperty()方法能够肯定该属性是自有属性仍是继承属性.
var o = {a:1}; var obj = Object.create(o); obj.b = 2; console.log(obj.hasOwnProperty('a'));//false console.log(obj.hasOwnProperty('b'));//true
因而能够将hasOwnProperty方法和in运算符结合起来使用,用来鉴别原型属性
function hasPrototypeProperty(object,name){ return name in object && !object.hasOwnProperty(name); }
原型对象的共享机制使得它们成为一次性为全部对象定义方法的理想手段。
能够利用该机制实现完整的面向对象的写法。
function Person(name){ this.name = name; } Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person('bai'); var person2 = new Person('hu'); person1.sayName();//'bai'
虽然能够在原型对象上一一添加属性,可是直接用一个对象字面形式替换原型对象更简洁
function Person(name){ this.name = name; } Person.prototype = { sayName: function(){ console.log(this.name); }, toString : function(){ return '[person ' + this.name + ']' } }; var person1 = new Person('bai'); console.log(person1 instanceof Person);//true console.log(person1.constructor === Person);//false console.log(person1.constructor === Object);//true
构造函数、原型对象和实例对象之间的关系是实例对象和构造函数之间没有直接联系.
例如:
function Foo(){}; var f1 = new Foo;
以上代码的原型对象是Foo.prototype,实例对象是f1,构造函数是Foo
学习如何建立对象是理解面向对象编程的第一步,第二步是理解继承。开宗明义,继承是指在原有对象的基础上,略做修改,获得一个新的对象。javascript主要包括类式继承、原型继承和拷贝继承这三种继承方式。
大多数面向对象的编程语言都支持类和类继承的特性,而JS却不支持这些特性,只能经过其余方法定义并关联多个类似的对象,如new和instanceof。不过在后来的ES6中新增了一些元素,好比class关键字,但这并不意味着javascript中是有类的,class只是构造函数的语法糖而已
类式继承的主要思路是,经过构造函数实例化对象,经过原型链将实例对象关联起来。
javascript使用原型链做为实现继承的主要方法,实现的本质是重写原型对象,代之以一个新类型的实例。下面的代码中,原来存在于SuperType的实例对象中的属性和方法,如今也存在于SubType.prototype中了。
function Super(){ this.value = true; } Super.prototype.getValue = function(){ return this.value; }; function Sub(){} //Sub继承了Super Sub.prototype = new Super(); Sub.prototype.constructor = Sub; var instance = new Sub(); console.log(instance.getValue());//true
原型链最主要的问题在于包含引用类型值的原型属性会被全部实例共享,而这也正是为何要在构造函数中,而不是在原型对象中定义属性的缘由。在经过原型来实现继承时,原型实际上会变成另外一个类型的实例。因而,原先的实例属性也就瓜熟蒂落地变成了如今的原型属性了
function Super(){ this.colors = ['red','blue','green']; } function Sub(){}; //Sub继承了Super Sub.prototype = new Super(); var instance1 = new Sub(); instance1.colors.push('black'); console.log(instance1.colors);//'red,blue,green,black' var instance2 = new Sub(); console.log(instance2.colors);//'red,blue,green,black'
原型链的第二个问题是,在建立子类型的实例时, 不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响全部对象实例的状况下,给超类型的构造函数传递参数。再加上包含引用类型值的原型属性会被全部实例共享的问题,在实践中不多会单独使用原型链继承
借用构造函数(constructor stealing)的技术(有时候也叫作伪类继承或经典继承)。基本思想至关简单,即在子类型构造函数的内部调用超类型构造函数,经过使用apply()和call()方法在新建立的对象上执行构造函数。
function Super(){ this.colors = ['red','blue','green']; } function Sub(){ //继承了Super Super.call(this); } var instance1 = new Sub(); instance1.colors.push('black'); console.log(instance1.colors);// ['red','blue','green','black'] var instance2 = new Sub(); console.log(instance2.colors);// ['red','blue','green']
相对于原型链而言,借用构造函数有一个很大的优点,便可以在子类型构造函数中向超类型构造函数传递参数
function Super(name){ this.name = name; } function Sub(){ //继承了Super,同时还传递了参数 Super.call(this,"bai"); //实例属性 this.age = 29; } var instance = new Sub(); console.log(instance.name);//"bai" console.log(instance.age);//29
可是,若是仅仅是借用构造函数,那么也将没法避免构造函数模式存在的问题——方法都在构造函数中定义,所以函数复用就无从谈起了。
组合继承(combination inheritance)有时也叫伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而经过借用构造函数来实现对实例属性的继承。这样,既经过在原型上定义方法实现了函数复用,又可以保证每一个实例都有它本身的属性
function Super(name){ this.name = name; this.colors = ['red','blue','green']; } Super.prototype.sayName = function(){ console.log(this.name); }; function Sub(name,age){ //继承属性 Super.call(this,name); this.age = age; } //继承方法 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; Sub.prototype.sayAge = function(){ console.log(this.age); } var instance1 = new Sub("bai",29); instance1.colors.push("black"); console.log(instance1.colors);//['red','blue','green','black'] instance1.sayName();//"bai" instance1.sayAge();//29 var instance2 = new Sub("hu",27); console.log(instance2.colors);//['red','blue','green'] instance2.sayName();//"hu" instance2.sayAge();//27
组合继承有它本身的问题。那就是不管什么状况下,都会调用两次父类型构造函数:一次是在建立子类型原型的时候,另外一次是在子类型构造函数内部。子类型最终会包含父类型对象的所有实例属性,但不得不在调用子类型构造函数时重写这些属性
function Super(name){ this.name = name; this.colors = ["red","blue","green"]; } Super.prototype.sayName = function(){ return this.name; }; function Sub(name,age){ // 第二次调用Super(),Sub.prototype又获得了name和colors两个属性,并对上次获得的属性值进行了覆盖 Super.call(this,name); this.age = age; } //第一次调用Super(),Sub.prototype获得了name和colors两个属性 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; Sub.prototype.sayAge = function(){ return this.age; };
解决两次调用的方法是使用寄生组合式继承。寄生组合式继承与组合继承类似,都是经过借用构造函数来继承不可共享的属性,经过原型链的混成形式来继承方法和可共享的属性。只不过把原型继承的形式变成了寄生式继承。使用寄生组合式继承能够没必要为了指定子类型的原型而调用父类型的构造函数,从而寄生式继承只继承了父类型的原型属性,而父类型的实例属性是经过借用构造函数的方式来获得的
function Super(name){ this.name = name; this.colors = ["red","blue","green"]; } Super.prototype.sayName = function(){ return this.name; }; function Sub(name,age){ Super.call(this,name); this.age = age; } if(!Object.create){ Object.create = function(proto){ function F(){}; F.prototype = proto; return new F; } } Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; var instance1 = new Sub("bai",29); instance1.colors.push("black"); console.log(instance1.colors);//['red','blue','green','black'] instance1.sayName();//"bai" var instance2 = new Sub("hu",27); console.log(instance2.colors);//['red','blue','green'] instance2.sayName();//"hu"
这个例子的高效率体如今它只调用了一次Super构造函数,而且所以避免了在Sub.prototype上面建立没必要要的、多余的属性。与此同时,原型链还保持不变
所以,开发人员广泛认为寄生组合式继承是引用类型最理想的继承范式,YUI的YAHOO.lang.extend()方法就采用了这种继承模式
若是使用ES6中的class语法,则上面代码修改以下
class Super { constructor(name){ this.name = name; this.colors = ["red","blue","green"]; } sayName(){ return this.name; } } class Sub extends Super{ constructor(name,age){ super(name); this.age = age; } } var instance1 = new Sub("bai",29); instance1.colors.push("black"); console.log(instance1.colors);//['red','blue','green','black'] instance1.sayName();//"bai" var instance2 = new Sub("hu",27); console.log(instance2.colors);//['red','blue','green'] instance2.sayName();//"hu"
ES6的class语法糖隐藏了许多技术细节,在实现一样功能的前提下,代码却优雅很多
原型继承,在《你不知道的javascript》中被翻译为委托继承
道格拉斯·克罗克福德(Douglas Crockford)在2006年写了一篇文章,《javascript中的原型式继承》。在这篇文章中,他介绍了一种实现继承的方式,这种方式并无使用严格意义上的构造函数。他的想法是借助原型能够基于已有的对象来建立新对象,同时没必要所以建立自定义类型
原型继承的基础函数以下所示:
function object(o){ function F(){}; F.prototype = o; return new F(); }
在object()函数内部,先建立了一个临时性的构造函数,而后将传入的对象做为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制
例如:
var superObj = { init: function(value){ this.value = value; }, getValue: function(){ return this.value; } } var subObj = object(superObj); subObj.init('sub'); console.log(subObj.getValue());//'sub'
ES5经过新增Object.create()方法规范化了原型式继承
var superObj = { init: function(value){ this.value = value; }, getValue: function(){ return this.value; } } var subObj = Object.create(superObj); subObj.init('sub'); console.log(subObj.getValue());//'sub'
原型继承虽然只是看上去将原型链继承的一些程序性步骤包裹在函数里而已。可是,它们的一个重要区别是父类型的实例对象再也不做为子类型的原型对象
一、使用原型链继承
function Super(){ this.value = 1; } Super.prototype.value = 0; function Sub(){}; //将父类型的实例对象做为子类型的原型对象 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; //建立子类型的实例对象 var instance = new Sub; console.log(instance.value);//1
二、使用原型继承
function Super(){ this.value = 1; } Super.prototype.value = 0; function Sub(){}; Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; //建立子类型的实例对象 var instance = new Sub; console.log(instance.value);//0
上面的Object.create函数一行代码Sub.prototype = Object.create(Super.prototype)能够分解为
function F(){}; F.prototype = Super.prototype; Sub.prototype = new F();
由上面代码看出,子类的原型对象是临时类F的实例对象,而临时类F的原型对象又指向父类的原型对象;因此,实际上,子类能够继承父类的原型上的属性,但不能够继承父类的实例上的属性
拷贝继承在《javascript面向对象摘要》中翻译为混入继承,jQuery使用的就是拷贝继承
拷贝继承不须要改变原型链,经过拷贝函数将父例的属性和方法拷贝到子例便可
下面是一个深拷贝的拷贝函数
function extend(obj,cloneObj){ if(typeof obj != 'object'){ return false; } var cloneObj = cloneObj || {}; for(var i in obj){ if(typeof obj[i] === 'object'){ cloneObj[i] = (obj[i] instanceof Array) ? [] : {}; arguments.callee(obj[i],cloneObj[i]); }else{ cloneObj[i] = obj[i]; } } return cloneObj; } var obj1={a:1,b:2,c:[1,2,3]}; var obj2=extend(obj1); console.log(obj1.c); //[1,2,3] console.log(obj2.c); //[1,2,3] obj2.c.push(4); console.log(obj2.c); //[1,2,3,4] console.log(obj1.c); //[1,2,3]
因为拷贝继承解决了引用类型值共享的问题,因此其彻底能够脱离构造函数实现对象间的继承
function extend(obj,cloneObj){ if(typeof obj != 'object'){ return false; } var cloneObj = cloneObj || {}; for(var i in obj){ if(typeof obj[i] === 'object'){ cloneObj[i] = (obj[i] instanceof Array) ? [] : {}; arguments.callee(obj[i],cloneObj[i]); }else{ cloneObj[i] = obj[i]; } } return cloneObj; } var superObj = { arrayValue:[1,2,3], init: function(value){ this.value = value; }, getValue: function(){ return this.value; } } var subObj = extend(superObj); subObj.arrayValue.push(4); console.log(subObj.arrayValue);//[1,2,3,4] console.log(superObj.arrayValue);//[1,2,3]
若是要使用构造函数,则属性可使用借用构造函数的方法,而引用类型属性和方法使用拷贝继承。至关于再也不经过原型链来创建对象之间的联系,而经过复制来获得对象的属性和方法
function extend(obj,cloneObj){ if(typeof obj != 'object'){ return false; } var cloneObj = cloneObj || {}; for(var i in obj){ if(typeof obj[i] === 'object'){ cloneObj[i] = (obj[i] instanceof Array) ? [] : {}; arguments.callee(obj[i],cloneObj[i]); }else{ cloneObj[i] = obj[i]; } } return cloneObj; } function Super(name){ this.name = name; this.colors = ["red","blue","green"]; } Super.prototype.sayName = function(){ return this.name; }; function Sub(name,age){ Super.call(this,name); this.age = age; } Sub.prototype = extend(Super.prototype); var instance1 = new Sub("bai",29); instance1.colors.push("black"); console.log(instance1.colors);//['red','blue','green','black'] instance1.sayName();//"bai" var instance2 = new Sub("hu",27); console.log(instance2.colors);//['red','blue','green'] instance2.sayName();//"hu"
上面介绍了几种继承方式,其中最多见的是类式继承。再加上ES6语法糖的缘故,因此致使更多的人使用。对于 通常开发来讲,类式继承也足以应付。