高频前端面试题汇总之JavaScript篇(下)

769 阅读21分钟

一、this/call/apply/bind

1. 对this对象的理解

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。

函数调用模式:当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象
方法调用模式:如果一个函数作为一个对象的方法来调用时,this 指向这个对象
构造器调用模式:如果一个函数用 new 调用,函数执行前会新创建一个对象,this 指向新创建的对象
apply 、 call 和 bind 调用模式:这三个方法都可以显示的指定调用函数的 this 指向

优先级:构造器调用 > apply、call 和 bind 调用 -> 方法调用 -> 函数调用

注意:非严格模式下 this 指向全局对象,严格模式下 this 会绑定为 undefined;箭头函数没有 this

2. call() 和 apply() 的区别?

它们的作用一模一样,区别仅在于传入参数的形式的不同。

1. apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,
第二个参数为一个带下标的集合(数组/类数组)
2. call 的参数数量不固定,第一个参数也是指定函数体内的 this 指向,
从第二个参数开始往后,每个参数被依次传入函数。

3. 实现call、apply 及 bind 函数

Function.prototype.call2 = function(context, ...args) {
  context = (context === undefined || context === null) ? window : context
  context.__fn = this
  let result = context.__fn(...args)
  delete context.__fn
  return result
}
Function.prototype.apply2 = function(context, args) {
  context = (context === undefined || context === null) ? window : context
  context.__fn = this
  let result = context.__fn(...args)
  delete context.__fn
  return result
}
Function.prototype.bind2 = function(context, ...args1) {
  context = (context === undefined || context === null) ? window : context
  let _this = this
  return function(...args2) {
    context.__fn = _this
    let result = context.__fn(...[...args1, ...args2])
    delete context.__fn
    return result
  }
}

二、异步编程

1. 异步编程解决方案的发展历程以及优缺点

image.png

image.png

image.png

2. 对AJAX的理解,实现一个AJAX请求

AJAX(异步 JavaScript 和 XML), 指的是通过 JavaScript 进行异步通信,在不刷新整个网页的前提下,更新网页的部分内容。

const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);

3. 使用Promise封装AJAX:

// promise 封装实现:
function getJSON(url) {
  // 创建一个 promise 对象
  let promise = new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest();
    // 新建一个 http 请求
    xhr.open("GET", url, true);
    // 设置状态的监听函数
    xhr.onreadystatechange = function() {
      if (this.readyState !== 4) return;
      // 当请求成功或失败时,改变 promise 的状态
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    // 设置错误监听函数
    xhr.onerror = function() {
      reject(new Error(this.statusText));
    };
    // 设置响应的数据类型
    xhr.responseType = "json";
    // 设置请求头信息
    xhr.setRequestHeader("Accept", "application/json");
    // 发送 http 请求
    xhr.send(null);
  });
  return promise;
}

4. 对Promise的理解

Promise 是异步编程的一种解决方案:从语法上讲,promise是一个对象,从它可以获取异步操作的消息;从本意上讲,它是承诺,承诺过一段时间会给你一个结果。他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

三个状态:
Pending(进行中)
Resolved(已完成)
Rejected(已拒绝)

特点:
1.一旦状态改变就不会再变
2.对象的状态不受外界影响。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变
3.一旦新建它就会立即执行,无法中途取消
4.当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

5. Promise 的基本用法

(1)创建Promise对象

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value)
  } else {
    reject(error)
  }
})

一般情况下都会使用 new Promise() 来创建promise对象,但是也可以使用 promise.resolve 和 promise.reject 这两个方法:

Promise.resolve:其返回值也是一个promise对象,可以对返回值进行.then调用

Promise.resolve(11).then(function(value){
  console.log(value); // 打印出11
});

resolve(11) 代码中,会让 promise 对象进入确定(resolve状态),并将参数11传递给后面的 then 所指定的onFulfilled 函数

Promise.reject:也是new Promise的快捷形式,也创建一个 promise 对象。

new Promise(function(resolve,reject){
   reject(new Error("我错了!"));
});

// 简写
Promise.reject(new Error(“我错了,请原谅俺!!”));

(2)Promise方法

1. then()

当Promise执行的内容符合成功条件时,调用resolve函数,失败就调用reject函数。Promise创建完了,那该如何调用呢?

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。 then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

2. catch()

Promise对象除了有then方法,还有一个catch方法,该方法相当于then方法的第二个参数,指向 reject 的回调函数。不过 catch 方法还有一个作用,就是在执行 resolve 回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。

p.then((data) => {
     console.log('resolved',data);
},(err) => {
     console.log('rejected',err);
     }
); 
p.then((data) => {
    console.log('resolved',data);
}).catch((err) => {
    console.log('rejected',err);
});

3. finally()

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例:

promise
.finally(() => {
  // 语句
});
// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

6. Promise解决了什么问题

