一种误解是Javascript跟Java有渊源。其实很多书上也提到了Js的历史,笔者简单说一下。Javascript最早是由网景公司开发的一种脚本语言,其语法源头大部分是借鉴另外一门脚本语言Perl,早期的名称是Livescript, 跟Java完全不相关。最后改成Javascript并一致流行到现在完全是网景公司(Netscape跟Sun公司的一种行销策略)。现在的Js官方名称应该叫ECMAScript,ECMA代表欧洲计算机制造商协会,它对Js进行标准化制定,标准的实现包括浏览器对Js的解释。区别Js和Java是学好它的前提(尽管Js和Perl等脚本语言也逐渐推出了模拟面向对象的实现方式,本质上来说还是过程化的脚本语言)。
既然是脚本语言,也就是说类似Html这种标记性语言一样,Js的执行其实不需要经过编译的过程,各种厂商的浏览器内置Js解释器能对Js进行正确解析,很显然我们能推理出的信息就是Js的解释执行应该是按照从上到下的顺序来进行的。写一些例子要运行不要任何开发工具,记事本写完丢进浏览器就可以被执行。当然,浏览器内置解释Js的调适和报错机制因厂商而异,特别是浏览器底Js执行的报错不如人意,甚至很多时候都无法准确给出错误出现在哪一行,使得Js程序员在初学经验尚浅的时候饱受折磨(笔者深有体会)。
先来看看Js的语法特点吧:
Js程序是用Unicode字符集编写的,也就是说16位的Unicode编码可以标识地球上通用的每一种书面语言。这事国际化的一个重要特征,在Web开发需要国际化的时候作用相当大。(这里指ECMAScript标准化之后的Js版本1.5)。
Html不支持大小写,Js对大小写敏感,另外Js是一种松散的弱类型语言,比如语句放在不同的行上没有分号也不会报错,大家都可以联想一下常用编程语言在编译时候IDE的编译器会提示的错误在Js中通通不会出现,这样就大概都能猜到编写Js代码的时候有哪些必须注意的地方了。
弱类型的意思是Js中的变量没有类型之分,声明不声明都不会有错,若声明则统一用var关键字即可。Js支持的类型包括数字、文本字符串、布尔值、null、undefined、Object、Function。其中复合对象Object本质上包括两种,一种是对命名变量值的无序集合(稍后见示例),另外一种是有序集合(即大家所熟悉的数组),顺便提一下,Js对数组实现原理是栈存储,所以Js的数组才会有内置的push()和pop()方法。Js语言内定义的专用对象包括Date、RegExp(正则表达式)、Array、Error、Math、String、Number等。
Js数字格式允许的精度范围是正负2的53次方,但某些运算的数字范围只支持正负2的31次方。Js的字符串支持单引号或者双引号的括起来的Unicode字符序列,在数字环境中,解释器会自动将字符串转换成数字,我们只要把一个字符串减去0就可以将其转换成一个数字(转换规则相对比较严格,数字前后不能带空格等)。Js的解释器在遇到bool类型的变量时候也会根据使用环境自动完成转换,例如数字环境中true就转换为数字1,false就转换为0,字符串环境中就会转换为true或false串。
Js的函数非常特殊,因为它被当作一个变量来处理,可以被赋值传递,存储在变量中。总是说理论,这里举一个非常实用体现Js函数做为变量来处理的例子吧,现在我们需要在浏览器加载页面之后调用一系列函数来完成业务逻辑。然后Html页面只有一个onload函数可以重写赋值,怎么实现一个onload调用多个自定义函数呢:
function addLoadEvent(func){ var oldOnload = window.onload; if(typeof window.onload != 'function'){ window.onload = func; }else{ window.onload = function(){ oldOnload(); func(); } } }
我们先把window.onload函数保存在变量oldOnload中,之后再判断window的onload函数是否是函数类型,针对第一次加载之前onload为空的情况,如果不是函数类型则将onload函数第一次赋值为func参数存储的函数;如果onload已经是函数类型了,则先调用原oldOnload变量中存储的onload函数,之后再紧接着执行传递进来的func函数。 由此我们可以看到一个调用队列构成,如果我们需要在页面加载时候按次序调用A()、B()、C()三个函数只需要加三行代码:
addLoadEvent(A);
addLoadEvent(B);
addLoadEvent(C);
从这个例子我们可以体会到Js函数的灵活,毕竟是不需要通过编译直接被解释的语言,熟悉了浏览器解析Js的规律之后才能实用Js强大的功能。所以Js就是一把双刃剑,有人说它简单,其实它的链式调用结构可以很高效的完成一些算法,不需要遵循繁琐的语法规则(适配编译器),功能十分强大;然而眼下的Web开发现状是大多数人不遵循合理的规范去写Js,使得页面Js代码乱而且不易维护,大家都因Js语言松散而养成了写Js习惯也松散的坏毛病。说到底还是一个程序员的基本素质问题,编写良好风格的代码,不管使用什么语言都是应该遵守的规则。
继续介绍Js语言的其它特点吧,Js中声明了变量未赋值的会默认为null,而调用未声明的变量返回的值应该是undefined,虽然undefined和null值不同,但==运算却将两者看作相等。Js中的传值和传址基本规则是:基本数据类型通过传值来操作,而引用类型传址。比较特殊的是字符串在Js中被当作基本类型传值来操作,比较字符串可以用基本类型操作符==来完成,但有一种情况例外,就是两个都使用了new关键字的String对象比较是比较地址,其它情况都是比较值:
var s1 = new String(“hello”); var s2 = new String(“hello”); var s3 = “hello”; alert(s1==s2); //return false alert(s1==s3); //return true
前文提到了,一些在其它语言的编译错误操作在Js中有着不会出错的解释方式,因为Js不需要编译直接解释执行。例如重复声明变量在Js中完成的只不过是一次赋值操作而已;未声明(没有var)的变量会自动隐式创建为全局变量, 即使在在一个函数体内使用。这里就出现了一种坏习惯造成的问题,如果在函数内不声明变量直接使用,一旦全局有一个同名变量,程序员误认为这只是局部变量,实际全局变量会被修改。
Js语法中有一个容易被忽略的地方就是并没有块级别作用域,只存在全局和局部两种作用而已。这也是Js函数的特殊性相关的,在函数内部无论几层嵌套括号,局部变量在整个函数中都是有定义的(注意是整个函数中),下面这个例子可以给出有力的证明:
var ss = “global”; function tt(){ alert(ss); var ss = “local”; alert(ss); }
由于局部变量ss的定义占据整个函数,所以第一次输出是undefined,不会是全局的global。这里顺便提到一个在Js中很强大却难以理解的功能就是闭包,什么是Js中的闭包呢?先了解一下Js作用域链(又称调用链)的概念:每个Js执行环境都有一个和它关联在一起的作用域链,这个作用域链是一个对象列表或者对象链,当Js需要解析变量x的值时,就开始查找该链的第一个对象,以此类推找下去。例如在一个不含嵌套的函数体中作用域链就有两个对象构成,一个函数调用对象,另外一个是全局对象,引用变量首先查找调用对象再是全局对象。
Js中的函数是通过词法来划分作用域的,这意味着函数是在定义它们的作用域里运行,而不是执行它们的作用域里运行,当定义了函数,当前的作用域链就保存起来,并成为函数内部状态的一部分(除了顶层作用域链仅由全局对象组成,跟词法没关系)。那么,当定义一个嵌套的函数时,作用域链就包括了外围函数,在这个嵌套函数里,可以访问外围函数中定义的所有变量(参照刚讲完的局部变量原则)。虽然作用域链已经形成,但其链上调用点的属性值是可以更改的,当嵌套函数的引用保存到一个全局作用域中,即使函数被调用返回退出了,函数的局部变量名字和值都依然存在,这就形成了一个闭包(将要执行的代码以及执行这些代码的作用域构成一个综合体)。可能理解闭包对大多数人比较困难,需要弄清楚词法分析等概念,但其实用起来很清楚简单,我们来看一个例子吧:
function f(s){ //传递参数s var x = "local"; //局部变量x function g(){ //嵌套函数 alert(x); alert(s); } return g; //返回嵌套函数引用 } var global = f("hello"); //对全局变量global赋值 global(); //调用全局函数变量,以此弹出”local”和”hello”
这个例子说明,即使函数f已经执行完毕,但由于闭包形成,f的局部变量和参数值在定义f(即使浏览器解释前5行)的时候就已经定义在调用链上了,然而随嵌套函数引用g的返回而挂在了全局调用链上,所以通过闭包连接依然可以找到已经执行退出的函数的局部变量和参数值。这就是所谓的闭包!
闭包是一种有趣又强大的技术,我们并不是一定要用它做什么,但真正理解它可以帮助我们学习到Js的精髓。
最后介绍一下Js的面向对象模拟特性吧,虽然不是真正的面向对象。Js中每个对象都有一个constructor属性,它引用了初始化这个对象的构造函数,有助于确定一个对象的类型。Js中的instanceof运算符本质就是检查constructor属性值。所有的函数都有一个prototype属性,当这个函数被定义的时候,prototype属性自动创建和初始化,初始化值是一个对象,只带有一个属性,名为constructor,指回到和原型相关联的那个构造函数上。强调一点:Js没有真正的类,只有通过构造函数和原型函数实现的伪类而已。看个例子:
function Person(name, age){ this.name = name; this.age = age; } Person.prototype.display = function(){ return this.name+”,”+this.age;}
定义了一个Person类,Js中this关键字代表函数调用对象(还记得刚说完的函数调用链么),正因如此,每次调用this值都会不同,所以模拟出了类成员变量的效果。而prototype代表函数的constructor属性,不因调用对象而改变,但可以通过this关键字引用调用对象,实现了静态方法的效果。那么面向对象的封装即私有变量又怎么实现呢?答案自然是闭包!
function Person(name, age){ this.getName = function(){return name;} this.getAge = function(){ return age;} }
——还记得刚讲过的闭包么?即使在函数调用退出之后,函数参数name和age也会保留在函数返回值的作用域中能被访问到,这个返回值就是所谓构造函数返回的新建对象,而保留下来的参数值就是这个新建对象的私有变量。只能通过getName和getAge才能在调用链上找到闭包形成的name和age值。