js作用域及作用域链概念理解及使用


Posted in Javascript onApril 15, 2013

(1)作用域

一个变量的作用域(scope)是程序源代码中定义的这个变量的区域。

1. 在JS中使用的是词法作用域(lexical scope)

不在任何函数内声明的变量(函数内省略var的也算全局)称作全局变量(global scope)
在函数内声明的变量具有函数作用域(function scope),属于局部变量

局部变量优先级高于全局变量

var name="one";
function test(){
 var name="two";
 console.log(name); //two
}
test();

函数内省略var的,会影响全局变量,因为它实际上已经被重写成了全局变量

var name="one";
function test(){
 name="two";
 
}
test();
console.log(name); //two

函数作用域,就是说函数是一个作用域的基本单位,js不像c/c++那样具有块级作用域 比如 if for 等

function test(){
 for(var i=0;i<10;i++){
  if(i==5){
   var name = "one";
  }
 }
 console.log(name); //one
}

test(); //因为是函数级作用域,所以可以访问到name="one"

当然了,js里边还使用到了高阶函数,其实可以理解成嵌套函数

function test1(){
 var name = "one";
 return function (){
  console.log(name);
 }
}
test1()();

test1()之后将调用外层函数,返回了一个内层函数,再继续(),就相应调用执行了内层函数,所以就输出 ”one"
嵌套函数涉及到了闭包,后面再谈..这里内层函数可以访问到外层函数中声明的变量name,这就涉及到了作用域链机制

2. JS中的声明提前

js中的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。并且,变量在声明之前就可以使用了,这种情况就叫做声明提前(hoisting)
tip:声明提前是在js引擎预编译时就进行了,在代码被执行之前已经有声明提前的现象产生了

比如

var name="one";
function test(){
 console.log(name); //undefined
 var name="two";
 console.log(name); //two
}

test();

上边就达到了下面的效果

var name="one";
function test(){
 var name;
 console.log(name); //undefined
 name="two";
 console.log(name); //two
}

test();

再试试把var去掉?这是函数内的name已经变成了全局变量,所以不再是undefined

var name="one";
function test(){
 console.log(name); //one
 name="two";
 console.log(name); //two
}

test();

3. 值得注意的是,上面提到的都没有传参数,如果test有参数,又如何呢?

function test(name){
 console.log(name); //one
 name="two";
 console.log(name); //two
}

var name = "one";
test(name);
console.log(name); // one

之前说过,基本类型是按值传递的,所以传进test里面的name实际上只是一个副本,函数返回之后这个副本就被清除了。
千万不要以为函数里边的name="two"把全局name修改了,因为它们是两个独立的name

(2)作用域链

上面提到的高级函数就涉及到了作用域链

function test1(){
 var name = "one";
 return function (){
  console.log(name);
 }
}
test1()();

1. 引入一大段话来解释:
每一段js代码(全局代码或函数)都有一个与之关联的作用域链(scope chain)。

这个作用域链是一个对象列表或者链表,这组对象定义了这段代码中“作用域中”的变量。

当js需要查找变量x的值的时候(这个过程称为变量解析(variable resolution)),它会从链的第一个对象开始查找,如果这个对象有一个名为x的属性,则会直接使用这个属性的值,如果第一个对象中没有名为x的属性,js会继续查找链上的下一个对象。如果第二个对象依然没有名为x的属性,则会继续查找下一个,以此类推。如果作用域链上没有任何一个对象含有属性x,那么就认为这段代码的作用域链上不存在x,并最终抛出一个引用错误(ReferenceError)异常。

2. 作用域链举例:

在js最顶层代码中(也就是不包括任何函数定义内的代码),作用域链由一个全局对象组成。

在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。

在一个嵌套的函数体内,作用域上至少有三个对象。

3. 作用域链创建规则:

当定义一个函数时(注意,是定义的时候就开始了),它实际上保存一个作用域链。

当调用这个函数时,它创建一个新的对象来储存它的参数或局部变量,并将这个对象添加保存至那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。

