JavaScript变量、作用域问题

一、基本类型和引用类型的值

ECMAScript变量包含两种不同数据类型的值:基本类型引用类型;基本类型指的是简单的数据段,而引用类型值指的是那些可能由多个值构成的对象。

在将一个值赋给变量时,解析器必须确定这个值是基本类型值还是引用类型值。基本类型的数据变量有Undefined、Null、Boolean、Number、String,这5种基本数据类型是按值访问的,因为可以操作保存在变量中的实际值。

引用类型的值是保存在内存中的对象;javascript不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值是按引用访问的。

1.1、动态属性

定义基本类型值和引用类型值的方式是类似的:创建一个变量并为该变量赋值。但是,当这个值保存到变量中之后,对基本类型和引用类型的操作则大不相同。

对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法(像Java这种语言,除非改变类的本身,否则是无法动态删除在类中定义的属性或方法的)。请看如下的示例代码:

<script>
  let person = new Object();
  person.name = 'Tom';
  alert(person.name); // 输出Tom
</script>

上述示例代码通过new Object()操作创建了一个对象,并将其保存在了变量person中。然后我们为该对象添加了一个名为name的属性,并将字符串值“Tom”赋给了这个属性。紧接着,又通过alert函数访问了这个属性。如果对象不被销毁或者这个属性不被删除,则这个属性将一直存在。

但是基本类型我们就不能为其添加属性,虽然这样做不会报错,看如下示例代码:

<script>
  let name = 'Jerry';
  name.age = 27;
  alert(name.age);    // 输出undefined
</script>

在上述示例中,给字符串name定义了一个age属性,并为其赋值27。但在下一行访问这个属性时,发现该属性不见了。这说明只能给引用类型值动态地添加属性,以备后续使用。

1.2、复制变量值

除了保存的方式不同之外,在从一个变量向另一个变量复制基本类型和引用类型值时,也存在不同。

1.2.1、基本类型

如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。看如下示例:

let num1 = 5;
let num2 = num1;

num1保存的值是5,当使用num1的值来初始化num2时,num2中也保存了值5。但num2中的5与num1中的5是完全独立的,该值只是num1中5的一个副本。此后,这两个变量可以参与任何操作而不会相互影响。

复制基本类型的过程
复制基本类型的过程

 

1.2.2、引用类型

当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。

不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束之后,两个变量实际上将引用同一个对象。因此改变一个变量就会影响另一个变量,如下示例:

<script>

  let p1 = new Object();
  p1.name = 'Tom';
  let p2 = p1;
  p2.age = 25;
  
  console.log(p1);

</script>

p2保存了从p1复制来的指针,而p1和p2的指针都指向了同一个Object,所以当通过p2给Object对象添加了一个age属性时,再通过p1访问,则也会输出的是包含name和age属性的对象。

输出结果

 

引用类型的值复制

 

1.3、传递参数

ECMAScript中所有的函数都是按值传递的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。

1.3.1、基本类型值传递

在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用ECMAScript的概念来说,就是arguments对象中的一个元素)。请看下面的这个例子:

<script>

  function addTen(num){
    num += 10;
    return num;
  }

  let count = 20;
  let result = addTen(count);

	console.log(count);		// 输出20
  console.log(result);	// 输出30

</script>

这里的函数addTen()有一个参数num,而参数实际上是函数的局部变量。在调用这个函数时,变量count作为参数被传递给函数,这个变量的值是20。于是,数值20被复制给参数num以便在addTen()中使用。在函数内部,参数num的值被加上了10,但这一变化不会影响函数外部的count变量。

参数num与变量count互不相识,它们仅仅是具有相同的值。假如num是按引用传递的话,那么变量count的值也将变成30,从而反映函数内部的修改。

1.3.2、引用类型值传递

在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。请看下面的这个例子:

<script>

  function setName(obj){
    obj.name = 'Tom';
    obj = new Object();
    obj.name = 'Jerry';
  }

  let person = new Object();
  setName(person);
  
  console.log(person);

</script>

上面这个例子,在setName()函数内部,给传递进来的person添加了一个name属性并赋值“Tom”,因为通过函数的obj参数传递进来的是person对于new Object()这个对象的引用值,所以在setName()函数内部修改的是根person指向的统一对象,在内部的修改会影响外部的那个对象。

而又在函数内部重新创建了一个对象,并且这个对象的引用赋值给了obj,但是在输出person时,仍然显示的是Tom,这表明即使在函数内部修改了参数的值,但原始的引用仍然保持未变。实际上,当在函数内部重写obj时,这个变量引用的就是一个局部对象,而这个局部对象会在函数执行完毕后立即被销毁。

个人理解这个跟js的作用域有关系,setName()函数本身也是一个对象,虽然在函数的作用域可以正常访问和使用,当这个函数执行完毕,对象被销毁,就无法再使用作用域内的属性了。

就像Java里也是一样,当某个类的实例对象被销毁,是无法再访问这个实例对象里的任何属性的。

1.3.3、检测类型

使用typeof检测基本数据类型,示例代码如下:

<script>

  let name = 'Jerry';
  let age = 12;
  let sex = false;
  let unknown;
  let empty = null;
  let obj = new Object();
  let setName = function(){}
  
  console.log("name type is " + typeof name);         // name type is string
  console.log("age type is " + typeof age);           // age type is number
  console.log("sex type is " + typeof sex);           // sex type is boolean
  console.log("unknown type is " + typeof unknown);   // unknown type is undefined
  console.log("empty type is " + typeof empty);       // empty type is object
  console.log("obj type is " + typeof obj);           // obj type is object
  console.log("setName type is " + typeof setName);   // setName type is function

</script>

typeof检测引用数据类型的值用处不大。通常,我们并不是想知道某个值是对象,而是想知道它是什么类型的对象。所以ECMAScript提供了instanceof操作符,其语法如下:

result = variable instanceof constructor

示例代码如下:

<script>

  let arr = [];
  let person = new Object();
  let pattern = new RegExp('');
  
  console.log(arr instanceof Array);      // true
  console.log(person instanceof Object);  // true
  console.log(pattern instanceof RegExp); // true

</script>

根据规定,所有引用类型的值都是Object实例。因此,在检测一个引用类型值和Object构造函数时,instanceof操作符始终会返回true。当然,如果使用instanceof操作符检测基本类型的值,则该操作符始终会返回false,因为基本数据类型不是对象。

1.4、执行环境及作用域

执行环境(execution context,为简单起见,有时也简称为“环境”、“上下文”)是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

全局执行环境是最外围的一个执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象得到属性和方法创建的。某个执行环境中的所有代码执行完毕之后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出—例如关闭网页或浏览器—时才会被销毁)。

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流正是由这个方便的机制控制着。

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。

作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境变量是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,即arguments(这个对象在全局环境中是不存在的)。

作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象来自下一个包含环境。这样,一致延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。

<script>

  let color = 'blue';

  function changeColor(){
  
    let anotherColor = 'red';
  
    function swapColor(){
      let tempColor = anotherColor;
      anotherColor = color;
      color = tempColor;
    }
    swapColor();
  }
  
  changeColor();

</script>

以上代码共涉及3个执行环境:全局环境、changeColor()的局部环境和swapColor()的局部环境。

    • 全局环境中有一个变量color和一个函数changeColor()。
    • changeColor()的局部环境之中有一个名为anotherColor的变量和一个swapColor()函数,但是它可以访问全局环境中的color。
    • swapColor()的局部环境中有一个变量tempColor,该变量只能在这个环境中访问到。

无论是全局环境还是changeColor()的局部环境都无权访问tempColor。然而在swapColor()内部则可以访问其他两个环境中的所有变量,因为那两个环境是它的父执行环境。

内部环境可以通过作用域链访问所有的外部环境,但外部的环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性的、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。

1.4.1、延长作用域链

虽然执行环境的类型总共只有两种—全局和局部(函数),但是还有其他办法来延长作用域链。这么说是因为有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。

具体来说,就是当执行流进入下列任何一个语句时,作用域链就会得到加长:

    • try-catch语句的catch块
    • with语句

这两个语句都会在作用域链的前端添加一个变量对象。对with来说,会将指定的对象添加到作用域链中。对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。看下面的列子:

<script>

        function buildUrl(){
            let qs = '?debug=true';
            with (location) {
                var url = href + qs;
            }
            return url;
        }

        alert(buildUrl());

</script>

在此,with语句接收的是location对象,因此其变量对象中就包含了location对象的所有属性和方法,而这个变量对象被添加到了作用域链的前端。buildUrl()函数中定义了一个变量gs。当在with语句中引用变量href时(实际引用的是location.href),可以在当前执行环境的变量对象中找到。当引用变量qs时,引用的则是在buildUrl()中定义的那个变量,而改变量位于函数环境光的变量对象中。至于with语句内部,则定义了一个名为url的变量,因而url就成了函数执行环境的一部分,所以可以作为函数的值被返回。

1.4.2、JavaScript没有块级作用域

JavaScript没有块级作用域经常会导致理解上的困惑。在其他类C的语言中,由花括号封闭的代码块都有自己的作用域(如果用ECMAScript的话来讲,就是它们自己的执行环境),因而支持根据条件来定义变量。例如,下面的代码在JavaScript中并不会得到想象中的结果:

<script>

    if (true) {
        var color = 'red';
    }

    console.log(color);

</script>

这里是在一个if语句中定义了变量color。如果是在C、C++或Java中,color会在if语句执行完毕之后被销毁。但在JavaScript中,if语句中的变量声明会将变量添加到当前的执行环境(在这里是全局环境)中。在使用for循环的时候牢记这一点,例如:

<script>

      for (var i = 0; i < 10; i++) {
          console.log(i);
      }

      alert(i);		// 输出10

</script>

对于有块级作用域的语言来说,for语句初始化变量的表达式所定义的变量,只会存在于循环的环境之中。而对于JavaScript来说,由for语句创建的变量i即使在for循环结束之后,也依旧会存在于循环外部的执行环境中。

1.4.2.1、声明变量

使用var声明的变量会自动被添加到最接近的环境中。在函数内部,最接近的环境就是函数的局部环境;在with语句中,最接近的环境是函数环境。如果初始化变量时没有使用var声明,该变量会自动被添加到全局环境,如下所示:

<script>

      function add(num1, num2){
          var sum = num1 + num2;
          return sum;
      }

      var res = add(12, 12);
      console.log(res);

</script>

因为sum只存在于add函数执行中,所以在函数之外是无法访问的。而如果在函数执行环境中不使用var来声明sum,则默认会将该变量添加到全局环境中,如下示例代码:

  <script>

      function add(num1, num2){

          function add(num1, num2){
                sum = num1 + num2;
                return sum;
          }
          console.log(sum);     // Uncaught ReferenceError: sum is not defined

          return add(num1, num2);
      }

      var res = add(12, 12);
      console.log(sum);     // 24

</script>

这个例子中的变量sum在初始化赋值时没有使用var关键字。于是,当调用完add()之后,添加到全局环境中的变量sum将继续存在;即使函数已经执行完毕,后面的代码依旧可以访问它。

1.4.2.2、查询标识符

当在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索来确定该标识符实际代表什么。搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。如果在局部环境中找到了该标识符,搜索过程停止,变量就绪。如果在局部环境中没有找到该变量名,则继续沿作用域链向上搜索。搜索过程将一直追溯到全局环境的变量对象。如果在全局环境中也没有找到这个标识符,则意味着该变量尚未声明。

通过下面这个示例,可以理解查询标识符的过程:

<script>

        let color = 'red';

        function getColor(){
            return color;
        }

        alert(getColor());

</script>

调用本例中的函数 getColor()时会引用变量 color。为了确定变量 color 的值,将开始一个两步的搜索过程。首先,搜索 getColor()的变量对象,查找其中是否包含一个名为 color 的标识符。在没有找到的情况下,搜索继续到下一个变量对象(全局环境的变量对象 ),然后在那里找到了名为color 的标识符。因为搜索到了定义这个变量的变量对象,搜索过程宣告结束.

在这个搜索过程中,如果存在一个局部的变量的定义,则搜索会自动停止,不再进入另一个变量对象。换句话说,如果局部环境中存在着同名标识符,就不会使用位于父环境中的标识符,如下面的例子所示:


 <script>

        let color = 'red';

        function getColor(){
            let color = 'blue';
            return color;
        }

        alert(getColor());

</script>

修改后的代码在 getcolor()函数中声明了一个名为 color 的局部变量。调用函数时,该变量就会被声明。而当函数中的第二行代码执行时,意味着必须找到并返回变量 color 的值。搜索过程首先从局部环境中开始,而且在这里发现了一个名为 color 的变量,其值为"blue"。因为变量已经找到了,所以搜索即行停止,return 语句就使用这个局部变量,并为函数会返回"blue"。也就是说任何位于局部变量 color 的声明之后的代码,如果不使用 window.color 都无法访问全局 color变量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值