前端面试题js篇,持续更新

1.解释重绘与回流,以及如何优化?

重绘与回流:

  在页面加载时,浏览器会把获取到的html代码解析成dom树,dom树中包含html所有标签以及js动态生成的元素等。浏览器会把所有的样式(即css)解析为样式结构体。dom树和样式结构体组合后形成渲染树。简单来说就是dom树和样式结构体组合在一起形成render树。

重绘:

  当渲染树中的一些元素需要更改属性,而这些属性只涉及元素外观风格,而不会影响布局时,成为重绘

回流:

  当渲染树中的元素,因为规模尺寸、布局、隐藏等属性需要变动而需要重新构建时,则称为回流。每个页面至少有一次回流,就是在初始加载页面时,因为要构建渲染树。在回流时,浏览器会将渲染树中需要重新构建的部分失效,并重新构建。完成回流后,浏览器会重新绘制受影响的部分到页面中,该过程称为重绘。

如何减少重绘与回流?

css

1、尽量不去使用内联样式和表格布局。(在解析html时,内联样式会触发回流。而表格则至少需要一次精细的计算,来确定其单元格尺寸)

2、最小化css规则的数量,避免使用复杂的css选择器。

3、图片尽量设置尺寸

4、能用css就尽量少用js

dom

1、减少DOM深度以及数量(层级越深绘制时间越久)

2、更改dom树中的小类(即没有多层嵌套子元素的元素,这可以将重排的范围限制为必要的节点数)

3、批量操作DOM

4、事件代理

5、防抖和节流

这篇文章很不错,可直接翻译成中文:https://www.sitepoint.com/10-ways-minimize-reflows-improve-performance/

2.防抖和节流

防抖:

用户在高频触发一个事件时,设置一个时间段,在时间段内再次触发事件则从新计时,只执行时间段内的最后一次事件。

使用场景:
(1)搜索联想,用户在不断输入时,使用防抖节约请求资源;

(2)防止重复提交(按钮)

(3)scroll事件滚动触发

(4)窗口缩放resize

export const debounce = (fn, delay, immediate) => {
    let timer = null;
    let func = function() {
        let context = this;
        let args = arguments;

        if(timer) clearTimeout(timer);
        if(immediate) {
                let now = !timer;
                timer = setTimeout(()=>{
                        timer = null
                },delay)
                if(now) {
                        fn.apply(context,args)
                }
        } else {
                timer = setTimeout(()=>{
                        fn.apply(context,args)
                },delay)
        }
    }
    func.cancel = function() {
        clearTimeout(timer);
        timer = null;
    }
    return func;
}

节流:

在规定时间内,无论触发多少次函数,只会执行一次,稀释函数执行频率

使用场景:
(1)用户不断点击加载更多,每隔一段时间执行一次

(2)拖拽事件

//节流函数,一段时间内只执行第一次事件,且是第一次
export const throttle = (fn,delay) => {
    let last = 0;	//保存上次调用的时间
    return function() {
        let context = this;
        let params = arguments;

        let now = new Date().valueOf();
        if(now - last > delay) {
                fn.apply(context,params)
                last = now;
        }
    }
}

3.闭包

(1)概念:

能够读取其他函数内部变量的函数,称为闭包函数(使用作用域嵌套,将原有的局部变量转化为自由变量)。在js中变量的作用域属于函数作用域,在函数执行后,作用域会被销毁,内存也随之被收回。由于闭包函数可以读取外部函数的作用域,在函数执行完毕后作用域也不会被销毁。

(2)作用:

保存函数变量,不被销毁

不会污染全局环境,方便进行模块化开发

(3)使用场景:

想要在循环中打印每次的变量值

let arr = [1,2,3,4,5]
//这种方式打印出来,每次都是5
//onclick事件是异步触发的,当事件被触发时,for循环早已结束,此时变量i的值已经是5!
//故当onclick事件顺着作用域链从内向外查找变量i时,找到的值总是5。
for(var i=0;i<arr.length;i++){
    arr[i].onclick = function( ) {
        console.log(i)
    }
}
//方法1
for(var i=0;i<arr.length;i++){
    fuction fn(index) {
        arr[index].click = function(){
            console.log(index)
        }
    }
    fn(i)
}
//方法2
for(var i=0;i<arr.length;i++){
    (function(index){
        arr[index].onclick = function( ) {
            console.log(index)
        }
    })(i)
}
//方法3
for(var i=0;i<arr.length;i++){
    arr[i].onclick = (function(index ) {
        return function(){
            console.log(index)
        }
    })(i)
}

