前端高频面试题 —— JS 篇

319 阅读11分钟

1. 原型&原型链

所有引用类型的隐式原型都指向它的构造函数的显式原型:obj._ proto _ === Object.prototype。 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,会去它的 _ proto _ (即其构造函数的 prototype )中寻找,这就形成了原型链。


2. 继承

//父类
function Person () {
  this.name = 'a';
  this.message = function() {
    console.log(this.name);
  }
  function sayHello() {
    console.log('hello')
  }
}

//一、原型链继承
function Student() {}
Student.prototype = new Person();
let lily = new Student();
lily.message()
console.log(lily) //{}
console.log(lily.name)	//a 

//二、构造函数继承
function Teacher() {
  Person.call(this)
}
let lucy = new Teacher();
lily.message()
lily.sayHello()
console.log(lucy) //{"name":"a"} 
console.log(lucy.name)	//a 

//三、es6 class extends继承
class Gril extends Person{
    constructor(age){
        super();
        this.age=age;
    }
    getAge(){
        console.log(this.age)
    }
}
let coco = new Gril(12)
coco.getAge()
console.log(coco) //{"name":"a","age":12} 
console.log(coco.name)	//a 

参考:www.cnblogs.com/ranyonsue/p…

class 与 function 的区别

  • class 是一种特殊的 function
  • function 声明会提升,重复定义会覆盖,而 class 声明不会提升,重复定义会报错
  • function 可以用 call、apply、bind 来改变执行上下文,而 class 不可以

3. 构造函数 new

3.1 new 构造函数的过程

  • 创建一个新对象

  • 将构造函数的作用域赋给新对象

  • 执行构造函数重的代码

  • 返回新对象

3.2 用原生 js 实现一个 new

function new () {
  let obj = Object.create(Person.prototype)
  Person.call(obj, ...arguments)
  return obj
}

4. 判断数据类型

typeof: 返回类型对应字符串,但引用类型只能区分出 'function' 和 'object',无法区分出 'array'、'date'等;

instanceof: 只能用来判断引用类型,Object、Function、Date、Array等;

constructor: 不能用来判断 Symbol、Null 及 Undefined 类型;

Object.prototype.toString: 返回[object Undefined] 等,比较好用。

详见:juejin.cn/post/1#head…


5. 闭包

概念:

声明在一个函数中的函数,是闭包函数,他可以访问其所在外部函数中的参数和变量,这就是闭包。

应用场景:

  • 给 setTimeout 第一个参数传参
  • 事件绑定
  • 实现防抖节流
  • 实现单例模式

缺点:

  • 可以在父函数外部改变父函数内部变量的值,也就是有副作用,要慎重
  • IE 下可能会导致内存泄漏,要记得手动回收,删除不用的变量

参考:

zhuanlan.zhihu.com/p/22486908

blog.csdn.net/weixin_4358…


6. JS 内存泄漏

内存泄漏(Memory Leak) 是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

  • 全局变量:
  • 闭包:
  • 被遗忘的定时器或回调:

如何避免内存泄漏

  • 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收,严格模式可以避免创建意外全局变量;
  • 注意程序逻辑,避免“死循环”之类的 ;
  • 避免创建过多的对象,原则:不用了的东西要及时归还,定时器、回调不需要时显示删除,对于闭包,要解除闭包或外部函数中手动删除变量

垃圾回收详细内容见:JavaScript 垃圾回收及内存泄漏


7. call、apply、bind —— 改变函数运行时的 this 指向

call、apply 是立即调用,bind 是返回对应函数,便于后续调用。

fun.call(this, arg1, arg2, arg3)
fun.apply(this, [arg1, arg2, arg3])
fun.bind(this)

8. var、let、const 的区别

ES6 中增加了 let 和 const :

let 用来定义变量,定义后可更改;

const 用来定义常量,定义后不可更改。

let 和 var 的区别:

  • let 是块级作用域,var 是函数级作用域,let 的作用域更小;
  • let 无变量提升。

下面定义的变量,在上面使用会报错;

var有变量提升,下面定义的变量,在上面值为undefined,不会报错。

  • let 同一个变量只能声明一次,而 var 可声明多次。

