js知识点和手写题整理集合

357 阅读24分钟

css

1.实现三栏布局

1.1 流体布局

   <div class="container">
        <div class="left"></div>
        <div class="right"></div>
        <div class="main"></div>
    </div>
       .left {
            float: left;
            width: 100px;
            height: 200px;
            background-color: aqua;
        }
        .right {
            float: right;
            width: 200px;
            height: 200px;
            background-color: bisque;
        }

        .main {
            margin-left: 110px;
            margin-right: 210px;
            height: 200px;
            background-color: brown;
        }

左右模块各自向左右浮动,并设置中间模块的 margin 值使中间模块宽度自适应。

缺点就是主要内容无法最先加载,当页面内容较多时会影响用户体验。

1.2 BFC 三栏布局

   <div class="container">
        <div class="left"></div>
        <div class="right"></div>
        <div class="main"></div>
    </div>
       .left {
            float: left;
            width: 100px;
            margin-right: 10px;
            height: 200px;
            background-color: aqua;
        }
        .right {
            float: right;
            width: 200px;
            margin-left: 10px;
            height: 200px;
            background-color: bisque;
        }

        .main {
            height: 200px;
            overflow: hidden; // 创建BFC
            background-color: brown;
        }

1.3 绝对定位布局

   <div class="container">
        <div class="main"></div>
        <div class="left"></div>
        <div class="right"></div>  
    </div>
       .container {
           position: relative;
        }
        .main {
            margin: 0 210px 0 110px;
            height: 200px;
            background-color: brown;
        }
        .left {
            width: 100px;
            height: 200px;
            position: absolute;
            top: 0;
            left: 0;
            background-color: aqua;
        }
        .right {
            width: 200px;
            height: 200px;
            position: absolute;
            top: 0;
            right: 0;
            background-color: lightgreen;
        }

中间内容优先加载

1.4 table布局

   <div class="container"> 
        <div class="left"></div>
        <div class="main"></div> 
        <div class="right"></div>
    </div>
       .container {
          display: table;
          width: 100%;
        }
        .main {
            display: table-cell;
            height: 200px;
            background-color: brown;
        }
        .left {
            display: table-cell;
            width: 100px;
            height: 200px;
            background-color: aqua;
        }
        .right {
            display: table-cell;
            width: 200px;
            height: 200px;
            background-color: lightgreen;
        }

不可以设置边距。

1.5 双飞翼布局

    <div class="container"> 
        <div class="main"></div>
    </div>
    <div class="left"></div>
    <div class="right"></div>
       .container {
            float: left; //向左浮动
            width: 100%;
        }
        .main {
            height: 200px;
            margin-left: 110px;
            margin-right: 210px;
            background-color: brown;
        }
        .left {
            float: left; //向左浮动
            width: 100px;
            height: 200px;
            margin-left: -100%;
            background-color: aqua;
        }
        .right {
            float: left;//向左浮动
            width: 200px;
            margin-left: -200px;
            height: 200px;
            background-color: bisque;
        }

优先加载主体内容。

1.6 圣杯布局

    <div class="container"> 
        <div class="main"></div>
        <div class="left"></div>
        <div class="right"></div>
    </div>
        .container {
           margin-left: 110px;
           margin-right: 210px;
        }
        .main {
            float: left;
            width: 100%;
            height: 200px;
            background-color: brown;
        }
        .left {
            float: left;
            width: 100px;
            height: 200px;
            margin-left: -100%;
            position: relative;
            left: -110px;
            background-color: aqua;
        }
        .right {
            float: left;
            width: 200px;
            height: 200px;
            margin-left: -200px;
            position: relative;
            right: -210px;
            background-color: bisque;
        }

优先加载主体内容。

1.7 flex实现圣杯布局

    <div id="container">
        <p class="center">我是中间(圣杯布局中间内容优先加载)</p>
        <p class="left">我是左边</p>
        <p class="right">我是右边</p>
    </div>
        #container {
            display: flex;
            height: 200px;
        }
        .center {
            flex:1;
            order: 1;
            background-color: aquamarine;
        }
        .left {
            width: 200px;
            order: 0;
        }
        .right {
            width: 200px;
            order: 2;
        }

主体内容优先加载,利用order属性值控制显示位置。

2. 文本溢出省略

2.1 单行文本

       <div class="ellipsis">对于单行文本,使用单行省略</div>
    
        .ellipsis {
           width: 100px;
           text-overflow: ellipsis;
           white-space: nowrap;
           overflow: hidden;
           outline: 1px solid #ffdd00;
        }

2.2 多行文本

        <div class="ellipsis">对于多行文本的超长省略对于多行文本的超长省略</div>
        
        .ellipsis {
            width: 100px;
            text-overflow: ellipsis;
            overflow: hidden;
            display: -webkit-box;
            white-space: normal;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;
        }

1. 将指定数组转换为树结构

  // 方式1:迭代
  const arrayToTree = arr => {
    let result = [];
    if (!Array.isArray(arr) || arr.length === 0) {
      return result;
    }
    let map = {};
    arr.forEach(item => map[item.id] = item);
    arr.forEach(item => {
        const parent = map[item.pid];
        if (parent) {
          (parent.children || (parent.children = [])).push(item);
        } else {
            result.push(item);
        }
    })
    return result;
}
// 方式2:递归
const arrayToTree2 = (arr, pid) => {
    let res = [];
    if (!Array.isArray(arr) || arr.length === 0) {
        return result;
    }
    arr.forEach(item => {
        if (item.pid === pid) {
         let itemChildren = arrayToTree2(arr, item.id);
         if (itemChildren.length) {
           item.children = itemChildren;
         }
         res.push(item)
        }
    });
    return res;
}
// 测试输入结构
const arr = [
            {
                name: '电器',
                id: 1,
                pid: 0,
            },
            {
                name: '冰箱',
                id: 11,
                pid: 1,
            },
            {
                name: '格力',
                id: 111,
                pid: 11,
            },
            {
                name: '海尔',
                id: 112,
                pid: 11,
            },
            {
                name: '电视',
                id: 12,
                pid: 1,
            },
            {
                name: '化妆品',
                id: 2,
                pid: 0,
            },
            {
                name: '口红',
                id: 21,
                pid: 2,
            },
            {
                name: '眉笔',
                id: 22,
                pid: 2,
            }
        ];

        const result = arrayToTree(arr);
        const result2 = arrayToTree2(arr,0);
        console.log(result)
        console.log(result2)

结果:

1647775526(1).jpg

2.切换字符串中字母的大小写