(4)注意事项

闭包会常驻内存,使用不当会造成内存泄漏

外部函数声明内部函数,内部函数能够使用外部函数的局部变量,这些变量不会被垃圾回收机制回收,会常驻内存。(闭包会将外部包含它的函数的活动对象也包含在自己的作用域中,会比一般的函数更占内存),因此在退出函数之前,将不再使用的局部变量删除。

4.undefined和null的区别

1.1 null是一个表示"无"的对象(空对象指针),转为数值时为0;undefined是一个表示"无"的原始值,转为数值时为NaN。

1.2Null常用来表示尚未存在的对象,一般出现在函数企图返回一个不存在的对象

1.3 Undefined表示缺少值,此处应该有值,但还未赋值。

a.变量声明了还未赋值

b.函数本应有参数,但没传

c.对象未赋值属性,该属性为undefined

d.函数没有返回值时,默认返回undefined

1.4 null表示没有对象,即该处不该有值

a.作为函数得参数,表示参数不是对象

b.作为原型链终点

5.作用域和作用域链?

  在js中,作用域分为全局作用域和局部作用域。全局作用域是指在代码任何地方都可以访问到,局部作用域比如函数作用域,只能在函数内部访问到。作用域最大的用处就是隔离变量,不同作用域的同名变量不会有冲突。

  一般情况下,变量取值会在创建这个变量的作用域内去查找,如果当前作用域未找到则会向上级作用域去查找,一直查找到全局作用域,这个查找过程形成的链条就是作用域链。如果查找到全局仍然没找到变量,则会报错(。。 is not defined)

6.原型和原型链?

为什么设计原型对象?

  在js中,可以通过new 一个构造函数生成实例对象。于是构造函数成了实例对象的原型对象,但是这种原型设计有一个缺点,就是无法共享属性。所以要设计一个对象来存储共有的属性。

什么是原型对象?

  要想让构造函数生成的实例对象能够共享属性,就要给构造函数添加一个属性prototype,这个属性指向构造函数的原型对象。我们将所有需要共享的属性和方法都放在原型对象上,不需要共享的就放在构造函数中。同时原型对象中会有一个constructor属性指回构造函数

  在js中,只要是对象就会有一个内置属性,即__proto__,这是对象刚创建就会形成的。这个属性指向原型对象。可以看下图有助于理解。

微信图片编辑_20210319143521.jpg
  总结一下,构造函数的prototype属性指向原型对象,实例对象的__proto__属性也指向原型对象,原型对象的constructor属性则指回构造函数。

  那么问题来了,原型也是对象,那么原型的__proto__也指向他的构造函数的原型。要想知道其构造函数的原型,就要知道是哪个构造函数创建了原型对象。

  所有js对象都继承了一个叫Object的对象,可以理解为Object这个构造函数创造了万物。但是Object构造函数的原型对象也有__proto__,而Object的构造函数原型对象是本身,所以其__proto__值为null。

什么是原型链?

  既然每个对象都有__proto__属性指向原型对象,那原型对象也有__proto__属性指向原型对象的原型对象,这么一个查找的过程便形成了一个原型链。

  补充知识:在 JS 中,所有的 function函数都是由Function继承来的,可以说是Function是所有 function的祖宗。那Function是由谁生产来的?Function函数有_proto_属性了,而且属性指向自己的原型对象,那不就是自己繁衍自己吗?可以这么理解。

最后总结:

(1).所有实例对象的__proto__都指向其构造函数的原型对象(prototype);

(2).所有函数(包括构造函数)都是Function的实例,所以所有函数的__proto__都指向Function的原型对象;

(3).所有原型对象(包括Function的原型对象)都是Object的实例,所以__proto__指向Object构造函数的原型对象,而Object构造函数的__proto__指向null;

(4).Function构造函数本身就是 Function 的实例,所以_proto_指向Function的原型对象;

7. js的继承

1)首先来说最基础的继承,就是构造函数的原型继承

  实例会继承构造函数的原型,这是最基础的继承。

    function Fn(n) {
        this.name = n //实例不共享
    }
    Fn.prototype.age = 18 //实例共享
    let child = new Fn()
    console.log(child.age) //18