对于嵌套函数来说,情况又有所变化:每次调用外部函数的时候,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都要微妙的差别---在每次调用外部函数时,内部函数的代码都是相同的,而且关联这段代码的作用域链也不相同。

(tip: 把上面三点理解好,记住了,最好还要能用自己的话说出来,不然就背下来,因为面试官就直接问你:请描述一下作用域链...)

举个作用域链的实用例子:

var name="one";
function test(){
 var name="two";
 function test1(){
  var name="three";
  console.log(name); //three
 }
 function test2(){
  console.log(name); // two
 }
 
 test1();
 test2();
}

test();

上边是个嵌套函数,相应的应该是作用域链上有三个对象
那么在调用的时候,需要查找name的值,就在作用域链上查找

当成功调用test1()的时候,顺序为 test1()->test()->全局对象window 因为在test1()上就找到了name的值three,所以完成搜索返回
当成功调用test1()的时候,顺序为 test2()->test()->全局对象window 因为在test2()上没找到name的值,所以找test()中的,找到了name的值two,就完成搜索返回

还有一个例子有时候我们会犯错的,面试的时候也经常被骗到。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script type="text/javascript">
function buttonInit(){
  for(var i=1;i<4;i++){
    var b=document.getElementById("button"+i);
    b.addEventListener("click",function(){ 
      alert("Button"+i); //都是 Button4
    },false);
  }
}
window.onload=buttonInit;
</script>
</head>
<body>
<button id="button1">Button1</button>
<button id="button2">Button2</button>
<button id="button3">Button3</button>
</body>
</html>

为什么?
根据作用域链中变量的寻找规则:

b.addEventListener("click",function(){ 
      alert("Button"+i);
    },false);

这里有一个函数,它是匿名函数,既然是函数,那就在作用域链上具有一个对象,这个函数里边使用到了变量i,它自然会在作用域上寻找它。
查找顺序是 这个匿名函数 -->外部的函数buttonInit() -->全局对象window

匿名函数中找不到i,自然跑到了buttonInit(), ok,在for中找到了,

这时注册事件已经结束了,不要以为它会一个一个把i放下来,因为函数作用域之内的变量对作用域内是一直可见的,就是说会保持到最后的状态

当匿名函数要使用i的时候,注册事件完了,i已经变成了4,所以都是Button4

那怎么解决呢?

给它传值进去吧,每次循环时,再使用一个匿名函数,把for里边的i传进去,匿名函数的规则如代码

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script type="text/javascript">
function buttonInit(){
  for(var i=1;i<4;i++){
    (function(data_i){
    var b=document.getElementById("button"+data_i);
    b.addEventListener("click",function(){ 
      alert("Button"+data_i);
    },false);
    })(i);
  }
}
window.onload=buttonInit;
</script>
</head>
<body>
<button id="button1">Button1</button>
<button id="button2">Button2</button>
<button id="button3">Button3</button>
</body>
</html>

这样就可以 Button1..2..3了

4.上述就是作用域链的基本描述,另外,with语句可用于临时拓展作用域链(不推荐使用with)

语法形如:

with(object)

statement

这个with语句,将object添加到作用域链的头部,然后执行statement,最后把作用域链恢复到原始状态

简单用法:

比如给表单中各个项的值value赋值

一般可以我们直接这样

var f = document.forms[0];
f.name.value = "";
f.age.value = "";
f.email.value = "";

引入with后(因为使用with会产生一系列问题,所以还是使用上面那张形式吧)

with(document.forms[0]){
f.name.value = "";
f.age.value = "";
f.email.value = "";
}

另外,假如 一个对象o具有x属性,o.x = 1;
那么使用

with(o){
 x = 2;
}

就可以转换成 o.x = 2;
假如o没有定义属性x,它的功能就只是相当于 x = 2; 一个全局变量罢了。

因为with提供了一种读取o的属性的快捷方式,但他并不能创建o本身没有的属性。

