21道精選高頻JavaScript手寫面試題(建議收藏)


文章轉載自https://mp.weixin.qq.com/s/xy0aeBt5yDbivvnPqIAnsg

前言

基本上面試的時候,經常會遇到手撕XXX之類的問題,這次准備梳理總結一遍,鞏固我們原生JS基礎的同時,下次想復習面試手撕題的時候,找起來方便,也節省時間。

代碼在這里👉GitHub

梳理的順序是隨機的,不按照難度高低程度。

實現一個事件委托(易錯)

事件委托這里就不闡述了,比如給li綁定點擊事件

看錯誤版,(容易過的,看「面試官水平了」)👇

ul.addEventListener('click', function (e) {
            console.log(e,e.target)
            if (e.target.tagName.toLowerCase() === 'li') {
                console.log('打印')  // 模擬fn
            }
})

「有個小bug,如果用戶點擊的是 li 里面的 span,就沒法觸發 fn,這顯然不對」👇

<ul id="xxx">下面的內容是子元素1
        <li>li內容>>> <span> 這是span內容123</span></li>
        下面的內容是子元素2
        <li>li內容>>> <span> 這是span內容123</span></li>
        下面的內容是子元素3
        <li>li內容>>> <span> 這是span內容123</span></li>
</ul>

這樣子的場景就是不對的,那我們看看高級版本👇

function delegate(element, eventType, selector, fn) {
    element.addEventListener(eventType, e => {
        let el = e.target
        while (!el.matches(selector)) {
            if (element === el) {
                el = null
                break
            }
            el = el.parentNode
        }
        el && fn.call(el, e, el)
    },true)
    return element
}

實現一個可以拖拽的DIV

這個題目看起來簡單,你可以試一試30分鍾能不能完成,直接貼出代碼吧👇

var dragging = false
var position = null

xxx.addEventListener('mousedown',function(e){
  dragging = true
  position = [e.clientX, e.clientY]
})


document.addEventListener('mousemove', function(e){
  if(dragging === false) return null
  const x = e.clientX
  const y = e.clientY
  const deltaX = x - position[0]
  const deltaY = y - position[1]
  const left = parseInt(xxx.style.left || 0)
  const top = parseInt(xxx.style.top || 0)
  xxx.style.left = left + deltaX + 'px'
  xxx.style.top = top + deltaY + 'px'
  position = [x, y]
})
document.addEventListener('mouseup', function(e){
  dragging = false
})

手寫防抖和節流函數

「節流throttle」,規定在一個單位時間內,只能觸發一次函數。如果這個單位時間內觸發多次函數,只有一次生效。場景👇

  • scroll滾動事件,每隔特定描述執行回調函數
  • input輸入框,每個特定時間發送請求或是展開下拉列表,(防抖也可以)

節流重在加鎖「flag = false」

function throttle(fn, delay) {
    let flag = true,
        timer = null
    return function(...args) {
        let context = this
        if(!flag) return
        
        flag = false
        clearTimeout(timer)
        timer = setTimeout(function() {
            fn.apply(context,args)
            flag = true
        },delay)
    }
}

 

「防抖debounce」,在事件被觸發n秒后再執行回調,如果在這n秒內又被觸發,則重新計時。場景👇

  • 瀏覽器窗口大小resize避免次數過於頻繁
  • 登錄,發短信等按鈕避免發送多次請求
  • 文本編輯器實時保存

防抖重在清零「clearTimeout(timer)」

function debounce(fn, delay) {
    let timer = null
    return function(...args) {
        let context = this
        if(timer) clearTimeout(timer)
        timer = setTimeout(function(){
            fn.apply(context,args)
        },delay)
    }
}

實現數組去重

這個是Array數組測試用例👇

var array = [1, 1, '1', '1', null, null, 
                undefined, undefined, 
                new String('1'), new String('1'), 
                /a/, /a/,
                NaN, NaN
            ];

如何通過一個數組去重,給面試官留下深印象呢👇

使用Set

let unique_1 = arr => [...new Set(arr)];

使用filter

function unique_2(array) {
    var res = array.filter(function (item, index, array) {
        return array.indexOf(item) === index;
    })
    return res;
}

使用reduce

let unique_3 = arr => arr.reduce((pre, cur) => pre.includes(cur) ? pre : [...pre, cur], []);

使用Object 鍵值對🐂🐂,這個也是去重最好的效果👇

function unique_3(array) {
    var obj = {};
    return array.filter(function (item, index, array) {
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}

使用obj[typeof item + item] = true,原因就在於對象的鍵值只能是字符串,所以使用typeof item + item代替

實現柯里化函數

柯里化就是把接受「多個參數」的函數變換成接受一個「單一參數」的函數,並且返回接受「余下參數」返回結果的一種應用。

思路:

  • 判斷傳遞的參數是否達到執行函數的fn個數
  • 沒有達到的話,繼續返回新的函數,並且返回curry函數傳遞剩余參數
let currying = (fn, ...args) =>
            fn.length > args.length ?
            (...arguments) => currying(fn, ...args, ...arguments) :
            fn(...args)

測試用例👇

let addSum = (a, b, c) => a+b+c
let add = curry(addSum)
console.log(add(1)(2)(3))
console.log(add(1, 2)(3))
console.log(add(1,2,3))

實現數組flat

「將多維度的數組降為一維數組」

Array.prototype.flat(num)
// num表示的是維度
// 指定要提取嵌套數組的結構深度,默認值為 1
使用 Infinity,可展開任意深度的嵌套數組

寫這個給面試官看的話,嗯嗯,應該會被打死,寫一個比較容易的👇

let flatDeep = (arr) => {
    return arr.reduce((res, cur) => {
        if(Array.isArray(cur)){
            return [...res, ...flatDep(cur)]
        }else{
            return [...res, cur]
        }
    },[])
}

「你想給面試官留下一個深刻印象的話」,可以這么寫,👇

function flatDeep(arr, d = 1) {
    return d > 0 ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val),
    []) :
        arr.slice();
};

// var arr1 = [1,2,3,[1,2,3,4, [2,3,4]]];
// flatDeep(arr1, Infinity);

可以傳遞一個參數,數組扁平化幾維,簡單明了,看起來逼格滿滿🐂🐂🐂

深拷貝

深拷貝解決的就是「共用內存地址所導致的數據錯亂問題」

思路:

  • 遞歸
  • 判斷類型
  • 檢查環(也叫循環引用)
  • 需要忽略原型
function deepClone(obj, map = new WeakMap()) {
    if (obj instanceof RegExp) return new RegExp(obj);
    if (obj instanceof Date) return new Date(obj);

    if (obj == null || typeof obj != 'object') return obj;
    if (map.has(obj)) {
        return map.get(obj);
    }
    let t = new obj.constructor();
    map.set(obj, t);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            t[key] = deepClone(obj[key], map);
        }
    }
    return t;
}
//測試用例
let obj = {
    a: 1,
    b: {
        c: 2,
        d: 3
    },
    d: new RegExp(/^\s+|\s$/g)
}

let clone_obj = deepClone(obj)
obj.d = /^\s|[0-9]+$/g
console.log(clone_obj)
console.log(obj)

實現一個對象類型的函數

核心:Object.prototype.toString

let isType = (type) => (obj) => Object.prototype.toString.call(obj) === `[object ${type}]`

// let isArray = isType('Array')
// let isFunction = isType('Function')
// console.log(isArray([1,2,3]),isFunction(Map))

isType函數👆,也屬於「偏函數」的范疇,偏函數實際上是返回了一個包含「預處理參數」的新函數。

手寫call和apply

改變this指向,唯一區別就是傳遞參數不同👇

// 實現call
Function.prototype.mycall = function () {
    let [thisArg, ...args] = [...arguments]
    thisArg = Object(thisArg) || window
    let fn = Symbol()
    thisArg[fn] = this
    let result = thisArg[fn](...args)
    delete thisArg[fn]
    return result
}
// 實現apply
Function.prototype.myapply = function () {
    let [thisArg, args] = [...arguments];
    thisArg = Object(thisArg)
    let fn = Symbol()
    thisArg[fn] = this;
    let result = thisArg[fn](...args);
    delete thisArg.fn;
    return result;
}

//測試用例
let cc = {
    a: 1
}

function demo(x1, x2) {
    console.log(typeof this, this.a, this)
    console.log(x1, x2)
}
demo.apply(cc, [2, 3])
demo.myapply(cc, [2, 3])
demo.call(cc,33,44)
demo.mycall(cc,33,44)

手寫bind

bind它並不是立馬執行函數,而是有一個延遲執行的操作,就是生成了一個新的函數,需要你去執行它👇

// 實現bind
Function.prototype.mybind = function(context, ...args){
    return (...newArgs) => {
        return this.call(context,...args, ...newArgs)
    }
}