2)多个构造函数之间的继承

  通过改变this指向来实现继承,简单方便,可以实现多继承。但是只能继承构造函数,不能继承原型。

    function Fn1(n) {
        this.name = n //实例不共享
        this.game = function() {
            console.log(this.name + '正在玩游戏') 
        }
    }

    function Fn2(n) {
        this.name = n //实例不共享
        this.study = function() {
            console.log(this.name + '正在读书') 
        }
    }

    function Fn(n) {
        // Fn.bind(this,n)()  //bind继承
        // Fn.call(this,n)  //call继承
        Fn1.apply(this,[n])  //apply继承,或者不用传参,用arguments
        Fn2.apply(this,[n])
    }

    let child1 = new Fn('函数Fn')
    child1.game(); //函数Fn正在玩游戏
    child1.study(); //函数Fn正在读书

3)原型继承(原型对象继承)

  直接让两根原型对象相等(注意需要用到深拷贝)。只能继承原型,不能继承构造函数。

    function Parent() {}
    function Child() {}
    Parent.prototype.show=function(){
        console.log('hello world')
    }
    //Child.prototype = Parent.prototype  //浅拷贝
    for (var i in Parent.prototype) {     //深拷贝
        Child.prototype[i] = Parent.prototype[i]
    }

    let hai = new Child()
    hai.show();  //hello world

4)原型链继承

  通过设置或者修改对象的原型链属性,是对象直接继承另一对象的实例。
相当于将构造函数的原型直接切换成另一个构造函数的实例,从而继承他的属性和方法。

  既能继承原型,又能继承构造函数,但是不方便传参。

    function Parent() {
        this.name = '父亲'
    }
    Parent.prototype.act = function() {
        console.log('我正在教育我的好大儿')
    }

    function Child() {}
    Child.prototype = new Parent()
    
    let hai = new Child()
    console.log(hai.name) //父亲
    hai.act(); //我正在教育我的好大儿

5)混合继承

  通过改变this指向实现构造函数的继承,通过原型对象继承来继承原型。也就是将上述2和3混合起来。

  既能继承原型,又能继承构造函数,方便简洁

    function Parent(name) {
        this.name = name
    }
    Parent.prototype.act = function() {
        console.log(this.name + '正在炒菜')
    }

    function Child(n) {
        Parent.bind(this,n)()
    }
    
    for(var i in Parent.prototype){
        Child.prototype[i] = Parent.prototype[i]
    }
    
    let hai = new Child('小孩')
    hai.act() //小孩正在炒菜
    console.log(hai)    //Child {name: "小孩"}

6)es6继承

  ES6继承原理是封装了改变this指向继承以及原型链继承

  简洁方便,但有些低版本浏览器不支持。

    class Parent{
        constructor(n){
            this.name = n
        }
        show(){
            console.log(this.name + '在执行show')
        }
    }

    class Child extends Parent{
        constructor(n){
            super(n)
        }
        show(){
            console.log('2222') 
        }
    }

    var p = new Parent('zhangsan')
    p.show()    //zhangsan在执行show
    console.log(p) //Parent {name: "zhangsan"}

    var c = new Child('lisi')
    c.show()    //2222
    console.log(c) //Child {name: "lisi"}

8.ES6箭头函数和普通函数有什么区别?

1). 箭头函数不是构造函数,不能使用new关键字生成实例。同时箭头函数也就没有原型属性。

2). 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

3). 箭头函数不能当作Generator函数,因此不能使用yield命令

4). 箭头函数的this指向上下文,普通函数this指向调用它的对象。(this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。)

9.深拷贝和浅拷贝,实现对象浅拷贝和深拷贝的方法有哪些?

浅拷贝:如果数据类型是基础数据类型,拷贝的就是基本类型的值。如果是引用数据类型,拷贝的是内存地址,所以其中一个值发生改变,另一个也会跟着改变。

深拷贝:深拷贝会拷贝所有属性,引用类型数据的拷贝会在堆中重新分配内存,与原有对象隔离,互不影响。(深拷贝相对于浅拷贝来说速度较慢,开销大)

浅拷贝

1) = 直接赋值

2)for in,被循环的对象存在嵌套对象时为浅拷贝

3)Object.assign或者三点运算符进行拷贝

拷贝的是对象的属性的引用,而不是对象本身。当数据只有一层的时候,是深拷贝。(相同的还有数组方法slice、concat,他们都为浅拷贝,当数据只有一层的时候,可实现深拷贝的效果)

深拷贝

1) JSON.parse(JSON.stringify())

tips:(1)会忽略undefined、symbol和函数

