前端面试系列之最重要的基础知识——JavaScript篇

前阵子总结了一下前端面试相关的知识,有八股文,有算法。分享给大家!大家也可以关注一下专栏,后面会陆续发表相关文章!

注:大部分总结来源于网络资料,因为总结的时候没留意来源,如有侵权,请联系我哈!

前端面试系列:

JavaScript 基础

JS数据类型有哪些?

  • JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
  • JavaScript 可以分为两种类型的值,一种是基本数据类型,一种是复杂数据类型。
  • 复杂数据类型指的是 Object 类型,所有其他的如 Array、Date 等数据类型都可以理解为 Object 类型的子类。
  • 两种类型间的主要区别是它们的存储位置不同,基本数据类型的值直接保存在栈中,而复杂数据类型的值保存在堆中,通过使用在栈中保存对应的指针来获取堆中的值。

说说 Symbol 和 BigInt

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。

    • 应用场景

      • 对象中保证不同的属性名

        • 注意:使用 Symbol 值定义属性的时候,必须放在方括号中
        • 读取的时候也是不能使用点运算符
      • 定义一组常量,保证这组常量都是不相等的

      • 使用 Symbol 定义类的私有属性/方法(???)

      • Vue 中的 project 和 inject

  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

  • 【注】:以上只是最简单的版本,可以的话,答出更多的应用场景会更优。

null 和 undefined 的区别?

  • 首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
  • undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。
  • undefined 在 js 中不是一个保留字,这意味着我们可以使用 undefined 来作为一个变量名,这样的做法是非常危险的,它会影响我们对 undefined 值的判断。但是我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
  • 当我们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。
  • 当我们使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

JavaScript 数据类型如何判定?

  • typeof。其中数组、对象、null都会被判断为object,其他判断都正确。
  • Instanceof。instanceof 可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。可以看到,instanceof 只能正确判断引用数据类型,而不能判断基本数据类型。
  • constructor。console.log((2).constructor === Number); // true
  • Object.prototype.toString.call()

JavaScript 数组如何判定?

  • 通过Object.prototype.toString.call()做判断:

    • Object.prototype.toString.call(obj).slice(8,-1) === ‘Array’;
  • 通过ES6的Array.isArray()做判断。Array.isArrray(obj);

  • 通过instanceof做判断。obj instanceof Array

  • 通过原型链做判断。

    • obj.__proto__ === Array.prototype;

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

  • 计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100…(1100循环),0.2的二进制是:0.00110011001100…(1100循环),这两个数的二进制都是无限循环的数。
  • 对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3。
  • 可以将其转换为整数后在进行运算,运算后再转为对应的小数

typeof null 输出 object

  • 在 JS 的最初版本中,为了性能考虑,使用低位存储变量的类型信息,000开头表示对象,然而 null 代表全 0,所以将它错误判断为对象

为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组?

  • arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有callee和length等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。

  • 要遍历类数组,有三个方法:

    • 将数组的方法应用到类数组上,这时候就可以使用call和apply方法

      • Array.prototype.forEach.call(arguments, a => console.log(a))
    • 使用Array.from方法将类数组转化成数组

      • const arrArgs = Array.from(arguments)
    • 使用展开运算符将类数组转化成数组

      • const arrArgs = […arguments]

修改this指向

  • call() 方法
  • apply() 方法
  • bind() 方法。bind() 方法的用法和call()一样,直接运行方法,需要注意的是:bind返回新的方法,需要重新调用是需要自己手动调用的。

数组有哪些原生方法?

  • 数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法可以指定转换为字符串时的分隔符。
  • 数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数。
  • 数组首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。
  • 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。
  • 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。
  • 数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法
  • 数组归并方法 reduce() 和 reduceRight() 方法

哪些数组方法不会改变原数组?

  • 原数组改变的方法有:shift unshift reverse sort push pop splice
  • 不改变原数组的方法有:concat map filter join every some indexOf slice forEach

JS 的 new 操作符做了哪些事情

  • (1)首先创建了一个新的空对象
  • (2)设置原型,将对象的原型设置为函数的 prototype 对象。
  • (3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
  • (4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

map和Object的区别?

  • 意外的键:Map 默认情况不包含任何键,只包含显式插入的键。Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。
  • 键的类型:Map的键可以是任意值,包括函数、对象或任意基本类型。Object 的键必须是 String 或是Symbol。
  • 键的顺序:Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。Object 的键是无序的
  • Size:Map 的键值对个数可以轻易地通过size 属性获取。Object 的键值对个数只能手动计算
  • 迭代:Map 是 iterable 的,所以可以直接被迭代。迭代Object需要以某种方式获取它的键然后才能迭代。
  • 性能:在频繁增删键值对的场景下表现更好。在频繁添加和删除键值对的场景下未作出优化。

事件捕获和事件冒泡的区别?如何阻止事件冒泡与默认行为问题?

  • DOM事件流

    • 事件捕获:事件从最不精确的对象(document 对象)开始触发,然后到最精确(也可以在窗口级别捕获事件,不过必须由开发人员特别指定)。
    • 事件目标:当到达目标元素之后,执行目标元素该事件相应的处理函数。如果没有绑定监听函数,那就不执行。
    • 事件冒泡:事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发,当一个元素接收到事件的时候会把他接收到的事件传给自己的父级,一直到window 。
  • 阻止事件冒泡

    • event.stopPropagation(); // 一般浏览器停止冒泡
    • event.cancelBubble; // IE 6 7 8 的停止冒泡

什么是事件委托?

  • 事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。
  • 使用事件代理我们可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理我们还可以实现事件的动态绑定,比如说新增了一个子节点,我们并不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。

JavaScript 进阶

对原型、原型链的理解

  • 在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。
  • 当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。
  • 一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 __proto__ 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。
  • 当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。
  • 原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。Object.prototype.__proto__=== null // true

JavaScript 中如何进行隐式类型转换?

  • 首先要介绍ToPrimitive方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:

    • ToPrimitive(obj,type) @obj 需要转换的对象。@type 期望的结果类型

      • type的值为number或者string。

        • (1)当type为number时规则如下:

          • 调用obj的valueOf方法,如果为原始值,则返回,否则下一步;
          • 调用obj的toString方法,后续同上;
          • 抛出TypeError 异常。
        • (2)当type为string时规则如下:

          • 调用obj的toString方法,如果为原始值,则返回,否则下一步;
          • 调用obj的valueOf方法,后续同上;
          • 抛出TypeError 异常。
      • 可以看出两者的主要区别在于调用toString和valueOf的先后顺序。默认情况下:

        • 如果对象为 Date 对象,则type默认为string;
        • 其他情况下,type默认为number。
      • 而 JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。

        • Link

== VS ===

  • == 判断流程

    • 判断类型是否相同,相同的话就是比较大小了

    • 类型不同,进行类型转换

    • 是否在对比 null 和 undefined

      • 是返回true
    • 是否为 string 和 number

      • 是的话将 string 转为 number
    • 判断一方是否为 boolean

      • 将 boolean 转换为 number 类型
    • 判断一方是否为 Object ,而且另一方是 string、number 或者 symbol

      • 将 Object 转换成原始类型
    • 隐式类型转换判断神器

    • 详细的标准

    • new String(“a”) == new String(“a”)

闭包

  • 概念

    • 该函数可以访问它被创建时候所处的上下文环境《JavaScript 语言精粹》
    • 闭包是指有权访问另一个函数作用域中的变量的函数 —- 《JavaScript 高级程序设计》
    • 函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包
    • 闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量
  • 循环中使用闭包解决 var 定义函数的问题

    • 原题:
for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

都是输出6,注意边界

  • 闭包
for (var i = 1; i <= 5; i++) {
    (function (j) {
        setTimeout(function timer() {
        console.log(j)
      }, i * 1000)
    })(i)
}
  • setTimeout 第三个参数

    • setTimeout 第三个参数以及之后的参数,会作为timer 的传参
  • let 定义 i

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i)
}
  • 闭包有两个常用的用途;

    • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
    • 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

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

  • 概念:AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。

  • 创建AJAX请求的步骤:

    • 创建一个 XMLHttpRequest 对象。
    • 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
    • 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
    • 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。

get 和 post 请求的区别

  • GET在浏览器回退时是无害的,而POST会再次提交请求。
  • GET产生的URL地址可以被Bookmark,而POST不可以。
  • GET请求会被浏览器主动cache,而POST不会,除非手动设置。
  • GET请求只能进行url编码,而POST支持多种编码方式。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求在URL中传送的参数是有长度限制的,而POST没有。
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
  • GET参数通过URL传递,POST放在Request body中

你了解到的作用域有哪些?

  • (1)全局作用域

    • 最外层函数和最外层函数外面定义的变量拥有全局作用域
    • 所有未定义直接赋值的变量自动声明为全局作用域
    • 所有window对象的属性拥有全局作用域
    • 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。
  • (2)函数作用域

    • 函数作用域声明在函数内部的变量,一般只有固定的代码片段可以访问到
    • 作用域是分层的,内层作用域可以访问外层作用域,反之不行
  • (3)块级作用域

    • 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段)
    • let和const声明的变量不会有变量提升,也不可以重复声明
    • 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。

说下作用域链

  • 作用域链: 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。
  • 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。

执行上下文类型

  • (1)全局执行上下文

    • 任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
  • (2)函数执行上下文

    • 当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
  • (3)eval函数执行上下文

    • 执行在eval函数中的代码会有属于他自己的执行上下文,不过eval函数不常使用,不做介绍。

垃圾回收

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

  • 垃圾回收的方式

    • 1)标记清除

      • 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。

      • 整个标记清除算法大致过程就像下面这样

        • 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
        • 然后从各个根对象开始遍历,把不是垃圾的节点改成1
        • 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
        • 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
    • 2)引用计数

      • 2)引用计数

      • 另外一种垃圾回收机制就是引用计数,这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。

        • 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
        • 如果同一个值又被赋给另一个变量,那么引用数加 1
        • 如果该变量的值被其他的值覆盖了,则引用次数减 1
        • 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
      • 这种方法会引起循环引用的问题:例如: obj1和obj2通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1和obj2还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。这种情况下,就要手动释放变量占用的内存。

  • V8 垃圾回收机制

    • 新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持 1~8M 的容量,而老生代的对象为存活事件较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大

    • 新生代垃圾回收:

      • 新加入的对象都会存放到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作
      • 当开始进行垃圾回收时,新生代垃圾回收器会对使用区中的活动对象做标记,标记完成之后将使用区的活动对象复制进空闲区并进行排序,随后进入垃圾清理阶段,即将非活动对象占用的空间清理掉。最后进行角色互换,把原来的使用区变成空闲区,把原来的空闲区变成使用区
      • 当一个对象经过多次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用老生代的垃圾回收策略进行管理
      • 另外还有一种情况,如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中,设置为 25% 的比例的原因是,当完成 Scavenge 回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配
    • 老生代垃圾回收

      • 相比于新生代,老生代的垃圾回收就比较容易理解了,上面我们说过,对于大多数占用空间大、存活时间长的对象会被分配到老生代里,因为老生代中的对象通常比较大,如果再如新生代一般分区然后复制来复制去就会非常耗时,从而导致回收执行效率不高,所以老生代垃圾回收器来管理其垃圾回收执行,它的整个流程就采用的就是上文所说的标记清除算法了
      • 首先是标记阶段,从一组根元素开始,递归遍历这组根元素,遍历过程中能到达的元素称为活动对象,没有到达的元素就可以判断为非活动对象
      • 清除阶段老生代垃圾回收器会直接将非活动对象,也就是数据清理掉
    • 优化机制

      • 前面我们也提过,标记清除算法在清除后会产生大量不连续的内存碎片,过多的碎片会导致大对象无法分配到足够的连续内存,而 V8 中就采用了我们上文中说的标记整理算法来解决这一问题来优化空间

        • 标记整理(Mark-Compact)算法 就可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存
      • 并行回收

        • 简单来说,使用并行回收,假如本来是主线程一个人干活,它一个人需要 3 秒,现在叫上了 2 个辅助线程和主线程一块干活,那三个人一块干一个人干 1 秒就完事了,但是由于多人协同办公,所以需要加上一部分多人协同(同步开销)的时间我们算 0.5 秒好了,也就是说,采用并行策略后,本来要 3 秒的活现在 1.5 秒就可以干完了
      • 增量标记与懒性清理

        • 增量就是将一次 GC 标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮 GC 标记

        • 懒性清理

          • 增量标记其实只是对活动对象和非活动对象进行标记,对于真正的清理释放内存 V8 采用的是惰性清理(Lazy Sweeping)
        • 增量标记完成后,惰性清理就开始了。当增量标记完成后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,可以将清理过程稍微延迟一下,让 JavaScript 脚本代码先执行,也无需一次性清理完所有非活动对象内存,可以按需逐一进行清理直到所有的非活动对象内存都清理完毕,后面再接着执行增量标记

  • 哪些情况会导致内存泄漏

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

异步编程

  • 什么是回调函数?有什么缺点?

    • 可以举个例子说明:ajax(url, () => { // 处理逻辑 })
    • 缺点(回调地狱(Callback hell)):
    • 可读性很差
    • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
    • 嵌套函数一多,就很难处理错误
  • Promise.all和Promise.race的区别的使用场景

    • (1)Promise.all

      • Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
      • Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。
      • 需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。
    • (2)Promise.race

      • 顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决。
      • Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
  • Promise的缺点?

    • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
    • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
    • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
  • 对async/await 的理解

    • async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。

    • 从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

    • 语法上强制规定await只能出现在asnyc函数中。

      • 目前 await 可以直接脱离 async 在顶层调用,但是需要在 ESM 模块中。Chrome 中可以没有模块限制,但是这只是 V8 的一个特性。
    • async 函数会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

    • ….

  • async/await对比Promise的优势

    • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
    • Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
    • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余
    • 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。
  • 异步编程的实现方式?

    • 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
    • Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
    • generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
    • async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

继承有哪些?有什么优缺点?

  • 组合继承
    • 在子类的构造函数中通过 Parent.call(this) 继承父类的属性

    • 改变子类的原型为 new Parent() 来继承父类的函数

    • 优点

      • 构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数
    • 缺点

      • 在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
  function Parent(value) {
    this.val = value
  }
  Parent.prototype.getValue = function() {
    console.log(this.val)
  }
  function Child(value) {
    Parent.call(this, value)
  }
  Child.prototype = new Parent()
  
  const child = new Child(1)
  
  child.getValue() // 1
  child instanceof Parent // true
  • 寄生组合继承
    • 将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
  function Parent(value) {
    this.val = value
  }
  Parent.prototype.getValue = function() {
    console.log(this.val)
  }
  
  function Child(value) {
    Parent.call(this, value)
  }
  Child.prototype = Object.create(Parent.prototype, {
    constructor: {
      value: Child,
      enumerable: false,
      writable: true,
      configurable: true
    }
  })
  
  const child = new Child(1)
  
  child.getValue() // 1
  child instanceof Parent // true
  • Class 继承

    • class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。
    • 在 JS 中并不存在类,class 的本质就是函数。

常用定时器函数

  • setTimeout

    • 因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。
  • setInterval

    • 它和 setTimeout 一样,不能保证在预期的时间执行任务
    • 回调函数执行时间不确定,可能会出现意外情况
    • 每个 setTimeout 产生的任务会直接 push 到任务队列中;而 setInterval 在每次把任务 push 到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中,如果有则不添加,没有则添加)。
  • requestAnimationFrame

    • requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题

ES6

let、const、var的区别

  • (1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:

    • 内层变量可能覆盖外层变量
    • 用来计数的循环变量泄露为全局变量
  • (2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。

  • (3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。

  • (4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。

  • (5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。

  • (6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。

  • (7)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

箭头函数与普通函数的区别

  • (1)箭头函数比普通函数更加简洁
  • (2)箭头函数没有自己的this
  • (3)箭头函数继承来的this指向永远不会改变
  • (4)call()、apply()、bind()等方法不能改变箭头函数中this的指向
  • (5)箭头函数不能作为构造函数使用
  • (6)箭头函数没有自己的arguments
  • (7)箭头函数没有prototype

扩展运算符的作用及使用场景

  • (1)对象扩展运算符

    • 实现对象浅拷贝。对象的扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。
  • (2)数组扩展运算符

    • 将数组转换为参数序列。const numbers = [1, 2]; add(…numbers) // 3
    • 复制数组。const arr2 = […arr1];
    • 合并数组。const arr2 = [‘one’, …arr1, ‘four’, ‘five’];
    • 扩展运算符与解构赋值结合起来,用于生成数组。const [first, …rest] = [1, 2, 3, 4, 5];
    • 将字符串转为真正的数组。[…’hello’]
    • 任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组。const args = […arguments];

对对象与数组的解构的理解

  • 解构是 ES6 提供的一种新的提取数据的模式,这种模式能够从对象或数组里有针对性地拿到想要的数值。

  • 1)数组的解构 在解构数组时,以元素的位置为匹配条件来提取想要的数据的:

    • const [a, b, c] = [1, 2, 3]
  • 2)对象的解构 对象解构比数组结构稍微复杂一些,也更显强大。在解构对象时,是以属性的名称为匹配条件,来提取想要的数据的。现在定义一个对象:

    • const stu = { name: ‘Bob’, age: 24 }

ES6中模板语法与字符串处理

  • 模板字符串有以下优点:

    • 在模板字符串中,空格、缩进、换行都会被保留
    • 模板字符串完全支持“运算”式的表达式,可以在${}里完成一些计算
  • 字符串处理

    • (1)存在性判定。在过去,当判断一个字符/字符串是否在某字符串中时,只能用 indexOf > -1 来做。现在 ES6 提供了三个方法:includes、startsWith、endsWith,它们都会返回一个布尔值来告诉你是否存在。
    • (2)自动重复:可以使用 repeat 方法来使同一个字符串输出多次(被连续复制多次)
    • ….

对 Set,Map 的理解

  • Set

    • 概念:ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值
    • 基础用法
    • 应用场景
  • Map

    • 概念:类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
    • 基础用法
    • 应用场景

ES Module 和 Common JS 区别?

  • ES6 module是编译时导出接口,CommonJS是运行时导出对象。
  • ES6 module输出的值的引用,CommonJS输出的是一个值的拷贝。
  • ES6 module导入模块的是只读的引用,CommonJS导入的是可变的,是一个普通的变量。
  • ES6 module语法是静态的,CommonJS语法是动态的。
  • ES6 module支持异步,CommonJS不支持异步。

什么是回调函数?有什么缺点?

  • 可以举个例子说明:ajax(url, () => { // 处理逻辑 })
  • 缺点(回调地狱(Callback hell)):
  • 可读性很差
  • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
  • 嵌套函数一多,就很难处理错误

Promise.all和Promise.race的区别的使用场景

  • (1)Promise.all

  • Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

  • Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。

  • 需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。

  • (2)Promise.race

  • 顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决。

  • Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

Promise的缺点?

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

对async/await 的理解

  • async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。

  • 从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

  • 语法上强制规定await只能出现在asnyc函数中。

  • 目前 await 可以直接脱离 async 在顶层调用,但是需要在 ESM 模块中。Chrome 中可以没有模块限制,但是这只是 V8 的一个特性。

  • async 函数会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

  • ….

async/await对比Promise的优势

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
  • Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余
  • 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。

异步编程的实现方式?

  • 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
  • Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
  • generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
  • async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

用过哪些 ES6 特性?(综合)

  • 可以根据上面提到的知识点提炼出自己的答案。

原文链接:https://juejin.cn/post/7244335987575521336 作者:Gopal

(0)
上一篇 2023年6月14日 上午10:00
下一篇 2023年6月14日 上午10:10

相关推荐

发表回复

登录后才能评论