Javascript中作用域的修改以及with语句

背景

问题来自看JavaScript高级程序设计遇到了延长作用域的问题.不能理解文中解释的内容,于是网上查到一些相关文章.

首先需要了解的是,JavaScript中执行环境一共有两种:全局作用域和局部作用域.而在浏览器中.全局作用域是Windows对象.自下而上,局部作用域可以访问全局作用域(如果在子作用域中没有找到的话).反之则不可以.

但是使用with则可以起到延长作用域的作用.

摘抄相关文章

文章一

关于Javascript静态作用域的一些心得。

之前在看JS大牛BYVoid的<<NodeJS开发指南一书>>时, 学习到了最能说明Javascript静态作用域特性的代码, 如下:

var scope = "global";
function f1() {
    console.log(scope);
}
f1() // output: global
function f2() {
    scope = "f2"
    f1();
}
f2(); // output: global

第一次输出“global”,?
是因为f1()函数找到父级执行作用域下(全局作用域)有定义全局变量scope = “global“。

第二次输出”global“,

因为JS静态作用域链的特性使得作用域链是在函数定义时候被决定的,而不是在调用时候被决定的。

所以在f2()函数中执行f1()函数的时候,f1()函数的父级作用域仍然是全局作用域,所以依然会输出“global”。

那如何在不改变f1函数的结构和定义的情况下,使得在f2函数中输出的scope的变量的值可以是“f2”呢

使用with。

var scope = "global";
function f1() {
    console.log(scope);
}
f1() // output: global
function f2() {
    scope = "f2"
    with(scope) {
        f1();
    }
}
f2(); // output: f2

with会改变其范围内的作用域链。

这里with把在其范围内的f1()函数的执行作用域链的父级作用域更改为了自己,所以在执行f1()函数时,找到父级作用域链中scope定义为“f2”.

因此加入with后,第二次可以输出“f2”。

文章二

首先看看举的例子:

function buildUrl(){ 
     var qs="?debug=true"; 
     with(location){ 
          var url=href+qs; 
     } 
     return url; 
} 
var result=buildUrl(); 
   
alert(result);

如果你没读过着本书,并且需要学习javascript,请思考并尝试运行该例子。

最后弹出的不是undefined,而是你的静态页地址+qs的值。

来看一下with语句的作用:通俗的说,就是引用对象,并对该对象上的属性进行操作,其作用是可以省略重复书写该对象名称,起到简化书写的作用。

但是有几个问题需要注意:

1、with代码块中,javascript引擎对变量的处理方式是:先查找是不是该对象的属性,如果是,则停止。如果不是继续查找是不是局部变量。(在《Javascript高级程序设计(第二版)》中提到的观点,跟这一点恰好相反,但是实例可证明其是错误的,会在接下来介绍)
2、就算在with语句中使用 var 运算符重新定义变量(该变量是with引用对象的属性),如果该属性是可写属性,那么也会给对象的属性赋值。

3、如果你想通过with语句,对引用对象添加多个属性,并为每个属性赋值,这是不可能的!也就是说,要赋值的只能是对象已经存在并且可以写入的属性(不能是只读属性)。

再来看看开头提到的那句话

“由于with语句的变量对象是只读的,结果url就成了函数执行环境的一部分,因而可以作为函数的值被返回。”

反过来:如果with语句的变量对象是可写入的…… 刚才第3点提过,不能给对象写入原来不存在的属性,先这样理解,下面还有另外的含义。

那延长作用域链又是怎么回事?
一般的,“由于with语句块中作用域的‘变量对象’是只读的,所以在他本层定义的标识符,不能存储到本层,而是存储到它的上一层作用域”。这里又要理解有一层“只读”的含义。

在Javascript的作用域中(作用域,想想就是函数块,每个函数都会有个函数名,就算是匿名函数也有个空函数名),那么创建作用域的时候,本层的标识符就可以寄托在这个作用域下,而with语句块中作用域的‘变量对象’是只读的,不能存储标识符,只能存储在其上一层,这就是延长作用域链。其实,这和上面说的不能给对象添加属性有同工之处。

其实,完全可以这样理解,在Javascript中,没有块级作用域,就是说除了函数,其他的块级代码都没有自己的作用域。

现在说一下之前提到的with代码块中变量处理方式的问题,用事实说话:

var o={href:"sssss"};  
var href="1111";  
function buildUrl(){  
     var qs="?debug=true";       
     with(o){  
          href="2222";  
          var url=href+qs;  
     }      
     return url;  
}  
var result=buildUrl();  
alert(result);  
alert(href);

结果:2222?debug=true + 1111

很明显,with语句中并没有更改变量href的值,而是更改了 o 对象的 href 属性。

就是说,with中首先查找的是相关对象的属性,如果没有,才改变变量的值。你可以将以上例子o对象的href属性去掉看看。

文章三

虽然执行环境的类型总共只有两种——全局和局部(函数),但还是有办法来延长作用域链的,这么说是因为有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。在两种情况下会发生这种现象。具体来说,就是当执行流进入下列任何语句时,作用域链就会得到加长:

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

var mother = {
    name: ‘Rose‘,
    age: 30
}

function father(){
    var fName = ‘Jack‘;
    var name = ‘张三‘;

    function son(){
        var sName = ‘son‘;
        with(mother){
            var word = ‘my name: ‘ + sName + ‘, father name: ‘ + fName +
                ‘, mother name: ‘ + name + ", mother age: " + age;
        }
        console.log(word);//my name: son, father name: Jack, mother name: Rose, mother age: 30
    }
    son();
}
father();

首先声明一个对象mother,包含两个属性:name和age,然后定义一个函数father,里面声明两个变量:fName和name,又声明一个函数son,son中定义了一个变量sName和一个with语句块。我们来看打印的结果,mother name为对象mother中的Rose,而不是函数father中的张三,这就是with语句在起作用。

在Chrome中调试可以发现,如下图:Javascript中作用域的修改以及with语句

代码执行到19行with语句块中时,作用域链的前端增加了一个With Block作用域,根据作用域链搜索机制,此时的name应该是Rose。再看下图:

Javascript中作用域的修改以及with语句

代码执行到22行,with语句块执行结束,with作用域从作用域链中移除,with语句块中声明的变量绑定在with所在的函数中,所以console.log(word)可以正常输出word。

参考

相关推荐