深入浅出的前端面试

一、请描述var,let和const的区别

letconstES6新增的命令,用于声明变量,这两个命令跟ES5var有许多不同,并且letconst也有一些细微的不同

varlet/const的区别

  • 块级作用域
  • 不存在变量提升
  • 暂时性死区
  • 不可重复声明
  • let、const声明的全局变量不会挂在顶层对象下面

const命令两个注意点:

  • const 声明之后必须马上赋值,否则会报错
  • const 简单类型一旦声明就不能再更改,复杂类型(数组、对象等)指针指向的地址不能更改,内部数据可以更改

为什么需要块级作用域?

ES5只有全局作用域和函数作用域,没有块级作用域。

这带来很多不合理的场景:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量
var tmp = new Date();
function f() {
  console.log(tmp); // 想打印外层的时间作用域
  if (false) {
    var tmp = 'hello world'; // 这里声明的作用域为整个函数
  }
}
f(); // undefined

var s = 'hello';
for (var i = 0; i < s.length; i++) {
  console.log(s[i]); // i应该为此次for循环使用的变量
}
console.log(i); // 5 全局范围都可以读到

复制代码

块级作用域

  1. 作用域
function f1() {
  let n = 5;
  if (true) {
    let n = 10;
    console.log(n); // 10 内层的n
  }
  console.log(n); // 5 当前层的n
}
复制代码
  1. 块级作用域任意嵌套
{{{{
  {let insane = 'Hello World'}
  console.log(insane); // 报错 读不到子作用域的变量
}}}};

复制代码
  1. 块级作用域真正使代码分割成块了
{
let a = ...;
...
}
{
let a = ...;
...
}

复制代码

以上形式,可以用于测试一些想法,不用担心变量重名,也不用担心外界干扰

块级作用域声明函数:

在块级作用域声明函数,因为浏览器的要兼容老代码,会产生一些问题!

在块级作用域声明函数,最好使用匿名函数的形式。

if(true){
  let a = function () {}; // 作用域为块级 令声明的函数作用域范围更清晰
}

复制代码

ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。

// 报错
'use strict';
if (true)
  function f() {} // 我们需要给if加个{}

复制代码

不存在变量提升

变量提升的现象:在同一作用域下,变量可以在声明之前使用,值为undefined

ES5 时使用var声明变量,经常会出现变量提升的现象。

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

复制代码

暂时性死区:

只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

var tmp = 123; // 声明
if (true) {
  tmp = 'abc'; // 报错 因为本区域有tmp声明变量
  let tmp; // 绑定if这个块级的作用域 不能出现tmp变量
}
复制代码

暂时性死区和不能变量提升的意义在于:

在测试时出现这种情况:var a= '声明';const a '不报错',这种情况是因为babel在转化的时候,做了一些处理,在浏览器的控制台中测试,就成功报错

letconst不允许在相同作用域内,重复声明同一个变量

function func(arg) {
  let arg; // 报错
}

function func(arg) {
  {
    let arg; // 不报错
  }
}

复制代码

let、const声明的全局变量不会挂在顶层对象下面

  1. 浏览器环境顶层对象是: window
  2. node环境顶层对象是: global
  3. var声明的全局变量会挂在顶层对象下面,而let、const不会挂在顶层对象下面。如下面这个栗子
var a = 1;
// 如果在 Node环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined
复制代码

const命令

  1. 一旦声明,必须马上赋值
let p; var p1; // 不报错
const p3 = '马上赋值'
const p3; // 报错 没有赋值
复制代码
  1. const一旦声明值就不能改变

简单类型:不能改动

const p = '不能改变';
p = '报错'
复制代码

复杂类型:变量指针不能变

考虑如下情况:

const p = ['不能改动']
const p2 = {
  name: 'OBKoro1'
}
p[0] = '不报错'
p2.name = '不报错'
p = ['报错']
p2 = {
  name: '报错'
}
复制代码

