基本知识

JavaScript变量基本常识


1、js变量定义语法(全局变量、局部变量)
ECMA-262 写道
If the variable statement occurs inside a FunctionDeclaration, the variables are defined with function-local
scope in that function, as described in s10.1.3. Otherwise, they are defined with global scope (that is, they
are created as members of the global object, as described in 10.1.3) using property attributes { DontDelete
}. Variables are created when the execution scope is entered. A Block does not define a new execution
scope. Only Program and FunctionDeclaration produce a new scope. Variables are initialised to undefined
when created. A variable with an Initialiser is assigned the value of its AssignmentExpression when the
VariableStatement is executed, not when the variable is created.



文档指出,当在函数声明中声明变量时,变量将作为函数局部变量(准确来说,挂在与该函数相关联的执行环境中变量对象下成为其属性),否则,他们将成为全局变量。变量是在执行流进入函数执行环境时被创建的。文档同时指出,ECMAScript没有块级作用域的概念。只有程序(各种Statement与 FunctionDeclaration的集合)或函数声明能创建一个新的作用域。

变量创建后默认被初始化为undefined,如果提供初始化器,则在变量声明被执行的时候赋值(笔者认为赋值的时间点在创建时或在执行时,对编码影响不大,FIXME)。



值得一提的是,文档所提到的variable statement,是不能缺少var关键字的,隐藏含义即,缺少var,即使定义(严谨来说不算定义,理解为不声明而直接初始化之)在函数声明中,也是全局变量。给出相关语法:


Syntax 写道
VariableStatement :
var VariableDeclarationList ;

VariableDeclarationList :
VariableDeclaration
VariableDeclarationList , VariableDeclaration

VariableDeclaration :
Identifier Initialiseropt

Initialiser :
= AssignmentExpression

来看一些demo
Js代码 收藏代码

1. var n0;//初始化为undefined
2. var n1 = 1;//全局变量
3. n2 = 2;//全局变量
4. //window.n2=2;//与上一行等价,但IE部分版本不支持
5. //n2;//错误语法,抛出ReferenceError,必须提供初始化器,或提供var关键字
6. function demo() {
7. var n3 = 3;//局部变量
8. n4 = 4;//全局变量,不推荐这样定义
9. }
10. alert(n0);//undefined
11. alert(n1);//1
12. alert(n2);//2
13. demo();//别漏掉这行,否则n4不会创建
14. //alert(n3);//抛出ReferenceError
15. alert(n4);//4
16. alert(window.n4);//虽IE不支持,但每个版本都可以通过它来取值


再补充一些demo

全局变量的属性是无法删除的(后来发现局部的也是如此)


Js代码 收藏代码
1. var n1 = 1;
2. delete n1;//无效
3. //delete window.n1;//IE不支持,证明其{DontDelete}特点,chrome支持,但都无效:
4. alert(n1);//1
6. //顺便熟悉一下delete语法
7. var r1 = new Object();//reference
8. r1.name = "jack";
9. alert(r1.name);//jack
10. delete r1.name;
11. alert(r1.name);//undefined
12.
13. var p1 = "hello";//primitive
14. p1.name = "mike";//不可在原始类型上动态添加属性,不会导致错误但无效
15. alert(p1.name);//undefined


为了或得更好的页面性能,可尝试尽早的释放全局变量,尤其是引用类型的全局变量。(置为null,解除引用),否则只能依赖垃圾回收或应用的关闭了。至于局部变量,因为在执行流退出其执行环境的时候,环境本身会释放,所以不用操心。

一些猜想:即使能delete也只能导致其值为undefined,而垃圾回收应只针对空指针引用(null)而非undefined,大概这是 ECMA-262从第三版引入Undefined类型的原因之一,正式区分空指针和未初始化变量,便于更有效的管理内存。FIXME

文档中指出全局变量不可delete,是否局部的可以呢(尽管这样实在没什么必要。。),笔者YY了一些代码,仅供参考:


Js代码 收藏代码

1. function demo() {
2. var n = 1;
3. delete n;//无效。。
4. delete arguments.callee.n;//也无效。。。但都不会报错
5. alert(n);
6. }
7. demo();


当执行流进入函数时,即进入某一个执行环境,该环境关联一个变量对象(variable object),该对象以property形式保存在该环境中声明的所有变量及函数,换句话说,函数中声明的所有变量其base object都是variable object,根据delete操作符的语法,找到base object才有可能删除其下的属性,虽然ECMA-262并未明文指出variable object的明确定义,但在文档中多处提到“using xxx as the variable object”,上面这个demo中的callee就是持有arguments的对象,但删除失败,只能证明这个variable object和其关联的activation object(活动对象)是无法通过代码引用的。FIXME


2、弱类型
ECMA-262 Language Overview 写道
ECMAScript syntax intentionally resembles Java syntax. ECMAScript syntax is relaxed to enable it to
serve as an easy-to-use scripting language. For example, a variable is not required to have its type declared
nor are types associated with properties, and defined functions are not required to have their declarations
appear textually before calls to them.

ECMAScript语法类似Java,但语法较为松散。变量在声明时无需指定类型,被定义函数也无需在调用前进行声明(像C语言那样)


语法的松散特点之一体现在变量不光是值,其类型也可以在生命周期中改变(不推荐反复更改一个变量的值类型)变量包含2种:原始类型(Undefined,Boolean,Null,Number,String) 和引用类型.原始类型保存于栈内存,占有固定大小空间,具有索引速度快的特点.
引用类型为保存于堆内存中的对象,大小不固定,索引相对较慢,变量实际上保存的是指向某对象的一个指针不同类型变量,操作方式也不一样,下面仅浅析部分特点(FIXME):

原始类型在对变量进行复制时,会在栈内存中创建一个新值交由新变量持有,而引用类型,则是在栈内存中复制指针地址值,交由新变量持有,但此时,堆中对象仍然保持一个不变,即2指针指向同一对象。
Js代码 收藏代码

1. var n1 = 1;
2. var n2 = n1;
3. n1 = 2;
4. alert(n2);//1,不受n1影响
5.
6. var r1 = new Object();
7. r1.name = "jack";
8. var r2 = r1;
9. alert(r2.name);//jack
10. r1.name = "mike";
11. alert(r2.name);//mike,受r1影响

2、浅析JavaScript变量作用域

在谈及变量作用域的时候,execution context(执行环境)是一个非常关键的概念,并且相关伴随很多内部机制,理解其工作原理,能帮助我们更好的识别变量的行为和状态。
ECMA-262 写道
When control is transferred to ECMAScript executable code, control is entering an execution context. Active
execution contexts logically form a stack. The top execution context on this logical stack is the running
execution context.
......
Every execution context has associated with it a variable object. Variables and functions declared in the
source text are added as properties of the variable object. For function code, parameters are added as
properties of the variable object.
......
Every execution context has associated with it a scope chain. A scope chain is a list of objects that are
searched when evaluating an Identifier. When control enters an execution context, a scope chain is
created and populated with an initial set of objects, depending on the type of code. During execution
within an execution context, the scope chain of the execution context is affected only by with
statements (see 12.10) and catch clauses (see 12.14).




当执行流进入一段可执行代码时,便进入一个执行环境,活动的执行环境组成一个环境栈,栈顶端的执行环境即为当前正在运行的环境。当环境退出时,相关资源也会得以释放。



我们知道JavaScript是一个基于对象的语言(object-based),那么对于变量及函数的管理也是基于这个特点。每个执行环境都会关联一个变量对象(variable object),变量对象是在进入某一个环境的时候进行创建并初始化的,所有在环境中声明的变量和函数都会作为其property挂载在这个对象上,包括函数中的参数。


那么我们所关注的变量,其在使用过程中是如何被访问,当遇到同名变量时,如何对其进行追溯并确认的呢?在其内部,是通过一个名为作用域链(scope chain)这样的线性数据结构来保证对环境中所有变量和函数的有序访问的。作用域链是在执行流进入一个执行环境的时候进行初始化的,里面填充的对象,根据进入的代码类型不一样,会产生动态变化,但大多数填充的都是每个执行环境当中的变量对象,并且,在作用域的前端保存的为当前正在运行的执行环境的变量对象(也有一个说法,称之为活动对象activation object)。文档同时指出,还可以通过2种方式来改变作用域链的格局,with和try-catch。

对标识符的解析,是一个沿着作用域链从左至右的搜索过程,直至找到标识符并返回为止。就变量的可见性而言,作用域链成为当前环境可以访问到的变量的严格范围,即如果访问一个当前环境所关联的作用域以外的变量,则会抛出类似引用不到的错误(注意,不是undefined)。FIXME


ECMA-262 写道
Every function and constructor call enters a new execution context, even if a function is calling itself
recursively. Every return exits an execution context. A thrown exception, if not caught, may also exit one
or more execution contexts.
When control enters an execution context, the scope chain is created and initialised, variable instantiation
is performed, and the this value is determined.
The initialisation of the scope chain, variable instantiation, and the determination of the this value depend
on the type of code being entered.

文档对何时会进入一个新的执行环境进行了明确的定义:

函数和构造器的调用,包括函数的递归调用,都会导致进入一个新的环境,return则退出,如果抛出异常,无catch的情况下也会导致进入新环境。

好了,将这段信息结合上文理解,那么问题来了,读者可能会提出这样的大哉问:不同的执行环境都关联着这样一个作用域,那么,在某一个环境中所能访问到的变量,随着执行流的forward,是如何因执行环境的不同而产生变化的?更进一步的说,环境所占用的资源,又是怎样释放的?要分析这个问题,对作用域在不同时期状态的理解,将成为解决问题的关键。



下面我们一起来看2个demo,笔者主要结合环境栈和作用域来尝试进行分析:
Js代码 收藏代码

1. var n1 = 1;
2. alert(n1);//mark1
3. function first() {
4. var n1 = 2;
5. var n3 = 3;
6. alert(n1);//mark2
7. second();
8. //alert(n4);//mark4
9. }
10. function second() {
11. var n4 = 4;
12. //alert(n3);//mark3
13. }
14. first();
15. alert(n1);//mark5


第一步:当执行流进入全局执行环境(对web浏览器来说,我们一般认为是window对象)的时候,会创建该环境的一个变量对象,在这个变量对象中,保存n1及2个函数声明。这时window执行环境会被压入环境栈,并根据该window执行环境的变量对象初始化作用域链。

环境栈:
window执行环境
......









作用域链:
window变量对象





那么在代码mark1处n1的值为1。刁钻的读者可能会问,为什么不是在first函数中定义的n1=2呢,first函数不是也有一个相应的变量对象吗?要知道,虽然在window执行环境中存在2个函数的定义,并且他们也确实保存在window执行环境的变量对象下,但此时执行流并未进入 first函数的执行环境,所以与first函数执行环境对应的变量对象此时还并未创建。



第二步:执行流在运行至倒数第二行时进入first函数执行环境,并将此环境执行压栈操作,因为并未退出window对象,所以此时环境栈自顶向下依次为first环境、window环境。
first执行环境
window执行环境
......













对作用域链插入新的变量对象时,是从前端操作的。此时它演变成:
first变量对象

window变量对象





在mark2处对同名标识符n1的解析,将从first变量对象(作用域链的前端)中开始查找,此时找到n1的值为2并返回,也就不会再在作用域链的下一个(window变量中)查找了。



第三步: 此时,在first函数内部对second函数进行调用,执行流离开first执行环境并进入second执行环境,程序对first环境执行出栈操作,然后对second环境执行压栈操作,因为没遇到first函数结束符“}”,first环境虽然出栈,但所占用资源仍然不会释放。
second执行环境
window执行环境
......













而此时,作用域链则产生如下变化,首先first环境的出栈,将会导致从前端移除first变量对象,然后将当前活动的执行环境(second环境)的变量对象添加在作用域前端。
second变量对象 window变量对象





那么在mark3处如试图对在first环境中定义的变量n3求值,则会导致错误(不是undefined),因为此时n3在second变量对象和window变量对象中,都找不到定义。

读者此时应该注意到一个规律,环境栈中的顺序与作用域链中的顺序是一致的。除此之外,我们说,在环境栈中,下方的环境相对于上方相邻的这个环境来说为包含环境,换言之,在一个环境中能访问到的变量,除开自身环境的变量定义外(优先查找,因为在作用域链前端),还有来自包含环境及其级联包含环境的。



第四步:执行流退出second环境,返回first环境,相应的second环境出栈,first环境压栈,由于执行流遇到second函数结束符“}”,second环境在出栈的同时,所占用的资源也得以释放。
first执行环境
window执行环境
......













相应的作用域链移除second变量对象,然后在前端插入first变量对象,变为
first变量对象 window变量对象





在mark4处试图访问second环境中定义的n4变量也是会导致错误的(过程不难分析,略)



第五步:执行流离开first环境, 回到window环境,first环境出栈,同时释放其所占资源,作用域链移除前端first变量对象,只留下window变量对象。mark5处n1值为1。(图、分析略)



再来看另一个demo


Js代码 收藏代码

