前端基础查漏补缺

180 阅读12分钟

日常工作中,有时会遇到一些js相关内容,还未掌握i熟练,因此在这里边练边记录。

JS基础

一、ES5实现let和const

let的实现

// 利用自执行函数模拟

    (function(){
      var a = 1;
    }());
    console.log(a) // 报错
    
    babel对es6 let 的转化是在定义的变量前加了下划线 var _a = 1;

实现const

    // const a = 1;
    function _const(prop, value){
        let desc = {
            value,
            writable: false; //是否可修改
            // configurable: false // 是否能通过delete删除
            // enumable: false // 是否能通过 for...in... 遍历
        };
        
         Object.defineProperty(window, prop, desc);
    }
    
    _const('name', 'caixukun');
    window.name //'caixukun'
    window.name = 6;// 不生效

var 和 let/const 的区别:

  1. 变量提升:var定义的变量,声明部分会被添加到当前作用域的顶部,在赋值之前获取为undefined;暂时性死区:let/const/class定义的变量在声明和赋值之前使用会报错;
  2. var可以重复声明,let/const不可以;
  3. var是函数作用域,可以跨块访问;let/const是块级作用域;块级作用域是函数作用域的子集;
  4. var声明的变量会被挂载到全局window上,而let/const不会

二、防抖/节流

防抖:延迟执行;

实现原理: 给一个事件设置一个定时器,约定x秒后触发执行;如果在x秒内再次触发了该事件,则清空定时器,重新设置;

    function debounce(func, wait){
        let timer = null;
        return function(){
            if(timer) clearTimeout(timer); // 如果已经存在定时器,则清除
            timer = setTimeout(() => {
                func.apply(this, arguments)
            }, wait)
        }
    
    }

节流: 间隔执行;

实现原理1:给一个事件设置一个定时器,约定x秒后触发执行;如果在x秒内再次触发了该事件,则忽略此次触发,等待定时器执行;

    function throttle(func, wait){
        let timer = null;
        return function(){
            if(!timer){
                timer = setTimeout(() => {
                    timer = null; //先清空,如果使用clearTimeout(timer) , timer为被赋值为1,因此使用赋值方式
                    func.apply(this, arguments);
                }, wait)
            }
        }
    }

实现原理2: 比较时间戳;如果此刻触发的时间戳减去初始时间戳大于等于约定时间,则执行;

    function throttle(func, wait){
        let prev = 0; // 初始值为0
        return function(){
            let now = Date.now(); // 获取此刻时间戳
            if(now - prev >= wait){
                prev = now; // 将初始值设置为此刻
                func.apply(this, arguments);
            }
        }
    }

三、手写call/apply/bind

实现call

    //func.call(thisArg, ...args) // 调用方式
    //call: 给定一个this对象和一系列参数来调用函数func
    
    Function.prototype._call = function(thisArg, ...args){
        //声明独一无二的属性,防止thisArg上属性被覆盖
        const fn = Symbol('fn');
        //thisArg的异常判断,如果传入的是undefined,则指向window;
        thisArg = thisArg || window;
        //函数体内的this指向函数的直接调用者,因此this指向的是func
        thisArg[fn] = this;
        //获取结果
        let result = thisArg[fn](...args);
        //移除thisArg上多余的属性fn
        delete thisArg[fn];
        //返回结果
        return result;
    }
   

实现apply

    //func.apply(thisArg, args) //调用方式
    //apply: 给定一个this对象和一个数组形式的参数来调用函数func
    
    Function.prototype._apply = functon(thisArg, args){
        //声明独一无二的属性,防止thisArg上属性被覆盖
        const fn = Symbol('fn');
        //thisArg的异常判断,如果传入的是undefined,则指向window;
        thisArg = thisArg || window;
        //函数体内的this指向函数的直接调用者,因此this指向的是func
        thisArg[fn] = this;
        //获取结果
        let result = thisArg[fn](...args);
        //移除thisArg上多余的属性fn
        delete thisArg[fn];
        //返回结果
        return result;
    }
    

实现bind

    // func.bind(thisArg, args)// 调用方式
    // 给定一个this对象和一个数组形式的参数来调用函数func
    // 返回一个函数,需要注意:
    // 返回的函数也可以接受参数,两者参数合并传递给func
    // 返回的函数也可能出现被new的情况,如果被 new ,则this指向被new出来的对象
    // 返回的函数需要继承func的原型
    // 根据以上:
    
    Function.prototype._bind = function(thisArg, args){
        //函数内部的this指向函数的调用者,因此此时this指向func;
        let self = this;
        
        let Bind = function(){
            // 如果是new出来的,则指向new出来的对象
            // 参数合并,传递给func
            return self.apply(this instanceOf Bind ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)));
        };
        // 浅拷贝func的原型对象
        Bind.prototype = Object.create(self.prototype);
        
        return Bind;
    }

四、多维数组扁平化

以此多维数组为例: var arr = [1,[2,[3,[4,[5]]]]];

