一. 全局变量和局部变量分析
  1. 在函数外部由var定义的变量称为全局变量,变量的作用范围是整个程序,只有当程序运行完后(在浏览器中一般指关掉这个页面)才会释放其内存空间。
    不用var声明的变量称为隐式全局变量,注意在函数内部不用var定义的变量也是隐式全局变量。
    注意用var声明的变量(无论是全局变量还是局部变量)是不能用delete删除的,因为var定义的变量是不可配置的。但是隐式全局变量是可以利用delete删除的,因为它是可配置的。
function f(){
var x = 1;//在函数内部用var定义,它是一个局部变量
}
console.log(x);//会报错,x没有被定义
function f(){
x = 1;//在函数内部定义,但它是一个隐式全局变量
}
console.log(x);//输出1
  1. 在函数内部定义的变量称为局部变量, 变量的作用范围只在定义的这个函数内部,像上面的第一个程序,在函数内部定义了x,但是函数外部在程序台输出x时会报错,提示x没有被定义。delete运算符同样也不能删除var 定义的局部变量,因为var定义的变量是不可配置的。
  2. 全局变量和局部变量是允许同名的,但是在函数内部局部变量会覆盖全局变量,如下例所示:
var x = 100;//定义全局变量x并初始化为100
function f(){
	var x = 10;//定义局部变量x并初始化为10
	console.log(x);//输出10,全局变量被局部变量覆盖
}
console.log(x);//输出100
二. 从两个角度去理解变量作用域
  • 变量的作用域:作用指的是变量能够起作用,域:指的是区域,范围。连起来作用域指的就是变量能够起作用的范围。
  1. 第一种是直观地通过变量定义所在的位置去理解变量作用域,变量定义在函数外部那么变量的作用域就是整个程序,变量定义在函数内部,那么变量作用域就是这个函数的内部。
  2. 我们知道<mark>当var 声明一个全局变量时,实际上是定义了全局变量的一个属性</mark>。js全局变量是全局对象的属性,这是在ES规范中强制规定的。对于局部变量则没有此规定,但是我们可以推断出,<mark>局部变量是跟函数调用相关的某个对象的属性</mark>,比如定义一个函数f它有一个局部变量x,这个局部变量x就和函数(对象,js中函数也是对象)f1有关,它是对象f1的一个属性。
var x = 10; //定义一个全局变量并初始化为10
function f(){ //定义一个函数实际上是开辟一段内存空间,然后f指向这段内存空间,实际上f是这段
 //内存空间的引用(按c语言来理解f中存放的是这段内存空间的首地址,但是不是一回事,因为c中要想访
 //这段内存空间的内容需要*f,而js中直接通过f就可以访问),通过f我们就直接可以访问这段内存空间里
 //内容
    var x = 0;//定义一个局部变量
    console.log(x);
}
var f1 = f;//将函数f的引用赋给f1,此时f1和f同时引用这个函数。
f1();//调用上面那个函数
console.log(f);//访问这个函数的内容
console.log(f1);//访问这个函数的内容
console.log(typeof(f));//function类型
console.log(typeof(f1));//function类型

<mark>如果将一个局部变量看成是某个对象的属性的话,我们就可以换一个角度来理解变量作用域</mark>每一段js代码都有一个与之相关联的作用域链,作用域链就是一个对象列表或链表。这组对象定义了这段代码作用域中的变量。以上面这段程序为例,下面是作用域链:

js引擎解析标识符的过程:
当js在函数内部需要查找变量x的时候会先从对象链表中的第一个对象开始查找,如果这个对象有一个名为x的属性,那么则会直接使用这个属性的值,如果没有这个属性,那么js会接着查找对象链表中的下一个对象。以此类推,如果作用域链上没有任何一个对象含有属性x,那么js认为这段代码的作用域链上不存在x,并最终抛出一个引用错误异常。
在此程序中,要查找变量x,在作用域链表上第一个对象中就能够查找到,并将其返回。

