天天看点

前端工程师JS基础知识部分(上)

写在前面:

这篇博文偏应用,改天再写篇概念性的。该篇幅内容包括:减少DOM操作,时间委托,冒泡事件,Promise相关, 微任务宏任务,作用域,变量提升,闭包,变量类型,深浅拷贝,原型和作用域链后续争取把js重点都记录上,深入浅出。

DOM操作

  1. 使用文档碎片减少DOM操作
  2. 冒泡事件:stopPropagation();阻止向上冒泡
  3. 事件委托 :减少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");
})**
           
前端工程师JS基础知识部分(上)

在没有绑定阻止冒泡时,给了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 的信息。

异步

  1. 使用promise加载一张图片
  2. then,catch改变promise的状态
  3. async , await , 和Promise
  4. 微任务和宏任务

使用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);
})
           
前端工程师JS基础知识部分(上)

使用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。结果显示:

前端工程师JS基础知识部分(上)

再回过头看先前的结论,发现也不难理解。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();
           
前端工程师JS基础知识部分(上)

例子比较通俗易懂,配合结果应该能更清晰的理解结论。

微任务和宏任务

宏任务有:setTimeout , setInterval , Dom事件,AJAX请求

微任务有:Promise ,async / await

微任务会比宏任务先执行,具体可搜该类型的答应题目进行深入理解。

作用域,变量提升,和闭包

关于作用域,有全局和局部(函数作用域,块级作用域ES6才有),局部能访问全局,全局不能访问局部。在查找变量的时候,变量的父子级关系就是一条作用域链。找同名变量优先局部,注意this引用

变量提升,这得多看几道作用域输出变量的题(下面这张我是去牛客网刷js专项训练截来的)

前端工程师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);
           
前端工程师JS基础知识部分(上)

深拷贝是拷贝对象各个层级的属性,看对象的属性是否是对象或数组类型,进行相应属性的一一复制。

原型和原型链

原型:

每一个JavaScript对象在创建的时候就会预制管理另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型继承属性。可以这么理解:你是你爸创造出来的实例对象,你爸就是你的原型,你以后要找什么方法,会通过原型链找到你爸,懂了吧。

那原型在实际中有什么用呢?简单来说就是给原型添加方法,好给后面调用。

原型链怎么找?你爸创造你时,就给了你一个法宝(

__proto__

),你只要通过这个法宝喊一声“ 爸” ,他就会理你。当然你还有爷爷,也能通过这个找到你爷爷,这就看你爸的(

prototype

)给不给了,哈哈。

原型链:

每个对象都有属于自己的隐式原型

__proto__

,每个函数或方法都有自己的显示原型

prototype

。实例对象继承使用原型方法的时候通过隐式原型向上查找,如果在原型的显示原型中找到,就不用在通过原型的隐式原型再向上找。

这么说有点拗口,其实蛮好理解的。下面通过一张图说明:

前端工程师JS基础知识部分(上)
前端工程师JS基础知识部分(上)

如图所示,实例 teacher 要想调用 teach 方法,首先通过显示原型查找有没有 teach 这个方法,发现没有就通过

_proto_

隐式原型查找他的上一级原型(Teacher),看他有没有。Teacher同样先看自己的显示原型,发现有就返回。同理,如果要找 drink 方法,则还要通过

_proto_

再向上找原型(Person)看他有没有,找到就层层返回。

前端工程师JS基础知识部分(上)

如果找到尽头都没有那个方法,就回返回null。这样,一条完整的方法查找路径就是原型链。

继续阅读