ES6: Array.prototype.flat()方法

    // 接收一个参数,表示扁平层级
    arr.flat(Infinity) //Infinity表示全部; [1,2,3,4,5]
    Array.prototype.flat.call(arr, Infinity) // [1,2,3,4,5]

循环+递归

    function flat(arr){
        let result = [];
        arr.forEach(i => {
            i instanceOf Array ? result = result.concat(flat(i)) : result.push(i);
        });
        return result;
    }

Json序列化+反序列化

    let arrStr = JSON.stringify(arr); //转为Json
    let str1 =  arrStr.replace(/\[|\]/g, ''); //去掉[ ]符号
    let newArrStr = `[${str1}]`
    let newArr = JSON.parse(newArrStr) // [1,2,3,4,5]

reduce + 递归

    function flat(arr) {
        return arr.reduce((pre, cur) => {
           return pre.concat(cur instanceof Array ? flat(cur) : cur)
        }, [])
    }

展开运算符

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

五、数组转树结构数据

以此数组为例子:

    var arr = [
        {id:1, pid: 0},
        {id:2, pid: 1},
        {id:3, pid: 2}
    ]

方法1: filter + map

    /**
       * @description 
       * @param {*} 接收两个参数,要转换的数组 / 根节点id
       * @public 
    */
    function arrToTree(arr, root){
        return arr.filter( o => o.id === root).map(i => ({...i, children: arrToTree(arr, i.id)}));
    }
    // 因为多个循环,算法复杂度在大数据量场景下,时间复杂度高
    // [{id:1,pid: 0, children:[{id:2,pid: 1, children:[{id:3,pid: 2, children:[]}]}]}]

方法2: for + {}映射

    // 只用一次for循环 + 对象映射,降低复杂度
    function arrToTree(arr, root){
       //存储结果
       let result = [];
       //保存数据
       let treeMap = {};
       
       for(let i of arr){
           let id = i.id;
           let pid = i.pid;
           
           //此判断主要是针对arr不是有序数组的情况下,会报错,因此判断
           if(!treeMap[id]){
               treeMap[id] = {
                   children: []
               }
           }
           
           treeMap[id]= {
               ...i,
               children: treeMap[id].children
           };
              
            //如果当前节点的pid是约定(传入)的根节点,则将当前节点 i 作为根节点
           if(pid === root){
               result.push(treeMap(id));
           }else {
               //此判断主要是针对arr不是有序数组的情况下,会报错,因此判断
               if(!treeMap[pid]){
                   treeMap[pid] = {
                       children: [];
                   }
               };
               
               //当前节点的id是它的子节点的pid; 或者说当前节点的pid对应它父节点的id;
               treeMap[pid].children.push(treeMap[id]);
           }
       }
       
       return result;
    }

JS面向对象

原型链和继承

在js中,一切皆对象; 但Js并不是一门真正的面向对象语言,因为它缺少类的概念;虽然ES6引入了 class和extends 的概念,但也是基于函数和原型链模拟的

原型链

    在js中:
    
    每个实例对象内部都有一个原型链指针`__proto__`,指向它的构造函数的原型对象;

    每个构造函数内部都有一个`prototype`原型对象;
    原型对象内部也有一个原型指针指向上一层的构造函数的原型对象;如此层层向上,直至Object的原型对象,构成了原型链;

    当在实例上查找一个属性时,会先从实例自身查起,如果自身存在此属性,则直接返回(属性遮蔽); 如果自身上不存在,则会沿着原型链层层查找,直至Object的原型对象,不存在则输出`undefined`
new操作符干了什么?
    function _new(Con, args){
       //创建一个继承构造函数原型的对象
       // 相当于: 1. let o = {}; 
                //2. o.__proto__ = Con.prototype /  Object.setPrototypeOf(o, Con.prototype)
        let o = Object.create(Con.prototype);
       //执行构造函数
       let result = Con.apply(o, args);
       //对返回结果进行判断
       return Object.prototype.toString.call(result) == '[object Object]' ? result : o;
    }

继承

一、原型链继承

    function Parent(){
        this.pName = 'PF'
    }
    
    Parent.prtottype.getName = function(){
        reuturn this.pName;
    }
    
    function Child(){}
    
    Child.prototype = new Parent();
    Child.prototype.constructor = Child;