1. var n1 = 1;
2. function outter() {
3. var n1 = 2;
4. alert(n1);//mark1
5. function inner() {
6. var n3 = 3;
7. alert(n1);//mark2
8. }
9. inner();//mark0
10. //alert(n3);//mark3
11. }
12. outter();
13. alert(n1);//mark4

var n1 = 1;
function outter() {
var n1 = 2;
alert(n1);//mark1
function inner() {
var n3 = 3;
alert(n1);//mark2
}
inner();//mark0
//alert(n3);//mark3
}
outter();
alert(n1);//mark4





第一步:执行流运行至mark1时,n1值为2。分析同上。环境栈及作用域链状况如下
outter执行环境
window执行环境
......












outter变量对象 window变量对象





第二步:在mark0处对inner函数的调用,使执行流进入inner执行环境,但此时,并不会导致outter执行环境的出栈,仅仅是在环境栈原有2个执行环境的基础上继续对inner环境执行压栈,因为当前活动的执行环境(inner),其函数是声明在outter环境中的,即这个inner 函数本身就是作为outter环境的变量对象中properties当中的一员。这一点与上例有着本质上的区别(上例,2个函数之间的关系为并列关系,都是window变量对象的一员)。环境栈及作用域链状况如下
inner执行环境
outter执行环境
window执行环境
......














inner变量对象 outter变量对象 window变量对象





在mark2处,n1值为2,在outter变量对象中找到(分析略)



第三步:在mark3处,尝试访问n3会报错(图、分析略)



第四步:在mark4处,n1值为1(图、分析略)



最后补充一个带with的demo:


Js代码 收藏代码

1. var n1 = 1;
2. function outter() {
3. var n1 = 2;
4. function inner() {
5. var n1 = 3;
6. }
7. inner();
8. with (n1) {
9. //var n1;//mark3
10. //var n1 = 4;//mark2
11. var n2 = n1 + 4;
12. }
13. alert(n2);//mark1
14. }
15. outter();

var n1 = 1;
function outter() {
var n1 = 2;
function inner() {
var n1 = 3;
}
inner();
with (n1) {
//var n1;//mark3
//var n1 = 4;//mark2
var n2 = n1 + 4;
}
alert(n2);//mark1
}
outter();

看读者能否自行分析出,mark1处n2值为6的结果,提示:

1、with/try-catch语句,都会在作用域链的前端添加一个变量对象(with后所跟参数的所有属性、方法所作的变量声明)

2、执行流进入with后所跟的“{}”块,并不会导致进入一个新的执行环境,因为没有块级作用域一说,所以在这个块中声明的变量会被添加在所在执行环境的变量对象中。



读者可尝试把mark2和mark3处代码交替取消注释运行查看结果。


注意一点,在javascript中没有块级别的作用域,也就是说在java或c/c++中我们可以用"{}"来包围一个块,从而在其中定义块内的局部变量,在"{}"块外部,这些变量不再起作用,同时,也可以在for循环等控制语句中定义局部的变量,但在javascript中没有此项特性:

# function f(props) {
# for(var i=0; i<10; i++) {}
# alert(i); //10 虽然i定义在for循环的控制语句中,但在函数
# //的其他位置仍旧可以访问该变量.
# if(props == "local") {
# var sco = "local";
# alert(sco);
# }
# alert(sco); //同样,函数仍可引用if语句内定义的变量
# }
# f("local");


# var sco = "global";
# function print1() {
# alert(sco); //global
# }
# function print2() {
# var sco = "local";
# alert(sco); //local
# }
# function print3() {
# alert(sco); //undefined
# var sco = "local";
# alert(sco); local
# }
#
# print1(); //global
# print2(); //local
# print3(); //undefined local


前面两个函数都很容易理解,关键是第三个:第一个alert语句并没有把全局变量"global"显示出来,而是undefined,这是因为在 print3函数中,我们定义了sco局部变量(不管位置在何处),那么全局的sco属性在函数内部将不起作用,所以第一个alert中sco其实是局部 sco变量,相当于:
Js代码 收藏代码

1. function print3() {
2. var sco;
3. alert(sco);
4. sco = "local";
5. alert(sco);
6. }

function print3() {
var sco;
alert(sco);
sco = "local";
alert(sco);
}

从这个例子我们得出,在函数内部定义局部变量时,最好是在开头就把所需的变量定义好,以免出错


函数的作用域在定义函数的时候已经确定了,例如:
Js代码 收藏代码

