JS作用域详解

参考文章书籍:
JavaScript高级程序设计-第四章
深入了解JavaScript,从作用域链开始(1)
JS作用域面试题总结
索引:


一、涉及概念、知识点

1.执行环境

  • 执行环境定义了变量或者函数有权访问的其他数据,执行环境有与之相关联的变量对象。
  • 执行环境会在环境内所有代码执行完毕后,销毁该环境。(全局执行环境会等到应用程序退出或者浏览器窗口关闭才会销毁)

    1. 全局执行环境
      全局执行环境也即window对象,因此所有的全局变量、函数都是window对象的属性和方法。
    2. 局部执行环境(函数执行环境)
      当执行流进入一个函数时,执行环境变为这一特定函数的局部执行环境。待函数执行完后,栈将环境弹出,转而进入下一个执行环境。

正由于不同执行环境间的切换,因此产生了变量和函数的作用域

2.作用域

  • 作用域代表变量与函数的可访问范围,可以说作用域控制着变量与函数的可见性和生命周期。
  • 在JavaScript中,变量的作用域有全局作用域和局部作用域两种,局部作用域又称为函数作用域。
全局作用域

拥有全局作用域的对象:
1.程序最外层定义的函数或者变量

var a = "tsrot";
function hello(){
    alert(a);
}

function sayHello(){
    hello();
}

alert(a);     //能访问到tsrot
hello();      //能访问到tsrot
sayHello();   //能访问到hello函数,然后也能访问到tsrot

2.所有末定义直接赋值的变量(不推荐)

function hello(){
    a = "tsrot";
    var b = "hello tsrot";
}

alert(a);  //能访问到tsrot
alert(b);  //error 不能访问

3.所有window对象的属性和方法
如window.name、window.location、window.top等等。

局部作用域(函数作用域)

局部作用域在函数内创建,在函数内可访问,函数外不可访问

function hello(){
    var a = "tsrot";
    alert(a);
}

hello(); //函数内可访问到tsrot
alert(a); //error not defined

3.作用域链

▷作用域链的用途:

保证该执行环境下有权访问的所有变量、函数的有序访问

▷作用域链搜索方法:

目标标识符的解析是从执行环境的最前端开始,沿着作用域链一级一级向后回溯,直到找到标识符为止。

var color = "blue";

function changeColor(){
        if(color=="blue"){
            color = "red";
        }
        else{
            color = "blue";
        }
    }

changeColor();
console.log(color); //red

以上代码解释了标识符color的搜索过程。在调用函数changeColor()后,执行到if判定时需要用到变量color。于是在当前执行环境下先于函数内部作用域搜索变量color;未找到后向外层检索,此时访问到了全局变量color="blue"if判定符合条件,执行颜色修改为red

▷作用域链访问不可逆。

整个搜索访问的过程中,可以通过作用域链从内部环境访问外部环境,但不可逆转,是线性有向的过程。

var color = "blue";
function changeColor(){
    var anotherColor = "red";
    function swapColor(){
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        //这里可以访问color、anotherColor和tempColor

    }
    //这里可以访问color和anotherColor

}

//这里只能访问color
changeColor();

以上代码中,全局环境changgeColor()swapColor()为三个不同的执行环境,可以由内层向外层搜索访问,但不可向内层访问

★JavaScript没有块级作用域

JavaScript与其它语言不同的一点是,没有块级作用域
块级作用域:由花括号封闭的代码块都有自己的作封闭执行环境(作用域)。

JavaScript中只有函数具有类似块级作用域的函数作用域。
其它if、for、while等具有花括号的语句则没有块级作用域,也即语句执行结束后,内部变量、函数仍可以在外层执行环境中访问到!

for (var i;i<10;i++) {                     ------for示例
    dosomething(i);
}
alert(i);    //10
//依旧可以访问i

if(true){                                  ------if示例
    var color = "blue";
}
alert(color);  //"blue"
//同样可以访问到color

function add(a,b){                         ------function示例
    var sum = a+b;
    return sum;
}
var result = add(10,20);
alert(sum);      //error:sum is not defined

二、作用域习题测试

1.纯作用域、作用域链类

1.

var x = 10;

function foo() {
    var y = 20;

    function bar() {
        var z = 30;

        console.log(x + y + z);
    };

    bar()
};

foo();

代码的输出结果为”60″。**函数bar可以直接访问”z”,然后又通过作用域链访问上层
的”x”和”y”。**

2.

var y = 'global';  
function test(x){  
    if(x){  
        var y ='local';  
    }  
    return y;  
}  
console.log(test(true)); //local 

在当前作用域(test()函数)内,可以找到目标标识符y,因此不需要向上访问全局变量y=“global”

3.

var y = 'global';  
function test(x){  
    (function(){  
        if(x){  
            var y = 'local';  
        }  
    })();  
    return y;  
}  
console.log(test(true));  //global

在当前作用域(test()函数)内,找不到标识符y,因此按照向上搜索的规则,沿着作用域链访问全局变量y=“global”
不能向内层访问自执行函数中的y

4.★★★★函数嵌套类

var a=10; 
function aaa(){ 
 alert(a);
};            
function bbb(){
var a=20;
aaa();
}
bbb();   //10

因为bbb()访问不了内层作用域的变量a,因此向上访问全局变量a = 10.
我们一步步来分析这个过程:
先看个简单的:若果aaa本身就有,肯定拿自己的。

var a = 10;

function aaa() {                 //step-4
    var a=30;
    alert(a);                    //step-5->执行alert,此时最先找到aaa作用域下的a=30
}; 

function bbb() {                 //step-2
    var a = 20;
    aaa();                      //step-3
}
//定义了函数没啥用,调用才是真格的所以这里是step-1
bbb();        //30              //step-1
  • 假如aaa()内未定义a呢,会取谁?
var a = 10;

function aaa() {                 //step-4
    alert(a);                    //step-5->执行alert,此时最先找到aaa的父作用域中的a=10
}; 

function bbb() {                 //step-2
    var a = 20;
    aaa();                      //step-3
}
//定义了函数没啥用,调用才是真格的所以这里是step-1
bbb();        //10              //step-1
  • 要是全局的var a=10都没有呢?
function aaa() {                 //step-4
    alert(a);                    //step-5->执行alert,此时沿着作用域一层一层找,没有a,所以报错。
}; 

function bbb() {                 //step-2
    var a = 20;
    aaa();                      //step-3
}
//定义了函数没啥用,调用才是真格的所以这里是step-1
bbb();        //10              //step-1
★★注意:

函数作用域的嵌套关系是定义时决定的,而不是调用时决定的,也就是说,JavaScript 的作用域是静态作用域,又叫词法作用域,这是因为作用域的嵌套关系可以在语法分析时确定,而不必等到运行时确定。
具体可参看这篇文章

放在我们上面的实例中理解就是:aaa和bbb函数作用域是兄弟作用域,互相不能访问内部变量,这在定义的时候就确定了。虽然调用的时候,aaa是在bbb函数的内部调用,但是作用域链却不会被改变!
★5. a=b=10类特殊情况

function aaa(){
      var a=b=10; 
}
 aaa();
 alert(a);//结果为,无法访问到
 alert(b);//结果为10;

var a=b=10; 可以解析成 b=10;var a=b; 也就是b为全局变量,a为局部变量,所以外部访问a访问不到,访问b结果为10;

2.作用域+变量(函数)提升类(笔试面试最爱!)

1.

var y = 'global';  

function test(x){  
    console.log(y);                //undefined  
    if(x){  
        var y = 'local';  
    }  
    return y;  
}  

console.log(test(true));           //local  

这里涉及到JavaScript中的变量提升,JavaScript中会自动把变量声明的语句提升到当前作用域的最前方 。
以上代码可以这样来理解

var y = 'global';  
function test(x){  
    var y;                   //声明提前了
    console.log(y);  
    if(x){  
        y = 'local';        //赋值仍留着原地
    }  
    return y;  
}  
console.log(test(true));   //local

当test函数中打印y时,变量y只是被声明了,并没有赋值,所以先打印出了undefined;
当程序继续向下执行,则将local返回出来。
2.

var a = 1;  
function b(){  
    a = 10;
    console.log(a);  //10  
    return;  
    var a = 100;  
}  
b();  
console.log(a);     // 1

变量a先是全局声明,在调用b()函数时,在内部又声明了局部的a变量(因为var a=100变量提升),改变其值为10.在执行完毕后,退出函数环境,局部的a变量销毁,因此访问到的a为全局的1
4.

var a = 100;    
function testResult(){    
  var b = 2 * a;    
  var a = 200;    
  var c = a / 2;    
  alert(b);    
  alert(c);    
}    
testResult()        //NaN  100

同样是基于变量声明提前,原理参考第1题过程。局部变量a在函数中声明提前到第一行,值为undefined。因此b值为NaN,在a赋值后,c值为100。

5.

var getName = function(){
    console.log(2);
}
function getName (){
    console.log(1);
}
getName();

这个例子同时涉及到了变量声明提升函数声明提升
上例等同于:

var getName;    //变量声明提升
function getName(){    //函数声明提升到顶部
    console.log(1);
}
getName = function(){    //变量赋值依然保留在原来的位置
    console.log(2);
}
getName();    // 最终输出:2

由于变量和函数声明均提升到顶部,因此getName又被后面函数表达式的赋值操作给覆盖了,所以输出2
而如果将上代码稍加改动,变这样的话:

getName();  //最终输出:1
var getName = function(){
    console.log(2);
}
function getName (){
    console.log(1);
}

等同于:

var getName;    //变量声明提升
function getName(){    //函数声明提升到顶部
    console.log(1);
}
getName();    // 最终输出:1

getName = function(){    //变量赋值依然保留在原来的位置
    console.log(2);
}

看到这里,有些人可能会觉得,当存在两个相同的声明,是不是由于覆盖的现象?造成了上面的结果?
那么接下来我们研究一下,到底两者共存遵循什么规律
比如,有下述代码:

    var a = funtion () {
        console.log(10)
    }
    var a;
    console.log(a);    // f a() {console.log(10)}
    console.log(a());  // 10undefined

    a = 3;
    console.log(a)   //3
    a = 6;
    console.log(a());   //a() is not a function;

从结果我们可以看到:首先变量a提升到顶部,然后给它赋值一个函数,此时打印a,a为函数。后面进行了赋值3操作后,a变为3,此时再执行a(),浏览器会提示我们出错了。也就是说此时a又被覆盖为一个变量,而非函数。
a的变化过程如下:

var a;                  //变量提升a=undefined
a =  function (){
    console.log(10);    //a赋值一个函数
}

a = 3;                 //a赋值一个变量
a = 6;

总结:

1.同时出现变量提升和函数提升时,函数会首先被提升,然后才是变量。
2.函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。

详细内容可以参看:书籍《你不知道的JavaScript》

三、延长、扩展作用域

1.延长作用域链

实现原理:在作用域链的前端增加一个变量对象,当执行流进入下列语句时,作用域链就得到加长:
- try-catch语句的catch
- with语句

1.对于with语句来说,会将指定的对象添加到作用域链中。
2.对于catch语句来说,会创建一个新的变量对象。

例:

function alterUrl() {
        var qs = "?debug=true";

        with(location) {
            var url = href + qs;
        }

        return url;
}

with语句接收location对象,等同于with语句的作用域链扩展添加了location对象作用域部分。
因此,在with语句中可以调用所有的location对象的属性和方法。此时的href默认将获取浏览器的href,无需赋值。

2.扩展作用域

扩展作用域的方法常用有两种:
- call()方法
- apply()方法

PS:

callapply方法是函数本身具有的非继承的方法,不仅可以传递参数,还可以扩充函数运行的作用域

apply()方法能劫持另外一个对象的方法,继承另外一个对象的属性.

Function.apply(obj,args)`方法接收两个参数:
obj:这个对象将代替Function类里this对象
args:这个是数组,可以是Array,也可以是arguments对象。总之它将作为参数传给 Function(args–>arguments)

call:和apply的意思一样,只不过是将参数数组改为了参数列表.

Function.call(obj,[param1[,param2[,…[,paramN]]]])
obj:这个对象将代替Function类里this对象.
params:这个是一个参数列表.

示例:
1.call():
(1)不传递参数,只改变this指向,以扩充作用域。

 window.color = 'red';
        document.color = 'yellow';

        var s1 = {color: 'blue' };
        function changeColor(){
            console.log(this.color);
        }

        changeColor.call();         //red (默认传递参数为window对象参数)
        changeColor.call(window);   //red
        changeColor.call(document); //yellow
        changeColor.call(this);     //red
        changeColor.call(s1);       //blue

★(2)call的参数列表必须一一对应,否则将会出现赋值交叉。

function food(name,price){
    this.name = name;
    this.price = price;
}

function fruits(name,price,weight){
    food.call(this,name,price,weight);
    this.weight = weight;
}

var result = new fruits("pingguo",5,2)
alert(result.name+"--"+result.price+"--"+result.weight);   //pingguo--5--2

以上代码参数列表与food的变量顺序是保持一致。如果将参数列表顺序打乱(或者food函数参数顺序打乱),就会出现赋值交叉的情况,如:

function food(name,price){
    this.name = name;
    this.price = price;
}

function fruits(name,price,weight){
    food.call(this,price,name,weight);
    this.weight = weight;
}

var result = new fruits("pingguo",5,2)
alert(result.name+"--"+result.price+"--"+result.weight);  //5--pingguo--2

2.apply():
(1)不传递参数,只改变this指向,扩展作用域

window.number = 'one';
document.number = 'two';
var s1 = {number: 'three' };
function changeColor(){
    console.log(this.number);
}

changeColor.apply();         //one (默认传参为window对象下参数)
changeColor.apply(window);   //one
changeColor.apply(document); //two
changeColor.apply(this);     //one
changeColor.apply(s1);       //three

(2)函数间作用域扩展,实现方法调用。

function Person(name,age){                //定义一个人类
    this.name = name;
    this.age = age;
}

function Student(name,age,grade){        //定义一个学生类
    Person.apply(this,arguments);        //扩展Student的作用域到Person。
    //或者Person.call(this,name,age); 也可实现作用域扩展!
    this.grade = grade;
}

var student1 = new Student("xiaowang",21,90);
alert(student1.name+student1.age+student1.grade); //xiaowang2190            

详细的扩展作用域的方法解析,可以参看这篇文章:call,apply,bind改变this指向

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值