美团前端面试题集锦_2023-02-28

2023-02-28 11:33:18 浏览数 (2)

HTML5有哪些更新

1. 语义化标签
  • header:定义文档的页眉(头部);
  • nav:定义导航链接的部分;
  • footer:定义文档或节的页脚(底部);
  • article:定义文章内容;
  • section:定义文档中的节(section、区段);
  • aside:定义其所处内容之外的内容(侧边);
2. 媒体标签

(1) audio:音频

代码语言:html复制
<audio src='' controls autoplay loop='true'></audio>

属性:

  • controls 控制面板
  • autoplay 自动播放
  • loop=‘true’ 循环播放

(2)video视频

代码语言:html复制
<video src='' poster='imgs/aa.jpg' controls></video>

属性:

  • poster:指定视频还没有完全下载完毕,或者用户还没有点击播放前显示的封面。默认显示当前视频文件的第一针画面,当然通过poster也可以自己指定。
  • controls 控制面板
  • width
  • height

(3)source标签

因为浏览器对视频格式支持程度不一样,为了能够兼容不同的浏览器,可以通过source来指定视频源。

代码语言:html复制
<video>
     <source src='aa.flv' type='video/flv'></source>
     <source src='aa.mp4' type='video/mp4'></source>
</video>
3. 表单

表单类型:

  • email :能够验证当前输入的邮箱地址是否合法
  • url : 验证URL
  • number : 只能输入数字,其他输入不了,而且自带上下增大减小箭头,max属性可以设置为最大值,min可以设置为最小值,value为默认值。
  • search : 输入框后面会给提供一个小叉,可以删除输入的内容,更加人性化。
  • range : 可以提供给一个范围,其中可以设置max和min以及value,其中value属性可以设置为默认值
  • color : 提供了一个颜色拾取器
  • time : 时分秒
  • data : 日期选择年月日
  • datatime : 时间和日期(目前只有Safari支持)
  • datatime-local :日期时间控件
  • week :周控件
  • month:月控件

表单属性:

  • placeholder :提示信息
  • autofocus :自动获取焦点
  • autocomplete=“on” 或者 autocomplete=“off” 使用这个属性需要有两个前提:
    • 表单必须提交过
    • 必须有name属性。
  • required:要求输入框不能为空,必须有值才能够提交。
  • pattern=" " 里面写入想要的正则模式,例如手机号patte="^( 86)?d{10}$"
  • multiple:可以选择多个文件或者多个邮箱
  • form=" form表单的ID"

表单事件:

  • oninput 每当input里的输入框内容发生变化都会触发此事件。
  • oninvalid 当验证不通过时触发此事件。
4. 进度条、度量器
  • progress标签:用来表示任务的进度(IE、Safari不支持),max用来表示任务的进度,value表示已完成多少
  • meter属性:用来显示剩余容量或剩余库存(IE、Safari不支持)
    • high/low:规定被视作高/低的范围
    • max/min:规定最大/小值
    • value:规定当前度量值

设置规则:min < low < high < max

5.DOM查询操作
  • document.querySelector()
  • document.querySelectorAll()

