前端面试中最全高频JavaScript手写题

746 阅读13分钟

前言

本文整理了前端面试高频出现的手写js相关的题目,如果对答案有不一样见解的同学欢迎评论区补充讨论,当然有问题,也欢迎在评论区指出。

切记:笔者记录的这些js手写题,做之前,想想这些方法是用来干什么,需要传什么参数,返回的什么,另外还需要注意一些边界条件,这对js基础有很大考验,基础不好的,一定要好好补补js(尤其,原型链、this指向、promise、继承等)

学习就是这么简单,你听懂了吗?

image.png

引用加参考:感谢两位大佬以及全掘金作者帮助,让我收获很多 juejin.cn/post/696871… juejin.cn/post/684490…

1、实现new操作符

new 出来的是一个实例对象,有自己的proto属性,而且实例的proto指向构造函数的prototype

// 传入构造函数和构造函数参数
function myNew(fun,...args){
    // 初始化一个对象,分配空间
    const obj  = {};
    // 链接到函数原型
    obj.__proto__ = fun.prototype;
    //等同Object.setPrototypeOf(obj, fun.prototype)
    // 将 obj 绑定到构造函数上,并且传入剩余的参数
    const res = fun.apply(obj,args);
    // 返回构造好的对象,特别的若res返回一个非空对象,那么返回res
    return res instanceof Object ? res : obj
    //等同return (typeof res!== null && typeof res === 'object')?res:obj;
}
​
function Animal(name) {
    this.name = name;
}
​
let animal = myNew(Animal, 'dog');
console.log(animal.name)  // dog

2、实现instanceOf

function myInstanceof(left,right){
    // 验证如果为基本数据类型,就直接返回 false
    const baseType = ['string', 'number', 'boolean', 'undefined', 'symbol']
    if(baseType.includes(typeof(left))) return false;
​
    const prototype = right.prototype;
    let proto = left.__proto__;
    while(true) {
        if(proto===null) return false;// 找到最顶层
        if(proto===prototype) return true;
        proto = proto.__proto__;// 没找到继续向上一层原型链查找
    }
}
​
function Func(){
    console.log('hzy')
}
​
let fun = new Func();
myInstanceof(fun,Function);

3、实现浅/深拷贝

  • 浅拷贝:拷贝的是对象的指针,修改内容会互相影响
  • 深拷贝:将整个对象拷贝到另一个内存中,修改内容互不影响

浅拷贝方法

  • concat()和slice(0)和展开符(...)和Object.assign()
let arr = [1, 3, {
    username: 'kobe'
}];
let arr2 = arr.concat();
//let arr2 = arr.slice(0);
//let arr2 = [...arr]
//let arr2 = Object.assign([], arr);  合并对象
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]

深拷贝方法

方法1:JSON.parse(JSON.stringify(arr))

let arr = {
  a: 1,
  b: [ 'e', 'f', 'g' ],
  c: { h: { i: 2 } }
};
let arr2 = JSON.parse(JSON.stringify(arr));
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'kobe' } ]
//缺点:
//1.他无法实现对函数 、RegExp等特殊对象的克隆
//2.会抛弃对象的constructor,所有的构造函数会指向Object
//3.对象有循环引用,会报错
//如:
const arr = {
  a: say,
  b: new Array(1),
  c: new RegExp('ab+c', 'i'),
  d: Messi
};

方法2:基础方式(缺陷同上)

function deepClone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {  //遍历对象要用  。。。in。。。
            cloneTarget[key] = deepClone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};
​
const target = {
    field1: 1,
    field2: undefined,
    field3: 'hzy',
    field4: {
        child: 'child',
    },
    field5: [2, 4, 8]
};
​
let arrCopy =deepClone(target);
target['one'] = 5
console.log(target,arrCopy)

4、实现apply、call、bind

apply方法实现

1、实现效果
function fn(age){
    this.age = age
}
let target = {name:'hzy'}
fn.myApply(target,[18,3])  // 同fn.apply(target,18,3)
console.log(target) //{ name: 'hzy', age: 18 }
2、分析apply原理
//俩参数,fn构造函数,args参数数组
//myApply 应该挂在 Function.prototype 上
//如果不传入参数,target默认指向为 window,args默认是[]
//指定this到target.fn并传入给定参数执行函数
//删除target里的fn函数
3、代码实现
Function.prototype.myApply = function (target,args){
    // this:[Function: fn]
    if(typeof this !== 'function'){
        throw new TypeError('not a function')
    }
    //如果不传入参数,target默认指向为 window,args默认是[]
    target = target || window;
    args = args || [];
    target.fn = this;   //target:{ name: 'hzy', fn: [Function: fn] }
    target.fn(...args)  // 执行target里的fn函数
    delete target.fn  //删除target里的fn函数
    return target
}

call方法实现

apply 与 call 的区别: apply 参数要为数组,call参数是以逗号分开

bind方法实现

fn.bind(obj,arg1,arg2) 创建一个新的绑定函数,不会立即执行 fn 函数,而 call, apply 会立即执行

fn绑定的对象是传进的第一个参数,后面的参数作为函数参数传入,如:fn.bind(obj,arg1).bind(arg2,arg3).bind(arg4)

//bind实现要复杂一点  因为他考虑的情况比较多 还要涉及到参数合并(类似函数柯里化)
Function.prototype.myBind = function (target,...args){
    if(typeof this !== 'function'){
        throw new TypeError('not a function')
    }
    const self = this; // 存储源函数以及函数参数
    // 对返回的函数 secondArgs 二次传参
    let fnToBind = function (...secondArgs){
        // 判断this是否是fnToBind绑定函数的实例 也就是fnToBind函数返回的值是否通过new调用所得
        // new调用的函数this指向当前函数,否则就绑定到传入的target上
        let context = this instanceof fnToBind ? this : Object(target);
        // 用call调用源函数绑定this的指向并传递参数,返回执行结果
        return self.call(context,...args,...secondArgs);
    }
    if (self.prototype) {
        // 复制源函数的prototype给fnToBind 有些函数没有prototype,比如箭头函数
        fnToBind.prototype = Object.create(self.prototype);
    }
    return fnToBind; // 返回绑定的函数
}

let obj = { name: "hzy" }
function test(x,y,z) {
    console.log(this.name) // ciel
    console.log(x+y+z) // 6
}
let Bound = test.myBind(obj,1,2)
Bound(3) //二次传参3 => 6// 测试
let obj = { name: "hzy" }
function test(x,y,z) {
    console.log(this.name) // ciel
    console.log(x+y+z) // 6
}
let Bound = test.myBind(obj,1,2)
Bound(3) //二次传参3 => 6

5、实现Object.create()

new 出来的是一个实例对象,有自己的proto属性,而且实例的proto指向构造函数的prototype

1、实现效果
console.log(myCreate({a:2}).__proto__) //{ a: 2 }
相当于Object.create({a:2})
2、分析Object.create()原理
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
3、代码实现
function create(proto) {
    function F() {}
    F.prototype = proto;
    return new F();
}

6、实现JSON.parse和JSON.stringify

JSON.parse()

  • 可以通过eval()这种方式,但有xss漏洞
eval('(' + target + ')');

JSON.stringify()

function myStringify(target) {
    if (typeof target !== 'object' || target === null) {
        // 对字符串特殊处理,多双引号
        if(typeof target==='string'){
            return `"${target}"`
        }else{
            return String(target);
        }
    }else{
        //处理对象和数组
        const json = [];
        for(let key in target){
            let value = target[key];
            if(Array.isArray(target)){
                json.push(`${myStringify(value)}`)
            }else{
                json.push(`"${key}":${myStringify(value)}`)
            }
        }
        //这里需要注意,当数组或对象被转化为字符串时,就没了括号
        if (Array.isArray(target)) {
            return `[${json}]`
        } else {
            return `{${json}}`
        }
    }
}

console.log(myStringify('string'))
// "string"
console.log(myStringify(666))
// 666
console.log(myStringify({name1: {name2: "abc"}}))
// {"name1":{"name2":"abc"}}
console.log(myStringify([1, "false", false]))
// [1,"false",false]

7、版本号排序

将一组版本号['','0.1.1', '0.1.1', '0.1.1.1', '2.2', '4.3.5'],排列成这样[ '', '0.1.1', '0.1.1', '0.1.1.1', '2.2', '4.3.5' ]

let arr = ['','0.1.1', '0.1.1', '0.1.1.1', '2.2', '4.3.5']
​
arr.sort((a, b) => {
    const arr1 = a.split('.');
    const arr2 = b.split('.');
    let i = 0;
    while (true){
        let str1 = arr1[i];
        let str2 = arr2[i];
        i++;
        if(str1===undefined||str2===undefined){
            return arr1.length-arr2.length
        }
        if(str1===str2) continue;
        return parseInt(str1)-parseInt(str2);
    }
});
console.log(arr);//[ '', '0.1.1', '0.1.1', '0.1.1.1', '2.2', '4.3.5' ]

8、LRU(最近最少使用)算法

先带你了解LRU:blog.csdn.net/belongtocod…

// 可以用一个特殊的栈来保存当前正在使用的各个私钥key。
// 当一个新的私钥key加进来,便将该私钥key压入栈顶,其他的私钥key往栈底移,如果内存不够,则将栈底的页面号移除。
// 这样,栈顶始终是最新被访问的私钥key,而栈底则是最近最久未访问的私钥key。
// 实现一个LRU(最近最少使用)缓存机制
// 支持get(key)获取数据,push(key,value)写入数据class LRU{
    constructor(capacity) {
        this.capacity = capacity;//设置缓存容量大小
        this.secrets = new Map();//存储密钥和密钥值
    }
    get(secretKey){
        if(this.secrets.has(secretKey)){
            let secretValue = this.secrets.get(secretKey)
            this.secrets.delete(secretKey);//先删掉原来位置上的key,重新加入栈顶
            this.secrets.set(secretKey,secretValue);
            return secretValue;
        }else{
            return -1;
        }
    }
    put(secretKey,secretValue){
        //先判断栈里有没有这个key,有的话,删了重新加入栈顶
        if(this.secrets.get(secretKey)){
            this.secrets.delete(secretKey);
            this.secrets.set(secretKey,secretValue);
        }else if(this.secrets.size < this.capacity){// key不存在,capacity未满
            this.secrets.set(secretKey,secretValue);
        }else{// key不存在,capacity满了,得移除栈底层的key,在栈顶加入新key
            this.secrets.delete([...this.secrets.keys()][0]);
            //this.secretKey.delete(this.secretKey.keys().next().value);//同上
            this.secrets.set(secretKey,secretValue);
        }
    }
}
​
let cache = new LRU(2);
cache.put(1, '1');
cache.put(2, '2');
console.log(cache.get(1))// 返回  '1'
cache.put(3, '3');// 该操作会使得密钥 2 作废
console.log(cache.get(2))// 返回 -1 (未找到)
cache.put(4, '4');// 该操作会使得密钥 1 作废
console.log(cache.get(1))// 返回 -1 (未找到)
console.log(cache.get(3))// 返回  '3'
console.log(cache.get(4))// 返回  '4'

9、将函数柯里化

function curry(fn,args){
    const fnLen = fn.length;//整个函数参数长度
    args = args || [];
    return function (){  //每次返回一个函数
        console.log(arguments)//第一个函数的参数对象---类数组
        let newArgs = args.concat(Array.prototype.slice.call(arguments))
        if(newArgs.length<fnLen){
            return curry(fn,...newArgs); //如果长度不够,继续柯里化
        }else{
            return fn.call(this,...newArgs);//长度够,直接返回柯里化后的函数
        }
    }
}
function add(a, b, c) {
    return a + b + c;
}
let sum = curry(add);
// console.log(sum(2)(3)(4))
// console.log(sum(2,3,4))
// sum(2)(3,4);
console.log(sum(2,3)(4))

10、dom树转json对象以及dom的遍历

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="header">
    <div class="content1">111</div>
    <div class="content2">222</div>
    <div class="content3">333</div>
</div>
<div id="main">
    <div class="content4">
        <span>内容1</span>
        <span>内容2</span>
        <span>内容3</span>
    </div>
</div>
<div id="footer">
    <ul class="list">
        <li>1</li>
        <li>2</li>
        <li>3</li>
    </ul>
</div>
</body>
<script>
    window.onload = function (){
        let root = document.getElementsByTagName('body')[0];
        if(root.length) return [];
        
        // dom树转json
        // function dom2json(root){
        //     let obj = {};
        //     obj.name = root.tagName;
        //     console.log(root.tagName);
        //     obj.children = [];
        //     root.childNodes.forEach(child=>{
        //         if(child.tagName!==undefined) {
        //             obj.children.push(dom2json(child))
        //         }
        //     })
        //     return obj
        // }
        // console.log(dom2json(root))
        
        //层次遍历dom树
        let nodes = [];
        let queue = [root];
        console.log(root)
        while (queue.length){
            let levelLength = queue.length;
            for(let i=0;i<levelLength;i++){
                const cur = queue.shift()
                nodes.push(cur);
​
                for(let item of cur.children){
                    queue.push(item);
                }
            }
        }
        console.log(nodes)
    }
