面试题笔记——JS篇

29 阅读8分钟

什么是原型/原型链?

原型的本质就是一个对象

当我们在创建一个构造函数之后,这个函数会默认带上一个prototype属性,而这个属性的值就指向这个函数的原型对象。

这个原型对象是用来为通过该构造函数创建的实例对象提供共享属性,即用来实现基于原型的继承和属性的共享

所以我们通过构造函数创建的实例对象都会从这个函数的原型对象上继承上面具有的属性

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止(最顶层就是Object.prototype的原型,值为null)。

所以通过原型一层层相互关联的链状结构就称为原型链

什么是闭包?

定义:闭包是指引用了其他函数作用域中变量的函数,通常是在嵌套函数中实现的。

从技术角度上所有 js 函数都是闭包。

从实践角度来看,满足以下俩个条件的函数算闭包

  1. 即使创建它的上下文被销毁了,它依然存在。(比如从父函数中返回)
  2. 在代码中引用了自由变量(在函数中使用的既不是函数参数也不是函数局部变量的变量称作自由变量)

使用场景:

  • 创建私有变量

    vue 中的data,需要是一个闭包,保证每个data中数据唯一,避免多次引用该组件造成的data共享

  • 延长变量的生命周期

    一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的

应用

  • 柯里化函数
  • 例如计数器、延迟调用、回调函数等

this 的指向

在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)

1、全局的this非严格模式指向window对象,严格模式指向 undefined

2、对象的属性方法中的this 指向对象本身

3、apply、call、bind 可以变更 this 指向为第一个传参

4、箭头函数中的this指向它的父级作用域,它自身不存在 this

浏览器的事件循环?

js 代码执行过程中,会创建对应的执行上下文并压入执行上下文栈中。

如果遇到异步任务就会将任务挂起,交给其他线程去处理异步任务,当异步任务处理完后,会将回调结果加入事件队列中。

当执行栈中所有任务执行完毕后,就是主线程处于闲置状态时,才会从事件队列中取出排在首位的事件回调结果,并把这个回调加入执行栈中然后执行其中的代码,如此反复,这个过程就被称为事件循环。

事件队列分为了宏任务队列和微任务队列,在当前执行栈为空时,主线程回先查看微任务队列是否有事件存在,存在则依次执行微任务队列中的事件回调,直至微任务队列为空;不存在再去宏任务队列中处理。

常见的宏任务有setTimeout()setInterval()setImmediate()、I/O、用户交互操作,UI渲染

常见的微任务有promise.then()promise.catch()new MutationObserverprocess.nextTick()

宏任务和微任务的本质区别

  1. 宏任务有明确的异步任务需要执行和回调,需要其他异步线程支持
  2. 微任务没有明确的异步任务需要执行,只有回调,不需要其他异步线程支持。

javascript中数据在栈和堆中的存储方式

1、基本数据类型大小固定且操作简单,所以放入栈中存储

2、引用数据类型大小不确定,所以将它们放入堆内存中,让它们在申请内存的时候自己确定大小

3、这样分开存储可以使内存占用最小。栈的效率高于堆

4、栈内存中变量在执行环境结束后会立即进行垃圾回收,而堆内存中需要变量的所有引用都结束才会被回收

讲讲v8垃圾回收

1、根据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同分代采用不同的回收算法

2、新生代采用空间换时间的 scavenge 算法:整个空间分为两块,变量仅存在其中一块,回收的时候将存活变量复制到另一块空间,不存活的回收掉,周而复始轮流操作

3、老生代使用标记清除和标记整理,标记清除:遍历所有对象标记标记可以访问到的对象(活着的),然后将不活的当做垃圾进行回收。回收完后避免内存的断层不连续,需要通过标记整理将活着的对象往内存一端进行移动,移动完成后再清理边界内存

函数调用的方法

1、普通function直接使用()调用并传参,如:function test(x, y) { return x + y}test(3, 4)

2、作为对象的一个属性方法调用,如:const obj = { test: function (val) { return val } }, obj.test(2)

3、使用callapply调用,更改函数 this 指向,也就是更改函数的执行上下文

4、new可以间接调用构造函数生成对象实例

defer和async的区别

一般情况下,当执行到 script 标签时会进行下载 + 执行两步操作,这两步会阻塞 HTML 的解析;

async 和 defer 能将script的下载阶段变成异步执行(和 html解析同步进行);

async下载完成后会立即执行js,此时会阻塞HTML解析;

defer会等全部HTML解析完成且在DOMContentLoaded 事件之前执行。

浏览器事件机制

DOM 事件流三阶段:

  1. 捕获阶段:事件最开始由不太具体的节点最早接受事件, 而最具体的节点(触发节点)最后接受事件。为了让事件到达最终目标之前拦截事件。

    比如点击一个div,则 click 事件会按这种顺序触发: document => <html> => <body> => <div>,即由 document 捕获后沿着 DOM 树依次向下传播,并在各节点上触发捕获事件,直到到达实际目标元素。

  2. 目标阶段

    当事件到达目标节点的,事件就进入了目标阶段。事件在目标节点上被触发(执行事件对应的函数),然后会逆向回流,直到传播至最外层的文档节点。

  3. 冒泡阶段

    事件在目标元素上触发后,会继续随着 DOM 树一层层往上冒泡,直到到达最外层的根节点。

所有事件都要经历捕获阶段和目标阶段,但有些事件会跳过冒泡阶段,比如元素获得焦点 focus 和失去焦点 blur 不会冒泡

扩展一

e.target 和 e.currentTarget 区别?

  • e.target 指向触发事件监听的对象。
  • e.currentTarget 指向添加监听事件的对象。

例如:

<ul>
  <li><span>hello 1</span></li>
</ul>
​
let ul = document.querySelectorAll('ul')[0]
let aLi = document.querySelectorAll('li')
ul.addEventListener('click',function(e){
  let oLi1 = e.target  
  let oLi2 = e.currentTarget
  console.log(oLi1)   //  被点击的li
  console.log(oLi2)   // ul
  console.og(oLi1===oLi2)  // false
})
复制代码

给 ul 绑定了事件,点击其中 li 的时候,target 就是被点击的 li, currentTarget 就是被绑定事件的 ul

事件冒泡阶段(上述例子),e.currenttargete.target是不相等的,但是在事件的目标阶段,e.currenttargete.target是相等的

作用:

e.target可以用来实现事件委托,该原理是通过事件冒泡(或者事件捕获)给父元素添加事件监听,e.target指向引发触发事件的元素

扩展二

addEventListener 参数

语法:

addEventListener(type, listener);
addEventListener(type, listener, options || useCapture);
复制代码
  • type: 监听事件的类型,如:'click'/'scroll'/'focus'

  • listener: 必须是一个实现了 EventListener 接口的对象,或者是一个函数。当监听的事件类型被触发时,会执行

  • options:指定 listerner 有关的可选参数对象

    • capture: 布尔值,表示 listener 是否在事件捕获阶段传播到 EventTarget 时触发
    • once:布尔值,表示 listener 添加之后最多调用一次,为 true 则 listener 在执行一次后会移除
    • passive: 布尔值,表示 listener 永远不会调用 preventDefault()
    • signal:可选,AbortSignal,当它的abort()方法被调用时,监听器会被移除
  • useCapture:布尔值,默认为 false,listener 在事件冒泡阶段结束时执行,true 则表示在捕获阶段开始时执行。作用就是更改事件作用的时机,方便拦截/不被拦截。