1. 如何在ES5環境下實現let
這個問題實質上是在回答let和var有什么區別,對於這個問題,我們可以直接查看babel轉換前后的結果,看一下在循環中通過let定義的變量是如何解決變量提升的問題
babel在let定義的變量前加了道下划線,避免在塊級作用域外訪問到該變量,除了對變量名的轉換,我們也可以通過自執行函數來模擬塊級作用域
(function(){
for(var i = 0; i < 5; i ++){
console.log(i) // 0 1 2 3 4
}
})();
console.log(i) // Uncaught ReferenceError: i is not defined
不過這個問題並沒有結束,我們回到var
和let/const
的區別上:
- var聲明的變量會掛到window上,而let和const不會
- var聲明的變量存在變量提升,而let和const不會
- let和const聲明形成塊作用域,只能在塊作用域里訪問,不能跨塊訪問,也不能跨函數訪問
- 同一作用域下let和const不能聲明同名變量,而var可以
- 暫時性死區,let和const聲明的變量不能在聲明前被使用
babel的轉化,其實只實現了第2、3、5點
2. 如何在ES5環境下實現const
實現const的關鍵在於
Object.defineProperty()
這個API,這個API用於在一個對象上增加或修改屬性。通過配置屬性描述符,可以精確地控制屬性行為。Object.defineProperty()
接收三個參數:Object.defineProperty(obj, prop, desc)
參數 | 說明 |
---|---|
obj | 要在其上定義屬性的對象 |
prop | 要定義或修改的屬性的名稱 |
descriptor | 將被定義或修改的屬性描述符 |
屬性描述符 | 說明 | 默認值 |
---|---|---|
value | 該屬性對應的值。可以是任何有效的 JavaScript 值(數值,對象,函數等)。默認為 undefined | undefined |
get | 一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined | undefined |
set | 一個給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。當屬性值修改時,觸發執行該方法 | undefined |
writable | 當且僅當該屬性的writable為true時,value才能被賦值運算符改變。默認為 false | false |
enumerable | enumerable定義了對象的屬性是否可以在 for...in 循環和 Object.keys() 中被枚舉 | false |
Configurable | configurable特性表示對象的屬性是否可以被刪除,以及除value和writable特性外的其他特性是否可以被修改 | false |
對於const不可修改的特性,我們通過設置writable屬性來實現
function _const(key, value) {
const desc = {
value,
writable: false
}
Object.defineProperty(window, key, desc)
}
_const('obj', {a: 1}) //定義obj
obj.b = 2 //可以正常給obj的屬性賦值
obj = {} //無法賦值新對象
3. 手寫call()
call()` 方法使用一個指定的 this 值和單獨給出的一個或多個參數來調用一個函數 語法:`function.call(thisArg, arg1, arg2, ...)
call()的原理比較簡單,由於函數的this指向它的直接調用者,我們變更調用者即完成this指向的變更:
//變更函數調用者示例
function foo() {
console.log(this.name)
}
// 測試
const obj = {
name: '前端腦洞'
}
obj.foo = foo // 變更foo的調用者
obj.foo() // '前端腦洞'
基於以上原理, 我們兩句代碼就能實現call()
Function.prototype.myCall = function(thisArg, ...args) {
thisArg.fn = this // this指向調用call的對象,即我們要改變this指向的函數
return thisArg.fn(...args) // 執行函數並return其執行結果
}
但是我們有一些細節需要處理:
Function.prototype.myCall = function(thisArg, ...args) {
const fn = Symbol('fn') // 聲明一個獨有的Symbol屬性, 防止fn覆蓋已有屬性
thisArg = thisArg || window // 若沒有傳入this, 默認綁定window對象
thisArg[fn] = this // this指向調用call的對象,即我們要改變this指向的函數
const result = thisArg[fn](...args) // 執行當前函數
delete thisArg[fn] // 刪除我們聲明的fn屬性
return result // 返回函數執行結果
}
//測試
foo.myCall(obj) // 輸出'前端腦洞'
4. 手寫apply()
apply()
方法調用一個具有給定this值的函數,以及作為一個數組(或類似數組對象)提供的參數。 語法:func.apply(thisArg, [argsArray])
apply()
和call()
類似,區別在於call()
接收參數列表,而apply()
接收一個參數數組,所以我們在call()
的實現上簡單改一下入參形式即可
Function.prototype.myApply = function(thisArg, args) {
const fn = Symbol('fn') // 聲明一個獨有的Symbol屬性, 防止fn覆蓋已有屬性
thisArg = thisArg || window // 若沒有傳入this, 默認綁定window對象
thisArg[fn] = this // this指向調用call的對象,即我們要改變this指向的函數
const result = thisArg[fn](...args) // 執行當前函數(此處說明一下:雖然apply()接收的是一個數組,但在調用原函數時,依然要展開參數數組。可以對照原生apply(),原函數接收到展開的參數數組)
delete thisArg[fn] // 刪除我們聲明的fn屬性
return result // 返回函數執行結果
}
//測試
foo.myApply(obj, []) // 輸出'前端腦洞'
5. 手寫bind()
bind()` 方法創建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定為 bind() 的第一個參數,而其余參數將作為新函數的參數,供調用時使用。 語法: `function.bind(thisArg, arg1, arg2, ...)
從用法上看,似乎給call/apply包一層function就實現了bind():
Function.prototype.myBind = function(thisArg, ...args) {
return () => {
this.apply(thisArg, args)
}
}
但我們忽略了三點:
- bind()除了this還接收其他參數,bind()返回的函數也接收參數,這兩部分的參數都要傳給返回的函數
- new會改變this指向:如果bind綁定后的函數被new了,那么this指向會發生改變,指向當前函數的實例
- 沒有保留原函數在原型鏈上的屬性和方法
Function.prototype.myBind = function (thisArg, ...args) {
var self = this
// new優先級
var fbound = function () {
self.apply(this instanceof self ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)))
}
// 繼承原型上的屬性和方法
fbound.prototype = Object.create(self.prototype);
return fbound;
}
//測試
const obj = { name: '前端腦洞' }
function foo() {
console.log(this.name)
console.log(arguments)
}
foo.myBind(obj, 'a', 'b', 'c')() //輸出前端腦洞 ['a', 'b', 'c']
6. 手寫一個防抖函數
防抖和節流的概念都比較簡單,所以我們就不在“防抖節流是什么”這個問題上浪費過多篇幅了,簡單點一下: 防抖,即短時間內大量觸發同一事件,只會執行一次函數,實現原理為設置一個定時器,約定在xx毫秒后再觸發事件處理,每次觸發事件都會重新設置計時器,直到xx毫秒內無第二次操作,防抖常用於搜索框/滾動條的監聽事件處理,如果不做防抖,每輸入一個字/滾動屏幕,都會觸發事件處理,造成性能浪費。
function debounce(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
7. 手寫一個節流函數
防抖是延遲執行,而節流是間隔執行,函數節流即每隔一段時間就執行一次,實現原理為設置一個定時器,約定xx毫秒后執行事件,如果時間到了,那么執行函數並重置定時器,和防抖的區別在於,防抖每次觸發事件都重置定時器,而節流在定時器到時間后再清空定時器
function throttle(func, wait) {
let timeout = null
return function() {
let context = this
let args = arguments
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
func.apply(context, args)
}, wait)
}
}
}
實現方式2:使用兩個時間戳prev舊時間戳和now新時間戳,每次觸發事件都判斷二者的時間差,如果到達規定時間,執行函數並重置舊時間戳
function throttle(func, wait) {
var prev = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - prev > wait) {
func.apply(context, args);
prev = now;
}
}
}
8. 數組扁平化
對於[1, [1,2], [1,2,3]]這樣多層嵌套的數組,我們如何將其扁平化為[1, 1, 2, 1, 2, 3]這樣的一維數組呢:
1.ES6的flat()
const arr = [1, [1,2], [1,2,3]]
arr.flat(Infinity) // [1, 1, 2, 1, 2, 3]
2.序列化后正則
const arr = [1, [1,2], [1,2,3]]
const str = `[${JSON.stringify(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str) // [1, 1, 2, 1, 2, 3]
3.遞歸 對於樹狀結構的數據,最直接的處理方式就是遞歸
const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
let result = []
for (const item of arr) {
item instanceof Array ? result = result.concat(flat(item)) : result.push(item)
}
return result
}
flat(arr) // [1, 1, 2, 1, 2, 3]
4.reduce()遞歸
const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
return arr.reduce((prev, cur) => {
return prev.concat(cur instanceof Array ? flat(cur) : cur)
}, [])
}
flat(arr) // [1, 1, 2, 1, 2, 3]
5.迭代+展開運算符
// 每次while都會合並一層的元素,這里第一次合並結果為[1, 1, 2, 1, 2, 3, [4,4,4]]
// 然后arr.some判定數組中是否存在數組,因為存在[4,4,4],繼續進入第二次循環進行合並
let arr = [1, [1,2], [1,2,3,[4,4,4]]]
while (arr.some(Array.isArray)) {
arr = [].concat(...arr);
}
console.log(arr) // [1, 1, 2, 1, 2, 3, 4, 4, 4]