要理解变量的作用域范围就得先理解作用域链
用var关键字声明一个变量时,就是为该变量所在的对象添加了一个属性。
作用域链:由于js的变量都是对象的属性,而该对象可能又是其它对象的属性,而所有的对象都是window对象的属性,所以这些对象的关系可以看作是一条链
链头就是变量所处的对象,链尾就是window对象

看下面的代码:

function t() { 

var a; 

function t2() { 

var b; 

} 

}

js中函数也是对象,所以变量a所在的对象是t,t又在window对象中,所以a的作用域链如下
t--window
那么b所以在的对象即t2,t2又包含在t中,t又在window对象,所以b的作用域链如下
t2--t--window
明白了作用域链下面就开始变量的作用域分析了
1 javascript 没有var的变量都为全局变量,且为window对象的属性
function test1() { 

//执行这个句的时候它会找作用域对象,这个函数就是作用域链中的第一个对象,但这个对象中没有相关的var语句 

//于里就找作用域链的第二个对象,即全局对象,而全局对象中也没有相关的var语句 

//由于没有相关的var语句,js隐式在函数地声明了变量即var all; 

all = 30; 

alert(all); 

} 

test1(); 

alert(all); 

alert(window.all);

2 函数内(函数内的函数除外)定义的变量在整个函数内部都有效
function test2() { 

var t = 0; 

//在for的条件里定义变量,这个变更的作用域链对象是这个函数 

//因此在整个的函数里它是有效的 

for (var i = 0; i < 5; i++) { 

t += i; 

} 

alert(i); 

} 

test2();

3 函数内部的变量取代全局同名变量
var t = "bb"; 

function test() { 

//执行t的时候,它会先找作用域链对象,由于它定义在函数内部,所以这个函数就是它的作用域链的第一个对象 

//而在这个对象里又有t的定义,所以t就是局部变量了,它替换了全局变量t 

//t只是此时有定义,但并没有赋值,赋值在下一行,所以这里输出了undefined 

alert(t); 

var t = "aa"; 

alert(t); 

} 

test();

4 没块的作用域
if (true) { 

//在块中定义了一个变量,它的作用域链的第一个对象就是全局对象window 

var tmp = 0; 

} 

//tmp的作用域链的第一个对象就是全局对象window,而上面又有全局对象中相关的var语句,因此输出0 

alert(tmp);

以下内容来自读网上博客的总结,当笔记使用,只记重点,同时非常感谢乐于分享的博主们,是你们让我站在了巨人的肩旁上!
1、

var temp = (function(){ 

var name ="test"; 

return function(){ 

 alert(name); 

} 

})();

以上代码片断是我们jser经常见到的写法,是传说中的闭包。 众所周知:调用 temp();会弹出 “ test”;该过程可以有以下三条理论作为依据来解释:

1)js 作用域只和函数的界定符相关,函数与函数的嵌套形成了作用域链;
2)作用域链的创建规则是复制上一层环境的作用域链,并将指向本环境变量对象的指针放到链首;
3)在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。

如果看了以上3条还不明白,可看接下来结合理论对代码的详细解释:
首先外层函数执行完,被销毁;但是外层函数的作用域链被复制到内层函数的作用域链里,组成内层函数的作用域链的一部分,记住是复制,不是引用(依据第2条),所以内层函数仍然可以访问到 name;由于 返回的内层函数被 temp 引用,所以当外层函数执行完被销毁后,内层函数虽然作为外层函数的一部分,但是依然存在,正如第3条依据那样,它被第三者引用了;传说中的闭包也就是这个理