</script>
</html>

11、vdom(json对象)转真实dom树

let vdom = {
    tag: 'DIV',
    attrs: {
        id: 'app'
    },
    children: [
        {
            tag: 'SPAN',
            children: [
                {tag: 'A', children: []}
            ]
        },
        {
            tag: 'SPAN',
            children: [
                {tag: 'A', children: []},
                {tag: 'A', children: []}
            ]
        }
    ]
}
把上面虚拟Dom转化成下方真实Dom
<div id="app">
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>
function myRender(vdom) {
    let dom = document.createElement(vdom.tag);
    // 设置标签上的属性
    vdom.attrs&&Object.keys(vdom.attrs).forEach(key=>{
        dom.setAttribute(key,vdom.attrs[key]);
    })
    // 递归添加并渲染所有孩子节点
    vdom.children&&vdom.children.forEach(child=>{
        dom.appendChild(myRender(child));
    })
    return dom;
}
//let el = document.getElementsByTagName('body')[0].appendChild(myRender(vdom))
console.log(myRender(vdom));

12、按父子关系实现数组转JSON(v-Dom)

let arr = [{
    id: 'id1',
    parentId: '',
    children:[]
}, {
    id: 'id2',
    parentId: 'id1',
    children:[]
}, {
    id: 'id3',
    parentId: 'id1',
    children:[]
}, {
    id: 'id4',
    parentId: 'id1',
    children:[]
}, {
    id: 'id5',
    parentId: 'id2',
    children:[]
}, {
    id: 'id6',
    parentId: 'id3',
    children:[]
}, {
    id: 'id7',
    parentId: 'id3',
    children:[]
}, {
    id: 'id8',
    parentId: 'id4',
    children:[]
}]
​
结果为:
let res = {
    "id": "id1",
    "parentId": "",
    "children": [
        {
            "id": "id2",
            "parentId": "id1",
            "children": [
                {
                    "id": "id5",
                    "parentId": "id2",
                    "children": []
                }
            ]
        },
        {
            "id": "id3",
            "parentId": "id1",
            "children": [
                {
                    "id": "id6",
                    "parentId": "id3",
                    "children": []
                },
                {
                    "id": "id7",
                    "parentId": "id3",
                    "children": []
                }
            ]
        },
        {
            "id": "id4",
            "parentId": "id1",
            "children": [
                {
                    "id": "id8",
                    "parentId": "id4",
                    "children": []
                }
            ]
        }
    ]
}
function array2json(arr) {
    let obj = {}
    arr.forEach(item => {
        if (item.parentId === '') {
            obj = item;
        }
    })
    function dfs(obj) {
        //找到所有子孩子
        let childs = arr.filter(item => {
            return item.parentId === obj.id;
        })
        obj.children.push(...childs)
        for (let child of childs) {//遍历所有的孩子节点
            dfs(child);//对孩子节点,再重新当作当前节点,进行dfs
        }
    }
​
    dfs(obj);
    return obj;
}
​
console.log(array2json(arr))

13、按父子关系实现JSON(v-Dom)转数组

function json2array(res){
    let arr = []
    const dfs = function(obj){
        for(let child of obj.children){
            dfs(child);
        }
        obj.children = [];
        arr.push(obj);
    }
    dfs(res);
    return arr
}
​
console.log(json2array(res))

14、实现Object.is

Object.is不会转换被比较的两个值的类型,这点和===更为相似,他们之间也存在一些区别。

  1. NaN在===中是不相等的,而在Object.is中是相等的
  2. +0和-0在===中是相等的,而在Object.is中是不相等的
Object.is = function (x, y) {
  if (x === y) {
    // 当前情况下,只有一种情况是特殊的,即 +0 -0
    // 如果 x !== 0,则返回true
    // 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断
    return x !== 0 || 1 / x === 1 / y;
  }
​
  // x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样
  // x和y同时为NaN时,返回true
  return x !== x && y !== y;
};
​

15、实现AJAX请求

function getJSON(url){
    return new Promise((resolve,reject)=>{
        //创建xhr对象
        let xhr = new XMLHttpRequest();
        //调用open方法设置基本请求信息
        xhr.open('get',url,false)//false表示async
        //设置请求头信息
        xhr.setRequestHeader("Content-Type", "application/json")
        //注册监听函数
        xhr.onreadystatechange = function (){
            if(xhr.readyState !== 4) return;//未响应完成
            if(xhr.status === 200 || xhr.status === 304){
                resolve(xhr.responseText) //返回响应结果
            }else{
                reject(new Error(xhr.responseText))
            }
        }
        xhr.send();
    })
}

