一道题引发的对于自执行函数、异步调用与event loop、作用域链的思考

依旧是由一道面试题引发的思考:

        for(var i=0;i<5;i++){ 
            setTimeout(function(){console.log(i)},i*1000); 
        }

大家知道出来的结果是什么吗?

5 5 5 5 5 隔1秒出来一个数

首先,我们需要明确的是setTimeout()是一个异步函数。异步函数在写项目的过程中非常常见,比如ajax,利用xhr回调函数,当获取到服务器数据之后再对返回的数据responseText或responseXML进行处理,在获取到数据之前也不影响我们干其他事情。再比如事件监听addEventListener(),当事件发生时先执行回调函数,再进行其他操作。再比如我们现在说的setTimeout()在指定的毫秒数后,将定时任务处理的函数添加到执行队列的队尾(不是指定的毫秒数后执行)

在匿名函数function(){console.log(i)}内部查找是否调用了i,结果是没有定义;因此他会向上查找,查找结果是已经定义了,并且i的值为5,所以每隔1秒出来一个5。

立刻同一时间蹦出来 0 1 2 3 4

        for(var i=0;i<5;i++){ 
            setTimeout((function(i){console.log(i)})(i),i*1000);
        }

我相信不少同学会说:自执行函数嘛~肯定是 0 1 2 3 4隔1秒出来一个数咯微笑

NONONO这个地方怎么会没坑?!答案是 0 1 2 3 4 同时出来这五个数 Σ(っ °Д °;)っ

setTimeout()第一个参数只接受Function或String,而你传入的自执行函数显然不是Function类型(自执行函数可以看作一个变量,它的值就是你的return值,它的类型就是你return的类型)解释器会把你传入的参数扔进eval()中进行执行。又因为你这个是一个自执行函数,所以会把你这个立即执行函数的运行结果(return值)当做代码扔到eval()中执行。

        for(var i=0;i<5;i++){ 
            setTimeout((function(i){
                return console.log(i);
            })(i),i*1000); 
        }

上面这种情况也是一样直接同一时间蹦出来0 1 2 3 4。由此可见自执行函数无法帮我们实现任务。

在这里引申出一个问题,如果我在这个自执行函数中直接return 'console.log(i)'

for(var i=0;i<5;i++){ 
    setTimeout((function(){
        return 'console.log(i)'
    })(i),i*1000); 
}

为什么出来的还是每隔1秒出来一个5呢?eval()的作用域为什么是全局的呢?

原因是:通过查看文档可知,现在只有全局对象有eval(),默认调用window.eval()

每隔1秒蹦出来0 1 2 3 4

那如果我们想每隔1秒蹦出来0 1 2 3 4的话最简单的改法是?

        for(var i=0;i<5;i++){ 
            setTimeout(function(i){console.log(i)},i*1000,i); 
        } 

setTimeout的第三个参数(可选),是传给执行函数的参数,所以我们可以不需要自执行函数,也可以获得无污染的i。

~~~~~~~~~~~~~~~~~~~2018年7月20日更新~~~~~~~~~~~~~~~~~~~

今天看到了一道今日头条的面试题,与上面的题很类似且包含了this指向问题,所以整理到一起:

如下题目存在哪些问题(改错)?

var obj = {  
    name: " jsCoder",  
    skill: ["css3","html5", "es6", "react", "angular"],  
    say: function () {        
        for(var i = 0, len = this.skill.length; i< len; i++){  
            setTimeout(function(){  
                console.log("No." + i + this.name);  
                console.log(this.skill[i]);  
                console.log('--------------------------');  
            },100);  
        }  
    }  
}  
obj.say();  

当for循环执行完毕才会将定时器内的函数于100毫秒之后加入队列,但此时所获取到的this指向window。我们需要获取指向obj的this,就是在for循环之前声明一个变量self=this,闭包虽然不可以访问this但是可以访问等于this的变量self。

在此提出一个例外

        var name = "The Window";
        var object = {
            name: "My Object",
            getName: function(){
                return this.name;
            }
        };
        console.log(object.getName());//My Object 
        console.log((object.getName = object.getName)());//The Window

