说明
前面的两篇博客介绍了js中的基本知识中的变量类型、标识符等。这篇博客主要谈表达式以及运算符。
原始表达式
原始表达式就是表达式中最小的,不能在分割的表达式,一般指变量、常数直接量、关键字(true、false、null、this),这里需要注意的是undefined是一个全局变量,而不是关键字。
对象和数组的初始化表达式
数组初始化表达式是一个方括号,中间放置数组中的元素,元素间用逗号分割。元素可以是表达式、数组、空。
1 [] //一个空数组,数组里面没有元素
2 [1+2,3+4] //一个拥有两个元素的数组,第一个是3,第二个是7。
3 [[1,2,3],[4,5,6],[7,8,9,]] //数组中的元素还可以是数组。
4 [1,,,,5] //数组中的元素可以为空,这时空位会填充值undefined。
在执行var a=[1,,,,5]这条语句后,在chrome中表现为下图所示:
对象初始化表达式和数组初始化表达式非常类似。
1 var stu={name:"张三", age:15}; //定义一个拥有两个属性的对象。
上面这行初始化对象的表达式类似于下面的数组,事实上js也确实支持这种写法访问对象中的属性。
1 stu["name"]="张三";
2 stu["age"]=15;
如果你学过java那么上面那句var stu={name:"张三", age:15};可以很容易的写成下面的java代码:
1 class Student{
2 String name;
3 int age;
4
5 Student(String name, int age){
6 this.name=name;
7 this.age=age;
8 }
9 }
10
11 Student stu=new Student("张三", 15);
不过js可以完美支持多层的内部类,而java却对内部类有好多限制。
属性访问表达式
在js中,属性访问有①expression.identifier、②expression[expression]这两种方式。其中第一种虽然方便却只能用于对象,而且要求访问的属性(identifier)不能是关键字、不能含有空格、不能是数字。而第二种访问方式不仅支持对象还能用于数组,并且用于对象访问自己的属性时没有第一种那么多的限制。
1 var o={x:1, y:{z:3}};
2 var a=[o, 4, [5, 6]];
3 o.x //1:访问o的x属性
4 o.y.z //3:访问o的y的z属性
5 o["x"] //1:访问o的x属性
6 a[1] //4:访问a的索引为1的元素
7 a[2]["1"] //6:访问a[2]中的索引为1的元素
8 a[0].x //1:通过访问a[0]得到o,再访问o的x属性
运算符
js所支持的运算符如上图所示,学过其它编程的同学基本上看上一眼就知道了。这里只对需要注意的部分做出说明。在基本算术运算符(加减乘除和求余)只有加法是相对有难度的,因为有时候它是加法,而有时候它又是字符串连接符,具体用法可概括为只有+运算符两边都是数字才是加法,更详细的用法请参考这篇博客。此外还需要谈的就是求余运算符,其运算结果的符号和左操作数相同,即5%1=1、-5%1=-1。
在js的位运算时,会有一个隐式转换过程。默认情况下js中的数字都是存储为64位浮点数格式,在位运算的时候会转换为32位整数,所以隐式转换会截断小数部分和整数中超过32位能表示的部分。此外NaN、infinity和-infinity会转换为0。
关于等于(==)运算符和严格等于(===)运算符请参考本系列教程的第一篇。
in运算符
in运算符希望它的左操作数是一个字符串或者能转换为字符串,希望它的右操作数是一个对象。如果右侧对象拥有一个名为左操作数的属性则整个in表达式返回true。
1 var point={x:1, y:1};
2 "x" in point //true:对象中有一个名为x的属性
3 "z" in point //false:对象中不存在一个名为z的属性
4 "toString" in point //true:对象继承了toString()方法
5
6 var data=[7, 8, 9];
7 "0" in data //true:数字包含下标为0的元素
8 1 in data //true:数字转换为字符串
9 3 in data //false:没有索引为3的元素
&&与||
学过C和java等语言的同学应该知道这两个运算符有一个特性就是短路,这个特性在js中也同样存在,习惯性利用这种特性为函数提供默认值。
1 function copy(o, p){
2 o.p = p || {};
3 }
上面的代码中,如果copy函数被调用的时候p形参有值,则将p复制给o.p,否则将默认值{}复制给o.p。
表达式计算
在js中存在一个函数叫eval(),虽然这是一个函数,但一般被当成运算符来对待。一般来说函数是可以起别名的,比如var f=eval;再通过f调用eval函数。但是一般不推荐对eval函数起别名,ECMAScript3标准甚至直接禁止对eval函数起别名,所以说它几乎被当成运算符对待。之所以这样是因为这涉及到代码优化的问题,如果允许对eval函数起别名将严重影响代码的优化过程。
上面谈了eval的特点,现在来说一下它的作用。这个函数只有一个参数,且一般是字符串。如果传入的参数是字符串,js解释器会把这个字符串当成js代码来执行,且其执行环境为直接调用这个eval函数的上下文(下面将详细解释),如果其传入的参数不是字符串,则eval函数直接返回这个参数值。
1 var global=1;
2 var foo = function(ele){
3 var local = 2;
4 eval(ele);
5 console.log(where);
6 }
7 foo("console.log(global);console.log(local);var where='I am here!';");
8 console.log(where);
上面这行代码的执行结果(在chrome中)如下图所示:
根据上面所说的eval的执行环境为直接调用eval的上下文,在上图显示的结果中为第8行所处的上下文,这样的话eval自然能访问global和local变量,但是其定义的where变量只能让foo函数内部且在第8行以后的代码访问,这样以来第9行代码能正确执行,而第12行代码抛出异常。
前面谈过ECMAScript3禁止对eval起别名,但是现实中大多数解释器的实现并非如此,之后的ECMAScript5标准也允许别名调用,但规范了其行为。当eval被别名调用时,其运行环境永远为全局环境,与其调用者环境没有关系,也就是说间接调用(别名调用)无法读、写、定义局部变量。
1 var geval = eval;
2 var x = "global", y = "global";
3 function f(){
4 var x="local";
5 eval("x += ' changed';");
6 return x;
7 }
8 function g(){
9 var y="local";
10 geval("y += ' changed';");
11 return y;
12 }
13 console.log(f(), x);
14 console.log(g(), y);
上面代码执行的结果(在chrome中)为: