9月阅读周·JavaScript权威指南:表达式和运算符,表达式的创建、调用与计算篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读八个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》。
当前阅读周书籍:《JavaScript权威指南》。
对象创建表达式
对象创建表达式(object creation expression)创建一个对象并调用一个函数(这个函数称做构造函数)初始化新对象的属性。对象创建表达式和函数调用表达式非常类似,只是对象创建表达式之前多了一个关键字new:
new Object()
new Point(2,3)
如果一个对象创建表达式不需要传入任何参数给构造函数的话,那么这对空圆括号是可以省略掉的:
new Object
new Date
当计算一个对象创建表达式的值时,和对象初始化表达式通过{}创建对象的做法一样,JavaScript首先创建一个新的空对象,然后,JavaScript通过传入指定的参数并将这个新对象当做this的值来调用一个指定的函数。这个函数可以使用this来初始化这个新创建对象的属性。那些被当成构造函数的函数不会返回一个值,并且这个新创建并被初始化后的对象就是整个对象创建表达式的值。如果一个构造函数确实返回了一个对象值,那么这个对象就作为整个对象创建表达式的值,而新创建的对象就废弃了。
函数定义表达式
函数定义表达式定义一个JavaScript函数。表达式的值是这个新定义的函数。从某种意义上讲,函数定义表达式可称为“函数直接量”,毕竟对象初始化表达式也称为“对象直接量”。一个典型的函数定义表达式包含关键字function,跟随其后的是一对圆括号,括号内是一个以逗号分割的列表,列表含有0个或多个标识符(参数名),然后再跟随一个由花括号包裹的JavaScript代码段(函数体),例如:
//这个函数返回传入参数值的平方
var square=function(x){return x*x;}
函数定义表达式同样可以包含函数的名字。函数也可以通过函数语句来定义,而不是函数表达式。
表达式计算
和其他很多解释性语言一样,JavaScript同样可以解释运行由JavaScript源代码组成的字符串,并产生一个值。JavaScript通过全局函数eval()来完成这个工作:
eval("3+2")//=>5
动态判断源代码中的字符串是一种强大的语言特性,几乎没有必要在实际中应用。如果你使用了eval(),你应当仔细考虑是否真的需要使用它。
下面讲解eval()的基础用法,并且介绍严格使用它的两种方法,从代码优化的角度讲,这两种方法对于原有代码造成的影响是最小的。
eval()
eval()是一个函数,但由于它已经被当成运算符来对待了,因此将它放在本章来讲述。JavaScript语言的早期版本定义了eval()函数,从那时起,该语言的设计者和解释器的作者对其实施了更多限制,使其看起来更像运算符。现代JavaScript解释器进行了大量的代码分析和优化。而eval()的问题在于,用于动态执行的代码通常来讲是不能分析。一般来讲,如果一个函数调用了eval(),那么解释器将无法对这个函数做进一步优化。而将eval()定义为函数的另一个问题是,它可以被赋予其他的名字:
var f=eval;
var g=f;
如果允许这种情况的话,那么解释器将无法放心地优化任何调用g()的函数。而当eval是一个运算符(并作为一个保留字)的时候,这种问题就可以避免掉。接下来的4.12.2节和4.12.3节将会介绍如何对eval()实施更多的限制,以便让它的行为更接近运算符。
eval()只有一个参数。如果传入的参数不是字符串,它直接返回这个参数。如果参数是字符串,它会把字符串当成JavaScript代码进行编译(parse),如果编译失败则抛出一个语法错误(SyntaxError)异常。如果编译成功,则开始执行这段代码,并返回字符串中的最后一个表达式或语句的值,如果最后一个表达式或语句没有值,则最终返回undefined。如果字符串抛出一个异常,这个异常将把该调用传递给eval()。
关于eval()最重要的是,它使用了调用它的变量作用域环境。也就是说,它查找变量的值和定义新变量和函数的操作和局部作用域中的代码完全一样。如果一个函数定义了一个局部变量x,然后调用eval("x"),它会返回局部变量的值。如果它调用eval("x=1"),它会改变局部变量的值。如果函数调用了eval("var y=3;"),它声明一个新的局部变量y。同样地,一个函数可以通过如下代码声明一个局部函数:
eval("function f(){return x+1;}");
如果在最顶层代码中调用eval(),当然,它会作用于全局变量和全局函数。
全局eval()
eval()具有更改局部变量的能力,这对于JavaScript优化器来说是一个很大的问题。然而作为一种权宜之计,JavaScript解释器针对那些调用了eval()的函数所做的优化并不多。但当脚本定义了eval()的一个别名,且用另一个名称调用它,JavaScript解释器又会如何工作呢?为了让JavaScript解释器的实现更加简化,ECMAScript 3标准规定了任何解释器都不允许对eval()赋予别名。如果eval()函数通过别名调用的话,则会抛出一个EvalError异常。
实际上,大多数的实现并不是这么做的。当通过别名调用时,eval()会将其字符串当成顶层的全局代码来执行。执行的代码可能会定义新的全局变量和全局函数,或者给全局变量赋值,但却不能使用或修改主调函数中的局部变量,因此,这不会影响到函数内的代码优化。
ECMAScript 5是反对使用EvalError的,并且规范了eval()的行为。“直接的eval”,当直接使用非限定的"eval"名称(eval看起来像是一个保留字)来调用eval()函数时,通常称为“直接eval”(direct eval)。直接调用eval()时,它总是在调用它的上下文作用域内执行。其他的间接调用则使用全局对象作为其上下文作用域,并且无法读、写、定义局部变量和函数。下面有一段示例代码:
var geval=eval;//使用别名调用eval将是全局eval
var x="global",y="global";//两个全局变量
function f(){//函数内执行的是局部eval
var x="local";//定义局部变量
eval("x+='changed';");//直接eval更改了局部变量的值
return x;//返回更改后的局部变量
}
function g(){//这个函数内执行了全局eval
var y="local";//定义局部变量
geval("y+='changed';");//间接调用改变了全局变量的值
return y;//返回未更改的局部变量
}
console.log(f(),x);//更改了局部变量:输出"local changed global":
console.log(g(),y);//更改了全局变量:输出"local globalchanged":
严格eval()
ECMAScript 5严格模式对eval()函数的行为施加了更多的限制,甚至对标识符eval的使用也施加了限制。当在严格模式下调用eval()时,或者eval()执行的代码段以"use strict"指令开始,这里的eval()是私有上下文环境中的局部eval。也就是说,在严格模式下,eval执行的代码段可以查询或更改局部变量,但不能在局部作用域中定义新的变量或函数。
此外,严格模式将"eval"列为保留字,这让eval()更像一个运算符。不能用一个别名覆盖eval()函数。并且变量名、函数名、函数参数或者异常捕获的参数都不能取名为"eval"。
总结
复杂表达式是由简单表达式组成的。比如,数组访问表达式是由一个表示数组的表达式、左方括号、一个整数表达式和右方括号构成。它们所组成的新的表达式的运算结果是该数组的特定位置的元素值。同样的,函数调用表达式由一个表示函数对象的表达式和0个或多个参数表达式构成。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)