[转]深入分析javascript里对象的创建(上)
读jQuery源码时候,我常想到底那些因素会让我读不懂代码,其中最关键的是哪个,最早觉得是jQuery的架构设计,当我查阅资料终于找到jQuery架构设计的入口时候,我发现javascript基础语法的熟练运用才是读源码的关键,因此现在把javascript基础知识系统回顾下很有必要,而且jQuery源码的研究就是对javascript基础知识的加深和灵活运用。
最近深入学习javascript后,有个体会:面向对象的方式编程才是高效灵活的编程,也是现在唯一可以让代码更加健壮的编程方式。如果我们抛开那些玄乎的抽象出类等等思想,我自己对面向对象的从写程序的角度理解就是:复用和封装。复用具体就是让你尽量少写重复代码,封装就是将一些耦合度很高的逻辑放到一个程序块里,而且尽量让里面内容不受外界影响。最后的结论是:优秀的javascript代码都是面向对象的。
如何构建javascript对象。
ECMA-262对对象的定义是:无序属性的集合,其属性可以包含基本值、对象或函数。javascript的对象其实就是java里的map,即键值对。
在javascript创建一个对象一共有三种方式:
方式一:通过Object对象
方式二:通过构造函数
方式三:对象初始化
var obj = new Object(); obj.id = '001'; obj.name = 'Mary'; obj.teststring = 'Test '; obj.sayHello = function() { console.log('Id:' + this.id + ' Name:' + this.name + ' Teststring:' + this.teststring); } obj.sayHello(); //Id:001 Name:Mary Teststring:Test obj['sayHello']();//Id:001 Name:Mary Teststring:Test var str = 'sayHello'; obj[str]();//Id:001 Name:Mary Teststring:Test
(注意:我这里使用了两种访问对象属性的方式,一种是点运算符,一种是方括号运算符,二者是等价的,但是方括号运算似乎要更强大些,方括号里面我们可以放置变量)
这是最常用,最直观的一种创建对象方法,但是它的缺点太明显了,就是代码复用度很低,我们想到一个对象就创建一个对象,如是就会造成大量的重复代码,因此,javascript程序员将工厂模式引入到了javascrip编程里,请大家看下面的代码:
function createObj(id,name,teststring) { var o = new Object(); o.id = id; o.name = name; o.teststring = teststring; o.sayHello = function() { console.log('Id:' + this.id + ' Name:' + this.name + ' Teststring:' + this.teststring); } return o; } var obj = createObj('002','Mary','Test Obj2'); obj.sayHello();//Id:002 Name:Mary Teststring:Test Obj2 obj['sayHello']();//Id:002 Name:Mary Teststring:Test Obj2 var str = 'sayHello'; obj[str]();//Id:002 Name:Mary Teststring:Test Obj2
工厂模式解决了创建相似对象的问题,如果抛开它构造对象的对象识别问题,工厂模式挺完美的,如果你做的javascript应用不是太复杂,建议使用工厂模式构造对象,这种写法可读性很高。
2.通过构造函数
几乎所有使用构造函数方式构建对象都会使用到new运算符,javascript里面的构造函数比较特别的,在javascript里没有类的概念,new 后面跟的直接是构造函数,大家看下面的代码:
function Obj(id1,name1,teststring1) { this.id = id1; this.name = name1; this.teststring = teststring1; this.sayHello = function() { console.log('Id:' + this.id + ' Name:' + this.name + ' Teststring:' + this.teststring); } } var obj = new Obj('003','Mary','Test Obj3'); obj.sayHello();//Id:002 Name:Mary Teststring:Test Obj2 obj['sayHello']();//Id:002 Name:Mary Teststring:Test Obj2 var str = 'sayHello'; obj[str]();//Id:002 Name:Mary Teststring:Test Obj2
构造函数式和工厂模式从代码角度而言很像,我在学习javascript初期,单独看一种方式,思维总是惯性的把二者混为一谈,如果两个放在一起还是觉得有差异,主要是javascript里面function被赋予的功能太多,如果在一个大型程序里面二者交替使用,不晕头才怪了。其实通过两者构造对象的不同,我可以把function的使用分为构造函数式和函数式。下面我将重点分析下两种方式。
首先我去掉构造方法里各个属性的this指针,而sayHello里面引用的this指针不去掉。代码如下:
function Obj(id1,name1,teststring1) { id = id1; name = name1; teststring = teststring1; sayHello = function() { console.log('Id:' + this.id + ' Nname:' + this.name + ' Teststring:' + this.teststring); } } var obj = new Obj('004','Mary','Test Obj4'); sayHello();//Id:004 Nname:Mary Teststring:Test Obj4 window.sayHello();//Id:004 Nname:Mary Teststring:Test Obj4 obj.sayHello();//obj.sayHello is not a function
接着去掉syaHello里面的指针:
function Obj(id1,name1,teststring1) { id = id1; name = name1; teststring = teststring1; sayHello = function() { console.log('Id:' + id + ' Name:' + name + ' Teststring:' + teststring); } } var obj = new Obj('005','My Name is obj5','Test Obj5'); sayHello();//Id:005 Name:My Name is obj5 Teststring:Test Obj5 window.sayHello();//Id:005 Name:My Name is obj5 Teststring:Test Obj5
由上面内容我得出了下面的结论:
一.javascript是可以做面向对象编程的,我们不能把它当做面向过程的语言;
二.javascript里面对象的创建是特别的,特别在于它和传统的面向对象语言比较起来做了简化,简化到直接使用构造函数来创建对象;
三.function在javascript里面既可以当做函数式使用,又可以作为构造函数的标示(你也可以直接当做类来看待),而区别构造函数式和函数式的区别就是new;
四.如果我们用到了构造函数式才创建对象,那么这里和其他面向对象语言一样,也是有封装的,换种说法是里面定义的属性或者方法是属于该对象的,而让其属于该对象的方式就是用this指针,否则属性和方法将属于window对象。
如果我们不用new运算符,直接使用函数,例如下面代码:
function Obj(id1,name1,teststring1) { this.id = id1; this.name = name1; this.teststring = teststring1; this.sayHello = function() { console.log('Id:' + this.id + ' Name:' + this.name + ' Teststring:' + this.teststring); } } Obj('006','My Name is obj6','Test Obj6'); sayHello();//Id:006 Name:My Name is obj6 Teststring:Test Obj6 window.sayHello();//Id:006 Name:My Name is obj6 Teststring:Test Obj6
直接调用function,this指针都是指向window,也就是全局对象,我以前看到过一句话:不论哪里直接调用函数,里面的this都是指向全局的。只要是函数调用this指向的方法和函数都是指向window的,即全局对象。
具体例子如下:
function OuterObj(id1,name1,teststring1) { this.id = id1; this.name = name1; this.teststring = teststring1; this.sayHello = function() { console.log('id:' + this.id + '@!@name:' + this.name + '@!@teststring:' + this.teststring); } var InnerObj = function(id2,name2,teststring2) { this.id = id2; this.name = name2; this.teststring = teststring2; this.sayHello = function() { console.log('id:' + this.id + '@!@name:' + this.name + '@!@teststring:' + this.teststring); } } var innerVal = new InnerObj('101','InnerObj','Test InnerObj'); console.log(innerVal instanceof InnerObj);//true innerVal.sayHello();//id:101@!@name:InnerObj@!@teststring:Test InnerObj InnerObj('102','InnerObj0','Test InnerObj0'); } var outObj = new OuterObj('007','My Name is obj7','Test Obj7'); outObj.sayHello();//id:007@!@name:My Name is obj7@!@teststring:Test Obj7 sayHello();//id:102@!@name:InnerObj0@!@teststring:Test InnerObj0 window.sayHello();//id:102@!@name:InnerObj0@!@teststring:Test InnerObj0 console.log(id);//102 console.log(name);//InnerObj0 console.log(teststring);//Test InnerObj0
大家都知道页面嵌入了javascript(浏览器支持javascript的意思),javascript解析器会自动构建window对象,这个道理可以这么理解,浏览器的javascript解析器里早就写好了window类,页面一加载window对象就被new了一下,为了简便程序员的开发,这个window对象一般可以省略,但是它确实肯定以及一定存在我们的页面里。此外javascript有个我们常常会忽视的特点:极晚绑定,也就是说在javascript对象实例化后我们任然可以为该对象定义方法和属性。
那么我可以这么猜想了,其实javascript里面都是通过new来产生实例对象,window也不例外,而且方法的调用都是[某某对象].[方法]的格式,我们平时直接在script标签里面写function方法,然后直接调用其实就是通过极晚绑定来为window对象定义新方法,在javascript没有不属于任何对象的function,当javascript发现某个方法被调用时候找不到该方法的对象,javascript自动会把这个弃儿丢给window这个福利院。
javascript有个定义变量的关键字var,但是javascript同时也可以对没有做var定义的变量进行正确的解析,如果变量本身作用域里找到该变量的var定义,javascript解析器会一直向上一层作用域查找知道找到该变量的var定义为止,但是如果到了window作用域下还没有var定义,那么javascript解析器会自动把这个变量授予window对象。
3.对象初始化
var obj = { id:'008', name:'My Name is obj8', teststring:'Test Obj8', sayHello:function(){ console.log(' Id:' + this.id + ' Nname:' + this.name + ' Teststring:' + this.teststring); } }; obj.sayHello();// Id:008 Nname:My Name is obj8 Teststring:Test Obj8 obj['sayHello']();// Id:008 Nname:My Name is obj8 Teststring:Test Obj8 var str = 'sayHello'; obj[str]();// Id:008 Nname:My Name is obj8 Teststring:Test Obj8
这种方式是我比较喜欢的一种构建对象的方式,有位javascript大师级的人物曾经建议构建空对象,最好是var obj = {},这句话就表明var obj = {}和var obj = new Object();是等价的。var obj = {}方式更加简洁,避免了o.id;o.name等等繁琐的写法,jQuery源码里面大量使用这样的构造对象的方式。
function Obj(id,name,teststring) { id = id; name = name; teststring = teststring; console.log(window.id);//undefined console.log(window.name);//(an empty string) console.log(window.teststring);//undefined console.log(id);//004 console.log(name);//My Name is obj4 console.log(teststring);//Test Obj4 sayHello = function(){ console.log('id:' + this.id + '@!@name:' + this.name + '@!@teststring:' + this.teststring); } } var obj = new Obj('004','My Name is obj4','Test Obj4'); sayHello();//id:undefined@!@name:@!@teststring:undefined window.sayHello();//id:undefined@!@name:@!@teststring:undefined obj.sayHello();//obj.sayHello is not a function [在此错误处中断] obj.sayHello(
以上结果是因为:(未完待续)

浙公网安备 33010602011771号
楼主所喜欢的在构造函数里初始化属性和方法的方式的确能够模拟出类的一些特性——同名的属性、方法,然而要知道这真的只是同名而已,因为你的属性和方法都是利用Javascript的动态特性硬性附加上去的,而不像Java里的类,那些属性是在对象被创建之前就存在的,是与生俱来的,那么Javascript里的对象与生俱来的属性和方法在哪儿?没错,就在prototype里。prototype是一个对象没错,然而熟悉javascript运行方式的同学们应该了解,当你请求一个对象的属性方法的时候,它是先检查对象本身,如果没有,会去请求该对象的prototype里的同名属性或方法,属性的定义是否在prototype里体现倒不是很重要,反正也是用来改的(当然如果你的某个属性修改并不频繁,最好也写到原型里,原理参考前几句javascript运行方式的描述),你定义了,最多实现一个初始值的效果,一旦更改就会在对象里放置一个同名属性,而方法我们知道是很少在运行时动态改的,所以在prototype里定义方法是一个非常好的方式。比如我们想给String类增加方法,只需要编写String.prototype.fun = function() {},那所有的字符串都可以请求到这个方法了,真的是太方便了。
那么除了方便,prototype还有什么好处吗?有,非常关键的好处——性能,要知道在构造函数里去定义方法,那就相当于每一次执行构造函数都要创建一个同样作用的确完全全新的Function对象实例,如果你的new操作很频繁的话,那么消耗的CPU时间和内存会有多大的浪费,楼主可以自己试验一下同样的方法在构造函数里定义和在原型里定义创建100000个对象实例的运行时间,可以负责任的告诉你,无论在任何一个浏览器,这个时间的差距都会非常大,这就是我说你在构造函数里定义的属性和方法真的只是同名而已的另一个原因,在Java里同一个类的所有实例的同名方法只会在内存中产生一个实例,所以prototype方式是最接近java的类定义的方式。
所以楼主还是不要嫌prototype写着麻烦 :),能用prototype方式定义属性和方法就绝对不要在构造函数里定义,切忌。