因为object.getName = object.getName相当于给object的getName属性赋值,于是这段代码相当于

var _func = object.getName
_func()

所以this才会指向window~

执行环境

  1. 处于全局执行环境
  2. 执行A函数,进入A执行环境,A函数的执行环境就会被推入执行环境栈
  3. 执行B函数,进入B执行环境,B的执行环境被推入执行环境栈
  4. B函数执行完,退出B,B的执行环境被从这个栈的顶部删除
  5. A函数执行完,退出A,A的执行环境被从这个栈的顶部删除

作用域链

每个执行环境都有与之关联的变量对象作用域链

函数声明时都会给自己创建一个[[scope]]属性,会把当前的作用域链赋给[[scope]]属性。A函数的[[scope]]为【Global Object】(全局对象),接着A函数中有一个B函数,B函数的[[scope]]有A的【Activation Object】(A的活动对象)&【Global Object】(全局对象)。


函数执行时(例如A),会先创建一个A的活动对象【Activation Object】(这个对象包含了this、arguments、局部变量的定义、[[scope]]属性),A函数的[[scope]](也就是【Global Object】(全局对象))按顺序复制到[[scope chain]]里,然后把这个A的活动对象【Activation Object】推入到[[scope chain]]顶部。

环境中定义的变量和函数都会在其变量对象中,形参实参则会以属性-值的形式在变量对象中图从有名的大红书上扒的:

闭包的实现就是巧妙运用了作用域链,看下面这个函数:

createComparisonFunction函数返回之后他的作用域链会被销毁,但是由于返回的函数的作用域链中有createComparisonFunction的变量对象【Activation Object】。这也就能解释为什么闭包占用大量内存。只有返回的匿名函数被销毁,createComparisonFunction的变量对象才会被销毁。

异步调用与event loop

首先明确一点,JS是单线程的,同一个时间只能做一件事(微笑插一菊花:我就比较厉害了,能一边看动漫一边吃东西一边聊微信……

前一个任务完事了才能继续解决下一个,可是前一个要是特别慢呢,你要是有正当理由也就算了,偏偏你还在那悠哉游哉等数据传过来,从网络读数据好慢的好伐!于是机智的Javascript语言设计者决定所有任务可以开两种,一种放在主线程(同步任务),另一个放在任务队列(异步任务),只有任务队列通知主线程了,事件对应的回调函数才会进入主线程执行。

扒一张很经典的图,看图说话:

主线程运行的时候,产生heap堆和stack栈,栈中的代码是要执行的,执行完毕的代码就会弹出到堆中。此时就会读取Queue任务队列中的事件,依次执行事件所对应的回调函数,所以Queue也就是Event Queue。

Event Loop(事件循环)是 JavaScript 的运行环境(比如浏览器)提供的机制,和 JavaScript 语言本身没有关系!

所以异步事件一定会等着主线程的任务完成之后才会执行!

不难理解为什么i一定是5了吧~如果我们想实现蹦出1~5咋办捏?

        //方法1 通过setTimeout的第三个参数
        var obj1 = {  
            name: " jsCoder",  
            skill: ["css3","html5", "es6", "react", "angular"],  
            say: function () {  
                var self = this;     
                for(var i = 0, len = self.skill.length; i< len; i++){  
                    setTimeout(function(n){  
                        console.log("No." + (n+1) + self.name);  
                        console.log(self.skill[n]);  
                        console.log('--------------------------');  
                    },100,i);  
                }  
            }  
        } 
        //方法2 通过自执行函数
        var obj2 = {  
            name: " jsCoder",  
            skill: ["css3","html5", "es6", "react", "angular"],  
            say: function () { 
                var self = this;        
                for(var i = 0, len = self.skill.length; i< len; i++){  
                    setTimeout(function(n){  
                        console.log("No." + (n+1) + self.name);
                        console.log(self.skill[n]);  
                        console.log('--------------------------');  
                    }(i),100);  
                }  
            }  
        } 

完毕!

猜你喜欢

转载自blog.csdn.net/weixin_40322503/article/details/79928838
今日推荐