醉心于Github上美妙的WebGL Demo之中,我试图努力学习JavaScript。我曾经囫囵吞枣地将那本犀牛书《JavaScript权威指南》看了一遍,无奈功力不够,对那些抽象的概念理解很有限。前一阵子亚马逊有买300减100活动,凑书的时候忽然想起之前汤姆大叔送的那本《JavaScript编程精解》评价不错,我当时手气不好(其实主要还是没有使用地精黑科技),没有抢到,就在亚马逊上买了一本。书拿到手后,发现其异乎寻常的薄,翻了两页感觉还不错,于是决定花点时间以此书为基础,再梳理一下JavaScript。
值和变量
值六种类型:number,string,Boolean,object,function,undefined,创建一个值的方法是使用直接量。值是由环境(浏览器)自动创建和维护的——当使用了值的直接量,环境就在内存中创建了这个值;当不再需要这个值的恰当的时候,这个值就被销毁。除此之外,没有可以影响或操纵值的方法。
变量是值的引用,通过关键字var声明,由用户负责创建和维护。变量可以“抓取”值,却不能改变值:当为一个已赋值的变量赋新值的时候,实际上是创建了一个新值,并令该变量“抓取”之,而不是将新值填充到旧值中去。
var x = 1; var add = function(a,b){return a+b;} var origin = {x:0,y:0};
JavaScript中,数字是值,函数是值,对象也是值。上面三条语句的效果都是创建一个值,并声明一个对象引用该值,本质上没有区别。
函数:作用域链、闭包
JavaScript没有块级作用域,因此语句块中的定义的变量在语句块外是可见的。取而代之的是函数作用域,即在函数体内定义的变量在函数体外不可见,而函数体外定义的变量在函数体内可见。
var s = “hello”; var t = s+” world”; function test(){ var y = 1; t; //->“hello world” } y; //->undefined
函数本质上也是值(我会反复强调这一点,因为只有理解了这一点,才能明白JavaScript为什么是解释型语言,为什么看起来和C++、C#相差那么大),在运行之前并不被环境所理解。浏览器像处理流一样运行JavaScript文本,事实上它根本不知道下一条语句是什么,更不会尝试“理解”该语言。因此JavaScript中的函数可以和定义函数的作用域中的其他变量直接交流,就像其他变量之间交流一样。上面的代码中,变量t和s有交流,函数变量test和t有交流,两者没有区别。在其他语言中,函数往往只通过参数和外界交流,而在JavaScript中函数可以和定义函数的作用域中的其他变量交流。有一点值得注意,就是函数中引用的函数外变量属于定义函数时的作用域,而不是调用函数时的作用域,下面的代码说明了这种令人费解的“闭包”特性——即,作用域并未随着函数定义语句结束而消失,函数在调用的时候仍然能够访问到函数定义时的作用域。
function text(){ var s=”local string”; var f = function(){ return s; } return f; } var s = “global string”; text()();//->”local string”;
既然函数也是值,那么将函数本身作为参数传入其他函数也很自然,对于只需要使用一次的函数仅使用匿名直接值也很自然。下面这样的写法很常见,也很方便,对于刚刚接触JavaScript的同学可能需要习惯一段时间。
var theArray = [1,2,3,4]; var theSum = 0; function forEach(array, action){ if(array.length==undefined){ throw “Input is not array.” } for(var i=0;i<array.length;i++){ action(array[i]); } } forEach(theArray, function(n){ theSum+=n; })
上面的语句只有4句,浏览器对这4句语句顺序执行:1.定义一个数组theArray;2.定义一个值theSum;3.定义一个函数forEach(函数是值,所以这一句和之前两句没有区别,都是将值赋给一个变量)。4.调用函数forEach,将theArray和一个匿名函数传给forEach函数作为参数,该匿名函数影响了theSum值。
函数内部有隐式的变量arguments,该变量是一个数组,顾名思义表示函数传入的参数。
JavaScript说到底是一种脚本语言,最初的使命就是在一个特定的环境下,完成一项特定的有始有终的工作。原则上完成某个工作,只需要顺序编写语句就可以了。函数对工作进行了抽象,因此对于复杂工作,往往先嵌套地定义函数,然后调用之。上面说到,这里的函数依赖于当前作用域,更类似于“过程”。这样进行函数式的编程,最后程序的基本流程大致就变成了:定义全局变量、定义函数、调用函数这样的形式。
对象和类:原型
JavaScript中的对象本质上就是名值对。对象也是是object类型的值,可以赋给变量。对象可以通过对象直接量创建,也可以通过构造函数创建。
var point1 = {x:1,y:0,z:0} var point2 = {x:0,y:1,z:0} function Point(theX,theY,theZ){ this.x=theX; this.y=theY; this.z=theZ; } var point3 = new point(1,1,0);
对象最重要的特征是继承。在其他面向对象编程语言中,编译器很轻松地知道,哪几个对象时同一类的,哪个类继承了哪个类,这在JavaScript中是由原型实现的。每一个对象都有一个原型对象,可以通过prototype字段访问。对象的原型对象就是对象构造函数的原型,比如point3.prototype就是Point.prototype,构造函数的原型对象在定义构造函数时也就生成了。构造函数是值,因此即使是函数体完全相同的构造函数,其值也是不一样的,其原型自然也就不一样。具有相同原型的对象,往往由相同的构造函数产生,这意味着这些对象是“同一类”,如同C#中同一类的多个实例一样。以对象直接量(即名值对,如point1和point2)赋值给对象的时候,实际上是隐式调用了构造函数Object(),因此这样的对象其原型是Object.prototype。当对象访问某个属性(即通过名查询值)的时候,首先查看对象本身是否是具有该值,然后是原型是否具有,然后是原型的原型是否具有……直到Object.prototype,只有该对象没有原型。
具有相同原型的对象共享原型(这好像是废话),使得原型成为储存这些对象的公有成员的理想场所。比如,每个三角形对象不一样,但是计算三角形面积的方法是一样的,因此该方法应当定义在三角形对象的原型上,否则,对于每个三角形对象都会生成一个一模一样的函数——函数是值,正如我们之前强调的一样,函数会占用空间。
function Triangle(thePoint1, thePoint2, thePoint3){……} Triangle.prototype.area = function(){……}; var t=Triangle(point1, point2, point3); t.area(); //->0.5
上面的代码使用了本节开始处代码的上下文,实际上point1和point2是同一类,point3是另外一类,这并不影响它们在Triangle函数中的表现,只需要能够提供x、y、z字段的对象都能正常工作。
错误处理
使用try…catch…finally块作错误处理,使用throw关键字抛出异常。异常由用户自己定义,如使用对象直接量,注意这里的type不是关键字,可以换成其他需要的字眼。
throw {
type:”ArgumentInvalidException”,
message:”Input is not number.”
}
往往希望在某一层次只处理某一种类的异常,非该种类异常交由上层处理,可以使用捕捉异常、检查异常、不符合要求再抛出的方法。
catch(e){ if(e.type!=” ArgumentInvalidException”){ throw e; } // 处理ArgumentInvalidException类型的异常 }
对功能函数编写测试函数,尽可能多地实验各种异常情况是好的习惯。