背景介紹:
從研一剛開始找實習到現在秋招,這一路經歷了不少八股拷打,經常被要求手撕一些js基礎題,每次面試完後不語,只是默默打開筆記,把被問到的八股/手撕自己整理,方便日後複習。因此,記錄了很多手撕題,在此做個分享,有誤之處歡迎討論指正。
下面的幾乎每道題都是筆者被大廠問到過的,都是些基礎的題目,基礎不牢地動山搖,書到用時方恨少啊~。切忌走馬觀花,務必深刻理解爛熟於心。建議以本文為大綱,自行拓展廣度和深度。面試感悟:
手撕題應該理解原理,練習/默寫3遍及以上,確保能立即寫出來。
基礎八股在回答時,一個是要説話條理清晰,第二個是回答全面。
數據類型
7種原始類型:String, Number, Boolean, Null, Undefined, Symbol, BigInt
引用類型:Object
拓展 - TS的數據類型
基本類型(如 string, number, boolean,undefined,symbol,bigint)
複合類型(如 array, tuple, object,enum)
特殊類型(如 any,unknown, void,never)
類型判斷的幾種方式?
typeof
typeof 判斷基礎類型和函數,但不能區分對象類型(不能區分Object和Array)。
注意typeof的兩個槽點:typeof null === 'object' 和 typeof NaN === 'number'
instanceof
a instanceof A用來判斷某個實例是否是某個構造函數創造出來的。原理是:這個實例的原型鏈上,有沒有這個構造函數的原型。
Object.prototype.toString.call()
Object 原型上的toString方法被xx調用,用來判斷xx的類型。
//判斷基礎類型
Object.prototype.toString.call(123); // "[object Number]"
//判斷數組
Object.prototype.toString.call([]); //"[object Array]"
//判斷其他內置類型
Object.prototype.toString.call(new Map()); //"[object Map]"
Object.prototype.toString.call(/abc/); // "[object RegExp]"
Array.isArray()
用來判斷是否為數組。
判斷數組的3中方式:
Array.isArray(arr)arr instanceof ArrayObject.prototype.toString.call(arg) === '[object Array]'
你用過Symbol和BigInt嗎?
Symbol
Symbol是一種ES6引入的、表示獨一無二值的第七種基本數據類型,主要用於避免命名衝突,作為對象屬性的標識符,以實現唯一標識和支持可定製的屬性
特性:使用Symbol用同樣字符串構造產生的變量,是不全等的。
let k1 = Symbol("KK");
console.log(k1); // Symbol(KK)
typeof(k1); // "symbol"
// 相同參數 Symbol() 返回的值不相等
let k2 = Symbol("KK");
k1 === k2; // false
使用場景舉例:
1.vue中創建provide/inject使用的key
//創建不同key
export const ThemeKey = Symbol('theme')
export const UserKey = Symbol('user')
//父組件
<script setup>
import { provide, ref } from 'vue'
import { ThemeKey, UserKey, UpdateUserKey } from './keys.js'
const theme = ref('dark')
const user = ref({ name: 'Alice', age: 25 })
provide(ThemeKey, theme)
provide(UserKey, user)
</script>
//子組件
<script setup>
import { inject } from 'vue'
import { ThemeKey, UserKey } from './keys.js'
const theme = inject(ThemeKey)
const user = inject(UserKey)
</script>
2.apply/call函數的實現中,為了防止污染屬性
Bigint
能創建任意大的整數,避免了Number的最大整數限制(2^53-1)
1.創建BigInt::在數字後面添加 n 後綴即可創建BigInt。也可以使用 BigInt() 函數。
2.支持兩個BigInt之間的加減乘除、取模% 和 指數**運算
const a = 10n;
const b = 5n;
console.log(a + b); // 15n
console.log(a * b); // 50n
Map和WeakMap的區別?
Map的key可以是任意值,當key為對象時,如果作為key的對象沒有被引用,Map的key不會被回收。
WeakMap的key只能是對象(Object/Array/Function)和Symbol,當key為對象時,如果作為key的對象沒有被引用,WeakMap的key會被回收——垃圾回收機制會回收該鍵值對。
Map和普通對象區別?
1.key的順序
Map保留了key-value插入的順序。
普通字面對象遍歷順序沒有保證。
2.key的類型
Map的key可以是任意類型(String, Boolean, Number, Symbol等)
普通字面對象的key只能是 String和Symbol(注意:當使用數字做key,實際上是給轉成了字符串處理);
3.迭代方式
Map本身是可迭代對象,可以被for of迭代。
普通字面對象本身不可迭代,需要藉助Object.keys()來迭代(不包含原型上的key)。(注意:使用for in 可以遍歷普通字面對象的所有key,這個key包含了來自原型上的key)
4.序列化
普通字面對象可以被JSON.stringify()序列化
Map無法被JSON.stringify()序列化
5.性能考慮
在涉及頻繁增刪鍵值對的場景下,Map 的性能通常優於普通對象
拓展 - for of可以迭代可迭代對象的原理,這個就寫在後面了~
浮點數精度 0.1+0.2!=0.3 問題
0.1+0.2 = 0.30000000000000004 。原因解釋:這是所有采用IEEE 754標準的浮點數都存在的問題,0.1和0.2在進行計算的時候要轉為二進制進行計算,但是0.1和0.2的二進制是無限循環的,那麼當計算(加)完成後的數也是無限循環小數,有效位存不下就會“截斷”。所以計算結果是一個近似值(會存在一點點誤差)。
解決方法1
toFixed(參數表示小數位數)toPrecision(參數表示有效數位數)
console.log((0.1 + 0.2).toFixed(1)); // 0.3
console.log((0.1 + 0.2).toPrecision(1)); // 0.3
解決方法2
使用mathjs等第三方庫,
解決方法3
不使用浮點數,轉換成整數計算(用整數表示浮點數)。比如1塊4分,比起1.04,可直接使用104。
手撕
深拷貝
1.JSON.stringify深拷貝存在的問題
- 序列化會丟失:undefined,symbol,function這些會被忽略
- 序列化過程中Date會變成字符串,RegExp會變成空{}
- 無法處理原型鏈 (原型鏈丟失)
- 無法處理循環引用(報錯)
循環引用問題舉例:
const a = {
next: null
}
const b = {
next = a;
}
a.next = b;
Ps.後面會有個JSON.stringify的實現(手撕題)哦~
2.深拷貝基礎版 - 考慮基礎類型、數組、對象和對象的原型鏈
/*
按基礎類型、數組、內置對象、對象 4塊分類處理
基礎類型 - 直接返回
數組 - 遍歷deepCopy
內置對象 - 可迭代的就迭代並deepCopy,Date和RegExp就重新new一個
對象 - 創建一個空的新對象(創建時帶原型),遍歷對象的所有屬性並deepCopy,依次掛到新對象上
*/
function deepCopy(obj){
const type = Object.prototype.toString.call(obj) // '[object String]','[object Object]'...
const isPrimitive = /String|Number|Boolean|Null|Undefined|Symbol|BigInt|Function/.test(type)
// 基礎類型
if(isPrimitive){
return obj
}else if(type === '[object Array]'){ //數組
return obj.map(item=>deepCopy(item))
}else if(type === '[object Map]'){
const map = new Map()
for(const [k, v] of obj.entries()){
map.set(k, deepCopy(v))
}
return map
}else if(type === '[object WeakMap]'){
const map = new WeakMap()
for(const [k, v] of obj.entries()){
map.set(k, deepCopy(v))
}
return map
}else if(type === '[object Set]' ){
const set = new Set()
for(const item of obj){
set.add(deepCopy(item))
}
return set
}else if(type === '[object WeakSet]'){
const set = new WeakSet()
for(const item of obj){
set.add(deepCopy(item))
}
return set
}else if(type === '[object Date]'){
return new Date(obj)
}else if(type === '[object RegExp]'){
return new RegExp(obj)
}else{ //對象
const ans = Object.create(obj.__proto__) // 考慮原型,以obj.__proto__創建一個新對象
for(const key of Object.keys(obj)){ // for in會把原型上的屬性直接拷貝過來,所以用keys()
ans[key] = deepCopy(obj[key])
}
return ans;
}
}
測試:
const obj = {
name: '瘋狂踩坑人',
children: [
{
name: 'God',
children: [{
name: 'Jessie'
}]
},
],
say : function(){
console.log('my name is '+this.name);
},
skills: ["CET-6", "Coding"],
relationship:{
parent: Symbol('parent'),
brothers: Symbol('brothers')
},
}
console.log(deepCopy(obj))
3.深拷貝高級版 - 考慮循環引用
/*
例. a = {next: a}
用WeakMap記錄上層出現過的對象,當某個屬性引用到前面出現的對象,説明出現了循環引用,直接從WeakMap返回該對象即可。
*/
function advanceDeepCopy(target){
const wkMap = new WeakMap()
function _deepCopy(obj){
const type = Object.prototype.toString.call(obj)
const isPrimitive = /String|Number|Boolean|Null|Undefined|Symbol|BigInt|Function/.test(type)
// 基礎類型
if(isPrimitive){
return obj
}else if(type === '[object Array]'){ //數組
obj.map(item=>_deepCopy(item))
}else if(type === '[object Map]'){
const map = new Map()
for(const [k, v] of obj.entries()){
map.set(k, _deepCopy(v))
}
return map
}else if(type === '[object WeakMap]'){
const map = new WeakMap()
for(const [k, v] of obj.entries()){
map.set(k, _deepCopy(v))
}
return map
}else if(type === '[object Set]' ){
const set = new Set()
for(const item of obj){
set.add(_deepCopy(item))
}
return set
}else if(type === '[object WeakSet]'){
const set = new WeakSet()
for(const item of obj){
set.add(_deepCopy(item))
}
return set
}else if(type === '[object Date]'){
return new Date(obj)
}else if(type === '[object RegExp]'){
return new RegExp(obj)
}else{ //對象。用weakMap記錄對象,如果後續要拷貝同一對象,則不要深拷貝,而是直接反返回記錄的這個對象
if(wkMap.has(obj)){
return wkMap.get(obj)
}
const ans = Object.create(obj.__proto__) // 考慮原型
wkMap.set(obj, ans)
for(const key of Object.keys(obj)){
ans[key] = _deepCopy(obj[key])
}
return ans;
}
}
return _deepCopy(target)
}
判斷是否為空對象
可以使用lodash中的isEmpty方法判斷,那麼如何實現isEmpty呢?
方式一:stringify. 存在的小問題是無法處理函數和循環引用
return !(JSON.stringify(obj) === '{}')
方式二:使用for ... in
for(let key in obj){
return true;
}
return false;
方式三:使用Object.keys (Date/RegExp/空Map/空Set/空數組 這些內置對象通過Object.keys獲取到都是一個空數組)
return !Object.keys(obj).length
判斷是否為空對象,除了判斷普通對象外,還需要判斷null、內置對象這些情況
function isEmpty(obj){
if(typeof obj === 'object' && obj !== null){
return !Object.keys(obj).length
}
return false;
}
//測試
console.log(isEmpty({})) // true
console.log(isEmpty({ name:'瘋狂踩坑人' })) // false
console.log(isEmpty([])) // true
console.log(isEmpty([1,2])) // false
console.log(isEmpty(new Date())) //true
原型和原型鏈
原型鏈
我覺得這篇文章 一文徹底搞懂原型鏈很清晰的解釋了原型鏈的,可以仔細看看。
這裏借用了一下這篇文章的圖,如下:
原型鏈關係對於剛接觸的同學會有點繞,所以一定要先建立先驗知識,記住下面兩點:
- 記住第1點:原型是一個普通對象(是普通對象,就有
__protop__指針) - 記住第2點:函數和普通對象上(默認/自動)都掛載了一個原型(函數通過
prototype指針指向,對象通過私有屬性__proto__指向)
如果普通對象是通過某個(構造)函數創建來的,那麼它們的原型是同一個。普通對象通過__proto__指向原型,剛才説了原型本身也是一個普通對象,有__proto__,這樣就構成了原型鏈。原型鏈就是通過__proto__一級級往上找原型形成的鏈。
下面舉一個例子來理解:
function A(name){
this.name = name
}
const a = new A('瘋狂踩坑人');
默認/自動/沒有手動修改的情況下,A.prototype(A的原型)是一個Object構建出來的對象,所以此時a的原型鏈如下:
a.__proto__ 指向 {..., __proto__指向{..., __proto__:null} }
打印結果如圖:
繼承
下面將重點介紹4種方式:借用構造函數繼承、原型式繼承、組合式繼承和寄生式組合式繼承。
還有另外兩種(原型式繼承, 寄生式繼承)就不貼代碼了,自行了解下即可。
1.借用構造函數繼承:在子構造方法中調用父構造方法,完成this的屬性方法綁定。
/*1.構造函數繼承*/
function test1(){
function Foo(label){
this.labels = [label]
this.say = function(){
console.log('my name is '+this.name+', a '+this.label+'.');
}
}
function Bar(name, label){
Foo.call(this, label)
this.name = name
}
const b1 = new Bar('瘋狂踩坑人', '程序員')
b1.say() // my name is 瘋狂踩坑人, labels: 程序員.
}
- 優點:創建子類對象可以傳參,每個子類實例保留自己獨立的屬性
- 缺點:方法不能共享(每個實例都有一個獨立的say方法)
2.原型鏈繼承:通過原型鏈,沿用祖先的屬性和方法。
/*2.原型鏈繼承*/
function test2(){
function Foo(label){
this.labels = [label]
}
Foo.prototype.say = function(){
console.log('my name is '+this.name+', labels: '+String(this.labels)+'.');
}
function Bar(name){
this.name = name;
}
Bar.prototype = new Foo('自由職業');
const b1 = new Bar('瘋狂踩坑人');
b1.labels.push('程序員')
const b2 = new Bar('瘋狂踩坑人2');
b2.labels.push('程序員2') //
b1.say() // my name is 瘋狂踩坑人, labels: 自由職業,程序員,程序員2.
b2.say() // my name is 瘋狂踩坑人2, labels: 自由職業,程序員,程序員2.
}
- 優點:方法共享,不用每個對象都創建一個方法
- 缺點:屬性共享,不能在創建子類實例時給父類傳參。(後續操作繼承的屬性時,會影響到所有實例)
3.組合式繼承:結合了前面兩項的優點
把實例方法都放在原型對象上,通過Child.prototype = new Father(),以實現函數複用。
通過Foo.call(this)繼承父類的屬性,並保留能傳參的優點.
/* 組合式繼承 */
function test3(){
function Foo(label){
this.labels = [label]
}
Foo.prototype.say = function(){
console.log('my name is '+this.name+', labels: '+String(this.labels)+'.');
}
function Bar(name, label){
Foo.call(this, label)
this.name = name;
}
Bar.prototype = new Foo();
const b1 = new Bar('瘋狂踩坑人', '自由職業');
b1.labels.push('程序員')
const b2 = new Bar('瘋狂踩坑人2', '自由職業2');
b2.labels.push('程序員2') //
b1.say() // my name is 瘋狂踩坑人, labels: 自由職業,程序員.
b2.say() // my name is 瘋狂踩坑人2, labels: 自由職業2,程序員2.
}
- 優點:屬性不共享,相互獨立,可以通過傳參來初始化。函數複用/共享。
- 缺點:直接用父類構造對象作為原型,這個原型是有一些不必要的屬性(浪費內存)。上述代碼中就是,子類實例本身有
this.labels,子類的原型上也有labels
4.寄生式組合式繼承:主要在組合式的基礎上做一個小改動——原型不使用new Foo(),而是使用Object.create(Foo.prototype)
function test4(){
// 屬性放構造方法裏,方法放原型上
function Foo(label){
this.labels = [label]
}
Foo.prototype.say = function(){
console.log('my name is '+this.name+', labels: '+String(this.labels)+'.');
}
function Bar(name, label){
Foo.call(this, label)
this.name = name;
}
Bar.prototype = Object.create(Foo.prototype, {
constructor:{
value: Bar
}
})
// 這樣一來,Bar.prototype = {__proto__: Foo.prototype}
const b1 = new Bar('瘋狂踩坑人', '自由職業');
b1.labels.push('程序員')
const b2 = new Bar('瘋狂踩坑人2', '自由職業2');
b2.labels.push('程序員2') //
b1.say() // my name is 瘋狂踩坑人, labels: 自由職業,程序員.
b2.say() // my name is 瘋狂踩坑人2, labels: 自由職業2,程序員2.
}
這是最完美的繼承方案。具備了組合式繼承的屬性不共享、方法共享的優點,同時解決了它的缺點,令子類的原型上沒有多餘的屬性。
手撕
instanceof
a instanceof A用來判斷某個實例是否是某個構造函數創造出來的。原理是:這個實例的原型鏈上,有沒有這個構造函數的原型。
function instanceOf(obj, constructor){
//obj的原型鏈上,是否存在constructor.prototype
let proto = obj.__proto__
while(proto){
if(proto === constructor.prototype){
return true;
}
proto = proto.__proto__
}
return false
}
測試:
function A(a){
this.a = a
}
function B(b){
this.b = b
}
// B繼承A
B.prototype = Object.create(B.prototype, {
constructor:{
value: B
}
})
const x = new B('hello')
console.log(x instanceof A); //true
console.log(instanceOf(x, A)); //true
作用域和作用域鏈
作用域鏈是指==在JavaScript中,當訪問一個變量時,解釋器會按照從內到外的順序查找變量的機制,這個查找的路徑就構成了一個鏈條==。這個鏈條由當前的作用域開始,然後逐級向上查找,直到全局作用域。
在var變量中,只有functon作用域和全局作用域。下面通過例子來説明作用域鏈:
var x = "global scope";
function checkscope(){
var x = "local scope";
console.log(x); // "local scope"
}
// 在checkscope函數中,先從checkscope函數域中找x變量,發現存在x的聲明,就使用函數域的x,不會找到外面的全局x.
var x = "global scope";
function checkscope(){
console.log(x); // "global scope"
}
// 在checkscope函數中,先從checkscope函數域中找x變量,發現沒有x聲明,然後從外面第一層(這裏是全局作用域)找到了x的聲明,就使用了外面的全局x
var, let, const 區別
var和function存在變量提升,以var舉例:
function funcTest() {
console.log(arg);
var arg = 2;
}
funcTest();
等價於
function funcTest() {
var arg; // 變量聲明被提升到函數作用域頂部,初始值為 undefined
console.log(arg); // 因此這裏輸出 undefined
arg = 2; // 賦值操作保留在原位執行
}
這樣一來var的變量可以先使用,再聲明。對於function關鍵字定義的函數也是如此。
var和let區別一
- var只有funciton作用域和全局作用域
- let則除了funciton作用域和全局作用域,還有塊級(
{})作用域
var和let區別二
- var有變量提升,可以先使用後聲明。
- let/const也有變量提升,但是由於
暫時性死區,先使用後聲明會導致報錯。
作用域考題一:
// 題1
for(var i=0; i<5; i++){
setTimeout(()=>{
console.log(i);
}, 1000)
}
// 5 5 5 5 5
// 每次迭代,都是一個新的塊作用域,每個塊作用域都有一個獨立的i變量
for(let i=0; i<5; i++){
setTimeout(()=>{
console.log(i);
}, 1000)
}
// 0 1 2 3 4
作用域考題二:
// 題2
var a = 1;
(() => {
console.log(a);
a = 2;
})();
// 輸出 1,查找到全局 a
var a = 1;
(() => {
console.log(a);
var a = 2;
})();
// 輸出 undefined,變量聲明提升
var a = 1;
(() => {
console.log(a);
let a = 2;
})();
// 輸出 報錯
閉包
閉包是什麼?
一句話説明:==閉包是一種編程形式,通過內部函數訪問外部函數的變量,從而做到變量複用和避免污染全局變量等作用。== (也可以簡單的把這個內部函數稱為閉包,一種共識的表示而已)
閉包需要滿足的條件:
- 函數嵌套,外面函數內部定義了一個內部函數
- 將內部函數返回
閉包舉例:
function outer() {
const x = 10;
function inner() {
console.log('inner: ', x);
}
return innner;
}
const fn = outer();
fn(); // inner: 10
下面舉一個反面例子,不少人會誤以為這也是一種閉包:
function outer(callback) {
const x = 10;
callback(x);
}
function inner() {
console.log('callback: ', x);
}
outer(inner); // callback: 10
這種通過參數將函數傳入的方式,閉包的兩個條件其實都不滿足。其實可以從本質上分析為什麼上面這種不是閉包。
首先,關於這個問題還可以進一步深入解釋,從詞法環境(執行上下文)和垃圾回收的角度去分析。時間充足且有興趣的同學可以看:
理解 JavaScript 中的執行上下文和執行棧
變量作用域,閉包
然後,這裏簡單分析下。
| 第一個例子 | 第二個例子 |
|---|---|
| 當執行outer函數時會創建一個詞法環境,執行棧將x、inner裝入。由於inner函數(閉包)被引用,導致執行棧中的x、inner變量都不會被清理,內存並沒有釋放。 | 當執行outer函數時會創建一個詞法環境,執行棧將x、inner裝入。當outer函數結束,outer的詞法環境(執行上下文)會被銷燬,執行棧中的變量會被清理。 |
手撕柯里化
一句話説明:把一個接收多個參數的函數,轉換為一系列接收單個參數的函數,直到參數全部收集才執行計算。
記住兩個關鍵詞:收集參數、延遲計算。
下面通過一個例子來認識下柯里化:
編寫一個像 sum(a)(b) = a+b 這樣工作的 sum 函數。
function sum(a, b){
return a+b;
}
function currySum(sum){
return function(a){
return function(b){
return a+b
}
}
}
這個currySum函數就被稱為"柯里化函數",它返回的函數稱為"被柯里化的函數"。看完這個例子,相信聰明的同學看出來了,一個通用型的柯里化函數是應該通過遞歸來實現的。下面就來手撕一個通用型的柯里化函數吧
實現一個通用的curry函數:
function curry(func, initArgs){
const arity = func.length; // func函數的形參數量
initArgs = initArgs || []
return function(...args){
const accArgs = initArgs.concat(args)
if(accArgs.length < arity){
return curry(func, accArgs)
}else {
return func.apply(this, accArgs) //誰調用了包裝器,這個func就被誰調用
}
}
}
測試:
function log(date, importance, info){
console.log(`${date} : ${importance} : ${info}`)
}
// 測試
const curriedLog = curry(log)
const logTool = curriedLog(new Date())
logTool('error', '出錯了!')
logTool('warn', '這是警告')
logTool('info', '正常輸出')
this指向
全局的this(瀏覽器環境): 1.undefined(嚴格模式); 2.window(非嚴格模式)
全局的this(node環境):1.嚴格模式,undefined; 2.非嚴格模式, 指向模塊自身的導出對象 module.exports
函數的this:
- 普通函數作為對象的方法調用,this指向該對象
- 構造函數調用(new),this指向實例對象
- 全局定義的函數會自動成為window的方法,直接調用相當於window調用,this是window
- 非全局定義的普通函數,通過賦值給全局變量,再調用,this是window
- 箭頭函數沒有自己的this,指向定義時所在this域
手撕
有三種方法可以改變函數this的指向,需要對這三種方法隨時能手撕出來。
| 方法 | 調用時機 | 參數形式 | 返回值 |
|---|---|---|---|
call |
立即執行 | 參數列表 (arg1, arg2, ...) |
原函數的執行結果 |
apply |
立即執行 | 參數數組 ([arg1, arg2, ...]) |
原函數的執行結果 |
bind |
不立即執行,返回新函數 | 參數列表 | 一個永久綁定 this 的新函數 |
apply
Function.prototype.apply = function(context, args){
const fn = Symbol('fn')
context[fn] = this
const res = context[fn](...args)
delete context[fn]
return res;
}
call
Function.prototype.call = function(context, ...args){
const fn = Symbol('fn')
context[fn] = this
const res = context[fn](...args)
delete context[fn]
return res;
}
bind
// 顯式綁定,不管誰調用,函數最終的this綁定的是context
Function.prototype.bind = function(context){
context = context || window
const fn = Symbol('fn')
context[fn] = this
return function (){
// 不是直接指向this()
// 而是改變this的指向,再執行
return context[fn](...arguments)
}
}
根據實現代碼,提一個小問題:為什麼需要用Symbol ?
因為要把函數this掛載到上下文context對象上,但不能污染上下文對象原來的屬性,所以用Symbol。
函數
箭頭函數和普通函數的區別
箭頭函數和普通函數的區別:
- 箭頭函數沒有this,不能改變this指向 (即無法通過call, apply, bind去顯示的改變this)
- 箭頭函數沒有arguments
- 箭頭函數不能作為構造函數 (不能new)
- 箭頭函數沒有原型
動態函數
API new Function ([arg1[, arg2[, ...argN]],] functionBody)
例子:
// 創建一個加法函數。最後一個參數字符串,可以被當做js執行
const add = new Function('a', 'b', 'return a + b');
console.log(add(2, 3)); // 輸出: 5
new實現
new的過程(5步):
- 創建一個空對象,作為將要返回的對象實例
- 將這個空對象的原型,指向了構造函數的
prototype屬性 - 將這個空對象賦值給函數內部的
this關鍵字 - 開始執行構造函數內部的代碼
- 如果構造函數返回一個對象,那麼就直接返回該對象,否則返回創建的對象
下面是代碼實現:
function myNew(Constructor, ...args) {
obj = Object.create(Constructor.prototype) //1,2
const result = Constructor.apply(obj, arguments) //3,4
return (result !== null && (typeof result === 'object' || typeof result === 'function')) ? result : obj; //5
}
數組
數組有哪些方法?
| 方法分類 | 方法名稱 | 描述 | 返回值 | 是否改變原數組 | ES版本 |
|---|---|---|---|---|---|
| 添加/刪除元素 | push() |
向數組末尾添加一個或多個元素 | 新數組的長度 | 是 | ES5 |
pop() |
刪除並返回數組的最後一個元素 | 被刪除的元素 | 是 | ES5 | |
unshift() |
向數組開頭添加一個或多個元素 | 新數組的長度 | 是 | ES5 | |
shift() |
刪除並返回數組的第一個元素 | 被刪除的元素 | 是 | ES5 | |
| 數組截取/拼接 | slice() |
返回數組的指定部分(淺拷貝) | 新數組 | 否 | ES5 |
splice() |
在指定位置刪除/添加元素 | 被刪除元素組成的數組 | 是 | ES5 | |
concat() |
合併兩個或多個數組 | 合併後的新數組 | 否 | ES5 | |
| 數組轉換 | join() |
將數組元素連接成字符串 | 字符串 | 否 | ES5 |
toString() |
將數組轉換為字符串 | 字符串 | 否 | ES5 | |
Array.from() |
將類數組對象轉換為數組 | 新數組 | 否 | ES6 | |
| 排序/反轉 | sort() |
對數組元素進行排序 | 排序後的數組 | 是 | ES5 |
reverse() |
反轉數組元素的順序 | 反轉後的數組 | 是 | ES5 | |
| 查找/判斷 | indexOf() |
查找元素第一次出現的索引 | 索引值(未找到返回-1) | 否 | ES5 |
lastIndexOf() |
查找元素最後一次出現的索引 | 索引值(未找到返回-1) | 否 | ES5 | |
includes() |
判斷數組是否包含某個元素 | 布爾值 | 否 | ES6 | |
find() |
查找第一個符合條件的元素 | 元素值(未找到返回undefined) | 否 | ES6 | |
findIndex() |
查找第一個符合條件的元素索引 | 索引值(未找到返回-1) | 否 | ES6 | |
findLast() |
查找最後一個符合條件的元素 | 元素值(未找到返回undefined) | 否 | ES2023 | |
findLastIndex() |
查找最後一個符合條件的元素索引 | 索引值(未找到返回-1) | 否 | ES2023 | |
| 遍歷/迭代 | forEach() |
遍歷數組執行回調函數 | undefined | 否 | ES5 |
map() |
對每個元素執行函數並返回新數組 | 新數組 | 否 | ES5 | |
filter() |
過濾符合條件的元素組成新數組 | 新數組 | 否 | ES5 | |
reduce() |
從左到右累加數組元素 | 累加結果 | 否 | ES5 | |
reduceRight() |
從右到左累加數組元素 | 累加結果 | 否 | ES5 | |
| 其他操作 | every() |
檢測所有元素是否都滿足條件 | 布爾值 | 否 | ES5 |
some() |
檢測是否有元素滿足條件 | 布爾值 | 否 | ES5 | |
flat() |
將嵌套數組扁平化 | 新數組 | 否 | ES6 | |
flatMap() |
先map後扁平化(深度為1) | 新數組 | 否 | ES6 | |
fill() |
用固定值填充數組元素 | 填充後的數組 | 是 | ES6 |
數組去重的方法?
方式一、使用Set
function unique(arr){
Array.from(Set(arr))
}
方式二、for雙重循環
function unique(arr){
const uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
let isUnique = true;
// 檢查當前元素是否已在結果數組中
for (let j = 0; j < uniqueArr.length; j++) {
if (arr[i] === uniqueArr[j]) {
isUnique = false;
break;
}
}
if (isUnique) {
uniqueArr.push(arr[i]);
}
}
return uniqueArr;
}
方式三、使用filter嵌套循環
function unique(arr){
return arr.filter((item, index) => arr.indexOf(item) === index);
}
方式1的時間複雜度O(n),另外兩個時間複雜度是O(n^2). (Set的插入和查找時間複雜度是O(1),所以能做到第一種方式時間複雜度O(n).)
迭代方式 for in 和 for of的區別?
for of 能迭代可迭代對象的原理
JavaScript 中許多內置類型默認就是可迭代的,因為它們都實現了 [Symbol.iterator]方法,例如數組、字符串、Map 和 Set。這也是你能直接用 for...of循環它們的原因。
看定義:
type IteratorFn = () => {
next(): { value: any; done: boolean };
};
type Iterator = {
[Symbol.iterator]: IteratorFn;
};
就是可迭代對象有一個屬性[Symbol.iterator],它表示一個函數,這個函數返回一個帶有next方法的對象。
不知道你見沒見過Map.next(),這説明了可迭代對象上可以通過不斷調用next方法來遍歷。
基於這一點,你可以創建一個對象來實現這種協議,從而變成可迭代的,比如:
const myIterable: Iterator = {
[Symbol.iterator]: () => {
let count = 0;
return {
next: () => {
if (count < 3) {
return { value: count++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
for in 工作原理
for in是用於遍歷對象屬性的一種循環機制。它可以遍歷對象上除了Symbol外的所有可枚舉屬性,包括繼承的可枚舉屬性。
// 創建一個對象並設置其原型
const proto = { inheritedProp: '來自原型' };
const obj = Object.create(proto);
obj.ownProp = '自身屬性';
// 使用 for...in 遍歷(會遍歷自身和原型鏈上的可枚舉屬性)
for (let key in obj) {
console.log(key); // 輸出: ownProp, inheritedProp
}
由於會找原型鏈上的屬性,所以性能不高。通常使用for of 遍歷Object.keys()來替代。
數組亂序的方法
核心思想:遍歷數組的每個位置,從該位置後面的元素中隨機選擇一個和該位置元素交換。
for (var i = 0; i < arr.length; i++) {
const randomIndex = Math.floor(Math.random() * (arr.length - i)) + i;
[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
//測試
var arr = [1,2,3,4,5,6,7,8,9,10];
console.log(arr);
隨機獲取數組的一個元素索引:
const randomIndex = Math.floor(Math.random() * arr.length)
隨機獲取數組某區間[i,j]的索引:
const randomIndex = Math.floor(Math.random() * (j-i+1)) + i
手撕
reduce
Array.prototype.myReduce = function(fn, initialValue) {
var arr = Array.prototype.slice.call(this);
var res, startIndex;
res = initialValue ? initialValue : arr[0]; // 不傳默認取數組第一項
startIndex = initialValue ? 0 : 1;
for(var i = startIndex; i < arr.length; i++) {
// 把初始值、當前值、索引、當前數組 傳遞給調用函數
res = fn.call(null, res, arr[i], i, this);
}
return res;
}
// 測試
const res = [1,2,3].myReduce((pre, cur)=>{
return pre + cur;
})
console.log(res) // 6
flat
遍歷數組,如果元素是數組,那麼遞歸繼續「打平」,返回一個新數組。
function flat(arr, n=1){
if(n<1){
return arr; //n<1後不打平了
}
let res = []
for(const item of arr){
if(Array.isArray(item)){
res = res.concat(flat(item, n-1));
}else{
res.push(item)
}
}
return res;
}
// 測試
const arr = [1,2,['ss','hh', ['peace', 'love', null, {name: '瘋狂踩坑人'}]], 3]
console.log(flat(arr, 1)); // [ 1, 2, 'ss', 'hh', [ 'peace', 'love', null, { name: '瘋狂踩坑人' } ], 3 ]
異步Promise
宏任務&微任務以及事件循環
這篇文章由淺入深瞭解瀏覽器的事件循環Event Loop可謂是比較詳細介紹了事件循環。這裏總結下就是:
- 瀏覽器/JS引擎中有兩個隊列:宏任務隊列、微任務隊列,分別用來存放瀏覽器的兩類異步任務——宏任務和微任務。
- 所謂的宏任務和微任務就是一段延後執行的腳本/回調函數,比如
setTimeout(fn, 5)這裏的fn將是一個宏任務。 - 瀏覽器先從微任務隊列中取出所有任務執行完,然後從宏任務隊列中取出一個宏任務執行。之後總是這樣循環:先取出所有微任務,再取一個宏任務執行。構成了事件循環。
下面列舉了宏任務和微任務:
宏任務
| # | 任務類型 | 瀏覽器 | Node |
|---|---|---|---|
| 1 | I/O (請求,讀寫文件) | ✅ | ✅ |
| 2 | setTimeout | ✅ | ✅ |
| 3 | setInterval | ✅ | ✅ |
| 4 | setImmediate | ❌ | ✅ |
微任務
| # | 任務類型 | 瀏覽器 | Node |
|---|---|---|---|
| 1 | process.nextTick | ❌ | ✅ |
| 2 | MutationObserver | ✅ | ❌ |
| 3 | IntersectionObserver | ✅ | ❌ |
| 4 | Promise.then/catch/finally | ✅ | ✅ |
Promise規範和原理
請你簡單介紹下Promise?
1.介紹創建:Promise初始化接受一個函數參數,該函數會立即執行。這個參數函數接受兩個參數:resolve函數、reject函數。Promise對象有3個狀態——pending,fulfilled和rejected,resolve能將狀態變成fulfilled,reject能將狀態變成rejected。一旦狀態從pending變成結果狀態,狀態就不再改變。
2.介紹then/catch方法:當Promise對象狀態變成結果狀態,就會調用then的回調方法,then函數接受兩個函數參數:成功回調&失敗回調;並且返回一個新的Promise,這使得Promise可以鏈式調用。then返回的這個新的Promise的結果狀態取決於上一個Promise的狀態和then的兩個回調函數的處理。catch方法可以作為錯誤的兜底處理。
3.介紹靜態方法:Promise.resolve, Promise.reject, Promsie.all, Promise.race, Promise.allSettled
建議時間充足的情況下,都嘗試自己實現一個簡單的Promise,理解其運行原理,可以參考「硬核JS」圖解Promise迷惑行為|運行機制補充或其他資料。
異步輸出練習
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
/*
過程分析:
宏任務隊列:setTimeout
微任務隊列:async1 end, promise2
輸出 =======================
script start
async1 start
async2
promise1
script end (到這裏,整個腳本結束。下面就是先取所有微任務,再取一個宏任務)
async1 end
promise2
setTimeout
*/
手撕
實現delay
實現一個delay函數,等待ms時間後繼續執行後面代碼。
function delay(ms){
return new Promise((resolve)=>{
setTimeout(()=>{
resolve()
}, ms)
})
}
async function test(){
await delay(2000)
console.log('print after ms')
}
實現promise的all/race/allSettled
實現all
all接受一個promise數組/可迭代對象,返回一個promise。當數組的所有promise都成功,則結果fulfilled;任一個失敗則結果是rejected
Promise.all([pa, pb]).then([resA, resB]=>{
// 全部成功
}).catch(e=>{
// 任一個失敗
})
具體實現:
Promise.all = (iterable) => {
return new Promise((resolve, reject) => {
// 處理空迭代對象的情況
if (!iterable || iterable.length === 0 || iterable.size === 0) {
return resolve([]);
}
const len = Array.isArray(iterable) ? iterable.length : iterable.size; // 數組是length, Map是size
const results = Array(len).fill(null);
let successCnt = 0;
// 處理每個promise
iterable.forEach((item, i) => {
Promise.resolve(item).then((res) => {
results[i] = res;
successCnt++;
if (successCnt === len) {
resolve(results);
}
}).catch(e => {
reject(e);
});
});
});
}
這裏注意,需要用Promise.resolve包裹item,因為item可能不是Promise。(Promise.resolve(item)在item是Promise的時候直接返回item,否則會返回一個內部創建的成功Promise,value是item)
實現race
race接受一個promise數組/可迭代對象,返回一個promise。當數組中任一個promise先成功,則結果fulfilled;任一個先失敗則結果是rejected
Promise.race([pa, pb]).then(res=>{
// 任一個先成功
}).catch(e=>{
// 任一個先失敗
})
具體實現:
Promise.race = (iterable)=>{
if(!iterable || iterable.length === 0 || iterable.size === 0){
return Promise.reject(new TypeError('Promise.race requires an iterable argument'))
}
// 重點
return new Promise((resolve, reject)=>{
iterable.forEach(item=>{
Promise.resolve(item).then((res)=>{
resolve(res)
}).catch(e=>{
reject(e)
})
})
})
}
實現allSettled
MDN allSettled規範
allSettled規範:
- 當參數數組中所有Promise都達到結束狀態時才結束promise,並且一定是返回一個成功的Promise
- 失敗的promise以
{status:'rejected', reason: r}形式保存到數組,成功的promise以{status:'fulfilled', value: v}的形式保存到數組。
具體實現
Promise.allSettled = (iterable)=>{
const len = Array.isArray(iterable) ? iterable.length : (iterable?.size || 0)
const results = Array(len)
let cnt = 0
return new Promise((resolve, reject)=>{
if (!iterable || len === 0) return resolve([])
iterable.forEach((p,i)=>{
Promise.resolve(p).then((val)=>{
results[i] = {status: 'fulfilled', value:val}
cnt++
if(cnt === len)
resolve(results)
}).catch(e=>{
results[i] = {status:'rejected', reason:e}
cnt++
if(cnt === len)
resolve(results)
})
})
})
}
//測試
const p1 = new Promise((resolve, reject)=>{
setTimeout(_=>resolve(1), 2000)
})
const p2 = new Promise((resolve, reject)=>{
setTimeout(_=>reject('bad'), 1000)
})
Promise.allSettled([p1, p2]).then(results=>{
console.log(results);
/*
[
{ status: 'fulfilled', value: 1 },
{ status: 'rejected', reason: 'bad' }
]
*/
})
限制異步併發數
方式一: 先運行limit個promise,然後當其中一個promise結束,喚醒下一個promise執行。
Promise.limitConcurrency = (urls, request, limit)=>{
if (!urls || !urls.length) return Promise.resolve([]);
return new Promise((resolve)=>{
let idx = 0; //資源序號
const result = Array(urls.length).fill(null)
let finishCnt = 0;
function next(){
if(idx === urls.length){ //調完
return
}
const curIdx = idx; //記錄索引快照,用於then後的回調使用
idx++;
request(urls[curIdx]).then((val)=>{
result[curIdx] = {
status:'fulfilled',
value: val
}
console.log({
status:'fulfilled',
value: val
});
}).catch(err=>{
result[curIdx] = {
status:'rejected',
reason: err
}
console.log({
status:'rejected',
reason: err
});
}).finally(()=>{
next()
finishCnt++;
if(finishCnt === urls.length){
resolve(result)
}
})
}
// 先執行limit個,在next內部,當一個promise結束時,再啓動下一個。
for(let i=0; i<limit && i<urls.length; i++){
next();
}
})
}
測試代碼:
// Mock請求
const request = item=> new Promise((resolve, reject)=>{
setTimeout(()=>{
if(item===2){
reject('error')
}else{
resolve(item)
}
}, 1000)
})
const urls = [1,2,3,4,5] // 請求資源
Promise.limitConcurrency(urls, request, 2).then(results=>{
console.log(results);
})
你可以按我下面的思路來記住/默寫這個方法
1.先搭建框架,傳入多個資源、一個執行資源的異步方法和限制併發數limit. 結果返回一個Promise.
Promise.limitConcurrency = (urls, request, limit)=>{
if (!urls || !urls.length) return Promise.resolve([]);
return new Promise((resolve)=>{
const result = Array(urls.length).fill(null); //存放結果
})
}
2.先假設有一個next方法,用來執行一個promise產生結果。一開始要執行limit次next方法。
Promise.limitConcurrency = (urls, request, limit)=>{
if (!urls || !urls.length) return Promise.resolve([]);
return new Promise((resolve)=>{
const result = Array(urls.length).fill(null); //存放結果
//next執行promise和處理結果
function next(){}
//先執行 limit次
for(let i=0; i<limit && i<urls.length; i++){
next();
}
})
}
3.完善next方法,當next中的promise結束了,應該喚起下一個next(promise)的執行。並且考慮當所有promise結束,resolve結果.
Promise.limitConcurrency = (urls, request, limit)=>{
if (!urls || !urls.length) return Promise.resolve([]);
return new Promise((resolve)=>{
const result = Array(urls.length).fill(null); //存放結果
let idx = 0;
let finishCnt = 0;
//next執行promise和處理結果
function next(){
//記錄索引快照,用於then後的回調使用。這一步需要稍微理解下為什麼要curIdx。因為request調用後要保存結果到result,不確定當前的異步什麼時候結束,而idx在這期間可能是變化了的,所以要保留一個快照。
const curIdx = idx;
idx++;
request(urls[curIdx]).then(()=>{
//...
})
.catch(e=>{})
.finally(()=>{
next(); //喚醒下一次
finishCnt++;
if(finishCnt === urls.length){
resolve(result)
}
})
}
//先執行 limit次
for(let i=0; i<limit && i<urls.length; i++){
next();
}
})
}
4.考慮每次promise執行完成結果怎麼保存。就是最終版本辣~
//自行默寫一遍哦~
方式二: 使用race的特性,只要有一個成功就結束,這樣可以做到有一個完成後添加下一個promise執行。
Promise.limitConcurrency = async (urls, request, limit) => {
const executing = [];
const result = Array(urls.length).fill(null)
for (let i=0; i<urls.length; i++) {
const p = request(urls[i]).then((val)=>{
result[i] = {
status:'fulfilled',
value: val
}
console.log({
status:'fulfilled',
value: val
})
}).catch(err=>{
result[i] = {
status:'rejected',
reason: err
}
console.log({
status:'fulfilled',
reason: err
})
}).finally(()=>{
executing.splice(executing.indexOf(p), 1); //完成後刪除,讓位給下一個promise
});
executing.push(p);
if (executing.length >= limit) { //執行隊列達到限制,就race。當其中一個promise完成了就會出隊讓出位置來。
await Promise.race(executing);
}
}
if(executing.length)
await Promise.all(executing);
return result
}
解釋:
1.executing 數組存放執行中的promise。
2.遍歷url,構建promise,將promise放入executing。同時每個promise在結束後需要被從executing中移除。
3.當executing達到限制,就race併發,待其中一個完成後繼續循環(繼續往executing加promise)
4.executing中剩下的用all併發,全部完成後就返回結果。
實現串行請求
有一個資源數組(每個資源可以用來創建Promise),實現一個函數,接受這個數組做到串行執行每個資源promise。
具體來説,有[1,2,3]。像下面這樣的就是串行執行了:
// 寫法一
createPromise(1).then(()=>{
return createPromise(2).then(()=>{})
}).then(()=>{
return createPromise(3).then(()=>{})
})
// 寫法二
createPromise(1).then(()=>{
return createPromise(2).then(()=>{
return createPromise(3).then(()=>{
})
})
})
輔助代碼:
const createPromise = (id) => new Promise((solve, reject) =>
setTimeout(() => {
console.log("promise", id);
if(id === 2){
reject('2 error')
}
solve(id);
}, 1000)
);
// 實現queueExecPromise
// 測試
queueExecPromise([1,2,3]);
實現方式很多。
按寫法一的形式來實現: 前一個promise處理了,能在本次迭代拿到前一個promise。前一個promise的then方法中創建本次的promise.
function queueExecPromise(arr){
arr.reduce((prePromise, cur)=>{
return prePromise.then(val=>{
return createPromise(cur)
}).catch(e=>{
return undefined
})
}, Promise.resolve())
}
按寫法二的形式來實現: 一種遞歸的實現方式。(其實和併發限制很像,不過limit=1)
function queueExecPromise(arr){
function next(){
if(arr.length === 0){
return Promise.resolve()
}
const cur = arr.shift()
return createPromise(cur).then(val=>{
return next()
}).catch(e=>{
return undefined
})
}
return next()
}
還可以使用async/await 來實現
async function queueExecPromise(arr){
for(const item of arr){
try {
const res = await createPromise(item)
console.log(res)
} catch (e) {
console.log(e)
}
}
}
實現一個異步調度器
異步調度器 可以添加任務,然後調用flushQueue方法來刷新所有任務(即執行完成所有任務),並且執行任務有併發限制。
class Scheduler {
constructor(limit){
this.limit = limit;
this.queue = [];
}
add(taskFn){
this.queue.push(taskFn)
}
// 執行
flushQueue(){
for(let i=0; i<this.limit; i++){
this.next()
}
}
next(){
if(this.queue.length<=0){
return;
}
const task = this.queue.shift()
if(task){
task().then(res=>{
// console.log(res);
this.next()
}).catch(e=>{
// console.log(e);
this.next();
})
}
}
}
測試:
//測試
let scheduler = new Scheduler(2);
const addTask = (time, order) => {
const task = () => {
return new Promise((reslove, reject) => {
setTimeout(() => {
console.log(order);
reslove();
}, time);
});
};
scheduler.add(task);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.flushQueue();
實現一個具有重試功能的request
實現一個request,可以在失敗的時候重試,額外有兩個參數:
- interval 重試時間間隔(s)
- retry 重試次數(次)
async function request(url, options, interval=3, retry=5){
let leftCount = retry
// 閉包+遞歸實現
const _requestData = (_url, _options)=>{
return fetch(_url, _options).catch((err)=>{
// 失敗 => 重試(遞歸)
if(leftCount > 0){
return new Promise((resolve, reject)=>{ //=========catch返回的promise將取決於這個promise結果==================
setTimeout(()=>{
leftCount--;
console.log('重試...'+(retry-leftCount));
_requestData(_url, _options).then(resolve, reject)
}, interval*1000)
})
}else {
throw err
}
})
}
return _requestData(url, options)
}
測試:
request('https://www.baidusdf.com',{Method:"POST"}).then(res=>{
console.log(res);
}).catch(err=>{
console.log('出錯了!');
console.log(err);
})
/*
重試...1
重試...2
重試...3
重試...4
重試...5
出錯了!
TypeError: fetch failed
*/
節流防抖
手撕
節流
簡單實現:
// 節流:每單位時間只執行一次。
// 場景:滾動;收藏/點贊按鈕
// 實現:基於時間戳,判斷時間差是否大於等於delay,來決定是否執行
function throttle(fn, delay){
let lastTime = 0
return function(){
if(Date.now() - lastTime > delay){
fn.apply(this, arguments)
lastTime = Date.now()
}
}
}
Ps.也可以使用setTimeout定時器來實現。
function throttle2(fn, delay){
let timer = null
return function(){
if(timer){
return;
}
fn.apply(this, arguments)
timer = setTimeout(()=>{
timer = null;
}, delay)
}
}
測試:
let cnt = 0
const throttledFn = throttle((x)=>{
console.log(x);
}, 1000)
setInterval(()=>{
throttledFn(cnt)
cnt++
}, 400)
進階要求:保證節流的最後一次調用一定執行。
function throttle2(fn, delay){
let timer = null
let lastTimer = null;
return function(){
if(timer){ // 凍結期
if(lastTimer){
clearTimeout(lastTimer)
}
lastTimer = setTimeout(()=>{
lastTimer = null
fn.apply(this, arguments)
}, delay)
return;
}
fn.apply(this, arguments)
timer = setTimeout(()=>{
timer = null;
clearTimeout(lastTimer)
}, delay)
}
}
核心思想:
在凍結期間,設置一個setTimeout定時器(對應lastTimer)觸發fn調用,這個定時任務和凍結前的setTimeout(對應timer)的定時任務是互相“競賽”的。
若timer定時先觸發則最後一次就被取消,若lastTimer定時先觸發就是最後一次執行。
防抖
簡單實現:
// 防抖:延遲執行函數,等觸發停止了單位時間再執行 (每次觸發重新計算延遲時間)
// 場景:搜索輸入;調整窗口大小
// 實現: 函數觸發後, 延遲單位時間後執行,如果單位時間內觸發,則重新計時
function debounce(fn, delay){
let timer = null
return function(){
if(timer){
clearTimeout(timer)
}
timer = setTimeout(()=>{
fn.apply(this, arguments)
}, delay)
}
}
測試:
let cnt = 0
const debouncedFn = debounce((x)=>{
console.log(x);
}, 1000, true)
setTimeout(()=>{
debouncedFn(cnt)
cnt++
}, 500)
setTimeout(()=>{
debouncedFn(cnt)
cnt++
}, 500)
setTimeout(()=>{
debouncedFn(cnt)
cnt++
}, 1000)
訂閲發佈模式
手撕
常見的問法:"寫一個EventBus"、"寫一個EventMitter"和"寫一個訂閲發佈/觀察者模式"。
/*
功能要求:實現on, off, emit, once
*/
// 調度中心/中介: 負責訂閲事件、通知事件
function Event(){
this.listeners = {
}
}
// 1.註冊/訂閲
Event.prototype.on = function(eventName, listener){
if(this.listeners.hasOwnProperty(eventName)){
this.listeners[eventName].push(listener)
}else{
this.listeners[eventName] = [listener]
}
}
// 2.註銷
Event.prototype.off = function(eventName, listener){
if(this.listeners.hasOwnProperty(eventName)){
this.listeners[eventName] = this.listeners[eventName].filter(e=>e!==listener)
}
}
// 3.once 一次註冊,用完即註銷
Event.prototype.once = function(eventName, listener){
const context = this
const fn = function(...args){
context.off(eventName, fn)
listener(...args)
}
this.on(eventName, fn)
}
// 4.通知
Event.prototype.emit = function(eventName, ...args){
if(this.listeners.hasOwnProperty(eventName)){
for(const listener of this.listeners[eventName]){
listener(...args)
}
}
}
寫訂閲發佈模式有兩個注意點:
- 需要判斷是否有該事件,使用hasOwnProperty;
- once註冊,不是將listener直接註冊,而是註冊一個包裝函數,用完即註銷(這種方式比較優雅)。
另外,有些面試官會提到,可不可以訂閲後產生一個ID,後續通過ID取消某個訂閲?
解決這個問題,只需要Event維護一個全局的id,然後原來的listeners[eventName]從數組結構,改為對象結構({[id]:callback} , id做key,回調函數做值)。
正則相關
正則基礎知識
字符串的方法
1.match方法
string.match(regexp) 匹配到項則返回一個數組,沒有則返回null。數組的內容則根據正則是否是g模式有區分。
- 不是g模式:
[0]匹配的完整文本,後續元素表示捕獲組(括號()匹配的文本),最後兩項分別是index(匹配的字符串的起始位置)、input(原字符串) - 是g模式:返回所有匹配的字符串構成的數組(此時沒有捕獲組、index和input)
2.replace方法
string.replace(regexp, fn) ,當regexp不是g模式,則只調用一次fn來替換。當regexp是g模式則全局匹配了多少次就會調用多少次fn來替換。舉個例子如下:
//第二個回調函數fn的參數分別是 匹配的字符串、捕獲組、匹配字符串的起始索引和原始字符串引用(和match方法的非g模式是一樣的)
export function test(){
const res = "1abc".replace(/(ab)(c)/g, (match, g1, g2, index, intput)=>{
console.log(match) //abc
console.log(g1) //ab
console.log(g2) //c
console.log(index) //1
console.log(intput) //1abc
return '0'
})
console.log(res); //10
}
正則對象的方法
1.test方法
regexp.test(string)。測試字符串是否匹配正則,返回Boolean。
2.exec方法
regexp.exec(string)。g模式下,返回結果和match的非g模式幾乎一樣。不同的是正則對象可以多次調用exec方法,從而不斷的匹配下一項(符合全局模式行為),舉個例子:
export function test(){
const reg = /(ab)(c)/g
const str = "1abc2abc"
const res = reg.exec(str)
const res2 = reg.exec(str)
console.log(res); // [ 'abc', 'ab', 'c', index: 1, input: '1abc2abc', groups: undefined ]
console.log(res2); //[ 'abc', 'ab', 'c', index: 5, input: '1abc2abc', groups: undefined ]
}
手撕
千分位
function formatPrice(price) {
return String(price).replace(/\B(?=(\d{3})+$)/g, ',');
}
//測試
console.log(formatPrice('888999')); //888,999
console.log(formatPrice('7888999')); //7,888,999
console.log(formatPrice('77888999')); //77,888,999
解釋:
- 首先
(\d{3})+$匹配3個數字3個數字一組的,並以3個數字結尾。 ?=表示後面的正則內容僅僅是斷言(不是實際匹配),換句話説就是僅斷言是否能匹配,但不作為最終匹配結果,這樣就不會被replace掉。\B表示非單詞邊界(即前面應該有字符)
插值語法 {{}}匹配
如題:
/*
將插值{{}}的內容替換
*/
let str = "我是{{name }},年齡{ {age }},愛好{{ hobby}}";
const obj = {
name:'瘋狂踩坑人',
age: 24,
hobby: 'buy'
}
代碼:
function replaceVar(str){
return str.replace(/\{\s*?\{(.*?)\}\s*?\}/g, (matchStr, g1)=>{
return obj[g1.trim()]
})
}
console.log(replaceVar(str)); //我是瘋狂踩坑人,年齡24,愛好buy
url的params獲取
function getUrlQuery(url, key) {
const queryObj = {}
const matches = /.+?\?(.+)/.exec(url)
if(matches){
//matches是數組,從第二個參數開始是捕獲分組
if(!matches[1])
return undefined
const hashStartIndex = matches[1].indexOf('#')
const query = hashStartIndex===-1?matches[1]: matches[1].slice(0,hashStartIndex) //去hash
if(query){
const queries = query.split('&')
queries.reduce((acc, item)=> {
const kw = item.split('=')
const key = kw[0].trim()
if(acc[key] === undefined){
acc[key] = kw[1]
}else{
acc[key] = Array.isArray(acc[key]) ? [...acc[key], kw[1]] : [acc[key], kw[1]]
}
return acc
}, queryObj)
}
}
return key ? queryObj[key] : queryObj
}
const query = getUrlQuery('http://www.google.com/search?q=javascript&aqs=chrome.0.0l6j69i60j69i61j69i60.3518j1j7&sourceid=chrome&test=1&test=2#title');
console.log(query);
一些API實現
JSON.stringify (wxg經典的一道手撕)
只考慮普通字面對象,如何實現JSON.stringify呢?先了解下一些規則
- 對於對象、數組會遞歸序列化,使用
{}、[]表示對象和數組邊界 - 會丟失:Undefined/Function/Symbol
- 字符串需要
""邊界,其他基礎數據類型Number/Boolean/Null 等轉字符串
舉例:
const obj = {
name: '瘋狂踩坑人',
children: ['good', 'bad', {rank: 1, has: false}],
say: ()=>{
console.log('hhh')
},
x:undefined,
y:null,
z:Symbol(1),
1:1,
}
// 轉換後
// {"1":1,"name":"瘋狂踩坑人","children":["good","bad",{"rank":1,"has":false}],"y":null}
實現:
JSON.mystringify = (obj)=>{
const type = typeof obj
if(type === 'object' && obj!==null){ //對象
if(Array.isArray(obj)){
let ans = ''
for(const item of obj){
const value = JSON.mystringify(item)
if(value){
if(ans!==''){
ans += ','
}
ans += value
}
}
return `[${ans}]`
}else{ //簡單處理, 其實還要考慮Map, Set
let ans = ''
for(const key of Object.keys(obj)){
const value = JSON.mystringify(obj[key])
if(value){
if(ans!==''){
ans += ','
}
ans += `"${key}":${value}`
}
}
return `{${ans}}`
}
}else if( /undefined|function|symbol/.test(type)){
// 忽略function 、undefined 、symbol
return
}else{
// 基礎類型
return type === 'string' ? `"${obj}"` : String(obj)
}
}
面試官可能會深挖的點:
1.考慮Map,Set,情況是怎麼樣的?
Map和Set對象會被處理為空對象{}。
2.循環依賴了,怎麼處理?
使用一個WeakSet來記錄已訪問過的對象,如果遇到了訪問過的,説明循環依賴了,拋出錯誤
parseInt
leetcode有一道類似的題 字符串轉換整數 (atoi)
這裏實現一個簡單版本(一個正整數字符串 轉 數字類型,忽略第二個參數,就按10進制來).
比如:"42" -> 42
代碼:
function parseInt(str){
const baseCode = "0".charCodeAt(0)
let ans = 0;
for(let i=0; i<str.length; i++){
const curCode = str.charCodeAt(i)
const num = curCode - baseCode
ans *= 10;
ans += num;
}
return ans;
}
// 測試
console.log(parseInt("123")); //123
console.log(parseInt("042")); //42
這裏是有一點技巧的:
- 利用ASCII碼做減法,算出字符的數值
- 從左到右遍歷,每次對ans先乘10,一方面完成權重增加,另一方面有效處理了前導0。
trim
這是字符串的trim方法,作用:刪除兩端的空格。
function trim(str){
let i=0, j=str.length-1;
while(i<=j && str[i]===' '){
i++;
}
while(i<=j && str[j]===' '){
j--;
}
return str.substring(i, j+1)
}
// 測試
console.log(trim("x ab c d 1 ") + '_end');
console.log(trim(" x ab c d 1") + '_end');
console.log(trim(" x ab c d 1 ") + '_end');
lodash.get
function get(obj, str, defaultVal=undefined){
const strType = typeof str
let path = []
if(strType === 'string'){
path = str.split('.')
}else if(Array.isArray(str)){
path = str
}else{
return defaultVal
}
let i=0;
let parent = obj
while(i<path.length ){
const type = typeof parent[path[i]]
if(type === 'object' && type !== null){ //繼續搜
parent = parent[path[i]]
i++;
}else{
return parent[path[i]]===undefined ? defaultVal: parent[path[i]]
}
}
return parent;
}
測試:
const _ = {
get
}
const object = {
a: {
b: {
c: 3
}
}
};
console.log(_.get(object, 'a.b.c')); // 輸出: 3
console.log(_.get(object, 'a.b.x', 'default')); // 輸出: 'default'
console.log(_.get(object, ['a', 'b'])); // { c: 3 }
console.log(_.get(object, ['a', 'b', 'c'])); // 輸出: 3
遞歸/迭代 思想
1.路徑命名轉樹(字節面)
// 將arr轉為output的形式
const arr = ['A', 'A.B', 'A.B.C', 'A.C', 'A.C.D', 'E', 'E.F', 'E.G', 'E.G.H']
const output = [
{
name: 'A',
children: [
{
name: 'B',
children: [
{
name: 'C',
children: []
}
]
},
{
name: 'C',
children: [
{
name: 'D',
children: []
}
]
}
]
},
{
name: 'E',
children: [
{
name: 'F',
children: []
},
{
name: 'G',
children: [
{
name: 'H',
children: []
}
]
}
]
}
]
實現思路:路徑字符串轉數組,然後沿數組一級級找(沒找到就創建節點掛載到主體上,找到節點則作為新主體)。
function transform(arr){
const root = {children:[]}; //頂級節點
// 根據path一級級往下找,插入到root中
const _insert = (path)=>{
let cur = root;
for(const item of path){
const newCur = cur.children.find(child=>child.name === item);
if(newCur){ //找到節點,直接更新主體
cur = newCur
}else{ //沒找到節點,創建節點,掛載到主體上
const s = {name: item, children:[]}
cur.children.push(s)
cur = s;
}
}
}
for(const item of arr){
//1.轉數組
const names = item.split('.')
//2.沿數組找,插入
_insert(names)
}
return root.children;
}
2.解析DOM屬性(美團面)
<div>
<span>
<a>網址1</a>
</span>
<span>
<a>網址2</a>
<a>網址3</a>
</span>
</div>
已知一個DOM結構如上,轉換為下面的JSON格式
{
tag: 'DIV',
children: [
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] }
]
},
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] },
{ tag: 'A', children: [] }
]
}
]
}
實現代碼:
function dom2json(domTree){
const json = {}
if(typeof domTree === 'object' && domTree !== null){
json.tag = domTree.tagName
json.children = domTree.childNodes.map(child => dom2json(child))
}
return json;
}
3.對象的key駝峯化命名(騰訊面)
// 輸入
const input = {
err_msg:'hhh',
my_real_data: {
list: ['item1', {list_children: []}]
},
count: 13,
errors: [{field:'name', error_msg:'xx'}]
}
// 輸出
{
errMsg: 'hhh',
myRealData: { list: [ 'item1', listChildren:[] ] },
count: 13,
errors: [ { field: 'name', errorMsg: 'xx' } ]
}
實現代碼:
function transformKey(key){
return key.replace(/_([a-z])/g, (matchStr, g1)=>{
return g1.toUpperCase();
})
}
function transformObj(obj){
const type = typeof obj;
// 對象
if(type === 'object' && type!==null){
if(Array.isArray(obj)){
return obj.map(item=>transformObj(item))
}else{
let newObj = {}
for(const key of Object.keys(obj)){
const newKey = transformKey(key)
// console.log(newKey);
newObj[newKey] = transformObj(obj[key])
}
return newObj
}
}else{// 基礎數據類型
return obj;
}
}
4.扁平數組轉嵌套數組/Tree(猿輔導面)
//輸入
const data = [
{id:1, name:'a', pid: 0},
{id:2, name:'bb', pid: 6},
{id:3, name:'cc', pid: 5},
{id:4, name:'dd', pid: 3},
{id:5, name:'ee', pid: 6},
{id:6, name:'ff', pid: 0},
]
//轉換結果
{
"id": 0,
"children": [
{
"id": 1,
"name": "a",
"pid": 0
},
{
"id": 6,
"name": "ff",
"pid": 0,
"children": [
{
"id": 2,
"name": "bb",
"pid": 6
},
{
"id": 5,
"name": "ee",
"pid": 6,
"children": [
{
"id": 3,
"name": "cc",
"pid": 5,
"children": [
{
"id": 4,
"name": "dd",
"pid": 3
}
]
}
]
}
]
}
]
}
實現代碼:
function buildTree(arr){
// [pid]:nodes , pid為key記錄節點
const map = new Map();
arr.forEach(item=>{
if(map.has(item.pid)){
map.set(item.pid, map.get(item.pid).concat(item))
}else{
map.set(item.pid, [item])
}
})
// root = {id:0, children:[]}
// 含義:找到每個節點的children
function _dfs(nodes){
for(const node of nodes){
if(map.has(node.id)){
node.children = map.get(node.id);
_dfs(node.children)
}
}
}
const root = {id:0}
_dfs([root])
return root
}
排序算法
手撕
快速排序
可以去試試這道leetcode 排序數組
function partition(nums: number[], i:number, j:number){
const idx = Math.floor(Math.random()*(j+1-i)) + i;
[nums[i], nums[idx]] = [nums[idx], nums[i]];
const x = nums[i];
while(i<j){
while(i<j && nums[j]>=x){
j--;
}
nums[i] = nums[j]
while(i<j && nums[i]<=x){
i++
}
nums[j] = nums[i]
}
nums[i] = x;
return i;
}
function quickSort(nums: number[], i:number, j:number) {
if(i<j){
const mid = partition(nums, i, j)
quickSort(nums, i, mid-1)
quickSort(nums, mid+1, j)
}
}
function sortArray(nums: number[]): number[] {
quickSort(nums, 0, nums.length-1)
return nums;
};
快排應該是比較經常被問到的了,建議10min內能寫完代碼。並且能分析時間複雜度。