let obj = {
    name: 'A',
    name1: undefined,
    name3: function() {},
    name4:  Symbol('A')
}
let obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}

tips:(2)对象循环引用时,会报错

 let obj = {
      name1: 'A',
      name2: {
          name3: 'B'
      },
  }
  obj.name1 = obj.name2;
  obj.name2.name3 = obj.name1;
  let obj2 = JSON.parse(JSON.stringify(obj));
  console.log(obj2); // Converting circular structure to JSON

tips:(3)new Date,转换结果不正确
tips:(4)正则会被忽略

2)递归实现深拷贝

function deepClone(obj){
    let objClone = Array.isArray(obj)?[]:{};
    if(obj && typeof obj==="object"){
        for(key in obj){
            if(obj.hasOwnProperty(key)){
                //判断obj子元素是否为对象,如果是,递归复制
                if(obj[key]&&typeof obj[key] ==="object"){
                    objClone[key] = deepClone(obj[key]);
                }else{
                    //如果不是,简单复制
                    objClone[key] = obj[key];
                }
            }
        }
    }
    return objClone;
}    

3)$.extend()

通过 jQuery 的 extend() 方法实现深拷贝,第一个参数 true 为深拷贝,false 为浅拷贝

let obj = {
    name: '李四',
    age: 20,
    sex: null,
    tel: /^1[345789]\d{9}$/,
    address: undefined,
    flag: true,
    school: {
        name: '野鸡大学',
        grade: 2020,
        stuNo: 2020123456,
        colleges: '野鸡烧烤工程系',
    },
    say: () => {
        console.log('哇塞')
    }
}

var obj2 = $.extend(true, {}, obj); // true 为深拷贝,false 为浅拷贝

obj2.name = 'Rose';
obj2.school.name = '野鸡变凤凰';

console.log('obj', obj);
console.log('obj2', obj2);

推荐文章:https://juejin.cn/post/6906369563793817607

10. promise相关?

  promise是异步编程的一种解决方案,其实是一个构造函数,本身有resolve,reject,all方法。原型上有then,catch,finally方法

  1) 对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态,pending,resolve,reject。只有异步操作的结果可以决定当前处于哪一种状态。

  2) 状态一旦改变就不会再变。promise对象状态的改变只有两种可能。由pending->resolve,或者由pending->reject。在为其添加回调函数,得到的状态仍然不变。

  3) 原型中的then方法接受两个参数,这两个参数分别是两个函数。即resolve成功的回调函数,reject失败的回调函数。可以通过这两个函数分别拿到成功和失败的数据。

  4) 原型中的catch方法,是用来捕获异常的,其作用与then方法中reject的回调函数作用相似

  5) 补充:原型中的finally方法,是等待所有异步操作执行完后才会执行(不管异步执行结果)。 promise的all方法则是等到所有异步操作成功执行之后才会执行。

  建议:建议使用ES7的async/await进一步的优化Promise的写法,async函数始终返回一个Promise,await可以实现一个"等待"的功能,async/await被成为异步编程的终极解决方案,即用同步的形式书写异步代码,并且能够更优雅的实现异步代码顺序执行以及在发生异步的错误时提供更精准的错误信息。

小练习:

const promise = new Promise((resolve, reject) => {
  console.log(1)
  resolve()
  console.log(2)
})
promise.then(() => {
  console.log(3)
})
console.log(4)

1 2 4 3

Promise 构造函数是同步执行的,promise.then 中的函数是异步执行的。

11.reduce有什么用?

const arr = [‘Jack’,‘Petter’,‘Mark’]

arr.reduce((prev,current,index,arr)=>{},initValue)
prev:上一次回调的调用值,默认是initValue,不传initValue则默认数组第一项。

current:当前正在处理的数组项

index:当前正在处理的数组项的索引

arr:原数组

initValue:初始值

常用的参数只有 prev 和 current,initValue可能会用到。

reduce的用处

用处1:求数组所有项的和

const arr = [1,2,3,4,5]
arr.reduce((prev, current)=>{
    return prev + current
})
// 15

由于没有传initValue,prev的默认值为数组的第一项1,current为2,第一次执行prev+current后,prev的值变为3,current为3,以此类推,得到结果15。

用处2:数组去重

const arr = [1,1,2,2,3,3]
arr.reduce((prev,current)=>{
    !prev.includes(current) && prev.push(current)
    return prev
},[])
// [1,2,3]

