好程序员web前端带你了解JS的作用域链,我们都知道js是一个基于对象的语言,系统内置各种对象。

 

window作为一个天然存在的全局对象,它承担了所有全局资源的存储。

 

我们使用的任何全局变量,都是window下的。也就是说,在js中,实际上没有任何对象、方法是可以独立的,它们必须依赖于某个对象才可以被访问或执行。

 

就像alert(),它的完整写法是window.alert()

parseInt(), 完整写法是window.parseInt()

 

所有放在window对象下的资源,访问时可以默认省略window

 

但有一种情况非常特殊,例如函数中的局部变量:

function Person(){

       var name = “abc”;

}

当我们试图访问这个name属性时

console.log(newPerson().name); 

结果是undefined

我们只能在函数内部访问:

function Person(){

       var name = “abc”;

       console.log(name);

}

这种属性,在构造函数中,也被称为私有属性,我们必须提供一种对外公开的方法才可以被外界访问。例如:

function Person(){

       var name = “abc”;

       this.getName = function(){

       returnname;

}

this.setName= function(_name){

       name = _name;

}

}

这是一个典型的作用域问题似乎我们每个人都知道。

 

但这似乎也违反了我们的一个常识:那就是在js中,所有资源都必须依赖对象才能存在,不可独立使用。比如说:

function aaa(){

       function bbb(){  }

       bbb();

}

这段代码看上去并没有错,但是请问bbb这个函数为什么可以独立存在呢?如果我们把它换成这样:

function aaa(){

       function bbb(){  }

       window.bbb();

}

结果是运行错误!

 

那如果换成这样呢?

function aaa(){

       function bbb(){  }

       this.bbb();

}

结果还是运行错误!

 

那么我们不禁要发问了,bbb这个函数到底是属于哪个对象的?

 

当我们在调用一个函数的时候,浏览器会为这个函数的执行开辟一块内存区域用来存储这个方法在执行的临时数据。而对象作为js的基本存储单位,因此,临时数据实际上都被保存到了一个对象当中,这个对象,就是我们平时所说的执行上下文环境

 

当我们调用aaa函数时,例如window.aaa()

浏览器会为这一次函数的执行,创建一个执行上下文环境对象,我们暂时给它起个名字,叫做contextAAA吧,当然它是临时的,因为函数执行完它也就消失了。

 

那我们的代码实际上会变成这样:

function aaa(){

       function bbb(){   }

       contextAAA.bbb();

}

尽管contextAAA对象是看不见的,但它确实存在。

 

而当我们执行bbb函数时,浏览器会再次为这一次函数调用创建一个临时的执行上下文环境,我们暂且叫它contextBBB

那么contextAAA contextBBB以及window之间会形成链条关系,

举个例子来说明吧

var num = 888;

function aaa(){

       var num = 100;

       function bbb(){

       var num= 200;

       console.log(num);

}

bbb();

}

aaa();

那么contextAAA 如下:

contextAAA = {

       num 100,

       bbb : function(){ … },

       parentContext: window//父级上下文对象

}

那么contextBBB如下:

contextBBB = {

       num : 200,

       parentContext: contextAAA //父级上下文对象

}

因此我们发现,在父级上下文对象中,我们没有办法访问到子级上下对象,这是一个单向链表,这就是全局不能访问局部的原因。

 

bbb函数中打印出的num应该是多少呢?这取决在上下文对象中的查找顺序,顺序大概是这样的:

首先在当前上下文对象contextBBB中,找一下有没有num变量,找到就直接打印。以我们目前的代码看,结果应该是200

我们把代码改造一下:

var num= 888;

function aaa(){

       var num = 100;

       function bbb(){

       console.log(num);

}

bbb();

}

aaa();

由于这次在contextBBB对象中找不到num变量了,因此它会从父级上下文对象中查找,也就是contextAAA里面的num,因此打印的结果是100;

我们再把代码改造一下

var num= 888;

function aaa(){

       function bbb(){

       console.log(num);

}

bbb();

}

aaa();

由于这次连contextAAA对象里也找不到了,会再次向它的父级上下文对象,也就是window查找,因此打印结果是888

 

contextAAAcontextBBB的父子关系,在你写代码的一刻就决定了,这就是作用域。

function aaa(){

       var num = 10;

       function bbb(){  console.log(num);  }

}

而代码执行时,产生的上下文对象,是链表的关系。这就是我们所说的作用域链,它的原理跟原型链是一样的。

图片17.png

理解了这一点,也能弄明白闭包的原理。

function aaa(){

       var num = 10;

       return functionbbb(){  console.log(num);  }

}

aaa( )( )

尽管bbb函数通过return在全局范围被执行了,但作用域的链表关系并没有发生改变,因此,bbb函数依然可以访问num这个局部变量。