js变量,表达式和语句

Published: 12 Jun 2018 Category: javascript

一、Js的函数作用域和提前声明

在一些类似C语言的编程语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,我们称为块级作用域(block scope),而JavaScript中没有块级作用域。JavaScript取而代之地使用了函数作用域(function scope):变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。

JavaScript的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。有意思的是,这意味着变量在声明之前甚至已经可用。JavaScript的这个特性被非正式地称为声明提前(hoisting),即JavaScript函数里声明的所有变量(但不涉及赋值)都被“提前”至函数体的顶部[12],看一下如下代码:

var scope="global";
function f(){
console.log(scope);//输出"undefined",而不是"global"
var scope="local";//变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的
console.log(scope);//输出"local"
}

你可能会误以为函数中的第一行会输出”global”,因为代码还没有执行到var语句声明局部变量的地方。其实不然,由于函数作用域的特性,局部变量在整个函数体始终是有定义的,也就是说,在函数体内局部变量遮盖了同名全局变量。

在具有块级作用域的编程语言中,在狭小的作用域里让变量声明和使用变量的代码尽可能靠近彼此,通常来讲,这是一个非常不错的编程习惯。由于JavaScript没有块级作用域,因此一些程序员特意将变量声明放在函数体顶部,而不是将声明靠近放在使用变量之处。这种做法使得他们的源代码非常清晰地反映了真实的变量作用域。

二、作为属性的变量

当声明一个JavaScript全局变量时,实际上是定义了全局对象的一个属性。 当使用var声明一个变量时,创建的这个属性是不可配置的,也就是说这个变量无法通过delete运算符删除。可能你已经注意到了,如果你没有使用严格模式并给一个未声明的变量赋值的话,JavaScript会自动创建一个全局变量。以这种方式创建的变量是全局对象的正常的可配值属性,并可以删除它们:

var truevar=1;  //声明一个不可删除的全局变量
fakevar=2;      //创建全局对象的一个可删除的属性
this.fakevar2=3;//同上
delete truevar  //=>false:变量并没有被删除
delete fakevar2 //=>true:变量被删除
delete this.fakevar2 //=>true:变量被删除

JavaScript全局变量是全局对象的属性,这是在ECMAScript规范中强制规定的。对于局部变量则没有如此规定,但我们可以想象得到,局部变量当做跟函数调用相关的某个对象的属性。ECMAScript 3规范称该对象为“调用对象”(call object),ECMAScript 5规范称为“声明上下文对象”(declarative environment record)。JavaScript可以允许使用this关键字来引用全局对象,却没有方法可以引用局部变量中存放的对象。

三、作用域链

JavaScript是基于词法作用域的语言:通过阅读包含变量定义在内的数行源码就能知道变量的作用域。全局变量在程序中始终都是有定义的。局部变量在声明它的函数体内以及其所嵌套的函数内始终是有定义的。

如果将一个局部变量看做是自定义实现的对象的属性的话,那么可以换个角度来解读变量作用域。每一段JavaScript代码(全局代码或函数)都有一个与之关联的作用域链(scope chain)。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量。

当JavaScript需要查找变量x的值的时候(这个过程称做“变量解析”(variable resolution)),它会从链中的第一个对象开始查找,如果这个对象有一个名为x的属性,则会直接使用这个属性的值,如果第一个对象中不存在名为x的属性,JavaScript会继续查找链上的下一个对象。如果第二个对象依然没有名为x的属性,则会继续查找下一个对象,以此类推。如果作用域链上没有任何一个对象含有属性x,那么就认为这段代码的作用域链上不存在x,并最终抛出一个引用错误(ReferenceError)异常。

在JavaScript的最顶层代码中(也就是不包含在任何函数定义内的代码),作用域链由一个全局对象组成。在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象。

当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。对于嵌套函数来讲,事情变得更加有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都有微妙的差别——在每次调用外部函数时,内部函数的代码都是相同的,而且关联这段代码的作用域链也不相同。

作用域链的概念对于理解with语句是非常有帮助的,同样对理解闭包的概念也至关重要。

四、对象创建表达式

对象创建表达式(object creation expression)创建一个对象并调用一个函数(这个函数称做构造函数)初始化新对象的属性。

new Object()
new Point(2,3)

如果一个对象创建表达式不需要传入任何参数给构造函数的话,那么这对空圆括号是可以省略掉的

new Object
new Date

五、in运算符

in运算符希望它的左操作数是一个字符串或可以转换为字符串,希望它的右操作数是一个对象。如果右侧的对象拥有一个名为左操作数值的属性名(并不是值),那么表达式返回true,例如:

var point={x:1,y:1};    //定义一个对象
"x"in point     //=>true:对象有一个名为"x"的属性
"z"in point     //=>false:对象中不存在名为"z"的属性
"toString"in point      //=>true:对象继承了toString()方法
var data=[7,8,9];       //拥有三个元素的数组
"0"in data          //=>true:数组包含元素"0"
1 in data           //=>true:数字转换为字符串
3 in data           //=>false:没有索引为3的元素
7 in data           //=>false:没有索引为7的元素
data.indexOf(7)     //=>0

六、delete运算符

delete是一元操作符,它用来删除对象属性或者数组元素[9]。就像赋值、递增、递减运算符一样,delete也是具有副作用的,它是用来做删除操作的,不是用来返回一个值的,例如

var o={x:1,y:2};    //定义一个对象
delete o.x;         //删除一个属性
"x"in o             //=>false:这个属性在对象中不再存在
var a=[1,2,3];      //定义一个数组
delete a[2];        //删除最后一个数组元素
2 in a;             //=>false:元素2在数组中已经不存在了
a.length            //=>3:注意,数组长度并没有改变,尽管上一行代码删除了这个元素,但删除操作留下了一个“洞”,实际上并没有修改数组的长度,因此a数组的长度仍然是3

var o={x:1,y:2};    //定义一个变量,初始化为对象
delete o.x;     //删除一个对象属性,返回true
typeof o.x;     //属性不存在,返回"undefined"
delete o.x;     //删除不存在的属性,返回true
delete o;       //不能删除通过var声明的变量,返回false

七、with,debugger,use strict 语句

7.1 with

作用域链(scope chain)是一个可以按序检索的对象列表,通过它可以进行变量名解析。with语句用于临时扩展作用域链,例如:

with(object)
statement

这条语句将object添加到作用域的头部,然后执行statement,最后把作用域链恢复到原始状态。

在严格模式中是禁止使用with语句的,并且在非严格模式里也是不推荐使用with语句的。那些使用with语句的js代码非常难于优化,并且同没有使用with语句的代码相比,它运行得更慢。

在对象嵌套层次很深的时候通常会使用with语句来简化代码编写。例如,在客户端JavaScript中,可能会使用类似下面这种表达式来访问一个HTML表单中的元素:

document.forms[0].address.value

如果这种表达式在代码中多次出现,则可以使用with语句将form对象添加至作用域链的顶层:

with(document.forms[0]) {//直接访问表单元素,例如:
    name.value="";
    address.value="";
    email.value="";
}

这种方法减少了大量的输入,不用再为每个属性名添加document.forms[0]前缀。这个对象临时挂载在作用域链上,当JavaScript需要解析诸如address的标识符时,就会自动在这个对象中查找。当然,不使用with语句的等价代码可以写成这样:

var f=document.forms[0];
f.name.value="";
f.address.value="";
f.email.value="";

不要忘记,只有在查找标识符的时候才会用到作用域链,创建新的变量的时候不使用它,看一下下面这行代码:

with(o)x=1;

如果对象o有一个属性x,那么这行代码会给这个属性赋值为1. 如果o中没有定义属性x,这段代码和不使用with语句的代码x=1是一模一样的。它给一个局部变量或者全局变量x赋值,或者创建全局对象的一个新属性。with语句提供了一种读取o的属性的快捷方式,但它并不能创建o的属性。

7.2 debugger语句

debugger语句通常什么也不做,可用于调试。

这条语句会产生一个断点,执行到它时会停止。例如:

function f(o){
if(o===undefined)debugger;//这一行代码只是用于临时调试
...//函数的其他部分
}

7.3 “use strict”

