1

1. JS 作用域和作用域链

作用域

作用域就是代码的执行环境,全局执行环境就是全局作用域,函数的执行环境就是私有作用域,它们都是栈内存。
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。
全局执行环境是最外围的一个执行环境。根据 ECMAScript 实现所在的宿主环境不同,表示的执行环境的对象也不一样。

  • 在 Web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的
  • 在 Node 环境中,全局执行环境是 global 对象。

某个执行环境中所有的代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出时,如关闭浏览器或网页,才会被销毁)。
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将被环境弹出,把控制权返回给之前的执行环境。ECMAScript 程序中的执行流正是由这个方便的机制控制着。
作用域就是代码执行开辟栈内存。

作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链(作用域形成的链条)。

  • 作用域链的前端,始终都是当前执行的代码所在环境的变量对象。
  • 作用域链中的下一个对象来自于外部环境,而在下一个变量对象则来自下一个外部环境,一直到全局执行环境。
  • 全局执行环境的变量对象始终都是作用域链上的最后一个对象。

内部环境可以通过作用域链访问所有外部环境,但外部环境不能访问内部环境的任何变量和函数。
如函数的执行,形成一个私有作用域,形参和当前私有作用域中声明的变量都是私有变量,保存在内部的一个变量对象中,其下一个外部环境可能是函数,也就包含了函数的内部变量对象,直到全局作用域。
当在内部函数中,需要访问一个变量的时候,首先会访问函数本身的变量对象,是否有这个变量,如果没有,那么会继续沿作用域链往上查找,直到全局作用域。如果在某个变量对象中找到则使用该变量对象中的变量值。
由于变量的查找是沿着作用域链来实现的,所以也称作用域链为变量查找的机制。
这个机制也说明了访问局部变量要比访问全局变量更快,因为中间的查找过程更短。但是 JavaScript 引擎在优化标识符查询方面做得很好,因此这个差别可以忽略不计。

2. 首屏优化

首屏优化分为两大方向:资源加载优化 和 页面渲染优化。
压缩资源,使用 CDN ,http 缓存等。

路由懒加载

把不同路由对应的组件分割为不同的代码块,当路由被访问的时候,再加载对应的组件。
如果是SPA(单页面应用),优先保证首页加载。
SPA 是一种特殊的 Web 应用,是加载单个 HTML 页面并在用户与应用程序交互时动态更新该页面的。它将所有的活动局限于一个 Web 页面中,仅在该 Web 页面初始化时加载相应的 HTML 、 JavaScript 、 CSS 。一旦页面加载完成, SPA 不会因为用户的操作而进行页面的重新加载或跳转,而是利用 JavaScript 动态的变换 HTML(采用的是 div 切换显示和隐藏),从而实现UI与用户的交互。在 SPA 应用中,应用加载之后就不会再有整页刷新。相反,展示逻辑预先加载,并有赖于内容Region(区域)中的视图切换来展示内容。

服务端渲染SSR

服务端渲染(SSR)后,简单理解是将组件或页面通过服务器生成html字符串,再发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。渲染时请求页面,返回的body里已经存在服务器生成的html结构,之后只需要结合css显示出来。这就节省了访问时间和优化了资源的请求。
传统的 SPA 方式过程繁多:

  • 下载 html ,解析,渲染
  • 下载 js ,执行
  • ajax 异步加载数据
  • 重新渲染页面

而 SSR 则只有一步:

  • 下载 html ,解析,渲染

分页

根据显示设备的高度,设计尽量少的页面内容。即,首评内容尽量少,其他内容上滑时加载。

图片lazyLoad

先加载内容,再加载图片。

3. JS事件循环

一、在了解js的事件循环之前,我们先了解一下浏览器的任务线程
在浏览器执行各种进程,浏览器是多进程,每一个浏览器的标签都可以都可以理解为一个多进程的任务进程,浏览器渲染进程属于进程中的一种,主要负责,页面渲染,脚本执行,任务处理,那么线程大概有:(html的结构解析,css解析成dom树,),js脚本的主线程,事件触发线程,定时器线程,http线程。

二、关于事件循环
虽然说js是一个单线程执行,但是还是分为同步和异步,也可以说为主线程,和异步线程,那么如果在异步线程中,任务队列绝对起到绝对重要的作用,同步任务一般都会在主线程去执行,执行完之后,再去执行异步任务,也就是在异步线程,那么这时候异步任务就可以进入任务队列去执行异步任务,上诉不断地过程,让我们叫做循环事件(Event loop)。

三、关于宏任务和微任务
在事件循环中,每进行一次循环操作称为tick,通过阅读规范可知,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤可以总结如下:
1.在此次 tick 中选择最先进入队列的任务( oldest task ),如果有则执行(一次)
2.检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtask Queue
3.更新 render
4.主线程重复执行上述步骤

规范中规定,task分为两大类, 分别是 Macro Task (宏任务)和 Micro Task(微任务), 并且每个宏任务结束后, 都要清空所有的微任务,这里的 Macro Task也是我们常说的 task

宏任务:script( 整体代码),setTimeout,setInterval,UI交互事件

微任务:promise,nextTick

4. 线程和进程是什么?

进程:cpu分配资源的最小单位(是能拥有资源和独立运行的最小单位)。
线程:是cpu最小的调度单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)。
栗子:比如进程=火车,线程就是车厢。
一个进程内有多个线程,执行过程是多条线程共同完成的,线程是进程的部分。
一个火车可以有多个车厢。
每个进程都有独立的代码和数据空间,程序之间切换会产生较大的开销;线程可以看作轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。

为什么js是单线程

JS是单线程的原因主要和JS的用途有关,JS主要实现浏览器与用户的交互,以及操作DOM。
如果JS被设计为多线程,如果一个线程要修改一个DOM元素,另一个线程要删除这个DOM元素,这时浏览器就不知道该怎么办,为了避免复杂的情况产生,所以JS是单线程的。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

5. js中的基础数据类型有哪几种? 了解包装对象吗?

六种,string, number, boolean, undefiend, null, symbol
基础数据类型临时创建的临时对象,称为包装对象。其中 number、boolean 和 string 有包装对象,代码运行的过程中会找到对应的包装对象,然后包装对象把属性和方法给了基本类型,然后包装对象被系统进行销毁。

6. 对内存泄漏的了解

一、理解

  • 定义:程序中已在堆中分配的内存,因为某种原因未释放或者无法释放的问题
  • 简单理解: 无用的内存还在占用,得不到释放和归还,比较严重的时候,无用的内存还会增加,从而导致整个系统卡顿,甚至崩溃。

二、生命周期

  1. 分配期
    分配所需要的内存,在js中,是自动分配的。
  2. 使用期
    使用分配的内存,就是读写变量或者对象的属性值。
  3. 释放期
    不需要时将该内存释放,js会自动释放(除了闭包和一些bug以外)。内存泄漏就是出现在这个时期,内存没有被释放导致的。

三、可能出现内存泄漏的原因

  1. 意外的全局变量
  2. DOM元素清空时,还存在引用
  3. 闭包
  4. 遗忘的定时器

如何优化内存泄漏?

  • 全局变量先声明在使用。
  • 避免过多使用闭包。
  • 注意清除定时器和事件监听器。

7. HTTP与HTTPS的区别

HTTP(HyperText Transfer Protocol:超文本传输协议)是一种用于分布式、协作式和超媒体信息系统的应用层协议。 简单来说就是一种发布和接收 HTML 页面的方法,被用于在 Web 浏览器和网站服务器之间传递信息。HTTP 默认工作在 TCP 协议 80 端口,用户访问网站 http:// 打头的都是标准 HTTP 服务。HTTP 协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息,因此,HTTP协议不适合传输一些敏感信息,比如:信用卡号、密码等支付信息。
HTTPS(Hypertext Transfer Protocol Secure:超文本传输安全协议)是一种透过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。HTTPS 开发的主要目的,是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。
HTTPS 默认工作在 TCP 协议443端口,它的工作流程一般如以下方式:
1、TCP 三次同步握手
2、客户端验证服务器数字证书
3、DH 算法协商对称加密算法的密钥、hash 算法的密钥
4、SSL 安全加密隧道协商完成
5、网页以加密的方式传输,用协商的对称加密算法和密钥加密,保证数据机密性;用协商的hash算法进行数据完整性保护,保证数据不被篡改。

区别

  • HTTP 明文传输,数据都是未加密的,安全性较差,HTTPS(SSL+HTTP) 数据传输过程是加密的,安全性较好。
  • 使用 HTTPS 协议需要到 CA(Certificate Authority,数字证书认证机构) 申请证书,一般免费证书较少,因而需要一定费用。证书颁发机构如:Symantec、Comodo、GoDaddy 和 GlobalSign 等。
  • HTTP 页面响应速度比 HTTPS 快,主要是因为 HTTP 使用 TCP 三次握手建立连接,客户端和服务器需要交换 3 个包,而 HTTPS除了 TCP 的三个包,还要加上 ssl 握手需要的 9 个包,所以一共是 12 个包。
  • http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443。
  • HTTPS 其实就是建构在 SSL/TLS 之上的 HTTP 协议,所以,要比较 HTTPS 比 HTTP 要更耗费服务器资源。

8. 数组排序

一、简单的sort排序:

var arr = [1,5,3,87,23];
arr.sort(function(a,b) {
    return a-b;
})
console.log(arr); // 输出:[1,23,3,5,87]
注:若返回b-a可获得从大到小的排序;
数组的sort方法只能实现简单的按位排序,并不精确。

二、冒泡排序

var arr = [1,5,2,6,3,3,4,56,7,5,5,5,6,7,8];

function fn(arr) {   //冒泡排序(以从小到大为例)
    for(var i=0;i<arr.length-1;i++) { //控制比较的轮数
        for(var j=0; j<arr.length-1-i; j++){ //内层每轮比较的次数
            if(arr[j] > arr[j+1]) {  
                var temp = arr[j];  //交换这两个值的位置
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
    return arr;
}

三、选择排序

function fn(arr) { //选择排序
    //用这个数分别和别的数相比较,改变的是索引的位置,每轮结束后才交换为位置
    for(var i=0; i<arr.length-1; i++) {  //控制外层比较的轮数
        var minIndex = i;  //先假定一个最小值,定义变量minIndex指向该值的索引
        for(var j=i+1; j<arr.length; j++) {
            if(arr[minIndex]>arr[j]) {
                minIndex = j;  //改变最小索引的指向
            }
        }
        var temp = arr[i];   //每轮比较结束,将最初假定的最小值和实际最小值交换
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return arr;  //将排序后的数组返回
}

9. 一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?

当发送一个URL请求时,不管这个URL是web页面URl还是Web页面上每个资源的URL,浏览器都会开启一个线程来处理这个请求,同时在远程DNS服务器上启动一个DNS查询。这能使得浏览器得到请求对应的IP地址。
浏览器与远程web服务器通过TCP三次握手协商来建立一个TCP/IP连接,该握手包括一个同步报文,一个同步-应答报文和一个应答报文。这三个报文在浏览器和服务器之间传递。该握手首先由客户端尝试建立起通信,然后服务器相应并接受客户端的请求,最后由客户端发出该请求已被接受的报文。
一旦TCP/IP连接建立,浏览器会通过该连接向远程服务器发送HTTP的GET请求。远程服务器找到资源并使用HTTP相应返回该资源。

  1. 首先在浏览器中输入URL
  2. 查找缓存:浏览器先查看浏览器缓存-系统缓存-路由缓存中是否有该地址页面,如果有则显示页面内容。如果没有则进行下一步。
  3. DNS域名解析:浏览器向DNS服务器发起请求,解析该URL中的域名对应的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议。
  4. 建立TCP连接:解析出IP地址后,根据IP地址和默认80端口,和服务器建立TCP连接
  5. 发起HTTP请求:浏览器发起读取文件的HTTP请求,,该请求报文作为TCP三次握手的第三次数据发送给服务器
  6. 服务器响应请求并返回结果:服务器对浏览器请求做出响应,并把对应的html文件发送给浏览器
  7. 关闭TCP连接:通过四次挥手释放TCP连接
  8. 浏览器渲染:客户端(浏览器)解析HTML内容并渲染出来,浏览器接收到数据包后的解析流程为:

    • 构建DOM树:词法分析然后解析成DOM树(dom tree),是由dom元素及属性节点组成,树的根是document对象
    • 构建CSS规则树:生成CSS规则树(CSS Rule Tree)
    • 构建render树:Web浏览器将DOM和CSSOM结合,并构建出渲染树(render tree)
    • 布局(Layout):计算出每个节点在屏幕中的位置
    • 绘制(Painting):即遍历render树,并使用UI后端层绘制每个节点。

10. http缓存

http缓存指的是: 当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有“要请求资源”的副本,就可以直接从浏览器缓存中提取而不是从原始服务器中提取这个资源。常见的http缓存只能缓存get请求响应的资源,对于其他类型的响应则无能为力。

强缓存

在 Chrome 中,强缓存又分为 Disk Cache (存放在硬盘中)和 Memory Cache (存放在内存中),存放的位置是由浏览器控制的。是否强缓存由 Expires、Cache-Control 和 Pragma 3 个 Header 属性共同来控制。

  • Expires
    Expires 的值是一个 HTTP 日期,在浏览器发起请求时,会根据系统时间和 Expires 的值进行比较,如果系统时间超过了 Expires 的值,缓存失效。由于和系统时间进行比较,所以当系统时间和服务器时间不一致的时候,会有缓存有效期不准的问题。Expires 的优先级在三个 Header 属性中是最低的。
  • Cache-Control
    Cache-Control 是 HTTP/1.1 中新增的属性,在请求头和响应头中都可以使用,常用的属性值如有:

    • max-age:单位是秒,缓存时间计算的方式是距离发起的时间的秒数,超过间隔的秒数缓存失效
    • no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜,进入协商缓存
    • no-store:禁止使用缓存(包括协商缓存),每次都向服务器请求最新的资源
    • private:专用于个人的缓存,中间代理、CDN 等不能缓存此响应
    • public:响应可以被中间代理、CDN 等缓存
    • must-revalidate:在缓存过期前可以使用,过期后必须向服务器验证
  • Pragma(优先级高)
    Pragma 只有一个属性值,就是 no-cache ,效果和 Cache-Control 中的 no-cache 一致,不使用强缓存,需要与服务器验证缓存是否新鲜,在 3 个头部属性中的优先级最高。

协商缓存

  • ETag/If-None-Match(优先级高)
    首次请求的时候响应头会返回一个,ETAG,内容是一串hash值,此时刷新之后,发现状态码变成304,此时的请求头上会有一个If-None-Match属性,内容与上次请求的ETAG值一样。即通过对比两个属性的值。
  • Last-Modified/If-Modified-Since(有时间精度问题,或者文件修改,但内容没改,就不会走缓存)
    Last-Modified/If-Modified-Since 的值代表的是文件的最后修改时间,第一次请求服务端会把资源的最后修改时间放到 Last-Modified 响应头中,第二次发起请求的时候,请求头会带上上一次响应头中的 Last-Modified 的时间,并放到 If-Modified-Since 请求头属性中,服务端根据文件最后一次修改时间和 If-Modified-Since 的值进行比较,如果相等,返回 304 ,并加载浏览器缓存。

缓存位置(优先级从高到低)

  • Service Worker
    借鉴了Web work的思路 让JS 运行在主线程之外
  • Memory Cache
    内存缓存 效率快 存活短 渲染进程消失就结束。比较大的js css文件放进这里。
  • Disk Cache
    磁盘缓存 比内存缓存慢 但是储存容量大和储存时长长。小的js css放这里。内存使用率偏高的时候文件优先进入这里。
  • Push Cache
    http2的内容

本地存储

  • Cookie
    cookie的诞生是为了弥补http管理状态上的不足。缺陷:容量小。紧跟域名,同域情况下,无论当前地址是否需要,请求都会带上,当请求变多很影响性能。安全问题cookie能被获取到,然后修改发给服务器。
  • LocalStorage
    同样紧跟域名;容量大(5M);不接触服务器;永久会话,除非手动删除;通过setItem、getItem手动获取、设置。
  • sessionStorage
    基本同local,但是针对服务器,会话级别,关闭页面就没了。一般用于form表单,刷新页面后缓存表单的值。

11. 跨域问题

所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。

一、通过jsonp跨域

通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。缺点:只支持GET请求。

  1. 原生实现:

    <script>
     var script = document.createElement('script');
     script.type = 'text/javascript';
     
     // 传参并指定回调执行函数为onBack
     script.src = 'http://www.demo2.com:8080/login?user=admin&callback=onBack';
     document.head.appendChild(script);
     
     // 回调执行函数
     function onBack(res) {
         alert(JSON.stringify(res));
     }
     </script>
    服务端返回如下(返回时即执行全局函数):
    onBack({"status": true, "user": "admin"})
  2. jquery ajax:

    $.ajax({
     url: 'http://www.demo2.com:8080/login',
     type: 'get',
     dataType: 'jsonp',  // 请求方式为jsonp
     jsonpCallback: "onBack",    // 自定义回调函数名
     data: {}
    });
  3. vue.js:

    this.$http.jsonp('http://www.demo2.com:8080/login', {
     params: {},
     jsonp: 'onBack'
    }).then((res) => {
     console.log(res); 
    })

二、document.domain + iframe跨域

此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

  • 父窗口(http://www.demo.com/a.html))

    <iframe id="iframe" src="http://child.demo.com/b.html"></iframe>
    <script>
      document.domain = 'demo.com';
      var user = 'admin';
    </script>
  • 子窗口(http://child.demo.com/b.html))

    <script>
      document.domain = 'demo.com';
      // 获取父窗口中变量
      alert('get js data from parent ---> ' + window.parent.user);
    </script>

三、location.hash + iframe跨域

实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

  • a.html:(http://www.demo1.com/a.html))

    <iframe id="iframe" src="http://www.demo2.com/b.html" style="display:none;"></iframe>
    <script>
      var iframe = document.getElementById('iframe');
     
      // 向b.html传hash值
      setTimeout(function() {
          iframe.src = iframe.src + '#user=admin';
      }, 1000);
      
      // 开放给同域c.html的回调方法
      function onCallback(res) {
          alert('data from c.html ---> ' + res);
      }
    </script>
  • b.html:(http://www.demo2.com/b.html))

    <iframe id="iframe" src="http://www.demo1.com/c.html" style="display:none;"></iframe>
    <script>
      var iframe = document.getElementById('iframe');
     
      // 监听a.html传来的hash值,再传给c.html
      window.onhashchange = function () {
          iframe.src = iframe.src + location.hash;
      };
    </script>
  • c.html:(http://www.demo1.com/c.html))

    <script>
      // 监听b.html传来的hash值
      window.onhashchange = function () {
          // 再通过操作同域a.html的js回调,将结果传回
          window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
      };
    </script>

四、window.name + iframe跨域

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

  • a.html:(http://www.demo1.com/a.html))

    var proxy = function(url, callback) {
      var state = 0;
      var iframe = document.createElement('iframe');
     
      // 加载跨域页面
      iframe.src = url;
     
      // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
      iframe.onload = function() {
          if (state === 1) {
              // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
              callback(iframe.contentWindow.name);
              destoryFrame();
          } else if (state === 0) {
              // 第1次onload(跨域页)成功后,切换到同域代理页面
              iframe.contentWindow.location = 'http://www.demo1.com/proxy.html';
              state = 1;
          }
      };
     
      document.body.appendChild(iframe);
     
      // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
      function destoryFrame() {
          iframe.contentWindow.document.write('');
          iframe.contentWindow.close();
          document.body.removeChild(iframe);
      }
    };
     
    // 请求跨域b页面数据
    proxy('http://www.demo2.com/b.html', function(data){
      alert(data);
    });
  • proxy.html:(http://www.demo1.com/proxy....)
    中间代理页,与a.html同域,内容为空即可。
  • b.html:(http://www.demo2.com/b.html))

    <script>
      window.name = 'This is demo2 data!';
    </script>

通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

五、postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
a) 页面和其打开的新窗口的数据传递
b) 多窗口之间消息传递
c) 页面与嵌套的iframe消息传递
d) 上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数
data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

  • a.html:(http://www.demo1.com/a.html))

    <iframe id="iframe" src="http://www.demo2.com/b.html" style="display:none;"></iframe>
    <script>       
      var iframe = document.getElementById('iframe');
      iframe.onload = function() {
          var data = {
              name: 'aym'
          };
          // 向domain2传送跨域数据
          iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.demo2.com');
      };
     
      // 接受domain2返回数据
      window.addEventListener('message', function(e) {
          alert('data from demo2 ---> ' + e.data);
      }, false);
    </script>
  • b.html:(http://www.demo2.com/b.html))

    <script>
      // 接收domain1的数据
      window.addEventListener('message', function(e) {
          alert('data from demo1 ---> ' + e.data);
     
          var data = JSON.parse(e.data);
          if (data) {
              data.number = 16;
     
              // 处理后再发回domain1
              window.parent.postMessage(JSON.stringify(data), 'http://www.demo1.com');
          }
      }, false);
    </script>

六、跨域资源共享(CORS)

普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求,前后端都需要设置。
需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。如果想实现当前页cookie的写入,可参考下文:七、nginx反向代理中设置proxy_cookie_domain 和 八、NodeJs中间件代理中cookieDomainRewrite参数的设置。
目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。

  1. 前端设置:
    (1) 原生ajax

    // 前端设置是否带cookie
    xhr.withCredentials = true;

    示例代码:

    var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
     
    // 前端设置是否带cookie
    xhr.withCredentials = true;
     
    xhr.open('post', 'http://www.demo2.com:8080/login', true);
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xhr.send('user=admin');
     
    xhr.onreadystatechange = function() {
     if (xhr.readyState == 4 && xhr.status == 200) {
         alert(xhr.responseText);
     }
    };

    (2) jQuery ajax

    $.ajax({
    // ...
    xhrFields: {
        withCredentials: true    // 前端设置是否带cookie
    },
    crossDomain: true,   // 会让请求头中包含跨域的额外信息,但不会含cookie
    // ...
    });

    (3) vue框架
    在vue-resource封装的ajax组件中加入以下代码:

    Vue.http.options.credentials = true
  2. 服务端设置:
    若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。
    Nodejs后台示例:

    ar http = require('http');
    var server = http.createServer();
    var qs = require('querystring');
     
    server.on('request', function(req, res) {
     var postData = '';
     
     // 数据块接收中
     req.addListener('data', function(chunk) {
         postData += chunk;
     });
     
     // 数据接收完毕
     req.addListener('end', function() {
         postData = qs.parse(postData);
     
         // 跨域后台设置
         res.writeHead(200, {
             // 后端允许发送Cookie
             'Access-Control-Allow-Credentials': 'true',
             // HttpOnly:脚本无法读取cookie
             'Set-Cookie': 'l=a123456;Path=/;Domain=www.demo2.com;HttpOnly'
         });
     
         res.write(JSON.stringify(postData));
         res.end();
     });
    });
     
    server.listen('8080');
    console.log('Server is running at port 8080...');

七、Nodejs中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

  1. 非vue框架的跨域(2次跨域)
    利用node + express + http-proxy-middleware搭建一个proxy服务器。
    (1) 前端代码示例:

    var xhr = new XMLHttpRequest();
     
    // 前端开关:浏览器是否读写cookie
    xhr.withCredentials = true;
     
    // 访问http-proxy-middleware代理服务器
    xhr.open('get', 'http://www.demo1.com:3000/login?user=admin', true);
    xhr.send();

    (2) 中间件服务器:

    var express = require('express');
    var proxy = require('http-proxy-middleware');
    var app = express();
     
    app.use('/', proxy({
     // 代理跨域目标接口
     target: 'http://www.demo2.com:8080',
     changeOrigin: true,
     
     // 修改响应头信息,实现跨域并允许带cookie
     onProxyRes: function(proxyRes, req, res) {
         res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
         res.header('Access-Control-Allow-Credentials', 'true');
     },
     
     // 修改响应信息中的cookie域名
     cookieDomainRewrite: 'www.demo1.com'  // 可以为false,表示不修改
    }));
     
    app.listen(3000);
    console.log('Proxy server is listen at port 3000...');

    (3) Nodejs后台

    ar http = require('http');
    var server = http.createServer();
    var qs = require('querystring');
     
    server.on('request', function(req, res) {
     var params = qs.parse(req.url.substring(2));
     
     // 向前台写cookie
     res.writeHead(200, {
         // HttpOnly:脚本无法读取
         'Set-Cookie': 'l=a123456;Path=/;Domain=www.demo2.com;HttpOnly'
     });
     
     res.write(JSON.stringify(params));
     res.end();
    });
     
    server.listen('8080');
    console.log('Server is running at port 8080...');
  2. vue框架的跨域(1次跨域)
    利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。
    webpack.config.js部分配置:

    module.exports = {
     entry: {},
     module: {},
     // ...
     devServer: {
         historyApiFallback: true,
         proxy: [{
             context: '/login',
             target: 'http://www.demo2.com:8080',  // 代理跨域目标接口
             changeOrigin: true,
             secure: false,  // 当代理某些https服务报错时用
             cookieDomainRewrite: 'www.demo1.com'  // 可以为false,表示不修改
         }],
         noInfo: true
     }
    }

八、WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
前端代码:

<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.demo2.com:8080');
 
// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });
 
    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});
 
document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

Nodejs socket后台:

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});
 
server.listen('8080');
console.log('Server is running at port 8080...');
 
// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });
 
    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

九、nginx代理跨域

  1. nginx配置解决iconfont跨域
    浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

    location / {
      add_header Access-Control-Allow-Origin *;
    }
  2. nginx反向代理接口跨域
    跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
    实现思路:通过nginx配置一个代理服务器(域名与demo1相同,端口不同)做跳板机,反向代理访问demo2接口,并且可以顺便修改cookie中demo信息,方便当前域cookie写入,实现跨域登录。

nginx具体配置:

#proxy服务器
server {
    listen       81;
    server_name  www.demo1.com;
 
    location / {
        proxy_pass   http://www.demo2.com:8080;  #反向代理
        proxy_cookie_demo www.demo2.com www.demo1.com; #修改cookie里域名
        index  index.html index.htm;
 
        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与
        # 故没有同源限制,下面的跨域配置可不启用
        # 当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Origin http://www.demo1.com;
        add_header Access-Control-Allow-Credentials true;
    }
}

前端代码示例:

var xhr = new XMLHttpRequest();
 
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
 
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.demo1.com:81/?user=admin', true);
xhr.send();

Nodejs后台示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');
 
server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));
 
    // 向前台写cookie
    res.writeHead(200, {
        // HttpOnly:脚本无法读取
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.demo2.com;HttpOnly'
    });
 
    res.write(JSON.stringify(params));
    res.end();
});
 
server.listen('8080');
console.log('Server is running at port 8080...');

12. 数组去重

// 例
var arr = [
    1, 1, 'true', 'true', true, true, 15, 15, false, false,
    undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0,
    'a', 'a', {}, {}
];

1. 利用ES6中的Set去重(ES6中最常用)

function unique (arr) { 
    return Array.from(new Set(arr)) 
}

console.log(unique(arr))
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]
// {}没有去重

不考虑兼容性,这种去重的方法代码最少。但是无法去掉“{}”空对象

2. 利用for嵌套for,然后splice去重(ES5中最常用)

双层循环,外层循环元素,内层循环时比较值,值相同时,则删去这个值。

function unique(arr){ 
    for(var i=0; i<arr.length; i++){ 
        for(var j=i+1; j<arr.length; j++){
             if(arr[i]==arr[j]){ 
                 // 若第一个等同于第二个,splice方法删除第二个 
                 arr.splice(j,1); 
                 j--; 
             } 
        }
    } 
    return arr;
}
console.log(unique(arr)) 
// [1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {}, {}]
// NaN和{}没有去重,两个null直接消失了

3. 利用indexOf方法去重

新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!') 
        return 
    }
    var array = []; 
    for (var i = 0; i < arr.length; i++) {
         if (array .indexOf(arr[i]) === -1) {
              array .push(arr[i]) 
         } 
    }
    return array; 
}
console.log(unique(arr)) 
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {}, {}]
// NaN、{}没有去重

4. 利用数组的sort方法去重

利用sort()排序方法,然后根据排序后的结果进行遍历及相邻元素比对。

function unique(arr) { 
    if (!Array.isArray(arr)) {
        console.log('type error!') 
        return; 
    }
    arr = arr.sort() 
    var arrry= [arr[0]]; 
    for (var i = 1; i < arr.length; i++) {
        if (arr[i] !== arr[i-1]) {
            arrry.push(arr[i]); 
        } 
    }
    return arrry; 
} 
console.log(unique(arr)) 
// [0, 1, 15, "NaN", NaN, NaN, {}, {}, "a", false, null, true, "true", undefined]
// NaN、{}没有去重

5. 利用对象的属性不能相同的特点进行去重

这种数组去重的方法有问题,不建议用,有待改进。

function unique(arr) {
     if (!Array.isArray(arr)) {
          console.log('type error!') 
          return 
     }
     var arrry= []; 
     var obj = {}; 
     for (var i = 0; i < arr.length; i++) { 
         if (!obj[arr[i]]) {
              arrry.push(arr[i]) 
              obj[arr[i]] = 1 } 
         else {
              obj[arr[i]]++ 
         } 
     }
     return arrry; 
}
console.log(unique(arr)) 
// [1, "true", 15, false, undefined, null, NaN, 0, "a", {}]
// 两个true直接去掉 了,NaN和{}已去重

6. 利用includes方法去重

function unique(arr) {
     if (!Array.isArray(arr)) {
          console.log('type error!') 
          return 
     }
     var array =[]; 
     for(var i = 0; i < arr.length; i++) {
          if( !array.includes( arr[i]) ) {
              //includes 检测数组是否有某个值 
              array.push(arr[i]); 
          } 
     }
     return array 
}
console.log(unique(arr)) 
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]
// {}没有去重

七、利用hasOwnProperty方法去重

利用hasOwnProperty 判断是否存在对象属性。

function unique(arr) {
     var obj = {}; 
     return arr.filter(function(item, index, arr){
          return obj.hasOwnProperty(typeof item + item)
            ? false
            : (obj[typeof item + item] = true)
     }) 
} 
console.log(unique(arr))
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}] 
// 所有的都去重了

八、利用filter方法去重

function unique(arr) { 
    return arr.filter(function(item, index, arr) {
         //当前元素,在原始数组中的第一个索引 === 当前索引值,否则返回当前元素 
         return arr.indexOf(item, 0) === index; 
     }); 
}
console.log(unique(arr)) 
// [1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {}, {}]
// {}没有去重

九、利用递归去重

function unique(arr) {
     var array= arr; 
     var len = array.length; 
     array.sort(function(a,b) {
          //排序后更加方便去重 
          return a - b; 
     })
     function loop(index) {
          if(index >= 1) { 
              if(array[index] === array[index-1]) {
                   array.splice(index,1); 
              }
              loop(index - 1); //递归loop,然后数组去重 
         } 
     }
     loop(len-1); 
     return array;
}
console.log(unique(arr)) 
// [1, "a", "true", true, 15, false, 1, {}, null, NaN, NaN, "NaN", 0, "a", {}, undefined]

十、利用Map数据结构去重

创建一个空Map数据结构,遍历需要去重的数组,把数组的每一个元素作为key存到Map中。由于Map中不会出现相同的key值,所以最终得到的就是去重后的结果。

function arrayNonRepeatfy(arr) {
     let map = new Map(); 
     let array = new Array(); // 数组用于返回结果 
     for (let i = 0; i < arr.length; i++) {
          if(map .has(arr[i])) {// 如果有该key值 
               map .set(arr[i], true); 
          } 
          else {
               map .set(arr[i], false); // 如果没有该key值 
               array .push(arr[i]);
           } 
     }
     return array ; 
}
console.log(unique(arr)) 
// [1, "a", "true", true, 15, false, 1, {}, null, NaN, NaN, "NaN", 0, "a", {}, undefined]

十一、利用reduce+includes去重

function unique(arr) {
     return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur], []);
}
console.log(unique(arr)); 
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]
// {}没有去重

13. 对 HTML 语义化的理解?

用正确的标签做正确的事情。html 语义化让页面的内容结构化,结构更清晰,便于对浏览器、搜索引擎解析;即使在没有样式 CSS 情况下也以一种文档格式显示,并且是容易阅读的;搜索引擎的爬虫也依赖于 HTML 标记来确定上下文和各个关键字的权重,利于 SEO;使阅读源代码的人更容易将网页分块,便于阅读维护理解。

14. iframe的优缺点?

优点:

  • 解决加载缓慢的第三方内容如图标和广告等的加载问题
  • Security sandbox
  • 并行加载脚本

缺点:

  • iframe会阻塞主页面的Onload事件
  • 即时内容为空,加载也需要时间
  • 没有语意

15. 标签上 title 与 alt 属性的区别是什么?

alt 是给搜索引擎识别,在图像无法显示时的替代文本;title是关于元素的注释信息,主要是给用户解读。当鼠标放到文字或是图片上时有title文字显示。(因为 IE 不标准)在IE浏览器中alt起到了title的作用,变成文字提示。在定义img对象时,将alt和title属性写全,可以保证在各种浏览器中都能正常使用。

16. href 与 src 的区别?

href (Hypertext Reference)指定网络资源的位置,从而在当前元素或者当前文档和由当前属性定义的需要的锚点或资源之间定义一个链接或者关系。(目的不是为了引用资源,而是为了建立联系,让当前标签能够链接到目标地址。)
src source(缩写),指向外部资源的位置,指向的内容将会应用到文档中当前标签所在位置。

href与src的区别

  1. 请求资源类型不同:href 指向网络资源所在位置,建立和当前元素(锚点)或当前文档(链接)之间的联系。在请求 src 资源时会将其指向的资源下载并应用到文档中,比如 JavaScript 脚本,img 图片;
  2. 作用结果不同:href 用于在当前文档和引用资源之间确立联系;src 用于替换当前内容;
  3. 浏览器解析方式不同:当浏览器解析到src ,会暂停其他资源的下载和处理,直到将该资源加载、编译、执行完毕,图片和框架等也如此,类似于将所指向资源应用到当前内容。这也是为什么建议把 js 脚本放在底部而不是头部的原因。

17. CSS 盒子模型

有两种, IE 盒子模型、W3C 盒子模型。
盒模型:内容(content)、填充(padding)、边界(margin)、 边框(border)。
区别:

  • IE 的 content 部分把 border 和 padding 计算了进去。
  • 标准盒模型的width和height属性的范围只包含了content。
  • IE盒模型的width和height属性的范围包含了border、padding和content。

18. CSS 选择器优先级

!important > 行内样式(比重1000)> ID 选择器(比重100) > 类选择器(比重10) > 标签(比重1) > 通配符 > 继承 > 浏览器默认属性。

19. CSS垂直居中的几种方式

单行文本: line-height = height
图片: vertical-align: middle;
absolute 定位: top: 50%;left: 50%;transform: translate(-50%, -50%);
flex: display:flex; margin:auto;

20. CSS link 与 @import 的区别和用法

link 是 XHTML 标签,除了加载CSS外,还可以定义 RSS 等其他事务;@import 属于 CSS 范畴,只能加载 CSS。
link 引用 CSS 时,在页面载入时同时加载;@import 需要页面网页完全载入以后加载。
link 是 XHTML 标签,无兼容问题;@import 是在 CSS2.1 提出的,低版本的浏览器不支持。
link 支持使用 Javascript 控制 DOM 去改变样式;而@import不支持。

21. rgba 和 opacity 的透明效果有什么不同?

opacity 会继承父元素的 opacity 属性,而 RGBA 设置的元素的后代元素不会继承不透明属性。

22. display:none 和 visibility:hidden 的区别

display:none 隐藏对应的元素,在文档布局中不再给它分配空间,它各边的元素会合拢,就当他从来不存在。
visibility:hidden 隐藏对应的元素,但是在文档布局中仍保留原来的空间。

23. HTML5、CSS3 里面都新增了那些新特性?

HTML5

  • 新的语义标签
    article 独立的内容
    aside 侧边栏
    header 头部
    nav 导航
    section 文档中的节
    footer 页脚
  • 画布(Canvas) API
  • 地理(Geolocation) API
  • 本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失;
  • sessionStorage 的数据在浏览器关闭后自动删除
  • 新的技术webworker, websocket, Geolocation
  • 拖拽释放(Drag and drop) API
  • 音频、视频API(audio,video)
  • 表单控件,calendar、date、time、email、url、search

CSS3

  • 2d,3d变换
  • Transition, animation
  • 媒体查询
  • 新的单位(rem, vw,vh 等)
  • 圆角(border-radius),阴影(box-shadow),对文字加特效(text-shadow),线性渐变(gradient),旋转(transform)transform:rotate(9deg) scale(0.85,0.90) translate(0px,-30px) skew(-9deg,0deg);//旋转,缩放,定位,倾斜
  • rgba

24. BFC 是什么?

BFC 即 Block Formatting Contexts (块级格式化上下文),它属于普通流,即:元素按照其在 HTML 中的先后位置至上而下布局,在这个过程中,行内元素水平排列,直到当行被占满然后换行,块级元素则会被渲染为完整的一个新行,除非另外指定,否则所有元素默认都是普通流定位,也可以说,普通流中元素的位置由该元素在 HTML 文档中的位置决定。
可以把 BFC 理解为一个封闭的大箱子,箱子内部的元素无论如何翻江倒海,都不会影响到外部。
只要元素满足下面任一条件即可触发 BFC 特性。

  • body 根元素
  • 浮动元素:float 除 none 以外的值
  • 绝对定位元素:position (absolute、fixed)
  • display 为 inline-block、table-cells、flex
  • overflow 除了 visible 以外的值 (hidden、auto、scroll)

25. 常见兼容性问题

  • 浏览器默认的margin和padding不同。解决方案是加一个全局的*{margin:0;padding:0;}来统一。
  • Chrome 中文界面下默认会将小于 12px 的文本强制按照 12px 显示,可通过加入 CSS 属性 -webkit-text-size-adjust: none; 解决。

26. 判断一个值是什么类型有哪些方法?

  • typeof 运算符
  • instanceof 运算符
  • Object.prototype.toString 方法

27. null 和 undefined 的区别

null 表示一个对象被定义了,值为“空值”;undefined 表示不存在这个值。
(1)变量被声明了,但没有赋值时,就等于undefined。
(2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。
(3)对象没有赋值的属性,该属性的值为undefined。
(4)函数没有返回值时,默认返回undefined。

28. 怎么判断一个变量arr的话是否为数组?

用 typeof 不行
arr instanceof Array
arr.constructor == Array
Object.protype.toString.call(arr) == '[Object Array]'

29. new 操作符具体干了什么?

  1. 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
  2. 属性和方法被加入到 this 引用的对象中。
  3. 新创建的对象由 this 所引用,并且最后隐式的返回 this 。

30. documen.write 和 innerHTML 的区别

document.write 只能重绘整个页面;innerHTML 可以重绘页面的一部分。

31. ajax 过程?

(1) 创建XMLHttpRequest对象,也就是创建一个异步调用对象。
(2) 创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息。
(3) 设置响应HTTP请求状态变化的函数。
(4) 发送HTTP请求。
(5) 获取异步调用返回的数据。
(6) 使用JavaScript和DOM实现局部刷新。

32. 闭包和闭包常用场景

闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包常见方式,就是在一个函数的内部创建另一个函数。
使用闭包主要为了设计私有的方法和变量,闭包的优点是可以避免变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。在js中,函数即闭包,只有函数才会产生作用域的概念。

  • 闭包有三个特性:
    (1) 函数嵌套函数
    (2) 函数内部可以引用外部的参数和变量
    (3) 参数和变量不会被垃圾回收机制回收
  • 应用场景,设置私有变量的方法
  • 不适用场景:返回闭包的函数是个非常大的函数
  • 闭包的缺点就是常驻内存,会增大内存使用量,使用不当会造成内存泄漏

33. JavaScript 原型,原型链?有什么特点?

每个对象都会在其内部初始化一个属性,就是prototype(原型),当我们访问一个对象的属性时,
如果这个对象内部不存在这个属性,那么他就会去prototype里找这个属性,这个prototype又会有自己的prototype,于是就这样一直找下去,也就是我们平时所说的原型链的概念。

  • 关系:instance.constructor.prototype = instance.proto
  • 特点:
    JavaScript对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。

34. 如何判断一个数据是 NaN?

NaN是非数字,但是用 typeof 检测是number类型。

  • 用 typeof 判断是否是number类型并且判断是否满足isNaN
  • 利用 NaN 是唯一一个不等于任何自身的特点 n !== n
  • 利用ES6中提供的 Object.is() 方法(判断两个值是否相等)

35. 事件委托是什么?

事件委托就是利用事件冒泡,只定制一个事件处理程序,就可以管理某一类型的所有事件。
事件委托,称为事件代理,是JS中很常用的绑定事件的方法。事件委托就是把原本需要绑定在子元素上面的事件委托给父元素,让父元素担当事件监听的职务,原理是DOM元素的事件冒泡。

36. 什么是事件冒泡?

一个事件触发后,会在子元素和父元素之间传播,这种传播分为三个阶段:

  • 捕获阶段(从 window 对象传导到目标节点(从外到里),这个阶段不会响应任何事件)
  • 目标阶段(在目标节点上触发)
  • 冒泡阶段(从目标节点传导回window对象,从里到外。) 事件委托/事件代理就是利用事件冒泡的机制把里层需要响应的事件绑定到外层

37. 本地存储与 cookie 的异同?

相同点:

都是在浏览器(客户端)内存储,存储方式为字符串。

不同点:

  1. 生命周期:
    1)cookie如果不设置有效期,就是临时存储(存储在内存中),如果设置了有效期,到期之后会消失(存储在硬盘里)。
    2)localStorage是永久存储,关闭浏览器数据也不会消失,除非手动删除。
    3)sessionStorage是临时存储,仅在当前回话下有效。引入了一个窗口的概念,数据仅在当前窗口下生效,关闭当前窗口的话数据会被删除。
  2. cookie数据在每次网络请求的时候都会被发送给服务端,localStorage和sessionStorage则不会。
  3. cookie大小限制为4kb,storage则为5M。
  4. storage比cookie的安全性更高一些。

38. let、const 和 var 的区别

  • var声明的变量会挂载在window上,而let和const则不会
  • var声明的变量存在变量提升,而let和const则不会
  • 同一个作用域下var可以声明同名变量,而let和const则不行
  • let和const会生成块级作用域
  • const一旦声明之后不可修改,如果是复杂数据类型的话,可以修改其属性

39. 普通函数与构造函数的区别

  1. 构造函数也是一个普通函数,创建方式和普通函数一致,但是构造函数习惯性首字母大写。
  2. 调用方式不一样,普通函数直接调用,构造函数要用new去实例化。
  3. 调用时,构造函数内部会创建一个新对象,普通函数不会创建新对象。
  4. 构造函数内部的this指向实例,普通函数内部的this指向调用函数的对象,如果没有调用对象,则默认为window。
  5. 构造函数默认的返回值是创建的对象,普通函数的返回值由内部的return决定。
  6. 构造函数的函数名与类名相同。

40. 对 Promise 的理解

什么是Promise?

我们都知道,Promise是承诺的意思,承诺它过一段时间会给你一个结果。Promise是一种解决异步编程的方案,相比回调函数和事件更合理、更强大。从语法上讲,Promise是一个对象,从它可以获取异步操作的消息。

Promise的状态

  • pending 等待状态
  • rejected 失败状态
  • fulfiled 成功状态

状态一旦改变,就不会再变

Promise的特点

  • Promise对象不受外界影响
  • Promise的状态一旦改变,就不会再变,任何时候都可以得到这个结果,状态不可逆

Promise的缺点

  • 无法取消Promise,一旦新建就会立即执行,无法中途取消
  • 如果不设置回调函数,Promise内部抛出错误,不会反映到外部
  • 当处于pending状态时,无法得知目前进展到哪一阶段

41. async 的用法

async就是Generation和Promise的语法糖,async就是将Generator的“*”转换成async,将yield转换成await。
函数前必须加一个async,异步操作方法前加一个await关键字,意思就是等一下,执行完了再继续走。注意:await 只能在 async 函数中运行,否则会报错。
Promise 如果返回的是一个错误的结果,如果没有做异常处理,就会报错,所以用 try..catch 捕获一下异常就可以了。

42. GET请求传参长度的误区

我们常说GET请求参数的大小存在限制,而POST请求的参数大小是无限制的。
实际上HTTP协议从未规定GET/POST的请求长度是多少,对GET请求参数的限制是来源与浏览器或WEB服务器,浏览器或WEB服务器限制了url的长度。为了明确这个概念,我们必须强调下面几点。

  • HTTP协议未规定 GET 和 POST 的长度限制
  • GET 的最大长度显示是因为浏览器和web服务器限制了 URI 的
  • 长度不同的浏览器和 WEB 服务器,限制的最大长度不一样要支持
  • IE,则最大长度为 2083 byte,若只支持 Chrome,则最大长度8182 byte

43. 前端的事件流

HTML 与 JS 交互是通过事件驱动来实现的,例如鼠标点击事件 onclick、页面滚动事件 onscroll 等等。可以向文档或者文档中的元素添加事件监听来订阅事件。想要知道这些事件是在什么时候进行调用的,就需要了解一下 ”事件流“ 的概念。

什么是事件流

事件流描述的是从页面中接收事件的顺序,DOM2 级事件流包括下面几个阶段:事件捕获阶段,目标阶段,事件冒泡阶段。
addEventListener 是 DOM2 级事件新增的指定事件处理程序的操作,这个方法接收 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是 true,表示在捕获阶段调用事件处理程序;如果是 false,表示在冒泡阶段调用事件处理程序。IE只支持事件冒泡。

44. 自己实现一个bind函数

原理:通过apply或者call方法来实现。

  • 初始版本

    Function.prototype.bind = function(obj, arg) {
      var arg = Array.prototype.slice.call(arguments, 1);
      var context = this;
      return function(newArg) {
          arg = arg.concat(Array.prototype.slice.call(newArg));
          return context.apply(obj, arg);
      }
    }
  • 考虑到原型链(因为在new一个bind生成的新函数的时候,必须的条件是要继承原函数的原型)

    Function.prototype.bind = function(obj, arg) {
      var arg = Array.prototype.slice.call(arguments, 1);
      var content = this;
      var bound = function(newArg) {
          arg = arg.concat(Array.prototype.slice.call(newArg));
          return context.apply(obj, arg);
      }
      var F = function() {}
      F.prototype = context.prototype;
      bound.prototype = new F();
      return bound;
    }

45. 浏览器渲染机制、重绘、重排

网页生成的过程:

  • 构建DOM树:词法分析然后解析成DOM树(dom tree),是由dom元素及属性节点组成,树的根是document对象
  • 构建CSS规则树:生成CSS规则树(CSS Rule Tree)
  • 构建render树:Web浏览器将DOM和CSSOM结合,并构建出渲染树(render tree)
  • 布局(Layout):计算出每个节点在屏幕中的位置
  • 绘制(Painting):即遍历render树,并使用UI后端层绘制每个节点。

重排

当DOM的变化影响了元素的几何信息(DOM对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。
触发条件:

  • 添加或者删除可见的DOM元素
  • 元素尺寸改变——边距、填充、边框、宽度和高度

重绘

当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。
触发条件:改变元素的color、background、box-shadow等属性。

46. JS中各种位置clientHeight、scrollHeight、offsetHeight、以及scrollTop、offsetTop、clientTop的区别?

  • clientHeight:表示的是可视区域的高度,不包含border和滚动条。
  • offsetHeight:表示可视区域的高度,包含了border和滚动条。
  • scrollHeight:表示了所有区域的高度,包含了因为滚动被隐藏的部分。
  • clientTop:表示边框border的厚度,在未指定的情况下一般为0。
  • scrollTop:滚动后被隐藏的高度,获取对象相对于由offsetParent属性指引的父坐标(CSS定位的元素或body元素)距离顶端的高度

47. 文本超出显示为省略号

① 第一步: 溢出隐藏 —— overflow: hidden;
② 第二步:用省略号来代表未显示完的文本 ——text-overflow: ellipsis;
③ 第三步:必须设置盒子属性为-webkit-box——display: -webkit-box;
④ 第四步:设置超出几行后,超出部分显示省略号,比如-webkit-line-clamp:2;,则表示超出2行的部分显示省略号,如果设置为3,那么就是超出3行部分显示省略号
⑤ 第五步:单词破坏:主要用于破坏英文单词的整体性,即在英文单词还没有在一行完全展示时就换行,简单的理解就是一个单词可能会被分成两行展示——word-break: break-all;
⑥ 第六步:盒子实现多行显示的必要条件,文字垂直展示——-webkit-box-orient: vertical;

  • 单行文本

    <style>
     .box {
         width: 400px;
         height: 30px;
    
         /*第一步: 溢出隐藏 */
         overflow: hidden;
         /* 第二步:让文本不会换行, 在同一行继续 */
         white-space: nowrap;
         /* 第三步:用省略号来代表未显示完的文本 */
         text-overflow: ellipsis;
      }
    </style>
  • 多行文本

     <style>
      .box {
         /* 限定范围 */
         width: 300px;
         height: 40px;
     
         /* 1.溢出隐藏 */
         overflow: hidden;
         /* 2.用省略号来代替超出文本 */
         text-overflow: ellipsis;
         /* 3.设置盒子属性为-webkit-box  必须的 */
         display: -webkit-box;
         /* 4.-webkit-line-clamp 设置为2,表示超出2行的部分显示省略号,如果设置为3,那么就是超出3行部分显示省略号 */
         -webkit-line-clamp: 2;
         /* 5.字面意思:单词破坏:破坏英文单词的整体性,在英文单词还没有在一行完全展示时就换行  即一个单词可能会被分成两行展示 */
         word-break: break-all;
         /* 6.盒子实现多行显示的必要条件,文字是垂直展示,即文字是多行展示的情况下使用 */
         -webkit-box-orient: vertical;
      }
    </style>

47. 获取浏览器 url 中的参数

  • 正则法

    function getQueryString(name) {
      var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
      var r = window.location.search.substr(1).match(reg);
      if (r != null) {
          return unescape(r[2]);
      }
      return null;
    }
    // 这样调用:
    alert(GetQueryString("参数名1"));
    alert(GetQueryString("参数名2"));
    alert(GetQueryString("参数名3"));
  • split拆分法

    function GetRequest() {
      var url = location.search; //获取url中"?"符后的字串
      var theRequest = new Object();
      if (url.indexOf("?") != -1) {
          var str = url.substr(1);
          strs = str.split("&");
          for(var i = 0; i < strs.length; i ++) {
              theRequest[strs[i].split("=")[0]] = unescape(strs[i].split("=")[1]);
          }
      }
      return theRequest;
    }
    var Request = new Object();
    Request = GetRequest();
    // var 参数1,参数2,参数3,参数N;
    // 参数1 = Request['参数1'];
    // 参数2 = Request['参数2'];
    // 参数3 = Request['参数3'];
    // 参数N = Request['参数N'];

48. flex布局常用属性

  1. flex-direction属性决定主轴的方向(即项目的排列方向)。
  2. flex-wrap属性定义,如果一条轴线排不下,如何换行。
  3. flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap。
  4. justify-content属性定义了项目在主轴上的对齐方式。
  5. align-items属性定义项目在交叉轴上如何对齐。
  6. align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

49. position属性

  1. 固定定位 fixed: 元素的位置相对于浏览器窗口是固定位置,即使窗口是滚动的它也不会移动。Fixed定位使元素的位置与文档流无关,因此不占据空间。 Fixed 定位的元素和其他元素重叠。
  2. 相对定位 relative: 如果对一个元素进行相对定位,它将出现在它所在的位置上。然后,可以通过设置垂直 或水平位置,让这个元素“相对于”它的起点进行移动。 在使用相对定位时,无论是 否进行移动,元素仍然占据原来的空间。因此,移动元素会导致它覆盖其它框。
  3. 绝对定位 absolute: 绝对定位的元素的位置相对于最近的已定位父元素,如果元素没有已定位的父元素,那 么它的位置相对于。absolute 定位使元素的位置与文档流无关,因此不占据空间。 absolute 定位的元素和其他元素重叠。
  4. 粘性定位 sticky: 元素先按照普通文档流定位,然后相对于该元素在流中的 flow root(BFC)和 containing block(最近的块级祖先元素)定位。而后,元素定位表现为在跨越特定阈值前为相对定 位,之后为固定定位。
  5. 默认定位 Static: 默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声 明)。inherit: 规定应该从父元素继承 position 属性的值。

50. transition和animation的区别

  • transition是过度属性,强调过度,它的实现需要触发一个事件(比如鼠标移动上去,焦点,点击等)才执行动画。它类似于flash的补间动画,设置一个开始关键帧,一个结束关键帧。
  • animation是动画属性,它的实现不需要触发事件,设定好时间之后可以自己执行,且可以循环一个动画。它也类似于flash的补间动画,但是它可以设置多个关键帧(用@keyframe定义)完成动画。

51. 盒子水平垂直居中方法

  1. 利用绝对定位,先将元素的左上角通过top:50%和left:50%定位到页面的中心,然后再通过translate来调整元素的中心点到页面的中心。
  2. 利用绝对定位,设置四个方向的值都为0,并将margin设置为auto,由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。该方法适用于盒子有宽高的情况。
  3. 利用绝对定位,先将元素的左上角通过top:50%和left:50%定位到页面的中心,然后再通过margin负值来调整元素的中心点到页面的中心。该方法适用于盒子宽高已知的情况。
  4. 使用flex布局,通过align-items:center和justify-content:center设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。

52. ES6 新增数据类型Symbol、Set、Map

Symbol

Symbol类型用于表示一个在内存中独一无二的值。使用Symbol()函数可以生成一个Symbol类型的值,但是不能在调用Symbol函数时使用new关键字,因为Symbol是基本数据类型,而不是对象。每个从Symbol()返回的symbol值都是唯一的,一个symbol值能作为对象属性的标识符,这是该数据类型仅有的目的。Symbol()可以作为对象的key存在,此时对应的value将作为隐藏属性被隐藏起来,不能被外部读取。Symbol真正存储了什么并不重要,重要的是它的值永远不会与别的值相等,Symbol的中文释义为“标志、符号”,一个Symbol类型的变量只是为了标记一块唯一的内存而存在,正因如此,Symbol类型的值不参与运算。

Set

Set用来存储一组唯一不重复的数据。它提供的方法有:

  • add(value) - 追加一个新的元素到Set中,返回Set对象,可使用链式调用;
  • clear() - 清空Set中的所有元素;
  • delete(value) - 删除Set中的元素;
  • entries() - 返回一个新的Iterator(可迭代对象),包含[value, value];
  • forEach(callback, [, thisArg]) - 像数组的forEach一样,并且每次调用会将this设置为thisArg;
  • has(value) - 判断Set中是否有该元素,返回true/false;
  • keys() - 和values()方法一样;
  • [@@iterator] - 返回一个新的Iterator object(可迭代对象)。

Map

Map数据类型是对Object的补充,传统Object键名只能是字符串,而Map的键名不限于字符串。
Map数据类型提供的属性和方法:
1、size返回该实例存储的键值对数,类似数组的length;
2、set(key, value) 设置键名key及对应键值value,返回实例本身;
3、has(key) 判断实例中是否存在键名为key的元素,存在则返回true,否则返回false;
4、delete(key) 删除实例中键名为key的元素,成功返回true,否则返回false;
5、clear() 清空所有元素,无返回值;
6、keys() 返回键名;
7、values() 返回键值;
8、entries() 返回键值对;
9、forEach() 遍历Map实例,用法和数组的forEach相同;

53. Promise.all 的实现

function promiseAll(promises) {
    return new Promise((resolve, reject) => {
        if (!Array.isArray(promises)) {
            throw new Error('The argument must be an array.');
        }
        let resolvedCount = 0;
        const promiseNum = promises.length;
        const resolvedResult = [];
        for (let i = 0; i < promiseNum; i++) {
            Promise.resolve(promises[i]).then(value => {
                resolvedCount++;
                resolvedResult[i] = value;
                if (resolvedCount === promiseNum) {
                    return resolve(resolvedResult);
                }
            }, error => {
                return reject(error);
            });
        }
    });
}
Promise.prototype.all = promises => {
    return new Promise((resolve, reject) => {
        const resultArr = [];
        let count = 0;
        const resultByKey = (value, index) => {
            resultArr[index] = value;
            if (++count === promises.length) {
                resolve(resultArr);
            }
        };
        promises.forEach((promise, index) => {
            promise.then(value => {
                resultByKey(value, index);
            }, reject);
        });
    });
};

54. 简述 Webpack 的编译原理

一、 Webpack的作用

  1. 依赖管理:方便引入第三方模块、让模块更容易复用;避免全局注入导致的冲突,避免重复加载或加载不需要的模块;会一层一层地读取依赖的模块,添加不同的入口,同时,不会重复打包依赖的模块。
  2. 合并代码:把各个分散的模块集中打包成大文件,减少HTTP请求链接数,配合UglifyJS(压缩代码)可以减少、优化代码的体积。
  3. 各路插件:统一处理引入的插件,babel编译ES6文件、TypeScript,检查编译期的错误。

一句话总结:Webpack 的作用就是处理依赖,模块化,打包压缩文件,管理插件。

二、Webpack 的编译原理

webpack 的作用就是根据入口文件将源代码编译(构建,打包)成最终代码,中间经过 webpack 打包,打包的过程就是编译。整体的过程分为三个步骤:初始化,编译(最重要),输出。

初始化

在初始化这个阶段 webpack 会将 CLI 参数,配置文件,默认配置进行融合,形成一个最终的配置对象。使用命令行工具,可能会添加一些参数进去。
配置文件:webpack.config.js 文件里面的配置。
默认配置:比如入口文件 entry , 默认为 ./src/index.js。
对配置的处理过程是依托于一个第三方的库 yargs 完成的,yargs 库就是融合配置的,初始化阶段相对比较简单,主要是为了接下来的编译阶段做准备。
目前,可以理解为:初始化阶段主要的作用是用于产生一个最终的配置。

编译阶段

1. 创建chunk

chunk 是 webpack 内部构建过程中的一个概念,它译为 “块”,是指通过某个入口 找到的所有依赖的统称。

比如说:入口模块(./src/index.js),依赖a 模块(./src/a.js),a 模块又依赖 b(./src/b.js)模块,通过一个入口模块分析依赖关系,可以找到三个模块。那么index.js、a.js、b.js这三个就统称为一个chunk。

根据入口模块插件一个chunk,每一个chunk都是有名字的,意味着 chunk 有可能也会有多个,入口文件是可以有多个的。
默认情况下只有一个chunk,每个 chunk 都有至少两个属性:
name:默认为 mian;
id: 唯一编号,如果是开发环境,那么 id 和 name 相同,如果是生产环境,则是一个数字,从 0 开始。

2. 构建所有的依赖模块

第一步:根据入口模块文件(./src/index.js)进行构建,模块文件是有一个路径的,入口模块文件的路径就是./src/index.js,他会通过这个路径检查这个模块是否已经加载过,注意:它不是运行模块,而是看一眼,看看模块记录表(上面图中的蓝色部分),有没有这个模块,如果有的话就表示当前这个模块已经被加载过,如果没有的话就表示他没有被加载过。
第二步:如果说模块记录表中有记录,说明这个模块已经加载过了,如果没有记录,就继续走下一步,说明该模块需要加载。
第三步:读取模块里面的内容,里面的内容其实就是一个字符串。
第四步:对模块里面的内容进行语法分析,树形结构遍历,找到所有的依赖,最后找到生成 AST 抽象语法树。
第五步:将分析出来的依赖记录到 dependencies 数组中。
第六步:替换依赖函数,什么意思呢?就是把有依赖的地方变一种代码格式,将 require 改为_webpack_require,将依赖的模块改为模块id。
第七步:我们将替换后的代码称为转化后的代码,并且把他保存到模块记录中。
第八步:index.js 模块处理完成,由于 index.js 依赖其他的模块,所以递归循环保存在 dependencies 数组中的依赖,开始分析 ./src/a.js 模块,从头再走一遍这个流程就可以了,假设 a 模块依赖./src/b.js 模块,那么它会等 a 模块处理完成后,再处理 a 模块所依赖的 b 模块,再最后处理 index.js 依赖的 b 模块,此时他发现 b 模块在处理 a 模块所依赖的 b 模块已经加载过了,那么index.js 模块所依赖的 b 模块是不会进行下一次处理,直接结束。
上面就是webpack 的编译过程,做这一切的最终目的就是为了形成一个模块记录表。

3. 生成chunk assets

在第2步完成后,chunk中会产生一个模块列表,列表中包含了模块id 和 模块转换之后的代码,接下来,webpack 会根据配置为 chunk 生成一个资源列表,即 chunk assets,资源列表可以理解为是生成到最终文件的文件名和文件内容。
chunk hash: 是根据所有的 chunk assets 的所有内容是生成的一个 hash 字符串。
hash :一种算法,具体有很多类,特点是将一个任意长度的字符串转化为一个固定长度的字符串,而且可以保证原始内容不变,生成的 hash 字符就不变。

4. 合并 chunk assets

将多个chunk 的assets 合并到一起,并产生一个总的 hash

输出 emit

webpack 将利用 node 中的 fs 模块(文件处理模块),根据编译产生的总的 assets ,生成相应的文件。

总过程

当敲下 webpack 打包命令之后,文件开始初始化,各个参数进行融合,形成一个最终的配置对象,然后把配置对象交给编译器进行编译, 通过入口模块找到互相依赖模块形成模块列表,接下来webpack会根据配置,为chunk生成一个资源列表,然后将每一个chunk生成的资源合并成一个完整的资源,并且生成一个完整的hash值,最终根据完整的资源列表输出到文件。


LunanticSky
82 声望1 粉丝

因为不想跑,所以才去跑,这是长跑者的思维方式。——村上春树


« 上一篇
cre-ui文档