前端JS面试题

1.JS数据类型

基本数据类型:null,undefined,Number,String,Boolean,Symbol,BigInt

引用数据类型:Object,Array,Function,Date,RegExp等等

2.堆和栈的存储机制有什么区别

堆:一种非连续的树形存储数据结构,每个节点有一个值,整棵数是经过排序的。特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。常用来实现优先队列,存取随意。

栈:一种连续存储的数据结构,具有先进后出的特性。

通常的操作有入栈,出栈。想要读取战中的某个元素,就是将其之间的所有元素出栈才能完成。

3.什么是事件

事件是文档和浏览器窗口中发生的特定的交互瞬间,事件就发生啦。

一是直接在标签内直接添加执行语句,二是定义执行函数。

DOM事件分为两种类型:事件捕获、事件冒泡。

事件捕获:网景公司提出的事件流叫事件捕获。由外向内,从事件发生的顶点开始,逐级往下查找,一直找到目标元素。

事件冒泡:IE提出的事件流叫事件冒泡。由内向外,从具体的目标节点元素触发,逐级向上传递,直到根节点。

事件流:页面接受事件的先后顺序就形成了事件流。

4.什么是事件委托

事件委托,又名事件代理,事件委托就是利用事件冒泡,就是把子元素的事件绑定到父元素上,如果子元素阻止了时间冒泡,那么委托也就无法实现啦。

阻止事件冒泡

event.stopPropagation()

好处:提高性能,减少了事件绑定,从而减少内存占用。

  • 111
  • 222
  • 333
  • 444

5.防抖和节流

防抖:指触发事件后在n秒内函数只能执行一次,如果在n秒内又触发了事件,则会重新计算函数执行时间。

节流:所谓节流,指连续触发事件但在n秒中只执行一次函数

点我执行防抖函数
点我执行节流函数

6.什么是深拷贝,浅拷贝,浅拷贝和赋值的区别,如何实现

深拷贝和浅拷贝是针对复杂数据类型来说的,浅拷贝只拷贝一层,而深拷贝是层层拷贝。

1.浅拷贝:

将原对象或原数组的引用直接赋给新对象,新数组,新对象只是对原对象的一个引用,而不是复制对象本身,新旧对象还是共享一块内存

如果属性是一个基本数据类型,拷贝就是基本类型的值看,如果属性是引用类型,拷贝的就是内存地址。

2.深拷贝:

创建一个新的对象和数组,将原对象的各项属性的"值"拷贝过来,是“值”而不是“引用”

深拷贝就是把一个对象,从内存中完整的拷贝出来,从堆内存中开辟了新区域,用来存新对象,并且修改新对象不会影响原对象。

3.赋值

当我们把一个对象赋值给一个新的变量时,赋的是该对象在栈中的内存地址,而不是堆中的数据。

/**
         * 浅拷贝的实现方式:
         * 1.Object.assign()
         * 2.lodash里面的_.clone
         * 3. ...扩展运算符
         * 4.Array.prototype.concat
         * 5.Array.prototype.slice
         **/

         /**
          * 深拷贝的实现方式
          * 1.JSON.parse(JSON.stringify)
          * 2.递归操作
          * 3.cloneDeep
          * 4.Jquery.extend()
          **/
// 深拷贝:
        function checkType(target){
            return Object.prototype.toString.call(target).slice(8,-1);
        }
        function deepClone(data){
            const obj = checkType(data) === 'Array' ? [] : {};
            if (['Object','Array'].includes(checkType(data))) {
                for (const key in data) {
                    const value = data[key];
                    // 如果拷贝的是简单类型的值直接进行赋值
                    if(!["Object","Array"].includes(checkType(value))){
                        obj[key] = value;
                    }else {
                        // 定义一个映射,初始化的时候将data本身加入映射中
                        const map = new WeakMap();
                        // 如果拷贝的是复杂数据类型第一次拷贝后存入map
                        // 第二次再次遇到该值时直接复制为null,结束递归
                        map.set(data,true);
                        if (map.has(value)) 
                        obj[key] = null;
                        else {
                            map.set(value,true);
                            obj[key] = deepClone(value);
                        }
                    }
                }
            } else {
                return data
            }
            

            return obj
        }
        