16、实现大数相加

function add(a,b){
    //取两个数的最大长度,补0补齐长度
    let maxLen = Math.max(a.length,b.length);
    a = a.padStart(maxLen,'0')
    b = b.padStart(maxLen,'0');
​
    //定义进位
    let flag = 0;
    let sum = ""
    for(let i=maxLen-1;i>=0;i--){
        let temp = parseInt(a[i])+parseInt(b[i])+flag;
        flag = Math.floor(temp/10);
        sum = temp%10 + sum
    }
    return sum;
}
// 大数相加
let a = "9007199254740991";
let b = "1234567899999999999";
console.log(add(a,b));

17、数组扁平化

常规方法

function flatten(arr){
    if(!Array.isArray(arr)){
        throw new TypeError('args must be an array');
    }
    let res = [];
    arr.forEach(item => {
        if(Array.isArray(item)){
            res.push(...flatten(item));//最小层数组时,需展开,push进res   [2,4]--[1,2,2,4]--[1,1,2,2,4]
        }else{
            res.push(item);
        }
    })
    return res;
}
​
console.log(flatten([1,[1,2,[2,4]],3,5]));  // [1, 1, 2, 2, 4, 3, 5]

reduce方法实现扁平化

let arr = [1, 2, '3js', [4, 5, [6], [7, 8, [9, 10, 11], null, 'abc'], {age: 58}], '[]', null];
function flatten(arr) {
    if(!Array.isArray(arr)){
        throw new TypeError('not an array');
    }
    return arr.reduce((prev, cur) => {
        return Array.isArray(cur) ? prev.concat(flatten(cur)) : prev.concat(cur)
    }, [])
}
console.log(flatten(arr))

18、实现reduce

reduce使用

var arr = [1, 2, 3, 4];
var sum = arr.reduce(function(pre, cur, curIndex, arr) {
    // pre上次回调返回的值,cur当前索引值,curIndex当前索引
    console.log(pre, cur, curIndex);
    return pre + cur;
},0)   //传递给函数的初始值。默认是0
console.log(sum);

reduce实现

//例子: arr.reduce(cb,initValue)
//  1.函数中有哪些参数cb,initValue
//  2.回调函数参数(pre: 代表累加值,cur: 目前值,curIndex: 第几个,arr 调用 reduce 的数组)
//  3.整体返回 pre 累加值
Array.prototype.myReduce = function (cb,initValue){
    //判断arr是不是数组
    if(!Array.isArray(this)){
        throw new TypeError('not a array')
    }
    let arr = this;
    // 判断有没有初始值,没有就默认是0
    initValue = initValue || 0;
    let pre = initValue;
    // cb 每次执行完都会返回一个新的pre,覆盖之前的
    arr.forEach((cur,curIndex)=>{
        pre = cb(pre,cur,curIndex,arr)
    })
    return pre;
}

reduce实现map

//例子: arr.map(cb)
//  1.函数中有哪些参数cb
//  2.回调函数参数(cur: 目前值,curIndex: 第几个,arr:遍历的数组)
//  三个参数刚好和reduce函数接收的回调函数的第2、3、4个参数是对应的//思路:将每次遍历的元素,作为传入的函数的参数,并将函数执行的结果放入新的数组中
Array.prototype.myMap = function (cb){
    //判断arr是不是数组
    if(!Array.isArray(this)){
        throw new TypeError('not an array')
    }
    //操作完数组返回最终结果
    return this.reduce((pre,cur,index,arr)=>{
        pre.push(cb(cur,index,arr));
        return pre;
    },[])
}
​
console.log([1,3,4].myMap((item)=>{
    return item*2;
}))

reduce实现filter

//例子: arr.filter(cb)
//  1.函数中有哪些参数cb
//  2.回调函数参数(cur: 目前值,curIndex: 第几个,arr:遍历的数组)
//  3.三个参数刚好和reduce函数接收的回调函数的第2、3、4个参数是对应的//思路:将每次遍历的元素,作为回调函数的参数,并通过函数执行结果true或false判断是否将当前值加入新数组
Array.prototype.myFilter = function (cb){
    //判断arr是不是数组
    if(!Array.isArray(this)){
        throw new TypeError('not an array')
    }
    //操作完数组返回最终结果
    return this.reduce((pre,cur,index,arr)=>{
        if(cb(cur,index,arr)){
            pre.push(cur)
        }
        return pre;
    },[])
}
​
console.log([1,3,4].myFilter((item)=>{
    return item===3;
}))

reduce实现数组去重

let arr = [1, 2, 3, 1, 1, 2, 3, 3, 4, 3, 4, 5]
​
let result = arr.reduce((pre, item, index, arr) => {
    // !pre.includes(item) && pre.push(item);
    if(!pre.includes(item)){
        pre.push(item);
    }
    return pre;
}, [])
console.log(result);  //[1, 2, 3, 4, 5]//其他方式去重(set+解构+展开运算符)
let result = [...new Set(arr)]

reduce实现数组求最大值最小值

let arr = [1, 2, 3, 4, 5, 6, 7]
console.log(arr.reduce((prev, cur)=>{
    return Math.max(prev, cur)
})); // 7
console.log(arr.reduce((prev, cur)=>{
    return Math.min(prev, cur)
})); // 1

19、对象扁平化

实现效果:

const obj = {
    a: {
        b: 1,
        c: 2,
        d: {e: 5}
    },
    b: [1, 3, {a: 2, b: 3}],
    c: 3
}
​
结果返回如下
// {
//  'a.b': 1,
//  'a.c': 2,
//  'a.d.e': 5,
//  'b[0]': 1,
//  'b[1]': 3,
//  'b[2].a': 2,
//  'b[2].b': 3
//   c: 3
// }
function flatten(obj){
    let res = {};
    const dfs = function (preStr,cur){
        // 2.终止条件:当前值cur既不是对象,也不是数组时
        if(cur.constructor === Array){
            //处理数组时的字符串,递归
            cur.forEach((item,index)=>{
                dfs(`${preStr}[${index}]`, item);
            })
        }else if(cur.constructor === Object){
            //处理对象字符串,递归
            Object.keys(cur).forEach(key=>{
                // 这里要判断第一个对象遍历时,没有 . ,即preStr=''时,不要 .
                dfs(`${preStr}${preStr?'.':''}${key}`, cur[key])
            })
        }else{
            res[preStr] = cur;
        }
    }
    // 1.递归需要传的参数,preStr:key组成的字符串,obj:当前key对应的值
    dfs('',obj);
    return res;
}
console.log(flatten(obj))

20、解构和展开运算符结合

// 实现字符串转数组
let str ='abcdefg'
console.log([...str])
console.log(Array.prototype.slice.call(str))
console.log(str.split(''))
​
//其它小tips
let arr = [1,2,3,4];  //类数组都可
const [a,...arr1] = arr
console.log(a,arr1)   //1 [ 2, 3, 4 ]

21、实现setTimeout=>setInterval

//有缺陷
function mySetInterval(cb,delay){
    let timeId = null;
    const dfs = function (){
        cb();// 执行传入的回调函数
        setTimeout(()=>{
            dfs()
        },delay)
    }
    // 第一个setTimeout,获取timeId
    timeId = setTimeout(()=>{
        dfs()
    },delay)
    return timeId;
}
let timeId = mySetInterval(()=>{
    console.log('a')
},1000)
console.log(timeId) // 数字
//完整版
let timeMap = {}
let id = 0 // 简单实现id唯一
const mySetInterval = (cb, time) => {
    let timeId = id // 将timeId赋予id
    id++ // id 自增实现唯一id
    let fn = () => {
        cb()
        timeMap[timeId] = setTimeout(() => {
            fn()
        }, time)
    }
    timeMap[timeId] = setTimeout(fn, time)
    return timeId // 返回timeId
}
​
let id2 = mySetInterval(()=>{
    console.log('一秒一个亿');
},1000)
​
// console.log(id) //Number数字
const myClearInterval = (id) => {
    clearTimeout(timeMap[id]); // 通过timeMap[id]获取真正的id
    delete timeMap[id];
}
setTimeout(()=>{
    myClearInterval(id2);
},3000)

小知识

1、setTimeout回调函数里的this指向window,如果想用外层的this,有两种方式:

  • 回调函数用箭头函数,这样this就指向外层
  • 在外部设置变量的方式

2、js定时器输出时间不精确问题

setInterval(()=>{
    console.log('data:' + new Date());
},1000)
//因为js是单线程的,要执行代码,需要先放入任务队列等待执行
setTimeout() 的第二个参数告诉 JavaScript 再过多长时间把当前任务添加到队列中。
如果队列是空的,那么添加的代码会立即执行;
如果队列不是空的,那么它就要等前面的代码执行完了以后再执行
function test(){
    this.name = 'hzy';
    setTimeout(function (){
        console.log(this) //window
    },1000)
}
// 利用箭头函数
function test(){
    this.name = 'hzy';
    setTimeout(()=>{
        console.log(this) //test { name: 'hzy' }
    },1000)
}
// 设置变量
function test(){
    this.name = 'hzy';
    let self = this;
    setTimeout(function (){
        console.log(self) //test { name: 'hzy' }
    },1000)
}
​
new test()

22、函数防抖(debounce)

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

  • 使用echarts改变浏览器宽度时,希望重新渲染(防抖可以替代resize函数)
  • 典型案例:输入搜索时,因为通过输入的内容向后端请求数据是异步的,不做处理会导致bug
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>防抖</title>
</head>
<body>
<button id="debounce">点我防抖!</button><script>
    window.onload = function() {
        // 1、获取这个按钮,并绑定事件
        var myDebounce = document.getElementById("debounce");
        myDebounce.addEventListener("click", debounce(sayDebounce));
    }
​
    // 2、防抖功能函数,接受传参
    function debounce(fn) {
        // 4、创建一个标记用来存放定时器的返回值
        let timeout = null;
        return function() {//闭包timeout
            // 5、每次当用户点击/输入的时候,把前一个定时器清除
            clearTimeout(timeout);
            // 6、然后创建一个新的 setTimeout,
            // 这样就能保证点击按钮后的 interval 间隔内
            // 如果用户还点击了的话,就不会执行 fn 函数
            timeout = setTimeout(() => {
                fn.call(this, arguments);
            }, 1000);
        };
    }
​
    // 3、需要进行防抖的事件处理
    function sayDebounce() {
        // ... 有些需要防抖的工作,在这里执行
        console.log("防抖成功!");
    }
</script></body>
</html>

23、函数节流(throttle)

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

理解记忆: 函数节流就是fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹

function throttle(fn,delay) {
    // 通过闭包保存一个标记
    let canRun = true;
    return function() {
        //在函数开头判断标志是否为 true,不为 true 则中断函数
        if(!canRun) {
            return;
        }
        // 将 canRun 设置为 false,防止执行之前再被执行
        canRun = false;
        setTimeout( () => {
            fn.call(this, arguments);
            //执行完事件(比如调用完接口)之后,重新将这个标志设置为 true
            canRun = true;
        }, delay);
    };
}

24、利用set实现交、并、差集

// 实现并集、交集、差集
let a =[1,2,2,3,4,2];
let b =[2,3,3,4,5];
​
let union =new Set([...a, ...b]);
console.log(union);//并集
//集合,map没有filter方法
let intersect =new Set(a.filter(item => new Set(b).has(item)));
console.log(intersect);//交集
//差集=并集-交集
let difference = [...union].filter(item=> !intersect.has(item))
console.log(difference)

25、高级过滤map+filter

// 需求: 年龄大于18的姓名
let arrObj = [{
    name: 'aa', age: 13
}, {
    name: 'bb', age: 23
}, {
    name: 'cc', age: 18
}, {
    name: 'dd', age: 11
}, {
    name: 'ee', age: 28
}]
let arrObjFilter = arrObj.filter(ele => {
    return ele.age > 18;
}).map(ele=>{
    return ele.name;
})
console.log(arrObjFilter) // ['bb', 'ee']

26、实现函数组合运算compose

// 用法如下:
function fn1(x) {
    return x + 1;
}
function fn2(x) {
    return x + 2;
}
function fn3(x) {
    return x + 3;
}
function fn4(x) {
    return x + 4;
}
const a = compose(fn1,fn2,fn3,fn4);
console.log(a(1));//相当于fn1(fn2(fn3(fn4(1))))
// 1+4+3+2+1=11
function compose(...fns){
    if(fns.length===0) {
        return function (...args){
            return args;
        }
    }
    return fns.reduce((pre,cur)=>{
        return function (...args1){
            return pre(cur(...args1))
        }
    })
}

27、url相关操作

匹配参数值

  • (^|&)这里匹配空开头的或者&开头的字符串,举个例子,如:“ad”,"&ad"都可以
  • ([^&]*)这个意思是不能以0个或者多个&开头
  • (&|$)后面是&或者以空结尾的字符串,如"&ad",“ad”。
const url = 'http://www.xxxxxxx.cn/?lx=1&name=JS&from=baidu&name=2#video'
let key = 'name'
const reg = new RegExp("(^|&)"+key+"=([^&]*)(&|$)");
console.log(url.split('?')[1].split('#')[0].match(reg)[2])