// 1: 使用正则表达式
        function tottleLetter(str) {
           if (!str.length) return str;
           let newStr = '';
           let reg1 = /[a-z]/;
           let reg2 = /[A-Z]/;
           for(let char of str) {
              if (reg1.test(char)) {
                 newStr += char.toUpperCase();
              } else if (reg2.test(char)) {
                 newStr += char.toLowerCase();
              } else {
                  newStr += char;
              }
           }
           return newStr;
        }
 // 2:使用ascii
        function tottleLetter2(str) {
            if (!str.length) return str;
            let newStr = '';
            for (let i = 0; i < str.length; i++) {
               let charCode = str.charCodeAt(i)
               if (charCode >= 65 && charCode <= 90) {
                  newStr += str[i].toLowerCase();
               } else if (charCode >= 97 && charCode <= 122) {
                  newStr += str[i].toUpperCase();
               } else {
                  newStr += str[i]
               }
            }
            return newStr;
        }
 // 3: 使用辅助数组,因为已经明确知道是26个字母大小写之间的转换;
        // 万一忘记正则表达式的写法和ascii码范围的情况下可使用。
        function tottleLetter3(str) {
           const helper1 = ['a','b','d','d','e','f','g',
                            'h','i','j','k','l','m','n',
                            'o','p','q','r','s','t','u',
                            'v','w','x','y','z'];
           const helper2 = ['A','B','C','D','E','F','G',
                            'H','I','J','K','L','M','N',
                            'O','P','Q','R','S','T','U',
                            'V','W','X','Y','Z'];  
           let newStr = ''                 
           for(let char of str) {
            if (helper1.indexOf(char) > -1) {
                 newStr += char.toUpperCase();
            } else if (helper2.indexOf(char) > -1) {
                 newStr += char.toLowerCase();
            } else {
                  newStr += char;
            }
           }     
           return newStr;                         
        }

总结一下三种方法: 方法1是最容易想到的实现,但是我本人目前对正则表达式不熟悉,在面试的时候不一定能写对某些正则,当然啦,这道题的正则比较简单。 方法2用的ASCII编码去实现,个人认为这种实现应该是三者间性能最好的一种,但是,还是那句话,面试碰到还真不一定记得字母的编码区间。 方法3用的两个辅助数组实现,本来想着用map结构,因为它读取数据较快,但是读入26个大小写字母太麻烦了,再加上辅助空间里就26个常量级的字母,indexOf()方法带来的时间消耗应该也可以视为常量级吧。

3.日期格式化

       Date.prototype.format = function(format) {
           let obj = {
               'M+': this.getMonth() + 1, // 月份
               'd+': this.getDate(), // 日
               'H+': this.getHours(), // 小时
               'm+': this.getMinutes(), // 分
               's+': this.getSeconds(), // 秒
               'q+': Math.floor((this.getMonth() + 3) / 3), // 季度
               'S': this.getMilliseconds() // 毫秒
           }
           if (/(y+)/.test(format)) {
               format = format.replace(RegExp.$1,
               (this.getFullYear() + '').substr(4 - RegExp.$1.length))
           }
           for (let key in obj) {
              if (new RegExp('(' + key + ')').test(format)) {
                 format = format.replace(RegExp.$1, 
                 (RegExp.$1.length == 1) ? obj[key] : (('00') + obj[key]).substr(('' + obj[key]).length))
              }
           }
           return format;
        }

        let d = new Date();
        console.log(d.format('yyyy-MM-dd HH:mm:ss.S'))
        console.log(d.format('yyyy-MM-dd'))
        console.log(d.format('yyyy-MM-dd q HH:mm:ss'))

4. 计算一个对象最大的层数

这是我在一篇文章里看到的一道题,记录一下自己的解法。

function getObjLevel(obj){
    if(getType(obj) !== 'object'){
        throw new Error('paramater must be object')
    }
    // 用来保存结果,初始化层级为0
    let res = 0;
    function loopGetLevel(obj, level=res) {
        //当前数据是不是对象,是对象就继续,否则比较下层级和值,哪个大取哪个
        if (typeof obj === 'object') {
            //对对象的每个属性进行遍历,如果还是对象就递归计算,否则就比较res和level取最大值
            for (var key in obj) {
                if (typeof obj === 'object') {
                    loopGetLevel(obj[key], level + 1);
                } else {
                    // 当前层数和返回值比较取更大的
                    res = level + 1 > res ? level + 1 : res;
                }
            }
        } else {
            // 当前层数和返回值比较取更大的
            res = level > res ? level : res;
        }
    }
    loopGetLevel(obj)
    return res
}
// 辅助函数用来判断数据类型
function getType(param){
    return Object.prototype.toString.call(param).slice(8,-1).toLowerCase()
}

// 测试输入输出
const obj = {
           a: {
               b: [2]
           },
           c: {
               b: {
                   d: 1
               }
           }
       } 
       console.log(getObjLevel(obj)); // 3

5. 数字的千分位格式化

        // 10200300 => 10,200,300
        function numFormat(num) {
           if (typeof num !== 'number') {
               throw new Error('parameter should be type of number')
           }
           let source = num.toString();
           let target = '';
           // 从后向前遍历
           for (let i = source.length - 1; i >= 0; i--) {
              if (i != 0 && (source.length - i) % 3 === 0) {
                target = ',' + source[i] + target;
              } else {
                  target = source[i] + target;
              }
           }
           return target;
        }
        console.log(numFormat(10200300)) // 10,200,300
        console.log(numFormat(200300)) // 200,300
        console.log(numFormat(100200300)) // 100,200,300
        console.log(numFormat(1200300)) // 1,200,300
        console.log(numFormat(300)) // 300
        console.log(numFormat(30)) // 30
        console.log(numFormat(3)) // 3

6. 使用arguments参数实现不定参数求和函数

6.1 arguments对象相关知识点:

  1. arguments对象是所有函数都具有的一个内置局部变量,表示的是函数实际接收的参数,是一个类数组结构。因为它除了具有length属性外,不具有数组的一些常用方法
  2. arguments对象只能在函数内部使用,无法在函数外部访问到arguments对象。
  3. arguments对象是一个类数组结构,可以通过索引访问,每一项表示对应传递的实参值,如果该项索引值不存在,则会返回“undefined”。
  4. 关于arguments对象与形参之间的关系,可以总结为以下几点。
    1. arguments对象的length属性在函数调用的时候就已经确定,不会随着函数的处理而改变。
    2. 指定的形参在传递实参的情况下,arguments对象与形参值相同,并且可以相互改变。
    3. 指定的形参在未传递实参的情况下,arguments对象对应索引值返回“undefined”。
    4. 指定的形参在未传递实参的情况下,arguments对象与形参值不能相互改变。
  5. arguments对象有一个很特殊的属性callee,表示的是当前正在执行的函数,在比较时是严格相等的。
  6. 使用arguments.callee属性后会改变函数内部的this指向。如果需要在函数内部进行递归调用,推荐使用函数声明或者使用函数表达式,给函数一个明确的函数名。
function foo(a,b,c) {
  console.log(arguments.length); // 2
}
// 在形参a、b都传递了实参的情况下,对应的arguments[0]与arguments[1]与a和b相互影响,
// 因此输出a与arguments[1]时,会输出“11”与“12”
arguments[0] = 11;
console.log(a); // 11

b = 12;
console.log(arguments[1]); // 12

arguments[2] = 3;
console.log(c); // undefined
// 在形参c未传递实参的情况下,对arguments[2]值的设置不会影响到c值,对c值的设置也不会影响到arguments[2]
c = 13;
console.log(arguments[2]); // 3

// arguments对象的length属性是由实际传递的参数个数决定的,所以arguments.length会输出“2”
// arguments对象的长度从一开始foo(1, 2)调用时就已经确定,
// 所以即使在函数处理时给arguments[2]赋值,都不会影响到arguments对象的length属性,输出“2”。
console.log(arguments.length); // 2

foo(1, 2);
   

6.2 求和函数实现:

        function getSum() {
            let arr = Array.prototype.slice.call(arguments);
            return arr.reduce((prev, cur) => prev + cur, 0);
        }
        
        console.log(getSum(1)) // 1
        console.log(getSum(1, 2)) // 3
        console.log(getSum(1,2,3)) // 6

7. 实现a + 1 < a 返回 true

可以将变量a定义成一个对象,对象在进行运算时,会进行隐式类型转换,即会调用valueOf()函数,只要改写一下该函数即可。valueOf函数的改写,是实现类似问题的关键。

    // 案例1
    const a = {
            value: 1,
            valueOf: function() {
                return this.value += 2
            }
    }
    console.log(a + 1 < a); // true
    // 案例2
    const b = {
       value: 0,
       valueOf: function() {
          return this.value += 1
       }
    }
    
    console.log(b == 1 && b == 2 && b == 3) // true
    
    // 判断b == 1时,b的valueOf函数执行,自增1,此时b的数值为1, 1==1 => true
    // 后面两个判断逻辑一样
  

8. ['1','2','3'].map(parseInt)的输出是什么?

先看一下运行结果:

image.png

我第一次看到这个题的时候,心里还鄙视了一下这种写法,认为parseInt怎么没有显示的传参,我不可能写出这样的代码,后来了解到这是point free编程风格,可能写react的小伙伴用的比较多。

首先了解一下parseInt(string, radix)函数:

parseInt()函数用于将一个字符串解析成十进制整数,并且用指定的基数(进制)用于解析。如果该字符串无法转换成Number类型,则会返回“NaN”。基数的范围是2~36。

若没有传递radix,

  1. 当string以'0x'开头,则按16进制解析
  2. 当string以'0'开头,则按8进制解析(但es5取消了,部分浏览器可能仍然可以按8进制解析)
  3. 其他情况均按10进制解析

所以,当使用parseInt函数时,一定要传入第二个参数。

其次,可以看看在控制台打印出map函数遍历数组时两个参数的输出:

image.png 有上图可知,第一个参数是数组值,第二个参数是数组下标值。

那么根据point free风格,map中函数的两个参数将对应传递给parseInt两个参数。最后执行的函数形式相当于:

 ['1','2','3'].map((value, index) => parseInt(value, index));
 // parseInt('1', 0) 1:0不在基数范围,但会解析成没有传第2个参数,最后会用十进制解析"1",故结果为1
 // parseInt('2', 1) NaN: 1不在基数范围,故无法将字符串解析成Number类型,故结果为NaN
 // parseInt('3', 2) NaN: 2在基数范围,有效数字为0、1,但是3超过进制范围,故无法解析成Number,结果为NaN

9. 数组去重的8种实现

// 定义一个待去重的的数组
const testArr = [1,1,0,0,true,true,'true','true',false,false,undefined,undefined,null,null,NaN,NaN,'a','a',{},{},{a:2},{a:2}];

1. 双重循环去重

function distinct1(arr) {
      for (let i = 0; i<arr.length; i++) {
          for (let j = i+1; j<arr.length; j++) {
              if (arr[j] === arr[i]) {
                  arr.splice(j, 1);
                  j--;
              }
          }
      }
      return arr;
 }

image.png

严格相等无法去除NaN和对象

2.使用indexOf去重

    function distinct2(arr) {
        const newArr = [];
        for (let i = 0; i < arr.length; i++) {
            if(newArr.indexOf(arr[i]) === -1){
                newArr.push(arr[i]);
            }
        }
        return newArr;
    }

image.png

indexOf和严格相等去重结果一样。

3.使用对象的属性唯一性去重

    function distinct3(arr) {
        const obj = {};
        const newArr = [];
        for (let i = 0; i < arr.length; i++) {
            if(!obj[arr[i]]) {
               obj[arr[i]] = true;
               newArr.push(arr[i]);
            }
        }
        return newArr;
    }

数组中的值转为对象的键值,由于对象的键为string类型,所以这种方式无法区分 true 和'true'以及对象类型。

image.png

image.png

4. 使用JSON.stringify去重(在方法3的基础上增强了类型检查和对象的区分)

     function distinct4(arr) {
        const obj = {};
        const newArr = [];
        for (let i = 0; i<arr.length; i++) {
            if(!obj[typeof arr[i] + JSON.stringify(arr[i])]) {
                obj[typeof arr[i] + JSON.stringify(arr[i])] = true;
                newArr.push(arr[i]);
            }
        }
        return newArr;
    }

image.png

image.png

5. 使用reduce去重

   function distinct5(arr) {
     const newArr = arr.sort().reduce(function (prev,cur) {
         if (prev.indexOf(cur) === -1) {
             prev.push(cur);
         }
         return prev;
      },[])
        return newArr;
    }

image.png

6. 使用sort()函数去重

    function distinct6(arr) {
        arr.sort();
        const newArr = [arr[0]]; // 问题:若不先赋第一个数值,0将会消失
        for(let i = 1; i < arr.length; i++) {
            if (arr[i] !== arr[i-1]) {
                newArr.push(arr[i])
            }
        }
        return newArr;
    }

image.png

7. 使用set去重

    function distinct7(arr) {
        let s = new Set(arr);
        return Array.from(s);
    }

Set可以去重NaN

image.png

8. 使用map去重

    function distinct8(ary) {
        let newAry =[];
        let map = new Map();
        for(let i = 0; i<ary.length; i++){
            if(!map.has(ary[i])){
                map.set(ary[i], true);
                newAry.push(ary[i]);
            }
        }
        return newAry;
    }

image.png

10. 实现防抖和节流

防抖(debounce)

限制执行次数,多次密集的触发只执行一次。

image.png 假设在input框中连续输入进行查询,比如上图的每一个颜色块代表一波输入,使用了防抖后,在停止输入后的比如200ms后才真正查询最后一次的输入。

       function debounce(fn, delay = 200) {
            let timeout;
            return function () {
                let context = this;
                let args = arguments;
                // 每一次用户查询触发,都会取消上次的回调
                // 这样才能保证持续输入过程中不会触发回调
                // 保证最后一次的输入停止200ms后触发
                // 若停止后的200ms内又触发了搜索,那么继续清除上一次的回调
                clearTimeout(timeout);
                
                timeout = setTimeout(() => {
                    fn.apply(context, args)
                }, delay);
            }
        }

节流(throttle)

限制执行频率,有节奏的执行 image.png

比如说拖拽元素的场景中,鼠标选中一个元素进行拖拽,假设过程中回调函数会实时输出元素的位置,如果使用了节流函数并且设定delay为100ms,那么第一次回调执行后,此后的100ms内不会再执行回调,100ms后再执行第二次。

所以表现上,回调的触发具有时间规律感。如果拖拽持续了1000ms,那么回调从0ms开始,每隔100ms执行一次回调。

        function throttle(fn, delay) {
            let timer;
            return function() {
                let context = this;
                let args = arguments;
                if (timer) return; // 如果有回调函数在执行,不再执行新的回调
                timer = setTimeout(() => {
                        func.apply(context, args);
                        // 回调执行完毕后清空timer
                        timer = null; 
                    }, delay)

            }
        }

11. 闭包

概念:

在JavaScript中存在一种内部函数,即函数声明和函数表达式可以位于另一个函数的函数体内,在内部函数中可以访问外部函数声明的变量,当这个内部函数在包含它们的外部函数之外被调用时,就会形成闭包。

闭包的优点:

  1. 保护函数内变量的安全,实现封装,防止变量流入其他环境发生命名冲突,造成环境污染。
  2. 在适当的时候,可以在内存中维护变量并缓存,提高执行效率。

闭包的缺点

  1. 消耗内存:通常来说,函数的活动对象会随着执行上下文环境一起被销毁,但是,由于闭包引用的是外部函数的活动对象,因此这个活动对象无法被销毁,这意味着,闭包比一般的函数需要消耗更多的内存。
  2. 泄漏内存:在IE9之前,如果闭包的作用域链中存在DOM对象,则意味着该DOM对象无法被销毁,造成内存泄漏。

闭包封装一个栈:

        const stack = (function() {
            const arr = [];
            return {
                push: function(val) {
                  arr.push(val) // 内部函数引用了外部函数的arr变量
                },
                pop: function() {
                    arr.pop(); // 内部函数引用了外部函数的arr变量
                },
                size: function() {
                   return arr.length; // 内部函数引用了外部函数的arr变量
                }
            }
        })();

        stack.push(1)
        stack.push(2)
        console.log(stack.size()) // 2
        stack.pop()
        console.log(stack.size()) // 1

上面的代码中存在一个立即执行函数,在函数内部会产生一个执行上下文环境,最后返回一个表示栈的对象并赋给stack变量。在匿名函数执行完毕后,其执行上下文环境并不会被销毁,因为在对象的push()、pop()、size()等函数中包含了对arr变量的引用,arr变量会继续存在于内存中,所以后面几次对stack变量的操作会使stack变量的长度产生变化。

闭包实现缓存:

因为闭包不会释放外部变量的引用,所以能将外部变量值缓存在内存中。

      let cachedBox = (function() {
            const cache = {};
            return {
                searchBox: function(id) {
                   if (id in cache) {
                       return `查找的结果为:${cache[id]}`;
                   }
                   const result = dealFn(id);
                   cache[id] = result;
                   return `查找的结果为:${result}`
                }
            }
        })();
        function dealFn(id) {
            console.log('这是一个很耗时的操作');
            return id;
        }

        console.log(cachedBox.searchBox(1))
        console.log(cachedBox.searchBox(1))

在上面的代码中,末尾两次调用searchBox(1)()函数,在第一次调用时,id为1的值并未在缓存对象cache中,因为会执行很耗时的函数,输出的结果为“1”。

而第二次执行searchBox(1)函数时,由于第一次已经将结果更新到cache对象中,并且该对象引用并未被回收,因此会直接从内存的cache对象中读取,直接返回“1”,最后输出的结果为“1”。

这样并没有执行很耗时的函数,还间接提高了执行效率。

看代码,说输出:


       function logNum() {
            const arr = [1,2,3];
            for (var i = 0; i < arr.length; i++) {
                setTimeout(() => {
                    console.log(arr[i])
                }, 100);
            }
        }
       logNum(); // undefined undefined undefined 

setTimeout()函数与for循环在调用时会产生两个独立执行上下文环境,当setTimeout()函数内部的函数执行时,for循环已经执行结束,而for循环结束的条件是最后一次i++执行完毕,此时i的值为3,所以实际上setTimeout()函数每次执行时,都会输出arr[3]的值。而因为arr数组最大索引值为2,所以会间隔一秒输出“undefined”。

       function foo(a, b) {
           console.log(b);
           return {
               foo: function(c) {
                 return foo(c, a)  // 内部函数引用了外部函数的a变量
               }
           }
        }
        // 在执行foo(0)时,未传递b值,所以输出“undefined”,并返回一个对象,将其赋给变量x。
        let x = foo(0); // undefined
        // 在执行x.foo(1)时,foo()函数闭包了外层的a值,就是第一次调用的0,此时c=1,
        // 因为第三层和第一层为同一个函数,所以实际调用为第一层的的foo(1, 0),此时a为1,b为0,输出“0”。
        x.foo(1); // 0
        // 执行x.foo(2)和x.foo(3)时,和x.foo(1)是相同的原理,因此都会输出“0”。
        x.foo(2); // 0
        x.foo(3); // 0
        
        // 执行foo(0)时,未传递b值,所以输出“undefined”,紧接着进行链式调用foo(1),实际调用为foo(1, 0),此时a为1,b为0,会输出“0”。
        
        // foo(1)执行后返回的是一个对象,其中闭包了变量a的值为1,当foo(2)执行时,实际是返回foo(2, 1),此时的foo()函数指向第一个函数,因此会执行一次foo(2,1),此时a为2,b为1,输出“1”。
        
        // foo(2)执行后返回一个对象,其中闭包了变量a的值为2,当foo(3)执行时,实际是返回foo(3, 2),因此会执行一次foo(3, 2),此时a为3,b为2,输出“2”

        let y = foo(0).foo(1).foo(2).foo(3) // undefined 0 1 2

        // 前两步foo(0).foo(1)的执行结果与x、y的分析相同,输出“undefined”和“0”。
        let z = foo(0).foo(1); // undefined 0
        // foo(0).foo(1)执行完毕后,返回的是一个对象,其中闭包了变量a的值为1,当调用z.foo(2)时,实际是返回foo(2, 1),因此会执行foo(2, 1),此时a为2,b为1,输出“1”。
        z.foo(2); // 1
        // 执行z.foo(3)时,与z.foo(2)一样,实际是返回foo(3, 1),因此会执行foo(3, 1),此时a为3,b为1,输出“1”。
        z.foo(3) // 1

在上面的代码中,出现了3个具有相同函数名的foo()函数,返回的第三个foo()函数中包含了对第一个foo()函数参数a的引用,因此会形成一个闭包。

首先捋一捋三个foo函数:

最外层的foo()函数是一个具名函数,返回的是一个具体的对象。

第二个foo()函数是最外层foo()函数返回对象的一个属性,该属性指向一个匿名函数。

第三个foo()函数是一个被返回的函数,该foo()函数会沿着原型链向上查找,而foo()函数在局部环境中并未定义,最终会指向最外层的第一个foo()函数,因此第三个和第一个foo()函数实际是指向同一个函数。

12. this的指向

  1. this指向全局对象
  2. this指向所属对象
  3. this指向实例对象
  4. this指向call()函数、apply()函数、bind()函数调用后重新绑定的对象
     // 1. this指向全局对象
        var value = 10;
        var obj = {
            varlue: 100,
            method: function() {
                var foo = function() {
                    console.log(this.value); // 10
                    console.log(this)
                };
                foo();
                return this.value;
            }
        }
        // 2. this指向所属对象
        console.log(obj.method()) // 100

        // 3. this指向实例对象
        var number = 20;
        function Person() {
            number = 30;
            this.number = 40;
        }
        Person.prototype.getNumber = function() {
            return this.number;
        }

        var p = new Person();
        console.log(p.getNumber()); // 40

        // 4. this指向call()函数、apply()函数、bind()函数调用后重新绑定的对象
         var age = 10;
         var personA = {
             age: 18
         }

         var getAge = function() {
             console.log(this.age)
         }
          // 在直接调用getAge()函数时,没有所属的对象,
          // getAge()函数中的this指向的是全局window对象,
          // 输出window.age值,因此输出“10”。
         getAge(); // 10

         // 调用getAge.call(personA)时,将getAge()函数调用的主体改为personA对象,
         // 此时this指向的是personA对象,输出personA.age值,因此输出“18”。
         getAge.call(personA) // 18
         getAge.apply(personA) // 18

         // bind()函数在改变函数的执行主体后,并没有立即调用,而是可以在任何时候调用
         getAge.bind(personA)(); // 18
         var newGetAge = getAge.bind(personA);
         newGetAge() // 18

13. call(),apply(),bind()的使用场景整理

1. 求数组中的最大项和最小项

        //使用apply()函数来改变Math.max()函数和Math.min()函数的执行主体,
        //然后将数组作为参数传递给Math.max()函数和Math.min()函数。
        const arr = [1,3,4,5,6];
        // apply()函数的第一个参数为null,
        // 这是因为没有对象去调用这个函数,
        // 我们只需要这个函数帮助运算,得到返回结果。
        console.log(Math.max.apply(null, arr))
        console.log(Math.min.apply(null, arr))

2. 类数组对象转换为数组对象

        function sum() {
            // 类数组对象转换为数组对象
            const nums = Array.prototype.slice.call(arguments);
            // 求和
            return nums.reduce((prv, cur) => prv + cur, 0);
        }
        console.log(sum(1,2)) // 3
        console.log(sum(1,2,4)) // 7

3. 实现构造继承

       function Person(age) {
          this.age = age;
          this.sleep = function() {
              return this.name + ' is sleeping.'
          }
        }

        function Student(name, age) {
         // 使用call函数实现继承
         // 将Person构造函数的执行主体转换为Student对象
          Person.call(this, age);
          this.name = name || "lily"; 
        }

        const stu = new Student('Jack', 2);
        console.log(stu.sleep()); // Jack is sleeping.
        console.log(stu.age); // 2

Student构造函数最后相当于

        function Student(name, age) {
          // 来自父类的继承
          this.age = age;
          this.sleep = function() {
              return this.name + ' is sleeping.'
          }
          // 自身的实例属性
          this.name = name || "lily"; 
        }

4. 执行匿名函数

        const animals = [
            {species: 'Lion', name:'King'},
            {species: 'Monkey', name:'wukong'}
        ];
        for (var i = 0; i < animals.length; i++) {
           (function(i) {
               this.print = function() {
                   console.log(`#${i} ${this.species}: ${this.name}`)
               }
               this.print();
           }).call(animals[i], i);//匿名函数内部的this就指向animals[i]
        }

5. bind()函数配合setTimeout

        function LateBloomer() {
            this.petalCount = Math.ceil(Math.random() * 12) + 1;
        }
       /* 
            当调用setTimeout()函数时,由于其调用体是window,
            因此在setTimeout()函数内部的this指向的是window,
            而不是对象的实例。这样在1秒后调用declare()函数时,
            其中的this将无法访问到petalCount属性,
            从而返回“undefined”,因此需要手动修改this的指向
       */
        LateBloomer.prototype.bloom = function() {
            window.setTimeout(this.declare.bind(this), 1000);
        }

        LateBloomer.prototype.declare = function() {
            console.log(`I am a flower with ${this.petalCount} petals.`)
        }

        const flower = new LateBloomer();
        flower.bloom();

14. js创建对象的7种方式

1. 基于Object()构造函数

通过Object构造函数生成一个实例,然后给它增加需要的各种属性。

        let p = new Object();
        p.name = 'Jack';
        p.age = 11;
        p.getNuma = function() {
            return this.name;
        }
        p.address = {
            city: 'Shanghai',
            code: '201607'
        }

2. 基于对象字面量

       let p1 = {
            name:'Lily',
            age: 12,
            getName: function() {
                return this.name;
            },
            address: {
                city: 'AnQing',
                code: '246100'
            }
        }

3. 基于工厂模式

使用工厂方法可以减少很多重复的代码,解决了创建多个相似对象的问题;但是创建的所有实例都是Object类型,存在对象类型识别问题。

        // 工厂方法,对外暴露接收的name,age,address属性值
        function createPerson(name,age,address) {
            // 函数内部创建一个对象,并为其添加各种属性
            const obj = new Object();
            obj.name = name;
            obj.age = age;
            obj.address = address;
            obj.getName = function() {
                return this.name;
            }
            // 返回创建的对象
            return obj;
        }
        const p3 = createPerson('Jenny', 8, {city:'Changchun', code:'298789'})

4. 基于构造函数模式

使用构造函数创建的对象可以确定其所属类型,解决了方法3存在的问题。但是使用构造函数创建的对象存在一个问题,即相同实例的函数是不一样的。

这就意味着每个实例的函数都会占据一定的内存空间,其实这是没有必要的,会造成资源的浪费,另外函数也没有必要在代码执行前就绑定在对象上。

       function Person(name,age,address) {
          this.name = name;
          this.age = age;
          this.address = address;
          this.getName = function() {
              return this.name;
          }
        }

        const p4 = new Person('Jenny', 8, {city:'Changchun', code:'298789'})
        const p5 = new Person('Jack', 8, {city:'Beijing', code:'298789'})

        console.log(p4 instanceof Person); // true
        console.log(p4.getName === p5.getName) // false

5. 基于原型对象的模式

基于原型对象的模式是将所有的函数和属性都封装在对象的prototype属性上。

使用基于原型对象的模式创建的实例,其属性和函数都是相等的,不同的实例会共享原型上的属性和函数,解决了方法4存在的问题。

但是方法5也存在一个问题,因为所有的实例会共享相同的属性,那么改变其中一个实例的属性值,便会引起其他实例的属性值变化,这并不是我们所期望的。

        function Person1() {}
        Person1.prototype.name = 'Lily';
        Person1.prototype.age = 18;
        Person1.prototype.address = {
            city: 'AnQing',
            code: '246100'
        }
        Person1.prototype.getName = function() {
            return this.name;
        }
        
        const pA = new Person1();
        const pB = new Person1();
        console.log(pA.name === pB.name) // true
        console.log(pA.getName === pB.getName) // true
        // 方法5的问题,会影响其他实例对象的值
        console.log(pB.age); // 18
        pA.age = 9;
        console.log(pB.age); // 9

6. 构造函数和原型混合的模式

构造函数中用于定义实例的属性,原型对象中用于定义实例共享的属性和函数。通过构造函数传递参数,这样每个实例都能拥有自己的属性值,同时实例还能共享函数的引用,最大限度地节省了内存空间。

构造函数和原型混合的模式是目前最常见的创建自定义类型对象的方式。

        function Person2(name, age, address) {
            // 构造函数中定义实例的属性
           this.name = name;
           this.age = age;
           this.address = address;
        }
         // 原型中添加实例的共享函数
        Person2.prototype.getName = function() {
            return this.name;
        }
        
        const p21 = new Person2('Jenny', 8, {city:'Changchun', code:'298789'})
        const p22 = new Person2('Jack', 8, {city:'Beijing', code:'298789'})

        console.log(p21.name) // Jenny
        console.log(p22.name)  // Jack
        
        // 实例对象属性不会相互影响
        console.log(p21.age); // 8
        p22.age = 9;
        console.log(p21.age); // 8
        
        // 共享函数
        console.log(p21.getName === p22.getName) // true

7. 基于动态原型模式(构造函数和原型混合的懒汉式模式)

动态原型模式是将原型对象放在构造函数内部,通过变量进行控制,只在第一次生成实例的时候进行原型的设置。

动态原型的模式相当于懒汉模式,只在生成实例时设置原型对象(如果没有new一个对象就不会再其上定义属性和方法),但是功能与构造函数和原型混合模式是相同的。

方法6和7相比就是饿汉式,无论是否实例化该构造函数,构造函数原型上的方法和实例都已经定义好了。

        function Person3(name, age, address) {
           this.name = name;
           this.age = age;
           this.address = address;
           if (typeof Person3._initialized === 'undefined') {
                Person3.prototype.getName = function() {
                return this.name;
                };
                Person3._initialized = true;
           }
        }
        // 下面p3实例化前,Person3.prototype上并没有getName方法,实例化后才有
        const p3 = new Person3('Jenny', 8, {city:'Changchun', code:'298789'})

15. for...in 和 for...of的区别

for...of用于遍历可迭代(Itaratorable)数据,如数组、字符串、Map、Set,且遍历的是数据的value;

for...in用于遍历可枚举(Enumerable)的数据,如对象(及其原型链上的属性)、数组和字符串,且遍历的是数据的key。

       let obj = {
            name: 'Zoey',
            age: 18,
            getAge() {
                return this.age;
            }
        }
        obj.__proto__.sayHello = function() {
            console.log('Hi, my name is'+ this.name)
        }

        for (let key in obj) {
           console.log(key) // name, age, getAge, sayHello
        }

        for (let value of obj) {
           console.log(value) // Uncaught TypeError: obj is not iterable
        }

16. 浅克隆和深克隆

浅克隆

浅克隆由于只克隆对象最外层的属性,如果对象存在更深层的属性,则不进行处理,这就会导致克隆对象和原始对象的深层属性仍然指向同一块内存。具体表现为:如果对象的深层引用属性值改变,那么所有浅克隆而来的变量相应的属性值也会发生同样的改变。

浅克隆实现:

       function shallowClone(origin) {
           const res = {};
           // 对源对象的key进行遍历
           for (let key in origin) {
              // 避免克隆原型对象上的属性
              if (origin.hasOwnProperty(key)) {
                 res[key] = origin[key]
              }
           }
           return res;
        }

        const origin = {
            a: 1,
            b: [1, 2, 4],
            c: {
                d: 'abc'
            },
            d: new Set([5,6,7])
        }

        const res = shallowClone(origin);
        console.log(res);
        
        // es6的assign函数可以进行对象浅复制
        const res1 = Object.assign({}, origin);
        console.log(res1);
        
        // 浅拷贝的引用属性的内容改变,将会影响所有变量(即origin,res也会改变)
        res1.c.d = 'new str';

深克隆

1. 使用JSON序列化和反序列化实现

先使用JSON.stringify()函数将原始对象序列化为字符串,再使用JSON.parse()函数将字符串反序列化为一个对象,这样得到的对象就是深克隆后的对象。

缺陷:

  1. 无法RegExp、Set、Map等进行克隆,这些类型数据克隆结果是空对象
  2. function类型属性值丢失
  3. 破坏原型链
  4. 若存在循环引用,会抛出异常
        function Animal(name) {
            this.name = name;
        }

        var animal = new Animal('Mimi');

        const obj = {
          a: function() {return 'a'},
          b: new RegExp('\d', 'g'),
          c: animal,
          d: new Set([1,3,4]),
          e: new Map(),
         // f: obj 循环引用会抛出异常
        }


        const resObj = JSON.parse(JSON.stringify(obj));
        console.log('resObj:', resObj)

image.png

2. 只考虑数组和对象引用类型的深克隆
       /**
         * @description: 基本的数组和对象类型深克隆
         * @param {*} origin
         * @return {*}
         */                 
        function basicDeepClone(origin) {
           if (typeof origin !== 'object' || origin == null) return origin;
           let res;
           if (Array.isArray(origin)) {
              res = [];
           } else {
               res = {};
           }
           for (let key in origin) {
              if (origin.hasOwnProperty(key)) {
                res[key] = basicDeepClone(origin[key])
              }
           }
           return res;
        }

        const obj1 = {
            a: '1',
            b:2,
            c: [1,2,3],
            d: {
                e: [1,4,5]
            }
        }

        const copiedObj1 = basicDeepClone(obj1);
        console.log('copiedObj1:', copiedObj1)

image.png

3. 考虑多种引用类型以及循环引用的深克隆
       /**
         * @description: 深克隆
         * @param {*} origin
         * @param {*} map 避免循环引用
         * @return {*}
         */        
        function deepClone (origin, map = new WeakMap()) { // 额外开辟一个存储空间WeakMap来存储当前对象
            if (origin === null) return origin // 如果是 null 就不进行拷贝操作
            if (origin instanceof Date) return new Date(origin) // 处理日期
            if (origin instanceof RegExp) return new RegExp(origin) // 处理正则
            // DOM 元素直接返回,拷贝 DOM 元素没有意义,都是指向页面中同一个
            if (origin instanceof HTMLElement) return origin // 处理 DOM元素
            // 原始类型和函数直接返回,拷贝函数没有意义,两个对象使用内存中同一个地址的函数,没有任何问题
            if (typeof origin !== 'object') return origin // 处理原始类型和函数 不需要深拷贝,直接返回

            // 是引用类型的话就要进行深拷贝
            if (map.get(origin)) return map.get(origin) // 当需要拷贝当前对象时,先去存储空间中找,如果有的话直接返回
            const cloneOrigin = new origin.constructor() // 创建一个新的克隆对象或克隆数组
            map.set(origin, cloneOrigin) // 如果存储空间中没有就存进 map 里

            Reflect.ownKeys(origin).forEach(key => { // 引入 Reflect.ownKeys,处理 Symbol 作为键名的情况
                cloneOrigin[key] = deepClone(origin[key], map) // 递归拷贝每一层
            })
            return cloneOrigin // 返回克隆的对象
        }

        const obj3 = {
            a: true,
            b: 100,
            c: 'str',
            d: undefined,
            e: null,
            f: Symbol('f'),
            g: {
                g1: {} // 深层对象
            },
            h: [], // 数组
            i: new Date(), // Date
            j: /abc/, // 正则
            k: function () {}, // 函数
            l: [document.getElementById('root')] // 引入 WeakMap 的意义,处理可能被清除的 DOM 元素
        }

        obj3.obj = obj3 // 循环引用

        const name = Symbol('name')
        obj3[name] = 'Zoey'  // Symbol 作为键

        const newObj = deepClone(obj3)

        console.log('newObj:',newObj)

image.png

17. 实现instanceOf

       /**
         * @description: 实现instanceof: Object instanceof Object
         * @param {*} leftVal
         * @param {*} rightVal
         * @return {*}
         */        
        function myInstanceof(leftVal, rightVal) {
           let leftValProto = leftVal.__proto__;
           while(true) {
              if (leftValProto === rightVal.prototype) return true;
              if (leftValProto === null) return false;
              // 实现的核心:对象通过__proto__属性实现原型链并可以通过原型链查找
              leftValProto = leftValProto.__proto__;
           }
        }

        function Person() {};
        const p = new Person();
        const obj = {};
        console.log(myInstanceof(p, Person)); // true
        console.log(myInstanceof(p, Object)); // true
        console.log(myInstanceof(p, Function)); // false
        console.log(myInstanceof(obj, Object)); // true
        console.log(myInstanceof(Object, Object)); // true
        console.log(myInstanceof(Function, Function)); // true
        console.log(myInstanceof(Person, Person)); // false

18.前端攻击

1. Cross Site Script 跨站脚本攻击

手段: 黑客将js代码插入到网页内容中,渲染时执行js代码

预防: 前端或者后端替换特殊字符

2. Cross Site Request Forgery 跨站请求伪造

手段:黑客诱导用户去访问另一个网站的接口,伪造请求

预防:严格的跨域限制 + 验证码机制

19. 数组扁平化

1. 递归实现

        function flatten(arr) {
            let newArr = [];
            if (Array.isArray(arr)) {
                for (let i = 0; i < arr.length; i++) {
                    if (Array.isArray(arr[i])) {
                       newArr = newArr.concat(flatten(arr[i]));
                    } else {
                        newArr.push(arr[i]);
                    }
                }
            } else {
                new Error('请输入一个数组类型变量');
            }
            return newArr;
        }

2.使用归并函数 reduce()

        function flatten(arr) {
           return arr.reduce(function(prev, next) {
              return prev.concat(Array.isArray(next) ? flatten(next) : next)
            }, []);
        }

3. es6 ...解构运算符 + some()

       function flatten(arr) {
            while(arr.some(item => Array.isArray(item))) {
              arr = [].concat(...arr);
            }
            return arr;
        }

4. Array.prototype.flat

flat([depth]):参数是深度,默认扁平化一层,任意层数参数设置为Infinity

        function flatten(arr) {
           return arr.flat(Infinity)
        }

20. 实现getType()函数,获取变量类型

判断数据类型的四种方式:

  1. typeof: 用来判断原始(基本)类型,其返回的字符串类型值有:boolean,number,string,undefined,bigint,object(引用类型和null的返回结果)。
  2. instanceof:判断一个数据类型是否在另一个类型的原型链上(a.proto === A.prototype)。
  3. constructor:其本质也是运用原型链去寻找,实例对象可以通过__proto__访问到原型对象,再通过原型对象的constructor属性访问到实例对象的构造函数,从而判断类型,但是undefined和null没有构造函数属性
  4. Object.prototype.toString.call():返回[Object Type], 其中Type就是数据类型,此方式也是最佳方式
        function getType(param) {
          const originType = Object.prototype.toString.call(param); // [Object String]
          const spaceIndex = originType.indexOf(' ');
          const type = originType.slice(spaceIndex + 1, -1);
          return type.toLowerCase();
        }

        console.log(getType(0)) // number
        console.log(getType(null)) // null
        console.log(getType({})) // object
        console.log(getType(undefined)) // undefined
        console.log(getType(new Set())) // set
        console.log(getType([])) // array
        console.log(getType(NaN)) // number

21. 实现new操作符

new的过程做了什么?

  1. 创建了一个新对象
  2. 将新对象的__proto__指向构造函数的prototype对象
  3. 将构造函数的作用域赋给新对象,也就是让构造函数的this指向新对象
  4. 返回新对象
       function customeNew(constructor, ...args) {
           if (typeof constructor !== 'function') {
               throw new Error('第一个参数必须为函数类型')
           }
           // 1. 创建一个空对象
           let newObj = {}
           // 2. 以下三种方式可以将新对象的__proto__指向构造函数的prototype对象
           // newObj = Object.create(constructor.prototype);
           // Object.setPrototypeOf(newObj, constructor.prototype);
            newObj.__proto__ = constructor.prototype;
           // 3. 将构造函数的this指向新对象 
           // constructor.apply(newObj, args)
           constructor.call(newObj, ...args)
           // 4. 返回新对象
           return newObj;
        }

        // 测试构造函数
        function Student(name) {
            this.name = name;
        }

        Student.prototype.getName = function() {
            console.log(this.name);
        }

        const stu1 = customeNew(Student, 'Zoey');

        console.log(stu1.__proto__ == Student.prototype); // true
        stu1.getName(); // Zoey
   

22. 深度和广度优先遍历DOM树

如果熟练掌握二叉树的各种遍历方式,这道题一点都不难,无非要清楚有childNodes和children属性。

待遍历的dom结构:

   <div id="root">
        <p>hellow <b>world</b></p>
        <img src="https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF" alt="img">
        <!-- 注释 -->
        <ul>
            <li>aaa</li>
            <li>cccc</li>
        </ul>
    </div>

获取根节点:

        const root = document.getElementById('root');
        // 注意两个属性间的区别:
        console.log(root.childNodes) // Node类型的节点集合
        console.log(root.children)  // HTMLElement类型节点集合
        
         // 输出遍历的节点信息
        const visitNode = function(node) {
           if (node instanceof Comment) console.info('Comment Node:', node.textContent)

           if (node instanceof Text && node.textContent?.trim()) console.info('Text node:', node.textContent?.trim())

           if (node instanceof HTMLElement) console.info('Element Node:', `<${node.tagName.toLocaleLowerCase()}>`)
        }

深度优先遍历:

     // 深度优先遍历:递归实现,也可用迭代方式实现
        const dfs = function(root) {
           // 递归结束条件 
           if (root == null || !root.childNodes) return;
           root.childNodes.forEach(node => {
               visitNode(node)
               dfs(node)
           });
        }
        dfs(root)

广度优先遍历:

      // 广度优先遍历,需要用到队列
        const bfs = function(root) {
           const queue = []; // 用数组当作队列
           if (root && root.childNodes) queue.push(root);
           while(queue.length) {
             const curr = queue.shift()
             const size = curr.childNodes.length;
             for (let i = 0; i < size; i++) {
                const node = curr.childNodes[i] 
                visitNode(node)
                if (node && node.childNodes) queue.push(node)
             }
           }
        }
        bfs(root)

23. 手写LazyMan,实现链式调用和sleep机制

      class LazyMan {
            name;
            tasks = []; // 任务队列
            constructor(name) {
                this.name = name;
                // 触发任务
                setTimeout(() => {
                    this.next();
                });
            }
            // 执行任务
            next() {
                const task = this.tasks.shift();
                if (task) task();
            }

            eat(food) {
                // 定义任务函数
                const task = () => {
                    console.log(`${this.name} eat ${food}`)
                    // 任务执行完毕,立即触发下一个任务
                    this.next();
                }
                // 将任务加入队列
                this.tasks.push(task)
                // 返回this,实现链式调用
                return this;
            }

            sleep(seconds) {
                const task = () => {
                    console.log(`${this.name}开始等待`)
                    setTimeout(() => {
                        console.log(`${this.name}已经等待了${seconds}s`)
                        this.next();
                    }, seconds * 1000)
                }
                this.tasks.push(task)
               
                return this;
            }
        }
        const me = new LazyMan('Zoe')
        me.eat('apple').eat('peach').sleep(2).eat('banana')

执行结果:

image.png

24. 手写eventBus

      class EventBus {
            constructor() {
                this.events = {};
            }
            // 绑定事件,可以执行多次
            on(type, fn, isOnce = false) {
               const events = this.events;
               if (events[type] == null) {
                   events[type] = [];
               }
               events[type].push({fn, isOnce})
            }
            // 绑定事件,最多能执行一次
            once(type, fn) {
               this.on(type, fn, true);
            }

            off(type, fn) {
               if (!fn) {
                 // 解绑所有 type 的函数
                 this.events[type] = []
               } else {
                   // 解绑单个fn
                   const fnList = this.events[type]
                   if (fnList) {
                      this.events[type] = fnList.filter(item => item.fn !== fn)
                   }
               }
            }

            emit(type, ...args) {
               const fnList = this.events[type];
               if (fnList == null) return;

               this.events[type] = fnList.filter(item => {
                   const {fn, isOnce} = item;
                   fn(...args);
                   // once 执行一次就要被过滤掉
                   if (!isOnce) return true;
                   return false;
               })
            }
        }

        const e = new EventBus();
        function fn1(a, b) {console.log('fn1', a, b)}
        function fn2(a, b) {console.log('fn2', a, b)}
        function fn3(a, b) {console.log('fn3', a, b)}

        e.on('key1', fn1)
        e.on('key1', fn2)
        e.once('key1', fn3)
        e.on('key2', fn3)

        e.emit('key1', 10, 20)
        e.emit('key1', 102, 203)

25. js实现LRU缓存的两种方式

LRU是一种缓存淘汰策略,按访问的时许淘汰。通俗的讲,若按时间顺序先后访问了a -> b -> c,假设缓存的容量为2,那么按照LRU策略,要删除的就是最先被访问的a。

1. 利用js中Map结构能够记住键的原始插入顺序的特点实现

        class LRUCache {
            constructor(size) {
                if (size < 1) throw new Error('Invalid size.')
                // 利用js map结构有序特点,靠前的是旧数据,结尾部分是最新数据
                this.cache = new Map();
                this.size = size;
            }

            set(key, value) {
                if (this.cache.has(key)) {
                    this.cache.delete(key)    
                } 

                this.cache.set(key, value)
                // 若插入数据后,cache的大小超过指定值,那么删除缓存中第一个键值对
                if (this.cache.size > this.size) {
                    // 获取缓存中第一键值对的key值并删除
                    const firstKey = this.cache.keys().next().value;
                    this.cache.delete(firstKey)   
                }  
            }

            put(key) {
                // 缓存中该key没有对应值
               if (!this.cache.has(key)) return;
               
               // 访问过缓存中的项,那么该项需要删除,并重新更新到缓存的最后
               const value = this.cache.get(key);
               this.cache.delete(key)
               this.cache.set(key, value)

               return value;
            }
        }   

        const lruCache = new LRUCache(2)
        lruCache.set(1, 1) // {1=1}
        lruCache.set(2, 2) // {1=1, 2=2}
        console.info(lruCache.put(1)) // 1 {2=2, 1=1}
        lruCache.set(3, 3) // {1=1, 3=3}
        console.info(lruCache.put(2)) // undefined
        lruCache.set(4, 4) // {3=3, 4=4}
        console.info(lruCache.put(1)) // undefined
        console.info(lruCache.put(3)) // 3 {4=4, 3=3}
        console.info(lruCache.put(4)) // 4 {3=3, 4=4}

26. 实现继承的6中方式

1. 原型继承

        // 1. 原型继承
        // 问题1:子类所有实例会共享引用属性,Child.prototype.colors
        // 问题2:子类无法像父类的构造函数传参
        function Parent(name) {
            this.name = name;
            this.colors = ['red', 'black']
        }
        Parent.prototype.getName = function() {
            console.log(this.name)
        }

        function Child(age) {
            this.age = age
        }
        // 子类的原型是父类的实例
        Child.prototype = new Parent();

        Child.prototype.getAge = function() {
            console.log(this.age)
        }

2. 构造函数继承

         // 2. 构造函数继承
        // 问题:保证了每个实例都有自己的属性,但每个实例都有功能相似的方法,无法做到函数复用
        function Parent2(name) {
            this.name = name;
            this.colors = ['red', 'black'];
            this.getName = function() {
                console.log(this.name)
            }  
        }

        function Child(name, age) {
            Parent2.call(this, name)
            this.age = age;
            this.getAge = function() {
                console.log(this.age)
            }
        }

3. 组合继承

        // 3. 组合继承(最常用的继承模式)
        // 思想:使用原型链实现对原型属性和方法的继承,通过构造函数来实现对实例属性的继承
        // 问题:会调用两次父类构造函数
        function Parent3(name) {
            this.name = name;
            this.colors = ['red', 'black'];
        }
        Parent3.prototype.getName = function() {
            console.log(this.name)
        }

        function Child3(name, age) {
            Parent3.call(this, name) // 调用一次
            this.age = age
        }
        // 子类的原型是父类的实例
        Child3.prototype = new Parent(); // 调用两次
        // Child3.prototype.constructor = Child3 // 这种定义的构造函数属性是可以遍历的
        Object.defineProperty(Child3.prototype, 'constructor', {
            configurable: false,
            enumerable: false,
            value: Child3
        })

        Child3.prototype.getAge = function() {
            console.log(this.age)
        }

        console.log(new Child3())

4. 原型式继承

        // 4. 原型模式
        // 用一个已有对象替换一个构造函数的原型,已有对象将是创建出来对象的原型
        // 本质也是替换原型对象,但是和原型继承相比,4的实现无法知道确切的对象类型
        function object(obj) {
            function F() {};
            F.prototype = obj;
            return new F();
        }

        let p = {
            name: 'll',
            age: 10
        }
        let p1 = object(p)
        console.log(p1)
        console.log(p1.__proto__ === p) // true
        // 等同于
        let p2 = Object.create(p)
        console.log(p2)
        console.log(p2.__proto__ === p) // true
         // create函数第二个参数注意点
        let p3 = Object.create(p, {
           age: {
            value: 99,
            enumerable: false
           }
        })

        console.log(p3)
        console.log(p3.__proto__ === p) // true

5. 寄生继承

        // 5. 寄生式继承
        // 基于originObj返回了一个新的对象,
        // 新对象不仅有originObj的属性和方法,还有自己的sayHi方法
        function createTheObj(originObj) {
           let cloneObj = Object.create(originObj);
           // 增强对象
           cloneObj.sayHi = function() {
            console.log('hi')
           }
           return cloneObj;
        }

6. 寄生组合式继承(最理想的继承实现)

        // 6. 寄生组合式继承
        // 原型链能保持不变
        // 只调用一次父类构造函数,避免子类型上创建不必要的、多余的属性和方法
        function inheritPrototype(parent, child) {
           // 创建对象
           let prototype = Object.create(parent.prototype) 
           // 增强对象
           prototype.constructor = child;
           // 指定对象
           child.prototype = prototype;
        }

        function Parent6(name) {
            this.name = name;
            this.colors = ['red', 'black'];
        }
        Parent6.prototype.getName = function() {
            console.log(this.name)
        }

        function Child6(name, age) {
            Parent6.call(this, name) // 调用一次
            this.age = age
        }
         // 相比组合模式,仅仅是获取父类原型上的属性和方法的方式的改变
         // 避免了第二次调用父类构造函数,在子类原型上增加属性和方法
        inheritPrototype(Parent6, Child6)
        
        Child6.prototype.getAge = function() {
            console.log(this.age)
        }

        console.log(new Child6())