7.改变数组本身的api

(1)pop() 尾部弹出一个元素

(2)push() 尾部插入一个元素

(3)shift() 头部弹出一个元素

(4)unshift() 头部插入一个元素

(5) sort([func]) 对数组进行排序,func有两个参数,其返回值小于0,那么参数1被排列到参数2之前,反之参数2排在参数1之前

(6)reverse() 原位反转数组中的元素

(7)splice(pos,deleteCount,...item) 返回修改后的数组,从pos开始删除deleteCount个元素,并在当且位置插入items

(8)copyWithin(target, start, end),复制数组从start到end(不包括end)的元素,到数组target的位置上,返回改变后的数组

(9)arr.fill(value,start,end) 从start到end(默认到数组的最后一个位置),不包括end,填充value,返回填充后的数组.

8.数组去重

// 普通数组
        let arr = [1,2,3,7,7,5,2];
        // 1. 使用ES6中的new Set是最简单的去重方法
        console.log([...new Set(arr)]);

        // 2. new Set对对象数组去重
        const list = [
            {name:'张三',age:18,address:"北京"},
            {name:'李四',age:20,address:"天津"},
            {name:'王五',age:22,address:"河北"},
            {name:'张三',age:18,address:"北京"},
            {name:'李四',age:20,address:"天津"},
        ];
        const str = list.map((item) => JSON.stringify(item));
        const removeDupList = [...new Set(str)];
        const result = removeDupList.map((item) => JSON.parse(item));
        console.log("result:",result);

        // 3.new Set对字符串进行去重
        let str1 = "352255";
        let unique = [...new Set(str1)].join("");

9.数组扁平

// reduce 和 concat 实现 遍历数组每一项,若值为数组则递归遍历,否则直接concat
        function myFlat(arr) {
            return arr.reduce((result,item)=> {
                return result.concat(Array.isArray(item) ? myFlat(item):item)
            },[])
        }
        console.log(myFlat([12,14,[125,58,[1,2]]]));