// 測試用例
let cc = {
    name : 'TianTian'
}
function say(something,other){
    console.log(`I want to tell ${this.name} ${something}`);
    console.log('This is some'+other)
}
let tmp = say.mybind(cc,'happy','you are kute')
let tmp1 = say.bind(cc,'happy','you are kute')
tmp()
tmp1()

實現new操作

核心要點👇

  1. 創建一個新對象,這個對象的__proto__要指向構造函數的原型對象
  2. 執行構造函數
  3. 返回值為object類型則作為new方法的返回值返回,否則返回上述全新對象

代碼如下👇

function _new() {
    let obj = {};
    let [constructor, ...args] = [...arguments];
    obj.__proto__ = constructor.prototype;
    let result = constructor.apply(obj, args);
    if (result && typeof result === 'function' || typeof result === 'object') {
        return result;
    }
    return obj;
}

實現instanceof

「instanceof」 「運算符」用於檢測構造函數的 prototype 屬性是否出現在某個實例對象的原型鏈上。

語法👇

object instanceof constructor
object 某個實例對象
construtor 某個構造函數

原型鏈的向上找,找到原型的最頂端,也就是Object.prototype,代碼👇

function my_instance_of(leftVaule, rightVaule) {
    if(typeof leftVaule !== 'object' || leftVaule === null) return false;
    let rightProto = rightVaule.prototype,
        leftProto = leftVaule.__proto__;
    while (true) {
        if (leftProto === null) {
            return false;
        }
        if (leftProto === rightProto) {
            return true;
        }
        leftProto = leftProto.__proto__
    }
}

實現sleep

某個時間后就去執行某個函數,使用Promise封裝👇

function sleep(fn, time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(fn);
        }, time);
    });
}
let saySomething = (name) => console.log(`hello,${name}`)
async function autoPlay() {
    let demo = await sleep(saySomething('TianTian'),1000)
    let demo2 = await sleep(saySomething('李磊'),1000)
    let demo3 = await sleep(saySomething('掘金的好友們'),1000)
}
autoPlay()

實現數組reduce

更多的手寫實現數組方法,看我之前這篇👉 「數組方法」從詳細操作js數組到淺析v8中array.js

直接給出簡易版👇

Array.prototype.myreduce = function(fn, initVal) {
    let result = initVal,
        i = 0;
    if(typeof initVal  === 'undefined'){
        result = this[i]
        i++;
    }
    while( i < this.length ){
        result = fn(result, this[i])
    }
    return result
}

實現Promise.all和race

不清楚兩者用法的話,異步MDN👉Promise.race()  Promise.all()

// 實現Promise.all 以及 race

Promise.myall = function (arr) {
    return new Promise((resolve, reject) => {
        if (arr.length === 0) {
            return resolve([])
        } else {
            let res = [],
                count = 0
            for (let i = 0; i < arr.length; i++) {
                // 同時也能處理arr數組中非Promise對象
                if (!(arr[i] instanceof Promise)) {
                    res[i] = arr[i]
                    if (++count === arr.length)
                        resolve(res)
                } else {
                    arr[i].then(data => {
                        res[i] = data
                        if (++count === arr.length)
                            resolve(res)
                    }, err => {
                        reject(err)
                    })
                }

            }
        }
    })
}

Promise.myrace = function (arr) {
    return new Promise((resolve, reject) => {
        for (let i = 0; i < arr.length; i++) {
            // 同時也能處理arr數組中非Promise對象
            if (!(arr[i] instanceof Promise)) {
                Promise.resolve(arr[i]).then(resolve, reject)
            } else {
                arr[i].then(resolve, reject)
            }

        }
    })
}

測試用例👇

// 測試用例
let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(11)
    }, 2000);
});
let p2 = new Promise((resolve, reject) => {
    reject('asfs')

});
let p3 = new Promise((resolve) => {
    setTimeout(() => {
        resolve(33);
    }, 4);
});

Promise.myall([p3, p1, 3, 4]).then(data => {
    // 按傳入數組的順序打印
    console.log(data); // [3, 1, 2]
}, err => {
    console.log(err)
});

Promise.myrace([p1, p2, p3]).then(data => {
    // 誰快就是誰
    console.log(data); // 2
}, err => {
    console.log('失敗跑的最快')
})

手寫繼承

繼承有很多方式,這里不過多追溯了,可以看看這篇 JS原型鏈與繼承別再被問倒了

主要梳理的是 寄生組合式繼承 和Class繼承怎么使用

「寄生組合式繼承」

