JavaScript欲速则不达—关于变量以及“预解析”对变量的影响

变量类型

JavaScript中变量可以用来保存任何类型的数据。而且变量的类型是根据所赋值的类型来决定自己的类型,前面我们说过javascript有两种类型的:基本类型和引用类型。所以一个变量到底是什么类型,不取决于变量定义本身,取决于它所包含的数据。所以变量的类型由保存数据类型决定。(点击查看基本数据类型介绍)。

  • 基本类型变量的值是保存在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。
  • 引用类型变量的值是保存在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),直接保存在栈中,指向保存在堆中的值。

由于引用型变量的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。栈空间中保存的值是该对象在堆中的指针,地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。

变量赋值

向另一个变量赋值其实就是把一个值复制给另外一个变量。

基础类型变量的复制过程是这样的:


JavaScript欲速则不达—关于变量以及“预解析”对变量的影响

基础类型变量的复制过程

当一个变量向另一个变量复制基本类型的值时,也会给其分配新的栈内存,所以这两个变量时完全独立的,互不影响。

当一个变量向另一个变量引用基本类型的值时,在栈中复制的是一个指针,指向存储在堆中的一个对象,所以跟复制基本类型变量不同,实例代码:

var obj1 = new Object;
var obj2 = obj1;
obj1.name = "xiaobei";
alert(obj2.name); //"alertxiaobei"

obj1保存了一个对象的指针,然后把这个值赋给了obj2,等于这两个变量指向了同一个对象,改变其中一个变量,就会影响另一个变量。引用类型变量的复制过程如下图所示:


JavaScript欲速则不达—关于变量以及“预解析”对变量的影响

引用类型变量的复制过程

传递参数

javascript中所有函数的参数的传递,其实就是把变量复制给函数的参数。

参数的传递过程跟基本类型数据复制一样,不管你的参数是基本类型还是引用类型,传进来的值都像基本类型变量复制一样,不会影响传进来的变量值。也就是说外部的变量不会因为参数的变化而变化,还是互不影响的。如下代码(摘自javascript高级程序设计)

function setName(obj){
    obj.name = "xiaobei";
    obj = new Object();
    obj.name = "admin";
}
var person = new Object();
setName(person);
alert(person.name); // 结果依旧是 xiaobei

也可以javascript函数的参数想象成局部变量,外部变量不受影响。

JavaScript中 变量不仅仅是使用 var 操作符 后跟变量名,那个变量名就是变量了,其实JavaScript中函数名本身就是变量,所以函数也可以当做值来使用。所以变量也可以以参数的形式传进另一个函数,也可以将一个函数作为另一个函数的结果返回。从一个函数中返回另一个函数,是常用的技术,如下:

function callSomeFunction(someFunction,someArgument){
    return someFunction(someArgument)
}
function add10(num){
    return num +10;
}
var result1 = callSomeFunction (add10,10);
alert(result1);//20

所以无论callSomeFunction传进的是什么函数,它都会返回第一个参数后的结果。我们知道函数名后面没有圆括号,就只是访问它的指针而非运行此函数。

了解JavaScript“预解析”对变量的影响

什么是“预解析”?

“预解析”我们在下节介绍JavaScript解析机制详细介绍,现在我们知道在JavaScript执行之前,会进行“预解析”,会记录代码中全部的变量声明和函数定义,构造函数的调用对象(Call Oject、Activation Object、Activate Object、活动对象,不同称呼罢了)和在全局环境下的作用域链。通俗讲,就是在代码定义而不是在执行时,这些东西就已经存在。

alert("a" in window);   //true -1
    if( false ){
        var a = 1;
    }
alert("b" in window);   //false -2
    if( false ){
        b = 1;
}
alert("b" in window);    //false -3
alert("c" in window);   //false -4
if( true ){
    c = 1;
}
alert("c" in window);  //true -5

第一个例子,Javascript在”预解析“的时候已经将变量a的声明添加到了window对象中,于是在运行时 “a” in window 为true;而2 – 5句正好验证了没有使用var定义的变量需要在运行时进行处理。

应避免在变量被初始化之前使用变量

JavaScript引擎会优先解析var变量和function定义,但是我们还是应该在变量被初始化之后使用变量,为什么?

因为,在”预解析“时那些用var声明的变量的定义是变量,赋值undefined;用function声明的函数定义是函数,赋的值就是定义的函数。如下代码:

function text(){
    alert(arg);
} ;
text(); //undefined
var arg = 22;

text();是在解释到 var arg = 22; 这句之前就执行了,此时尚未给arg赋值,弹出的是在“预解析”阶段创建变量对象时赋给的undefined值。

函数重载

我们前面讲过将函数名想象成指针,有助于理解没有函数重载的概念。如下:

var a = function addNum(num) {
    return num+100
}
var a = function addNum(num) {
    return num+200
}
alert(a(100)) //300

不管是用函数声明语法和函数赋值表达式,通过观察,很容易发现在创建第二个函数时,覆盖的是指向第一个函数的指针变量a。

