初探
我们知道JavaScript中的变量定义基本如下:
- varname='ChenHao';;
- varemail='haoel(@)hotmail.com';
- varwebsite='http://coolshell.cn';
如果要用对象来写的话,就是下面这个样子:
- varchenhao={
- name:'ChenHao',
- email:'haoel(@)hotmail.com',
- website:'http://coolshell.cn'
- };
于是,我就可以这样访问:
- //以成员的方式
- chenhao.name;
- chenhao.email;
- chenhao.website;
- //以hashmap的方式
- chenhao["name"];
- chenhao["email"];
- chenhao["website"];
关于函数,我们知道JavaScript的函数是这样的:
- vardoSomething=function(){
- alert('HelloWorld.');
- };
于是,我们可以这么干:
- varsayHello=function(){
- varhello="Hello,I'm"+this.name
- +",myemailis:"+this.email
- +",mywebsiteis:"+this.website;
- alert(hello);
- };
- //直接赋值,这里很像C/C++的函数指针
- chenhao.Hello=sayHello;
- chenhao.Hello();
相信这些东西都比较简单,大家都明白了。可以看到JavaScript对象函数是直接声明,直接赋值,直接就用了。runtime的动态语言。
还有一种比较规范的写法是:
- //我们可以看到,其用function来做class。
- varPerson=function(name,email,website){
- this.name=name;
- this.email=email;
- this.website=website;
- this.sayHello=function(){
- varhello="Hello,I'm"+this.name+",\n"+
- "myemailis:"+this.email+",\n"+
- "mywebsiteis:"+this.website;
- alert(hello);
- };
- };
- varchenhao=newPerson("ChenHao","[email protected]",
- "http://coolshell.cn");
- chenhao.sayHello();
顺便说一下,要删除对象的属性,很简单:
- deletechenhao['email']
上面的这些例子,我们可以看到这样几点:
属性配置 – Object.defineProperty
先看下面的代码:
- //创建对象
- varchenhao=Object.create(null);
- //设置一个属性
- Object.defineProperty(chenhao,
- 'name',{value:'ChenHao',
- writable:true,
- configurable:true,
- enumerable:true});
- //设置多个属性
- Object.defineProperties(chenhao,
- {
- 'email':{value:'[email protected]',
- writable:true,
- configurable:true,
- enumerable:true},
- 'website':{value:'http://coolshell.cn',
- writable:true,
- configurable:true,
- enumerable:true}
- }
- );
下面就说说这些属性配置是什么意思。
Get/Set访问器
关于get/set访问器,它的意思就是用 get/set 来取代 value(其不能和 value 一起使用),示例如下:
- varage=0;
- Object.defineProperty(chenhao,
- 'age',{
- get:function(){returnage+1;},
- set:function(value){age=value;}
- enumerable:true,
- configurable:true
- }
- );
- chenhao.age=100;//调用set
- alert(chenhao.age);//调用get输出101(get中+1了);
我们再看一个更为实用的例子——利用已有的属性(age)通过get 和 set 构造新的属性(birth_year):
- Object.defineProperty(chenhao,
- 'birth_year',
- {
- get:function(){
- vard=newDate();
- vary=d.getFullYear();
- return(y-this.age);
- },
- set:function(year){
- vard=newDate();
- vary=d.getFullYear();
- this.age=y-year;
- }
- }
- );
- alert(chenhao.birth_year);
- chenhao.birth_year=2000;
- alert(chenhao.age);
这样做好像有点麻烦,你说,我为什么不写成下面这个样子:
- varchenhao={
- name:"ChenHao",
- email:"[email protected]",
- website:"http://coolshell.cn",
- age:100,
- getbirth_year(){
- vard=newDate();
- vary=d.getFullYear();
- return(y-this.age);
- },
- setbirth_year(year){
- vard=newDate();
- vary=d.getFullYear();
- this.age=y-year;
- }
- };
- alert(chenhao.birth_year);
- chenhao.birth_year=2000;
- alert(chenhao.age);
是的,你的确可以这样的,不过通过defineProperty ()你可以干这些事:
1)设置如writable,configurable,enumerable 等这类的属性配置。
2)动态地为一个对象加属性。比如:一些 HTML 的 DOM 对像。
查看对象属性配置
如果查看并管理对象的这些配置,下面有个程序可以输出对象的属性和配置等东西:
- //列出对象的属性.
- functionlistProperties(obj)
- {
- varnewLine="<br/>";
- varnames=Object.getOwnPropertyNames(obj);
- for(vari=0;i<names.length;i++){
- varprop=names[i];
- document.write(prop+newLine);
- //列出对象的属性配置(descriptor)动用getOwnPropertyDescriptor函数。
- vardescriptor=Object.getOwnPropertyDescriptor(obj,prop);
- for(varattrindescriptor){
- document.write("..."+attr+':'+descriptor[attr]);
- document.write(newLine);
- }
- document.write(newLine);
- }
- }
- listProperties(chenhao);
call,apply,bind和this
关于 Javascript 的 this 指针,和C++/Java 很类似。 我们来看个示例:(这个示例很简单了,我就不多说了)
- functionprint(text){
- document.write(this.value+'-'+text+'<br>');
- }
- vara={value:10,print:print};
- varb={value:20,print:print};
- print('hello');//this=>global,output"undefined-hello"
- a.print('a');//this=>a,output"10-a"
- b.print('b');//this=>b,output"20-b"
- a['print']('a');//this=>a,output"10-a"
我们再来看看call和apply,这两个函数的差别就是参数的样子不一样,另一个就是性能不一样,apply 的性能要差很多。(关于性能,可到 JSPerf 上去跑跑看看)
- print.call(a,'a');//this=>a,output"10-a"
- print.call(b,'b');//this=>b,output"20-b"
- print.apply(a,['a']);//this=>a,output"10-a"
- print.apply(b,['b']);//this=>b,output"20-b"
但是在 bind 后,this 指针,可能会有不一样,但是因为 Javascript 是动态的。如下面的示例
- varp=print.bind(a);
- p('a');//this=>a,output"10-a"
- p.call(b,'b');//this=>a,output"10-b"
- p.apply(b,['b']);//this=>a,output"10-b"
继承和重载
通过上面的那些示例,我们可以通过 Object.create ()来实际继承,请看下面的代码,Student 继承于 Object。
- varPerson=Object.create(null);
- Object.defineProperties
- (
- Person,
- {
- 'name':{value:'ChenHao'},
- 'email':{value:'[email protected]'},
- 'website':{value:'http://coolshell.cn'}
- }
- );
- Person.sayHello=function(){
- varhello="<p>Hello,Iam"+this.name+",<br>"+
- "myemailis:"+this.email+",<br>"+
- "mywebsiteis:"+this.website;
- document.write(hello+"<br>");
- }
- varStudent=Object.create(Person);
- Student.no="1234567";//学号
- Student.dept="ComputerScience";//系
- //使用Person的属性
- document.write(Student.name+''+Student.email+''+Student.website+'<br>');
- //使用Person的方法
- Student.sayHello();
- //重载SayHello方法
- Student.sayHello=function(person){
- varhello="<p>Hello,Iam"+this.name+",<br>"+
- "myemailis:"+this.email+",<br>"+
- "mywebsiteis:"+this.website+",<br>"+
- "mystudentnois:"+this.no+",<br>"+
- "mydepartentis:"+this.dept;
- document.write(hello+'<br>');
- }
- //再次调用
- Student.sayHello();
- //查看Student的属性(只有no、dept和重载了的sayHello)
- document.write('<p>'+Object.keys(Student)+'<br>');
通用上面这个示例,我们可以看到,Person 里的属性并没有被真正复制到了 Student 中来,但是我们可以去存取。这是因为 Javascript 用委托实现了这一机制。其实,这就是 Prototype,Person 是 Student 的 Prototype。
当我们的代码需要一个属性的时候,Javascript 的引擎会先看当前的这个对象中是否有这个属性,如果没有的话,就会查找他的 Prototype 对象是否有这个属性,一直继续下去,直到找到或是直到没有 Prototype 对象。
为了证明这个事,我们可以使用 Object.getPrototypeOf ()来检验一下:
- Student.name='aaa';
- //输出aaa
- document.write('<p>'+Student.name+'</p>');
- //输出ChenHao
- document.write('<p>'+Object.getPrototypeOf(Student).name+'</p>');
于是,你还可以在子对象的函数里调用父对象的函数,就好像 C++ 里的 Base::func () 一样。于是,我们重载 hello 的方法就可以使用父类的代码了,如下所示:
- //新版的重载SayHello方法
- Student.sayHello=function(person){
- Object.getPrototypeOf(this).sayHello.call(this);
- varhello="mystudentnois:"+this.no+",<br>"+
- "mydepartentis:"+this.dept;
- document.write(hello+'<br>');
- }
这个很强大吧。
组合
上面的那个东西还不能满足我们的要求,我们可能希望这些对象能真正的组合起来。为什么要组合?因为我们都知道是这是 OO 设计的最重要的东西。不过,这对于 Javascript 来并没有支持得特别好,不好我们依然可以搞定个事。
首先,我们需要定义一个 Composition 的函数:(target 是作用于是对象,source 是源对象),下面这个代码还是很简单的,就是把 source 里的属性一个一个拿出来然后定义到 target 中。
- functionComposition(target,source)
- {
- vardesc=Object.getOwnPropertyDescriptor;
- varprop=Object.getOwnPropertyNames;
- vardef_prop=Object.defineProperty;
- prop(source).forEach(
- function(key){
- def_prop(target,key,desc(source,key))
- }
- )
- returntarget;
- }
有了这个函数以后,我们就可以这来玩了:
- //艺术家
- varArtist=Object.create(null);
- Artist.sing=function(){
- returnthis.name+'startssinging...';
- }
- Artist.paint=function(){
- returnthis.name+'startspainting...';
- }
- //运动员
- varSporter=Object.create(null);
- Sporter.run=function(){
- returnthis.name+'startsrunning...';
- }
- Sporter.swim=function(){
- returnthis.name+'startsswimming...';
- }
- Composition(Person,Artist);
- document.write(Person.sing()+'<br>');
- document.write(Person.paint()+'<br>');
- Composition(Person,Sporter);
- document.write(Person.run()+'<br>');
- document.write(Person.swim()+'<br>');
- //看看Person中有什么?(输出:sayHello,sing,paint,swim,run)
- document.write('<p>'+Object.keys(Person)+'<br>');
Prototype和继承
我们先来说说 Prototype。我们先看下面的例程,这个例程不需要解释吧,很像C语言里的函数指针,在C语言里这样的东西见得多了。
- varplus=function(x,y){
- document.write(x+'+'+y+'='+(x+y)+'<br>');
- returnx+y;
- };
- varminus=function(x,y){
- document.write(x+'-'+y+'='+(x-y)+'<br>');
- returnx-y;
- };
- varoperations={
- '+':plus,
- '-':minus
- };
- varcalculate=function(x,y,operation){
- returnoperations[operation](x,y);
- };
- calculate(12,4,'+');
- calculate(24,3,'-');
那么,我们能不能把这些东西封装起来呢,我们需要使用 prototype。看下面的示例:
- varCal=function(x,y){
- this.x=x;
- this.y=y;
- }
- Cal.prototype.operations={
- '+':function(x,y){returnx+y;},
- '-':function(x,y){returnx-y;}
- };
- Cal.prototype.calculate=function(operation){
- returnthis.operations[operation](this.x,this.y);
- };
- varc=newCal(4,5);
- c.calculate('+');
- c.calculate('-');
这就是 prototype 的用法,prototype 是 javascript 这个语言中最重要的内容。网上有太多的文章介始这个东西了。说白了,prototype 就是对一对象进行扩展,其特点在于通过“复制”一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的“原型”,这个原型是可定制的(当然,这里没有真正的复制,实际只是委托)。上面的这个例子中,我们扩展了实例 Cal,让其有了一个 operations 的属性和一个 calculate 的方法。
这样,我们可以通过这一特性来实现继承。还记得我们最最前面的那个 Person 吧, 下面的示例是创建一个 Student 来继承 Person。
- functionPerson(name,email,website){
- this.name=name;
- this.email=email;
- this.website=website;
- };
- Person.prototype.sayHello=function(){
- varhello="Hello,Iam"+this.name+",<br>"+
- "myemailis:"+this.email+",<br>"+
- "mywebsiteis:"+this.website;
- returnhello;
- };
- functionStudent(name,email,website,no,dept){
- varproto=Object.getPrototypeOf;
- proto(Student.prototype).constructor.call(this,name,email,website);
- this.no=no;
- this.dept=dept;
- }
- //继承prototype
- Student.prototype=Object.create(Person.prototype);
- //重置构造函数
- StudentStudent.prototype.constructor=Student;
- //重载sayHello()
- Student.prototype.sayHello=function(){
- varproto=Object.getPrototypeOf;
- varhello=proto(Student.prototype).sayHello.call(this)+'<br>';
- hello+="mystudentnois:"+this.no+",<br>"+
- "mydepartentis:"+this.dept;
- returnhello;
- };
- varme=newStudent(
- "ChenHao",
- "[email protected]",
- "http://coolshell.cn",
- "12345678",
- "ComputerScience"
- );
- document.write(me.sayHello());
兼容性
上面的这些代码并不一定能在所有的浏览器下都能运行,因为上面这些代码遵循 ECMAScript 5 的规范,关于 ECMAScript 5 的浏览器兼容列表,你可以看这里“ES5浏览器兼容表”。
本文中的所有代码都在 Chrome 最新版中测试过了。
下面是一些函数,可以用在不兼容 ES5 的浏览器中:
Object.create ()函数
- functionclone(proto){
- functionDummy(){}
- Dummy.prototype=proto;
- DummyDummy.prototype.constructor=Dummy;
- returnnewDummy();//等价于Object.create(Person);
- }
- varme=clone(Person);
defineProperty ()函数
- functiondefineProperty(target,key,descriptor){
- if(descriptor.value){
- target[key]=descriptor.value;
- }else{
- descriptor.get&&target.__defineGetter__(key,descriptor.get);
- descriptor.set&&target.__defineSetter__(key,descriptor.set);
- }
- returntarget
- }
keys ()函数
- functionkeys(object){varresult,key
- result=[];
- for(keyinobject){
- if(object.hasOwnProperty(key))result.push(key)
- }
- returnresult;
- }
Object.getPrototypeOf() 函数
- functionproto(object){
- return!object?null
- :'__proto__'inobject?object.__proto__
- :/*notexposed?*/object.constructor.prototype
- }
bind函数
- varslice=[].slice
- functionbind(fn,bound_this){varbound_args
- bound_args=slice.call(arguments,2)
- returnfunction(){varargs
- args=bound_args.concat(slice.call(arguments))
- returnfn.apply(bound_this,args)}
- }