Web前端面试题(上)

312 阅读15分钟

Web前端面试题之考官高频问点

1. js数据类型

  • 基本数据类型:Boolean、Number、String、undefined、Null、Symbol (ES6新增,表示独一无二的值)
  • 引用数据类型:Object、Array、Function

2. js数据类型判断

  • typeof :typeof返回一个表示数据类型的字符串,返回结果包括:Number、Boolean、String、Symbol、Object、undefined、Function等7种数据类型,但不能判断Null、Array等
  • instanceof :instanceof 是用来判断A是否为B的实例,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性,但它不能检测Null和 undefined
  • constructor :constructor作用和instanceof非常相似。但constructor检测 Object与instanceof不一样,还可以处理基本数据类型的检测。不过函数的 constructor 是不稳定的,这个主要体现在把类的原型进行重写,在重写的过程中很有可能出现把之前 的constructor给覆盖了,这样检测出来的结果就是不准确的。
  • Object.prototype.toString.call() :是最准确最常用的方式。

Object.prototype.toString.call();               // [object String]

Object.prototype.toString.call(1);              // [object Number]

Object.prototype.toString.call(true);           // [object Boolean]

Object.prototype.toString.call(undefined);      // [object Undefined]

Object.prototype.toString.call(null);           // [object Null]

Object.prototype.toString.call(new Function()); // [object Function]

Object.prototype.toString.call(new Date());     // [object Date]

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

Object.prototype.toString.call(new RegExp());   // [object RegExp]

Object.prototype.toString.call(new Error());    // [object Error]

3. call, apply, bind的区别,怎么实现call,apply方法?

在js中,每个函数的原型都指向Function.prototype对象(js基于原型链的继承)。因此,每个函数都会有apply,call,和bind方法,这些方法继承于Function。 它们的作用是一样的,都是用来改变函数中this的指向。

  1. 方法定义:
  • apply:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.apply(A, arguments);即A对象应用B对象的方法。
  • call:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.call(A, args1,args2);即A对象调用B对象的方法。
  1. call 与 apply 的相同点:
  • 方法的含义是一样的,即方法功能是一样的;
  • 第一个参数的作用是一样的;
  1. call 与 apply 的不同点:
  • 两者传入的列表形式不一样
  • call可以传入多个参数;
  • apply只能传入两个参数,所以其第二个参数往往是作为数组形式传入
  1. bind的传参方式和call一样,只不过它的不同之处是,apply和call方法调用之后会立即执行,而bind方法调用之后会返回一个新的函数,它并不会立即执行,需要我们手动执行。
  2. 存在的意义:
  • 实现(多重)继承

Function.prototype.myBind = function(content) {
    if(typeof this !='function'){
        throw Error('not a function')
    }
    let _this = this;
    let args = [...arguments].slice(1) 
    let resFn=function(){
        return _this.apply(this instanceof resFn?this:content,args.concat(...arguments))
    }
    return resFn
};
 
 /**
 * 每个函数都可以调用call方法,来改变当前这个函数执行的this关键字,并且支持传入参数
 */
Function.prototype.myCall=function(context=window){
      context.fn = this;//此处this是指调用myCall的function
      let args=[...arguments].slice(1);
      let result=content.fn(...args)
      //将this指向销毁
      delete context.fn;
      return result;
}
/**
 * apply函数传入的是this指向和参数数组
 */
Function.prototype.myApply = function(context=window) {
    context.fn = this;
    let result;
    if(arguments[1]){
        result=context.fn(...arguments[1])
    }else{
        result=context.fn()
    }
    //将this指向销毁
    delete context.fn;
    return result;
}

4. this指向问题

由于 JS 的设计原理: 在函数中,可以引用运行环境中的变量。因此就需要一个机制来让我们可以在函数体内部获取当前的运行环境,这便是this。因此要明白 this 指向,其实就是要搞清楚 函数的运行环境,说人话就是,谁调用了函数。 this的值是在执行的时候才能确认,定义的时候不能确认。 因为this是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。

  • 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window

  • 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象

  • 在构造函数模式中,类中(函数体中)出现的this.xxx=xxx中的this是当前类的一个实例

  • call、apply和bind:this 是第一个参数

  • 箭头函数this指向:箭头函数没有自己的this,看其外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window。 [箭头函数特点]

  • 没有构造函数,不能new实例对象

  • 本身没有this指向,this指向父级

  • 不能使用argumetns

  • 没有原型

