天天看點

前端工程師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。這樣,一條完整的方法查找路徑就是原型鍊。

繼續閱讀