我们给prev设置默认值 - 空数组[],current从数组第一项开始,如果current的值在prev中不存在,则往prev中push current,否则不做处理,以此类推,最终返回的就是去重之后的新数组[1,2,3].

用处3:求数组最大值

const arr = [1,2,3,4]
arr.reduce((prev,current)=>{
    return Math.max(prev,current)
})
// 4

prev默认值为1,current为2,用Math.max()函数将两数中较大的数赋给prev,接着用新的prev和下一个current值对比,以此类推,得到数组最大值为4。

用处4:扁平化数组

const arr = [[1,2],[3,4],[5,6]]
arr.reduce((prev, current)=>{
    return [...prev, ...current]
},[])
// [1, 2, 3, 4, 5, 6]

将二维数组转化为一维数组,现实中用的比较少。

用处5:根据指定属性,对对象数组中的对象进行分组

const arr = [{province:'山东',city:'青岛'},
             {province:'山东',city:'济南'},
             {province:'江苏',city:'南京'}]
let arr2 = arr.reduce((prev,cur) => {
    //保存当前的这一项的省份
    const curPro = cur['province'];
    if(!prev[curPro]) {
        //如果之前那个对象中没有该省份的属性,先将属性新增到对象,赋以空数组
        prev[curPro] = []
    }
    //将该项直接加入到相应的省份数组下
    prev[curPro].push(cur)
    return prev;
},{})
console.log(arr2)
    // {"山东":
    //         [{"province":"山东","city":"青岛"},
    //         {"province":"山东","city":"济南"}],
    //     "江苏":
    //     [{"province":"江苏","city":"南京"}]
    //   }

12.判断一个变量是否为数组,是否为对象的方法?

一、判断是否为数组

方法1:instanceof

instanceof操作符是检测对象的原型链是否指向构造函数的prototype对象的

let arr = [1, 2, 3]
console.log(arr instanceof Array)

//  true     返回true,说明变量arr是数组类型

方法2:constructor

利用构造函数来判断他的原型是否为Array, 用法: 变量.constructor === 变量类型

let arr = [1, 2, 3]
console.log(arr.constructor === Array)

//  true     返回true,说明变量arr是数组类型

方法3:isArray

Array对象的 isArray方法

let a = [2,543,32];

console.log(Array.isArray(a));//true

方法4:Object.prototype.toString.call()

跨原型链调用toString()方法

let a = [2,543,32];

console.log(Object.prototype.toString.call(a));// [object Array]

方法5:通过对象的原型方式来判断

Array对象的 isArray方法

let arr = [1, 2, 3]
console.log(arr.__proto__ === Array.prototype)

//  true     返回true,说明变量arr是数组类型

方法6:通过Object.getPrototypeOf()

Array对象的 isArray方法

let arr = [1, 2, 3]
console.log(Object.getPrototypeOf(arr) === Array.prototype)

//  true     返回true,说明变量arr是数组类型

方法7:通过isPrototypeOf() 方法来判断是否为数组类型

Array对象的 isArray方法

let arr = [1, 2, 3]
console.log(Array.prototype.isPrototypeOf(arr))

//  true     返回true,说明变量arr是数组类型

二、判断是否为对象

方法1:constructor

var arr = ['aa','bb','cc'];
var obj = {
'a': 'aa',
'b': 'bb',
'c': 'cc'
};
console.log(arr.constructor === Array); //true
console.log(arr.constructor === Object); //false
console.log(obj.constructor === Object); //true

方法2:Object.prototype.toString.call()

跨原型链调用toString()方法

let a = {a:'ss'}

console.log(Object.prototype.toString.call(a));// [object object]

13.判断一个变量是否为空数组或者空对象?

一、判断是否为空数组

方法1:isPrototypeOf

var obj = [];
Array.prototype.isPrototypeOf(obj); // true

// 满足以下判断表达式的都是 空数组 []
Array.prototype.isPrototypeOf(obj) && obj.length === 0

二、判断是否为空对象

方法1:constructor

var obj = {};
obj.constructor === Object; // true

// 满足以下判断表达式的都是 空对象
obj.constructor === Object && Object.keys(obj).length === 0

判断数值是否为空的函数封装

// 判断是否为空值。包括''、undefined、null、[]、{}
export const isEmpty = value => {
    if (typeof value === 'number') {
            return false;
    }
    if (!value) {
            return true;
    }
    if (Array.isArray(value) && !value.length) {
            return true;
    }
    if (value.constructor === Object && Object.keys(value).length === 0) {
            return true;
    }
    return false;
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值