站在巨人的肩膀上
黑馬程序員前端JavaScript入門到精通全套視頻教程,javascript核心進階ES6語法、API、js高級等基礎知識和實戰教程
(十五)高階技巧
1. 深淺拷貝
- 開發中我們經常需要復制一個對象。如果直接用賦值會有下面問題:
// 一個 pink 對象 const pink = {name: 'pink老師',age: 18 } const red = pink console.log(red) // { name: 'pink老師', age: 18 } red.name = 'red老師' console.log(red) // { name: 'red老師', age: 18 } // 但是 pink 對象里面的 name 值也發生了變化 console.log(pink) // { name: 'red老師', age: 18 }
1.1 淺拷貝
- 首先淺拷貝和深拷貝只針對引用類型。
- 淺拷貝:拷貝的是地址
- 常見方法:
- 拷貝對象:Object.assgin() / 展開運算符 {…obj} 拷貝對象
- 拷貝數組:Array.prototype.concat() 或者 […arr]
- 例子1:
const obj = {uname: 'pink' } const o = { ...obj } console.log(o) // { uname: 'pink' } o.uname = 'red' console.log(o) // { uname: 'red' } console.log(obj) // { uname: 'pink' }
- 例子2:
// 一個 pink 對象 const pink = {name: 'pink老師',age: 18 } const red = {} Object.assign(red, pink) console.log(red) // { name: 'pink老師', age: 18 } red.name = 'red老師' console.log(red) // { name: 'red老師', age: 18 } // 不會影響 pink 對象 console.log(pink) // { name: 'pink老師', age: 18 }
- 例子3:
```js
// 一個 pink 對象
const pink = {
name: ‘pink老師’,
age: 18,
family: {
mother: ‘pink媽媽’
}
}
const red = {}
Object.assign(red, pink)
console.log(red) // { name: ‘pink老師’, age: 18 }
red.name = ‘red老師’
// 更改對象里面的 family 還是會有影響
console.log(red) // { name: ‘red老師’, age: 18 }
// 不會影響 pink 對象
console.log(pink) // { name: ‘pink老師’, age: 18 }- 如果是簡單數據類型拷貝值,引用數據類型拷貝的是地址 (簡單理解:如果是單層對象,沒問題,如果有多層就有問題)
- 直接賦值和淺拷貝有什么區別?
- 直接賦值的方法,只要是對象,都會相互影響,因為是直接拷貝對象棧里面的地址
- 淺拷貝如果是一層對象,不相互影響,如果出現多層對象拷貝還會相互影響
- 淺拷貝怎么理解?
- 拷貝對象之后,里面的屬性值是簡單數據類型直接拷貝值
- 如果屬性值是引用數據類型則拷貝的是地址
1.2 深拷貝
- 首先淺拷貝和深拷貝只針對引用類型
- 深拷貝:拷貝的是對象,不是地址
- 常見方法:
- 通過遞歸實現深拷貝
- lodash/cloneDeep
- 通過JSON.stringify()實現
1.2.1 通過遞歸實現深拷貝
- 函數遞歸:
- 如果一個函數在內部可以調用其本身,那么這個函數就是遞歸函數
- 簡單理解:函數內部自己調用自己, 這個函數就是遞歸函數
- 遞歸函數的作用和循環效果類似
- 由于遞歸很容易發生“棧溢出”錯誤(stack overflow),所以必須要加退出條件return
- 例子:
let num = 1 // fn 就是遞歸函數 function fn() {console.log('我要打印6次')if(num >= 6) {return}num++fn() // 函數內部調用函數自己 } fn()
- 利用遞歸函數實現 setTimeout 模擬 setInterval效果。
function getTime() {const time = new Date().toLocalString()console.log(time)setTimeout(getTime, 1000) // 定時器調用當前函數 } getTime()
- 通過遞歸函數實現深拷貝(簡版)
const o = {} function deepCopy(newObj, oldObj) {for(let k in oldObj) {if(oldObj[k] instanceof Array) {newObj[k] = []deepCopy(newObj[k], oldObj[k])}else if(oldObj[k] instanceof Object) {newObj[k] = {}deepCopy(newObj[k], oldObj[k])}else {newObj[k] = oldObj[k]}} }
1.2.2 js庫lodash里面cloneDeep內部實現了深拷貝
const obj = {uname: 'pink',age: 18,hobby: ['籃球', '足球'],family: {baby: '小pink'}
}
// 語法:_.cloneDeep(要被克隆的對象)
const o = _.cloneDeep(obj)
console.log(o)
o.family.baby = '老pink'
console.log(obj)
1.2.3 通過JSON.stringify()實現
const obj = {uname: 'pink',age: 18,hobby: ['籃球', '足球'],family: {baby: '小pink'}
}
const o = JSON.parse(JSON.stringify(obj))
console.log(o)
o.family.baby = '老pink'
console.log(obj)
2. 異常處理
2.1 throw 拋異常
- 異常處理是指預估代碼執行過程中可能發生的錯誤,然后最大程度的避免錯誤的發生導致整個程序無法繼續運行
- 總結:
- throw 拋出異常信息,程序也會終止執行
- throw 后面跟的是錯誤提示信息
- Error 對象配合 throw 使用,能夠設置更詳細的錯誤信息
- 例子:
function counter(x, y) {if(!x || !y) {// throw '參數不能為空!';throw new Error('參數不能為空!')}return x + y } counter()
2.2 try/catch 捕獲異常
- 我們可以通過try / catch 捕獲錯誤信息(瀏覽器提供的錯誤信息) try 試試 catch 攔住 finally 最后
- 總結:
- try…catch 用于捕獲錯誤信息
- 將預估可能發生錯誤的代碼寫在 try 代碼段中
- 如果 try 代碼段中出現錯誤后,會執行 catch 代碼段,并截獲到錯誤信息
- finally 不管是否有錯誤,都會執行
- 例子:
function foo() {try {// 查找 DOM 節點const p = document.querySelectro('.p')p.style.color = 'red'} catch(error) {// try 代碼段中執行有錯誤時,會執行 catch 代碼段// 查看錯誤信息console.log(error.message)// 終止代碼繼續執行return}finally {alert('執行')}console.log('如果出現錯誤,我的語句不會執行') } foo()
2.3 debugger
- 我們可以通過 try / catch 捕獲錯誤信息(瀏覽器提供的錯誤信息)
- 例子:
const arr = [1, 3, 5] const newArr = arr.map((item, index) => {debuggerconsole.log(item) // 當前元素console.log(index) // 當前元素索引號return item + 10 // 讓當前元素 + 10 }) console.log(newArr) // [11, 13, 15]
3. 處理this
- this 是 JavaScript 最具“魅惑”的知識點,不同的應用場合 this 的取值可能會有意想不到的結果,在此我們對以往學習過的關于【 this 默認的取值】情況進行歸納和總結。
3.1 this指向
3.1.1 普通函數this指向
- 普通函數的調用方式決定了 this 的值,即【誰調用 this 的值指向誰】
- 普通函數沒有明確調用者時 this 值為 window,嚴格模式下沒有調用者時 this 的值為 undefined。
- 例子1:
// 普通函數 function sayHi() {console.log(this) } // 函數表達式 const sayHello = function() {console.log(this) } // 函數的調用方式決定了 this 的值 sayHi() // window windows.sayHi()
- 例子2:
// 普通對象 const user = {name: '小明',walk: function() {console.log(this)} } // 動態為 user 添加方法 user.sayHi = sayHi user.sayHello = sayHello // 函數的調用方式決定了 this 的值 user.sayHi() user.sayHello()
- 例子3:
<script>'use strict'function fn() {console.log(this) // undefined}fn() </script>
3.1.2 箭頭函數this指向
- 箭頭函數中的 this 與普通函數完全不同,也不受調用方式的影響,事實上箭頭函數中并不存在 this !
- 箭頭函數會默認幫我們綁定外層 this 的值,所以在箭頭函數中 this 的值和外層的 this 是一樣的
- 箭頭函數中的this引用的就是最近作用域中的this
- 向外層作用域中,一層一層查找this,直到有this的定義
- 例子1:
console.log(this) // 此處為 window // 箭頭函數 const sayHi = function() {console.log(this) // 該箭頭函數中的 this 為函數聲明環境中 this 一致 }
- 例子2:
// 普通對象 const user = {name: '小明',// 該箭頭函數中的 this 為函數聲明環境中 this 一致walk: () => {console.log(this)} }
- 注意情況1:
- 在開發中【使用箭頭函數前需要考慮函數中 this 的值】,事件回調函數使用箭頭函數時,this 為全局的 window
- 因此DOM事件回調函數如果里面需要DOM對象的this,則不推薦使用箭頭函數
- 例子:
// DOM 節點 const btn = document.querySelector('.btn') // 箭頭函數,此時 this 指向了 window btn.addEventListener('click', () => {console.log(this) }) // 普通函數,此時 this 指向了 DOM 對象 btn.addEventListener('click', function () {console.log(this) })
- 注意情況2:
- 同樣由于箭頭函數 this 的原因,基于原型的面向對象也不推薦采用箭頭函數
- 例子:
function Person() { } // 原型對象上添加了箭頭函數 Person.prototype.walk = () => {console.log('人都要走路...')console.log(this); // window } const p1 = new Person() p1.walk()
- 總結:
- 函數內不存在this,沿用上一級的
- 不適用
- 構造函數,原型函數,dom事件函數等等
- 適用
- 需要使用上層this的地方
- 使用正確的話,它會在很多地方帶來方便,后面我們會大量使用慢慢體會
3.2 改變this
- JavaScript 中還允許指定函數中 this 的指向,有 3 個方法可以動態指定普通函數中 this 的指向:
- call()
- apply()
- bind()
3.2.1 call()–了解
- 使用 call 方法調用函數,同時指定被調用函數中 this 的值
- 語法:
fun.call(thisArg, arg1, arg2, ...)
- thisArg:在 fun 函數運行時指定的 this 值
- arg1,arg2:傳遞的其他參數
- 返回值就是函數的返回值,因為它就是調用函數
- 例子1:
const obj = {name: 'pink' } function fn() {console.log(this) // 指向 obj {name: 'pink'} } fn.call(obj)
- 例子2:
const obj = {name: 'pink' } function fn(x, y) {console.log(this) // 指向 obj {name: 'pink'}console.log(x + y) // 傳遞過來的參數相加 } fn.call(obj, 1, 2)
3.2.2 apply()-理解
- 使用 apply 方法調用函數,同時指定被調用函數中 this 的值
- 語法:
fun.apply(thisArg, [argsArray])
- thisArg:在fun函數運行時指定的 this 值
- argsArray:傳遞的值,必須包含在數組里面
- 返回值就是函數的返回值,因為它就是調用函數
- 因此 apply 主要跟數組有關系,比如使用 Math.max() 求數組的最大值
- 例子:
// 求和函數 function counter(x, y) {return x + y } // 調用 counter 函數,并傳入參數 let result = counter.apply(null, [5, 10]) console.log(result)
- 求數組最大值2個方法:
// 求數組最大值 const arr = [3, 5, 2, 9] console.log(Math.max.apply(null, arr)) // 9,利用apply console.log(Math.max(...arr)) // 9,利用展開運算符
3.2.3 bind()-重點
- bind() 方法不會調用函數。但是能改變函數內部this 指向
- 語法:
fun.bind(thisArg, arg1, arg2, ...)
- thisArg:在 fun 函數運行時指定的 this 值
- arg1,arg2:傳遞的其他參數
- 返回由指定的 this 值和初始化參數改造的 原函數拷貝 (新函數)
- 因此當我們只是想改變 this 指向,并且不想調用這個函數的時候,可以使用 bind,比如改變定時器內部的this指向.
- 例子:
// 普通函數 function sayHi() {console.log(this) } let user = {name: '小明',age: 18 } // 調用 bind 指定 this 的值 let sayHello = sayHi.bind(user); // 調用使用 bind 創建的新函數 sayHello()
3.2.4 call apply bind 總結
- 相同點:
- 都可以改變函數內部的this指向.
- 區別點:
- call 和 apply 會調用函數, 并且改變函數內部this指向.
- call 和 apply 傳遞的參數不一樣, call 傳遞參數 aru1, aru2…形式,apply 必須數組形式[arg]
- bind 不會調用函數, 可以改變函數內部this指向.
- 主要應用場景:
- call 調用函數并且可以傳遞參數
- apply 經常跟數組有關系. 比如借助于數學對象實現數組最大值最小值
- bind 不調用函數,但是還想改變this指向. 比如改變定時器內部的this指向.
4. 性能優化
4.1 防抖
- 所謂防抖(debounce),就是指觸發事件后在 n 秒內函數只能執行一次,如果在 n 秒內又觸發了事件,則會重新計算函數執行時間
- 現實例子:北京買房政策,需要連續5年的社保,如果中間有一年斷了社保,則需要從新開始計算
- 開發使用場景:搜索框防抖
- 假設輸入就可以發送請求,但是不能每次輸入都去發送請求,輸入比較快發送請求會比較多
- 我們設定一個時間,假如300ms, 當輸入第一個字符時候,300ms后發送請求,但是在200ms的時候又輸入了一個字符,則需要再等300ms 后發送請求
- 利用防抖來處理-鼠標滑過盒子顯示文字
const box = document.querySelector('.box') let i = 1 function mouseMove() {box.innerHTML = i++ } function debounce(fn, t = 500) {let timeIdreturn function () {// 如果有定時器,先清除if(timeId)clearTimeout(timeId)// 開啟定時器timeId = setTimeout(function() {fn()}, t)} } box.addEventListener('mousemove', debounce(mouseMove, 500))
- 核心思路:利用定時器實現,當鼠標滑過,判斷有沒有定時器,還有就清除,以最后一次滑動為準開啟定時器
4.2 節流
-
所謂節流(throttle),就是指連續觸發事件但是在 n 秒中只執行一次函數
-
現實例子:只有等到了上一個人做完核酸,整個動作完成了,第二個人才能排隊跟上
-
開發使用場景:小米輪播圖點擊效果、鼠標移動、頁面尺寸縮放resize、滾動條滾動 就可以加節流
- 假如一張輪播圖完成切換需要300ms,不加節流效果,快速點擊,則嗖嗖嗖的切換
- 加上節流效果,不管快速點擊多少次,300ms時間內,只能切換一張圖片。
-
利用節流來處理-鼠標滑過盒子顯示文字:
const box = document.querySelector('.box') let i = 1 function mouseMove() {box.innerHTML = i++// 如果存在開銷較大操作,大量數據處理,大量dom操作,可能會卡 } function throttle(fn, t = 500) {let startTime = 0return function () {let now = Date.now()if(now - startTime >= t) {fn()startTime = now}} } box.addEventListener('mousemove', throttle(mouseMove, 500))
- 利用節流的方式,鼠標經過,500ms,數字才顯示
-
節流和防抖的區別是?
- 節流: 就是指連續觸發事件但是在 n 秒中只執行一次函數,比如可以利用節流實現 1s 之內只能觸發一次鼠標移動事件
- 防抖:如果在 n 秒內又觸發了事件,則會重新計算函數執行時間
-
節流和防抖的使用場景是?
- 節流: 鼠標移動,頁面尺寸發生變化,滾動條滾動等開銷比較大的情況下
- 防抖: 搜索框輸入,設定每次輸入完畢n秒后發送請求,如果期間還有輸入,則從新計算時間
4.3 Lodash 庫實現節流和防抖
-
節流
const box = document.querySelector('.box') let i = 1 function mouseMove() {box.innerHTML = i++// 如果存在開銷較大操作,大量數據處理,大量dom操作,可能會卡 } box.addEventListener('mousemove', _.throttle(mouseMove, 1000))
-
防抖
const box = document.querySelector('.box') let i = 1 function mouseMove() {box.innerHTML = i++// 如果存在開銷較大操作,大量數據處理,大量dom操作,可能會卡 } box.addEventListener('mousemove', _.debounce(mouseMove, 1000))
5. 節流綜合案例
頁面打開,可以記錄上一次的視頻播放位置
- 思路:
- 在ontimeupdate事件觸發的時候,每隔1秒鐘,就記錄當前時間到本地存儲
- 下次打開頁面, onloadeddata 事件觸發,就可以從本地存儲取出時間,讓視頻從取出的時間播放,如果沒有就默認為0s
- 獲得當前時間 video.currentTime
- 代碼:
const video = document.querySelector('video') video.ontimeupdate = _.throttle(() => {localStorage.setItem('currentTime', video.currentTime) }, 1000) video.onloadeeddata = () => {video.currentTime = local.getItem('currentTime') || 0 }