作用域链是由于 js 的变量都是对象的属性,而该对象可能又是其它对象的属性,而所有的对象都是 window 对象的属性,所以这些对象的关系可以看作是一条链
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
(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;
- }
- }
首先外层函数执行完,被销毁;但是外层函数的作用域链被复制到内层函数的作用域链里,组成内层函数的作用域链的一部分,记住是复制,不是引用(依据第 2 条),所以内层函数仍然可以访问到 name; 由于 返回的内层函数被 temp 引用,所以当外层函数执行完被销毁后,内层函数虽然作为外层函数的一部分,但是依然存在,正如第 3 条依据那样,它被第三者引用了;传说中的闭包也就是这个理
如果看了以上 3 条还不明白,可看接下来结合理论对代码的详细解释:
3)在 Javascript 中,如果一个对象不再被引用,那么这个对象就会被 GC 回收。如果两个对象互相引用,而不再被第 3 者所引用,那么这两个互相引用的对象也会被回收。
2)作用域链的创建规则是复制上一层环境的作用域链,并将指向本环境变量对象的指针放到链首;
1)js 作用域只和函数的界定符相关,函数与函数的嵌套形成了作用域链;
以上代码片断是我们 jser 经常见到的写法,是传说中的闭包。 众所周知:调用 temp(); 会弹出" test"; 该过程可以有以下三条理论作为依据来解释:
1、
以下内容来自读网上博客的总结,当笔记使用,只记重点,同时非常感谢乐于分享的博主们,是你们让我站在了巨人的肩旁上!
4 没块的作用域
3 函数内部的变量取代全局同名变量
2 函数内 (函数内的函数除外) 定义的变量在整个函数内部都有效
1 javascript 没有 var 的变量都为全局变量,且为 window 对象的属性
明白了作用域链下面就开始变量的作用域分析了
t2--t--window
那么 b 所以在的对象即 t2,t2 又包含在 t 中,t 又在 window 对象,所以 b 的作用域链如下
t--window
js 中函数也是对象,所以变量 a 所在的对象是 t,t 又在 window 对象中,所以 a 的作用域链如下
来源: