资讯专栏INFORMATION COLUMN

由 for 循环经典面试题延伸的 js 相关知识

justjavac / 3164人阅读

摘要:经过上网友的点拨,这个问题涉及到中的两个问题,作用域链和事件执行机制。常见的异步任务有定时器和事件回调等。异步任务三张图片的事件依次触发,回调函数进入任务队列,等主线程的循环执行完毕之后,依次执行这三个任务。

之前工作中碰到一个需求,需要根据从后台获取到的图片路径获得这些图片的 base64 文件。实现过程中遇到一个问题,代码如下:

var src=["http://www.w3school.com.cn/i/site_photoref.jpg",
    "http://www.w3school.com.cn/i/site_photoexa.jpg",
    "http://www.w3school.com.cn/i/site_photoqe.jpg"] 
for(var i=0;i<3;i++){
    var img=new Image();
    img.src=src[i];
    img.onload=function(){
        console.log(img)   
        //最终打印出来都是最后一个图片
        //
        //
        //
    }
}

这个问题其实跟之前经常碰到的一个面试题本质上是一致的。我们可以在上面函数中打印索引值,会发现打印出来的值都是3。经过 segmentfault 上网友的点拨,这个问题涉及到 js 中的两个问题,作用域链事件执行机制

作用域链

在 js 中,每个函数都有自己的执行环境,每个执行环境都有一个与之关联的变量对象。当代码在一个环境中执行时,会创建变量对象的一个作用域链。每个作用域链的起点都是当前执行代码所在的执行环境的变量对象,作用域链的下一个变量对象来自包含环境,一直延续到全局执行环境(浏览器中是指 window 对象)。
如果执行环境是函数,那么它的变量对象就包括活动对象,活动对象在一开始只包括 arguments 对象(函数的参数对象)。

当执行环境中要用到某个变量或者函数时,会从自己作用域链的起点也就是自己的变量对象中开始搜索相应的变量名或者函数名,如果搜索不到就接着在作用域链的上一级搜索,一直到找到相关变量名或者到作用域链的末尾为止。

js 事件执行机制

js 是一种单线程语言,在主线程中同一时间只能执行一个任务。

浏览器内核线程

浏览器内核是多线程的,通常包含以下线程:

GUI 渲染线程:
负责渲染网页,当页面需要重绘时,该线程就会执行。

JavaScript 引擎线程:
也就是 JS 内核,负责解析和运行 JS 代码。

定时器触发器线程:
通过这个线程计时来确定什么时候触发定时器。

事件触发线程:
监控某个事件是否触发,事件触发之后会被添加到任务队列中。

异步 HTTP 请求线程:
监控 AJAX 的状态变更时,就会把相应的任务添加到任务队列中。

同步和异步

js 中每个任务的操作可以简化为发起调用获得结果两步,根据这两步可以把js 中的任务可以分为同步任务和异步任务。所有任务的执行都在主线程进行。
同步任务:发起调用之后,立即就会执行来获取结果的任务。调用之后会一直等待直到返回结果,在这期间主线程不能进行其他操作。
异步任务:发起调用之后,并不会立即执行相关函数,而是需要额外的操作满足相关条件之后进行触发。相关任务被触发之后会进入任务队列等待主线程任务执行完成后按顺序进入主线程,调用和执行之间的时间可以介入其他异步任务。常见的异步任务有定时器、ajax和事件回调等。

事件循环机制(event loop)

js 中事件执行基本按照下面这三步进行循环。

主线程先按照代码顺序执行同步任务

在异步任务被注册之后,浏览器的其他线程(事件触发线程、定时器触发线程、异步 HTTP 请求线程)监控异步任务的触发条件,按照触发顺序把这些异步任务放在任务队列中

主线程上同步任务执行完之后,会依次执行任务队列中的任务

回到开头

在最上面的例子中,for 循环是同步任务,会立即执行,图片的 onload 事件是异步任务,需要等另行触发。这个例子中代码的执行顺序是这样:

同步任务:循环创建三张图片,每个图片赋予各自的 src 值,并且都注册了一个 onload 事件。
异步任务:三张图片的 onload 事件依次触发,回调函数进入任务队列,等主线程的 for 循环执行完毕之后,依次执行这三个任务。

当开始执行异步任务时,每个函数都需要用到 img 这个变量,就开始在自己的作用域链上开始寻找 img,自身变量对象中不存在,接着在包含环境中找到,由于 for 循环并不会创造一个新的执行环境,所以这个例子中包含环境其实就是全局执行环境。而在 for 循环完之后,img 变量的值已经经过两次覆盖变成了最后一个索引对应的图片。所以每个图片的 onload 函数都会打印出同一个 img 。打印 i 值出现的结果也是一样。

解决办法

弄清楚出现这个问题的原因,解决这个问题可以用下面的办法:

