写在前面:
这篇博文偏应用,改天再写篇概念性的。该篇幅内容包括:减少DOM操作,时间委托,冒泡事件,Promise相关, 微任务宏任务,作用域,变量提升,闭包,变量类型,深浅拷贝,原型和作用域链后续争取把js重点都记录上,深入浅出。
DOM操作
- 使用文档碎片减少DOM操作
- 冒泡事件:stopPropagation();阻止向上冒泡
- 事件委托 :减少DOM请求次数
使用文档碎片减少DOM操作
假设在一个ul中,如果持续添加一百次插入 li 的DOM操作,那这个过程无疑是非常消耗内存的,这时不妨使用文档碎片
fragment
,将插入的100次操作存储在这,结束后在一并添加到ul中,这样DOM操作从原本的100次变为一次。
<ul id="list">
</ul>
<script src="index.js"></script>
const list = document.getElementById('list');
// 创建文档碎片,内容放在内存里,最后将结果一次插入list,减少DOM操作
const fragment = document.createDocumentFragment();
for (let i = 0; i < 5; i++) {
const item = document.createElement('li');
item.innerHTML = `事件${i}`;
// 每次插入都是一次DOM操作,消耗性能,所以先往文档碎片放入li,循环完后在插入list
// list.appendChild(item);
fragment.appendChild(item);
}
list.appendChild(fragment);
阻止冒泡事件
依旧举个栗子:当点击一个 li , 他没有绑定事件处理函数,那么他会往父级元素(一直往上找直到body)看有没有事件处理函数,有的话同样触发。
这样可以简便的为子元素绑定处理事件,但弊端也很明显:如果父元素有处理函数,即使你给子元素绑定了处理事件,那么在触发子元素的处理事件同时,还会向上冒泡在触发父元素的处理函数!!这就需要到
stopPropagation();
阻止冒泡行为。
<div id="one">
<p id="p1">冒泡</p>
</div>
<hr>
<div id="two">
<p id="p4">冒泡</p>
<p id="p5">阻止</p>
</div>
const p3 = document.getElementById('p3');
const one = document.getElementById('one');
const body = document.body;
function bindEvent (elem, type, fn) {
elem.addEventListener(type, fn);
}
bindEvent(one, 'click', function () {
console.log("one的click");
})
bindEvent(body, 'click', function () {
console.log("body的click");
})
bindEvent(p3, 'click', function () {
console.log("p5的click");
})**

