[从jQuery看JavaScript]-变量与作用域链

时间:2022-07-22 14:52:14

jQuery片段:

  1. var
  2. // Will speed up references to window, and allows munging its name.
  3. window = this,
  4. // Will speed up references to undefined, and allows munging its name.
  5. undefined,
  6. // Map over jQuery in case of overwrite
  7. _jQuery = window.jQuery,
  8. // Map over the $ in case of overwrite
  9. _$ = window.$,
  10. jQuery = window.jQuery = window.$ = function( selector, context ) {
  11. // The jQuery object is actually just the init constructor 'enhanced'
  12. return new jQuery.fn.init( selector, context );
  13. },
  14. // A simple way to check for HTML strings or ID strings
  15. // (both of which we optimize for)
  16. quickExpr = /^[^<]*(<(.|/s)+>)[^>]*$|^#([/w-]+)$/,
  17. // Is it a simple selector
  18. isSimple = /^.[^:#/[/.,]*$/;

  从这一节开始,我们剥掉jQuery的外衣,看看里面藏着些什么。前一节中曾经提及,如果单看外层的匿名函数,不看里面的实现的话,这个实现肯定不是闭包。但是,如果把jQuery的实现加上的话,这个肯定就是一种闭包应用。万丈高楼平地起,要了解闭包应用,就首先要了解它的基础。而这一节,我们遇到的片段,就是这个基础的所在——变量。(虽然这个片段包含众多知识点,但请容许我一个个慢慢分说。)

  • 声明变量

  变量的英文名为variable,其前三个字母正是我们在JS声明变量的关键字——var。那么,我们先来看一下如何去声明一个变量:

  1. /*
  2. * 声明变量的格式为
  3. * var 变量名 初始化变量表达式列表(可选)
  4. */
  5. var a=1, b, c="test", d=a+b;// 虽然b还没有初始化,但是声明是合法的
  6. alert(a);// "1"
  7. alert(b);// "undefined"
  8. alert(c);// "test"
  9. alert(d);// "NaN"
  10. alert(e);// 这里将引发编译错误:"e"未定义
  11. // 同样地,如果在初始化中使用未定义的变量,也会引发编译错误。

  如上例所示,声明变量需要使用var关键字,然后在空格后紧跟变量的名字。在声明变量的同时,我们也可以选择帮变量初始化。初始化的值可以是任何类型的值或表达式,但是,如果你尝试使用未定义的变量名来初始化,JS的编译器将会判定发生编译错误,并阻止程序继续往下运行。无论你是否对声明的变量进行初始化,你都可以继续声明第二个变量而无须使用var关键字。你所需要的只是运算符“,”。(关于运算符将在稍后章节详细讨论。)但当你没有对声明的变量进行初始化时,变量将会被赋予值“undefined”——undefined也是JS的固有类型之一。PS:JS中使用的运算符必须是半角的英文字符,甚至空格也一样。

  • 重复声明的变量?!

  当我们声明了一个变量,而又在后续的代码中再次对他进行声明,结果会怎么样呢?或许在很多其他语言中,这都会引起重复定义的错误,但在JS中,这完全是合法的。并且,由于JS是弱数据类型,所以变量能被赋予任何类型的值。请看以下例子:

  1. var a=1;
  2. alert(typeof a); // "number"
  3. var a;
  4. alert(typeof a); // "number"
  5. var a="1";
  6. alert(typeof a); // "string"
  7. a=new String("1");
  8. alert(typeof a); // "object"

  看完上面的例子,你可能会产生两个疑问:

  a)为什么第二个a还是number?

  b)为什么第四个a是object?

  为了解答第一个问题,我们首先要了解声明一个变量到底是怎么运作的。而第二个问题,我们将他放到下一节再讨论。

  • var 变量声明的工作步骤

  当我们使用var关键字去声明变量的时候,JS解释器将会进行如下操作:

  1)预编译javascript代码块中所有非函数块内的var关键字;

  2)生成变量名标识并在其所在作用域分配空间;

  3)按代码顺序运行至第一个var关键字所在行;

  4)按变量声明列表表达式次序计算初始化表达式的值;

  5)每计算完一条初始化表达式,就将其计算结果赋予给对应的声明变量;

  6)继续运行后续代码至下一var关键字;

  7)重复4-7步到代码块结束;

  8)继续编译运行下一个代码块。

  PS:JS将以一个代码块,也就是一个script标签为单位去运行一段JS代码。

  正是因为var的工作方式,实际上程序执行时,解释器是根本看不到var关键字的。他执行的只是初始化表达式的赋值语句而已——所以问题a的答案就是例子中的第三句实际上什么事也没有做。所以,你一点也不用为代码中会否出现重复定义的变量名而烦恼。你真正需要担心的是,初始化语句所产生的变量的值的变化是否如你预期。除此之外,请不要尝试使用保留字作为变量名。这几乎在所有语言中都必须遵循的规范。

  另外,在函数块中声明变量的工作步骤也是类似的,但不同的是,他们是在函数运行时才创建的。

  • 没有var的变量声明?!

  很多朋友都应该有这个经验——“我们根本不需要使用var来声明变量也能直接赋值啊!”。这是因为JS解释器在遇到赋值表达式的时候,会先在作用域链中寻找这个变量是否已经声明。如果这个变量没有声明,则隐式强制为其在全局(Global)作用域中声明,并将表达式的值赋予给该变量。

  但究竟为什么会这样呢?其实一切都源自于变量的获取规则和作用域链的化合作用外加赋值运算符的催化作用。

  • 作用域链

  每个运行时的上下文都有与其对应的一个作用域。而作用域链正是把这些作用域连接起来的桥梁。它的作用与程序寻找某一变量标识有关:

  1)JS解释器会按调用的顺序把作用域加进作用域链(像栈般早进入的作用域会在作用域链的底部);

  2)然后在程序寻找某一变量标识时进入作用域链中的第一个索引,并在其中寻找该变量标识;

  3)如果没有找到该标识,则前往下一个索引继续寻找;

  4)如果已经找到该标识,则将该标识及其值返回;

  5)当搜索到最后一个索引仍未能找到该标识,则在最后的索引上创建该标识,并使其值为null,最后返回该标识与值。

  PS:而上述的第5步发生的前提是该标识处于赋值运算符表达式左侧。

  因此,当你没有使用var声明变量而直接使用对该变量作初始化操作(简单赋值)时,JS会自动为你创建该空值标识,并让它可以顺利执行赋值语句。

  • 变量与作用域链

  从上面的描述,我们可以很轻易的看到变量与作用域链的关系。因为只要程序需要寻找变量,就必须通过作用域链。而前面所谈及的闭包问题正是由此而来的。回想一下我们前面的示例代码:

  1. var abc=function(y){
  2. var x=y;// 这个是局部变量
  3. return function(){
  4. alert(x++);// 就是这里调用了闭包特性中的一级函数局部变量的x,并对它进行操作
  5. alert(y--);// 引用的参数变量也是*变量
  6. }}(5);// 初始化
  7. abc();// "5" "5"
  8. abc();// "6" "4"
  9. abc();// "7" "3"
  10. alert(x);// 报错!“x”未定义!

  再联系一下我们上面说的,自己回答一下这个问题:究竟这个闭包中的变量声明和初始化是如何执行的?我们将在“闭包”的那一节再解答这个问题。

  最后说明一下,标题中的英文是为了大家能更好的查找英文文献用的——因为我的整理只会是中文版。^0^y

  • 关于保留关键字(补遗)

  在ECMA-262规范中,规定了如下关键字作为保留关键字:

break else new var
case finally return void
catch for switch while
continue function this with
default if throw
delete in try
do instanceof typeof

  这些关键字不能并声明为变量名。但其实还有如下预留关键字是作为未来扩展使用的:

abstract enum int short
boolean export interface static
byte extends long super
char final native synchronized
class float package throws
const goto private transient
debugger implements protected volatile
double import public

  但实测中,以上预留关键字,仅有部分浏览器对部分关键字有作保留。例如IE中的class。(实测浏览器为IE6,FF3,Chrome,Opera)。但为了以后的兼容性,以上预留关键字最好还是别用来作为变量标识为好。

  • 关于变量标识(补遗)

  变量的标识必须要符合以下要求:

  1)不能是保留关键字;

  2)必须以英文字母或下划线(_)或美元符号($)开头;

  3)后续字母除了可以为英文字母或下划线和美元符号外,还能是数字。

  此外,JS是区分大小写的,变量a和变量A是两个不同的变量标识。