“use strict”是ECMAScript 5引入的一条指令。指令不是语句(但非常接近于语句)。”use strict”指令和普通的语句之间有两个重要的区别:

  • 它不包含任何语言的关键字,指令仅仅是一个包含一个特殊字符串直接量的表达式(可以是使用单引号也可以使用双引号)。将来的ECMAScript标准希望将use用做关键字,这样就可以省略引号了。
  • 它只能出现在脚本代码的开始或者函数体的开始、任何实体语句之前。但它不必一定出现在脚本的首行或函数体内的首行,因为”use strict”指令之后或之前都可能有其他字符串直接量表达式语句,并且JavaScript的具体实现可能将它们解析为解释器自有的指令。在脚本或者函数体内第一条常规语句之后字符串直接量表达式语句只当做普通的表达式语句对待;它们不会当做指令解析,它们也没有任何副作用。

使用”use strict”指令的目的是说明(脚本或函数中)后续的代码将会解析为严格代码(strict code)。

严格代码以严格模式执行。ECMAScript 5中的严格模式是该语言的一个受限制的子集,它修正了语言的重要缺陷,并提供健壮的查错功能和增强的安全机制(关注前三点):

  • 在严格模式中禁止使用with语句。
  • 在严格模式中,所有的变量都要先声明,如果给一个未声明的变量、函数、函数参数、catch从句参数或全局对象的属性赋值,将会抛出一个引用错误异常(在非严格模式中,这种隐式声明的全局变量的方法是给全局对象新添加一个新属性)。
  • 在严格模式中,调用的函数(不是方法)中的一个this值是undefined。(在非严格模式中,调用的函数中的this值总是全局对象)。可以利用这种特性来判断JavaScript实现是否支持严格模式:
  • 同样,在严格模式中,当通过call()或apply()来调用函数时,其中的this值就是通过call()或apply()传入的第一个参数(在非严格模式中,null和undefined值被全局对象和转换为对象的非对象值所代替)。
  • 在严格模式中,给只读属性赋值和给不可扩展的对象创建新成员都将抛出一个类型错误异常(在非严格模式中,这些操作只是简单地操作失败,不会报错)。
  • 在严格模式中,传入eval()的代码不能在调用程序所在的上下文中声明变量或定义函数,而在非严格模式中是可以这样做的。相反,变量和函数的定义是在eval()创建的新作用域中,这个作用域在eval()返回时就弃用了。
  • 在严格模式中,函数里的arguments对象拥有传入函数值的静态副本。在非严格模式中,arguments对象具有“魔术般”的行为,arguments里的数组元素和函数参数都是指向同一个值的引用。
  • 在严格模式中,当delete运算符后跟随非法的标识符(比如变量、函数、函数参数)时,将会抛出一个语法错误异常(在非严格模式中,这种delete表达式什么也没做,并返回false)。
  • 在严格模式中,试图删除一个不可配置的属性将抛出一个类型错误异常(在非严格模式中,delete表达式操作失败,并返回false)。
  • 在严格模式中,在一个对象直接量中定义两个或多个同名属性将产生一个语法错误(在非严格模式中不会报错)。
  • 在严格模式中,函数声明中存在两个或多个同名的参数将产生一个语法错误(在非严格模式中不会报错)

八、常量和局部变量

可以使用const关键字来定义常量。常量可以看成不可重复赋值的变量(对常量重新赋值会失败但不报错),对常量的重复声明会报错。

const pi=3.14;  //定义一个常量并赋值
pi=4;   //任何对这个常量的重新赋值都被忽略
const pi=4; //重新声明常量会报错
var pi=4;   //这里也会报错

一直以来,JavaScript中的变量缺少块级作用域的支持被普遍认为是JavaScript的短板,JavaScript 1.7针对这个缺陷增加了关键字let。

关键字let有4种使用方式:

  • 可以作为变量声明,和var一样;
  • 在for或for/in循环中,作为var的替代方案;
  • 在语句块中定义一个新变量并显式指定它的作用域;
  • 定义一个在表达式内部作用域中的变量,这个变量只在表达式内可用。

使用let最简单的方式就是批量替换程序中的var。通过var声明的变量在函数内都是可用的,而通过let声明的变量则只属于就近的花括号括起来的语句块(当然包括它所嵌套的语句块)。

function oddsums(n){
    let total=0,result=[];  //在函数内都是有定义的
    for(let x=1;x<=n;x++){ //x只在循环体内有定义
        let odd=2*x-1;          //odd只在循环体内有定义
        total+=odd;
        result.push(total);
    }
    //这里使用x或odd会导致一个引用错误
    return result;
}
oddsums(5); //返回[1,4,9,16,25]