函数声明语法和函数赋值表达式的区别

这里既然说到了变量声明,那顺便来比较下函数声明语法和函数赋值表达式的区别,我们知道赋值就是把一个变量复制给另外一个变量,函数赋值表达式,就是把一个函数声明赋值给一个变量,变量只是指针,所以可以用这个变量来调用函数。他们之间的区别,如下代码:

alert(a);   //函数声明a的函数代码 -1
alert(a()); //xx -2
function a() {
    return 'xx';
}
var a = function () {
    return  'x';
}
alert(a);//执行之后就把预加载函数属性覆盖了,弹出函数表达式a的 函数代码。 -3
alert(a()); //x -4

在预解析阶段就已定义了一个a的函数和一个a的同名变量,但是函数的优先级比变量的高,所以当执行第一二行代码时,你会看到是函数声明的代码和结果。然后一直执行下去之后,函数赋值表达式a覆盖把同名的函数a给覆盖了。

虽然以上函数运行都能达到预期的结果,但是函数赋值表达式不能在之前调用函数,如下:

alert(a);// undefined
alert(a()); //Uncaught TypeError: Property 'a' of object [object DOMWindow] is not a function
var a = function () {
    return 'x';
}

只有使用函数声明,JavaScript解释器才会率先读取函数声明(“预解析”并创建函数对象),才可以在执行任何代码之前可用(可以访问);而函数赋值表达式则必须等到解析器执行到它所在的代码行,才会真正的被解释执行。

为了更好理解JavaScript中function比var优先级大和给变量赋值是在JavaScript执行时,我们再来看下以下代码:

function a() {
    return  3;
}
var a;
    alert(a);//函数代码 一号
    function b() {
        return  2;
    }
var b =1;
alert(b);//1 二号

分析1号代码:完成了函数a的定义,即在window对象上添加变量a,并赋值为函数定义。之后遇到”var a;”,由于当前作用域已经有变量a,因此不需要做什么工作。在执行时,由于并没有给变量a赋值,因此原来的函数定义并没有覆盖,因此最终变量a是一个函数。二号代码跟一号的区别就是:在执行时,给变量b赋值了,就是说在执行阶段将1赋值给了 变量名。

隐式变量和使用 var 定义的变量的小差异

实际上隐式全局变量并不是真正的全局变量,真正的变量应该是在“预解析”阶段就已经明确是一个变量的属性(简单说就是JavaScript还没运行就告诉它,这孩子的职责是 当一个变量),而那些没有使用var定义的变量则需要在运行时去处理,所以它们只是全局对象的属性,如下代码:

if (!a in window)) {
    var a = 1;
}
alert(a); //undefined

“预解析”时已经将变量a添加到window上,因此!(”a” in window)为false,导致运行时a没有被赋值,所以a的值为undefined。

注意:在技术上,隐式全局变量并不是真正的全局变量,

总结

网上很多都是从变量作用域方向来阐述 变量(variable ),很多书上也是,而从执行层面来阐述呢。因为我觉得变量对象一个在解析和进入上下文 的阶段就已经存在的东西,所以我才把他们放在一块讲。其实作用域链也是在这个阶段产生的,了解了“预解析”阶段的 变量对象对理解作用域链很重要,而我打算把域链放在跟闭包一起讲,也是有一定原因的,描述行为是理解这些理论知识的最快途径。

最后总结:

  1. 避免全局变量同名冲突,尽可能少的使用全局变量是很重要的。
  2. 不要使用隐式全局变量,我们前面说过他们之间还是有差异的。
  3. 可以使用一个var语句声明多个变量,并以逗号分隔。这样子可以防止逻辑错误(所有未初始化但声明的变量的初始值是undefined)和增加代码的可读性。
  4. 由于JavaScript是弱类型语言,变量命名最好以类型前缀+有意义的单词组成,这样我们就很明确的知道当前变量是什么属性。
    • s:表示字符串。例如:sName,sHtml;
    • n:表示数字。例如:nPage,nTotal;
    • b:表示逻辑。例如:bChecked,bHasLogin;
    • a:表示数组。例如:aList,aGroup;
    • r:表示正则表达式。例如:rDomain,rEmail;
    • f:表示函数。例如:fGetHtml,fInit;
    • o:表示以上未涉及到的其他对象,例如:oButton,oDate;

本文源链接:http://www.html5jscss.com/js-var.html

转载请注明《JavaScript欲速则不达—关于变量以及“预解析”对变量的影响》| html5jscss

我来说几句 留言 . . . . . . . 4 篇回应 (访客:2 篇, 博主:2 篇)

  1. if (!("a" in window)) { xxxx }这个举例的代码a应该没有双引号吧?
    文章写的很好,感觉对这个js这个让人讨厌的语法有了了解,建议写js示例代码的时候用console.log,这样感觉读者边看边测试的时候会爽点

  2. 忘记说了,搜索功能没有用了,郁闷指向了 localhost
    点击查看基本数据类型介绍 改该链接失效。

评论关闭了.