存储参数键值

const url = 'http://www.xxxxxxx.cn/?lx=1&name=JS&from=baidu#video'
let params = new Map()
let paramList = url.split('?')[1].split('#')[0].split('&');
paramList.forEach(item => {
    let key = item.split('=')[0]
    let value = item.split('=')[1];
    params.set(key,value);
})
console.log(params)
// Map(3) { 'lx' => '1', 'name' => 'JS', 'from' => 'baidu' }

28、实现range

// 实现效果
const range = new Range(3,8);
// 一次性完成迭代
for (const num of range){
    console.log(num);// 3,4,5,6,7
}
//中途退出
for (const num of range){
    if(num>5){
        break;  //自动调用return方法
    }
    console.log(num);//3,4,5
}

iterator实现range

// 自定义一个仿range的可提前终止的迭代器
class Range{
    constructor(start,end) {
        this.start = start;
        this.end = end;
    }
    // for..of 默认调用iterator,每次迭代返回一个value
    [Symbol.iterator](){
        let left = this.start;
        let end = this.end;
        return { 
            next(){
                if(left<end){
                    return {done:false,value:left++}
                }else{
                    return {done: true}
                }
            },
            //这里是一个return()方法,不是return关键字
            return() {
                console.log('提前退出')
                return {done:true}
            }
        }
    }
}

generator实现range

function* myRange(start, end) {
    while (start < end) {
        yield start++;
    }
}
​
const range = myRange(3, 9);
console.log(range[Symbol.iterator]() === range);//true
for (const num of range) {
    console.log(num);//3 4 5 6 7 8
}

29、大数据渲染的分片思想

渲染百万条结构简单的大数据时,使用分片思想优化渲染-------递归

//在ul中插入li标签
let ul = document.getElementsByTagName("body")[0];
// 插入十万条数据
let total = 1000;
// 一次插入 20 条
let once = 20;
//每页记录的索引
let index = 0;
​
function loop(curTotal,curIndex){
    if(curTotal<=0){// 所有都渲染完成
        return false;
    }else if(curTotal<once){// 当剩余待渲染数不足once=20
        once = curTotal;
    }
    for(let i=0;i<once;i++){
        let li = document.createElement("li");
        li.innerHTML = `第${curIndex+i}条数据`;
        ul.appendChild(li);
    }
    loop(curTotal-once,curIndex+once)
}
loop(total,index)

30、正则表达式相关题

vue模板template变量替换

let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
    name: '姓名',
    age: 18
}
console.log(render(template, data));
// 我是姓名,年龄18,性别undefined


解法一:
function render(template,data){
    const reg = /{{(\w+)}}/;
    // 判断template还有没有{{}},没有则直接返回渲染完成之后的template
    if(reg.test(template)){
        // 查找当前模板里第一个模板字符串的字段
        const name = template.match(reg)[1];
        template = template.replace(reg, data[name]);
        //这里必须return,否则第一次渲染完后,直接return template了
        return render(template,data);
    }
    return template;
}
解法二:replace很强大,可以实现多个同时替换
function render(template,data){
    const reg = /{{(\w+)}}/g;
    return template.replace(reg, function (item){
        // console.log(item.slice(2,-2))切割第二个索引和倒数第二个索引之间的字符串
        return data[item.slice(2,-2)];
    })
}

驼峰

let str = "next-to-meet-you"
console.log(hump(str))
// nextToMeetYou

function hump(str){//驼峰
    const reg = /-\w/g;
    return str.replace(reg, function (item){
        return item.slice(1).toUpperCase()
    })
}

常用正则

//日期'2015-12-25'替换成'12/25/2015'
let date = '2015-12-25'
let reg = /(\d{4})-(\d{2})-(\d{2})/g
console.log(date.replace(reg,'$1/$2/$3'))

//电话号码
const reg = /^1[34578]\d{9}$/
//验证邮箱
zyhu_6@stu.xidian.edu
18843186666@163.com
const reg = /^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/
//省份证号
第一代居民身份证是15位编码
我国二代居民身份证的号码为18const reg = /(^\d{15}$)|(^\d{17}[\dxX]$)/

总结

觉得写得好的,对你有帮助的,可以分享给身边人,知识越分享越多,千万不要吝啬呀

后续更新vue底层相关原理讲解,请关注我,整理好,分享给你们,我们一起学前端