深入Javascript-作用域&Scope Chain

深入Javascript-作用域&Scope Chain

写在前面

前一段时间朋友面试keen问了个问题:JS作用域是什么?正好这两天有一道XSS-Challenge也涉及了作用域的trick,填补了很多知识空白(JS的世界真是太特喵的nb了),写一篇文章来扫个盲

什么是JavaScript的作用域

在JS中,一个函数内是否可访问某个变量,要看该变量的作用域(scope)。我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

变量的作用域有全局作用域和局部作用域(函数作用域)两种

全局作用域(Global Scope)

在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下三种情形拥有全局作用域

1-程序最外层定义的函数或者变量

举个最简单的例子如下,global变量就属于全局作用域,不管是在 checkscope() 函数内部还是外部,都能访问到全局变量 global,checkscope函数也属于全局作用域。

var global = "global";     // 显式声明一个全局变量
function checkscope() {
    var local = "local";   // 显式声明一个局部变量
    return global;         // 返回全局变量的值
}
console.log(scope);        // "global"
console.log(checkscope()); // "global"
console.log(local);        // error: local is not defined.

2-所有末定义直接赋值的变量

这个跟我们平常写代码的坏习惯有关,不加限制类型的变量会自动升级为全局作用域,这个就不限制在程序的最外层还是函数内部,示例如下:

username = 'hpdoger';

function echoName(){
    nickname = 'wuyanzu';
}

function CheckVal(){
    console.log(username); //hpdoger    
    console.log(nickname); //wuyanzu
}

echoName();
CheckVal();

3-Window对象的属性和方法

一般情况下,window对象的内置属性都拥有全局作用域,例如window.name、window.location、window.top等等

通常在Javascript中我们说全局对象,指的就是Window对象,引用this指代的也是Window对象,如果我们在程序中定义一个全局作用域的变量,那么它自然也会成为Window对象的属性,所以下面的用法是等价的

var name = 'hpdoger';

name == window.name; //true

局部作用域(Local Scope)

和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到。最常见的是在函数体内定义的变量,只能在函数体内使用

举个最简单的例子如下

function init() {
    var inVariable = "local";
}
init();
console.log(inVariable); //Uncaught ReferenceError: inVariable is not defined

var声明的inVariaiable属于局部作用域范畴,在全局作用域没有声明,只能在函数内部调用。这时候你可能有个疑问,它是var的声明啊,他喵的不应该是全局变量吗??

实际上这跟它的声明方式没有一点关系。变量是否可引用,只由它的作用域决定。

局部作用域与全局作用域的制约

在函数体内,局部变量的优先级高于同名的全局变量。如果在函数内声明的一个局部变量或者函数参数中带有的变量和全局变量重名,那么全局变量就被局部变量所遮盖,而全局变量并不会因此发生值的变化,举个例子来看。

var username = 'hpdoger';

function echoNameA(username){
    console.log(username);//wuyanzu
}

function echoNameB(){
    var username = 'wuyanzu';
    console.log(username); //wuyanzu
}

echoNameA('wuyanzu');
echoNameB();
console.log(username);   //hpdoger

我们在这里用到遮盖这个词,其实是不准确的。因为读取变量值的方式是查找作用域链,也就是遍历Scope Chain,只是实现效果类似于遮盖。下文我们来看一下什么是Javascript的作用域链

什么是Javascript作用域链

基础概念

在寻找一个变量可访问性(取值)时是根据作用域链来查找的,作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问

我们先引入两个概念来走通scope chain

  • AO:Activetion Object(活动对象)
  • VO:Variable Object(变量对象)

AO对应的是函数执行阶段,当函数被调用执行时,会建立一个执行上下文,该执行上下文包含了函数所需的所有变量,该变量共同组成了一个新的对象就是Activetion Object。该对象包含了:

  • 函数的所有局部变量
  • 函数的所有命名参数
  • 函数的参数集合
  • 函数的this指向

举个例子来看函数执行的时候AO的值

function add(a,b){
    var sum = a + b;
    function say(){
        alert(sum);
    }
    return sum;
}

add(4,5);

如果我们用JS的对象来描述AO,那么它的表现形式如下

  AO = {
        this : window,
        arguments : [4,5],
        a : 4,
        b : 5,
        say : ,
        sum : undefined
  }

VO对应的是函数创建阶段,JS解析引擎进行预解析时,所有的变量和函数的声明,统称为Variable Object。该变量与执行上下文相关,知道自己的数据存储在哪里,并且知道如何访问。它分为全局上下文VO(全局对象,Global object,我们通常说的global对象)和函数上下文的AO,它存储着在上下文中声明的以下内容:

  • 变量 (var, 变量声明);
  • 函数声明 (FunctionDeclaration, 缩写为FD);
  • 函数的形参
function add(a,b){
    var sum = a + b;
    function say(){
        alert(sum);
    }
    return sum;
}
// sum,say,a,b 组合的对象就是VO,不过该对象的值基本上都是undefined

遍历作用域链

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

他喵的,上面好多都是我复制过来的,概念很复杂其实挺简单的,我们举一个简单的例子来看。

var x = 10;

function foo() {
    var y = 20;

    function bar() {
        var z = 30;

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

    bar()
};

foo();

函数bar可以直接访问”z”,然后通过作用域链访问上层的”x”和”y”。此时的作用域链为:

此时作用域链(Scope Chain)有三级,第一级为bar AO,第二级为foo AO,然后Global Object(VO)

    scope -> bar.AO -> foo.AO -> Global Object

    bar.AO = {
        z : 30,
        __parent__ : foo.AO
    }

    foo.AO = {
        y : 20,
        bar : ,
        __parent__ : 
    }

    Global Object = {
        x : 10,
        foo : ,
        __parent__ : null
    }

很简单,就是先从当前的AO一步一步向上遍历AO对象查找,走后查到VO(存储全局对象的东西)

一个有趣的Scope Chain

我们看下面的例子,console.log的打印值为undefined

var username = 'hpdoger';

function echoName(){
    console.log(username);  //undefiend
    var username = 'wuyanzu';
}

echoName();

为啥不是hpdoger呢?这是因为AO建立的逻辑是要先声明变量,所以在函数eechoName中代码实际的执行流程是这样的:

function echoName(){
    var username;
    console.log(username);
    var username = 'wuyanzu';
}
not found!