JavaScript-执行环境
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);//输出什么 })();
好,我们的答案就是除了第一个代码输出是“项脊轩”,其他都是“林语”,后面四个代码向我们展示了如何绑定到当前对象,读者可以自己研究体会一下。
三:setTimeOut和setInterval
先上一道题,修改程序让它正确输出myName的值:
foo = function() { this.myName = "Foo function."; } foo.prototype.sayHello = function() { alert(this.myName); } foo.prototype.bar = function() { setTimeout(this.sayHello, 1000); } var f = new foo; f.bar();
我们来看看下面三个修改方式:
foo = function() { this.myName = "Foo function."; } foo.prototype.sayHello = function() { console.log(this.myName); } foo.prototype.bar = function() { setTimeout(this.sayHello.call(this), 1000); } var f = new foo; f.bar();
foo = function() { this.myName = "Foo function."; } foo.prototype.sayHello = function() { console.log(this.myName); } foo.prototype.bar = function() { setTimeout(this.sayHello.bind(this), 1000); } var f = new foo; f.bar();
foo = function() { this.myName = "Foo function."; } foo.prototype.sayHello = function() { console.log(this.myName); } foo.prototype.bar = function() { setTimeout(this.sayHello.call(this), 1000); } var f = new foo; f.bar();
下面我来为大家揭晓一下答案,在setTimeOut和setInterval的这两个方法,都会创建一个新的匿名函数,也就是创建了一个新的执行作用域,此时它们的执行环境是全局的执行环境,所以在我们的调用sayHello方法的时候,我们的this已经指向window对象,而在window对象下没有myName属性,所以我们的程序输出undefined。但是上面三段代码哪一个才是对的呢?我们得来分析一下call,apply,bind三者的区别,在使用了call和apply方法之后,我们的函数立即执行,就像自执行函数一样,所以我们的setTimeOut方法调用之后,不是在1秒之后输出,而是立即执行,显然不对。bind方法会生成一个新函数,但是这个函数的执行需要我们自己去启动,所以调用了bind方法的代码会在1秒钟之后输出我们的结果。
四、a标签
我们在页面上的结构是这样的:
<body> <a href="JavaScript:myA(this)">我是a,判断一下我被点击时的this是啥</a> <h1 onclick="myH(this)">我是h1,判断一下我被点击时的this是啥</h1> </body>
函数定义是这样的:
function myA(that){ console.log("a",that); } function myH(that){ console.log("h",that); }
当我分别去点击这两个标签的时候,我们看到控制台的输出是这样的:
这个说明我们的a标签href属性中的this是指向window的
最后为大家献上一个还不错的闭包题:尝试做做看吧
function fun(n, o) { console.log(o) return { fun: function(m) { return fun(m, n); } }; } var a = fun(0); a.fun(1); a.fun(2); a.fun(3); //undefined,?,?,? var b = fun(0).fun(1).fun(2).fun(3); //undefined,?,?,? var c = fun(0).fun(1); c.fun(2); c.fun(3); //undefined,?,?,? //问:三行a,b,c的输出分别是什么?