1. var scope = "global" //定义全局变量
2. function print() {
3. alert(scope);
4. }
5. function change() {
6. var scope = "local"; //定义局部变量
7. print(); //虽然是在change函数的作用域内调用print函数,
8. //但是print函数执行时仍旧按照它定义时的作用域起作用
9. }
10. change(); //golbal


变量名可以是任意长度。变量名必须符合下列规则:

* 变量名的第一个字符必须是英文字母,或者是下划线符号(underscore)_
* 变量名的第一个字母不能是数字。其后的字符,可以是英文字母,数字,和下划线符号符号(underscore)_
* 变量名不能是Javascript的保留字

Javascript代码是区分大小写的(case-sensitive)。变量myname和MyName表示的是两个不同的变量。写错变量的大小写,是初学者最常见的错误之一


什么是变量?

变量是用来临时存储数值的容器。在程序中,变量存储的数值是可以变化的。
变量的声明(Declaring Variables)

在使用一个变量之前,首先要声明这个变量。Javascript里,使用var来声明变量。

声明变量有以下几种方法:

1. 一次声明一个变量。例句如下:

var a;

2. 同时声明多个变量,变量之间用逗号相隔。例句如下:

var a, b, c;

3.声明一个变量时,同时赋予变量初始值。例句如下:

var a=2;

4. 同时声明多个变量,并且赋予这些变量初始值,变量之间用逗号相隔。例句如下:

var a=2, b=5;


同名则局部优先于全局的意思是在一个作用域中(如函数中),如果存在与全局变量相同的变量名,则当前作用域中(函数)中忽略全局变量名而值会使用局部变量的值.不是去替代全局变量.(你理解的替代是去改变吗?这是错误的理解.)


变量创建后默认被初始化为undefined


1. var n0;//初始化为undefined
2. var n1 = 1;//全局变量
3. n2 = 2;//全局变量
4. //window.n2=2;//与上一行等价,但IE部分版本不支持
5. //n2;//错误语法,抛出ReferenceError,必须提供初始化器,或提供var关键字
6. function demo() {
7. var n3 = 3;//局部变量
8. n4 = 4;//全局变量,不推荐这样定义
9. }
10. alert(n0);//undefined
11. alert(n1);//1
12. alert(n2);//2
13. demo();//别漏掉这行,否则n4不会创建
14. //alert(n3);//抛出ReferenceError
15. alert(n4);//4
16. alert(window.n4);//虽IE不支持,但每个版本都可以通过它来取值


var n1 = 1;
delete n1;//无效
//delete window.n1;//IE不支持,证明其{DontDelete}特点,chrome支持,但都无效:(
alert(n1);//1

//顺便熟悉一下delete语法
var r1 = new Object();//reference
r1.name = "jack";
alert(r1.name);//jack
delete r1.name;
alert(r1.name);//undefined

var p1 = "hello";//primitive
p1.name = "mike";//不可在原始类型上动态添加属性,不会导致错误但无效
alert(p1.name);//undefined


为了或得更好的页面性能,可尝试尽早的释放全局变量,尤其是引用类型的全局变量。(置为null,解除引用),否则只能依赖垃圾回收或应用的关闭了。至于局部变量,因为在执行流退出其执行环境的时候,环境本身会释放,所以不用操心。


一些猜想:即使能delete也只能导致其值为undefined,而垃圾回收应只针对空指针引用(null)而非undefined,大概这是ECMA-262从第三版引入Undefined类型的原因之一,正式区分空指针和未初始化变量,便于更有效的管理内存。


变量包含2种:原始类型(Undefined,Boolean,Null,Number,String) 和引用类型

原始类型保存于栈内存,占有固定大小空间,具有索引速度快的特点

引用类型为保存于堆内存中的对象,大小不固定,索引相对较慢,变量实际上保存的是指向某对象的一个指针


原始类型在对变量进行复制时,会在栈内存中创建一个新值交由新变量持有,而引用类型,则是在栈内存中复制指针地址值,交由新变量持有,但此时,堆中对象仍然保持一个不变,即2指针指向同一对象。


# var n1 = 1;
# var n2 = n1;
# n1 = 2;
# alert(n2);//2,不受n1影响
#
# var r1 = new Object();
# r1.name = "jack";
# var r2 = r1;
# alert(r2.name);//jack
# r1.name = "mike";
# alert(r2.name);//mike,受r1影响
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值