前端面试问题留存
- 汉德
-
- JS同步和异步问题
- js如何阻止事件冒泡
- (一)事件绑定的几种方式
- js 箭头函数和function的区别
- this指向及改变this指向的方法
- 图解JS闭包形成的原因
- JavaScript 闭包
- javascript-ES6块级作用域(笔记)
- 浏览器渲染原理与过程
- 从输入URL到页面加载发生了什么
- 值类型与引用类型及在内存中的存储
- 值类型与引用类型的区分
- typeof与instanceof区别
- JS数据类型
- JavaScript中三个等号和两个等号的区别(== 和 ===)浅析
- css下设置字体的em、rem、vw、vh字体单位详解
- css如何设置背景颜色透明?
- 标准盒模型和怪异盒模型的区别
- Window localStorage 属性
- CSS 伪元素
汉德
JS同步和异步问题
一、单线程
(1)单线程的概念
如果大家熟悉java,应该都知道,java是一门多线程语言,我们常常可以利用java的多线程处理各种各样的事,比如说文件上传,下载等,而JavaScript是否也可以支持多线程呢?
答案是否定的,JavaScript是一门单线程的语言,因此,JavaScript在同一个时间只能做一件事,单线程意味着,如果在同个时间有多个任务的话,这些任务就需要进行排队,前一个任务执行完,才会执行下一个任务,比如说下面这段代码
/ 同步代码
function fun1() {
console.log(1);
}
function fun2() {
console.log(2);
}
fun1();
fun2();
// 输出
1
2
很容易可以看出,输出会依次输入1,2,因为代码是从上到下依次执行,执行完fun1(),才继续执行fun2(),但是如果fun1()中的代码执行的是读取文件或者ajax操作,文件的读取和数据的获取都需要一定时间,难道我们需要完全等到fun1()执行完才能继续执行fun2()么?为了解决这个问题,后面我们会介绍同步和异步的概念
(2)为什么是单线程
其实,JavaScript的单线程,与它的用途是有很大关系,我们都知道,JavaScript作为浏览器的脚本语言,主要用来实现与用户的交互,利用JavaScript,我们可以实现对DOM的各种各样的操作,如果JavaScript是多线程的话,一个线程在一个DOM节点中增加内容,另一个线程要删除这个DOM节点,那么这个DOM节点究竟是要增加内容还是删除呢?这会带来很复杂的同步问题,因此,JavaScript是单线程的
二、同步任务和异步任务
(1)为什么会有同步和异步
因为JavaScript的单线程,因此同个时间只能处理同个任务,所有任务都需要排队,前一个任务执行完,才能继续执行下一个任务,但是,如果前一个任务的执行时间很长,比如文件的读取操作或ajax操作,后一个任务就不得不等着,拿ajax来说,当用户向后台获取大量的数据时,不得不等到所有数据都获取完毕才能进行下一步操作,用户只能在那里干等着,严重影响用户体验
因此,JavaScript在设计的时候,就已经考虑到这个问题,主线程可以完全不用等待文件的读取完毕或ajax的加载成功,可以先挂起处于等待中的任务,先运行排在后面的任务,等到文件的读取或ajax有了结果后,再回过头执行挂起的任务,因此,任务就可以分为同步任务和异步任务
(2)同步任务
同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务
(3)异步任务
异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程,当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务
function fun1() {
console.log(1);
}
function fun2() {
console.log(2);
}
function fun3() {
console.log(3);
}
fun1();
setTimeout(function(){
fun2();
},0);
fun3();
// 输出
1
3
2
有了异步,就算fun2()里面是文件的读取或ajax这种需要耗时的任务,也不怕fun3()要等到fun2()执行完才能执行啦
(4)异步机制
那么,JavaScript中的异步是怎么实现的呢?那要需要说下回调和事件循环这两个概念啦
首先要先说下任务队列,我们在前面也介绍了,异步任务是不会进入主线程,而是会先进入任务队列,任务队列其实是一个先进先出的数据结构,也是一个事件队列,比如说文件读取操作,因为这是一个异步任务,因此该任务会被添加到任务队列中,等到IO完成后,就会在任务队列中添加一个事件,表示异步任务完成啦,可以进入执行栈啦~但是这时候呀,主线程不一定有空,当主线程处理完其它任务有空时,就会读取任务队列,读取里面有哪些事件,排在前面的事件会被优先进行处理,如果该任务指定了回调函数,那么主线程在处理该事件时,就会执行回调函数中的代码,也就是执行异步任务啦
单线程从从任务队列中读取任务是不断循环的,每次栈被清空后,都会在任务队列中读取新的任务,如果没有任务,就会等到,直到有新的任务,这就叫做任务循环,因为每个任务都是由一个事件触发的,因此也叫作事件循环
总的来说,JavaScript的异步机制包括以下几个步骤:
(1)所有同步任务都在主线程上执行,行成一个执行栈 ;
(2)主线程之外,还存在一个任务队列,只要异步任务有了结果,就会在任务队列中放置一个事件 ;
(3)一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面还有哪些事件,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行 ;
(4)主线程不断的重复上面的第三步;
三、异步编程
那么,怎么才能实现异步编程,写出性能更好的代码呢,下面有几种方式
(1)回调函数
回调函数是实现异步编程最简单的方法啦,回调函数我们在使用ajax时应该用的很多啦,其实在使用ajax时,我们就用到了异步
1
2
3
4
var req = new XMLHttpRequest();
req.open(“GET”,url);
req.send(null);
req.onreadystatechange=function(){}
req.send()方法是 AJAX 向服务器发生数据,它是一个异步任务,而 req.onreadystatechange()属于事件回调,借由浏览器的HTTP请求线程发起对服务器的请求,在请求得到响应之后触发请求完成事件,将回调函数推入事件队列等待执行
其实像setTimeout,还有我们平时为元素绑定监听事件,和上面说的道理也是一样的
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数
(2)Promise
一直以来,JavaScript处理异步都是以callback的方式,在前端开发领域callback机制几乎深入人心,近几年随着JavaScript开发模式的逐渐成熟,CommonJS规范顺势而生,其中就包括提出了Promise规范,Promise完全改变了js异步编程的写法,让异步编程变得十分的易于理解,同时Promise也已经纳入了ES6,而且高版本的chrome、firefox浏览器都已经原生实现了Promise,只不过和现如今流行的类Promise类库相比少些API
Promise包括以下几个规范
一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换
promise必须实现then方法(可以说,then就是promise的核心),而且then必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致
then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用,同时,then可以接受另一个promise传入,也接受一个“类then”的对象或方法,即thenable对象
在使用Promise时,我们需要检测一些浏览器是否支持Promise
if(typeof(Promise)==="function") {
console.log("支持");
}
else {
console.log("不支持");
}
我们可以使用new Promise进行Promise的创建
function wait(time) {
return new Promise(function(resolve,reject) {
setTimeout(resolve,time);
});
}
这个时候我们就可以使用Promise处理异步任务啦
wait(1000).then(function(){
console.log(1);
})
上面这个例子表示1秒后输出1,同样的道理,我们可以使用Promise进行更加复杂的操作,关于更多的操作,就不继续说啦,关于异步的实现,其实还有其它的一些方法,但是因为上面说的这两种方法用的比较多,所以就只说上面这两种了。
js如何阻止事件冒泡
要阻止事件冒泡很简单,我们调用e.stopPropagation()方法就行了,其中e就是事件参数。
(一)事件绑定的几种方式
javascript给DOM绑定事件处理函数总的来说有2种方式:在html文档中绑定、在js代码中绑定。下面的方式1、方式2属于在html中绑定事件,方式3、方式4和方式5属于在js代码中绑定事件,其中方法5是最推荐的做法。
方式1:
HTML的DOM元素支持onclick、onblur等以on开头属性,我们可以直接在这些属性值中编写javascript代码。当点击div的时候,下面的代码会弹出div的ID:
这种做法很显然不好,因为代码都是放在字符串里的,不能格式化和排版,当代码很多的时候很难看懂。这里有一点值得说明:onclick属性中的this代表的是当前被点击的DOM对象,所以我们可以通过this.id获取DOM元素的id属性值。
方式2:
当代码比较多的时候,我们可以在onclick等属性中指定函数名。
1
2
3
4
5
6
7
8
9
10
方式3:在JS代码中通过dom元素的onclick等属性
1
2
3
var dom = document.getElementById(“outestA”);
dom.onclick = function(){alert(“1=” + this.id);};
dom.onclick = function(){alert(“2=” + this.id);};
这种做法this代表当前的DOM对象。还有一点:这种做法只能绑定一个事件处理函数,后面的会覆盖前面的。
方式4:IE下使用attachEvent/detachEvent函数进行事件绑定和取消。
attachEvent/detachEvent兼容性不好,IE6~IE11都支持该函数,但是FF和Chrome浏览器都不支持该方法。而且attachEvent/detachEvent不是W3C标准的做法,所以不推荐使用。在IE浏览器下,attachEvent有以下特点。
a) 事件处理函数中this代表的是window对象,不是dom对象。
1
2
3
4
5
6
7
var dom = document.getElementById(“outestA”);
dom.attachEvent(‘onclick’,a);
function a()
{
alert(this.id);//undefined
}
b) 同一个事件处理函数只能绑定一次。
1
2
3
4
5
6
7
var dom = document.getElementById(“outestA”);
dom.attachEvent(‘onclick’,a);
dom.attachEvent(‘onclick’,a);
function a()
{
alert(this.id);
}
虽然使用attachEvent绑定了2次,但是函数a只会调用一次。
c)不同的函数对象,可以重复绑定,不会覆盖。
1
2
3
var dom = document.getElementById(“outestA”);
dom.attachEvent(‘onclick’,function(){alert(1);});
dom.attachEvent(‘onclick’,function(){alert(1);}); // 当outestA的click事件发生时,会弹出2个对话框
匿名函数和匿名函数是互相不相同的,即使代码完全一样。所以如果我们想用detachEvent取消attachEvent绑定的事件处理函数,那么绑定事件的时候不能使用匿名函数,必须要将事件处事函数单独写成一个函数,否则无法取消。
方式5:使用W3C标准的addEventListener和removeEventListener。
这2个函数是W3C标准规定的,FF和Chrome浏览器都支持,IE6/IE7/IE8都不支持这2个函数。不过从IE9开始就支持了这2个标准的API。
1
2
3
4
5
// type:事件类型,不含"on",比如"click"、“mouseover”、“keydown”;
// 而attachEvent的事件名称,含含"on",比如"onclick"、“onmouseover”、“onkeydown”;
// listener:事件处理函数
// useCapture是事件冒泡,还是事件捕获,默认false,代表事件冒泡类型
addEventListener(type, listener, useCapture);
a) 事件处理函数中this代表的是dom对象,不是window,这个特性与attachEvent不同。
1
2
3
4
5
6
7
var dom = document.getElementById(“outestA”);
dom.addEventListener(‘click’, a, false);
function a()
{
alert(this.id);//outestA
}
b) 同一个事件处理函数可以绑定2次,一次用于事件捕获,一次用于事件冒泡。
1
2
3
4
5
6
7
8
var dom = document.getElementById(“outestA”);
dom.addEventListener(‘click’, a, false);
dom.addEventListener(‘click’, a, true);
function a()
{
alert(this.id);//outestA
}// 当点击outestA的时候,函数a会调用2次
如果绑定的是同一个事件处理函数,并且都是事件冒泡类型或者事件捕获类型,那么只能绑定一次。
1
2
3
4
5
6
7
8
9
10
var dom = document.getElementById(“outestA”);
dom.addEventListener(‘click’, a, false);
dom.addEventListener(‘click’, a, false);
function a()
{
alert(this.id);//outestA
}
// 当点击outestA的时候,函数a只会调用1次
c) 不同的事件处理函数可以重复绑定,这个特性与attachEvent一致。
(二)事件处理函数的执行顺序
方式1、方式2和方式3都不能实现事件的重复绑定,所以自然也就不存在执行顺序的问题。方式4和方式5可以重复绑定特性,所以需要了解下执行顺序的问题。如果你写出依赖于执行顺序的代码,可以断定你的设计存在问题。所以下面的顺序问题,仅作为兴趣探讨,没有什么实际意义。直接上结论:addEventListener和attachEvent表现一致,如果给同一个事件绑定多个处理函数,先绑定的先执行。下面的代码我在IE11、FF17和Chrome39都测试过。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(三) 事件冒泡和事件捕获
事件冒泡和事件捕获很好理解,只不过是对同一件事情的不同看法,只不过这2种看法都很有道理。
我们知道HTML中的元素是可以嵌套的,形成类似于树的层次关系。比如下面的代码:
1
2
3
4
5
事件捕获见下图:
一般来说事件冒泡机制,用的更多一些,所以在IE8以及之前,IE只支持事件冒泡。IE9+/FF/Chrome这2种模型都支持,可以通过addEventListener((type, listener, useCapture)的useCapture来设定,useCapture=false代表着事件冒泡,useCapture=true代表着采用事件捕获。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(四) DOM事件流
DOM事件流我也不知道怎么解释,个人感觉就是事件冒泡和事件捕获的结合体,直接看图吧。
DOM事件流:将事件分为三个阶段:捕获阶段、目标阶段、冒泡阶段。先调用捕获阶段的处理函数,其次调用目标阶段的处理函数,最后调用冒泡阶段的处理函数。这个过程很类似于Struts2框中的action和Interceptor。当发出一个URL请求的时候,先调用前置拦截器,其次调用action,最后调用后置拦截器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(五) 再谈事件函数执行先后顺序
在DOM事件流中提到过:
// 目标(自身触发事件,是冒泡还是捕获无所谓)
outC.addEventListener(‘click’,function(){alert(“target”);},true);
我们在outC上触发onclick事件(这个是目标对象),如果我们在outC上同时绑定捕获阶段/冒泡阶段事件处理函数会怎么样呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1
2
3
4
5
// 目标(自身触发事件,是冒泡还是捕获无所谓)
outC.addEventListener(‘click’,function(){alert(“target1”);},false);
outC.addEventListener(‘click’,function(){alert(“target2”);},true);
outC.addEventListener(‘click’,function(){alert(“target3”);},true);
outC.addEventListener(‘click’,function(){alert(“target4”);},false);
至此我们可以给出事件函数执行顺序的结论了:捕获阶段的处理函数最先执行,其次是目标阶段的处理函数,最后是冒泡阶段的处理函数。目标阶段的处理函数,先注册的先执行,后注册的后执行。
(六) 阻止事件冒泡和捕获
默认情况下,多个事件处理函数会按照DOM事件流模型中的顺序执行。如果子元素上发生某个事件,不需要执行父元素上注册的事件处理函数,那么我们可以停止捕获和冒泡,避免没有意义的函数调用。前面提到的5种事件绑定方式,都可以实现阻止事件的传播。由于第5种方式,是最推荐的做法。所以我们基于第5种方式,看看如何阻止事件的传播行为。IE8以及以前可以通过 window.event.cancelBubble=true阻止事件的继续传播;IE9+/FF/Chrome通过event.stopPropagation()阻止事件的继续传播。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
最后再看一段更有意思的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
js 箭头函数和function的区别
JS箭头函数和function的区别:
箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
箭头函数不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
箭头函数不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
不可以使用yield命令,因此箭头函数不能用作Generator函数。
this指向及改变this指向的方法
https://blog.csdn.net/xuehangongzi/article/details/80841167
图解JS闭包形成的原因
https://blog.csdn.net/weixin_33712987/article/details/88962919
JavaScript 闭包
https://www.runoob.com/js/js-function-closures.html
javascript-ES6块级作用域(笔记)
https://zhuanlan.zhihu.com/p/158901768
浏览器渲染原理与过程
https://www.jianshu.com/p/b6b42fd3f80e
从输入URL到页面加载发生了什么
https://segmentfault.com/a/1190000006879700
值类型与引用类型及在内存中的存储
https://blog.csdn.net/maladoufu/article/details/8270509
值类型与引用类型的区分
http://www.360doc.com/content/18/1224/18/13328254_804168072.shtml
typeof与instanceof区别
https://www.jianshu.com/p/4ff2332228be
JS数据类型
https://segmentfault.com/a/1190000005913731
JavaScript中三个等号和两个等号的区别(== 和 ===)浅析
https://www.jb51.net/article/93243.htm
css下设置字体的em、rem、vw、vh字体单位详解
https://blog.csdn.net/suny2020/article/details/103005598
css如何设置背景颜色透明?
http://www.divcss5.com/css3-style/c56734.shtml
标准盒模型和怪异盒模型的区别
https://www.jianshu.com/p/7dadcc458410
Window localStorage 属性
https://www.runoob.com/jsref/prop-win-localstorage.html
CSS 伪元素
https://www.w3school.com.cn/css/css_pseudo_elements.asp