缺点:1.所有的子类实例的原型对象都指向同一个父类示例,如果父类实例中有引用类型数据,则其中一个子类的改变,会引起所有的改变;
      2.无法向父类构造函数传参(super

二、构造函数继承

    function Parent(name){
        this.name = name;
    }
    
    Parent.prototype.getName = function(){
        return this.name;
    }
    
    function Child(args){
        Parent.call(this, ...args);
    }
    
    缺点:无法继承父类原型上的属性和方法;

三、组合继承

       function Parent (name){
            this.name = name;
       }
       Parent.prototype.getName = function(){
           reutnr this.name ;
       }
       
       function Child (args){
           Parent.call(this, ...args);
       }
       
       Child.prototype = new Parent();
       Child.prototype.constructor = Child;
       
       缺点:执行了两次构造函数(Parent.call() 和 new Parent()),因此子类实例和原型上会存在两份相同的父类实例的属性和方法;

四、寄生式组合继承

    function Parent(name){
        this.name = name;
    }
    Parent.prototype.getName = function(){
        return this.name;
    }
    
    function Child(args){
        Parent.call(this, ...args)
    }
    
    //浅拷贝父类原型对象
    Child.protoytype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;

寄生式组合继承,是目前最成熟的继承方式,babel对 ES6 extends的转化就是采用寄生式组合继承的方式;

V8引擎执行机制

一、V8如何解析执行一段代码?

image.png

  1. 预解析:检查语法错误,但不生成抽象语法树(AST)
  2. 生成AST:经过词法语法、分析,生成AST
  3. 生成字节码:基线编译器(Ignition)将AST转化为字节码
  4. 生成机器码: 优化编译器(Turbofan)将字节码转化成优化过的机器码;

V8的优化: 如果一段代码经常被执行,则V8就会将这段代码直接转化为机器码保存起来,以后的执行就不经过字节码,提升了执行速度;

二、引用计数和标记清除

引用计数:给一个变量赋值引用类型,则该对象的引用次数+1,如果此变量又被赋值为其他类型,则引用次数-1,垃圾回收器会回收引用次数为0的变量。注意:当变量被循环或相互引用时,无法被回收(闭包等)

标记清除(Mark-Sweep):垃圾回收器先给内存中所有的变量添加标记,然后从根节点开始遍历,去掉被引用和运行环境中变量的标记,剩下的就是需要回收的变量。

三、Chrome V8如何进行垃圾回收?

js语言中,对变量的存储主要有两个位置:堆内存栈内存中主要存储的是基本类型数据和引用类型的指针(内存地址),堆内存中主要存储的是引用类型数据

堆栈.png

栈内存回收:执行上下文栈在被切换后就被回收

堆内存回收:

自动垃圾回收有很多算法。

由于不同对象的生存周期不同,所以无法只用一种回收策略来解决问题,这样效率会很低。
所以,V8采用了一种代回收的策略,将内存分为两个生代:新生代(new generation)老生代(old generation)

  • 新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。
  • 对象最开始都会先被分配新生代(如果新生代内存空间不够,直接分配到老生代)
  • 默认情况下,32位系统新生代内存大小为16MB,老生代内存大小为700MB,64位系统下,新生代内存大小为32MB,老生代内存大小为1.4GB。

新生代回收策略:

新生代采用Scavenge垃圾回收算法,在算法实现时主要采用Cheney算法。

Cheney算法将内存一分为二,叫做semispace,每块内存大小8MB(32位)或16MB(64位)。一块处于使用状态称为From空间,一块处于闲置状态称为To空间

image.png

回收时,先扫描From,将非存活对象回收,将存活对象顺序复制到To中,然后清空form空间的全部内存,之后交换From和To空间,继续等待下一次回收

老生代回收策略:

晋升: 新生代中,当一个变量经过多次复制回收,仍然存在,那么就会被放进老生代内存中,采用新的算法管理

老生代中,存活对象占比较大,继续采用Scavenge算法不合适。
原因: 1.存活对象多,复制时效率变低 2.Scavenge算法会浪费一半内存,老生代堆内存远大于新生代,因此浪费会很严重。

所以,老生代采用 Mark-Sweep(标记清除)Mark-Compact(标记整理)相结合的方式;

**回收时,会先遍历内存中所有对象并打上标记,然后对正在使用或强引用的对象取消标记,回收被标记的对象**

内存碎片:Mark-Sweep在进行一次回收后,内存会出现不连续的状态,这种内存碎片会对后续的内存分配造成问题;

如果出现需要分配一个大内存的情况,由于剩余的碎片空间不足以完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。

Mark-Compact(标记整理):在标记完存活对象以后,会将存活对象向内存空间另一端移动,移动完成后,直接清理完边界以外的所有内存;

四、js相较于c++等强类型语言为什么慢?V8做了哪些优化?

  1. js的问题

    动态类型:导致每次存取属性或方法时,都需要先检查类型;此外动态类型也很难在编译阶段优化;
    属性存取:C++/java等语言中,方法属性是存储在数组中的,仅需数组位移就可以获取,而JS存储在对象中,每次获取需要进行哈希查询

  2. V8的优化
    即时编译(优化JIT):相较于C++/Java这类编译型语言,JS一边解释一边执行,效率低。V8对这个过程进行了优化:如果一段代码被执行多次,那么V8会把这段代码转化为机器码缓存下来,下次运行时直接使用机器码。
    隐藏类:对于C++这类语言来说,仅需几个指令就能通过偏移量获取变量信息,而JS需要进行字符串匹配,效率低,V8借用了类和偏移位置的思想,将对象划分成不同的组,即隐藏类;
    内嵌缓存:即缓存对象查询的结果。常规查询过程是:获取隐藏类地址 -> 根据属性名查找偏移值 -> 计算该属性地址,内嵌缓存就是对这一过程结果的缓存;
    垃圾回收管理:上文已介绍;