2019前端面试总结
HTML面试题
- 你是如何理解HTML语义化的?
- 比较简单的回答:我理解的语义化就是 标签用在合适的位置,比如段落要用
p
标签,标题要用h1-h6
标签. - 更细点的回答:我理解的HTML语义化是正确的标签做正确的事情,能够便于开发者阅读和写出更优雅的代码的同时,让网络爬虫更好的解析。
- 为什么要做到语义化?
- 有利于SEO,有利于搜索引擎爬虫更好的解析我们的页面,从而获取更多的有效信息,提升网页的权重。
- 在没有CSS的时候,能够清晰看出网页的结构,增强可读性。
- 便于团队合作开发和维护,提高开发效率
-
<!DOCTYPE> 文档声明,它不是HTML标签,是一个指示web浏览关于页面使用哪个HTML版本编写的指令。<!DOCTYPE> 声明必须位于文档的第一行,位于<html>标签之前。
<!DOCTYPE html>
-
<html lang='en'>lang属性设定文档语言。
作用:SEO搜索引擎优化;有助于阅读障碍人士,通过读屏器阅读页面
还可以是
<html lang="zh-CN">
5.meta标签的几种用法。
- meta指定文档编码
//这行代码的意思是,文档用UTF-8编码的,浏览器解析的时候用UTF-8编码解析。
<meta charset="UTF-8">
- 适配移动页面
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
- 添加页面描述
<meta name="description" content="腾讯网(www.qq.com)是中国最浏览量最大的门户网站">
6.你用过哪些 HTML5标签。
内容性的标签:
<header> 网页的头部
<nav> 网页的导航
<section> 标签定义文档中的节(比如章节、页眉、页脚或文档中的其他部分。)
<article> 标签的内容独立于文档的其余部分。比如外部的一篇文章,一个博客,论文等。
<aside> 网页侧边栏
<footer> 网页的页脚
功能性的标签
<canvas> 通过脚本绘制图像
<Audio> 播放音频
<Video> 播放视频
- 什么是H5?
H5是中国人制造的一个专有名词
实际上是指移动端页面,从某种意义上来说它是 HTML5,微信网页,移动PPT的母级。
CSS面试题
- 两种盒模型。
盒模型一共有两种模式:
- 标准盒模型
标准盒模型下 盒子的总宽度/高度=width/height+padding+border+margin,标准盒模型下 width是指content的宽度 - 怪异盒模型
怪异盒模型下 盒子的总宽度/高度=width/height+margin,怪异盒模型下 width 指的是内容content+padding+border的宽度------IE6,7,8 不设置 <!DOCTYPE html> 情况下,是怪异盒模型,如果加了就是标准盒模型 - 具体是使用哪一种盒模型,用box-sizing来设置
- 兼容性: box-sizing现代浏览器都支持,但IE家族只有IE8版本以上才支持
box-sizing:content-box;
box-sizing:border-box;
目前使用此属性,需要加前缀,Mozilla需要加上-moz-,Webkit内核需要加上-webkit-,Presto内核-o-,IE8-ms-
-webkit-box-sizing:content-box;
-moz-box-sizing:birder-box;
标准盒模型的缺点是,盒子的宽度和高度计算复杂(两子元素并排例子)
怪异盒模型的优点,当确定了width/height之后,可以随意修改padding和border的厚度值,而不用担心父容器被撑爆。
- 如何实现水平和垂直居中?
- 元素水平居中
//行内元素
text-align:center
//块级元素
margin-left: auto;
margin-right:auto;
- 元素垂直居中
//方案1 position-----元素固定宽高的情况下
<div class="out">
out
<div class="in">in</div>
</div>
<style>
.out{
width:300px;
height:300px;
background:#ccc;
color:red;
position:relative;
}
.in{
width:100px;
height:100px;
background:pink;
color:blue;
position:absolute;
left:50%;
top:50%;
margin-left:-50px;
margin-top:-50px;
}
</style>
image.png
//方案2 元素宽高不固定的情况下,把上边的 margin-left:(宽度/2);
margin-top:(高度/2) 换成 transform:translate(-50%,-50%);
<div class="out">
out
<div class="in">测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测</div>
</div>
<style>
.out{
width:300px;
height:300px;
background:#ccc;
color:red;
position:relative;
}
.in{
background:pink;
color:blue;
position:absolute;
left:50%;
top:50%;
transform:translate(-50%,-50%);
}
</style>
image.png
//方案3 flex
<div class="out">
<div class="in"></div>
</div>
<style>
.out{
width:300px;
height:300px;
background:#ccc;
display:flex; //flex布局
justify-content:center; //规定项目在主轴上居中对齐(水平居中)
align-items:center; //规定项目在交叉轴上居中对齐(垂直居中)
}
.in{
width:100px;
height:100px;
background:pink;
}
</style>
image.png
//方案4 table-cell
给父元素设置 display:table; 父元素变成块级表格元素(相当于table);
给子元素设置 display:table-cell;使子元素变成表格单元格(相当于td),然后设置 vertical-align:center; text-align:center;
<div class="out">
<div class="in">哈哈哈哈哈哈哈哈哈哈</div>
</div>
<style>
.out{
width:300px;
height:300px;
background:#ccc;
display:table;
}
.in{
background:pink;
display:table-cell;
vertical-align:middle;
text-align:center;
}
</style>
image.png
- flex怎么用?常用的属性有哪些?
flex弹性布局,为盒模型提供最大的灵活性,任何一个容器,都可以指定为flex布局;
.box{
display:flex;
}
//行内元素也可以设置为flex
.box{
display:inline-flex;
}
注意设为flex布局后,子元素的float,clear,vertical-align属性将失效。
采用flex布局的元素,成为flex容器。它的所有子元素自动成为容器成员,称为flex项目。
常用的,设置到容器上的属性有:
1.flex-direction:属性决定主轴的方向(即项目的排列方向)。
.box{
flex-direction:row | row-reverse | column | column-reverse
}
--------------------------------------------------------------------------------
2. flex-wrap:默认情况下,项目都排在一条线上。这个属性定义,如果一条轴线拍不下,如何换行。
.box{
flex-wrap:nowrap | wrap | wrap-reverse
}
--------------------------------------------------------------------------------
3. justify-content:属性定义了项目在主轴上的对齐方式。
.box{
justify-content:flex-start | flex-end | center | space-between | space-around
}
--------------------------------------------------------------------------------
4.align-items:属性定义项目在交叉轴上如何对齐
.box{
align-items:flex-start | flex-end | center | baseline | stretch
}
baseline 项目的第一行文字的基线对齐
stretch(默认值) 如果项目未设置高度或设为auto,将占满整个容器的高度。
--------------------------------------------------------------------------------
5. align-content:属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。
.box {
align-content: flex-start | flex-end | center | space-between | space-around | stretch;
}
--------------------------------------------------------------------------------
6. flex-flow 是flex-direction和 flex-wrap的简写,默认是 row nowrap
设置到项目上的属性:
1.order 属性定义项目的排列顺序。数值越小,排列越靠前。默认为0
.item {
order: 1;
}
2.flex-grow属性定义项目的放大比例,默认为0.即如果有剩余空间,也不放大。
.item {
flex-grow: <number>; /* default 0 */
}
3.flex-shrink 属性定义了项目的缩小比例。默认为1,如果空间不足,项目将缩小。
.item {
flex-shrink: <number>; /* default 1 */
}
如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。
负值对该属性无效。
4. flex-basis 属性定义了在分配多余空间之前,项目占据的主轴空间。默认为auto.即项目本来的大小。它可以设为跟width或height属性一样的值(比如350px),则项目将占据固定空间。
.item {
flex-basis: <length> | auto; /* default auto */
}
5. flex 属性是flex-grow,flex-shrink,flex-basis,默认值为0 1 auto
该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。
建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值。
6.align-self 属性 允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性,默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。
.item {
align-self: auto | flex-start | flex-end | center | baseline | stretch;
}
- BFC是什么?
(Block Formatting Context)块级格式化上下文。BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之也如此.并且在一个BFC中,块盒与行盒(行盒由一行中所有的内联元素所组成)都会垂直的沿着其父元素的边框排列。
这个可以具体回答,比如给一个div设置 overflow:hidden 它里边的浮动元素就会被包裹起来。
如何触发BFC?
1. 根元素 即HTML元素
2. float的值,不为none
3. overflow的值,不为visible
4. display的值,为inline-block,table-cell ,table-caption
5. position 的值,为absolute 或fixed
- 选择器的优先级?
CSS2的时候 (如果面试官更倾向于这个就这样回答)
!important 优先级最高 权值无穷大
style 写在元素行的内联样式其次 权值1000
id 权值100
class/伪类/属性 权值10
标签选择器 权值1
通配符 权值0
更准确的说法
1.越具体,优先级越高
2.写在后面的,会覆盖写在前面的
3.important优先级最高,但是要少用
6.清除浮动的方法:
方法
1:给父元素设置高度
2:给父元素设置overflow:hidden
3: 最佳实践
.clearfix::after{
content:'';
display:block;
clear:both;
}
.clearfix加到容器上,里边的子元素浮动就被清除了
原生JS面试题
1. ES6语法知道哪些?分别怎么用
A-------新增声明命令 let 和 const, let表示变量 const表示常量
特点:
1. 都是块级作用域,以{}代码块作为作用域范围,只能在代码块里面使用。
2. 不存在变量提升,只能先声明再使用,否则会报错。在代码块内,在声明变量之前,该变量都是不可用的,这在语法上成为'暂时性死区'
3. 在一个代码块内,不允许重复声明
4. const声明的是一个只读常量,在声明的时候就要赋值,否则会摆错,(如果const声明的是一个对象,对象所包含的值是可以修改的,抽象一点说,对象所指的地址是不能够改变的,变量成员是可以修改的)
B--------模板字符串
用一对反引号(``)标识,它可以当做普通字符串使用,也可以用来定义多行字符串。也可以在字符串中嵌入变量,JS表达式,或函数。需要写在${}中
var str = `abc
def
gh`;
console.log(str);
let name = '明';
function a(){
return 'ming'
}
console.log(`我的名字叫做${name},年龄${17+5}岁,我的性别是${'男'},游戏ID:${a()})
C------函数的扩展
- 函数的默认参数
ES6为参数提供了默认值,在定义函数的时候,便初始化了这个参数,以便在参数没有被传递进去的时候使用。
function A(a,b=1){
console.log(a+b)
console.log(a);
console.log(b);
}
A(1); // 2, 1,1
A(2,3); //5,2,3
- 箭头函数
ES6中提供了一种简洁的函数的写法,箭头函数。
//只有一个参数时候,可以省略参数的括号。
//如果没有参数,或者参数在2个及两个以上,必须带括号
//当代码只有一行,并且有立即返回值的时候,可以省略花括号{}
var f = a => a+1
var sayHi = ()=>{
console.log('hi')
}
var sum = (num1,num2)=>{
console.log(num1+mun2)
}
箭头函数的特点
箭头函数的this是在定义函数的时候绑定,而不是在执行函数的时候绑定。
所谓在定义函数的时候绑定的意思是,箭头函数的this是继承自父执行上下文。箭头函数没有自己的this.
D---------对象的扩展
- 属性的简写
ES6允许在对象之中,直接写变量,这时属性名为变量名,属性的值为变量的值。
var foo = 'bar';
var baz = {foo}; //相当于 var bar={foo:foo}
- 方法的简写
省略冒号和function关键字
var o = {
sayHi:function(){
console.log('hi')
}
}
相当于
var o = {
sayHi(){
console.log('hi')
}
}
- Object.keys()方法,获取对象的所有属性名和方法名。不包括原型的内容,返回一个数组
var person = {name:"john",age:20,study(){alert('study')}};
console.log(Object.keys(person)) // ["name", "age", "study"]
console.log(Object.keys(['aa','bb','cc']); //["0", "1", "2"]
console.log(Object.keys('abcdef')); //["0", "1", "2", "3", "4", "5"]
- Object.assign()方法
这个方法将多个原对象的属性和方法,合并到了目标对象上面。
可以接收多个参数,第一个参数是目标对象,后边都是源对象
var target ={}
var obj1 = {
name:'petter',
age:20,
sex:'女'
}
var obj2 = {
sex:'男',
score:100
}
Object.assign(target,obj1,obj2);
console.log(target);
//{age: 20,name: "petter", score: 100, sex: "男"}
E---------- for...of循环
是遍历所有数据结构的统一方法。for...of循环可以使用的范围包括数组,Set,Map结构,某写类数组对象(arguments, DOM NodeList对象)以及字符串。
var arr = ["水星","金星","地球","火星"];
for(var s of arr){
console.log(s); //"水星" "金星" "地球" "火星"
}
F--------import 和 export
ES6标准中,JavaScript 原生支持模块(module)了,这种将JS代码分割成不同功能的小块进行模块化,将不同功能的代码分别写在不同的文件中,各模块只需要导出公共接口部分,然后通过模块的导入方式可以在其他地方使用。
export 用于对外输出本模块(一个文件可以看做是一个模块)的变量接口
import 用于在一个模块中加载另外一个含有export接口的模块
import和export命令只能在模块的顶部,不能在代码块之中。
G-----Promise对象
Promise是异步编程的一种解决方案,将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
它有三种状态,分别是 pending-进行中,resolved-已完成,rejected-已失败
Promise构造函数接收一个参数,这个参数是函数,并且这个函数传入两个参数,分别是 resolve 和 reject,分别是异步操作执行成功后的回调函数,和异步操作执行失败后的回调函数。(按照标准来讲,resolve是将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected。)
所以我们用Promise的时候一般是包在一个函数中,在需要的时候去运行这个函数,如:
function runAsync(){
var p = new Promise((resolve,reject)=>{
setTimeout(function(){
console.log('执行完成');
resolve('随便什么数据')
},2000)
})
return p;
}
runAsync();
在我们包装好的函数最后,会return出Promise对象,也就是说,执行这个函数我们得到了一个Promise对象。
Promise对象上有then 和 catch方法。
runAsync().then(function(data){
console.log(data) //'随便什么数据'
//后面可以用传过来的数据做些其他操作
//......
})
在runAsync()的返回上直接调用then方法,then接收一个参数,是函数,并且会拿到我们在runAsync中调用resolve时传的的参数。
这时候你应该有所领悟了,原来then里面的函数就跟我们平时的回调函数一个意思,能够在runAsync这个异步任务执行完成之后被执行。这就是Promise的作用了,简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。
而Promise的优势在于,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。
链式操作的用法
从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。所以使用Promise的正确场景是这样的:
function runAsync1(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('异步执行1完成');
resolve('随便什么数据1');
}, 2000);
});
return p;
}
function runAsync2(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('异步执行2完成');
resolve('随便什么数据2');
}, 2000);
});
return p;
}
function runAsync3(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('异步执行3完成');
resolve('随便什么数据3');
}, 2000);
});
return p;
}
runAsync1()
.then(function(data){
console.log(data);
return runAsync2()
})
.then(function(data){
console.log(data);
return runAsync3()
})
.then(function(data){
console.log(data)
})
//2秒后打印
"异步执行1完成"
"随便什么数据1"
//又2秒后打印
"异步执行2完成"
"随便什么数据2"
//又2秒后打印
"异步执行3完成"
"随便什么数据3"
在then方法中,可以直接return 数据,而不是Promise对象,在后边的then中就可以接受到数据了。
runAsync1()
.then(function(data){
console.log(data);
return runAsync2()
})
.then(function(data){
console.log(data);
return "直接返回数据"
})
.then(function(data){
console.log(data)
})
//2秒后打印
"异步执行1完成"
"随便什么数据1"
//又2秒后打印
"异步执行2完成"
"随便什么数据2"
"直接返回数据"
reject的用法
我们前面的例子都是只有“执行成功”的回调,还没有“失败”的情况
reject就是把Promise的状态设置为 rejected,这样我们在then中就能捕捉到,然后执行失败情况的回调。
function getNumber(){
return new Promise((resolve,reject)=>{
setTimeout(function(){
var num = Math.ceil(Math.random()*10) //生成0-10之间的整数
if(num<=5){
resolve(num)
}else{
reject('数字太大了')
}
},2000)
})
}
getNumber()
.then(
function(data){
console.log('resolved')
console.log(data);
},
function(reason,data){
console.log('rejected')
console.log(reason)
}
)
getNumber函数用来异步获取一个数字,2秒后执行完成,如果数字小于等于5,我们认为是“成功”了,调用resolve修改Promise的状态。否则我们认为是“失败”了,调用reject并传递一个参数,作为失败的原因。 运行getNumber并且在then中传了两个参数,then方法可以接受两个参数,第一个对应resolve的回调,第二个对应reject的回调。所以我们能够分别拿到他们传过来的数据
catch 的用法
我们知道 Promise除了有一个then方法,还有一个catch方法,它是干什么的呢?
其实它跟then方法的第二个参数的作用是一样的。用来指定reject的回调。
它的用法:
getNumber()
.then(
function(data){
console.log('resolved')
console.log(data);
}
)
.catch(
function(reason){
console.log('rejected')
console.log(reason)
}
)
效果和写在then的第二个参数里面一样。不过它还有另外一个作用,在执行resolve的回调(也就是then方法的第一个参数)时,如果抛出异常(代码出错了),那么并不会报错卡死JS,而是会进入到catch方法中
getNumber()
.then(function(data){
console.log('resolved');
console.log(data);
console.log(somedata); //此处的somedata未定义
})
.catch(function(reason){
console.log('rejected');
console.log(reason);
在 resolve的会调用,我们console.log(somedata),而somedata是未定义的,如果不用Promise,代码运行到这里,就直接在控制台报错了,不往下运行了。但是在这里会得到这样的结果:
image.png
也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能。
all方法
Promise.all()提供了并行执行异步操作的能力。并且在所有异步操作执行完成以后,才执行回调。
Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});
Promise.all(),接收一个数组作为参数,数组里的元素最终都返回一个Promise对象。这样,三个异步的操作就并行执行了,等到他们都执行完以后,才会进入到then里边,那么三个异步操作执行以后返回的数据哪里去了呢? all会把所有异步操作的结果放进一个数组,并且把数组传递给then,就是上边的results.
所以上边代码输出的结果是:
image.png
有了all方法,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据,有一个场景很适合用这个方法。
比如一些游戏类的素材比较多的应用,打开网页时预先加载,需要用到各种资源,比如图片,flash,以及各种静态文件。所有都加载完以后,再进行页面的初始化。
race的用法
Promise.all方法,实际上是谁跑的慢,以谁为准执行回调。那么相对的就有另外一个方法,以谁跑的块,以谁为准执行回调。
就是race方法,这个词本来就是赛跑的意思。race的用法与all一样。我们修改下上边的计时器的时间:
function runAsync1(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('异步执行1完成');
resolve('随便什么数据1');
}, 1000);
});
return p;
}
function runAsync2(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('异步执行2完成');
resolve('随便什么数据2');
}, 3000);
});
return p;
}
function runAsync3(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('异步执行3完成');
resolve('随便什么数据3');
}, 2000);
});
return p;
}
Promise
.race(runAsync1(),runAsync2(),runAsync3())
.then(function(results){
console.log(results);
})
//1秒后打印
"异步执行1完成"
//再过1秒后打印
"异步执行3完成"
//再过2秒后打印
"异步执行2完成"
这个race有什么用呢?使用场景还是很多的,比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作,代码如下:
//请求某个图片资源
function requestImg(){
var p = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
img.src = 'xxxxxx';
});
return p;
}
//延时函数,用于给请求计时
function timeout(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){
reject('图片请求超时');
}, 5000);
});
return p;
}
Promise
.race([requestImg(), timeout()])
.then(function(results){
console.log(results);
})
.catch(function(reason){
console.log(reason);
});
requestImg函数会异步请求一张图片,我把地址写为"xxxxxx",所以肯定是无法成功请求到的。timeout函数是一个延时5秒的异步操作。我们把这两个返回Promise对象的函数放进race,于是他俩就会赛跑,如果5秒之内图片请求成功了,那么遍进入then方法,执行正常的流程。如果5秒钟图片还未成功返回,那么timeout就跑赢了,则进入catch,报出“图片请求超时”的信息。运行结果如下:
image.png
H-------解构赋值
ES6允许按照一定的模式,从数组和对象中提取值,对变量进行赋值。这杯成为解构赋值。
- 数组的解构赋值;
数组中的值会被自动解析到对应接收该值的变量中,数组的解构赋值要一一对应,如果有对应不上的,就是undefined
let [a,b,c,d] = [1,2,3];
console.log(a); 1
console.log(b); 2
console.log(c); 3
console.log(d); undefined
-对象的结构赋值
对象的结构赋值与数组有一个重要的不同,数组的元素是按照次序排列的,变量的取值由它的位置决定。而对象的属性是没有次序的,变量必须与属性同名,才能取到正确的值。
var person = {name:'小明',age:20,sex:'男'};
var {name,age,sex} = person;
console.log(name);
console.log(age);
console.log(sex);
//如果想要变量名和属性名不同,可以这样写
let {name:foo,age:baz} = {name:'小明',age:20,sex:'男'}
console.log(foo); '小明'
console.log(baz); 20
I--------set数据结构
Set的数据结构,类似数组,所有的数据都是唯一的,没有重复的值,它本身是一个构造函数;
var arr = [1,2,2,3,4,4];
var s = new Set(arr);
console.log(s); //{1,2,3,4}
属性和方法:
size 数据的长度
add() 添加某个值,返回 Set 结构本身。
delete() 删除某个值,返回一个布尔值,表示删除是否成功。
has() 查找某条数据,返回一个布尔值。
clear() 清除所有成员,没有返回值。
console.log(s.size); //4
console.log(s.add(5)); //{1,2,3,4,5}
console.log(s.delete(1)); //true
console.log(s.has(2)); //true
s.clear();
2.函数节流和函数防抖
前提条件:一个函数在短时间内被多次执行。
但是这种短时间内多次重复执行,通常情况下是没有必要的。我们需要优化这种高频执行的JS。
优化的方法就是 函数节流 和 函数防抖
函数节流:
让函数在特定的时间之内只执行一次。特定时间内如果多次触发函数,只有一次生效。
应用场景 :下拉加载。
函数防抖:
频繁触发某一事件,其中两次触发间隔时间大于等于特定时间,才执行代码一次。如果在特定时间内,又触发了一次事件,那么重新开始计算函数执行时间。简单的说,一个动作连续触发,只执行最后一次。
应用场景:搜索框搜索输入,比如用户输入字母间隔超过2秒再发送请求。
函数节流的例子
<button id="btn">大招<button>
<script>
var cd = false; //cd用来记录大招是否冷却,默认是没有冷却
function attack(){
console.log('发起攻击')
}
var button = document.queryselector('#btn');
button.onclick = function(){
if(cd){
//点击大招按钮的时候判断一下,如果大招冷却,给出提示。
console.log('大招冷却,无法攻击!')
}else{
//如果大招没有冷却,发起攻击
attack();
cd = true; //发起攻击后,技能状态调整为冷却
var timer = setTimeout(function(){
cd = false; //3秒钟后,技能冷却结束
},3000)
}
}
</script>
//函数防抖例子
//公交关门
function close(){
console.log('关门');
}
var button = document.querySelector('#btn');
var timer = null ; //定时器一开始是空的
button.onclick = function(){
//如果点击了按钮,发现上一个计时器还存在,那么就把它清除掉。
if(timer){
window.clearTimeout(timer);
}
//如果5秒种之内没有再点,就设置一个定时器,并执行关门函数,并且把定时器清除掉。
timer = setTimeout(function(){
close();
timer= null;
},5000)
}
3.手写AJAX
var xhr = new XMLHttpRequest();
xhr.open('GET','/xxx',true);
xhr.onreadystatechange = function(){
if(xhr.readystate === 4){
console.log('请求完成')
if(xhr.status>=200&&xhr.status<300||xhr.status === 304){
console.log('请求成功')
}else{
console.log('请求失败')
}
}
}
xhr.send();
4.这段代码里的this是什么?
主要是看函数怎么调用的!
fn() // this指向 window/global
obj.fn() // this 指向 obj
fn.call(xx) //this 指向 xx
fn.bind(xx) //this指向 xx
new Fn() //this指向 新的对象
fn = ()=>{} //this 指向外边的this
判断的通用方法
function fn(a,b){
console.log(this)
}
fn(1,2);//这句等价于下边
fn.call(undefined,1,2);
fn.apply(undefined,[1,2]);
注意:
在严格模式下 'use strict' ,此时 fn里的this,就是call和apply的第一个参数,也就是 undefined
在非严格模式下,也就是不用 'use strict' ,call和apply里的第一个参数如果是undefined或者是null,那么this会默认替换为 window
在看一个例子:
var obj = {
fn:function(a,b){
console.log(this)
},
child:{
fn2:function(){
console.log(this)
}
}
}
obj.fn(1,2); 等价于
obj.fn.call(obj,1,2);
obj.fn.apply(obj,[1,2]); //所以this是obj
obj.child.fn2();等价于
obj.child.fn2.call(obj.child);
obj.child.fn2.apply(obj.child); //所以this是 obj.child
5. 闭包和立即执行函数是什么?
-
闭包:
就是能读取其他函数内部变量的函数,在JS中,只有函数内部的子函数,能够读取函数内部的局部变量。所以闭包可以理解为,定义在一个函数内部的函数。 -
闭包的缺点:
让变量始终保持在内容中,内存消耗会很大。 -
什么是词法作用域?
词法作用域就是函数创建的时候所在的作用域。
所以,函数在执行的时候,使用的变量,会先从自己内部去找,如果自己内部没有定义,就到自己的词法作用域(scopes)去找。
function car(){
var speed = 0;
function fn(){
speed++;
console.log(speed);
}
return fn;
}
var speedUp = car();
speedUp(); //1
speedUp(); //2
// 如果在car函数里,再声明一个函数fn,并返回fn,fn()内部又使用了car声明的变量,然后调用car函数并赋值给 一个全局变量speedUp
// 因为speedUp 属于全局作用域,而全局作用域在页面没有关闭的时候,是不会销毁的。所以也就导致了fn函数不会被销毁
// 执行speedUp就相当于 执行fn(),而fn()函数内部有用到局部变量speed,按照作用域链来寻找,fn()函数内没有声明
// 继续往上一级找,fn()函数是声明在car()内的,而speed是在car内声明的
// 所以第一次执行 speedUp() 的时候,结果是1;执行之后,speed是没有被销毁的
// 再次执行就是2
//再看个例子
var fnArr = [];
for(var i=0;i<10;i++){
fnArr[i] = function(){
return i;
}
}
console.log(fnArr[1]); // 这个时候数组里存的都是函数 function(){ return i}
console.log(fnArr[2]()) //10
console.log(fnArr[5]()) //10
console.log(fnArr[7]()) //10
//为什么会输出10呢? 分析一下
fnArr[2]本身是一个函数,加括号,表示执行它指向的那个函数。
函数内部 return i,用到了i这个变量,所以会从自己内部去找这个变量,发现没有声明i,然后在去函数声明的作用域去找,函数是在for里声明的,但是for本身并不是函数,所以function函数声明的作用域是全局作用域。而全局作用域里,for循环完以后,i已经变为10了,所以fnArr[i]()会输出10.
那么如果我们想输出 0,1,2,3,4,5,....怎么办呢?
用立即执行函数
var fnArr = [];
for(var i=0;i<10;i++){
fnArr[i] =( function(j){
return j;
})(i)
}
console.log(fnArr[0]) //0
console.log(fnArr[5]) //5
console.log(fnArr[7]) //7
再看个例子
for(var i=0;i<5;i++){
setTimeout(function(){
console.log(i)
},2000)
}
//这里2秒后会输出5个5
for(var i=0;i<5;i++){
(function(j){
setTimeout(function(){
console.log(j)
},2000)
})(i)
}
//这里2秒后会输出 0,1,2,3,4
再熟悉一下作用域链
var x = 10
function foo(){
console.log(x);
}
foo(); //10
function bar(){
var x = 20;
foo();
}
bar(); //10
执行bar就是执行foo,foo输出x,先从自己内部去找
发现没有声明x,然后到foo声明的作用域去找
foo是声明在全局作用域的,而全做作用域下,有声明变量x,所以输出10
//下边之所以输出30 是因为foo是在bar内部声明的。
var x = 10;
bar(); //30
function bar(){
var x = 30;
function foo(){
console.log(x);
}
foo();
}
6.什么是JSONP,什么是CORS,什么是跨域?
什么是跨域?
--------是指不同源的网站之间的访问。
同源策略
---------提到跨域,就不得不说一下同源策略,同源策略是NetScape提出的浏览器的一种安全策略,也就是指a网站,不能随便读取b网站的内容。
所谓同源:
--------指的是,协议、域名、端口、相同
最常见的应用是,当我们调用ajax接口时,如果不设置跨域,浏览器会报错。这证明使用XMLHttpRequest对象不能发送跨域请求。
有疑惑的小伙伴肯定会问,那我用a标签请求其他网站,是不是就是跨域了呢?
这里要明白跨域是指在当前域下调用其他域下的东西,而链接则是直接跳转到对方的域下了,跟你之前的域名毫无关系。
如果想从你的网站跨域去另一个网站拿到一些资源,有一个前提是另外一个网站的服务器支持跨域,这个需要在服务器端设置,不同服务器的设置方法不一样,我们在这里不多说,就看客户端跨域如何解决?
解决跨域最常见的方式是 jsonp。
先弄清楚 json和jsonp的区别
json (JavaScript Object Notation)是一种轻量级的数据交换格式,用来在浏览器和服务器之间交换数据。
jsonp (JSON With Padding) 就是打包在函数中调动的json,或者包裹的json
json 是一种数据格式;jsonp是一种数据调用方式
//json
{
"name":"小明"
}
//jsonp
callback({
"name":"小明"
})
jsonp是jquery给我们封装的一个方法,使用方法如下:
$.ajax({
ulr:'xxx',
type:'GET',
dataType:'jsonp',
success:function(data){
console.log(data)
}
})
上边的代码是当我们调用外部的一个接口时,通过设置jquery里的ajax方法里的datatype属性的值为jsonp,就可以成功调用这个接口了。
首先我们需要明白,在页面上直接发起一个跨域的ajax请求是不可以的,但是,在页面上引入不同域上的js脚本却是可以的,就像你可以在自己的页面上使用<img src=""> 标签来随意显示某个域上的图片一样。
看下怎么利用<script src="">来完成一个跨域请求。
<div class="container">
<ul class="news"></ul>
<button class="show">show news</button>
</div>
$('.show').addEventListener('click', function () {
var script = document.createElement('script');
script.src = 'http://127.0.0.1:8080/getNews?callback=appendHtml';
document.head.appendChild(script);
// document.head.removeChild(script); //为了保持代码的整洁,直接删除了新增的标签,但是标签的作用已经起到了
})
function appendHtml(news) {
var html = '';
for (var i = 0; i < news.length; i++) {
html += '<li>' + news[i] + '</li>';
}
console.log(html);
$('.news').innerHTML = html;
}
jsonp跨域的原理:
- 使用script标签发送请求,这个标签支持跨域访问
- 在script标签里,给服务器传递一个callback
- callback的值对应到页面上定义的一个全局函数(为什么是全局函数呢?因为服务器接收到callback函数后,会返回页面中的script去寻找,如果不写到全局作用域中,根本找不到)。
4.服务器端返回的是一个函数的调用,调用的时候会把返回数据作为参数包裹在这个函数里。
缺点: jsonp只能解决get方式的跨域。如果传输的数据比较大,这种方式就不行了。
cors跨域
cors (cross origin resource sharing) 全称 '跨域资源共享'
相对于jsonp, cors支持所有类型的HTTP请求,并且实施起来非常简单。
cors背后的基本思想是,使用自定义的HTTP头部允许浏览器和服务器互相了解对方,从而决定请求和响应成功与否。
cors原理:
当使用XMLHttpRequest发送请求的时候,浏览器发现该请求不符合同源策略,会给该请求加一个请求头 origin,后台会进行一系列的处理,如果确定接受请求,则在返回结果中,加入一个响应头 Access-Control-allow-origin;浏览器判断该响应头中是否包含 origin的值,如果有,则浏览器会处理响应,我们就拿到数据。如果没有,则浏览器会直接驳回,我们就拿不到数据。
IE10以上支持,现代浏览器也都支持cors
JSONP和CORS的区别?
- JSONP只支持GET请求,而CORS支持所有类型的HTTP请求。
- 使用CORS,开发者可以使用普通的XMLHttpRequest对象发起请求和获得数据。比起JSONP有更好的错误处理。
3.JSONP主要被老的浏览器支持,它们往往不支持CORS,而绝大多数的现代浏览器都已经支持了CORS
7. async/await 怎么用? 怎么捕获异常?
async
是异步的缩写。而await 可以认为是 async wait的缩写。
async
作为一个关键字放在函数前面,用来声明这个函数是一个异步函数
。既然是异步函数就意味着该函数的执行不会阻塞后边的代码;
async函数返回的是一个Promise对象
。
await用于等待一个异步方法执行完成
,await等待的是一个表达式,而表达式的计算结果是一个Promise对象或者其他值。
(await相当于运算符,用于组成表达式,await表达式的运算结果取决于它等到的东西)
如果等到的不是一个Promise对象,那么await表达式的运算结果就是它等到的东西。
如果它等到的是一个Promise对象,await就忙起来了,它会阻塞后面的代码,等待Promise对象resolve,然后得到resolve的值。作为await表达式的运算结果。
await只能用到async函数中。
function sayHi(name){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('Hi'+name);
},2000)
})
}
async function test(){
var result = await sayHi('小明');
console.log(result);
}
test();
async和 await捕获异常
,需要用到 try/catch
的方式:
因为await后面跟着的是 Promise对象,当有异常的情况下,会被Promise对象内部的catch捕获。而await就是一个then的语法糖,并不会捕获异常,那么就需要借助
try/catch
来捕获异常,并进行相应的逻辑处理。
function sayHi(name){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
//生成0-10的随机整数
var num = Math.ceil(Math.random()*10);
if(num>5){
//如果随机数大于5,就resolve出结果
resolve('Hi'+name);
}else{
//否则就抛出错误
reject('出错了');
}
},2000)
})
}
async function test(){
try{
var result = await sayHi('小明');
console.log(result);
}catch(error){
console.log(error)
}
}
test();
8.如何实现深拷贝?
关键字:
1、递归
2、判断类型
3、检查循环引用(环)
4、不可能拷贝 _proto_
拷贝分为浅拷贝和深拷贝
在这之前,我们先弄清楚两个概念。
- 基本数据类型
Number ,String,Boolean,Null,Undefined 都是基本数据类型
基本数据类型时按值访问的,因为可以直接操作保存在保存在变量中的实际值。
var a = 10;
var b = a; //b只是保存了a复制的一个副本。所以 a和b是互相独立的。
console.log(a); //10
console.log(b); //10
//a值的改变不会影响b的值
a = 20;
console.log(a); //20
console.log(b); //10
- 引用数据类型
引用数据类型也就是对象类型 Object type.比如 Object,Array,Function,Data等。
JavaScript的引用数据类型是保存在堆内存中的对象。
与其他语言不同的是,你不可以直接访问堆内存空间中的位置和操作堆内存空间。只能操作对象在栈内存中的引用地址。
所以,引用类型数据在栈内存中保存的是对象在堆内存中的引用地址,通过这个引用地址可以快速查找到保存在堆内存中的对象。
var obj1 = new Object();
var obj2 = obj1;
obj2.name = '小明';
console.log(obj1.name);
说明这两个引用数据类型指向了同一个堆内存对象。obj1赋值给onj2,实际上这个堆内存对象在栈内存的引用地址复制了一份给了obj2,但是实际上他们共同指向了同一个堆内存对象。
实际上改变的是堆内存对象。
下面我们来演示这个引用数据类型赋值过程:
image.png好了,上边两个概念了解清楚以后,我们进入正题。
对象的浅拷贝
对象浅拷贝比较简单,就是将一个变量赋值给另外一个变量
var obj1 = {
name:"小明",
age:20
}
var obj2 = obj1;
console.log(obj2.age) //20
obj2经过浅拷贝,拥有了obj1的属性
封装浅拷贝方法
var easyCopy = function(oldObj){
//constructor 属性应用了该对象的构造函数
var newObj = oldObj.constructor === Array?[]:{};
if(typeof oldObj != 'object') return;
for(var key in oldObj){
if(oldObj.hasOwnProperty(key)){
newObj[key] = oldObj[key]
}
}
return newObj;
}
var obj1 = {
name:'小明',
age:20
}
var obj2 = easyCopy(obj1);
console.log(obj2)
浅拷贝存在的问题:
我们知道,引用数据类型的赋值,其实是变量a把对象在堆内存中的地址,复制了一份给变量b,这个时候,a和b指向的都是同一个对象。通过a或者b都可以改变堆内存中的对象(比如添加删除属性),a和b是可以相互影响的。所以这种浅拷贝会带来BUG。
对象的深拷贝
深拷贝的目的就是为了实现 复制出2个完全相同,却又完全独立,互不干扰的对象。
var deepCopy = function(obj){
var cloneObj = Array.isArray(obj)? []:{};
if(obj && typeof obj === 'object' ){
for(var key in obj){
if(obj.hasOwnProperty(key)){
cloneObj[key] = typeof obj[key] === 'object'?deepCopy(obj[key]):obj[key]
}
}
}
return cloneObj;
}
var obj = {
name:'小红',
age:18,
friend:{
name:'小明',
sex:'男',
study:["数学","英文","物理"]
}
}
var obj2 = deepCopy(obj);
console.log(obj2)
obj.friend = {
name:'小明明明',
sex:"女"
}
console.log(obj);
console.log(obj2);
深拷贝的缺点:虽然深拷贝能够避免浅拷贝出现的问题。但是却会带来性能上的问题,如果一个对象非常复杂且数据庞大,所消耗的性能将会是很可观的。
关于for in
for..in可以用来遍历任何一个对象,它会将对象上的所有的属性全部遍历出来,包裹原型链上的属性。所以上边代码中需要hasOwnProperty来判断这个属性到底是不是对象本身的属性。因为数组也是对象,for..in也可以用来遍历数组,但是for in损耗性能较多,所以尽量不要用它来遍历数组。
关于递归
一个方法,重复调用自身的情况,叫做递归。
需要注意的是,一定要有一个条件来结束递归,否则将会陷入无限的循环。
var num = 0;
function recursion(){
if(num < 50){
num++;
recursion()
}
}
recursion()
9 如何用正则实现trim()?
trim()方法去掉字符串两端的空格,无论有多少个空格都会去掉,字符串中间的空格不会被去掉。
function trim(string){
return string.replace(/^\s+|\s+$/g);
}
var str = ' ab cd ef ';
var res = replace(str);
console.log(res); //'ab cd ef'
10.不用class如何实现继承,用class如何实现继承?
//不用class
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.printName =function(){
console.log(this.name);
}
// 继承属性
function Mail(name,age,sex){
Person.call(this,name,age);
this.sex = sex;
}
// 继承方法
//Object.create 创建一个空对象,空对象的_proto_指向Person.prototype
Mail.prototype = Object.create(Person.prototype);
Mail.prototype.printSex = function(){
console.log(this.sex);
}
//因为Mail.prototype此时是指向Person.prototype的,所以Mail.prototype.contructor是指向 Person的。我们需要修改它的指向。
Mail.prototype.contructor = Mail
var john = new Mail('约翰',20,'男');
console.log(john.name);
john.printName();
john.printSex();
//用class
class Person {
constructor(name,age){
this.name = name;
this.age = age;
}
sayAge(){
console.log(`i am ${this.age}`)
}
}
class Student extends Person{
constructor(name,age,score){
supre(name,age);
this.socre = score;
}
sayScore(){
console.log(`i get ${this.score}`);
}
}
11.如何实现数组去重?
//双循环去重
function fn(arr){
if(Array.isArray(arr)){
//声明一个新的数组,然后把要遍历的数组的第一个元素放入这个新数组
var newArr = [arr[0]];
//然后从第二个元素开始遍历老的数组,同时遍历新数组
//把老数组和新数组的已有元素做对比,如果不相等,就放入新数组。
for(let i=1;i<arr.length;i++){
let flag = true;
for(let j=0;j<newArr.length;j++){
if(arr[i] === newArr[j]){
flag = false;
break;
}
}
if(flag){
newArr.push(arr[i])
}
}
}
return newArr
}
var arr = [1,1,2,2,3,4,5,5];
var arr2 = fn(arr);
console.log(arr2) [1, 2, 3, 4, 5]
//indexOf去重
//两个关键点 1、声明新数组 2、判断老数组里的元素是否在新数组里,没有就push进新数组
function fn(arr){
if(!Array.isArray(arr)){
console.log('type error');
return
}
var newArr = [];
for(let i=0;i<arr.length;i++){
if(newArr.indexOf(arr[i]) == -1){
newArr.push(arr[i])
}
}
return newArr;
}
var arr = [1,1,2,2,3,4,5,5];
var arr2 = fn(arr);
console.log(arr2) [1, 2, 3, 4, 5]
//set 去重
//ES6?新增了一个数据类型 set,set的一个最大的特点就是数据不重复。
//Set()函数可以接收一个数组(或者类数组对象)作为参数来初始化。
function fn(arr){
if(!Array.isArray(arr)){
console.log('type error');
return;
}
return [...new Set(arr)]
}
var arr = [1,1,2,2,3,4,5,5];
var arr2 = fn(arr);
console.log(arr2) [1, 2, 3, 4, 5]
DOM面试题
1. DOM事件模型是什么?
- 事件冒泡-----事件开始时,由最具体的元素接收,然后逐级向上传播到较为不具体的元素。
- 事件捕获------不太具体的节点更早的接收事件,而最具体的元素最后接收事件,和事件冒泡相反。
- DOM事件流------DOM2级事件规定,事件流包括三个阶段,事件捕获阶段,处于目标阶段,事件冒泡阶段。首先发生的是事件捕获,为截取事件提供机会,然后是实际目标接受事件,最后是事件冒泡
Opera、Firefox、Chrome、Safari都支持DOM事件流,IE不支持事件流,只支持事件冒泡
事件处理程序是什么?
响应某个事件的函数,就叫做事件处理程序
- 事件处理程序分为4种
//HTML事件处理程序
<input type="button" name="btn" value="点击" onclick="alert('clicked')"
//DOM0级事件处理程序
<button id="box">点击</button>
var box = document.querySelector('#box');
box.onclick = function(){
console.log('DOM0级事件处理程序')
}
box.onclick = null //删除绑定的事件
//DOM2级事件处理程序
<button id="box">点击</button>
var box = document.querySelector('#box');
box.addEventListener('click',function(){
console.log('DOM2级事件处理程序')
},true)
//删除绑定的事件
box.removeEventListener('click',函数名,boolean)
//IE事件处理程序
function showMes(){
console.log('我是IE事件处理程序')
}
var box = document.getElementById('box');
box.attatchEvent('onclick',showMes);
//删除绑定的事件处理程序
box.detachEvent('onclick',showMes);
跨浏览器事件处理程序
function showMes(){
console.log('我被点击了')
}
var box = document.getElementById('box');
var EventUtil = {
addHandler:function(element,type,handle){
if(element.addEventListener){
element.addEventListener(type,handle,false);
}else if(element.attatchEvent){
element.attatchEvent('on'+type,handle)
}else{
element['on'+type] = handle
}
},
removeHandler:function(element,type,handle){
if(element.removeEventListener){
element.removeEventListener(type,handle,false);
}else if(element.detachEvent){
element.detachEvent('on'+type,handle);
}else{
element['on'+type] = null;
}
}
}
EventUtil.addHandler(box,'click',showMes);
EventUtil.removeHandler(box,'click',showMes);
事件对象
在触发DOM上的事件时,都会产生一个事件对象Event
Event 对象代表事件的状态,比如事件在其中发生的元素、键盘按键的状态、鼠标的位置、鼠标按钮的状态。
兼容DOM的浏览器,会将一个对象,传入事件处理程序中。
例如:
var box = document.querySelector('#box');
box.addEventListener('click',function(event){
console.log(event.type) //输出事件的类型
})
event对象的属性
event.target 获取事件的类型
event.type 获取事件的目标(就是时间绑定到哪个元素上了)
...等等
event对象的属性
event.stopPropagation :
不再派发事件。就是终于事件在传播过程的捕获,目标处理,冒泡阶段的传播,事件不再被派到其他节点。
event.preventDefault :
通知浏览器不要执行与事件相关的默认动作。比如a标签
IE事件对象
window.event
常见的属性
window.event.type 相当于 event.type
window.event.srcElement 相当与 event.target
window.event.cancleBubble 相当于 event.stopPropagation()
window.event.returnValue 相当于 event.preventDefault()
2.移动端的触摸事件了解吗?
touch 触摸事件,有4种之分
1. touch start 手指触摸到屏幕会出发
2. touch move 当手指在屏幕上移动时会触发
3. touch end 当手指离开屏幕时会触发
4. touch cancel 可由系统进行的触发,比如手指触摸屏幕的时候,突然alert了一下,则可以触发该事件。
tap事件
触碰事件,一般用于代替click
tap:手指碰一下屏幕会触发
longTap :手指长按屏幕会触发
singleTap:手指碰一下屏幕会触发
doubleTap:手指双击屏幕会触发
swipe事件
滑动事件
swipe:手指在屏幕上滑动时触发
swipeLeft:手指在屏幕上向左滑动时会触发
swipeRight:手指在屏幕上向右滑动时会触发
swipeUp:手指在屏幕上向上滑动时会触发
swipeDown:手指在屏幕上向下滑动时会触发
模拟swipe事件:思路 记录两次touchmove的位置差,如果后一次在前一次的右边,说明向右滑了。
3.事件委托(也叫事件代理)是什么?
-
事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
-
举个取快递的例子:
有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台MM代为签收。
前台MM收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。
这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台MM也会在收到寄给新员工的快递后核实并代为签收。
这个例子包含2层意思
1.现在委托前台的同事,是可以代为签收的(即程序中,现有的DOM节点是有事件的)
2.新员工也是可以被前台代为签收的(即程序中,新添加的DOM节点也是有事件的)。
<div class="container">
<div class="box">box1</div>
<div class="box">box2</div>
</div>
<button class="add">add</button>
var con = document.querySelector('.container');
var box = document.querySelectorAll('.box');
var addBtn = document.querySelector('.add');
//给每个box都绑定一个事件
// box.forEach(function(node){
// node.onclick = function(){
// console.log(this.innerText);
// }
// })
//用父级div做事件代理
con.onclick = function(e){
if(e.target.classList.contains('box')){
console.log(e.target.innerText)
}
}
//有了事件代理以后,哪怕新增的box,也会被绑定事件。
var i = 4;
addBtn.onclick =function(){
var box = document.createElement('div');
box.classList.add('box');
box.innerText = 'box' + i++;
con.appendChild(box);
}