在没有绑定阻止冒泡时,给了body,第一个父元素和第三个子元素绑定处理事件,点击第三个会触发第三个和body的,点击第一个会触发父元素和body的。如果只想触发第三个事件绑定,可将p3的触发函数修改为:传入event,利用event下的
stopPropagation();
方法。
bindEvent(p3, 'click', function (event) {
event.stopPropagation();
console.log("阻止冒泡");
})
事件委托
事件指的是:不在要发生的事件(直接dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,监听子元素,来做出不同的响应。
例如在ul中有这么五个小 li ,要给他们绑定点击事件,可获取他们标签名后,利用slice方法对li的类数组转化为数组,在对其中的 li 添加注册事件。
<ul id="list">
<li>事件1</li>
<li>事件2</li>
<li>事件3</li>
<li>事件4</li>
<li>事件5</li>
<button id="btn">点击添加委托事件</button>
</ul>
const lis = document.getElementsByTagName('li');
listAray = Array.prototype.slice.call(lis);
listAray.forEach(li => {
addEvent(li, 'click', () => {
alert(li.innerHTML);
})
});
要是有500个li呢,那岂不是遍历绑定500次,这产生的事件监听器非常消耗内存。这时可以找到其父元素,设置事件监听器,利用
target
给 li 绑定事件监听。
// 绑定事件处理函数
function addEvent (elem, type, fn) {
elem.addEventListener(type, fn);
}
addEvent(list, 'click', (e) => {
const target = e.target;
if (target.nodeName === 'LI') {
alert(target.innerHTML);
}
})
btn.addEventListener('click', () => {
const li = document.createElement('li');
li.innerHTML = '新增绑定事件';
list.insertBefore(li, btn);
})
这样,即使不循环遍历 ul 下的 li ,也能通过对 ul 的绑定打印出子元素 li 的信息。
异步
- 使用promise加载一张图片
- then,catch改变promise的状态
- async , await , 和Promise
- 微任务和宏任务
使用promise加载一张图片
考验对promise的基本使用情况。思路:实例化一个promise对象,对图片的加载分别成功与否用
resolve, reject
处理。
function loadImage (src) {
// 创建并实例化一个promise对象
const promise = new Promise((resolve, reject) => {
const img = document.createElement('img');
// 如果图片加载成功,就传入图片进resolve
img.onload = function () {
resolve(img);
}
img.onerror = function () {
const err = new Error(`图片加载失败, url:${{ src }}`)
reject(err);
}
img.src = src;
});
return promise;
}
const url = 'https://cn.bing.com/th?id=OHR.WalhallaOverlook_ZH-CN1059655401_1920x1080.jpg&rf=LaDigue_1920x1080.jpg'
loadImage(url).then(img => {
console.log('img', img);
}).catch(e => {
console.log('err', e);
})
使用then,catch改变promise的状态
先说结论:
then 和 catch 正常返回的时候,Promise状态是 fulfilled ,抛出错误的时候,Promise 状态是 rejected。
fulfilled 状态的Promise执行then 的回调,而rejected则执行catch的回调。
怎么理解?看下面例子:
const p1 = Promise.resolve();
console.log('p1', p1);
const p1Then = p1.then(() => {
console.log('p1Then', p1Then);
})
const p1Error = p1.then(() => {
throw new Error('p1 then error')
})
console.log('p1Error', p1Error);
p1Then.then(() => {
console.log('fulfilled状态走then回调');
}).catch(() => {
console.log('fulfilled状态走catch回调');
})
p1Error.then(() => {
console.log('p1Error走then回调');
}).catch(() => {
console.log('p1Error走catch回调');
})
首先定义一个 resolve 返回值的promise对象,打印显示状态为 fulfilled ,保存当前信息在打印走catch还是then;然后在手动跑出个错误,打印当前状态,在判断走catch还是then。结果显示:
再回过头看先前的结论,发现也不难理解。reject类型的演示也差不多,结论见上。
async , await , 和Promise
这里也先说这三者的结论:
执行async 函数返回的都是Promise对象;promise.then的情况对应await ;promise.catch异常情况对应try…catch。
// async结论 :async返回的都是Promise对象
async function test1 () {
return 1;
}
const result1 = test1();
console.log('result1', result1);
// await 结论:promise.then成功情况对应await
async function test2 () {
const p2 = Promise.resolve(2);
p2.then(data => {
console.log('promise情况:', data);
})
const data = await p2;
console.log('await情况:', data);
}
test2();
// promise.catch异常情况对应try...catch
async function test3 () {
const p3 = Promise.reject(6);
try {
const data3 = await p3;
console.log('data3', data3);
} catch (e) {
console.log('try-catch捕捉的promise异常:', e);
}
}
test3();
例子比较通俗易懂,配合结果应该能更清晰的理解结论。
微任务和宏任务
宏任务有:setTimeout , setInterval , Dom事件,AJAX请求
微任务有:Promise ,async / await
微任务会比宏任务先执行,具体可搜该类型的答应题目进行深入理解。
作用域,变量提升,和闭包
关于作用域,有全局和局部(函数作用域,块级作用域ES6才有),局部能访问全局,全局不能访问局部。在查找变量的时候,变量的父子级关系就是一条作用域链。找同名变量优先局部,注意this引用
变量提升,这得多看几道作用域输出变量的题(下面这张我是去牛客网刷js专项训练截来的)
闭包和匿名函数不要搞混!那么什么是闭包,红宝书说:闭包是有权访问另一个函数作用域的变量函数。也就是说,局部之间并非是不能互相访问的,局部也可以通过闭包访问局部的变量。下面举俩个小例子:
// 函数作为返回值,在定义的地方向上找变量,而不是再调用的地方
function test1 () {
const a = 1;
return function () {
console.log('a', a); //输出:a 1
}
}
const a1 = test1();
const a = 2;
a1();
// 函数作为参数
function test2 (fn) {
const b = 6;
fn();
}
const b = 7;
function fn () {
console.log('b', b); //输出:b 7
}
test2(fn);
为什么要使用闭包: 因为正常情况下,函数执行完存储在内存的变量会被销毁,再次调用该函数时重新计算。假设这函数计算量大且耗时,如果每次调用都重新计算,那影响用户体验,这时就需要道闭包。他可以不释放外部的引用,将第一次计算的结果缓存到本地方便下次调用直接拿取。
闭包的弊端:由于闭包的变量被保存在内存中,所以内存消耗很大,容易导致内存泄漏;容易改变父函数内部变量的值。
变量类型,深浅拷贝
变量类型有:原始类型(let ,string , number , boolean , symbol),和引用类型(object , array , null ,function )。原始类型的存储改变发生在桟中执行,使用具体数值直接存储;引用类型则在堆中,且存储的数据是地址而非具体数值。
// 原始类型
let a = 10;
let b = a;
a = 20;
console.log('b:', b); //输出b: 10
// 引用类型
let c = { age: 20 };
let d = c;
c.age = 21;
console.log('d:', d); //输出d: { age: 21 }
如在下面的例子中,原始类型的变量有私有内存各自存储数值,一方的改变不会牵动自身;引用类型d指向c的存储地址,c值改变后,d用的依旧是c的地址所以输出21。
上面其实引出了深拷贝和浅拷贝的概念,浅拷贝:复制的同时一方变另一放跟着变(修改的是堆内存中的同一个值),深拷贝,完全复制了一个对象,一方的改变不会牵动另一方(修改堆的不同的值)。
如何实现浅拷贝?
如果是数组,可以使用
slice,concat,Array.from , 展开运算符
,来返回一个新数组的特性实现拷贝。但是如果数组嵌套了其他引用类型,
concat
方法将复制不完整。
深拷贝才是重点:
JSON大法不仅适用于数组还适用于对象(非常好用):
原理是JOSN对象中的stringify可以把一个js对象序列化为一个JSON字符串,parse可以把JSON字符串反序列化为一个js对象,通过这两个方法,也可以实现对象的深复制,但是这个方法不能够拷贝函数 。
手写一个深拷贝(巩固理解):
function deepClone (obj) {
let objClone = Array.isArray(obj) ? [] : {};
if (obj && typeof obj === "object") {
for (key in obj) {
if (obj.hasOwnProperty(key)) {
//判断ojb子元素是否为对象,如果是,递归复制,如果不是,简单复制
objClone[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
}
}
}
return objClone;
}
let a = [1, 2, 3, 4],
b = deepClone(a);
a[0] = 2;
console.log(a, b);
深拷贝是拷贝对象各个层级的属性,看对象的属性是否是对象或数组类型,进行相应属性的一一复制。
原型和原型链
原型:
每一个JavaScript对象在创建的时候就会预制管理另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型继承属性。可以这么理解:你是你爸创造出来的实例对象,你爸就是你的原型,你以后要找什么方法,会通过原型链找到你爸,懂了吧。
那原型在实际中有什么用呢?简单来说就是给原型添加方法,好给后面调用。
原型链怎么找?你爸创造你时,就给了你一个法宝(
__proto__
),你只要通过这个法宝喊一声“ 爸” ,他就会理你。当然你还有爷爷,也能通过这个找到你爷爷,这就看你爸的(
prototype
)给不给了,哈哈。
原型链:
每个对象都有属于自己的隐式原型
__proto__
,每个函数或方法都有自己的显示原型
prototype
。实例对象继承使用原型方法的时候通过隐式原型向上查找,如果在原型的显示原型中找到,就不用在通过原型的隐式原型再向上找。
这么说有点拗口,其实蛮好理解的。下面通过一张图说明:
如图所示,实例 teacher 要想调用 teach 方法,首先通过显示原型查找有没有 teach 这个方法,发现没有就通过
_proto_
隐式原型查找他的上一级原型(Teacher),看他有没有。Teacher同样先看自己的显示原型,发现有就返回。同理,如果要找 drink 方法,则还要通过
_proto_
再向上找原型(Person)看他有没有,找到就层层返回。
如果找到尽头都没有那个方法,就回返回null。这样,一条完整的方法查找路径就是原型链。