下面介绍一下不同位置代码的作用域链:

  • <mark>在js的最顶层代码中,作用域链是由一个全局对象组成。</mark>
    还是上面那个程序,红色的那些代码就是顶层代码,因为它们不在某个函数的内部,相应的作用域链见下图:
    //红色的代码是最顶层代码
    var x = 10; //定义一个全局变量并初始化为10
    function f(){
    var x = 0;//定义一个局部变量
    console.log(x);
    }

    var f1 = f;//将函数f的引用赋给f1,此时f1和f同时引用这个函数。
    f1();//调用上面那个函数
    console.log(f);//访问这个函数的内容
    console.log(f1);//访问这个函数的内容
    console.log(typeof(f));//function类型
    console.log(typeof(f1));//function类型

    最顶层代码的作用域链(它只有一个全局对象):

  • <mark>在不包含嵌套的函数体内的代码,作用域上有两个对象,第一个是这个函数对象,另一个就是全局对象。</mark>
    //绿色的代码是函数体内的代码
    var x = 10; //定义一个全局变量并初始化为10
    function f(){
    var x = 0;//定义一个局部变量
    console.log(x);
    }

    var f1 = f;//将函数f的引用赋给f1,此时f1和f同时引用这个函数。
    f1();//调用上面那个函数
    console.log(f);//访问这个函数的内容
    console.log(f1);//访问这个函数的内容
    console.log(typeof(f));//function类型
    console.log(typeof(f1));//function类型

    不包含嵌套的函数体内的代码的作用域链为:

  • <mark>在包含嵌套的函数体内的代码中,作用域链上至少有三个对象。</mark>

  • 这里以嵌套一次为例,f函数中嵌套着ff函数,如下面的程序:
    //紫色的代码是ff函数体内的代码
    var x = 10; //定义一个全局变量并初始化为10
    function f(){
    var x = 0;//定义一个局部变量
    console.log(x);

    function ff(){
    var y = 20;
    console.log(y);
    }

    ff();
    }

    var f1 = f;//将函数f的引用赋给f1,此时f1和f同时引用这个函数。
    f1();//调用上面那个函数
    console.log(f);//访问这个函数的内容
    console.log(f1);//访问这个函数的内容
    console.log(typeof(f));//function类型
    console.log(typeof(f1));//function类型

    ff函数体内代码的作用域链为:

  • 一些小启发

  • 通过上面的介绍,大家可能就会理解为什么最顶层的代码不能访问局部变量了,因为在最顶层的代码作用域链中根本只有一个全局对象,js引擎在查找变量时就在这个全局对象中去查找相应的属性。

  • 还有我们在学js的变量的章节时,会告诉我们尽量使用局部变量,为什么呢?在这里也能答案,在一个函数内部访问某个变量,这个变量在作用域链的位置越深,js引擎需要查询的次数就越多。当这个变量就在这个函数内部定义时,js引擎在查找作用域链的时候,第一次查询就能够查找到了,速度肯定要快一些。

  • 函数创建时,产生内部属性[[Scope]]包含函数被创建的作用域中对象的集合(作用域链)
    作用域链上每个对象称为可变对象(Variable Obejct),
    每一个可变对象都以键值对形式存在(VO要细分的话,全局对象GO和活动对象AO)

  • 函数执行时,创建内部对象叫做执行环境/执行上下文(execution context)
    它定义了一个函数执行时的环境,函数每次执行时的执行环境独一无二
    函数执行结束便会销毁

  • js引擎就通过函数执行上下文的作用域链规则来进行解析标识符(用于读写),从作用域链顶端依次向下查找

  • 尽量使用局部变量,降低作用域查找性能开销

三. 关于浮点数值计算产生舍入误差的问题分析
  • 在js中0.1+0.2并不等于0.3,而是等于0.30000000000000004,真的不可思议,计算机算出来的结果还会有错呀。这其实是由于js中的number类型是使用IEEE754格式来表示整数和浮点数值。
  • IEEE754是一个二进制表示法,可以精确地表示分数,比如1/2,1/4,1/8和1/1024.但遗憾的是,我们常用的分数(特别是在金融计算方面)都是十进制分数,比如1/10,1/100,等。二进制表示法并不能精确地表示类似0.1这样简单的数字。
  • js的数字具有足够的精度,并且可以极其近似于0.1,但是在编程时会遇到一些问题,比如你想判断两个数的和是不是0.3,这就出问题了,如下面的代码,if条件就不能满足,所以我们在写代码时要避免这种比较两个数值是否相等的情况:
var x = 0.1;
var y = 0.2;
if(x+y==0.3){
	console.log(x+"+"+y+"=0.3");//不会输出,因为if条件不能满足
}
  • 其中的一个解决方法就是尽量避免使用浮点数,比如在金融行业中,分作为最小单位时,使用整数表示分,而不是使用小数,这样就可以避开小数了。也就是说当你知道你要计算的某个数的的小数位数时可以先乘上10的多少次方将其转换为整数然后最终的结果再除以这个乘上的数即可。
四. 理解js预解析
  • 先上一段代码,看看能不能写出最终的执行结果.
    console.log(a);
    var a = 1;
    console.log(a);
    function a(){
        console.log(2);
    }
    var a = 3;
    console.log(a);
    function a(){
        console.log(4);
    }
    console.log(a);
    a();


解释一下:

  1. 首先预解析阶段遇到 var a = 1;声明变量a
  2. 然后遇到第一个函数a声明,这时由于和变量a重名,故将变量a替代,此时a表示一个函数
  3. 然后遇到var a = 3;变量a干不过函数a,a代表的还是一个函数
  4. 最后又遇到第二个函数a声明,后面函数a声明替换前面的,a还是代表一个函数但是函数体内容发生了变化
  • <mark>注意: 预解析阶段的变量可以理解为都被赋值为undefined,函数就是函数体的内容,当遇到变量名a和函数名a一致时,预解析完后a是函数a,里面存放的是函数体的内容,函数a和函数a同时出现时,后面声明的函数还会覆盖前面声明的函数。</mark>

故:

    console.log(a); //输出函数a,函数体内容是第二个函数声明
    var a = 1; //经过赋值之后,函数a变为了变量a,并且值为1
    console.log(a);//1
    function a(){
        console.log(2);
    }
    var a = 3;//变量a变为3
    console.log(a);//3
    function a(){
        console.log(4);
    }
    console.log(a);//3
    a();//a现在是一个变量,当然会报错
  • 再举一个小例子:
var a =1;
function fn(a){
	console.log(a)
	a = 2;
}
fn():
console.log(a);
  • 运行结果:
  • 主要想说明的一点就是function fn(a) 就相当于function fn(var a),因此预解析阶段是有对这个函数内部的a的预解析的,因此在函数内部输出的a为undefined而没有报错。
  • 将上面的例子稍微改动一下
var a =1;
function fn(a){
  console.log(a)
  a = 2;
}
fn(a): //将a的值传递进去
console.log(a);
  • 运行结果:
  • 将a当作实参传递进去之后,局部变量a的值变为1,再函数内部将a变为2,但是在外部输出时输出的是全局作用域下的a,因此还是1.
  • <mark>注意在函数内部a=2;当没有变量a的声明时,那么a=2生成的是全局作用域下的a,但是当函数内部有局部作用域下的a时,a=2仅仅就是赋值的作用</mark>

更新中…
本人是个小白,如有错误欢迎指正…