JavaScript的执行环境定义了变量和函数有权访问其他数据,修改它们的值。每一个执行环境有一个变量对象,定义了环境中的所有变量和函数。全局执行环境是我们最外边的一个执行环境,在web浏览器,这个全局的执行环境就是我们的window对象。每一个函数都有自己的执行环境,当执行流执行一个函数的时候,我们的JavaScript就会把当前的执行环境推进环境栈,执行完里面的代码就会从环境栈弹出,把控制权交给前一个执行环境。
我们来说说作用域链。其实作用域链定义了我们执行环境有权访问的所有变量和函数。在前面说到,我们有一个环境栈,环境栈最前面的变量对象的下一个变量对象来自包含环境,依次类推。所以栈低变量对象是全局执行环境的。
内部环境可以通过作用域链访问外部环境的所有的变量和函数,但是外部环境不能访问内部环境的任何变量和函数,这也是闭包的原理。
标识符解析也就是沿着作用域链一级一级地搜索标识符的过程,先从作用域链的前端开始,逐级向后回溯,直到找到为止,如果找不到就会报错。
我们拿一道网易的前端机试题目来帮助理解这个知识点:
在下面的html的结构下,写出点击其中一个<li>标签当前背景色改变红色,同胞的颜色为白色
<ul id="father"> <li>我是第一</li> <li>我是第二</li> <li>我是第三</li> <li>我是第四</li> <li>我是第五</li> </ul>
我的JavaScript代码:
//执行环境1-window (function() {//执行环境-2 var father = document.getElementById("father");//获取id var sub = father.getElementsByTagName("li");//获取li集合 //下面是用自执行的写法 for (var i = 0,size = sub.length; i < size; i++) {//通过遍历为每一个li元素都绑定点击事件 sub[i].onclick = (function(i) {//执行环境-3 return function() {//执行环境-4 for (var j = 0; j < size; j++) { sub[j].style.backgroundColor = "white"; } sub[i].style.backgroundColor = "red"; } })(i); } })();
我在执行环境-4的
sub[j].style.backgroundColor = "white";
这一句打断点,我们可以在chrome开发者调试工具看到我们的执行环境,当我点击第一个li元素的时候,我们来看看:
我们看到在chrome的开发者工具看到在当前执行环境的变量有j和this对象,this对象指向当前点击的li元素,当前执行环境的下一个执行环境中的变量有i,以此类推,在scope属性里面我们可以看到我们在当前执行环境下能够调用和修改的变量和函数。
说了那么多,或许我们需要一点练习来增强我们对执行环境这个知识点的理解了,来看下面的一些例题:
1、for循环的情况
var arr=[]; for(var i=0;i<3;i++){ arr[i]=function(){ return i; }; log(arr[i]);//这里存的是啥? } for(var i=0;i<3;i++){ log(arr[i]());//会打印啥? } log(arr[0]());//? log(arr[1]());//? log(arr[2]());//?
你可以先自己判断一下,不过里面可是有很多需要注意的点哦~,来看正确的答案
var arr=[]; for(var i=0;i<3;i++){ arr[i]=function(){ return i; }; log(arr[i]);//这里数组存的是一个个函数 } for(var i=0;i<3;i++){ log(arr[i]());//0,1,2 } log(arr[0]());//3 log(arr[1]());//3 log(arr[2]());//3
你猜到结果了吗?我们来解析一下,第一个log会输出一个函数数组,每一个函数返回当前执行环境的i值,在第二个for循环,这里需要注意的是,我们的i在全局执行环境下被重复定义,JavaScript可不会告诉你它被重复定义了哦,所以我们的i被更新,也就是当前执行环境中的i值会从0每次递增1,执行循环就会打印出0,1,2,最后一次循环之后,我们的i变成3,所以在最后打印i值的时候,我们会看到3输出。为什么我们的i不是开始定义函数时候的i值呢,也就是在最后执行函数数组的时候没有输出0,1,2呢,这是因为,我们的作用域链的机制限定闭包只能取得包含函数中任何变量的最后一个值,这也很容易理解,当我们在包含执行环境下创建一个新函数也就是新执行环境,我们将新执行环境推进栈,控制权交给新执行环境,所以包含执行环境下的状态此时只能是最后一次修改的值。
要想输出我们的预期可以这样写:
var arr=[]; for(var i=0;i<3;i++){ arr[i]=(function(num){ return num; })(i); } for(var i=0;i<3;i++){ log(arr[i]);//0,1,2 } log(arr[0]);//0 log(arr[1]);//1 log(arr[2]);//2
2、自执行函数
分析一下下面六段代码分别输出什么
var name="项脊轩"; (function(){ function Person(){ this.name="林语", this.getName=function(){ return function(){ return this.name; } } }; var female=new Person(); var myName=female.getName()(); log(myName);//这里会输出什么? })();
var name="项脊轩"; (function(){ function Person(){ this.name="林语", this.getName=function(){ return this.name; }; } var female=new Person(); var myName=female.getName(); log(myName);//输出什么 })();
var name="项脊轩"; (function(){ function Person(){ this.name="林语", this.getName=function(){ return (function(that){ return that.name; })(this); }; } var female=new Person(); var myName=female.getName(); log(myName);//输出什么 })();
var name="项脊轩"; (function(){ function Person(){ this.name="林语", this.getName=function(){ return function(){ return this.name; }.bind(this); }; } var female=new Person(); var myName=female.getName()(); log(myName);//输出什么 })();
var name="项脊轩"; (function(){ function Person(){ this.name="林语", this.getName=function(){ return function(){ return this.name; }.call(this); }; } var female=new Person(); var myName=female.getName(); log(myName);//输出什么 })();
var name="项脊轩"; (function(){ function Person(){ this.name="林语", this.getName=function(){ return function(){ return this.name; }.apply(this); }; } var female=new Person(); var myName=female.getName(); log(myName);//输出什么 })();
好,我们的答案就是除了第一个代码输出是“项脊轩”,其他都是“林语”,后面四个代码向我们展示了如何绑定到当前对象,读者可以自己研究体会一下。