你不得不知道的JS面试题

223 阅读22分钟

对象、原型、函数和闭包的紧密结合组成了JavaScript。本文会从函数,对象和浏览器三个方面全面梳理Javascript 知识。结合《javascript 忍者秘籍》和《你不知道的javascript》整理理解 JS。并且按逻辑总结出常见的 JS 面试题,可以通过目录快速定位查看,持续更新~  🔗

1.基本概念

Javascript 能够运行在很多环境中,这里将重点考虑浏览器环境。

image.png

image.png

🚀面试题:web应用生命周期

🚀面试题:JS事件循环

2.理解函数

函数是 javascript 的基础,函数是第一类对象,或者说被称为一等公民。函数和那些 更普通的JavaScript数据类型一样,它能被变量引用,能以字面量形式声明,甚至能被作为函数参数进行传递。通过以下几个部分让我们对函数有一个全面的了解吧。
入门:定义与参数
基础:函数调用和上下文
进阶:理解闭包与作用域
高级:生成器和Promise

2.1 函数入门:定义与参数

函数是程序执 行过程中的主要模块单元。除了全局JavaScript代码是在页面构建的阶段 执行的,我们编写的所有的脚本代码都将在一个函数内执行。 我们知道函数本质上也是一种特殊的对象,因此在 javascript 中函数还具有一些对象的特性。

🚀面试题:对象的常用功能

  • 对象可以通过字面量来创建{} const obj = {}
  • 对象可以赋值给变量、数组项,或其他对象的属性。
  • 对象可以作为参数传递
  • 对象可以作为函数的返回值
  • 对象能够动态创建和分配的属性

对应的我们函数也能支持这些操作

function ninjaFunction() {} // 通过字面量创建
var ninjaFunction = function() {}; // 为变量赋值一个新函数  
ninjaArray.push(function(){}); // 向数组中增加一个新函数 
ninja.data = function(){}; // 给某个对象的属性赋值为一个新函数
call(function(){}); // 一个新函数作为参数传递给函数
function returnNewNinjaFunction() {
    return function(){}; // 返回一个新函数 
}
ninjaFunction.ninja = "Hanzo"; //为函数增加一个新属性

🚀面试题:什么是函数式编程,优点是什么

把函数作为第一类对象是函数式编程(functional programming)的第一步,函数式编 程是一种编程风格,它通过书写函数式(而不是指定一系列执行步骤,就像那种更主 流的命令式编程)代码来解决问题。函数式编程可以让代码更容易测试、扩展及模块 化。 对象能做的任何一件事,函数也都能做。函数也是对象,唯一的特殊之处在于它是可调用的(invokable),即函数会被调用以便执行某项动作。

函数能够作为参数传入函数。基于这个特性就有了回调函数的概念

🚀面试题:什么是回调函数

如果我们将某个函数作为参数传入另一个函数, 传入函数会在应用程序执行的未来某个时间点才执行。
例如在事件处理排序,等一些原生JS方法也会让我们传递回调函数。

JavaScript的重要特征之一是可以在表达式出现的任意位置创建函数,除此之外这种方式能使代码更紧凑和易于理解(把函数定义放在函数使用处附近)。当一个函数不会在代码的多处位置被调用时,该特性可以避免用非必需的名字污染全局命名空间。

🚀面试题:函数的定义方式有哪几种

  • 函数声明
function myFun(){ return 1;}

即使定义在后面也可以直接调用

  • 函数表达式
var myFun = myFun(){ return 1;}

如果使用 var 定义会被提升,在定义函数表达式之前访问函数名会得到undefined

对于函数声明来说,函数名是强制性的,而对于函数 表达式来说,函数名则完全是可选的。

  • 箭头函数,ES6新增,一种简洁的语法
const fun = (a,b)=>{return a+b}
const fun = a => a*2 //如果真有一个参数和直接返回可以这样简写
  • 函数构造函数——不太常用,以字符串的形式动态创建函数