方法1:创建多带带的执行环境
for(var i=0;i<3;i++){
    (function(index){   
        var img=new Image();
        img.src=src[i];
        img.onload=function(){
            console.log(index)
            console.log(img)   
        }
    })(i)
}

这个方法实现的原理是:for 循环中立即执行函数每次都会创建一个新的执行环境,三张图片的 onload 事件函数的作用域链的包含环境分别是这三个立即执行函数,这三个立即执行函数里面保存的是不同的 img 变量和不同的参数 index,当三个 onload 回调函数执行时,分别在自己的作用域链上寻找各自对应的 img 变量。利用同样的原理,也可以写成这样:

for(var i=0;i<3;i++){
    var img=new Image();
    img.src=src[i];
    img.onload=function(index,img){
        return function(){
            console.log(index)
            console.log(img) 
        }  
    }(i,img)
}

也可以不通过传参,而是在立即执行函数内部创建一个变量来接收每次循环中的 i 值,原理都是一样的。

方法2:访问事件触发节点
for(var i=0;i<3;i++){
    var img=new Image();
    img.src=src[i];
    img.onload=function(){
        console.log(this)   
    }
}

这个方法的原理是:函数内部在执行过程中会有一个默认的 this 变量会把函数的调用对象保存起来,通过函数内部的 this 就可以访问调用函数的对象。或者可以通过 event 事件对象的 currentTarget 属性访问到事件触发节点,原理是一样的。

方法3:ES6 的新语法 let
for(let i=0;i<3;i++){
    let img=new Image();
    img.src=src[i];
    img.onload=function(){
        console.log(img)   
        console.log(i)
    }
}

在 ES6 中规定了一个新的变量声明命令 let,let 会创建一个块级作用域,用 let 声明的变量只在 let 所在的代码块中有效。这个例子中,三次循环会创建三个块级作用域,每个块级作用域中有各自的变量 i 和 img,互相独立,每个 onload 回调函数执行时都会获取各自代码块中的 i 和 img ,最终能实现我们想要的结果。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82587.html

相关文章

  • 8道经典JavaScript面试解析,你真掌握JavaScript了吗?

    摘要:浏览器的主要组成包括有调用堆栈,事件循环,任务队列和。好了,现在有了前面这些知识,我们可以看一下这道题的讲解过程实现步骤调用会将函数放入调用堆栈。由于调用堆栈是空的,事件循环将选择回调并将其推入调用堆栈进行处理。进程再次重复,堆栈不会溢出。 JavaScript是前端开发中非常重要的一门语言,浏览器是他主要运行的地方。JavaScript是一个非常有意思的语言,但是他有很多一些概念,大...

    taowen 评论0 收藏0
  • Deep in JS - 收藏集 - 掘金

    摘要:今天同学去面试,做了两道面试题全部做错了,发过来给道典型的面试题前端掘金在界中,开发人员的需求量一直居高不下。 排序算法 -- JavaScript 标准参考教程(alpha) - 前端 - 掘金来自《JavaScript 标准参考教程(alpha)》,by 阮一峰 目录 冒泡排序 简介 算法实现 选择排序 简介 算法实现 ... 图例详解那道 setTimeout 与循环闭包的经典面...

    enali 评论0 收藏0
  • 程序语言

    摘要:一面应该还问了其他内容,但是两次面试多线程面试问题和答案采访中,我们通常会遇到两个主题采集问题和多线程面试问题。多线程是关于并发和线程的。我们正在共享重要的多线程面试问题和答案。。 2016 年末,腾讯,百度,华为,搜狗和滴滴面试题汇总 2016 年未,腾讯,百度,华为,搜狗和滴滴面试题汇总 【码农每日一题】Java 内部类(Part 2)相关面试题 关注一下嘛,又不让你背锅!问:Ja...

    mtunique 评论0 收藏0
  • 程序语言

    摘要:一面应该还问了其他内容,但是两次面试多线程面试问题和答案采访中,我们通常会遇到两个主题采集问题和多线程面试问题。多线程是关于并发和线程的。我们正在共享重要的多线程面试问题和答案。。 2016 年末,腾讯,百度,华为,搜狗和滴滴面试题汇总 2016 年未,腾讯,百度,华为,搜狗和滴滴面试题汇总 【码农每日一题】Java 内部类(Part 2)相关面试题 关注一下嘛,又不让你背锅!问:Ja...

    stefan 评论0 收藏0
  • 用9种办法解决 JS 闭包经典面试for 循环取 i

    摘要:闭包正确的说应该是指一个闭包域每当声明了一个函数它就产生了一个闭包域可以解释为每个函数都有自己的函数栈每个闭包域对象都有一个不是属性内默认有个名为的全局引用有了这个引用就可以直接调用的属性或方法凡是在闭包域内声明的变量或方法外部无法直接访问 闭包 正确的说,应该是指一个闭包域,每当声明了一个函数,它就产生了一个闭包域(可以解释为每个函数都有自己的函数栈),每个闭包域(Function...

    Betta 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<