比如我使用ajax发一个A请求后,成功后拿到数据,需要把数据传给B请求。那么需要如下编写代码,这样会导致多个ajax请求嵌套的情况,代码不够直观

let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
  fs.readFile(data,'utf8',function(err,data){
    fs.readFile(data,'utf8',function(err,data){
      console.log(data)
    })
  })
})

Promise出现之后,代码变成这样:

let fs = require('fs')
function read(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,'utf8',function(error,data){
      error && reject(error)
      resolve(data)
    })
  })
}
read('./a.txt').then(data=>{
  return read(data) 
}).then(data=>{
  return read(data)  
}).then(data=>{
  console.log(data)
})

这样代码看起了就简洁了很多,解决了地狱回调的问题。

7. ajax、axios、fetch的区别

(1)AJAX (异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。其缺点如下:

  • 本身是针对MVC编程,不符合前端MVVM的浪潮
  • 基于原生XHR开发,XHR本身的架构不清晰
  • 配置和调用方式非常混乱

(2)Fetch: 是AJAX的替代品,使用了 promise 对象,fetch 不是 ajax 的进一步封装,而是原生 js,没有使用XMLHttpRequest对象,Fetch的代码结构比ajax 简单。

优点如下:

  • 语法简洁,更加语义化
  • 基于标准 Promise 实现,支持 async/await

缺点如下:

  • fetch只对网络请求报错,对400,500都当做成功的请求
  • fetch不支持abort,不支持超时控制,造成了流量的浪费
  • fetch没有办法原生监测请求的进度,而XHR可以

(3)Axios: 功能非常强大,其本质是 ajax,是一个基于 promise 的网络请求库,可以用于浏览器和 node.js。Axios 有以下的特性:

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

实际上,axios 可以用在浏览器和 node.js 中,是因为它会自动判断当前环境是什么,如果是浏览器,就会基于 XMLHttpRequests 实现 axios;如果是 node.js 环境,就会基于node内置核心模块 http 实现 axios。

8. 对async/await 的理解

async/await其实是Generator 的语法糖,它是为优化then链而开发出来的。从字面上来看,async是“异步”,await则为等待,所以 async 用于申明一个函数是异步的,而 await 用于等待一个异步方法执行完成,注意 await 只能出现在asnyc函数中。

我们来看下 async 函数的返回值是什么:

async function testAsy(){
   return 'hello world';
}
let result = testAsy(); 
console.log(result)

async 函数返回的是一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。当 async 函数没有返回值时,会返回 Promise.resolve(undefined)

注意: Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

9. await 到底在等啥?

一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值。所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:

function getSomething() {
    return "something";
}
async function testAsync() {
    return Promise.resolve("hello async");
}
async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}
test();

await 表达式的运算结果取决于它等的是什么。

  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
  • 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

10. async/await对比Promise的优势

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
  • Promise传递中间值非常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获非常冗余
  • 调试友好,Promise的调试很差

11. Promise 为什么能链式调用

由于它的then方法和catch、finally方法会返回一个新的 Promise 所以可以允许我们链式调用。

12. 并发与并行的区别?

  • 并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
  • 并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。

13. promise 值穿透

值穿透指的是,链式调用的参数不是函数时,会发生值穿透,会忽略非函数值,传入的是之前函数的参数。

// 例一:
Promise.resolve(1)
    .then(2)
    .then(Promise.resolve(3))  
    .then(console.log)  // 1

注意:Promise.resolve(3) 的值是一个 Promise 对象

// 例二:
Promise.resolve('foo')
    .then(Promise.resolve('bar'))
    .then(function(result){
      console.log(result)       // 'foo'
    })
// 例三:
Promise.resolve(1)
  .then(function(){return 2})
  .then(Promise.resolve(3))
  .then(console.log)  // 2
// 例四:
Promise.resolve(1)
  .then(function(){return 2})
  .then(function(){return Promise.resolve(3)})
  .then(console.log)  // 3

14. promise 异常穿透

// 例子一:
new Promise((resolve, reject) => {
        reject(1) 
      })
      .then(value => {
        console.log('成功', value);
      }, reason => {
        console.log('失败', reason); 
      })
      .then(value => {
        console.log('成功', value); 
      }, reason => {
        console.log('失败', reason);
      })

image.png

第一个 then 接受了两个函数,第二个是失败时候的回调,由于第一个 then 里面没有抛出 Error,则第二个 then 状态是 resolve。

// 例子二:
new Promise((resolve, reject) => {
        reject(1)
      })
      .then(value => {
        console.log('成功', value);
      }, reason => {
        console.log('失败', reason); 
      })
      .then(value => {
        console.log('成功', value); 
      }, reason => {
        console.log('失败', reason);
      })
      .catch(reason => console.log('失败', reason)) //这里增加catch,但是不会走到这里来

image.png