new Function('a', 'b', 'return a + b')
  • 生成器函数——ES6新增功能,能让我们创建不同于普通函数的函 数,在应用程序执行过程中,这种函数能够退出再重新进入,在这 些再进入之间保留函数内变量的值。我们可以定义生成器版本的函 数声明、函数表达式、函数构造函数。
function* myGen(){ yield 1; }

🚀面试题:什么是形参和实参

  • 形参(parameter)是我们定义函数时所列举的变量。
  • 实参(argument)是我们调用函数时所传递给函数的值。

image.png

🚀面试题: ES6中的剩余参数和默认参数

function multiMax(first, ...remainingNumbers) { //剩余参数以...作前缀

remainingNumbers叫做剩余参数数组,包含传入的剩余参数。
限制:只能放在参数列表的最后

function performAction(ninja, action = "skulking"){

像很多其它的语言一样,可以指定参数的默认值,当为传入参数时,使用默认值

2.2 函数基础:调用和上下文

这部分内容主要包括,理解函数中的两个隐藏参数argumentsthis,函数调用的不同方式和函数的上下文问题。

2.2.1 隐式函数参数

arguments参数是传递给函数的所有参数集合。无论是否有明确定义对应的形参,通过它我们都可以访问到函数的所有参数。由于ES6 引入的剩余参数,对于arguments的使用在慢慢减少。

🚀面试题:arguments 和 ES6中剩余参数的区别

  • arguments 得到的是所有的参数,而ES6中的剩余参数得到的是为匹配的剩余的参数。
  • arguments是一个对象,而剩余参数是一个数组。
  • arguments还有一个特性是它是对象的别名,比如修改arguments[0]会直接修改第一个参数的值。在严格模式下会被禁止这一特性

🚀面试题:什么是严格模式

严格模式是在ES5中引入的特性,它可以改变JavaScript引擎的默认行为并执行更加严格的语法检查,一些在普通模式下的静默错误会在严格模式下抛出异常。在严格模式下部分语言特性会被改变,甚至完全禁用一些不安全的语言特性(后面会详细介绍)

2.2.2 this参数

当调用函数时,除了显式提供的参数外,this参数也会默认地传递给函数。this参数是面向对象JavaScript编程的一个重要组成部分,代 函数调用相关联的对象。因此,通常称之为函数上下文。

在JavaScript中,将一个函数作为方法(method)调用仅仅是函数调用的一种方式。事实上,this参数 的指向不仅是由定义函数的方式和位置决定的,同时还严重受到函数调用方式的影响。

我们要清楚一点每个函数的this是在调用时被绑定的

🚀面试题:this的绑定规则

JavaScript中的this只有如下几种情况,并按他们的优先级从低到高划分如下:

  1. 独立函数调用,例如getUserInfo(),此时this指向全局对象window
  2. 对象调用,例如stu.getStudentName(),此时this指向调用的对象stu
  3. call()apply()bind()改变上下文的方法,this指向取决于这些方法的第一个参数,当第一个参数为null时,this指向全局对象window
  4. 箭头函数没有this,箭头函数里面的this只取决于包裹箭头函数的第一个普通函数的this
  5. new构造函数调用,this永远指向构造函数返回的实例上,优先级最高。

image.png

2.2.3 函数调用

事实上,函数的调用方式对函数内代码的执行有 很大的影响,主要体现在this参数以及函数上下文是如何建立的。

🚀面试题:函数的调用方式有哪些?

  • 作为一个函数(function)——skulk(),直接被调用。 当以这种方式调用时,函数上下文(this关键字的值)有两种可能 性:在非严格模式下,它将是全局上下文(window对象),而在严格 模式下,它将是undefined。

  • 作为一个方法(method)——ninja.skulk(),关联在一个对象上,实现 面向对象编程。
    当一个函数被赋值给一个对象的属性,并且通过对象属性引用的方式调用函数时,函数会作为对象的方法被调用。
    当函数作为某个对象的方法被调用时,该对象会成为函数的上下文,并且在函数内部可以通过参数访问到。这也是JavaScript实现面向对象编程的主要方式之一。

  • 作为一个构造函数(constructor)——new Ninja(),实例化一个新的对象。
    构造函数的声明和其他函数类似,通过可以使用函数声明和函数表达式很容易地构造新的对 象。若要通过构造函数的方式调用,需要在函数调用之前使用关键字 new

  • 通过函数的apply或者call方法——skulk.apply(ninja)或者 skulk.call(ninja)。 JavaScript为我们提供了一种调用函数的方式,从而可以显式地指定 任何对象作为函数的上下文。applycall 正是函数的方法。若想使用apply方法调用函数,需要为其传递两个参数:作为函数上下文的对象和一个数组作为函数调用的参数。call方法的使用方式类 似,不同点在于是直接以参数列表的形式,而不再是作为数组传递。

image.png

🚀面试题:使用new调用函数,会执行哪些操作?

  1. 创建一个全新的空对象
  2. 新对象会被执行原型绑定
  3. 新的空对象会被绑定到该函数的this
  4. 如果函数没有返回其它对象,那么就会自动返回这个新对象

2.2.4 箭头函数

箭头函数和bind方法,在一些情况下可以更优雅地实现相同的效果。
箭头函数没有单独的this值。箭头函数的this与声明所在的上下文的相同
函数还可访问bind方法创建新函数。无论使用哪种方法 调用,bind方法创建的新函数与原始函数的函数体相同,新函数被绑定 到指定的对象上。

🚀面试题:call(),apply(),bind()的区别

🚀面试题:普通函数与箭头函数的区别

  • this指向不同,普通函数的this是在调用时确定,而箭头函数的this在声明时就已经确定
  • 箭头函数没有原型prototype
  • 箭头函数不能当成一个构造函数
  • 箭头函数处于全局作用域中,则没有arguments,处于普通函数的函数作用域中,arguments则是上层普通函数的arguments,可以使用剩余参数列表
  • 箭头函数不能重复函数参数名称
  • 声明方式不同,箭头函数只能声明成匿名,然后使用表达式的方式指定别名

2.3 函数进阶:闭包和作用域

2.3.1 理解闭包

闭包是基于词法作用域写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据自己的意愿来识别、拥抱和影响闭包的思维环境。

闭包允许函数访问并操作函数外部的变量。只要变量或函数存在于声明函数时的作用域内,闭包即可使函数能够访问这些变量或函数。所声明的函数可以在声明之后的任何时间被调用,甚至当该函数声明的作用域消失之后仍然可以调用。

image.png 这就是闭包。闭包创建了被定义时的作用域内的变量和函数的安全气泡,因此函数获得了执行时所需的内容。该气泡与函数本身一起包含了函数和变量。
每一个通过闭包 访问变量的函数都具有一个作用域链,作用域链包含闭包的全部信息, 这一点非常重要。因此,虽然闭包是非常有用的,但不能过度使用。使 用闭包时,所有的信息都会存储在内存中,直到JavaScript引擎确保这些 信息不再使用(可以安全地进行垃圾回收)或页面卸载时,才会清理这 些信息。

2.3.2 使用闭包

原生JavaScript不支持私有变量。但是,通过使用闭包,我们可以实现很接近的、可接受的私有变量。

🚀面试题:如何通过闭包实现私有变量

function Ninja() {
    var feints = 0; 
    this.getFeints = function() { 
        return feints;
    };
    this.feint = function() { 
        feints++;
    };
}

image.png 在构造器内部,我们定义了一个变量feints用于保存状态。由JavaScript的作用域规则的限制,因此只能在构造器内部访问该变量。为了让作用域外部的代码能够访问该变量,我们定义了访问该变量的方法 getFeints。该方法可以读取私有变量,但不能改写私有变量。

2.3.3 函数执行上下文

JavaScript代码有两种类型:一种是全局代码,在所有函数外部定义;一种是函数代码,位于函数内部。JavaScript 引擎执行代码时,每一条语句都处于特定的执行上下文中。
既然具有两种类型的代码,那么就有两种执行上下文:全局执行上下文和函数执行上下文。二者最重要的差别是:全局执行上下文只有一个,当JavaScript程序开始执行时就已经创建了全局上下文;而函数执行上下文是在每次调用函数时,就会创建一个新的。

2.3.4 使用词法环境跟踪变量的作用域

词法环境是JavaScript作用域的内部实现机制,人们通常称为作用域(scopes)。

词法环境(lexical environment)是JavaScript引擎内部用来跟踪标识 符与特定变量之间的映射关系。
词法环境与特定的JavaScript代码结构关联,既可以是一个函数、一段代码片段,也可以是try-catch语句。这些代码结构(函数、代码片段、try-catch)可以具有独立的标识符映射表
对于下列函数

image.png 无论何时调用函数,都会创建一个新的执行环境,被推入执行上下文栈。此外,还会创建一个与之相关联的词法环境。现在来看最重要的部分:外部环境与新建的词法环境,JavaScript引擎将调用函数的内置 [[Environment]]属性与创建函数时的环境进行关联。类似原型链[[Prototype]]的关联。

image.png

🚀面试题:let,var和const的区别

  1. var声明的变量会提升到作用域的顶部,而letconst不会进行提升
  2. var声明的全局变量会被挂载到全局window对象上,而letconst不会
  3. var可以重复声明同一个变量,而letconst不可以
  4. var声明的变量作用域范围是函数作用域,而letconst声明的变量作用域范围是块级作用域。
  5. const声明的常量,一旦声明则不能再次赋值,再次赋值会报错(更改对象属性不会,因为对象地址没有变)

2.4 函数高级:生成器和promise

2.4.1 生成器

通过在function关键字后增加一个*号可以定义生成器,生成器(generator)函数能生成一组值的序列,但每个值的 生成是基于每次请求,并不同于标准函数那样立即生成。 我们必须显式地向生成器请求一个新的值,随后生成器要么响应一个新生成的值,要么就告诉我们它之后都不会再生成新值。 生成器函数和标准函数非常不同,调用生成器并不会执行生成器函数,相反,它会创建一个叫作迭代器(iterator) 的对象。

使用Promise

ES6 引入了一个新的概念,用 于更简单地处理异步任务:promise
promise对象用于作为异步任务结果的占位符。它代表了一个我们 暂时还没获得但在未来有希望获得的值。基于这点原因,在一个 promise对象的整个生命周期中,它会经历多种状态。一个promise对象从等待(pending)状态开始,此时我们对承诺的值一无所知。因此一个等待状态的promise对象也称为未实现(unresolved)的 promise。在程序执行的过程中,如果promise的resolve函数被调用, promise就会进入完成(fulfilled)状态,在该状态下我们能够成功获取到承诺的值。

image.png

3.理解对象

4.理解浏览器环境

文档对象模型(Document Object Model),简称 DOM,将所有页面内容表示为可以修改的对象。
浏览器对象模型(Browser Object Model),简称 BOM,表示由浏览器(主机环境)提供的用于处理文档(document)之外的所有内容的其他对象。

4.1文档对象模型DOM

HTML 文档对主干是标签,每个标签都是一个对象,标签内还可以嵌套子标签,也是一个对象。
我们可以就通过 JS 来访问这些对象,例如更改背景颜色document.body.style.background = 'red'; // 将背景设置为红色
对 DOM 的所有操作都是以 document 对象开始。它是 DOM 的主“入口点”。

4.1.1 遍历 DOM

  • 子节点 对应的是直系的子元素。换句话说,它们被完全嵌套在给定的元素中。。
  • 子孙元素 嵌套在给定元素中的所有元素,包括子元素,以及子元素的子元素等。
const cs = document.body.childNodes // 获取body的所有子元素 
document.body.firstChild  // 第一个子元素
document.body.lastChild  // 最后一个子元素
document.body.hasChildNodes()  // 检查是否有子元素

cs 是可迭代对象,而不是一个数组。可以使用for...of遍历,而不可可以使用 for...in
在 childNodes 中我们可以看到文本节点,元素节点,甚至包括注释节点(如果它们存在的话)。

  • 兄弟节点(sibling)具有相同的父节点
  • 父节点元素的父节点
document.head.nextSibling  // 下一个兄弟节点
document.body.previousSibling  // 上一个兄弟节点
document.body.parentNode   // 父节点
  • 纯元素形成页面结构的元素节点(不包括文本、注释等)只需要在上面等方法中加入Element
    • children —— 仅那些作为元素节点的子代的节点。
    • firstElementChildlastElementChild —— 第一个和最后一个子元素。
    • previousElementSiblingnextElementSibling —— 兄弟元素。
    • parentElement —— 父元素。

4.1.2 搜索 DOM

  • document.getElementById(id)获取指定id的元素(id唯一)
  • elem.getElementsByTagName(tag) 查找具有给定标签的元素,并返回它们的集合。
  • elem.getElementsByClassName(className) 返回具有给定CSS类的元素。
  • document.getElementsByName(name) 返回在文档范围内具有给定 name 特性的元素。
  • 通用的方法是 elem.querySelectorAll(css),它返回 elem 中与给定 CSS 选择器匹配的所有元素。
  • elem.querySelector(css) 调用会返回给定 CSS 选择器的第一个元素。
  • elem.matches(css) 不会查找任何内容,它只会检查 elem 是否与给定的 CSS 选择器匹配。它返回 true 或 false
  • elem.closest(css) 方法会查找与 CSS 选择器匹配的最近的祖先。

image.png

4.1.3 DOM的特性和属性(attribute,property)

  • 特性(attribute)—— 写在 HTML 中的内容。

  • 属性(property)—— DOM 对象中的内容。

  • elem.hasAttribute(name) —— 检查是否存在这个特性。

  • elem.getAttribute(name) —— 获取这个特性值。

  • elem.setAttribute(name, value) —— 设置这个特性值。

  • elem.removeAttribute(name) —— 移除这个特性。

  • elem.attributes —— 所有特性的集合。

在大多数情况下,最好使用 DOM 属性。仅当 DOM 属性无法满足开发需求,并且我们真的需要特性时,才使用特性。

4.1.4 修改DOM

创建节点

创建元素节点:document.createElement(tag)
创建文本节点:document.createTextNode(text)

插入节点

  • node.append(...nodes or strings) —— 在 node 末尾 插入节点或字符串,
  • node.prepend(...nodes or strings) —— 在 node 开头 插入节点或字符串,
  • node.before(...nodes or strings) —— 在 node 前面 插入节点或字符串,
  • node.after(...nodes or strings) —— 在 node 后面 插入节点或字符串,
  • node.replaceWith(...nodes or strings) —— 将 node 替换为给定的节点或字符串。 image.png

节点移除

想要移除一个节点,可以使用 node.remove()

克隆节点

调用 elem.cloneNode(true) 来创建元素的一个“深”克隆 —— 具有所有特性(attribute)和子元素。如果我们调用 elem.cloneNode(false),那克隆就不包括子元素。

DocumentFragment

DocumentFragment 是一个特殊的 DOM 节点,用作来传递节点列表的包装器(wrapper)。

我们可以向其附加其他节点,但是当我们将其插入某个位置时,则会插入其内容。

4.2 浏览器事件

4.2.1 基本介绍

  • HTML 特性
<input value="Click me" onclick="alert('Click!')" type="button">
  • DOM 属性
elem.onclick = function() { alert('Thank you'); };
  • addEventListener
element.addEventListener(event, handler[, options]);
// 移除绑定事件
element.removeEventListener(event, handler[, options]);

event:事件名,例如 "click"
handler:事件处理的回调函数,handler也可以是一个对象,会自动调用这个对象的handleEvent方法。
options:once|capture|passive

  • 事件对象,回调函数默认接受一个参数 event
    • event.type:获取事件类型,如"click"
    • event.currentTarget:处理事件的 DOM 元素
    • event.clientX | clientY:鼠标指针相对窗口的坐标
    • event.target 引发事件的层级最深的元素。
    • event.eventPhase 当前阶段(capturing=1,target=2,bubbling=3)。

4.2.2 事件冒泡与捕获

冒泡(bubbling)当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。 image.png 比如我们这里有3层嵌套,且各自拥有一个处理程序

<form onclick="alert('form')">FORM 
    <div onclick="alert('div')">DIV 
        <p onclick="alert('p')">P</p>
    </div> 
</form>

点击内部的 <p> 会首先运行 onclick

  1. 在该 <p> 上的。
  2. 然后是外部 <div> 上的。
  3. 然后是外部 <form> 上的。
  4. 以此类推,直到最后的 document 对象。 停止冒泡
  • event.stopPropagation()阻止此事件向上冒泡,其它事件不影响
  • event.stopImmediatePropagation()停止所有程序向上冒泡

捕获(capturing) DOM 事件标准描述了事件传播的 3 个阶段:

  1. 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
  2. 目标阶段(Target phase)—— 事件到达目标元素。
  3. 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。

image.png
点击 <td>,事件首先通过祖先链向下到达元素(捕获阶段),然后到达目标(目标阶段),最后上升(冒泡阶段),在途中调用处理程序。 我们可以指定事件处理程序是在捕获阶段处理还是冒泡阶段处理

elem.addEventListener(..., {capture: true})
//如果为 `false`(默认值),则在冒泡阶段设置处理程序。
//如果为 `true`,则在捕获阶段设置处理程序。

4.2.3 事件委托

如果我们有许多以类似方式处理的元素,那么就不必为每个元素分配一个处理程序 —— 而是将单个处理程序放在它们的共同祖先上。 我们获取 event.target 以查看事件实际发生的位置并进行处理。

🚀面试题:时间委托的处理流程,及其优缺点。

利用事件委托的处理流程

  1. 在容器(container)上放一个处理程序。
  2. 在处理程序中 —— 检查源元素 event.target
  3. 如果事件发生在我们感兴趣的元素内,那么处理该事件。

优点

  • 简化初始化并节省内存:无需添加许多处理程序。
  • 更少的代码:添加或移除元素时,无需添加/移除处理程序。
  • DOM 修改 :我们可以使用 innerHTML 等,来批量添加/移除元素。

局限性

  • 首先,事件必须冒泡。而有些事件不会冒泡。此外,低级别的处理程序不应该使用 event.stopPropagation()
  • 其次,委托可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应,而不管我们是否对该事件感兴趣。但是,通常负载可以忽略不计,所以我们不考虑它。

4.2.4 浏览器的默认行为

许多事件会自动触发浏览器执行某些行为。

  • 点击一个链接 —— 触发导航(navigation)到该 URL。
  • 点击表单的提交按钮 —— 触发提交到服务器的行为。
  • 在文本上按下鼠标按钮并移动 —— 选中文本。

addEventListener 的 passive: true 选项告诉浏览器该行为不会被阻止。

阻止浏览器的默认行为

  • event.preventDefault()阻止浏览器的默认行为
  • return false。只适用于通过 on<event> 分配的处理程序。

4.2.5 创建自定义事件

我们不仅可以分配事件处理程序,还可以从 JavaScript 生成事件。自定义事件可用于创建“图形组件”。
构造默认事件

let event = new Event(type[, options]);

构造自定义事件

let event = new CustomEvent(type[, options]);
  • bubbles: true/false —— 如果为 true,那么事件会冒泡。
  • cancelable: true/false —— 如果为 true,那么“默认行为”就会被阻止。

默认情况下,以上两者都为 false:{bubbles: false, cancelable: false}
dispatchEvent 事件对象被创建后,我们应该使用 elem.dispatchEvent(event) 调用在元素上“运行”它。

对于来自真实用户操作的事件,event.isTrusted 属性为 true,对于脚本生成的事件,event.isTrusted 属性为 false

自定义事件

// 事件附带给处理程序的其他详细信息 
elem.addEventListener("hello", function(event) { 
    alert(*event.detail.name*); 
}); 
elem.dispatchEvent(new CustomEvent("hello", { 
    detail: { name: "John" }
}));

CustomEvent 提供了特殊的 detail 字段,以避免与其他事件属性的冲突。