《悟透javascript》笔记
读书笔记,这里对javascript从函数到类,从函数式编程到面向对象式编程有一个很好的启发作用,主要是编程思想。尤其是后面利用javascript原型链机制实现类构建和继承,更是让人受益匪浅。如果你对javascript语法已经了然于胸,强面向对象语言实现机制也掌握的相当纯熟,那么你看这个小册子应该会更有收获。当然,如果你已经对javascript面向对象非常精通,那么希望你能多多拍砖,留下宝贵意见。
引子
- 变成世界里只有两种基本元素:数据(静态)、代码(动态)。
- 面向对象的思想首先把数据和代码结合成统一体,将杂乱的子程序以及纠缠不清的复杂的数据结构并以一个简单的对象结构描述出来,从而使程序更加易于理解。
- 对象本无根,类型亦无形。本来无一物,何处惹尘埃。
回归简单
- 放下对象,放下类,回归数据和代码的本源。
- 对象的类是从无到有,又不断演化,最终又消失的过程。
- 函数定义式和直接量式语法
1: //定义式2: function myfunc(){ };3: //直接量4: var myfunc = function(){ };5:
6: //考虑如下代码:7: function myfunc(){alert(1) };8: myfunc();//29: function myfunc(){alert(2)};10: myfunc();//211: //javascript预编译会将定义式代码提前执行,所以在执行myfunc()之前,myfunc函数的内容已经被覆盖了。12:
13: //再考虑如下代码:14: fn = function(){alert(1)};15: fn();//116: fn = function(){alert(2)};17: fn();//218: //函数从定义式变成了直接量式,于是,结果大不一样。19:
20: //第三段代码:21: <script>
22: function myfunc(){alert(1) };23: myfunc();//124: </script>
25: <script>
26: function myfunc(){alert(2) };27: myfunc();//228: </script>
29: //这次使用的是定义式函数,但由于使用script标记将代码分成了 两段 ,于是结果变成了1,2。原因是
javascript语言的分段预编译机制。
- javascript里的代码也是一种数据,同样可以被任意赋值和修改,而值就是代码的逻辑。
javascript对象化能力
- javascript中,函数可以构造出对象,我们可以像往常一样称之为构造函数,不过,javascript中的函数也是对象,它可以动态的拥有自己的属性和方法。
1: function Sing(){2: var singer = arguments.callee; //调用arguments对象的环境函数。3: alert(singer.author+"\n"+singer.poem);//调用该函数的相关属性4: }
5:
6: Sing.author = "李白";7: Sing.poem = "汉家秦地月,流影照明妃。一上玉关道,天涯去不归 ";8:
9: Sing();
- javascript对象访问与数组访问,数组为线性数据结构,有一定规律,便于批量迭代操作。对象类似于离散数据结构,适合分散和个性化的东西。javascript既有对象的特性也有数组的特性,不过据说底层实现还是数组。
1: function Sing(){2: var singer = arguments.callee;3: //数组形式访问对象属性。4: alert(singer["author"]+"\n"+singer["poem"]);5: }
6:
7: Sing.author = "李白";8: Sing.poem = "汉家秦地月,流影照明妃。一上玉关道,天涯去不归 ";9:
10: Sing();
放下对象,this
1: function WhoAmI(){2: alert(this.name)3: }
4:
5: //1:this 指代匿名对象{name:"MrZhou",whoAmI:WhoAmI}6: //({name : "MrZhou",whoAmI : WhoAmI}).whoAmI();//MrZhou7:
8: //2:this 指代对象zhou9: var zhou = {name : "MrZhou",whoAmI : WhoAmI};10: //zhou.whoAmI();//MrZhou11:
12: //3:通过函数从Function继承的call方法指定调用对象(zhou)。13: WhoAmI.call(zhou);//MrZhou14:
15: //4:调用其他对象的函数16: var sunny = {name : "MrSunny"};17: //zhou.whoAmI.call(sunny) //MrSunny18:
19: //5:通过函数自身就是对象的性质,使用函数自身呼出函数对象的属性。20: WhoAmI.name = "whoAmI";21: WhoAmI.WhoAmI = WhoAmI;
22: WhoAmI.WhoAmI();//whoAmI23:
24: //这里的this是活动的,在代码执行过程中可以灵活的控制this的指向,this始终为当前调用对象,不过不能为this显式赋值。在这
里,对象就是函数,函数就是对象。对象素描:建立对象
javascript object notation (json) javascript对象表示法
- 直接量语法创建空对象:var obj = {};
- 创建对象并初始化其属性:var mouse = {usb : true , color : ”black” ,click:function(){} }
new运算符加构造函数创建对象
- function Create(){};var obj = new Create();
- 而上面的方式等价于function Create(){}; var obj ={}; Create.call(obj);
对象继承:
1: //方式1:对象冒充 : 只能继承函数内this关键字指定的属性和方法。2: function person(name){3: this.name = name;4: this.say = function(){}5: }
6:
7: function employee(name,salary){8: person.call(this,name);//调用基类构造函数9:
10: this.salary = salary;11: this.showSalary = function(){}12:
13: }
1: //方式2:原型继承:通过复制对象实例为子类得prototype原型对象,从而得到父类构造函数内的this上的和prototype上的相关属性方法2: function person(name){3: this.name = name; //将基类属性设置在构造函数中4: }
5:
6: function employee(name,salary){7: person.call(this,name); //调用基类构造函数,初始化相关属性8:
9: this.salary = salary;10: }
11: person.prototype.say = function(){alert("hello")} //为基类的原型对象添加方法,该方法会通过原型链机制共享到employee对象上12: employee.prototype = new person(); //使用原型链机制实现继承。13: employee.prototype.showSalary = function(){alert(this.salary)} //为子类原型对象添加方法14:
15: var xiaoMing = new employee("xiaoMing",10000);16: xiaoMing.say();//hello17: xiaoMing.showSalary()//1000018:
19: employee.prototype.say = function(){alert("hello,everyone")}//为子类employee添加say方法,掩盖父类person的say方法,借此实现
override方法重载
20: var xiaoGang = new employee("xiaoGang",10000);21: xiaoMing.say();//hello,everyone22: xiaoGang.say();//hello,everyone23:
24: xiaoGang.say = function(){alert("hello,everyone. i`m"+this.name)}//为子类employee的对象实例添加say方法,掩盖子类和父类的say方法
25: xiaoMing.say();//hello,everyone26: xiaoGang.say();//hello,everyone. i`mXiaoGang 这里说明各个实例的方法各自独立互不影响。27:
28: person.prototype.sleep = function(){alert("zzzz~")}//通过动态修改函数原型,从而达到动态扩展基类属性和方法的目的。注意此时是在子类对象已经实例化了之后动态添加的。
29: xiaoGang.sleep();//zzzz~ 此时照样可以调用xiaoGang实例的sleep()方法。
1: //方式3:2: /*我们知道 var anObj = new aFunction(); 的方式创建对象实际上包括了以下三步:3: 1. 建立一个新对象。4: 2. 将该对象的内置原型对象设置为构造函数prototype引用的那个原型对象5: 3. 将该对象作为this参数调用构造函数,完成对象成员属性的初始化工作。在这里,new 关键字用来分配内存,创建对象副本。而aFunction函数则用来关联函数的原型对象和初始化对象的内置属性、方法。我们
自己也可以模拟实现这种创建对象的方式
7: */8: var Person = {9: create:function(name,age){10: this.name = name;11: this.age = age;12: },
13: getName:function(){14: return this.name;15: },
16: getAge:function(){17: return this.age;18: }
19: }
20: /* 《悟透javascript》作者说:“那么,我们能否自己定义一个对象来当作原型,并在这个原型上描述类,然后将这个原型设置给新创建的对象,将其当作对象的类呢?我们又能否将这个原型中的一个方法当作构造函数,去初始化新建的对象呢?”当时我看了半天,不得其解,后来
又看了几遍,想起java中强面向对象的语法和实现方式,茅塞顿开。
21: 我理解的是,如果我们能将Person对象当做Java或者C#里面的类,那么上面JSON的语法格式就跟java和C#的类非常相似了,但java的类写
好了之后,通过类创建的对象实例,必然具有其类内部实现的方法和属性。但是在这里,却无法通过对象实例访问类内部的属性和方法。或者必须
通过一些特殊渠道,如下例:*/
22: var newFish =new Person.create("zhou",123);23: newFish.getName()//TypeError: Object [object Object] has no method 'getName'24: /*这时候我们可以使用一个函数还作为中转站,想将该函数的prototype指向JSON对象,再在这个函数中调用JSON对象的构造函数,之后将生成的新对象返回,如下例*/
25: function NEW(Class,aParams){26: function transfer(){27: Class.create.apply(this,aParams);28: }
29: transfer.prototype = Class;
30: return new transfer();31: }
32: var xiaoA = NEW(Person,["a",12])33: xiaoA.getAge(); //1234: xiaoA.getName() //"a"35: xiaoA.constructor; //function Object() { [native code] }36: /* 这里首先通过transfer函数中转JSON对象的create构造函数内部逻辑,之后又将transfer的原型指向Class,中转JSON对象,最后通过new将修正后的对象返回。 而此时由于已经将transfer函数的原型设置为新的原型,那么transfer对象的函数与原型之间的引用链被解开,于是
transfer函数和他之前的原型对象同时被正确回收。
37: 而此时xiaoA的构造函数就已经不再是transfer了(因为已经被回收了,为什么会被回收呢?因为在这里xiaoA实际上并没有constructor
属性的,它自己会通过原型链(__proto__)去找constructor属性,而__proto__实际上就是transfer的prototype,当我将prototype销毁了之后,
这个原型指向__proto__当然也就不存在了,于是自动向上回朔,直到顶层的Object),而是Object。
38: 这样的话,不论从语法角度还是继承机制,都和java、c#比较相似了*/- 上面仅仅是通过类实例化对象,并建立实例对象与类之间关系的例子,下面再看一个加入了类继承的例子(在例子后面有我自己的理解,欢迎拍砖):
1: var object = //定义小写的 object 基本类,用于实现最基础的方法等2: {
3: isA: function(aType) //一个判断类与类之间以及对象与类之间关系的基础方法,在模式上类似于Object的toString,这样所有
继承了object基类的对象都将有isA方法。
4: {
5: var self = this;6: while(self)7: {
8: if (self == aType)9: return true;10: self = self.Type; //这个Type需要往下看才能明白11: };
12: return false;13: }
14: };
15:
16: function Class(aBaseClass, aClassDefine) //创建类的函数,用于声明类及继承关系 ,ps:这里如果只是继承的话,一个aBaseClass就够了,那么aClassDefine是干啥的?匿名对象,创建对象用的。
17: {
18: function class_() //创建类的临时函数壳 ,ps:用这个函数还中转原型链19: {
20: this.Type = aBaseClass; //我们给每一个类约定一个 Type属性,ps:引用其继承的类 ,实现显式访问原型链对象21: for(var member in aClassDefine)22: this[member] = aClassDefine[member]; //复制类的全部定义到当前创建的类 ,ps:所有属性和对象,如果有私有的捏?23: };
24: class_.prototype = aBaseClass;
25: return new class_();26: };
27:
28: function New(aClass, aParams) //创建对象的函数,用于任意类的对象创建 ,ps:使用aClass类创建对象实例29: {
30: function new_() //创建对象的临时函数壳 ps:中转类构造函数逻辑和原型链31: {
32: this.Type = aClass; //我们也给每一个对象约定一个 Type 属性,据此可以访问到对象所属的类 ps:留个指针,以后好找自己的类,跟反射有点儿像
33: if (aClass.Create)34: aClass.Create.apply(this, aParams); //我们约定所有类的构造函数都叫Create,这和DELPHI比较相似 ps:调用基类构造函数,这里有疑问:如果基类有父类呢?
35: };
36: new_.prototype = aClass; //ps:中转原型链37: return new new_(); //ps:构造实例对象,并将实例对象返回38: };
39:
40: //语法甘露的应用效果:41: var Person = Class(object, //派生至 object 基本类 ,ps:第二个实参是匿名对象42: {
43: Create: function(name, age)44: {
45: this.name = name;46: this.age = age;47: },
48: SayHello: function()49: {
50: alert("Hello, I'm " + this.name + ", " + this.age + " years old.");51: }
52: });
53:
54: var Employee = Class(Person, //派生至 Person 类, 是不是和一般对象语言很相似?55: {
56: Create: function(name, age, salary)57: {
58: Person.Create.call(this, name, age); //调用基类的构造函数59: this.salary = salary;60: },
61: ShowMeTheMoney: function()62: {
63: alert(this.name + " $" + this.salary);64: }
65: });
66:
67: var BillGates = New(Person, ["Bill Gates", 53]);68: var SteveJobs = New(Employee, ["Steve Jobs", 53, 1234]);69: BillGates.SayHello();
70: SteveJobs.SayHello();
71: SteveJobs.ShowMeTheMoney();
72:
73: var LittleBill = New(BillGates.Type, ["Little Bill", 6]); //根据 BillGate 的类型创建LittleBill74: LittleBill.SayHello();
75:
76: alert(BillGates.isA(Person)); //true77: alert(BillGates.isA(Employee)); //false78: alert(SteveJobs.isA(Person)); //true79: alert(Person.isA(Employee)); //false80: alert(Employee.isA(Person)); //true- 自己的理解:
非常纳闷为何这里会有一个Class函数,并且用这么曲折的方式来创建一个类?如果只是想构建一个对象,这个对象仅保存了对基类对象的引用的话,直接给该对象加个属性指向基类就可以了,为什么还要多此一举?
但后来想起javascript原型链机制,才恍然大悟:在javascript中如果想访问某个对象实例的属性或方法,假如该实例中没有这个属性或方法,javascript会自动沿着原型链(__proto__)向上查找,如果原型链(prototype)中存在这个属性或方法,那么就直接调用这个属性或方法,否则就继续沿着原型链向上查找,直到原型链顶层为止,不过这如果想让javascript为我们做这个自动做这个事情,那么我们就必须通过构造函数的prototype属性以及构造函数生成的实例对象才能实现,如果我们直接给一个子类对象加个prototype属性让他指向一个父类对象,那么javascript是不会自动通过原型链查找所需的属性或方法的。例如:1: Person={
2: name:"MrZhou",3: getName:function(){4: return this.name; }5: }
6: var someOne ={};7: someOne.prototype = Person;
8: someOne.name;//undefined;9: //但如果这样:10: var someOne = (function(){11: function transfer(){ };12: transfer.prototype = Person;
13: return new transfer();14: }())
15: someOne.name;//MrZhou这样就实现了继承和代码复用。
呵呵,实际上我上面得做法还是有问题的,因为javascript引擎本来就不是通过prototype属性实现向上回朔的,而是通过[[prototype]]来实现的,只不过这个属性只能被javascript引擎底层调用,不能被显式调用,虽然FF、Chrome都实现了通过对象实例的__proto__的属性访问原型链的方式,但IE8以下版本都不支持这个属性,不过我们这里不介意拿来做个测试。
1: var someOne.__proto__=Person;2: someOne.name;//MrZhou这样比刚才通过函数中转的方式要简单的多了,只不过IE8以下浏览器不支持罢了。
这里还有一个问题。
继承的好处在于代码复用,面向对象的层级结构体系更利于代码复用,子类继承父类后,可以使用父类的非公开属性和方法,并且在子类实例化时,会先自动调用父类的构造函数。
那么在这里又如何实现父类构造函数的自动调用呢?这个留待大家讨论吧.李战老师在这本书的最后给出了这样一段话,挺有意思的:
编程的快乐
在软件工业迅猛发展的今天,各式各样的编程语言层出不穷,新语言的诞生,旧语言的演化,似乎已经
让我们眼花缭乱。为了适应面向对象编程的潮流,JavaScript语言也在向完全面向对象的方向发展,新的
JavaScript标准已经从语义上扩展了许多面向对象的新元素。与此相反的是,许多静态的对象语言也在向
JavaScript的那种简洁而幽雅的方向发展。例如,新版本的C#语言就吸收了JSON 那样的简洁表示法,
以及一些其他形式的JavaScript特性。
我们应该看到,随着RIA(强互联应用)的发展和普及,AJAX技术也将逐渐淡出江湖,JavaScript也将
最终消失或演化成其他形式的语言。 但不管编程语言如何发展和演化, 编程世界永远都会在“数据”与“代码”
这千丝万缕的纠缠中保持着无限的生机。只要我们能看透这一点,我们就能很容易地学习和理解软件世界
的各种新事物。不管是已熟悉的过程式编程,还是正在发展的函数式编程,以及未来量子纠缠态的大规模
并行式编程,我们都有足够的法力来化解一切复杂的难题。
佛最后淡淡地说:只要我们放下那些表面的“类”,放下那些对象的“自我”,就能达到一种“对象本无根,
类型亦无形”的境界,从而将自我融入到整个宇宙的生命轮循环中。我们将没有自我,也没有自私的欲望,
你就是我,我就是你,你中有我,我中有你。这时,我们再看这生机勃勃的编程世界时,我们的内心将自
然生起无限的慈爱之心,这种慈爱之心不是虚伪而是真诚的。关爱他人就是关爱自己,就是关爱这世界中
的一切。那么,我们的心是永远快乐的,我们的程序是永远快乐的,我们的类是永远快乐的,我们的对象
也是永远快乐的。这就是编程的极乐!