// 箭头函数使表达更加简洁,隐式返回值
// 正常的写法
const isEven = function(n){
    return n % 2 === 0;
}
const square = function(n){
    return n * n;
}
const isEven = n => n % 2 === 0;
const square = n => n * n;
// 箭头函数的一个用处是简化回调函数
// 正常函数写法
[1,2,3].map(function (x) {
    return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x); //[1,4,9]

5. 原型 / 构造函数 / 实例

  • 原型(prototype): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 Firefox 和 Chrome 中,每个JavaScript对象中都包含一个__proto__ (非标准)的属性指向它爹(该对象的原型),可obj.__proto__进行访问。
  • 构造函数: 可以通过new来 新建一个对象 的函数。
  • 实例: 通过构造函数和new创建出来的对象,便是实例。 实例通过__proto__指向原型,通过constructor指向构造函数。

6. 原型链

概念:原型链是由原型对象组成,每个对象都有 proto 属性,指向了创建该对象的构造函数的原型,proto 将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链。

  • 属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototype,如还是没找到,则输出undefined;
  • 属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。

7. 继承(原型链继承)

概念:在 JS 中,继承通常指的便是 原型链继承,也就是通过指定原型,并可以通过原型链继承原型上的属性或者方法。原型链是实现继承最原始的模式,即通过prototype属性实现继承。将父类的实例作为子类的原型。常用的有6种继承方式:

  • 原型链继承
  • 借用构造函数继承
  • 组合继承(组合原型链继承和借用构造函数继承)(常用)
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承(常用)

8. stack 栈

    // 先进后出的数据结构
    // js中没有这种数据结构,但是我们可以通过数组去模拟。
    /**
     * 1、定义一个函数 传入字符串s
     * 2、s字符串取余数,当他等于1的时候就是个奇数,因为我们的对称的括号首要条件就是双数,所以直接返回false。
     * 3、定义一个stack存储左边的括号
     * 4、循环s字符串,做一个判断,当字符串为'(''{''[',就push到stack的栈里面。
     * 5else,如果循环中不等于'(''{''[',都会到 else 里面去,加入我传了一个"()[]{}"stack:['(','['.'{']
     * 6、倒着取stack:['(','['.'{']里面的值, 第一个输出的是'{',第二个输出的是 '[' ,第二个输出的是 '(' 
     * 7else 里面的c ) , ] ,}
     * 8、stack.pop(),从后开始删
    */

9. 作用域与作用域链

  • 作用域: 执行上下文中还包含作用域链。作用域其实可理解为该上下文中声明的变量和声明的作用范围。可分为 块级作用域 和 函数作用域( 也可理解为:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作 用域下同名变量不会有冲突。)
  • 作用域链:作用域链可以理解为一组对象列表,包含 父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数。我们知道,我们可以在执行上下文中访问到父级甚至全局的变量,这便是作用域链的功劳。

10. 闭包

概念:定义在函数内部的函数。里面的函数可以访问外面函数的变量,外面的变量的是这个内部函数的一部分。(其他说法:闭包属于一种特殊的作用域,称为 静态作用域。它的定义可以理解为: 父函数被销毁 的情况下,返回出的子函数的[[scope]]中仍然保留着> 父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。)

  • 作用:1.使用闭包可以访问函数中的变量。2.可以使变量长期保存在内存中,生命周期比较长。
  • 缺点:闭包不能滥用,否则会导致内存泄露,影响网页的性能。
  • 应用场景:1.函数作为参数传递。2.函数作为返回值

/**
*JavaScript中的函数会形成了闭包。
*闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。
**/

function foo(){
  var num = 1
  return compute(){
    return num+=1
  }
}

var fn = foo()
console.log(fn())

// num 变量和 compute 函数就组成了一个闭包


11. 浅拷贝与深拷贝

浅拷贝:

a. 概念:浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。修改时原对象也会受到影响。

b. 方法:

  • 利用 = 赋值操作符实现浅拷贝。
  • 数组的浅拷贝一般使用 slice、concat。
  • 数组浅拷贝 - 遍历 。
  • 对象浅拷贝 - Object.assign()。
  • 对象浅拷贝 - 扩展运算符
//要拷贝的对象
const obj0 = {
    x: 1,
    y: {
        name: "bawei",
    },
    z: [1, 2, 3, 4, 5]
};

// 1 for...in
let simple = {};
for (const key in obj0) {
    if (obj0.hasOwnProperty(key)) {
        simple[key] = obj0[key]
    }
}
console.log(simple);
// 2 Object.assign
let newObj = Object.assign({}, obj0);
console.log(newObj);
// 3. 剩余参数语法
let resObj = {...obj0 }
console.log(resObj);

深拷贝:

a. 概念:深拷贝就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份。简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系。修改时原对象不再受到任何影响。

b. 方法:

  • 利用 JSON 对象中的 parse 和 stringify。

JSON.parse(JSON.stringify(obj))

深拷贝弊端:

  • 时间会变成字符串形式,而不是时间对象。

  • 如果obj里面有function,undefined,则序列化的结果会把function,undefined丢失

  • 如果obj里面有NaN,Infinity和-Infinity,则序列化的结果会变成null

  • JSON.stringify()只能序列化对象的可枚举的自有属性

  • 如果obj里有RegExp、Error对象,则序列化的结果只能得到空对象

  • 利用递归来实现每一层都重新创建对象并赋值。

let obj1 = {
    a: {
        c: /a/,
        d: undefined,
        b: null,
    },
    b: function() {
        console.log(this.a);
    },
    c: [{
            a: "c",
            b: /b/,
            c: undefined,
        },
        "a",
        3,
    ],
    d: new Date(),
};

function deepCopy(val) {
    //判断是什么数据类型
    if (
        typeof val !== "object" ||
        val === null ||
        val === undefined ||
        val instanceof RegExp ||
        val instanceof Function ||
        val instanceof Date
    ) {
        return val
    }
    //定义空对象检测是不是数组 如果不是则改为数组
    //这里用三目运算符写的话是这样的  let temp = val instanceof Array ? '[]' : '{}'
    let temp = {};
    if (val instanceof Array) {
        temp = [];
    }

    for (const key in val) {
        temp[key] = deepCopy(val[key]);
    }
    return temp
}
console.log(obj1);
//到这才算是拷贝结束 为了验证拷贝的对与否 我们可以试一下
var resultObj = deepCopy(obj1)
//这个是修改值
resultObj.a = "aaa"
console.log(resultObj);

12. 防抖与节流

防抖与节流函数是一种最常用的 高频触发优化方式,能对性能有较大的帮助。详情可参考loadsh官网

  • 防抖 (debounce): 将多次高频操作优化为只在最后一次执行,通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可。

/**
 * 防抖 (debounce)将多次高频操作优化为只在最后一次执行
 * 
 * @param {Function} fn 需要防抖函数
 * @param {Number} delay  需要延迟的毫秒数
 * @return {Function}
 * 
 */
    function debounce(fn, delay = 1000) {
        let timer;

        return function (...args) {
          // 如果在定时时间内再次触发事件, 则清空定时器,重新计时
          if (timer) clearTimeout(timer);
          
          // 事件不再触发
          timer = setTimeout(() => {
            fn.apply(this, args);
          }, delay);
        };
      }

  • 节流(throttle): 每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms执行一次即可。

/**
 * 节流(throttle)将高频操作优化成低频操作,每隔 100~500 ms执行一次即可
 * 
 * @param {Function} fn 需要防抖函数
 * @param {Number}  timer  需要延迟的毫秒数
 * @return {Function}
 * 
 */
      // 节流:在固定时间内,函数只能触发一次
      // 就是指连续触发事件但是在 n 秒中只执行一次函数。**节流会稀释函数的执行频率。
      function throttle(fn, timer = 3000) {
        // 定义上一次时间戳
        let prev = 0;
        return function (...args) {
          // 延展参数语法
          // 定义下一次时间戳
          let now = new Date();
          if (now - prev > timer) {
            // 时间差大于一秒才触发函数(事件回调函数)
            fn.apply(this, args);
            // 更新时间戳
            prev = now;
          }
        };
      }

13. Array 常见操作方法

  • map: 遍历数组,返回回调返回值组成的新数组
  • forEach: 无法break,可以用try/catch中throw new Error来停止
  • filter: 过滤
  • some: 有一项返回true,则整体为true
  • every: 有一项返回false,则整体为false
  • join: 通过指定连接符生成字符串
  • push / pop: 末尾推入和弹出,改变原数组, 返回推入/弹出项
  • unshift / shift: 头部推入和弹出,改变原数组,返回操作项
  • sort(fn) / reverse: 排序与反转,改变原数组
  • concat: 连接数组,不影响原数组, 浅拷贝
  • slice(start, end): 返回截断后的新数组,不改变原数组
  • splice(start, number, value...): 返回删除元素组成的数组,value 为插入项,改变原数组
  • indexOf / lastIndexOf(value, fromIndex): 查找数组项,返回对应的下标
  • reduce / reduceRight(fn(prev, cur), defaultPrev): 两两执行,prev 为上次化简函数的return值,cur 为当前值(从第二项开始)
  • flat: 数组拆解, flat: [1,[2,3]] --> [1, 2, 3]

14. Object 常见操作方法

  • Object.keys(obj): 获取对象的可遍历属性(键)
  • Object.values(obj): 获取对象的可遍历属性值(值)
  • Object.entries(obj): 获取对象的可遍历键值对
  • Object.assign(targetObject,...object): 合并对象可遍历属性
  • Object.is(value1,value2): 判断两个值是否是相同的值

详细参考,请点击 js对象方法大全

15. 常见状态码

  • 1xx: 接受,继续处理
  • 200: 成功,并返回数据
  • 201: 已创建
  • 202: 已接受
  • 203: 成为,但未授权
  • 204: 成功,无内容
  • 205: 成功,重置内容
  • 206: 成功,部分内容
  • 301: 永久移动,重定向
  • 302: 临时移动,可使用原有URI
  • 304: 资源未修改,可使用缓存
  • 305: 需代理访问
  • 400: 请求语法错误
  • 401: 要求身份认证
  • 403: 拒绝请求
  • 404: 资源不存在
  • 500: 服务器错误

16. Promise

概念: Promise 是异步编程的一种解决方案,比传统的异步解决方案【回调函数】和【事件】更合理、更强大。现已被 ES6 纳入进规范中。

Promise 的常用 API 如下:

  • Promise.resolve(value) : 类方法,该方法返回一个以 value 值解析后的 Promise 对象
  • Promise.reject : 类方法,且与 resolve 唯一的不同是,返回的 promise 对象的状态为 rejected。
  • Promise.prototype.then : 实例方法,为 Promise 注册回调函数,函数形式:fn(vlaue){},value 是上一个任务的返回结果,then 中的函数一定要 return 一个结果或者一个新的 Promise 对象,才可以让之后的then 回调接收。
  • Promise.prototype.catch : 实例方法,捕获异常,函数形式:fn(err){}, err 是 catch 注册 之前的回调抛出的异常信息。
  • Promise.race :类方法,多个 Promise 任务同时执行,返回最先执行结束的 Promise 任务的结果,不管这个 Promise 结果是成功还是失败。
  • Promise.all : 类方法,多个 Promise 任务同时执行,如果全部成功执行,则以数组的方式返回所有 Promise 任务的执行结果。 如果有一个 Promise 任务 rejected,则只返回 rejected 任务的结果。

//基本用法
const promise = new Promise((resolve, reject) => {
    try{
       resolve('success')
    }catch(err){
        reject('error')
    }
  
})
//联用
promise
  .then((res) => {
    console.log('then: ', res)
  })
  .catch((err) => {
    console.log('catch: ', err)
  })
  
//all
//返回多个promise的结果,形成一个数组
let p1 = new Promise ((resolve,reject)=>{
setTimeout(()=>{
        resolve(1)
    },1500);
})
let p2 = new Promise ((resolve,reject)=>{
        setTimeout(()=>{
        resolve(2)
    },0);
})

Promise.all([p1,p2]).then(res=>{
    console.log(res)  // 2,1
})
//race
//返回多个promise中最快的那一个,其他不返回
let p1 = new Promise ((resolve,reject)=>{
        setTimeout(()=>{
        resolve(1)
    },1500);
})
let p2 = new Promise ((resolve,reject)=>{
setTimeout(()=>{
        resolve(2)
    },0);
})
Promise.race([p1,p2]).then(res=>{
    console.log(res)  //2
})

17. 内存泄露

  • 意外的全局变量: 无法被回收
  • 定时器: 未被正确关闭,导致所引用的外部变量无法被释放
  • 事件监听: 没有正确销毁 (低版本浏览器可能出现)
  • 闭包: 会导致父级中的变量无法被释放
  • dom 引用: dom 元素被删除时,内存中的引用未被正确清空

结语

以上皆为个人查资料和参考其他博客总结