JavaScript面向对象
创建对象
var person = new Object(); person.name="sjq"; person.age=25; person.job="Software Engneer"; person.sayName=function(){ alert(this.name); } person.sayName();
这种方式创建对象会产生大量的重复代码。
例如:已经有了person对象了。若要再创建person1对象,就麻烦了。
单例模式
用JSON来创建对象,是一种非常方便的单例模式。
var singleton = { name:"js", sayHi:function(){ alert("hi"); } };
例:Ajax对象,在程序中应该之后一个所以是单例的。
工厂模式
工厂模式可以解决以上创建对象会产生代码重复的问题。
function createPerson(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName=function(){ alert(this.name); } return o; }
var person = createPerson("sjq",25,"Software Engineer"); person.sayName();
但是工厂模式无法解决对象识别的问题。
构造函数模式
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName=function(){ alert(this.name); } } var person1 = new Person("sjq",25,"Software Engineer");
注意:
按照惯例,构造函数应该用大写字母开头,如“Person”。非构造函数使用小写字母开头,如“createPerson”。
new操作符
任何函数使用new操作符调用,那么它就可以作为构造函数。
类型检测
(1)对象的constructor属性最初是用来标识对象类型的。
person1.constructor == Person //true
(2)实际使用中,检测对象类型,instanceof操作符更可靠。
person1 instanceof Object //true person1 instanceof Person //true
1.将构造函数当作函数
不使用new操作符,函数中的this,将绑定到window对象上。
Person("sjq",25,"Software Engineer"); //this绑定到window对象上。 window.sayName(); //弹出"sjq" //在另一个对象的作用域中调用。使用call()或apply() var o = new Object(); Person.call(o,"sjq",25,"Software Engineer"); //this绑定到o对象上。 o.sayName(); //弹出"sjq"
2、构造函数的问题
使用构造函数,每个方法都要在每个实例上重新创建一遍。
即,对象方法的内存不是共用的。
例:
var person1 = new Person("sjq",25,"Software Engineer"); var person2 = new Person("sjq",25,"Software Engineer"); person1 == person2; //false
有一种解决方式,将函数的定义转移到构造函数外部。
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName=sayName; } function sayName(){ alert(this.name); } var person1 = new Person("sjq",25,"Software Engineer"); var person2 = new Person("sjq",25,"Software Engineer"); person1 == person2; //true
但是新问题是,全局作用域中定义的函数实际上只能被某对象调用,这让全局函数名不副实。而且,若对象有很多方法,则要定义多个全局函数,从而导致封装性的破坏。
原型模式
每个函数都有一个prototype(原型)属性,该属性是一个对象,用途是包含可以由特定类型的所有实例共享的属性和方法。
prototype是通过调用构造函数而创建的那个对象的原型对象。使用原型对象可以让所有对象实例共享它所包含的属性和方法。即,不必再构造函数中定义对象信息,可以将这些信息直接添加到原型对象中。
function Person(){ } Person.prototype.name="sjq"; Person.prototype.age=25; Person.prototype.job="Software Engneer"; Person.prototype.sayName=function(){ alert(this.name); } var person1 = new Person("sjq",25,"Software Engineer"); var person2 = new Person("sjq",25,"Software Engineer"); person1 == person2; //true
与构造函数不同的是,新创建的对象的属性和方法是所有实例共享的。
1、理解原型
每个函数都有一个prototype(原型)属性,prototype属性都有一个constructor(构造函数)属性。constructor属性是一个指向prototype属性所在函数的指针。
例,Person.prototype.constructor指向Person。
Person.prototype.constructor === Person //true
通过构造函数可以继续为原型添加其他属性和方法。
创建自定义的构造函数后,其原型属性默认只会取得constructor属性;至于其他方法,则是从Object继承而来。当调用构造函数创建一个新实例后,该实例的内部包含一个指针(内部属性),指向构造函数的原型属性。
通常,该内部属性名称为__proto__。(某些宿主环境下,__proto__可能是不可见的。)
要明确的真正重要一点,是这个链接存在于实例与构造函数的原型属性之间,而不是存在于实例与构造函数之间。
person1中虽然不包含属性和方法,但可以调用person1.sayName()。这是通过查找对象属性的过程来实现的。
由于某些实现中无法访问对象内部的__proto__属性,但可以通过isPrototypeOf()方法确定对象之间是否存在关系。
Person.prototype.isPrototypeOf(person1); //true
每当代码读取某个对象的属性时,都会执行一次搜索,目标是具有指定名字的属性。搜索从对象本身开始,若搜索到,则返回属性值;若无法搜索到,则继续搜索原型对象。
即对象实例的属性会屏蔽原型对象中同名的属性。
例:添加属性屏蔽原型对象中的属性。
function Person(){ } Person.prototype.name="sjq"; Person.prototype.age=25; Person.prototype.job="Software Engneer"; Person.prototype.sayName=function(){ alert(this.name); } var person1 = new Person(); var person2 = new Person(); person1.name="super man"; //在实例中添加属性。 alert(person1.name); //super man alert(person2.name); //sjq
实例中添加重名属性,不会修改实例原型对象中的属性。
例:添加属性屏蔽原型对象中的属性,使用delete删除实例中的重名属性。
function Person(){ } Person.prototype.name="sjq"; Person.prototype.age=25; Person.prototype.job="Software Engneer"; Person.prototype.sayName=function(){ alert(this.name); } var person1 = new Person(); var person2 = new Person(); person1.name="super man"; //在实例中添加属性。 alert(person1.name); //super man alert(person2.name); //sjq delete person1.name; //删除person1对象中的name属性。 alert(person1.name); //sjq
hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。该方法只在给定属性存在于对象实例中,才返回true。
function Person(){ } Person.prototype.name="sjq"; Person.prototype.age=25; Person.prototype.job="Software Engneer"; Person.prototype.sayName=function(){ alert(this.name); } var person1 = new Person(); var person2 = new Person(); alert(person1.hasOwnProperty("name")); //false person1.name="super man"; //在实例中添加属性。 alert(person1.name); //super man alert(person1.hasOwnProperty("name")); //true alert(person2.name); //sjq alert(person2.hasOwnProperty("name")); //false delete person1.name; //删除person1对象中的name属性。 alert(person1.name) //sjq alert(person1.hasOwnProperty("name")); //false
2、原型与in操作符
(1) 单独使用in
不区分实例与原型,只要可以搜索到指定的属性,in操作符即返回true。
function Person(){ } Person.prototype.name="sjq"; Person.prototype.age=25; Person.prototype.job="Software Engneer"; Person.prototype.sayName=function(){ alert(this.name); } var person1 = new Person(); var person2 = new Person(); alert(person1.hasOwnProperty("name")); //false alert("name" in person1); //true person1.name="super man"; //在实例中添加属性。 alert(person1.name); //super man alert(person1.hasOwnProperty("name")); //true alert("name" in person1); //true alert(person2.name); //sjq alert(person2.hasOwnProperty("name")); //false alert("name" in person2); //true delete person1.name; //删除person1对象中的name属性。 alert(person1.name) //sjq alert(person1.hasOwnProperty("name")); //false alert("name" in person1); //true
(2)判断属性存在与否,存在的位置。
通过in操作符与hasOwnPrototype()方法配合使用可以确定属性是原型中的属性,还是对象中的,还是不存在的。
(3)for - in 循环
通过for-in语句可以便利对象和对象原型中可访问的、可枚举的属性。
var o = { toString:function(){ return “My Object"; } } for(var prop in o){ if(prop=="toString"){ alert(“Found toString"); //在IE中不会显示 } }
IE中toString被标记为不可枚举。
3、更简单的原型语法
function Person(){ } Person.prototype = { name:"sjq"; age:25; job:"Software Engneer"; sayName:function(){ alert(this.name); } }
这种方式constructor属性不再指向Person。因为此处重写了默认的prototype读写,即constructor属性变成了新对象的constructor属性(指向Object构造函数),不再指向Person()函数。
var person = new Person(); alert(person instanceof Object); //true alert(person instanceof Person); //true alert(person.constructor == Object); //true alert(person.constructor == Person); //false
constructor的值比较重要。可以按照以下方式设置成适当的值。
function Person(){ } Person.prototype = { constructor:Person, name:"sjq", age:25, job:"Software Engneer", sayName:function(){ alert(this.name); } } var person = new Person();
alert(person instanceof Object); //true alert(person instanceof Person); //true alert(person.constructor == Object); //false alert(person.constructor == Person); //true
4、原型的动态性
动态的在原型对象中添加属性,实例对象通过上属性查找机制,可以访问到最新添加的属性。即可以随时为原型添加方法。
function Person(){ } Person.prototype.sayHi = function(){ alert("hi"); } var person = new Person(); person.sayHi(); //"hi"
对象实例与原型之间的连接是一个指针。注意,实例的指针指向原型,而不指向构造函数。
但是若重写整个原型对象。调用构造函数时会为实例添加一个指向最初原型的__proto__指针,而把原型修改为另一个对象,这切断构造函数与最初原型之间的联系。
function Person(){ } var person = new Person(); Person.prototype = { constructor:Person, name:"sjq", age:25, job:"Software Engneer", sayName:function(){ alert(this.name); } } person.sayName(); //error
即重写原型对象后,它们的引用仍然是最初的原型。
5、原生对象的原型
所有的元素的引用类型,都采用原型模式创建。
通过原生对象的原型,不仅可以取得所有默认方法的引用,且可以定义新方法。可以修改自定义对象的原型一样修改原生对象的原型,添加新方法。
例:
String.protoperty.startsWith = function(text){ return this.indexOf(text)==0; }
var msg="Hello World!"; alert(msg.startsWith ("Hello")); //true
注意:不推荐在实际使用中这样做。
6、原型对象的问题
原型模式的最大问题是由其共享的本性导致的。
单独使用原型模式,会使得对象的属性共享。修改一个对象的属性,会影响另外一个对象。
构造函数模式与原型模式的组合使用
这是创建自定义类型的最常见方法。对象拥有各自的字段,且共享方法。最大限度的节省了内存。
function Person(name.age,job){ this.name = name; this.age = age; this.job = job; } Person.prototype = { constructor:Person, sayName:function(){ alert(this.name); } }
动态原型模式
以上方案中,构造函数的定义和原型的初始化是分离的。这与传统OO有些偏离。
通过动态原型模式可以将所有信息封装在构造函数内部。
通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
例:
function Person(name.age,job){ //字段 this.name = name; this.age = age; this.job = job; //方法 if(typeof this.sayName != "function"){ Person.prototype.sayName = function(){ alert(this.name); } Person.prototype.sayHi = function(){ alert("hi"); } } } var person = new Person("sjq",25,"Software Engineer"); person.sayName(); person.sayHi();
原型会在第一个对象创建时被初始化。第二个对象创建时,不再创建原型对象。
说明:只需要一个if判断是不是原型已经被初始化了。
注意,动态原型模式不可以使用字面量重写原型。因为若已经创建实例的情况下,重写原型对象会切断现有实例与新原型之间的联系。
这种方法已经非常完美了。
寄生构造函数模式
基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后在返回创建的对象。
从表面上看,这个函数是典型的构造函数。
例:
function Person(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name); } return o; } var person = new Person("sjq",25,"Software Engineer"); person.sayName();
这种模式在特殊情况系用来为对象创建构造函数。
例:创建一个额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式:
function SpecialArray(){ //创建数组 var values = new Array(); values.push.apply(values,arguments); //添加方法 values.toPipedString = function(){ return this.join("|"); } //返回数组 return values; } var colors = new SpecialArray("red","blue","green"); alert(colors.toPipedString()); //"red|blue|green"
寄生构造函数模式,返回的对象与构造函数或者与构造函数的运行属性之间没有关系;也即,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。因此,不能用来instanceof操作符来确定对象类型。
建议在其他模式可以使用的情况下,不要使用寄生构造函数模式。
稳妥构造函数模式
稳妥对象,即没有公共属性,且其方法也不引用this的对象。稳妥对象适合在一些安全环境中(这些环境禁用this和new),或者在防止数据被其他应用程序改动时使用。
稳妥构造函数遵循与寄生构造函数类似的模式,有两点不同:
一是新创建对象的实例不引用this。
二是不使用new操作符调用构造函数。
例:
function Person(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(name); } return o; } var person = Person("sjq",25,"Software Engineer"); person.sayName();
person是一个稳妥对象,除了调用sayName()方法外,没有别的方法可以访问其数据成员。即使有其他代码给这个对象添加方法或数据成员,但也不能访问传入到参数函数中的原生数据。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全指向环境下使用。如ADsage和Caja。
注意:稳妥构造函数模式创建的对象与构造函数之间没有什么关系,一次instanceof操作符对这种对象也没有意义。
继承
继承主要依靠原型链来实现。继承没有一个标准方法,实现方法也比较多。
原型链
基本思想是利用原型让一个引用类型集成另一个引用类型的属性和方法。
即让原型对象等于另一个类型的实例,原型对象将包含一个指向一个原型的指针,相应的另一个原型中也包含着一个指向另一个构造函数的指针。
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } SubType.prototype = new SuperType(); //SubType继承SuperType SubType.prototype.getSubValue = function(){ return this.subproperty; } var instance = new SubType(); alert(instance.getSuperValue()); //true alert(instance instanceof Object); //true alert(instance instanceof SuperType); //true alert(instance instanceof SubType); //true alert(Object.prototype.isPrototypeOf(instance)); //true alert(SuperType.prototype.isPrototypeOf(instance)); //true alert(SubType.prototype.isPrototypeOf(instance)); //true
属性查处,当访问某属性时,搜索从实例对象开始,一直顺着原型链向上查找。
不要忘记Object。
确定原型对象和实例的关系
方法一,使用instanceof操作符,只要用该操作符来测试实例与原型链中出现过的构造函数,就会返回true。
方法二,使用isPrototypeOf()方法,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,isPrototypeOf()方法就会返回true。
谨慎定义方法
子类型优势需要重写超类型中的某个方法,或许需要添加超类型中不存在的某个方法。
需要注意,通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样会重写原型链。
原型链的问题
原型链的主要问题来自包含引用类型值的原型。即原型对象中的字段属性会被共享。
第二个问题是,创建子类型实例时,不能向超类型的构造函数中传递参数。
因此实践中很少单独使用原型链。
借用构造函数
借用构造函数技术,也叫做伪造对象或经典继承。这种技术的基本思想相对简单,即在子类型构造函数的内部调用超类型构造函数。
函数只不过是在特定换中哦你指向代码的对象,通过使用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"
借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
function SuperType(name){ this.name = name; } function SubType(){ //继承SuperType SuperType.call(this,"sjq"); this.age=29; } var instance1 = new SubType(); alert(instance1.name); //"sjq" alert(instance1.age); //"29"
实际上是为SubType的实例设置了name属性。
借用构造函数的问题
无法避免构造函数模式存在的问题——方法都在构造函数中定义,函数复用无从谈起。且超类型的原型中定义的方法,对子类型是不可见的,结果所有的类型都只能使用构造函数模式。
借用构造函数的技术也是很少单独使用的。
组合继承
组合集成,也称为伪经典继承。指的是将原型链和借用构造函数的技术组合到一起,从而发挥二者之长的一种继承模式。思路:使用原型链实现对原型属性和方法的继承,从而借用构造函数来实现对实例属性的基础。
function SuperType(name){ this.name = name; this.colors = ["red","blue","green"]; } SuperType.prototype.sayName = function(){ alert(this.name); } function SubType(name,age){ //继承SuperType属性 SuperType.call(this,name); //第一次调用SuperType() this.age = age; } //继承SuperType方法 SubType.prototype = new SuperType(); //第一次调用SuperType() SubType.prototype.sayAge = function(){ alert(this.age); }
instanceof和isPrototypeOf()也能够用于识别基于继承创建的对象。
组合继承是JavaScript中最常见的继承模式。
问题
最大的问题就是需要调用两次超类型构造函数。
个人认为这里使用
SubType.prototype = new SuperType();
这样的方式做继承不是很妥当。没有必要让SubType的原型对象整个都指向SuperType对象,而且,SubType若必须传入一个参数,则这样调用可能导致SubType运行出错。
改进,利用for遍历SuperType的方法。
function SuperType(name){ this.name = name; this.colors = ["red","blue","green"]; } SuperType.prototype.sayName = function(){ alert(this.name); } function SubType(name,age){ //继承SuperType属性 SuperType.call(this,name); //第一次调用SuperType() this.age = age; } //SubType继承SuperType方法 for(var i in SuperType.prototype ){ SubType.prototype[i] = A.prototype[i]; } SubType.prototype.sayAge = function(){ alert(this.age); }
原型式继承
这种方法没有严格使用严格意义上的构造函数。借助原型可以基于已有对象创建新对象,同时还不必因此创建自定义类型。
function object(o){ function F(){} F.prototype = o; return new F(); }
在object()函数内部,先创建一个临时性的构造函数,然后将传入的对象做这个构造函数的原型,最后返回这个老师类型的一个新实例。object()对传入的对象执行了一次浅复制。
例:
var person = { name:"sjq", friends:["f1","f2","f3"]; } var anotherPerson = object(person); anotherPerson.name="Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = other(person); yetAnotherPerson.name ="Linda"; yetAnotherPerson.friends.push("Barbie"); alert(person.friends); //"f1,f2,f3,Rob,Barbie" //person原型对象的friends属性被anotherPerson 与yetAnotherPerson 共享。
在没有构造函数,而只是需要让一个对象与另一个对象保持继承的关系,可以使用原型继承。这种继承是一种动态的继承。
但是原型式继承会导致引用类型值的属性被共享。
寄生式继承与原型式继承
寄生式继承的思路与构造海牛属和工程模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。
function createAnther(orgiginal){ var clone = object(orgiginal); //通过调用函数创建一个新对象。 clone.sayHi = function(){ //以某种方式增强对象。 alert("hi"); }; return clone; //返回对象。 } var person = { name:"sjq", friends:["f1","f2","f3"] } var anotherPerson = createAnther(person); anotherPerson.sayHi(); //"hi"
新对象具有person的所有属性和方法,且具有自己的sayHi()方法。
object()方法不是必须的,任何能返回对象的方法均能使用该模式。
寄生式继承为对象添加函数,无法做到函数的复用。
寄生组合式继承
借用构造函数来继承属性,通过原型链的混成形式来继承方法。
基本思路是,不必为了指定子类型的原型而调用超类型的构造函数,只需要超类型原型的一个副本。
基本模式如下所示:
function inheritPrototype(subType, superType){ var prototype = object(superType.prototype); //创建对象 prototype.constructor = subType; //创建对象 subType.prototype = prototype; //指定对象 }
例:
function SuperType(name){ this.name = name; this.colors = ["red","blue","green"]; } SuperType.prototype.sayName = function(){ alert(this.name); } function SubType(name,age){ SuperType.call(this,name); this.age = age; } //继承SuperType方法 inheritPrototype(SubType,SuperType); SubType.prototype.sayAge = function(){ alert(this.age); }
该模式下instanceof和isPrototypeOf()可以正常使用。
普遍认为寄生组合继承是实现继承的最有效方式。YUI的YAHOO.lang.extend()方法采用了寄生组合继承。
附:this,prototype,constructor
this
this表示当前对象,如果在全局作用范围内使用this,则指代当前页面对象window; 如果在函数中使用this,this为调用函数的对象。
prototype
prototype本质上还是一个JavaScript对象。 并且每个函数都有一个默认的prototype属性。
constructor
constructor始终指向创建当前对象的构造函数。