有次面试的时候,面试官让我谈谈对面向对象的理解,让我一下蒙了,竟然不知道从哪里说起?自己都是在看完视频后,直接用面向对象写东西,也没有好好梳理,导致自己只会简单的用一下,却不会说。于是我就翻了翻《JavaScript高级程序设计》,对其进行整理了一下。
1.什么是对象
在ECMAScript中,对象就是一堆无序属性的集合,这些属性可以是基本值,也可以是别的对象和函数。所以我们也可以吧对象当成一组名值对,一个属性名对应一个值,值可以是数据或方法。
2.创建一个对象
(1)创建一个最简单的对象是通过创建一个Object的实例,再为它添加属性和方法:
var person = new Object(); person.name = "Tom"; person.age = 18; person.sayName = function(){ alert(this.name); };
(2)使用Object创建对象的缺点很明显,就是它只能创建一个对象,所以我们开始使用工厂模式来创建对象
function createPerson(name,age,job){ var o = new Object(); o.name = name; o.job = job; o.age = age; o.sayname = function(){ alert("你好" + name); } return o; } var person1 = createPerson("tom",16,"doctor"); person1.sayname(); var person2 = createPerson("Candy",18,"police");
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题,即怎样知道一个对象的类型。
(3)构造函数模式
构造函数模式就可以创建特定类型的对象,我们将上一个函数改成构造函数如下:
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayname = function(){ alert("My name is" + this.name); }; }var person1 = new Person("tom",16,"doctor"); var person2 = new Person("Candy",18,"police"); alert(person1 instanceof Object); alert(person1 instanceof Person);
上面的函数与工厂模式的主要不同在于它直接将属性复制给this对象,并没有返回值,并且构造函数名都应是首字母大写,以便与其他函数做区别,因为构造函数本身也是函数,只不过可以用来创建对象而已 。
然后是创建Person的实例,这就用到了new操作符,我们就是用new创建了2个实例。在这两种实例中,都包含有一个constructor属性,这个属性指向的就是Person,就是实例的构造函数,例如:
alert(person1.constructor == Person);
这里弹出的就是true,person2同样是这样。而我们最后的instanceof操作符就是为了检测对象类型。上面的两条检测语句弹出的都是true,说明我们创建的实例,既是Person的实例,有属于Object的实例。
但构造函数也有一个缺点,就是相同的属性或方法会创建多遍,这样确实有些不必要。
(4)原型模式
首先,我们要知道每一个函数都有一个prototype的原型属性,这个属性是指向一个对象,这个对象可以包括属性和方法用来给实例共享,也可以说prototype就是通过调用构造函数而创建那个对象实例的原型对象。所以称prototype为原型属性是针对于构造函数的,原型对象是针对创建的实例的
function Person(){ } Person.prototype.name = "Nancy"; Person.prototype.age = 16; Person.prototype.sayName = function(){ alert(this.name); } var person1 = new Person(); var person2 = new Person();
这个原型模式有一个最大的问题,不是数据重复,而是在对于引用类型的属性,例如:
function Person(){ } Person.prototype = { constructor: Person, name : "Nicholas", age : 29, job : "Software Engineer", friends : ["Shelby", "Court"], sayName : function () { alert(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Court,Van" alert(person2.friends); //"Shelby,Court,Van" alert(person1.friends === person2.friends); //true
我们发现我们修改了person1的friends数组,但person2引用的friends也发生改变
(5)构造函数与原型函数相结合使用
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor : Person, sayName : function(){ alert(this.name); } } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Count,Van" alert(person2.friends); //"Shelby,Count" alert(person1.friends === person2.friends); //false alert(person1.sayName === person2.sayName); //true
这种构造函数与原型混成的模式,是目前在 ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。
(6)其它的模式
在《JavaScript高级程序设计》众还有其它的设计模式,如动态原型模式,寄生构造函数模式,稳妥构造函数模式等,但我们平时掌握上一种方法就够用了,所以我就偷个懒,就不介绍这几种了。
1.继承
许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在 ECMAScript 中无法实现接口继承。 ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
(1)原型链
讲到继承就不得不讲一讲原型链了,我们先讲一个对象的原型链:
function Person(name,age){ this.name = name; this.age = age; } Person.prototype.sayName = function(){ alert(this.name); } var person1 = new Person("tom",18);
在上面的例子中,我们创建了一个Person构造函数,这个构造函数有一个原型对象prototype,并且这个原型对象有一个属性constructor,这个属性是指向它的构造函数,即Person,在创建的实例中,person1也有一个原型属性_proto_,这个属性指向prototype,即Person.prototype。总的来说就是:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。这就是圆形脸的基本概念。
利用原型链实现继承可以这么写:
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.SubProperty = false; } //继承了SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ return this.SubProperty; }; var instance = new SubType(); alert(instance.getSuperValue()); //true
利用原型链虽然好用,但缺点也很大,其中一个就是方才讲到的对引用类型属性的改变:
function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){ } //继承了 SuperType SubType.prototype = new SuperType(); var instance1 = new SubType(); instance1.colors.push("black"); alert(instance1.colors); //"red,blue,green,black" var instance2 = new SubType(); alert(instance2.colors); //"red,blue,green,black"
原型链的第二个问题是:在创建子类型的实例时,不能向父类型的构造函数中传递参数。
(2)借用构造函数
这种技术的基本思想相当简单,即在子类型构造函数的内部调用父类型构造函数。因此通过使用 apply()和 call()方法也可以在(将来)新创建的对象上执行构造函数。
function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){ //继承了 SuperType SuperType.call(this); } var instance1 = new SubType(); instance1.colors.push("black"); alert(instance1.colors); //"red,blue,green,black" var instance2 = new SubType(); alert(instance2.colors); //"red,blue,green"
通过使用 call()方法(或 apply()方法也可以),我们实际上是在(未来将要)新创建的 SubType 实例的环境下调用了 SuperType 构造函数。这样一来,就会在新 SubType 对象上执行 SuperType()函数中定义的所有对象初始化代码。
但是,根据我们的套路,如果只是用构造函数的来实现继承,还是有问题的,那就是方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在父类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。所以我们和创建对象一样,采取两者相结合的方法。
(3)组合继承
function SuperType(name){ this.name = name; this.colors = ["red","black"]; } SuperType.prototype.sayName = function(){ alert(this.name); } function SubType(name,age){ //继承属性 SuperType.call(this,name); this.age = age; } //继承方法 SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ alert(this.age); } var insstance = new SubType("tom",16); insstance.sayName(); insstance.sayAge(); insstance.colors.push("yellow"); console.log(insstance.colors); var insstance2 = new SubType("Nancy",100); console.log(insstance2.colors); insstance2.sayName(); insstance2.sayAge();
在这个例子中, SuperType 构造函数定义了两个属性: name 和 colors。 SuperType 的原型定义了一个方法 sayName()。 SubType 构造函数在调用 SuperType 构造函数时传入name 参数,紧接着又定义了它自己的属性 age。然后,将 SuperType 的实例赋值给 SubType 的原型,然后又在该新原型上定义了方法 sayAge()。这样一来,就可以让两个不同的 SubType 实例既分别拥有自己属性——包括 colors 属性,又可以使用相同的方法了。
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。但它也有缺点,就是它在继承的时候会调用两次父类型构造函数 。
(4)这本书还有其他的继承方法,但是对于我这样写一写小项目的来说,上一种就够了,其他的我也只是做一种了解,我就讲一个寄生组合式的继承方式,
我们先写一个利用原型继承的函数
function object(o){ function F(){} F.prototype = o; return new F(); }
我们在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例
//寄生组合式继承 function inheritPrototype(subType,superType){ var prototype = object(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; }
在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。最后一步,将新创建的对象赋值给子类型的原型。
利用这个寄生组合的模式可以解决上面组合继承的不足,被开发人员认为是引用类型最理想的继承范式 。