它们选择的对象可以是标签,可以是类(需要加点),可以是ID(需要加#)

6. Web存储

HTML5 提供了两种在客户端存储数据的新方法:

  • localStorage - 没有时间限制的数据存储
  • sessionStorage - 针对一个 session 的数据存储
7. 其他
  • 拖放:拖放是一种常见的特性,即抓取对象以后拖到另一个位置。设置元素可拖放:
代码语言:html复制
<img draggable="true" />
  • 画布(canvas ): canvas 元素使用 JavaScript 在网页上绘制图像。画布是一个矩形区域,可以控制其每一像素。canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。
代码语言:html复制
<canvas id="myCanvas" width="200" height="100"></canvas>
  • SVG:SVG 指可伸缩矢量图形,用于定义用于网络的基于矢量的图形,使用 XML 格式定义图形,图像在放大或改变尺寸的情况下其图形质量不会有损失,它是万维网联盟的标准
  • 地理定位:Geolocation(地理定位)用于定位用户的位置。‘

总结: (1)新增语义化标签:nav、header、footer、aside、section、article

(2)音频、视频标签:audio、video

(3)数据存储:localStorage、sessionStorage

(4)canvas(画布)、Geolocation(地理定位)、websocket(通信协议)

(5)input标签新增属性:placeholder、autocomplete、autofocus、required

(6)history API:go、forward、back、pushstate

移除的元素有:

  • 纯表现的元素:basefont,big,center,font, s,strike,tt,u;
  • 对可用性产生负面影响的元素:frame,frameset,noframes;

写代码:实现函数能够深度克隆基本类型

浅克隆:

代码语言:javascript复制
function shallowClone(obj) {
  let cloneObj = {};

  for (let i in obj) {
    cloneObj[i] = obj[i];
  }

  return cloneObj;
}

深克隆:

  • 考虑基础类型
  • 引用类型
    • RegExp、Date、函数 不是 JSON 安全的
    • 会丢失 constructor,所有的构造函数都指向 Object
    • 破解循环引用
代码语言:javascript复制
function deepCopy(obj) {
  if (typeof obj === 'object') {
    var result = obj.constructor === Array ? [] : {};

    for (var i in obj) {
      result[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i];
    }
  } else {
    var result = obj;
  }

  return result;
}

HTTP状态码304是多好还是少好

服务器为了提高网站访问速度,对之前访问的部分页面指定缓存机制,当客户端在此对这些页面进行请求,服务器会根据缓存内容判断页面与之前是否相同,若相同便直接返回304,此时客户端调用缓存内容,不必进行二次下载。

状态码304不应该认为是一种错误,而是对客户端有缓存情况下服务端的一种响应。

搜索引擎蜘蛛会更加青睐内容源更新频繁的网站。通过特定时间内对网站抓取返回的状态码来调节对该网站的抓取频次。若网站在一定时间内一直处于304的状态,那么蜘蛛可能会降低对网站的抓取次数。相反,若网站变化的频率非常之快,每次抓取都能获取新内容,那么日积月累,的回访率也会提高。

产生较多304状态码的原因:

  • 页面更新周期长或不更新
  • 纯静态页面或强制生成静态html

304状态码出现过多会造成以下问题:

  • 网站快照停止;
  • 收录减少;
  • 权重下降。

代码输出结果

代码语言:javascript复制
var a, b
(function () {
   console.log(a);
   console.log(b);
   var a = (b = 3);
   console.log(a);
   console.log(b);   
})()
console.log(a);
console.log(b);

输出结果:

代码语言:javascript复制
undefined 
undefined 
3 
3 
undefined 
3

这个题目和上面题目考察的知识点类似,b赋值为3,b此时是一个全局变量,而将3赋值给a,a是一个局部变量,所以最后打印的时候,a仍旧是undefined。

说下对 JS 的了解吧

是基于原型的动态语言,主要独特特性有 this、原型和原型链。

JS 严格意义上来说分为:语言标准部分(ECMAScript) 宿主环境部分

语言标准部分

2015 年发布 ES6,引入诸多新特性使得能够编写大型项目变成可能,标准自 2015 之后以年号代号,每年一更

宿主环境部分

  • 在浏览器宿主环境包括 DOM BOM 等
  • 在 Node,宿主环境包括一些文件、数据库、网络、与操作系统的交互等

代码输出结果

代码语言:javascript复制
function foo() {
  console.log( this.a );
}

function doFoo() {
  foo();
}

var obj = {
  a: 1,
  doFoo: doFoo
};

var a = 2; 
obj.doFoo()

输出结果:2

在Javascript中,this指向函数执行时的当前对象。在执行foo的时候,执行环境就是doFoo函数,执行环境为全局。所以,foo中的this是指向window的,所以会打印出2。

vue-router

代码语言:text复制
vue-router是vuex.js官方的路由管理器,它和vue.js的核心深度集成,让构建但页面应用变得易如反掌

<router-link> 组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to 属性指定目标地址

<router-view> 组件是一个 functional 组件,渲染路径匹配到的视图组件。

<keep-alive> 组件是一个用来缓存组件

router.beforeEach

router.afterEach

to: Route: 即将要进入的目标 路由对象

from: Route: 当前导航正要离开的路由

next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。

介绍了路由守卫及用法,在项目中路由守卫起到的作用等等

Cookie、LocalStorage、SessionStorage区别

浏览器端常用的存储技术是 cookie 、localStorage 和 sessionStorage。

  • cookie: 其实最开始是服务器端用于记录用户状态的一种方式,由服务器设置,在客户端存储,然后每次发起同源请求时,发送给服务器端。cookie 最多能存储 4 k 数据,它的生存时间由 expires 属性指定,并且 cookie 只能被同源的页面访问共享。
  • sessionStorage: html5 提供的一种浏览器本地存储的方法,它借鉴了服务器端 session 的概念,代表的是一次会话中所保存的数据。它一般能够存储 5M 或者更大的数据,它在当前窗口关闭后就失效了,并且 sessionStorage 只能被同一个窗口的同源页面所访问共享。
  • localStorage: html5 提供的一种浏览器本地存储的方法,它一般也能够存储 5M 或者更大的数据。它和 sessionStorage 不同的是,除非手动删除它,否则它不会失效,并且 localStorage 也只能被同源页面所访问共享。

上面几种方式都是存储少量数据的时候的存储方式,当需要在本地存储大量数据的时候,我们可以使用浏览器的 indexDB 这是浏览器提供的一种本地的数据库存储机制。它不是关系型数据库,它内部采用对象仓库的形式存储数据,它更接近 NoSQL 数据库。

懒加载与预加载的区别

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。
  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。

如何优化动画?

对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作DOM,就就会导致页面的性能问题,我们可以将动画的position属性设置为absolute或者fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。

HTML5的离线储存怎么使用,它的工作原理是什么

离线存储指的是:在用户没有与因特网连接时,可以正常访问站点或应用,在用户与因特网连接时,更新用户机器上的缓存文件。

原理:HTML5的离线存储是基于一个新建的 .appcache 文件的缓存机制(不是存储技术),通过这个文件上的解析清单离线存储资源,这些资源就会像cookie一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示

使用方法: (1)创建一个和 html 同名的 manifest 文件,然后在页面头部加入 manifest 属性:

代码语言:html复制
<html lang="en" manifest="index.manifest">

(2)在 cache.manifest 文件中编写需要离线存储的资源:

代码语言:html复制
CACHE MANIFEST
    #v0.11
    CACHE:
    js/app.js
    css/style.css
    NETWORK:
    resourse/logo.png
    FALLBACK:
    / /offline.html
  • CACHE: 表示需要离线存储的资源列表,由于包含 manifest 文件的页面将被自动离线存储,所以不需要把页面自身也列出来。
  • NETWORK: 表示在它下面列出来的资源只有在在线的情况下才能访问,他们不会被离线存储,所以在离线情况下无法使用这些资源。不过,如果在 CACHE 和 NETWORK 中有一个相同的资源,那么这个资源还是会被离线存储,也就是说 CACHE 的优先级更高。
  • FALLBACK: 表示如果访问第一个资源失败,那么就使用第二个资源来替换他,比如上面这个文件表示的就是如果访问根目录下任何一个资源失败了,那么就去访问 offline.html 。

(3)在离线状态时,操作 window.applicationCache 进行离线缓存的操作。

如何更新缓存:

(1)更新 manifest 文件

(2)通过 javascript 操作

(3)清除浏览器缓存

注意事项:

(1)浏览器对缓存数据的容量限制可能不太一样(某些浏览器设置的限制是每个站点 5MB)。

(2)如果 manifest 文件,或者内部列举的某一个文件不能正常下载,整个更新过程都将失败,浏览器继续全部使用老的缓存。

(3)引用 manifest 的 html 必须与 manifest 文件同源,在同一个域下。

(4)FALLBACK 中的资源必须和 manifest 文件同源。

(5)当一个资源被缓存后,该浏览器直接请求这个绝对路径也会访问缓存中的资源。

(6)站点中的其他页面即使没有设置 manifest 属性,请求的资源如果在缓存中也从缓存中访问。

(7)当 manifest 文件发生改变时,资源请求本身也会触发更新。

代码输出结果

代码语言:javascript复制
var A = {n: 4399};
var B =  function(){this.n = 9999};
var C =  function(){var n = 8888};
B.prototype = A;
C.prototype = A;
var b = new B();
var c = new C();
A.n  
console.log(b.n);
console.log(c.n);

输出结果:9999 4400

解析:

  1. console.log(b.n),在查找b.n是首先查找 b 对象自身有没有 n 属性,如果没有会去原型(prototype)上查找,当执行var b = new B()时,函数内部this.n=9999(此时this指向 b) 返回b对象,b对象有自身的n属性,所以返回 9999。
  2. console.log(c.n),同理,当执行var c = new C()时,c对象没有自身的n属性,向上查找,找到原型 (prototype)上的 n 属性,因为 A.n (此时对象A中的n为4400), 所以返回4400。

浏览器的渲染过程

浏览器渲染主要有以下步骤:

  • 首先解析收到的文档,根据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。
  • 然后对 CSS 进行解析,生成 CSSOM 规则树。
  • 根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。
  • 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
  • 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。

大致过程如图所示:

注意: 这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

代码输出结果

代码语言:javascript复制
// a
function Foo () {
 getName = function () {
   console.log(1);
 }
 return this;
}
// b
Foo.getName = function () {
 console.log(2);
}
// c
Foo.prototype.getName = function () {
 console.log(3);
}
// d
var getName = function () {
 console.log(4);
}
// e
function getName () {
 console.log(5);
}

Foo.getName();           // 2
getName();               // 4
Foo().getName();         // 1
getName();               // 1 
new Foo.getName();       // 2
new Foo().getName();     // 3
new new Foo().getName(); // 3

输出结果:2 4 1 1 2 3 3

解析:

  1. Foo.getName(), Foo为一个函数对象,对象都可以有属性,b 处定义Foo的getName属性为函数,输出2;
  2. getName(), 这里看d、e处,d为函数表达式,e为函数声明,两者区别在于变量提升,函数声明的 5 会被后边函数表达式的 4 覆盖;
  3. Foo().getName(), 这里要看a处,在Foo内部将全局的getName重新赋值为 console.log(1) 的函数,执行Foo()返回 this,这个this指向window,Foo().getName() 即为window.getName(),输出 1;
  4. getName(), 上面3中,全局的getName已经被重新赋值,所以这里依然输出 1;
  5. new Foo.getName(), 这里等价于 new (Foo.getName()),先执行 Foo.getName(),输出 2,然后new一个实例;
  6. new Foo().getName(), 这 里等价于 (new Foo()).getName(), 先new一个Foo的实例,再执行这个实例的getName方法,但是这个实例本身没有这个方法,所以去原型链protot上边找,实例.protot === Foo.prototype,所以输出 3;
  7. new new Foo().getName(), 这里等价于new (new Foo().getName()),如上述6,先输出 3,然后new 一个 new Foo().getName() 的实例。

什么情况会阻塞渲染?

首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。

当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染。

0.1 0.2 === 0.3 嘛?为什么?

JavaScript 使用 Number 类型来表示数字(整数或浮点数),遵循 IEEE 754 标准,通过 64 位来表示一个数字(1 11 52)

  • 1 符号位,0 表示正数,1 表示负数 s
  • 11 指数位(e)
  • 52 尾数,小数部分(即有效数字)

最大安全数字:Number.MAX_SAFE_INTEGER = Math.pow(2, 53) - 1,转换成整数就是 16 位,所以 0.1 === 0.1,是因为通过 toPrecision(16) 去有效位之后,两者是相等的。

在两数相加时,会先转换成二进制,0.1 和 0.2 转换成二进制的时候尾数会发生无限循环,然后进行对阶运算,JS 引擎对二进制进行截断,所以造成精度丢失。

所以总结:精度丢失可能出现在进制转换和对阶运算中

常见的浏览器内核比较

  • Trident: 这种浏览器内核是 IE 浏览器用的内核,因为在早期 IE 占有大量的市场份额,所以这种内核比较流行,以前有很多网页也是根据这个内核的标准来编写的,但是实际上这个内核对真正的网页标准支持不是很好。但是由于 IE 的高市场占有率,微软也很长时间没有更新 Trident 内核,就导致了 Trident 内核和 W3C 标准脱节。还有就是 Trident 内核的大量 Bug 等安全问题没有得到解决,加上一些专家学者公开自己认为 IE 浏览器不安全的观点,使很多用户开始转向其他浏览器。
  • Gecko: 这是 Firefox 和 Flock 所采用的内核,这个内核的优点就是功能强大、丰富,可以支持很多复杂网页效果和浏览器扩展接口,但是代价是也显而易见就是要消耗很多的资源,比如内存。
  • Presto: Opera 曾经采用的就是 Presto 内核,Presto 内核被称为公认的浏览网页速度最快的内核,这得益于它在开发时的天生优势,在处理 JS 脚本等脚本语言时,会比其他的内核快3倍左右,缺点就是为了达到很快的速度而丢掉了一部分网页兼容性。
  • Webkit: Webkit 是 Safari 采用的内核,它的优点就是网页浏览速度较快,虽然不及 Presto 但是也胜于 Gecko 和 Trident,缺点是对于网页代码的容错性不高,也就是说对网页代码的兼容性较低,会使一些编写不标准的网页无法正确显示。WebKit 前身是 KDE 小组的 KHTML 引擎,可以说 WebKit 是 KHTML 的一个开源的分支。
  • Blink: 谷歌在 Chromium Blog 上发表博客,称将与苹果的开源浏览器核心 Webkit 分道扬镳,在 Chromium 项目中研发 Blink 渲染引擎(即浏览器核心),内置于 Chrome 浏览器之中。其实 Blink 引擎就是 Webkit 的一个分支,就像 webkit 是KHTML 的分支一样。Blink 引擎现在是谷歌公司与 Opera Software 共同研发,上面提到过的,Opera 弃用了自己的 Presto 内核,加入 Google 阵营,跟随谷歌一起研发 Blink。

Set,Map解构

代码语言:javascript复制
ES6 提供了新的数据结构 Set。
它类似于数组,但是成员的值都是唯一的,没有重复的值。 Set 本身是一个构造函数,用来生成 Set 数据结构。

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

Vue通信

代码语言:shell复制
1.props和$emit
2.中央事件总线 EventBus(基本不用)
3.vuex(官方推荐状态管理器)
4.$parent和$children
当然还有一些其他办法,但基本不常用,或者用起来太复杂来。 介绍来通信的方式,还可以扩展说一下使用
场景,如何使用,注意事项之类的。

JS闭包,你了解多少?

应该有面试官问过你:

  1. 什么是闭包?
  2. 闭包有哪些实际运用场景?
  3. 闭包是如何产生的?
  4. 闭包产生的变量如何被回收?

这些问题其实都可以被看作是同一个问题,那就是面试官在问你:你对JS闭包了解多少?

来总结一下我听到过的答案,尽量完全复原候选人面试的时候说的原话。

答案1: 就是一个function里面return了一个子函数,子函数访问了外面那个函数的变量。

答案2: for循环里面可以用闭包来解决问题。

代码语言:javascript复制
for(var i = 0; i < 10; i  ){
    setTimeout(()=>console.log(i),0)
}
// 控制台输出10遍10.
for(var i = 0; i < 10; i  ){
    (function(a){
    setTimeout(()=>console.log(a),0)
    })(i)
}
 // 控制台输出0-9

答案3: 当前作用域产产生了对父作用域的引用。

答案4: 不知道。是跟浏览器的垃圾回收机制有关吗?

开杠了。请问,小伙伴的答案和以上的内容有多少相似程度?

其实,拿着这些问题好好想想,你就会发现这些问题都只是为了最终那一个问题。

闭包的底层实现原理

1. JS执行上下文

我们都知道,我们手写的js代码是要经过浏览器V8进行预编译后才能真正的被执行。例如变量提升、函数提升。举个栗子。

代码语言:javascript复制
// 栗子:
var d = 'abc';
function a(){
    console.log("函数a");
};
console.log(a);   // ƒ a(){ console.log("函数a"); }
a();              // '函数a'
var a = "变量a";  
console.log(a);   // '变量a'
a();              // a is not a function
var c = 123;

// 输出结果及顺序:
// ƒ a(){ console.log("函数a"); }
// '函数a'
// '变量a'
// a is not a function

// 栗子预编后相当于:
function a(){
    console.log("函数a");
};
var d;
console.log(a);  // ƒ a(){ console.log("函数a"); }
a();              // '函数a'

a = "变量a";     // 此时变量a赋值,函数声明被覆盖

console.log(a); // "变量a"
a();         // a is not a function

那么问题来了。 请问是谁来执行预编译操作的?那这个谁又是在哪里进行预编译的?

是的,你的疑惑没有错。js代码运行需要一个运行环境,那这个环境就是执行上下文。 是的,js运行前的预编译也是在这个环境中进行。

js执行上下文分三种:

  • 全局执行上下文: 代码开始执行时首先进入的环境。
  • 函数执行上下文:函数调用时,会开始执行函数中的代码。
  • eval执行上下文:不建议使用,可忽略。

那么,执行上下文的周期,分为两个阶段:

  • 创建阶段
    • 创建词法环境
    • 生成变量对象(VO),建立作用域链作用域链作用域链(重要的事说三遍)
    • 确认this指向,并绑定this
  • 执行阶段。这个阶段进行变量赋值,函数引用及执行代码。

你现在猜猜看,预编译是发生在什么时候?

噢,我忘记说了,其实与编译还有另一个称呼:执行期上下文

预编译发生在函数执行之前。预编译四部曲为:

  1. 创建AO对象
  2. 找形参和变量声明,将变量和形参作为AO属性名,值为undefined
  3. 将实参和形参相统一
  4. 在函数体里找到函数声明,值赋予函数体。最后程序输出变量值的时候,就是从AO对象中拿。

所以,预编译真正的结果是:

代码语言:javascript复制
var AO = {
    a = function a(){console.log("函数a");};
    d = 'abc'
}

我们重新来。

1. 什么叫变量对象?

变量对象是 js 代码在进入执行上下文时,js 引擎在内存中建立的一个对象,用来存放当前执行环境中的变量。

2. 变量对象(VO)的创建过程

变量对象的创建,是在执行上下文创建阶段,依次经过以下三个过程:

  • 创建 arguments 对象。

对于函数执行环境,首先查询是否有传入的实参,如果有,则会将参数名是实参值组成的键值对放入arguments 对象中。否则,将参数名和 undefined组成的键值对放入 arguments 对象中。

代码语言:javascript复制
//举个栗子 
function bar(a, b, c) {
    console.log(arguments);  // [1, 2]
    console.log(arguments[2]); // undefined
}
bar(1,2)
  • 当遇到同名的函数时,后面的会覆盖前面的。
代码语言:javascript复制
console.log(a); // function a() {console.log('Is a ?') }
function a() {
    console.log('Is a');
}
function a() {
  console.log('Is a ?')
}

/**ps: 在执行第一行代码之前,函数声明已经创建完成.后面的对之前的声明进行了覆盖。**/
  • 检查当前环境中的变量声明并赋值为undefined。当遇到同名的函数声明,为了避免函数被赋值为 undefined ,会忽略此声明
代码语言:javascript复制
console.log(a); // function a() {console.log('Is a ?') }
console.log(b); // undefined
function a() {
  console.log('Is a ');
}
function a() {
console.log('Is a ?');
}
var b = 'Is b';
var a = 10086;

/**这段代码执行一下,你会发现 a 打印结果仍旧是一个函数,而 b 则是 undefined。**/

根据以上三个步骤,对于变量提升也就知道是怎么回事了。

3. 变量对象变为活动对象

执行上下文的第二个阶段,称为执行阶段,在此时,会进行变量赋值,函数引用并执行其他代码,此时,变量对象变为活动对象。

我们还是举上面的例子:

代码语言:javascript复制
console.log(a); // function a() {console.log('fjdsfs') }
console.log(b); // undefined
function a() {
   console.log('Is a');
}
function a() {
 console.log('Is a?');
}
var b = 'Is b';
console.log(b); // 'Is b'
var a = 10086; 
console.log(a);  // 10086
var b = 'Is b?';
console.log(b); // 'Is b?'

在上面的代码中,代码真正开始执行是从第一行 console.log() 开始的,自这之前,执行上下文是这样的:

代码语言:javascript复制
// 创建过程
EC= {
  VO: {}; // 创建变量对象
  scopeChain: {}; // 作用域链
}
VO = {
  argument: {...}; // 当前为全局上下文,所以这个属性值是空的
  a: <a reference> // 函数 a  的引用地址  b: undefiend  // 见上文创建变量对象的第三步}
词法作用域(Lexical scope

这里想说明,我们在函数执行上下文中有变量,在全局执行上下文中有变量。JavaScript的一个复杂之处在于它如何查找变量,如果在函数执行上下文中找不到变量,它将在调用上下文中寻找它,如果在它的调用上下文中没有找到,就一直往上一级,直到它在全局执行上下文中查找为止。(如果最后找不到,它就是 undefined)。

再来举个栗子:

代码语言:javascript复制
 1: let top = 0; // 
 2: function createWarp() {
 3:   function add(a, b) {
 4:     let ret = a   b
 5:     return ret
 6:   }
 7:   return add
 8: }
 9: let sum = createWarp()
10: let result = sum(top, 8)
11: console.log('result:',result)

分析过程如下:

  • 在全局上下文中声明变量top 并赋值为0.
  • 2 - 8行。在全局执行上下文中声明了一个名为 createWarp 的变量,并为其分配了一个函数定义。其中第3-7行描述了其函数定义,并将函数定义存储到那个变量(createWarp)中。
  • 第9行。我们在全局执行上下文中声明了一个名为 sum 的新变量,暂时,值为 undefined
  • 第9行。遇到(),表明需要执行或调用一个函数。那么查找全局执行上下文的内存并查找名为 createWarp 的变量。 明显,已经在步骤2中创建完毕。接着,调用它。
  • 调用函数时,回到第2行。创建一个新的createWarp执行上下文。我们可以在 createWarp 的执行上下文中创建自有变量。js 引擎createWarp 的上下文添加到调用堆栈(call stack)。因为这个函数没有参数,直接跳到它的主体部分.
  • 3 - 6 行。我们有一个新的函数声明,createWarp执行上下文中创建一个变量 addadd 只存在于 createWarp 执行上下文中, 其函数定义存储在名为 add 的自有变量中。
  • 第7行,我们返回变量 add 的内容。js引擎查找一个名为 add 的变量并找到它. 第4行和第5行括号之间的内容构成该函数定义。
  • createWarp 调用完毕,createWarp 执行上下文将被销毁。add 变量也跟着被销毁。add 函数定义仍然存在,因为它返回并赋值给了 sum 变量。 (ps: 这才是闭包产生的变量存于内存当中的真相
  • 接下来就是简单的执行过程,不再赘述。。
  • ……
  • 代码执行完毕,全局执行上下文被销毁。sum 和 result 也跟着被销毁。

小结一下

现在,如果再让你回答什么是闭包,你能答出多少?

其实,大家说的都对。不管是函数返回一个函数,还是产生了外部作用域的引用,都是有道理的。

所以,什么是闭包?

  • 解释一下作用域链是如何产生的。
  • 解释一下js执行上下文的创建、执行过程。
  • 解释一下闭包所产生的变量放在哪了。
  • 最后请把以上3点结合起来说给面试官听。

0 人点赞