Javascript 相关文章推荐
慎用 somefunction.prototype 分析
Jun 02 Javascript
Javascript创建自定义对象 创建Object实例添加属性和方法
Jun 04 Javascript
jQuery.holdReady()使用方法
May 20 Javascript
jquery实现鼠标滑过小图时显示大图的方法
Jan 14 Javascript
Bootstrap实现水平排列的表单
Jul 04 Javascript
jQuery实现下拉框多选 jquery-multiselect 的实例代码
Jul 14 Javascript
使用Ajax与服务器(JSON)通信实例
Nov 04 Javascript
遍历json获得数据的几种方法小结
Jan 21 Javascript
JavaScript中的遍历详解(多种遍历)
Apr 07 Javascript
JavaScript实现兼容IE6的收起折叠与展开效果实例
Sep 20 Javascript
react学习笔记之state以及setState的使用
Dec 07 Javascript
在博客园博文中添加自定义右键菜单的方法详解
Feb 05 Javascript
利用百度地图JSAPI生成h7n9禽流感分布图实现代码
Apr 15 #Javascript
jquery分页插件AmSetPager(自写)
Apr 15 #Javascript
Extjs 继承Ext.data.Store不起作用原因分析及解决
Apr 15 #Javascript
关于jQuery object and DOM element
Apr 15 #Javascript
在jQuery中 关于json空对象筛选替换
Apr 15 #Javascript
在Javascript中 声明时用&quot;var&quot;与不用&quot;var&quot;的区别
Apr 15 #Javascript
关于jQuery $.isNumeric vs. $.isNaN vs. isNaN
Apr 15 #Javascript
You might like
总集篇&特番节目先行播出!《SAO Alicization War of Underworld》第2季度TV动画4月25日放送!
2020/03/06 日漫
PHP4 与 MySQL 交互使用
2006/10/09 PHP
php判断输入是否是纯数字,英文,汉字的方法
2015/03/05 PHP
PHP实现CSV文件的导入和导出类
2015/03/24 PHP
PHP判断上传文件类型的解决办法
2015/10/20 PHP
PHPWind9.0手动屏蔽验证码解决后台关闭验证码但是依然显示的问题
2016/08/12 PHP
特殊字符、常规符号及其代码对照表
2006/06/26 Javascript
Mootools 1.2教程 排序类和方法简介
2009/09/15 Javascript
开源的javascript项目Kissy介绍
2014/11/28 Javascript
JavaScript中的anchor()方法使用详解
2015/06/08 Javascript
javascript实现列表滚动的方法
2015/07/30 Javascript
原生js编写autoComplete插件
2016/04/13 Javascript
基于JavaScript定位当前的地理位置
2017/04/11 Javascript
JavaScript实现三级联动菜单效果
2017/08/16 Javascript
基于Vue+element-ui 的Table二次封装的实现
2018/07/20 Javascript
vue2中引用及使用 better-scroll的方法详解
2018/11/15 Javascript
js嵌套的数组扁平化:将多维数组变成一维数组以及push()与concat()区别的讲解
2019/01/19 Javascript
js遍历详解(forEach, map, for, for...in, for...of)
2019/08/28 Javascript
分享Angular http interceptors 拦截器使用(推荐)
2019/11/10 Javascript
Python 文件读写操作实例详解
2014/03/12 Python
使用Python的PIL模块来进行图片对比
2016/02/18 Python
Python和Java进行DES加密和解密的实例
2018/01/09 Python
python解析xml简单示例
2019/06/21 Python
python字典嵌套字典的情况下找到某个key的value详解
2019/07/10 Python
Python中利用LSTM模型进行时间序列预测分析的实现
2019/07/26 Python
使用python 的matplotlib 画轨道实例
2020/01/19 Python
python文件路径操作方法总结
2020/12/21 Python
HTML5之WebGL 3D概述(下)—借助类库开发及框架介绍
2013/01/31 HTML / CSS
Clarins娇韵诗美国官网:法国天然护肤品牌
2016/09/26 全球购物
美赞臣营养马来西亚旗舰店:Enfagrow马来西亚
2019/07/26 全球购物
Linux文件操作命令都有哪些
2015/02/27 面试题
师范生自荐信模板
2014/05/28 职场文书
2014年财务科工作总结
2014/11/11 职场文书
nginx 防盗链防爬虫配置详解
2021/03/31 Servers
Spring Security中用JWT退出登录时遇到的坑
2021/10/16 Java/Android
MySQL数据库实验实现简单数据库应用系统设计
2022/06/21 MySQL