function inheritPrototype(subType, superType) {
    // 創建對象,創建父類原型的一個副本
    var prototype = Object.create(superType.prototype); 
    // 增強對象,彌補因重寫原型而失去的默認的constructor 屬性
    prototype.constructor = subType; 
    // 指定對象,將新創建的對象賦值給子類的原型
    subType.prototype = prototype; 
}

測試用例👇

// 父類初始化實例屬性和原型屬性
function Father(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
Father.prototype.sayName = function () {
    alert(this.name);
};

// 借用構造函數傳遞增強子類實例屬性(支持傳參和避免篡改)
function Son(name, age) {
    Father.call(this, name);
    this.age = age;
}

// 將父類原型指向子類
inheritPrototype(Son, Father);

// 新增子類原型屬性
Son.prototype.sayAge = function () {
    alert(this.age);
}

var demo1 = new Son("TianTian", 21);
var demo2 = new Son("TianTianUp", 20);

demo1.colors.push("2"); // ["red", "blue", "green", "2"]
demo2.colors.push("3"); // ["red", "blue", "green", "3"]

Class實現繼承👇

class Rectangle {
    // constructor
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
    // Getter
    get area() {
        return this.calcArea()
    }
    // Method
    calcArea() {
        return this.height * this.width;
    }
}

const rectangle = new Rectangle(40, 20);
console.log(rectangle.area);
// 輸出 800
// 繼承
class Square extends Rectangle {
    constructor(len) {
        // 子類沒有this,必須先調用super
        super(len, len);

        // 如果子類中存在構造函數,則需要在使用“this”之前首先調用 super()。
        this.name = 'SquareIng';
    }
    get area() {
        return this.height * this.width;
    }
}
const square = new Square(20);
console.log(square.area);
// 輸出 400

extends繼承的核心代碼如下,其實和上述的寄生組合式繼承方式一樣👇

function _inherits(subType, superType) {
  
    // 創建對象,創建父類原型的一個副本
    // 增強對象,彌補因重寫原型而失去的默認的constructor 屬性
    // 指定對象,將新創建的對象賦值給子類的原型
    subType.prototype = Object.create(superType && superType.prototype, {
        constructor: {
            value: subType,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    
    if (superType) {
        Object.setPrototypeOf 
            ? Object.setPrototypeOf(subType, superType) 
            : subType.__proto__ = superType;
    }
}

把實現原理跟面試官扯一扯,這小子基礎還行。

手寫一下AJAX

寫的初略版的,詳細版的就不梳理了,面試的時候,跟面試官好好探討一下吧🐂🐂🐂

var request = new XMLHttpRequest()
 request.open('GET', 'index/a/b/c?name=TianTian', true);
 request.onreadystatechange = function () {
   if(request.readyState === 4 && request.status === 200) {
     console.log(request.responseText);
   }};
 request.send();

用正則實現 trim()

去掉首位多余的空格👇

String.prototype.trim = function(){
    return this.replace(/^\s+|\s+$/g, '')
}
//或者 
function trim(string){
    return string.replace(/^\s+|\s+$/g, '')
}

實現Object.create方法

//實現Object.create方法
function create(proto) {
    function Fn() {};
    Fn.prototype = proto;
    Fn.prototype.constructor = Fn;
    return new Fn();
}
let demo = {
    c : '123'
}
let cc = Object.create(demo)

實現一個同時允許任務數量最大為n的函數

使用Promise封裝,給你一個數組,數組的每一項是一個Promise對象

function limitRunTask(tasks, n) {
  return new Promise((resolve, reject) => {
    let index = 0, finish = 0, start = 0, res = [];
    function run() {
      if (finish == tasks.length) {
        resolve(res);
        return;
      }
      while (start < n && index < tasks.length) {
        // 每一階段的任務數量++
        start++;
        let cur = index;
        tasks[index++]().then(v => {
          start--;
          finish++;
          res[cur] = v;
          run();
        });
      }
    }
    run();
  })
  // 大概解釋一下:首先如何限制最大數量n
  // while 循環start < n,然后就是then的回調
}

10進制轉換

給定10進制數,轉換成[2~16]進制區間數,就是簡單模擬一下。

function Conver(number, base = 2) {
  let rem, res = '', digits = '0123456789ABCDEF', stack = [];

  while (number) {
    rem = number % base;
    stack.push(rem);

    number = Math.floor(number / base);
  }

  while (stack.length) {
    res += digits[stack.pop()].toString();
  }
  
  return res;
}

數字轉字符串千分位

寫出這個就很逼格滿滿🐂🐂🐂

function thousandth(str) {
  return str.replace(/\d(?=(?:\d{3})+(?:\.\d+|$))/g, '$&,');
}
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM