深刻浅出的前端面试

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

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

varlet/const的区别css

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

const命令两个注意点:html

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

为何须要块级做用域?

ES5只有全局做用域和函数做用域,没有块级做用域。前端

这带来不少不合理的场景:vue

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量
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 = ...;
...
}

复制代码

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

块级做用域声明函数:node

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

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

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

复制代码

ES6 的块级做用域容许声明函数的规则,只在使用大括号的状况下成立,若是没有使用大括号,就会报错。web

// 报错
'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等方法也是容许的,你就是把值一个一个全都删光了都不会报错。

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

2、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比较可靠。

3、请描述下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也是在全部同源窗口中都是共享的

4、请描述下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)
复制代码

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

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

一、资源压缩与合并

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

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

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

一、异步加载的方式 异步加载的三种方式——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");  
} 
复制代码

二、异步加载的区别

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

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

三、使用CDN

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

四、预解析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">
复制代码

五、减小内存泄漏

内存泄漏的常见场景

  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. 注意清除定时器和事件监听器

六、开启gzip

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

固然服务器除了gzip外,也还会有其它压缩格式(如 deflate,没有gzip高效,且不流行),因此通常只须要在服务器上开启了gzip压缩,而后以后的请求就都是基于gzip压缩格式的,很是方便

七、利用浏览器缓存

对于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

各大缓存头部的总体关系以下图:

6、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
复制代码

7、编写一个方法去掉数组里面重复的内容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]
复制代码

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

  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的方法
复制代码

9、如下代码输出什么

(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中构造函数能够有返回值也能够没有。

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

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

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

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

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

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

遂最终输出3。

第七问

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

最终实际执行为:

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

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

遂最终结果为3

相关文章
相关标签/搜索