9. 箭头函数和普通函数的区别

  • this 指向不同
  • 箭头函数是匿名函数,不能作为构造函数
  • 不绑定 arguments

10. event loop

JavaScript 是单线程语言,但实际应用中是需要异步操作的,比如加载一些资源或者请求网络接口等,宏任务、微任务就是用来解决这种问题的异步操作。

  • 宏任务:setTimeout、setInterval
  • 微任务:Promise.then、Promise.catch 等

JavaScript 的运行机制 —— Event Loop:

每次任务执行完成,都会去检查是否还有其他任务,如果有微任务,就先执行微任务,如果没有微任务,就继续执行宏任务。

参考:

微任务、宏任务与Event-Loop

什么是 Event Loop?


11. Promise 、async、await

11.1 Promise

Promise 是异步的,可以解决回调函数代码耦合性强、维护性低的问题。

Promise 对象的三种状态: pending、fullfilled、rejected

相关Api: 基本Api可参考阮一峰大神的:ECMAScript 6 入门

  • Promise.all([promise1, promise2, promise3]):promise1, promise2, promise3 等状态都变成 fulfilled,才会继续执行。
  • Promise.race([promise1, promise2, promise3]):有一个状态变为fulfilled,就会继续执行。

Promise 很好地解决了回调函数代码耦合性强、维护性低的问题,而 async/await 的出现又对 Promise 进行了升级,使得需要多级链式调用时,体验更好。

11.2 async/await

async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

以看似同步的写法,执行异步操作。

无论 async/await 后面跟的是否是异步函数,最后都会转换为 Promise。

  // 嵌套
  function f1() { console.log(111) }
  function f2() { console.log(222) }
  function f3() {  console.log(333) }

  async function timeoutFn() {
    await f1(); // 执行第一个
    await f2(); // 第一个执行完,开始执行第二个
    await f3(); // 第二个执行完,开始执行第三个
  }
  timeoutFn();

参考:

ECMAScript 6 入门

Promise && Async/Await

11.3 实现一个 Promise

//new Promise((resolve, reject) => {})

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function CusPromise(executor) {

    let self = this;
    self.state = PENDING;   //状态
    self.value = null;  //结果-成功
    self.error = null;  //结果-失败
    self.onFulfilledCallbacks = [];  //成功的异步回调
    self.onRejectedCallbacks = [];  //失败的异步回调

    //成功回调
    function resolve(value) {
        if(self.state === PENDING) {
            self.state = FULFILLED;
            self.value = value;
        }
        console.log(self.state)
        //异步回调
        self.onFulfilledCallbacks.forEach(onFulfilled => {
            setTimeout(() => {
                try {
                    let x = onFulfilled(self.value)
                    resolvePromise(promise2, x, resolve, reject)
                } catch(err) {
                    reject(err)
                }
            }, 0)
        })
    }
    //失败回调
    function reject(error) {
        if(self.state === PENDING) {
            self.state = REJECTED;
            self.error = error;
        }
        //异步回调
        self.onRejectedCallbacks.forEach(onRejected => {
            setTimeout(() => {
                try {
                    let x = onRejected(self.error)
                    resolvePromise(promise2, x, resolve, reject)
                } catch(err) {
                    reject(err)
                }
            }, 0)
        })
    }

    try {
        executor(resolve, reject)
    } catch (err) {
        reject(err)
    }
}

CusPromise.prototype.then = function(onFulfilled, onRejected) {
    let self = this;
    //返回一个新的 promise
    let promise2 = new CusPromise((resolve, reject) => {
        if(self.state === PENDING) {
            if(onFulfilled && typeof onFulfilled === 'function') {
                self.onFulfilledCallbacks.push(onFulfilled)
            }
            if(onRejected && typeof onRejected === 'function') {
                self.onRejectedCallbacks.push(onRejected)
            }
        }
        if(self.state === FULFILLED && onFulfilled && typeof onFulfilled === 'function') {
            setTimeout(() => {
                try {
                    let x = onFulfilled(self.value)
                    resolvePromise(promise2, x, resolve, reject)
                } catch(err) {
                    reject(err)
                }
            }, 0)
        }
        if(self.state === REJECTED && onRejected && typeof onRejected === 'function') {
            setTimeout(() => {
                try {
                    let x = onRejected(self.error)
                    resolvePromise(promise2, x, resolve, reject)
                } catch(err) {
                    reject(err)
                }
            }, 0)
        }
    })
    return promise2;
}