// 例子三:
 new Promise((resolve, reject) => {
        reject(1)
      })
      .then(value => {
        console.log('成功', value); //没有指定失败的回调函数,不执行代码,去往下一级寻找失败状态回调函数
      })
      .then(value => {
        console.log('成功', value); //没有指定失败的回调函数,不执行代码,去往下一级寻找失败状态回调函数
      })
      .catch(reason => console.log('失败', reason))  //这里执行了,失败 1;

    //当then方法中没有指定失败的回调函数时,
    //使用.catch会默认为没有指定失败回调函数的.then指定失败回调函数为:
    reason => {
      throw reason
    }

image.png

总结:

  1. 当使用 .catch 时,会默认为没有指定失败状态回调函数的 .then 添加一个失败回调函数
  2. .catch 所谓的异常穿透并不是一次失败状态就触发 catch,而是一层一层的传递下来的。
  3. 异常穿透的前提条件是所有的.then 都没有指定失败状态的回调函数;如果.catch前的所有 .then 都指定了失败状态的回调函数,.catch 就失去了意义。

15. await的使用注意点

  1. await 命令后面的 Promise 对象,运行结果可能是 rejected ,所以最好把 await 命令放在try...catch代码块中。
  2. 多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
  3. await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
  4. async 函数可以保留运行堆栈。

16. async语法怎么捕获异常

async函数内部的异常可以通过 .catch()或者 try/catch来捕获,区别是

  • try/catch 能捕获所有异常,try语句抛出错误后会执行catch语句,try语句内后面的内容不会执行
  • catch()只能捕获异步方法中reject错误,并且catch语句之后的语句会继续执行

17. setTimeout、Promise、Async/Await 的区别

三、垃圾回收与内存泄漏

1. 浏览器的垃圾回收机制

(1)内存的生命周期

JS 环境中分配的内存, 一般有如下生命周期:

  1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存

  2. 内存使用:即读写内存,也就是使用变量、函数等

  3. 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存

全局变量一般不会回收, 一般局部变量的的值, 不用了, 会被自动回收掉

(2)垃圾回收的概念

垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。

回收机制

  • Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
  • JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续到页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。
  • 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。

(3)垃圾回收的方式

1)标记清除

  • 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。
  • 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

2)引用计数

  • 另外一种垃圾回收机制就是引用计数,这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。
  • 这种方法会引起循环引用的问题:例如: obj1obj2通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1obj2还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。
function fun() {
    let obj1 = {};
    let obj2 = {};
    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

这种情况下,就要手动释放变量占用的内存:

obj1.a =  null
obj2.a =  null

(4)减少垃圾回收

虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。

  • 对数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。
  • 对object进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。
  • 对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。

2. 哪些情况会导致内存泄漏

内存泄漏:是指由于疏忽或错误造成程序未能释放已经不再使用的内存

  • 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
  • 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
  • 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
  • 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。

四、 函数与函数式编程

1. 什么是函数式编程

函数式编程是一种"编程范式",一种编写程序的方法论。主要的编程范式有三种:命令式编程,声明式编程和函数式编程。

相比命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程

2. 函数式编程的优缺点

优点

  • 更好的管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错情况
  • 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响
  • 更优雅的组合:往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性
  • 隐性好处。减少代码量,提高维护性

缺点

  • 性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销
  • 资源占用:在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
  • 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作

3. 什么是高阶函数

高阶函数是指使用其他函数作为参数、或者返回一个函数作为结果的函数。

4. 函数柯里化

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。而对于Javascript语言来说,与数学和计算机科学中的柯里化的概念并不完全一样。

在数学和计算机科学中的柯里化函数,一次只能传递一个参数;而我们Javascript实际应用中的柯里化函数,可以传递一个或多个参数。

//普通函数
function fn(a,b,c,d,e) {
  console.log(a,b,c,d,e)
}
//生成的柯里化函数
let _fn = curry(fn);

_fn(1,2,3,4,5);     // print: 1,2,3,4,5
_fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
_fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5

总的来说:柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

函数柯里化的好处:

(1)参数复用:需要输入多个参数,最终只需输入一个,其余通过 arguments 来获取

(2)提前确认:避免重复去判断某一条件是否符合,不符合则 return 不再继续执行下面的操作

(3)延迟运行:避免重复的去执行程序,等真正需要结果的时候再执行

5. 说说你对递归函数的理解

如果一个函数在内部调用自身本身,这个函数就是递归函数。其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解

一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回

优点:结构清晰、可读性强

缺点:效率低、调用栈可能会溢出,其实每一次函数调用会在内存栈中分配空间,而每个进程的栈的容量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致栈溢出。

6. 什么是尾递归

尾递归,即在函数尾位置调用自身(或是一个尾调用本身的其他函数等等)。

在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,递归次数过多容易造成栈溢出

这时候,我们就可以使用尾递归,即一个函数中所有递归形式的调用都出现在函数的末尾,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误

7. 函数声明与函数表达式的区别

1)函数声明:funtion开头,有函数提升

(2)函数表达式: 不是funtion开头,没有函数提升