1,创建对象(先创建一个Object实例,再给它添加属性和方法)
var person=new Object(); person.name="Jim"; person.showName=function(){ alert(this.name); }; person.showName();//Jim
缺点:要创建成千上万个person,就要重写上述代码成千上万次,产生大量的重复的代码。
1.1字面量方式
var person={ name:"Jim", age:24, sayName:function(){ alert(this.name) ; } };
1.2 属性类型(数据属性和访问器属性):
ECMA-262第5版主定义只有内部才用的特性时,描述了属性的各种特征,这些特性是为了实现js引擎用的,因此,js不能直接访问它们,为了表示特性是内部值,该规范把它们放到[[]]里面,例如[[Enumerable]]
[[Configurable]]:表示能否通过delete删除属性从而重新定义属性
[[Enumerable]]:表示能否通过for-in循环来返回属性
[[Writable]]:表示能否修改属性的值
[[Value]]:包含这个属性的数据值,默认是undefined
要修改属性的默认特性,必须使用ECMAScript5的Object.definePorperty()(IE8才实现这个方法,了解一下就好)
var person={}; Object.defineProperty(person,"name",{ writable:false, value:"jim" }); console.log(person.name);//jim person.name="Jin"; console.log(person.name);//jim
var person={ name:"jim", age:20 }; Object.defineProperty(person,"name",{ configurable:false }); console.log(person.name);//jim console.log(person.age);//20 delete person.name; delete person.age; console.log(person.name);//jim console.log(person.age);//undefined
注意:把configurable设置为false,表示不能从对象中将此属性删除,设置为false之后,在非严格模式下,使用delete对其进行删除,什么也不会发生,但是在严格模式下就会导致错误,
而且一旦将configurable属性定义为false后,就不能再将其变回可配置的
var person={ name:"jim", age:20 }; Object.defineProperty(person,"name",{ configurable:false }); //报错 Object.defineProperty(person,"name",{ configurable:true });
访问器属性:
[[Configurable]]:表示能否通过delete删除属性
[[Enumerable]]:表示能否通过for-in循环返回属性
[[Get]]:在读取属性时调用的函数
[[Set]]:在写入属性时调用的函数
var book={ _year:2004, edition:1 }; Object.defineProperty(book,"year",{ get:function(){ return this._year; }, set:function(newValue){ if(newValue>2004){ this._year=newValue; this.edition+=newValue-2004; } } }); console.log(book._year);//2004 console.log(book.edition);//1 book.year=2005; console.log(book._year);//2005 console.log(book.edition);//2
/* *访问器的getter和setterie9+才支持,在此之前,要创建访问器的属性的话,一般使用两个非标准的方法: *__defineGetter__()和__defineSetter__()方法 */ var book={ _year:2004, edition:1 }; book.__defineGetter__("year",function(){ return this._year; }); book.__defineSetter__("year",function(newValue){ if(newValue>2004){ this._year=newValue; this.edition+=newValue-2004; } }); console.log(book._year); console.log(book.edition); book.year=2005; console.log(book._year); console.log(book.edition);
3 定义多个属性:Object.defineProperties()(_year和edition是数据属性,year是访问器属性)
var book={ }; Object.defineProperties(book,{ _year:{ value:2004 }, edition:{ value:1 }, year:{ get:function(){ return this._year; }, set:function(newValue){ if(newValue>2004){ this._year=newValue; this.edition+=newValue-2004; } } } });
4 读取属性的特性:Object.getOwnPropertyDescriptor(对象,属性)
var book={ _year:2004, edition:1 }; Object.defineProperty(book,"_year",{ configurable:false, value:2009, writable:true }); var desc=Object.getOwnPropertyDescriptor(book,"_year") console.log(desc.value); console.log(desc.configurable); console.log(desc.writable);
2,工厂模式:抽象了创建对象的过程,用函数来封装以特定接口创建对象的细节(将创建过程封装到一个函数中,创建实例的时候调用该函数即可)
function create(name){ var o=new Object(); o.name=name; o.showName=function(){ alert(this.name); } return o; } var p1=create("Jim"); var p2=create("Jhon"); p1.showName();//Jim
p2.showName();//Jhon
缺点:无法进行对象识别
p1 instanceof Object;//true p1 instanceof Person;//false
3,构造函数模式(构造函数的定义和其他任何普通函数的定义一样,只是使用new操作符调用了,那么它就可以认为是构造函数)
function Person(name){ this.name=name; this.showName=function(){ alert(this.name); } } var p1=new Person("Jim"); var p2=new Person("Jhon"); p1.showName();//jin alert(p1 instanceof Person);//true
alert(p1.constructor==Person);//true
alert(p1 instanceof Object);//true
缺点:每个方法都要在每个实例上创建一遍,例如上述两个实例p1和p2的方法showName()功能相同,况且有this对象在,根本不需要在执行代码前就把函数绑定到特定对象上
alert(p1.showName==p2.showName);//false,每一个person都有自己的showName方法
4,原型模式(每个函数都有一个prototype属性,该属性是一个对象,利用该属性可以让所有对象实例共享它所包含的属性和方法)
function Person(name){ } Person.prototype.name="Jim"; Person.prototype.showName=function(){ alert(this.name); } var p1=new Person(); var p2=new Person(); p1.showName();//Jim p2.showName();//Jim alert(p1.showName==p2.showName);//true
缺点:原型模式最大的问题是由其共享的本性所导致的,原型中的所有属性被所有的实例所共享,这些属性值只能访问,不能重写,但可以通过向实例对象添加同名属性,进行覆盖,例如上述实例中的name属性,可以通过在实例上添加一个name属性,来隐藏原型对象中对应的name属性。(覆盖的原因:在读取某个实例的属性时,首先从实例本身开始,如果能在实例本身搜索到给定名字的属性,就返回该属性的值,停止搜索,如果没有,就继续搜索指针指向的原型对象,正是由于读取属性的方式,才可以利用同名属性对基本数据类型的属性加以隐藏)。
p1.name="Jhon"
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性,只要实例中的这个属性存在,就不会访问到原型中的属性,不过,可以使用delete操作符,删除实例属性
function Person(name){ } Person.prototype.name="Jim"; Person.prototype.showName=function(){ alert(this.name); } var p1=new Person(); var p2=new Person(); p1.showName();//Jim p2.showName();//Jim p1.name="Jhon"; p1.showName();//Jhon p2.showName();//Jim delete p1.name; p1.showName();//Jim
但是对于引用类型的属性,例如数组,也会被共享,例:
function Person(name){ } Person.prototype.name="Jim"; Person.prototype.friends=["Amily","Jhon"]; Person.prototype.showName=function(){ alert(this.name); } var p1=new Person(); var p2=new Person(); p1.friends.push("Tom"); alert(p2.friends);//Amily,Jhon,Tom
friends数组存在于Person.prototype中,而不是属于p1或者p2的,而p1和p2的朋友按正常逻辑应该是不一样,但是由于friends数组存在于原型中,所以p1操作了friends,那么原型所有对象访问到的friends都会发生变化,这显然是不合理的
5,构造函数模式和原型模式的结合(构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性,每个实例都有一份自己的实例属性,但又同时可以共享对方法的引用)
function Person(name){ this.name=name; this.friends=[]; } Person.prototype.showName=function(){ alert(this.name); } var p1=new Person("Jim"); var p2=new Person("Jhon"); p1.friends.push("Tom"); alert(p1.friends);//"Tom" alert(p2.friends);//""
alert(p1.showName==p2.showName);//true
6,动态原型模式(由于构造函数模式和原型模式的组合中,构造函数和原型是独立的,这对于OO开发人员是很困惑的,动态原型模式将所有的信息封装在构造函数中,通过构造函数初始化原型),通过检查某个应该存在方法是否有效,来决定是否需要初始化原型
function Person(name){ this.name=name;
//下面这段代码只会在初次调用构造函数时执行 if(typeof this.showName!="function"){ Person.prototype.showName=function(){ alert(this.name); } } } var p1=new Person("Jim"); var p2=new Person("Jhon"); p1.showName();//Jim p2.showName();//Jhon alert(p1.showName==p2.showName);//true
7,稳妥构造函数模式(稳妥对象:没有公共属性,而且其方法也不引用this对象,在一些安全环境中,禁止使用new和this的环境,或者在防止数据被其他应用程序改动时使用)
function Person(name){ var o=new Object(); o.name=name; o.showName=function(){ alert(name); } return o; } var p1=Person("Jim"); p1.showName();//Jim
8,继承
8.1 继承分为接口继承和实现继承,其中接口继承只继承方法签名,实现继承则继承实际的方法
8.2 ECMAScript只支持实现继承,而实现继承主要依赖原型链来实现
8.3 原型链实现实现继承的基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法
8.4 回顾一下构造函数、原型、实例之间的关系
8.4.1 原型和构造函数之间的关系:每个构造函数都有一个prototype属性,该属性是一个指针,指向原型对象,原型对象有一个constructor指针,指向构造函数
8.4.2 原型与实例之间的关系:每个实例都有一个内部指针_proto_,该指针指向原型对象
8.5 原型链的概念:假设A类型的原型对象是B类型的实例,那么A类型的原型对象就会有一个_proto_的内部指针,指向B类型的原型对象,相应地B类型的原型对象又是C类型的实例,那么B类型的原型对象也会有一个_proto_的内部指针指向C类型的原型对象,以此类推,层层递进,就构成了实例和原型的链条
function C(){ this.name="C"; } C.prototype.sayHello=function(){ alert(this.name+" hello!"); }; function B(){ } B.prototype=new C();//B的原型是C类型的实例 function A(){ } A.prototype=new B();//A的原型是B类型的实例 var a=new A(); a.sayHello();
alert(a.constructor);//function C(){this.name="C";}
//可见最后A类型的实例的constructor指针指向的是最终的父级类型的构造函数function C(){this.name="C";}
8.6 默认的原型,所有的引用类型默认都继承了Object,而这个继承也是通过原型链实现的
原型链继承的缺点:第一,包含引用类型值的属性会被所有实例共享,在通过原型来实现继承时,原型实际上会变成另一个类型的实例,于是另一个类型的属性就顺理成章地变成了现在的原型属性
function B(){ this.color=["red","green"]; } function A(){ } A.prototype=new B(); var a=new A(); var b=new B(); b.color.push("yellow") alert(b.color);//red,green,yellow a.color.push("black"); alert(a.color);//red,green,black var aa=new A(); alert(aa.color);//red,green,black
B类型的构造函数定义了一个color属性,该属性是一个数组,B类型的所有实例都有属于自己的color属性,当A类型通过原型链继承了B类型之后,A类型的原型就成为B类型的一个实例,因此A类型的原型就拥有一个color属性,由于这个color属性是定义在A类型的原型,所以A类型的所有实例就会共享该属性,因此我们对A类型的实例a的color属性做了修改,就会在A类型的另一个实例aa中体现出来
第二、在创建子类型的实例时,无法向超类的构造函数传递参数,即没有办法在不影响所有对象实例的情况下,给超类的构造函数传递参数
因此在实践中很少单独使用原型链
8.7 借用构造函数:解决引用类型值给原型链带来的问题以及无法向超类传递参数的问题(思想:在子类构造函数的内部调用超类构造函数)
function B(name){ this.name=name; this.color=[]; } //在A类型的构造函数中调用B类型的构造函数,这样A类型的每个实例都有了自己的color属性,且可以向B类型的构造函数传递参数了 function A(name){ B.call(this,name); } var a=new A("Jim"); var aa=new A("Jhon"); a.color.push("Jim's black"); aa.color.push("Jhon's yellow"); alert(a.color);//Jim's black alert(aa.color);//Jhon's yellow
缺点:方法都是在构造函数中定义的,那么函数的复用就无从谈起,而且,在超类的原型中定义的方法,对子类而言也是不可见的,结果所有的类型都只能使用构造函数模式,因此该方法也很少单独使用
8.8 组合继承:最常用的继承模型,伪经典继承,将原型链和借用构造函数模式组合起来
(思想:使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承),既能通过在原型上定义方法来实现函数的复用,又能保证每个实例都有自己的属性
function B(name){ this.name=name; this.color=[]; } B.prototype.showName=function(){ alert(this.name); }; //在A类型的构造函数中调用B类型的构造函数,这样A类型的每个实例都有了自己的color属性,且可以向B类型的构造函数传递参数了 function A(name,age){ B.call(this,name);//第二次调用B类的构造函数B() this.age=age; } A.prototype=new B("Tom");//第一次调用B类的构造函数B()
//虽然A的原型会继承B类型的name和color属性,
//但是由于在A类型的构造函数中调用B类型的构造函数,
//A类型的构造函数中已经有了同名的属性,在生成A类型的实例时会屏蔽了A类型原型中的属性
//如果使用delete A类型的实例.name删除实例中的属性,就会暴露出A类型原型中的属性 A.prototype.sayAge=function(){alert(this.age); }; var a=new A("Jim",29); var aa=new A("Jhon",20); a.showName();//Jim
delete a.name;//这里使用delete a.name删除实例中的name属性,就会暴露出A类型原型中即B类型的实例的name属性
a.showName();//Tom a.sayAge();//29 a.color.push("Jim's black"); aa.color.push("Jhon's yellow"); alert(a.color);//Jim's black alert(aa.color);//Jhon's yellow
缺点:组合继承虽然是JS最常用的继承方式,但是组合继承最大的问题是无论什么情况下,都会调用两次超类的构造函数,一次是在构建子类型的原型时,一次是在子类型构造函数内部。因此子类型最终会包含超类对象的全部实例属性,我们不得不在调用子类的构造函数时重写这些属性
8.9 寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法(思路:不必为了指定子类型的原型而调用超类型的构造函数)
8.9.1 模式:利用Object函数创建一个父类的原型对象的实例,然后让这个实例的constructor指针指向子类型的构造函数,而子类型的构造函数的prototype属性又指向该实例对象的,在这里就不会调用父类的构造函数了,因此整个继承过程只调用一次父类型的构造函数,效率得到提高
function inheritModel(sonType,fatherType){ var prototype=Object(fatherType.prototype);//先创建一个对象,这个对象是父类型的原型的实例 prototype.constructor=sonType;//增强对象 sonType.prototype=prototype;//指定对象 }
function inheritModel(sonType,fatherType){ var prototype=Object(fatherType.prototype);//创建对象 prototype.constructor=sonType;//增强对象 sonType.prototype=prototype;//指定对象 } function B(name){ this.name=name; this.color=[]; } B.prototype.showName=function(){ alert(this.name); }; function A(name,age){ B.call(this,name);//只在这里调用一次父类的构造函数 this.age=age; } inheritModel(A,B); A.prototype.sayAge=function(){ alert(this.age); }; var a=new A("Jim",20); a.showName();