//处理返回值
function resolvePromise(promise, x, resolve, reject) {
    if(promise === x) {
        reject(new TypeError('Promise 循环引用'))
    }

    if(x !== null && (typeof x === 'object' || typeof x === 'function')) {
        try {
            let then = x.then;
            if(typeof then === 'function') {
                then.call(x, (r) => {
                    resolvePromise(promise, r, resolve, reject)
                }, (r) => {
                    reject(r)
                })
            } else {
                resolve(x)
            }
        } catch (err) {
            reject(err)
        }
    } else {
        resolve(x)
    }
}

参考:

segmentfault.com/a/119000001…

www.jianshu.com/p/c633a22f9…

11.4 generator

参考:www.jianshu.com/p/1c9e9c161…


12. 跨域

同源策略:

同源:协议+域名+端口 三者相同。同源策略:同源策略限制以下几种行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 和 Js对象无法获得
  • AJAX 请求不能发送 同源策略是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。

想要访问非同源的数据就是跨域,下面是几种常用的跨域方案。

12.1 JSONP

JSONP 是利用 script 标签能够请求非同源数据,来进行跨域,主要实现思路如下:

//1. 创建一个访问接口后的回调方法
function result(data) {
    alert(data.message)
}
//2. 通过 script 标签请求接口, 并将回调方法作为参数传递
function addScript(src) {
    let script = document.createElement('script');
    script.setAttribute("type", "text/javascript");
    script.src = src;
    document.body.appendChild(script)
}
window.onload = function() {
    addScript("http://test.js?type=1&callback=result")
}
//3. 服务端返回进行回调函数的执行 test.js
callback({message: "successful!"})

缺点: 只能实现 get 一种请求,无法发送特定头部,无法发送 body。

12.2 跨域资源共享(CORS)

只服务端请求头设置 Access-Control-Allow-Origin 即可,前端无须设置,若要带cookie请求:前后端都需要设置,设置withCredentials属性为true,。

13. 事件

事件冒泡: —— 事件开始时由最具体的元素(文档中嵌套层次最深的节点)接收,然后逐级向上传播到较为不具体的节点(文档)。

事件捕获: —— 不太具体的节点应更早接收到事件,与事件冒泡顺序相反。

事件流: 事件捕获 =>处于目标 => 事件冒泡

addEventListener 的第三个参数默认为 false,即默认为冒泡,不启用捕获,为 true 是启用事件捕获。

13.1 内存和性能

  • 添加到页面上的事件处理程序数量越多,内存中的对象越多,性能就越差。
  • 必须事先指定所有事件处理程序而导致的DOM访问次数,会延迟整个页面的交互就绪时间。

13.2 事件委托 (事件代理)—— 解决事件处理程序过多

  • 采用事件委托,只需要将事件处理程序指定给较高层次的元素,就可以管理这一类型的所有事件。
  • 采用事件委托需要占用的内存更少。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术。

事件详细内容可查看:JavaScript 高级程序设计学习笔记6——事件


14. 防抖、节流

  • 节流是一定时间内,回调函数只执行一次,期间有事件触发,不作处理。
  • 防抖是一定时间内,回调函数只执行一次,但期间若有事件触发,则重新计时。
  • 如果遇到的是连续不停的事件,则应该选择节流,因为不停触发的事件会让防抖只会执行一次回调函数

14.1 防抖

  • 非立即执行: 事件触发延迟设定时间后执行,期间若有事件发生,则重新计时。
function debounce(fn, wait, immediate) {
    let timeout;

    return function() {
        if(timeout) {
            clearTimeout(timeout)
        }
        timeout = setTimeout(() => {
            fn(arguments)
        }, wait)
    }
}
  • 立即执行: 事件触发马上执行回调函数,设定时间内不再执行回调函数,期间若有事件发生,则重新计时。
