teambition 面试题解析
一、事件捕获和冒泡的理解。
1、事件绑定的三种形式
1⃣️标签行间内绑定
行间绑定特点:当同一事件在标签上多次绑定时,只执行第一次绑定的事件函数。此时this指向绑定事件的DIV。
2⃣️JS脚本里绑定
脚本绑定 特点:当同一事件在标签上多次绑定时,只执行最后一次的,存在事件覆盖。
3⃣️侦听事件addEventListener()
addEventListener侦听事件特点:同一事件在标签上多次绑定时,都会按顺序全部执行,不存在覆盖现象。
2、addEventListener( type , function, useCapture )事件的三个参数
type : 绑定事件的类型 (注意:不是所有的事件支持冒泡。eg: focus、blur);
function : 事件处理函数;
useCapture : 是否捕获 true-捕获 false-冒泡
3、事件传递的三个阶段
捕获阶段 --- 目标阶段 --- 冒泡阶段
总结:1⃣️:先发生捕获阶段,然后到达目标阶段,最后再发生冒泡阶段
2⃣️重要:事件在目标阶段发生的顺序是和函数注册的顺序有关的。而在捕获阶段和冒泡阶段的函数注册不会影响函数的执行顺序。
(对目标DOM元素上同时在捕获阶段和冒泡阶段绑定处理函数,则函数的执行顺序是和注册顺序有关的,并不是捕获一定发生在冒泡的前面。)
3⃣️阻止冒泡
通过 event.stopPropagation()函数阻止冒泡。
注意:在捕获阶段是不能阻止冒泡的。
二、重排重绘
重排(重构/回流/reflow) 重绘(repaint/redraw)
(一)、浏览器运行机制
浏览器加载完html后生成 DOM树 ,这是页面的结构,页面所有元素按这个结构排列;
在加载完css和js后生成 渲染树 ,页面中的元素都有自己的规模尺寸(宽高)、位置、颜色等,按渲染树将页面绘制在浏览器上。
重排
1、当页面中的某些元素的规模尺寸、布局发生改变或元素的隐藏等都会触发渲染树重新构建。
每个页面至少触发一次重排和重绘(浏览器第一次加载页面时,页面初始化)
2、触发重排的情况:
1⃣️页面的初始化(无法避免)
2⃣️页面增加、删除、隐藏元素
3⃣️改变元素的宽高、位置,使用动画
4⃣️浏览器窗口尺寸的改变(resize事件)
5⃣️读取某些元素属性:(offsetLeft/Top/Height/Width, clientTop/Left/Width/Height, scrollTop/Left/Width/Height, width/height, getComputedStyle(), currentStyle(IE) )
重绘
当页面中的元素的颜色或背景色发生改变时,就会触发重绘。
注意:table及其内部元素可能需要多次计算才能确定好其在渲染树中节点的属性值,比同等元素要多花两倍时间,这就是我们尽量避免使用table布局页面的原因之一。
重排一定触发重绘,重绘不一定触发重排。
重排重绘的代价:浏览器运行速度减慢、耗时,浏览器卡死
(二)、优化
1、浏览器自己的优化
浏览器回维护一个队列,将所有的重排重绘放在这个队列中,当达到一定的数量或一定的时间间隔后再触发一次重排重绘。(此期间不要访问DOM元素的offsetleft等属性)
2、我们的优化
1⃣️尽量批次改变元素css属性
2⃣️将动画的元素设置position属性为absolute或fixed,这样使元素脱离文本流,不会影响渲染树。
3⃣️当多个添加DOM元素时,使用文本碎片fragment(隐藏的元素不再渲染树中,)只触发一次重排重绘
三、let和var声明变量的区别
1、作用域不同
JS是没有块级作用域的概念,只有函数作用域。所以由var生命的变量只在函数作用域或全局作用域起作用。
而由let声明的变量是有块级作用域的概念的,在for(){}和if(){}是不和外界有联系的。
2、变量提升现象的不同
var声明的变量在发生变量提升现象时,是将变量提升并初始化为undefined,而let声明的变量只提升而不初始化,会直接报错。
但是直接用let声明变量不赋值是会打印undefined,还是初始化了,只是let声明放在赋值之后,let声明会提前但不会初始化。
let a;
console.log(a); //undefined
四、函数声明和函数表达式
函数的两种创建方式:
1⃣️函数声明
function fn(){}
2⃣️函数表达式
var fn = function (){};
机制: 通过函数声明的函数在JS解析阶段会将函数声明和函数体全部提升到执行环境的顶部,所以在执行环境中都可以访问到; 通过函数表达式声明的函数,其实和变量声明是一样的机制,只提升函数声明,函数体的赋值还留在原处,在JS执行阶段赋值。
函数提升优先上面代码相当于
注意:函数和变量都会提升,但是函数会优先提升到执行环境顶部,然后变量再提升。
五、闭包的概念及应用场景
JS语言特点:JS中的作用域分为全局作用域和局部作用域,局部作用域里面可以访问到全局作用域的变量,但是反过来全局作用域是访问不到局部作用域的变量。
闭包的概念:闭包就是定义在一个函数内部的函数,通过这个定义函数可以再函数外面获取到函数内部的变量。
闭包的用途:1⃣️函数外部可以访问到函数内部的变量
2⃣️让函数内部的变量始终保存在内存中,不会因为函数调用完后被垃圾回收机制回收
使用闭包注意:不能随便使用闭包,因为使用闭包后函数内部的变量保存在内存中,所以内存消耗很大,有时会造成内存泄漏。
闭包应用场景:
1⃣️setTimeout()第一个函数参数可以带参
一般情况下,setTimeout( fn(opt),1000 )是达不到效果的,会直接执行,没有延迟效果。
使用闭包可以实现setTimeout()第一个函数参数可以带参数
2⃣️全局变量有变量污染和变量安全等问题
使用闭包实现局部的全局变量,防止全局变量污染等安全问题。
(function (){ var test = 123; })()
3⃣️函数外部或其他函数内访问某一函数内部数据
4⃣️
a、隔1s输出数字5
b、隔1s依次输出0、1、2、3、4 (bind()不会立即执行)
c、立即输出0、1、2、3、4 (call()方法会立即执行)
d、隔1s依次输出0、1、2、3、4
e、立即输出0、1、2、3、4
f、隔1s依次输出0、1、2、3、4
g、隔1s依次输出0、1、2、3、4
h、隔1s依次输出0、1、2、3、4
总结:
setTimeout是延迟执行的,如果直接打印i的话,由于i很早就变成了5,所以会隔1
s依次输出5,所以要想分别输出0、1、2、3、4的话,就得想办法将每次i增加后的值暂存起来;
暂存起来的方法有:.bind()、.call()、(function(){})(i)等方法。
现在是实现了分别输出0、1、2、3、4的效果,但由于.bind()不会立即执行,而.call()和(fn(){})(i)会立即执行,所以.bind()的效果是:隔1s依次输出0、1、2、3、4。而.call()和(fn(){})(i)会立即输出0、1、2、3、4。
要想让.call()和(fn(){})(i)也可以隔1s依次输出0、1、2、3、4,则要用到闭包,在函数里再返回一个函数。
{
1⃣️、 function( index ){ ..index..} .bind( null , i ) 、 let 将变量I暂存,不会立即执行 ;
2⃣️、 function(index){..index..} .call( null , i ) 、
function(index){..index..}.apply( null , [i] )、
( function(index){..index..} )(i)
会将变量i暂存,但会立即执行,所以要和闭包配合使用
}
六、作用域
函数作用域,函的数内部函数可以访问外部函数的变量和参数,但是不能访问外部函数的this和arguments对象。