const所说的一旦声明值就不能改变,实际上指的是:变量指向的那个内存地址所保存的数据不得改动

  • 简单类型(number、string、boolean):内存地址就是值,即常量(一变就报错)
  • 复杂类型(对象、数组等):地址保存的是一个指针,const只能保证指针是固定的(总是指向同一个地址),它内部的值是可以改变的(不要以为const就安全了!)
  • 所以只要不重新赋值整个数组/对象, 因为保存的是一个指针,所以对数组使用的pushshiftsplice等方法也是允许的,你就是把值一个一个全都删光了都不会报错。

复杂类型还有函数,正则等,这点也要注意一下。

二、CSS动画和JS动画的区别

CSS动画

优点:

  1. 浏览器可以对动画进行优化
    • 浏览器使用与 requestAnimationFrame 类似的机制,requestAnimationFrame比起setTimeout,setInterval设置动画的优势主要是:(1)requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。(2)在隐藏或不可见的元素中requestAnimationFrame不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。
    • 强制使用硬件加速 (通过 GPU 来提高动画性能)
  2. 代码相对简单,性能调优方向固定
  3. 对于帧速表现不好的低版本浏览器,CSS3可以做到自然降级,而JS则需要撰写额外代码

缺点:

  1. 运行过程控制较弱,无法附加事件绑定回调函数。CSS动画只能暂停,不能在动画中寻找一个特定的时间点,不能在半路反转动画,不能变换时间尺度,不能在特定的位置添加回调函数或是绑定回放事件,无进度报告
  2. 代码冗长。想用 CSS 实现稍微复杂一点动画,最后CSS代码都会变得非常笨重。

JS动画

优点:

  1. JavaScript动画控制能力很强, 可以在动画播放过程中对动画进行控制:开始、暂停、回放、终止、取消都是可以做到的。
  2. 动画效果比css3动画丰富,有些动画效果,比如曲线运动,冲击闪烁,视差滚动效果,只有JavaScript动画才能完成
  3. CSS3有兼容性问题,而JS大多时候没有兼容性问题

缺点:

  1. JavaScript在浏览器的主线程中运行,而主线程中还有其它需要运行的JavaScript脚本、样式计算、布局、绘制任务等,对其干扰导致线程可能出现阻塞,从而造成丢帧的情况。
  2. 代码的复杂度高于CSS动画

总结:如果动画只是简单的状态切换,不需要中间过程控制,在这种情况下,css动画是优选方案。它可以让你将动画逻辑放在样式文件里面,而不会让你的页面充斥 Javascript 库。然而如果你在设计很复杂的富客户端界面或者在开发一个有着复杂UI状态的 APP。那么你应该使用js动画,这样你的动画可以保持高效,并且你的工作流也更可控。所以,在实现一些小的交互动效的时候,就多考虑考虑CSS动画。对于一些复杂控制的动画,使用javascript比较可靠。

三、请描述下cookies,sessionStorage,loaclStorage的区别

共同点:都是保存在浏览器端,且同源的

区别:

  1. cookie数据始终在同源的http请求中携带,即cookie在浏览器和服务器间来回传递
  2. 而sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存
  3. cookie数据不能超过4k(适合保存小数据),sessionStorage和localStorage容量较大,可以达到5M或更大,数据有效期不同
  4. sessionStorage:仅在当前浏览器窗口关闭前有效
  5. localStorage始终有效,窗口或浏览器关闭也一直保存,需手动清除
  6. cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
  7. sessionStorage不在不同的浏览器窗口中共享;localStorage 在所有同源窗口中都是共享的
  8. cookie也是在所有同源窗口中都是共享的

四、请描述下JSONP的原理

什么是跨域?

跨域概念解释:当前发起请求的域与该请求指向的资源所在的域不一样。这里的域指的是这样的一个概念:我们认为若协议 + 域名 + 端口号均相同,那么就是同域。 如下表

JSONP原理

利用script标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以

JSONP和AJAX对比

JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)

JSONP优缺点

优点:

  1. 不受同源策略的限制
  2. 兼容性更好
  3. 支持老版本浏览器

缺点:只支持get请求具有局限性,,不安全可能会遭受XSS攻击

JSONP的实现

// index.html
function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    window[callback] = function(data) {
      resolve(data)
      document.body.removeChild(script)
    }
    params = { ...params, callback } // wd=b&callback=show
    let arrs = []
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${arrs.join('&')}`
    document.body.appendChild(script)
  })
}
jsonp({
  url: 'http://localhost:3000/say',
  params: { wd: 'Iloveyou' },
  callback: 'show'
}).then(data => {
  console.log(data)
})
复制代码

上面这段代码相当于向http://localhost:3000/say?wd=Iloveyou&callback=show这个地址请求数据,然后后台返回show('我不爱你'),最后会运行show()这个函数,打印出'我不爱你'

// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
  let { wd, callback } = req.query
  console.log(wd) // Iloveyou
  console.log(callback) // show
  res.end(`${callback}('我不爱你')`)
})
app.listen(3000)
复制代码

五、如何进行网站性能优化

互联网有一项著名的8秒原则。用户在访问Web网页时,如果时间超过8秒就会感到不耐烦,如果加载需要太长时间,他们就会放弃访问。

1、资源压缩与合并

主要包括这些方面:html压缩、css 压缩、js的压缩和混乱和文件合并。

资源压缩可以从文件中去掉多余的字符,比如回车、空格。你在编辑器中写代码的时候,会使用缩进和注释,这些方法无疑会让你的代码简洁而且易读,但它们也会在文档中添加多余的字节。

2、非核心代码异步加载异步加载的方式

1、异步加载的方式 异步加载的三种方式——asyncdefer、动态脚本创建

① async方式

  • async属性是HTML5新增属性,需要Chrome、FireFox、IE9+浏览器支持
  • async属性规定一旦脚本可用,则会异步执行
  • async属性仅适用于外部脚本
  • 如果是多个脚本,该方法不能保证脚本按顺序执行
<script type="text/javascript" src="xxx.js" async="async"></script>
复制代码

② defer方式

  • 兼容所有浏览器
  • defer属性规定是否对脚本执行进行延迟,直到页面加载为止
  • 如果是多个脚本,该方法可以确保所有设置了defer属性的脚本按顺序执行
  • 如果脚本不会改变文档的内容,可将defer属性加入到script标签中,以便加快处理文档的速度
<script type="text/javascript" src="xxx.js" defer></script>
复制代码

③动态创建script标签

在还没定义defer和async前,异步加载的方式是动态创建script,通过window.onload方法确保页面加载完毕再将script标签插入到DOM中,具体代码如下:

function addScriptTag(src){  
    var script = document.createElement('script');  
    script.setAttribute("type","text/javascript");  
    script.src = src;  
    document.body.appendChild(script);  
}  
window.onload = function(){  
    addScriptTag("js/index.js");  
} 
复制代码

2、异步加载的区别

  • defer是在HTML解析完之后才会执行,如果是多个,按照加载的顺序依次执行
  • async是在加载完之后立即执行,如果是多个,执行顺序和加载顺序无关

其中蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

3、使用CDN

通过将静态资源(例如javascript,css,图片等等)缓存到离用户很近的相同网络运营商的CDN节点上,不但能提升用户的访问速度,还能节省服务器的带宽消耗,降低负载。

4、预解析DNS

资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。

通过 DNS 预解析来告诉浏览器未来我们可能从某个特定的 URL 获取资源,当浏览器真正使用到该域中的某个资源时就可以尽快地完成 DNS 解析。例如,我们将来可从 example.com 获取图片或音频资源,那么可以在文档顶部的 标签中加入以下内容:

<link rel="dns-prefetch" href="//example.com">
复制代码

当我们从该 URL 请求一个资源时,就不再需要等待 DNS 的解析过程。该技术对使用第三方资源特别有用。通过简单的一行代码就可以告知那些兼容的浏览器进行 DNS 预解析,这意味着当浏览器真正请求该域中的某个资源时,DNS 的解析就已经完成了,从而节省了宝贵的时间。 另外需要注意的是,浏览器会对a标签的href自动启用DNS Prefetching,所以a标签里包含的域名不需要在head中手动设置link。但是在HTTPS下不起作用,需要meta来强制开启功能。这个限制的原因是防止窃听者根据DNS Prefetching推断显示在HTTPS页面中超链接的主机名。下面这句话作用是强制打开a标签域名解析

<meta http-equiv="x-dns-prefetch-control" content="on">
复制代码

5、减少内存泄漏

内存泄漏的常见场景

  1. 缓存

文章前言部分就有说到,JS 开发者喜欢用对象的键值对来缓存函数的计算结果,但是缓存中存储的键越多,长期存活的对象也就越多,这将导致垃圾回收在进行扫描和整理时,对这些对象做无用功。

  1. 作用域未释放(闭包)
var leakArray = [];
exports.leak = function () {
    leakArray.push("leak" + Math.random());
}
复制代码

以上代码,模块在编译执行后形成的作用域因为模块缓存的原因,不被释放,每次调用 leak 方法,都会导致局部变量 leakArray 不停增加且不被释放。

闭包可以维持函数内部变量驻留内存,使其得不到释放。

  1. 没必要的全局变量

声明过多的全局变量,会导致变量常驻内存,要直到进程结束才能够释放内存。

  1. 无效的 DOM 引用
//dom still exist
function click(){
    // 但是 button 变量的引用仍然在内存当中。
    const button = document.getElementById('button');
    button.click();
}
复制代码
  1. 定时器未清除
// vue 的 mounted 或 react 的 componentDidMount
componentDidMount() {
    setInterval(function () {
        // ...do something
    }, 1000)
}
复制代码

vue 或 react 的页面生命周期初始化时,定义了定时器,但是在离开页面后,未清除定时器,就会导致内存泄漏。

  1. 事件监听为清空
componentDidMount() {
    window.addEventListener("scroll", function () {
        // do something...
    });
}
复制代码

在页面生命周期初始化时,绑定了事件监听器,但在离开页面后,未清除事件监听器,同样也会导致内存泄漏。

内存泄漏优化

  1. 解除引用

确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释放其引用——这个做法叫做解除引用(dereferencing)

function createPerson(name){
    var localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}

var globalPerson = createPerson("Nicholas");

// 手动解除 globalPerson 的引用
globalPerson = null;
复制代码

解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

  1. 提供手动清空变量的方法
var leakArray = [];
exports.clear = function () {
    leakArray = [];
}
复制代码
  1. 在业务不需要用到的内部函数,可以重构在函数外,实现解除闭包
  2. 避免创建过多生命周期较长的对象,或将对象分解成多个子对象
  3. 避免过多使用闭包
  4. 注意清除定时器和事件监听器

6、开启gzip

首先,明确gzip是一种压缩格式,需要浏览器支持才有效(不过一般现在浏览器都支持),而且gzip压缩效率很好(高达70%左右)。然后gzip一般是由apachetomcat等web服务器开启。

当然服务器除了gzip外,也还会有其它压缩格式(如 deflate,没有gzip高效,且不流行),所以一般只需要在服务器上开启了gzip压缩,然后之后的请求就都是基于gzip压缩格式的,非常方便

7、利用浏览器缓存

对于web应用来说,缓存是提升页面性能同时减少服务器压力的利器。

缓存可以简单的划分成两种类型: 强缓存(200fromcache)与 协商缓存(304

区别简述如下:

  • 强缓存(200fromcache)时,浏览器如果判断本地缓存未过期,就直接使用,无需发起http请求
  • 协商缓存(304)时,浏览器会向服务端发起http请求,然后服务端告诉浏览器文件未改变,让浏览器使用本地缓存

对于协商缓存,使用 Ctrl+F5强制刷新可以使得缓存无效。但是对于强缓存,在未过期时,必须更新资源路径才能发起新的请求(更改了路径相当于是另一个资源了,这也是前端工程化中常用到的技巧)。

属于强缓存控制的:

(http1.1) Cache-Control(浏览器)/Max-Age(服务端)
(http1.0)Pragma(浏览器)/Expires(服务端)
复制代码

注意: Max-Age不是一个头部,它是Cache-Control头部的值。

属于协商缓存控制的:

(http1.1) If-None-Match(浏览器)/E-tag(服务端)
(http1.0) If-Modified-Since(浏览器)/Last-Modified(服务端)
复制代码

可以看到,上述有提到http1.1http1.0,这些不同的头部是属于不同http时期的。

再提一点,其实HTML页面中也有一个meta标签可以控制缓存方案- Pragma。

<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
复制代码

不过,这种方案还是比较少用到,因为支持情况不佳,譬如缓存代理服务器肯定不支持,所以不推荐。

头部的区别

首先明确,http的发展是从http1.0到http1.1,而在http1.1中,出了一些新内容,弥补了http1.0的不足。

http1.0中的缓存控制:

  • Pragma:严格来说,它不属于专门的缓存控制头部,但是它设置 no-cache时可以让本地强缓存失效(属于编译控制,来实现特定的指令,主要是因为兼容http1.0,所以以前又被大量应用)
  • Expires:服务端配置的,属于强缓存,用来控制在规定的时间之前,浏览器不会发出请求,而是直接使用本地缓存,注意,Expires一般对应服务器端时间,如Expires:Fri,30Oct 1998 14:19:41
  • If-Modified-Since/Last-Modified:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是 If-Modified-Since,而服务端的是Last-Modified,它的作用是,在发起请求时,如果If-Modified-SinceLast-Modified匹配,那么代表服务器资源并未改变,因此服务端不会返回资源实体,而是只返回头部,通知浏览器可以使用本地缓存。Last-Modified,顾名思义,指的是文件最后的修改时间,而且只能精确到 1s以内

http1.1中的缓存控制:

  • Cache-Control:缓存控制头部,是浏览器的头部,有no-cachemax-age等多种取值
  • Max-Age:服务端配置的,用来控制强缓存,在规定的时间之内,浏览器无需发出请求,直接使用本地缓存,注意,Max-AgeCache-Control头部的值,不是独立的头部,譬如 Cache-Control:max-age=3600,而且它值得是绝对时间,由浏览器自己计算
  • If-None-Match/E-tag:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是 If-None-Match,而服务端的是E-tag,同样,发出请求后,如果If-None-MatchE-tag匹配,则代表内容未变,通知浏览器使用本地缓存,和Last-Modified不同,E-tag更精确,它是类似于指纹一样的东西,基于FileEtagINodeMtimeSize生成,也就是说,只要文件变,指纹就会变,而且没有1s精确度的限制。

Max-Age相比Expires?

Expires使用的是服务器端的时间,但是有时候会有这样一种情况-客户端时间和服务端不同步。那这样,可能就会出问题了,造成了浏览器本地的缓存无用或者一直无法过期,所以一般http1.1后不推荐使用Expires。而 Max-Age使用的是客户端本地时间的计算,因此不会有这个问题,因此推荐使用 Max-Age

注意,如果同时启用了Cache-ControlExpiresCache-Control优先级高。

E-tag相比Last-Modified?

Last-Modified

  • 表明服务端的文件最后何时改变的
  • 它有一个缺陷就是只能精确到1s,
  • 然后还有一个问题就是有的服务端的文件会周期性的改变,导致缓存失效

E-tag

  • 是一种指纹机制,代表文件相关指纹
  • 只有文件变才会变,也只要文件变就会变,
  • 也没有精确时间的限制,只要文件一遍,立马E-tag就不一样了

如果同时带有E-tagLast-Modified,服务端会优先检查E-tag

各大缓存头部的整体关系如下图:

六、var arr = [0,245,7,986],请用apply()和call()方法求数组的最大值

// apply
let arr = [0,245,7,986];
let result = Math.max.apply(null,arr)
console.log(result) // 986
// call
let arr2 = [0,245,7,986];
let result2 = Math.max.call(null,[...arr])
console.log(result2) // 986
复制代码

七、编写一个方法去掉数组里面重复的内容var arr = [1,2,3,4,5,1,2,1]

// 第一种最方便
let arr = [1,2,3,4,5,1,2,1]
let result = Array.from(new Set(arr))
console.log(result) // [1,2,3,4,5]

// 第二种利用对象的属性不能相同的特点进行去重(兼容性好)
let arr = [1,2,3,4,5,1,2,1];
let array = [];
var obj = {};
for (var i = 0; i < arr.length; i++) {
  if (!obj[arr[i]]) {
    obj[arr[i]] = true;
    array.push(arr[i]);
  }
}
console.log(array); //[1,2,3,4,5]

// 第三种双层循环 (兼容性好)
var arr = [1,2,3,4,5,1,2,1];
var array = [];
for (var i = 0; i < arr.length; i++) {
  for (var j = 0; j < array.length; j++) {
    if (arr[i] == array[j]) {
      break;
    }
  }
  //如果这两个数相等说明循环完了,没有相等的元素
  if (j == array.length) {
    array.push(arr[i]);
  }
}
console.log(array); //[1,2,3,4,5]

// 第四种利用 indexOf()
var arr = [1,2,3,4,5,1,2,1];
var array = [];
for (var i = 0; i < arr.length; i++) {
  if (array.indexOf(arr[i]) == -1) {
    array.push(arr[i]);
  }
}
console.log(array); //[1,2,3,4,5]

// 第五种利用 forEach 和 indexOf()
var arr = [1,2,3,4,5,1,2,1];
var array = [];
arr.forEach(function(item, index) {
  if (array.indexOf(item) == -1) {
    array.push(item);
  }
});
console.log(array); //[1,2,3,4,5]

// 第六种利用 filter()和 indexOf()
var arr = [1,2,3,4,5,1,2,1];
var array = arr.filter(function(item, index) {
  return arr.indexOf(item) == index;
});
console.log(array); //[1,2,3,4,5]

// 第7种ES6 的 includes 实现去重
var arr = [1,2,3,4,5,1,2,1];
var array = [];
arr.forEach(function(item, index) {
  if (!array.includes(item)) {
    array.push(item);
  }
});
console.log(array); //[1,2,3,4,5]
复制代码

八、怎样添加,移除,插入,创建和查找节点

  1. 创建新节点
createDocumentFragment()    //创建一个DOM片段
createElement()   //创建一个具体的元素
createTextNode()   //创建一个文本节点
复制代码
  1. 添加、移除、替换、插入
appendChild() // 添加
removeChild() // 移除
replaceChild() // 替换
insertBefore() // 插入
复制代码
  1. 查找节点
getElementsByTagName()    //通过标签名称
getElementsByName()       //通过元素的Name属性的值
getElementById()          //通过元素Id,唯一性
querySelector()           // 通过选择器获取一个元素
querySelectorAll()        // 通过选择器获取一组元素
getElementsByClassName()  // 通过类名
document.documentElement  // 获取html的方法
document.body             // 获取body的方法
复制代码

九、以下代码输出什么

(1) function Foo () {
    getName = function () { alert(1) };
    return this;
}
(2) Foo.getName = function () { alert(2) };

(3) Foo.prototype.getName = function () { alert(3) };

(4) var getName = function () { alert(4) };

(5) function getName () { alert(5) };

//输出的值
Foo.getName();   // 2
getName();       // 4
Foo().getName(); // 1
getName();       // 1
new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); 3
复制代码

此题涉及的知识点众多,包括变量定义提升、this指针指向、运算符优先级、原型、继承、全局变量污染、对象属性及原型属性优先级等等

此题包含7小问,分别说下。

第一问

先看此题的上半部分做了什么,首先定义了一个叫Foo的函数,之后为Foo创建了一个叫getName的静态属性存储了一个匿名函数,之后为Foo的原型对象新创建了一个叫getName的匿名函数。之后又通过函数变量表达式创建了一个getName的函数,最后再声明一个叫getName函数。

第一问的 Foo.getName 自然是访问Foo函数上存储的静态属性,自然是2,没什么可说的

第二问

直接调用 getName 函数。既然是直接调用那么就是访问当前上文作用域内的叫getName的函数,所以跟1 2 3都没什么关系。此题有无数面试者回答为5。此处有两个坑,一是变量声明提升,二是函数表达式。

变量声明提升

即所有声明变量或声明函数都会被提升到当前函数的顶部。

备注:

  1. 初始化不会提升。
  2. 函数提升优先级高于变量提升,且不会被变量声明覆盖,但是会被变量赋值之后覆盖

例如下代码:

console.log('x' in window); // true 
var x;
x = 0;
复制代码

代码执行时js引擎会将声明语句提升至代码最上方,变为:

var x;
console.log('x' in window); // true
x = 0;
复制代码

函数表达式var getNamefunction getName 都是声明语句,区别在于 var getName 是函数表达式,而 function getName 是函数声明。

函数表达式最大的问题,在于js会将此代码拆分为两行代码分别执行。

例如下代码:

console.log(x); // 输出:function x(){} ?
function x(){}
var x=1;
function x(){}
复制代码

实际执行的代码为,先将 var x=1 拆分为 var x; 和 x = 1; 两行,再将 var x; 和 function x(){} 两行提升至最上方变成:

function x(){}
var x;
console.log(x); // function x(){}
x=1;
复制代码

所以最终函数声明的x覆盖了变量声明的x,log输出为x函数

同理,原题中代码最终执行时的是:

function Foo() {    
    getName = function () { alert (1); };   
     return this;
}
function getName() { alert (5);}//提升函数声明,覆盖var的声明
var getName;//只提升声明不提升初始化
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
getName = function () { alert (4);};//最终的赋值再次覆盖function getName声明
----------------------------------------------------------------------------
getName();//最终输出4
复制代码

第三问

第三问的 Foo().getName(); 先执行了Foo函数,然后调用Foo函数的返回值对象的getName属性函数。Foo函数的第一句 getName = function () { alert (1); };

是一句函数赋值语句,注意它没有var声明,所以先向当前Foo函数作用域内寻找getName变量,没有。再向当前函数作用域上层,即外层作用域内寻找是否含有getName变量,找到了,也就是第二问中的alert(4)函数,将此变量的值赋值为 function(){alert(1)}。

此处实际上是将外层作用域内的getName函数修改了。

**注意:**此处若依然没有找到会一直向上查找到window对象,若window对象中也没有getName属性,就在window对象中创建一个getName变量。之后Foo函数的返回值是this,而JS的this问题各大文章中已经有非常多的文章介绍,这里不再多说。

简单的讲,this的指向是由所在函数的调用方式决定的。而此处的直接调用方式,this指向window对象。

遂Foo函数返回的是window对象,相当于执行 window.getName() ,而window中的getName已经被修改为alert(1),所以最终会输出1

此处考察了两个知识点,一个是变量作用域问题,一个是this指向问题。

第四问

直接调用getName函数,相当于 window.getName() ,因为这个变量已经被Foo函数执行时修改了,遂结果与第三问相同,为1

第五问

第五问 new Foo.getName(); ,此处考察的是js的运算符优先级问题。

通过查文档可以得知点(.)的优先级高于new(无参数列表)操作,遂相当于是:

new (Foo.getName)();
复制代码

所以实际上将getName函数作为了构造函数来执行,遂弹出2。

第六问

第六问 new Foo().getName() ,首先看运算符优先级括号高于new,实际执行为

(new Foo()).getName()
复制代码

遂先执行Foo函数,而Foo此时作为构造函数却有返回值,所以这里需要说明下js中的构造函数返回值问题。

构造函数的返回值

在传统语言中,构造函数不应该有返回值,实际执行的返回值就是此构造函数的实例化对象。

而在js中构造函数可以有返回值也可以没有。

1、没有返回值则按照其他语言一样返回实例化对象。

2、若有返回值则检查其返回值是否为引用类型。如果是非引用类型,如基本类型

(string,number,boolean,null,undefined)则与无返回值相同,实际返回其实例化对象。

3、若返回值是引用类型,则实际返回值为这个引用类型。

原题中,返回的是this,而this在构造函数中本来就代表当前实例化对象,遂最终Foo函数返回实例化对象。

之后调用实例化对象的getName函数,因为在Foo构造函数中没有为实例化对象添加任何属性,遂到当前对象的原型对象(prototype)中寻找getName,找到了。

遂最终输出3。

第七问

第七问, new new Foo().getName(); 同样是运算符优先级问题。

最终实际执行为:

new ((new Foo()).getName)();
复制代码

先初始化Foo的实例化对象,然后将其原型上的getName函数作为构造函数再次new。

遂最终结果为3

猜你喜欢

转载自blog.csdn.net/weixin_34137799/article/details/88216534