function debounce(fn, wait) {
    let timeout;

    return function() {
        if(timeout) {
            clearTimeout(timeout)
            timeout = setTimeout(() => {
                timeout = null
            })
        } else {
            fn(arguments)
            timeout = setTimeout(() => {
                timeout = null
            })
        }
        
    }
}
  • 传参控制立即执行或非立即执行
function debounce(fn, wait, immediate) {
    let timeout;

    return function() {
        if(timeout) {
            clearTimeout(timeout)
        }
        if(immediate) {
            fn(arguments)
            timeout = setTimeout(() => {
                timeout = null
            })
        } else {
            timeout = setTimeout(() => {
                fn(arguments)
            }, wait)
        }
    }
}

14. 2 节流

  • 立即执行: 事件触发则立即执行,设定时间内不再执行
  • 非立即执行: 事件触发延迟设定时间后再执行,且设计时间内不再执行
//时间戳版——立即执行
function throttle (fn, wait) {
    let previous = 0;
    
    return function() {
        let now = new Date().now()
        if(now - previous > wait) {
            fn(arguments)
            previous = now
        }
    }
}
//setTimeout——传参控制是否立即执行
function throttle (fn, wait, immediate) {
    let timeout;

    return function() {
        if(!timeout) {
            if(immediate) {
                fn(arguments) 
                timeout = setTimeout(() => {
                    timeout = null
                })
            } else {
                timeout = setTimeout(() => {
                    fn(arguments)
                    timeout = null
                })
            }
        }
    }
}

参考:防抖&节流


15. 深拷贝的简单实现

  • 浅拷贝: 只对对象的最外一层进行了精确拷贝,如果对象的属性是引用类型的话,拷贝的是内存地址,一个的值改变,会影响另一个对象。

    Object.assign 可实现浅拷贝

  • 深拷贝: 一个对象值的改变不影响另一个对象。

15.1 序列化反序列化法 —— JSON.stringify & JSON.parse(适用于对象&数组)

function deepClone(obj){
    let _obj = JSON.stringify(obj),
        objClone = JSON.parse(_obj);
    return objClone
}

15.2 迭代递归法(适用于对象&数组)

function isObject(o) {
    return typeof o === 'object' && o !== null
}
// 迭代递归法:深拷贝对象与数组
function deepClone(obj) {
    if (!isObject(obj)) {
        throw new Error('obj 不是一个对象!')
    }
 
    let isArray = Array.isArray(obj)
    let cloneObj = isArray ? [] : {}
    for (let key in obj) {
        let item = obj[key]
        cloneObj[key] = isObject(item) ? deepClone(item) : item
    }
 
    return cloneObj
}

16. 深比较的简单实现

function isObject(o) {
    return typeof o === "object" && o !== null
}

function isEqual(a, b) {
    let flag = true

    if(!isObject(a) && !isObject(b) && a === b) {
        return true
    }

    if(!isObject(a) && !isObject(b) && a !== b) {
        return false
    }

    if(typeof a !== typeof b) {
        return false
    }

    for(let key in a) {
        if(!b.hasOwnProperty(key)) {
            flag = false
            break
        }
        let itemA = a[key]
        let itemB = b[key]
        flag = isEqual(itemA, itemB)
    }
    return flag
}

17. Map & Set

17.1 Set & WeakSet

  • 两者均不会有重复项;
  • 均有 add、delete、has 方法;
  • Set 还有 size 属性,clear、entries、forEach 方法,WeakSet 没有(不可遍历);
  • WeakSet 的成员必须是对象类型的值。

17.2 Map & WeakMap

  • Map 与对象类似,但它的 key 键名不再局限于字符串,可以是各种类型的值;
  • Map 包含 set、get、delete、clear 方法, forEach 遍历(value,key);
  • WeakMap 键名只支持引用类型的数据(数组、对象、函数);且不支持 clear、遍历;
  • Map.keys() 或 Map.values() 返回的是迭代器,需再通过 for of 进行遍历。

17.3 Map 和对象的区别

  • 对象的键只能是字符串,Map 的键可以是任意类型;
  • 遍历方式不同