10.数组排序

 // sort排序
        console.log([1,3,4,2].sort((a,b) => a-b));
        // 优化冒泡 解构赋值
        function insert(arr) {
            // 外层循环控制的是比较的轮数,你要循环几轮
            for (let i=0; i arr[j+1]) {
                        [arr[j],arr[j+1]] = [arr[j+1],arr[j]]
                    }
                }
            }
            return arr;
        }
        console.log(insert([1,3,4,2]));

        //快速排序主要是利用递归来实现
        function quick(arr) {
            if (arr.length <=1) return arr;
            // 1.找到数组中的中间项然后把他取出来,用中间项进行对比,小的放左边,大的放右边
            // Math.floor(arr/2) 数组的长度除以二,并且向下取整 得出中间项
            let f = Math.floor(arr.length / 2);
            // 得出中间的那个数字, splice返回删除后的数组 所以要加上一个[0]
            let cent = arr.splice(f,1)[0];
            // 2.准备两个数组,循环剩下数组每一项,比中间项小的放到左边,大的放到右边
            let arrLeft = [];
            let arrRight = [];
            for (let i=0;i

11.谈谈你对call、apply、bind的理解

前端面试题:谈谈你对bind、call、apply理解? - 简书

12.arguments对象是什么

arguments当我们不知道有多少个参数传进来的时候就用arguments来接收,是一个类似于数组的对象,他有length属性,可以arguments[i]来访问对象中的元素,但是它不能使用数组的一些方法。例如push、pop、slice等。arguments虽然不是一个数组,但是它可以转成一个真正的数组。取之可以用展开运算符来使用。

13.柯里化函数

柯里化函数:把一个多参数的函数转化为单参数函数的方法,并且返回接受余下的参数而返回结果的新函数的技术。

// 柯里化之前
        function add(x,y) {
            return x+y;
        }
        // 柯里化之后
        function add2(x){
            return function(y){
                return x+y
            }
        }

14.new的原理

/**
         * new实际上是在堆内存中开辟一个空间
         * (1)创建一个空对象,构造函数中的this指向这个空对象
         * (2)这个新对象被执行[ [原型] ]连接
         * (3)执行构造函数方法,属性和方法被添加到this引用的对象中
         * (4)如果构造函数中没有返回其他对象,则返回this,即创建的这个新对象,否则,返回构造函数中返回的对象
         **/
        function _new(){
            let target = {}; //创建的新对象
            let [constructor,...args] = [...arguments];
            // 执行[[原型]]链接,target是constructor的实例
            target.__proto__ = constructor.prototype;
            // 执行构造函数,将属性和方法添加到创建的空对象上
            let result = constructor.apply(target,args);
            if(result && (typeof (result) === 'object' || typeof(result) === 'function')){
                // 如果构造函数执行的结构返回的是一个对象,那么返回这个对象
                return result;
            }
            // 如果构造函数返回的不是一个对象,返回创建的对象
            return target;
        }

        // 构造器函数
        let Parent = function (name,age) {
            this.name = name;
            this.age = age;
        };
        Parent.prototype.sayName = function () {
            console.log(this.name);
        }
        // 创建实例,将构造函数Parent与形参作为参数传入
        const child = _new(Parent,'echo',26);
        console.log("child:",child);
        child.sayName();

15.谈谈你对JS eventloop(事件循环)的理解

浏览器内核是多线程,JavaScript是单线程;

JS单线程详解:因为JS是面向客户端的一门语言,主要是用户交互,操作dom,渲染数据。试想一下:如果是多线程,我们在一个线程删除了一个dom节点,另外一个线程添加了一个dom节点,以哪个线程为主呐,就会出现混乱的情况。当然你可以说我们在操作一个dom之后加上锁,只允许一个线程操作,这样其实增加了程序的复杂度,并不是一个好办法。

单线程产生的问题:必须要等待前一个程序执行完毕才执行下一个,所以将程序分为两类:同步任务和异步任务。

异步任务又可以分为宏任务和微任务。

栈:先进后出的数据结构,存储基本数据类型的变量。

堆:主要负责引用数据类型的存储。

任务队列:为什么会有任务队列呢,还是因为JS单线程的原因,单线程,就意味着一个任务一个任务的执行,执行完当前任务,执行下一个任务,这样也会遇到一个问题,就比如说,要想服务端通信,加载大量数据,如果是同步执行,js主线程就得等着这个通信完成,然后才能渲染数据,为了高效率的利用cpu,就有了同步任务和异步任务之分。

同步任务:指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

异步任务:指的是不进入主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

微任务:

promise的回调、node中的process.nextTick、对Dom变化监听的MutationObserver。

宏任务:

script脚本的执行、setTimeout,setInterval,setImmediate一类的定时事件,还有如I/O操作,UI渲染等。

16.如何让(a ==1 && a==2 && a==3)的值为true?

// == 操作符在左右数据不一致的i,会先进行隐式转换,意味着a不是基本数据类型
        
        // 方法一:数组的toString会默认调用数组的join方法,可以重定义join方法
        let a = [1,2,3];
        a.join = a.shift;
        console.log(a==1 && a==2 && a==3);

        // var b = {};
        // console.log("window.b:",window.b);
        // 方法二:利用definedProperty进行数据劫持
        let num =1;
        Object.defineProperty(window,'b',{
            num:1,
            get:function(){
                console.log("get");
                return num++
            },
            valueOf:function(){
                console.log(
                    "valueOf"
                );
                return num++
            },
            toString:function(){
                console.log("toString");
                return  num
            }
        })
        console.log(b==1);
         // 方法三:利用Proxy进行数据劫持(Proxy/Object.definedProperty)
        let c = new Proxy({},{
            i:1,
            get:function(){
                return () => this.i++
            }
        })
        console.log(c==1&&c==2&&c==3);
            // 改写对象的valueOf或者toString方法
            //JavaScript中当遇到不同类型的值进行比较时,会根据类型转换规则试图将它们转为同一个类型再比较。比如 Object 类型与 Number 类型进行比较时,Object 类型会转换为 Number 类型。转换为时会尝试调用 Object.valueOf 和 Object.toString 来获取对应的数字基本类型。
            // 先valueOf再toString
        let d = {
            num:1,
            valueOf:function(){
                console.log('valueOf');
                return  d.num
            },
            toString:function(){
                console.log("toString");
                return  d.num
            }
        }
        console.log(d==1);

17.为什么0.1+0.2!==0.3,如何让其相等

let n1 = 0.1;
        let n2 = 0.2;
        console.log(n1+n2);

        /**
         * 使用浮点数进行计算逻辑处理时,不注意就可能出现问题
         * 记住,用云不要直接比较两个浮点数的大小
         * 这个属于数字运算中的精度确实问题
         * 在0.1+0.2这个式子中,0.1和0.2都是近似表示的,在他们相加的时候,两个近似值进行了计算,导致问题的出现
         **/
        // 简单粗暴的方式
        parseFloat((0.1+0.2).toFixed(10)) === 0.3

18.require与import的区别和使用

(1)import是ES6中的语法标准也是用来加载模块文件的,import函数可以读取并执行一个JS文件,然后返回该模块的exporrt命令指定输出的代码,export与export default均可用于导出敞亮,函数,文件,模块,export可以有多个,erxport default只能有一个。

(2)require定义模块:module变量戴白哦当前模块,它的exports属性是对外的接口。通过exports可以将模块从模块中导出,其他文件加载模块实际上就是读取module.exports变量,他们可以是变量,函数,对象等。在node中如果用exports进行导出的话系统会帮你自动转成module.exports,知识导出需要定义导出名。

require与import的区别

(1)require是CommoJS规范的模块化语法,import是ECMAScript 6规范的模块化语法:

(2)require时运行时加载,import时编译时加载;

(3)require可以写在代码的任意位置,import只能写在文件的最顶端且不可再条件语句或函数作用域中使用;

(4)require通过module.exports导出的值就不能再变化,import通过export导出的值可以改变;

19.for...in迭代和for...of有什么区别

(1)推荐在循环对象属性的时候,使用for...in,在遍历数组的时候使用for...of.

(2)for in遍历的是数组的索引,而for...of遍历的是数组的元素值

(3)for...of不能循环普通的对象,需要通过和Object.keys搭配使用

(4)for...in遍历顺序以数字为先

(5)从遍历对象的角度来说,for...in会便利出来的为对象的key,但for...of会直接报错

20.谈谈你对模块化开发的理解

我对模块的理解是,一个模块是实现一个特定功能的一组方法。在最开始的时候,js只是实现一些简单的功能,所以并没有模块的概念,但随着程序越来越复杂,代码的模块化开发变得越来越重要。

由于函数具有毒理作用与的特点,最原始的写法是使用函数来作为模块,几个函数作为一个模块,但是这种方式容易造成全局变量的污染,并且模块之间没有联系。

后面提出了对象写法,通过将函数作为一个对象的方法来实现,这样解决了直接使用函数来作为模块的一些缺点,但是这种办法会暴所有的模块成员,外部代码可以修改内部属性的值。

现在最常用的是立即执行函数的写法,通过利用闭包来实现模块私有作用域的建立,同事不会对全局作用域造成污染。

21.js中的几种模块规范

第一种是CommonJS方案,它通过require来引入模块,通过module.exports定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。

第二种是AMD方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有用来这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js实现了AMD规范。

第三种是CMD方案,这种方案和AMD方案都是为了解决异步模块加载的问题,sea.js实现了CMD规范。它和require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。

第四种方案是ES6提出的方案,使用import和export的形式来导入导出模块。

22.qiankun的css沙箱隔离是怎样实现的

通过给子应用所有的样式选择器前面添加由子应用名称组成的前缀

如下:

// 假设应用名是 react16
.app-main {
  font-size: 14px;
}
div[data-qiankun-react16] .app-main {
  font-size: 14px;
}

你可能感兴趣的:(javascript)