InterviewMap —— Javascript (五)

1、垃圾回收机制和内存泄漏

不想C语言那样,拥有原始底层的内存操做方法如 malloc free。js使用的是自动垃圾回收机制,也就是说js引擎会自动去判别变量的使用状况来自动回收那些不使用的内存块。javascript

即便是使用高级语言,开发者对内存管理也应该有所了解(至少要有基础的了解)。有时,开发者必须理解自动内存管理会遇到问题(例如:垃圾回收中的错误或者性能问题等),以便可以正确处理它们。(或者是找到适当的解决方法,用最小的代价去解决。)css

若是一个值再也不须要了,可是垃圾回收机制确没法回收,这时候就是内存泄漏了。html

const arr = [1, 2, 3, 4];
console.log('hello world');
复制代码

上面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,所以引用次数为1。尽管后面的代码没有用到arr,它仍是会持续占用内存。前端

若是增长一行代码,解除arr[1, 2, 3, 4]引用,这块内存就能够被垃圾回收机制释放了。vue

const arr = [1, 2, 3, 4];
console.log('hello world');
arr = null; 
复制代码

以上例子是在全局下的,arr为全局变量,它属于全局变量对象,全局变量对象只有在浏览器窗口关闭的时候才会被销毁,所以咱们才会不推荐使用过多的全局变量。java

所以,并非说有了垃圾回收机制,程序员就轻松了。你仍是须要关注内存占用:那些很占空间的值,一旦再也不用到,你必须检查是否还存在对它们的引用。若是是的话,就必须手动解除引用。node

一、内存的生命周期

内存每每经历: 操做系统分配内存 == 使用内存 == 内存释放 三个阶段。

二、垃圾回收机制

(1)标记清除react

该算法由如下步骤组成:jquery

  • 垃圾回收器构建“roots”列表。Roots 一般是代码中保留引用的全局变量。在 JavaScript 中,“window” 对象能够做为 root 全局变量示例。
  • 全部的 roots 被检查并标记为 active(即不是垃圾)。全部的 children 也被递归检查。从 root 可以到达的一切都不被认为是垃圾。
  • 全部未被标记为 active 的内存能够被认为是垃圾了。收集器限制能够释放这些内存并将其返回到操做系统

若是是该算法,循环引用就不会出现。在函数调用后,两个对象再也不被从全局对象可访问的东西所引用。所以,垃圾回收器将发现它们是不可达的。nginx

(2)引用计数

若是一个值的引用次数是0,就表示这个值再也不用到了,所以能够将这块内存释放。

上图中,左下角的两个值,没有任何引用,因此能够释放。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 references o2
  o2.p = o1; // o2 references o1. This creates a cycle.
}
 
f();
复制代码

在函数调用以后,它们离开了做用域,所以它们实际上已经无用了,能够被释放了。然而,引用计数算法认为,因为两个对象中的每个至少被引用了一次,因此也不能被垃圾回收。

三、什么是内存泄漏

实质上,内存泄漏能够被定义为应用程序再也不须要的内存,但因为某种缘由,内存不会返回到操做系统或可用内存池中。

四、内存泄漏的例子

(1)意外的全局变量

function foo(arg) { 
    bar = "this is a hidden global variable";
    //等同于window.bar="this is a hidden global variable"
    this.bar2= "potential accidental global";
    //这里的this 指向了全局对象(window),等同于window.bar2="potential accidental global"
}
复制代码

若是是在函数中未使用var声明的变量,那么会将其放到全局window上,会产生一个意外的全局变量。全局变量会一直驻留内存,一次咱们要坚定避免这种意外发生。

解决办法就是使用'use strict'开启严格模式。

(2)循环引用

let obj1 = { a: 1 }; // 一个对象(称之为 A)被建立,赋值给 obj1,A 的引用个数为 1 
let obj2 = obj1; // A 的引用个数变为 2 
  
obj1 = null; // A 的引用个数变为 1 
obj2 = null; // A 的引用个数变为 0,此时对象 A 就能够被垃圾回收了
复制代码

可是引用计数有个最大的问题: 循环引用。

function func() {  
    let obj1 = {};  
    let obj2 = {};  
  
    obj1.a = obj2; // obj1 引用 obj2 
    obj2.a = obj1; // obj2 引用 obj1 
}
复制代码

函数执行完毕以后,按道理是能够被销毁的。内部的变量也会被销毁。但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,因此他们不会被回收。要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子能够这么作:

obj1 = null;  
obj2 = null;
复制代码

(3)被遗忘的计时器和回调函数

let someResource = getData();  
setInterval(() => {  
    const node = document.getElementById('Node');  
    if(node) {  
        node.innerHTML = JSON.stringify(someResource));  
    }  
}, 1000);
复制代码

每隔一秒执行一次匿名回调函数,该函数因为会被长期调用,所以其内部的变量都不会被回收,引用外部的someResource也不会被回收。那什么才叫结束呢?就是调用了 clearInterval。

好比开发SPA页面,当咱们的某一个页面中存在这类定时器,跳转到另外一个页面的时候,其实这里的定时器已经暂时没用了,可是咱们在另外一个页面的时候,内存中仍是回你保留上一个页面的定时器资源,所以这就会致使内存泄漏。解决办法就是即便的使用clearInterval来清除定时器。

(4)闭包

JavaScript 开发的一个关键方面就是闭包:一个能够访问外部(封闭)函数变量的内部函数。

值得注意的是闭包自己不会形成内存泄漏,但闭包过多很容易致使内存泄漏。闭包会形成对象引用的生命周期脱离当前函数的上下文,若是闭包若是使用不当,能够致使环形引用(circular reference),相似于死锁,只能避免,没法发生以后解决,即便有垃圾回收也仍是会内存泄露。

(5)console

console.log:向web开发控制台打印一条消息,经常使用来在开发时调试分析。有时在开发时,须要打印一些对象信息,但发布时却忘记去掉console.log语句,这可能形成内存泄露。

在传递给console.log的对象是不能被垃圾回收 ♻️,由于在代码运行以后须要在开发工具能查看对象信息。因此最好不要在生产环境中console.log任何对象。

(6)DOM泄漏

在Js中对DOM操做是很是耗时的。由于JavaScript/ECMAScript引擎独立于渲染引擎,而DOM是位于渲染引擎,相互访问须要消耗必定的资源。

假如将JavaScript/ECMAScript、DOM分别想象成两座孤岛,两岛之间经过一座收费桥链接,过桥须要交纳必定“过桥费”。JavaScript/ECMAScript每次访问DOM时,都须要交纳“过桥费”。所以访问DOM次数越多,费用越高,页面性能就会受到很大影响。

为了减小DOM访问次数,通常状况下,当须要屡次访问同一个DOM方法或属性时,会将DOM引用缓存到一个局部变量中。但若是在执行某些删除、更新操做后,可能会忘记释放掉代码中对应的DOM引用,这样会形成DOM内存泄露。

var refA = document.getElementById('refA');
document.body.removeChild(refA);
 // #refA不能回收,由于存在变量refA对它的引用。将其对#refA引用释放,但仍是没法回收#refA。
 
 // 使用refA = null; 来释放内存
复制代码
var MyObject = {}; 
document.getElementById('myDiv').myProp = MyObject; 

解决方法: 
在window.onunload事件中写上: document.getElementById('myDiv').myProp = null; 
复制代码

给DOM对象用attachEvent绑定事件:

function doClick() {} 
element.attachEvent("onclick", doClick); 

解决方法: 
在onunload事件中写上: element.detachEvent('onclick', doClick); 
复制代码

从外到内执行appendChild。这时即便调用removeChild也没法释放。范例:

var parentDiv = document.createElement("div"); 
var childDiv = document.createElement("div"); 
document.body.appendChild(parentDiv); 
parentDiv.appendChild(childDiv); 
解决方法: 
从内到外执行appendChild: 
var parentDiv = document.createElement("div"); 
var childDiv = document.createElement("div"); 
parentDiv.appendChild(childDiv); 
document.body.appendChild(parentDiv); 
复制代码

反复重写同一个属性会形成内存大量占用(但关闭IE后内存会被释放)。范例:

for(i = 0; i < 5000; i++) { 
    hostElement.text = "asdfasdfasdf"; 
} 
这种方式至关于定义了5000个属性! 
解决方法: 
其实没什么解决方法:P~~~就是编程的时候尽可能避免出现这种状况咯~~ 
复制代码

五、WeakMap 你了解吗?

前面说过,及时清除引用很是重要。可是,你不可能记得那么多,有时候一疏忽就忘了,因此才有那么多内存泄漏。

最好能有一种方法,在新建引用的时候就声明,哪些引用必须手动清除,哪些引用能够忽略不计,当其余引用消失之后,垃圾回收机制就能够释放内存。这样就能大大减轻程序员的负担,你只要清除主要引用就能够了。

ES6 考虑到了这一点,推出了两种新的数据结构:WeakSet 和 WeakMap。它们对于值的引用都是不计入垃圾回收机制的,是一种弱引用,因此名字里面才会有一个"Weak",表示这是弱引用。

const wm = new WeakMap();

const element = document.getElementById('example'); // 引用计数1

wm.set(element, 'some information'); // 此处是弱引用,不计数
wm.get(element) // "some information" 
复制代码

WeakMap里面对element的引用就是弱引用,不会被计入垃圾回收机制。

也就是说,DOM节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap保存的这个键值对,也会自动消失。

总结

虽然当下的浏览器已经对垃圾回收机制作出了必定的改进和提高,可是内存泄漏的问题咱们仍是须要关注的。

2、AJAX相关面试问题

一、什么是Ajax

Ajax是全称是asynchronous JavaScript andXML,简答来记就是异步的js和XML。它是一种异步加载数据的机制,可使得咱们在不刷新整个页面的状况下去请求数据内容,实现局部刷新。

优势就是:实现异步通讯,速度快,页面局部刷新,用户体验好。AJAX出现以前一直是服务端渲染的天下,服务器去处理页面的数据填充,而后响应页面给咱们。

当须要修改页面的时候,须要表单进行提交,而后服务器接收到请求后去查询和处理数据,从新填充到页面上,返回新的html页面给咱们,这种交互的的缺陷是显而易见的,任何和服务器的交互都须要刷新页面,用户体验很是差,Ajax的出现解决了这个问题。

ajax能够实现,咱们发送请求,获取相应的数据,而后经过js去动态渲染页面,而不须要服务器拼接HTML,页面的刷新也只是局部的刷新,再也不是整个页面的刷新了。

二、原生ajax怎么写?

// [1] 
var xhr = new XMLHttpRequest();

// [2]
xhr.onreadystatechange = function() {
	if(xhr.readyState == 4 && (xhr.status > 200 && xhr.status < 300 || xhr.status == 304)) {
		console.log(xhr.responseText);
	}
}

// [3]
xhr.open('POST', 'http://', true);

// [4]
xhr.setRequestHeader("Content-type",
        "application/x-www-form-urlencoded");

// [5]
xhr.send("name=zjj&pwd=123456");

// [6]
xhr.onerror = function() {
	console.log('err');
}
复制代码

IE中经过new ActiveXObject()获得,其余主流浏览器中经过newXMLHttpRequest()获得.使用jquery封装好的ajax,会避免这些问题.

三、XMLHttpRequest对象

XMLHttpRequest是ajax的核心。咱们的ajax请求就是经过该对象来完成的,他有一些属性和方法。

var xhr = new XMLHttpRequest();
复制代码

比较重要的两个方法open、send。

xhr.open(method, url, async)
// open 方法用于初始化一个请求,提供请求方式 、请求url、以及是否执行异步。

xhr.send(data)
// send方法用于发起请求,咱们能够将须要传递的数据做为参数传入。
// 当请求方式为 post 时,能够将请求体的参数传入
// 当请求方式为 get 时,能够不传或传入 null
// 无论是 get 仍是 post,参数都须要经过 encodeURIComponent 编码后拼接
复制代码
  • responseXML 接收响应的字符串类型数据
  • responseText 接收"text/xml""application/xml"格式的响应
  • status 响应的HTTP状态码
  • timeout 超时时间
  • readyState 请求和响应的当前阶段

如何肯定咱们的请求到了哪一阶段了呢,咱们须要借助 readyState来识别。

xhr.readyStatus==0 还没有调用 open 方法
xhr.readyStatus==1 已调用 open 但还未发送请求(未调用 send)
xhr.readyStatus==2 已发送请求(已调用 send)
xhr.readyStatus==3 已接收到请求返回的数据
xhr.readyStatus==4 请求已完成
复制代码

readyStatus的状态发生改变时,会触发 xhr 的事件onreadystatechange,因而咱们就能够在这个方法中,对接收到的数据进行处理.

xhr.onreadystatechange = () => {
    if (xhr.readyStatus === 4) {
        // HTTP 状态在 200-300 之间表示请求成功
        // HTTP 状态为 304 表示请求内容未发生改变,可直接从缓存中读取
        if (xhr.status >= 200 && 
            xhr.status < 300 || 
            xhr.status == 304) {
            console.log('请求成功', xhr.responseText)
        }
    }
}
复制代码

当网络不佳时,咱们须要给请求设置一个超时时间

// 超时时间单位为毫秒
xhr.timeout = 1000

// 当请求超时时,会触发 ontimeout 方法
xhr.ontimeout = () => console.log('请求超时')
复制代码
  • onprogress
xhr.onprogress = function(event){
  console.log(event.loaded / event.total);
}
复制代码

回调函数能够获取资源总大小total,已经加载的资源大小loaded,用这两个值能够计算加载进度

  • 该 ajax 方法经过 Promise 方式实现回调
function ajax (options) {
    let url = options.url
    const method = options.method.toLocaleLowerCase() || 'get'
    const async = options.async != false // default is true
    const data = options.data
    const xhr = new XMLHttpRequest()

    if (options.timeout && options.timeout > 0) {
        xhr.timeout = options.timeout
    }

    return new Promise ( (resolve, reject) => {
        xhr.ontimeout = () => reject && reject('请求超时')
        xhr.onreadystatechange = () => {
            if (xhr.readyState == 4) {
                if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
                    resolve && resolve(xhr.responseText)
                } else {
                    reject && reject()
                }
            }
        }
        xhr.onerror = err => reject && reject(err)

        let paramArr = []
        let encodeData
        if (data instanceof Object) {
            for (let key in data) {
                // 参数拼接须要经过 encodeURIComponent 进行编码
                paramArr.push( encodeURIComponent(key) + '=' + encodeURIComponent(data[key]) )
            }
            encodeData = paramArr.join('&')
        }

        if (method === 'get') {
              // 检测 url 中是否已存在 ? 及其位置
            const index = url.indexOf('?')
            if (index === -1) url += '?'
            else if (index !== url.length -1) url += '&'
              // 拼接 url
            url += encodeData
        }

        xhr.open(method, url, async)
        if (method === 'get') xhr.send(null)
        else {
            // post 方式须要设置请求头
            xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded;charset=UTF-8')
            xhr.send(encodeData)
        }
    } )
}
复制代码
ajax({
    url: 'your request url',
    method: 'get',
    async: true,
    timeout: 1000,
    data: {
        test: 1,
        aaa: 2
    }
}).then(
    res => console.log('请求成功: ' + res),
    err => console.log('请求失败: ' + err)
)
复制代码

四、jquery中的ajax

$.ajax({
     url:发送请求的地址,
     data:数据的拼接,//发送到服务器的数据
     type:'get',//请求方式,默认get请求
     contentType: 'application/json', // 设置参数类型
	 headers: {'Content-Type','application/json'},// 设置请求头
     dataType:'json',//服务器返回的数据类型
     async:true,//是否异步,默认true
     cache:false,//设置为 false 将不会从浏览器缓存中加载请求信息
     success:function(){},//请求成功后的回调函数
     error: function(){}//请求失败时调用此函数
})
复制代码

五、说如下异步与同步的区别

同步会阻塞,异步不会阻塞

同步:程序运行从上而下,浏览器必须把这个任务执行完毕,才能继续执行下一个任务

异步:程序运行从上而下,浏览器任务没有执行完,可是能够继续执行下一行代码
复制代码

六、AJAX的底层实现?

面试官曾考过我一次这样的问题,当时一脸懵,没有猜到面试管想问的底层原理是什么?结果没答出来~~回来后网上找了好多文章,说的都是XMLHttpRequest。哦个人天~~

后来本身斟酌以为其实面试管想考的并非太底层的东西,不会问你C++源码的实现原理。只是想考你几个关键词:异步线程回调

AJAX告诉浏览器,我要准备发送一个HTTP请求了,你帮我重开一个线程(网络线程),这时候咱们的请求就前往了网络线程去执行,主线程继续执行咱们的代码(这就是异步和线程)。同时回设置一个事件监听,去监听咱们请求的状态,若是请求完毕,就回去执行咱们回调队列中的回调函数,将其调入主线程去执行 (回调)。

其实完整的一次AJAX过程就是一次HTTP请求过程。

3、浏览器事件流

针对事件,面试官可能问:

一、了解事件流的顺序,对平常的工做有什么帮助么?
二、在 vue 的文档中,有一个修饰符 native ,把它用 . 的形式 连结在事件以后,就能够监听原生事件了。它的背后有什么原理?
三、事件的 event 对象中,有好多的属性和方法,该如何使用?
复制代码

一、事件流的概念

事件流分为三个阶段:捕获阶段、目标阶段、冒泡阶段。 先调用捕获阶段的处理函数,其次调用目标阶段的处理函数,最后调用冒泡阶段的处理函数。

最初网景公司提出了捕获事件,微软公司提出了冒泡事件。

低版本IE(IE8及如下版本)不支持捕获阶段

捕获事件流:Netscape提出的事件流,即事件由页面元素接收,逐级向下,传播到最具体的元素。(顶层元素先收到事件,而后往下传递,直到目标元素)

冒泡事件流:IE提出的事件流,即事件由最具体的元素接收,逐级向上,传播到页面。(目标元素先收到事件,而后往上,直到最顶层)

w3c 为了制定统一的标准,采起了折中的方式:先捕获在冒泡

W3C

同一个 DOM 元素能够注册多个同类型的事件,经过 addEventListenerremoveEventListener进行管理。addEventListener 的第三个参数,就是为了捕获和冒泡准备的。

  • 注册事件
target.addEventListener(type, listener[, useCapture]);

// 第三个事件来区分,true为事件捕获,false为事件冒泡
复制代码
  • 移除事件
target.removeEventListener(type, listener[, useCapture]);
复制代码
const btn = document.getElementById("test");
//将回调存储在变量中
const fn = function(e){
    alert("ok");
};
//绑定
btn.addEventListener("click", fn, false);
//解除
btn.removeEventListener("click", fn, false);
复制代码

兼容IE

  • 注册事件
target.attacEvent(type,listener);
复制代码
btn.attachEvent('onclick',function(){
  //do something...
})
复制代码
  • 移除事件
detachEvent(event,function);
复制代码
目前支持以addEventListener绑定事件的浏览器:
FF、Chrome、Safari、Opera、IE9-11
目前支持以attachEvent绑定事件的浏览器:IE6-10
复制代码

经过stopPropagation()cancelBubble来阻止事件进一步传播。 cancelBubbleIE标准下阻止事件传递的属性,设置cancelBubble=true表示阻止冒泡

通常来讲,咱们只但愿事件只触发在目标上,这时候可使用 stopPropagation 来阻止事件的进一步传播。一般咱们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也能够阻止捕获事件。stopImmediatePropagation 一样也能实现阻止事件冒泡,可是还能阻止该事件目标执行别的注册事件。

node.addEventListener(
  'click',
  event => {
    event.stopPropagation();
    console.log('只在目标阶段触发,不冒泡');
  },
  false
)
复制代码
node.addEventListener(
  'click',
  event => {
    event.stopImmediatePropagation()
    console.log('冒泡')
  },
  false
)
// 点击 node 只会执行上面的函数,该函数不会执行
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ')
  },
  true
)
复制代码

原本当一个DOM绑定了两个事件,一个冒泡、一个捕获,那么会按照哦顺序执行,可是使用了event.stopImmediatePropagation()以后,就只执行一个。

二、事件代理

咱们常常会遇到,要监听列表中多项 li 的状况,假设咱们有一个列表以下:

<ul id="list">
    <li id="item1">item1</li>
    <li id="item2">item2</li>
    <li id="item3">item3</li>
    <li id="item4">item4</li>
</ul>
复制代码

若是咱们要实现如下功能:当鼠标点击某一 li 时,输出该 li 的内容,咱们一般的写法是这样的:

window.onload=function(){
    const ulNode = document.getElementById("list");
    const liNodes = ulNode.children;
    for(var i=0; i<liNodes.length; i++){
        liNodes[i].addEventListener('click',function(e){
            console.log(e.target.innerHTML);
        }, false);
    }
}

复制代码

在传统的事件处理中,咱们可能会按照须要,为每个元素添加或者删除事件处理器。然而,事件处理器将有可能致使内存泄露,或者性能降低,用得越多这种风险就越大。JavaScript 的事件代理,则是一种简单的技巧。

事件代理: 经过监听子元素从哪里冒泡上来,实现事件的代理。

window.onload=function(){
    const ulNode=document.getElementById("list");
    ulNode.addEventListener('click', function(e) {
        /*判断目标事件是否为li*/
        if(e.target && e.target.nodeName.toUpperCase()=="LI"){
            console.log(e.target.innerHTML);
        }
    }, false);
};
复制代码

三、vue 中的 native 修饰符

四、 react 中的合成事件

五、事件对象 event

event.target:指的是触发事件的那个节点,也就是事件最初发生的节点。
event.target.matches:能够对关键节点进行匹配,来执行相应操做。
event.currentTarget:指的是正在执行的监听函数的那个节点。
event.isTrusted:表示事件是不是真实用户触发。
event.preventDefault():取消事件的默认行为。
event.stopPropagation():阻止事件的派发(包括了捕获和冒泡)。
event.stopImmediatePropagation():阻止同一个事件的其余监听函数被调用。
复制代码

六、测试题

题目二

<div class="test1">
    <div class="test2"></div>
</div>
<script>
    document.querySelector('.test1').addEventListener('click',function () {
        console.log(1)
    })
    document.querySelector('.test2').addEventListener('click',function () {
        console.log(2)
    })
</script>
复制代码

点击test1,只打印1。若是点击test2,打印2,1;

题目三

<div class="test1">
    <div class="test2"></div>
</div>
<script>
    document.querySelector('.test1').addEventListener('click', function () {
        console.log(1)
    }, true)
    document.querySelector('.test2').addEventListener('click', function () {
        console.log(2)
    }, true)
</script>
复制代码

点击test1,只打印1。若是点击test2,打印1,2;

题目四

<div class="test1">
    <div class="test2"></div>
</div>
<script>
    document.querySelector('.test1').addEventListener('click', function () {
        console.log(1)
    }, false)
    document.querySelector('.test2').addEventListener('click', function () {
        console.log(2)
    }, true)
</script>
复制代码

点击test1,只打印1。若是点击test2,打印2,1;

题目五

<div class="test1">
    <div class="test2"></div>
</div>
<script>
    document.querySelector('.test1').addEventListener('click', function () {
        console.log(1)
    }, true)
    document.querySelector('.test2').addEventListener('click', function () {
        console.log(2)
    }, false)
</script>
复制代码

点击test1,只打印1。若是点击test2,打印1,2;

从八道面试题看JavaScript DOM事件机制

4、事件循环EventLoop

一、js单线程

JavaScript语言的一大特色就是单线程,也就是说,同一个时间只能作一件事。那么,为何JavaScript不能有多个线程呢?这样能提升效率啊。

JavaScript的单线程,与它的用途有关。做为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操做DOM。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?

因此,为了不复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,未来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质。

二、任务队列

单线程就意味这咱们必须等待一个任务结束以后再去执行下一个任务,若是耗时很长,那么需等待好久,若是是计算任务也就算了,可是每每不少都是一个IO操做、网络请求、事件监听触发等,在等待期间CPU空闲,是的CPU资源浪费。

js设计者也认识到了,所以遇到的异步任务,将会放入到主线程以外的一个任务队列中。主线程的同步任务继续执行。具体来讲,事件循环的机制是这样的:

(1)全部的同步任务都在主线程上执行,造成了一个执行栈。
(2)遇到异步任务,就将去放入到任务队列中,只要异步任务有告终果,那么就放置一个事件(同时绑定相应的回调函数)。
(3)一旦执行栈中清空了,那么系统会自动读取任务队列,看看有哪些事件,那么就让其结束等待状态,将其调入主线程的执行栈,开始执行。
(4)以后会一直循环第三步。
复制代码

三、事件和回调函数

"任务队列"中的事件,除了IO设备的事件之外,还包括一些用户产生的事件(好比鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

回调函数就是那些被主线程挂起的代码。当拿到任务队列中的事件以后,放入主线程执行其实就是去调用相对应的回调函数去了~

四、事件循环

主线程从"任务队列"中读取事件,这个过程是循环不断的,因此整个的这种运行机制又称为Event Loop(事件循环)

五、不一样的任务队列

不一样的任务源会被分配到不一样的 Task 队列中,任务源能够分为 微任务(microtask)宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。

微任务包括 process.nextTickpromiseObject.observeMutationObserver

宏任务包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

先来个例子:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')
// script start => Promise => script end => promise1 => promise2 => setTimeout
复制代码

首先执行同步代码,遇到promise的话,会首先执行内部的同步代码,而后再继续执行同步代码。途中遇到的settimeout和promise放入不一样的任务队列中,这时候因为执行栈已经为空,因此须要开始执行异步任务,首先查看微任务队列,发现又promise已经能够了,那么就执行promise的then,把全部能够执行的微任务都执行完成以后才会去宏任务队列找,发现又setTimeout能够执行了,就执行内部的代码。

因此正确的一次 Event loop 顺序是这样的

先执行同步代码,(script)至关因而宏任务。
执行栈为空后,去查询微任务队列,若是有则调用执行全部的微任务(promise...)
必要的时候渲染UI
而后执行栈清空,开始下一轮循环,去执行宏任务队列中的任务(定时器...)
复制代码

六、测试题

题目一

console.log(1);
  setInterval(()=>{
    console.log('setInterval');
  },0);
  setTimeout(()=>{
    console.log(2);
  },0);
  setTimeout(()=>{
    console.log(3);
  },0);
  
  
  new Promise((resolve)=>{
    console.log(4);
    for(let i=0;i<10000;i++){
      i===9999&&resolve();
    }
    console.log(5);
  }).then(()=>{
    console.log(6);
  });
  
  new Promise((resolve)=>{
    resolve();
    console.log(10);
  }).then(()=>{
    console.log(11);
  });
  
  console.log(7);
  console.log(8);
  
// 一、四、五、十、七、八、六、十一、'setInterval'、二、3
复制代码

题目二

const interval = setInterval(() => {
  console.log('setInterval')
}, 0)

setTimeout(() => {  
  console.log('setTimeout 1')
  Promise.resolve().then(() => {
    console.log('promise 3')
  }).then(() => {
    console.log('promise 4')
  }).then(() => {
    setTimeout(() => {
      console.log('setTimeout 2')
      Promise.resolve().then(() => {
        console.log('promise 5')
      }).then(() => {
        console.log('promise 6')
      }).then(() => {
          clearInterval(interval)
      })
    }, 0)
  })
}, 0)

Promise.resolve().then(() => {
  console.log('promise 1')
}).then(() => {
  console.log('promise 2')
})
复制代码
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval // 大部分状况下2次, 少数状况下一次
setTimeout 2
promise 5
promise 6
复制代码

promise 4后面大部分状况下出现2次setInterval、少数状况出现一次的缘由就是浏览器在执行setInterval回调函数后、执行setTimeout回调函数前, 时间间隔大部分状况超过了这个最短期.

上诉题目尚未涉及async/await。因此推荐几篇文章,大佬写很是好~~

看完上诉几篇文章,相信咱们就能应付大部分的异步执行面试考题了,加油!!

5、跨域解决方案

跨域问题是常常出现的一种状况,咱们须要对其有必定的认识~~

一、同源和同源策略

域名、端口、协议都相同的状况下才是同源。有一处不一样都称为是非同源。

浏览器的同源策略是一种安全协议。保证了非同源下不能访问对方的资源。若是不使用同源策略,那么浏览器很容易受到XSS、CSFR的攻击。

二、跨域

非同源下的访问和交互就属于跨域行为。

script、img、link、iframe这几个标签容许跨域访问资源。

cookies不能在不一样域名下使用、ajax跨域不容许都是同源策略的限制。

  • 注意:协议和端口形成的跨域,前端没法处理。

若是发生跨域了,那么请求到底发送过去了吗?

跨域并非没有发请求也不是没有发过去,服务端可以接受到发来的请求,只是浏览器以为它不安全,因此拦截掉了。你可能会疑问为何表单可以发送跨域请求,为何ajax不会?由于归根揭底跨域就是浏览器为了阻止用户读取非同源下的目标。ajax能够响应,可是浏览器说它不安全,因此必须拦截掉,可是表单并不会获取新的内容,只是提交就好了,所以能够跨域请求。同时也说明了跨域不能彻底解决CSRF,由于毕竟请求浏览器仍是收到了。

三、跨域解决方案

JSONP跨域

jsonp跨域就是利用script标签能够跨域的特色,jsonp有一个缺点就是只支持get方法。

第一步

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <script src="./jsonp.js" charset="utf-8"></script>
    <script type="text/javascript"> JSONP({ url: 'http://localhost:3000/say', params: { wd: 'Iloveyou' }, callback: 'show' }).then(data => { console.log(data) }) </script>
  </body>
</html>

复制代码
// jsonp.js
var JSONP = (function(window) {
  var jsonp = function({ url, params, callback }) {
    return new Promise((resolve, reject) => {
      // 【1】动态建立script
      let script = document.createElement('script');
      // 【2】全局设置一个回调函数,服务器返回后会调用执行
      window[callback] = function(data) {
        resolve(data)
        document.body.removeChild(script)
      }
      // 【3】将回调函数做为参数传递
      params = {
        ...params,
        callback
      }
      // 数据拼接处理
      let arrs = []
      for (let key in params) {
        arrs.push(`${key}=${params[key]}`)
      }
      // wd=b&callback=show
      script.src = `${url}?${arrs.join('&')}`
      // 【4】开始请求数据
      document.body.appendChild(script)
    })
  }
  return jsonp;
})(window);

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

复制代码

jsonp跨域原理:

一、客户端使用script标签发送get请求,须要传递的参数拼接到后面,外加一个callback函数。
二、客户端回调函数须要在全局添加一个,参数为服务端返回的数据字符串。
三、服务器收到get请求以后,会解析参数,查询数据,将数据以字符串的形式与callback函数名拼接。返回出去。
四、客户端接收到以后就会调用全局的callback函数,而后经过参数能够接收到服务器返回的数据。
复制代码

CORS跨域资源共享

CORS被称为是跨域资源共享,须要客户端和服务端都支持。浏览器是默认开启的,关键就是服务器。一般项目中使用该方式来实现跨域访问。

原理是在请求头中设置Access-control-Allow-Origin来开启CORS。该属性能够指定哪些域名能够访问资源,若是是*则表示全部均可以。

若是Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误没法经过状态码识别,由于HTTP回应的状态码有多是200。【这样其实就是拦截掉了

const express = require('express');
const app = express();
let whitList = ['http://localhost:3000', 'http://127.0.0.1:62997'] //设置白名单


app.use((req, res, next) => {
  let origin = req.headers.origin // 获取来源
  if(whitList.includes(origin)) {
    // 设置那个源头访问的我
    res.setHeader('Access-Control-Allow-Origin', origin)
    // 容许携带哪一个头访问我
    res.setHeader('Access-Control-Allow-Headers', 'name')
    // 容许哪一个方法访问我
    res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT, HEAD')
    // 容许携带cookie
    res.setHeader('Access-Control-Allow-Credentials', true)
    // 预检的存活时间
    res.setHeader('Access-Control-Max-Age', 6)
    // 容许返回的头
    res.setHeader('Access-Control-Expose-Headers', 'name')

    if (req.method === 'OPTIONS') {
      res.end() // OPTIONS请求不作任何处理
    }
  }
  next()
})

app.put('/getData', function(req, res) {
  console.log(req.headers)
  res.setHeader('name', 'jw') //返回一个响应头,后台需设置
  res.end('我爱小宝贝')
})

app.get('/getData1', function(req, res) {
  res.end('get支持')
})

app.post('/getData2', function(req, res) {
  res.end('post支持')
})

app.listen(4000)
复制代码

上面是一个简单的例子,咱们能够设置白名单,当请求的Origin匹配的时候,就经过设置 Access-Control-Allow-Origin来告诉浏览器我服务器容许,而后浏览器就不会拦截了,这样就能看到服务器的响应了~~

代理服务器代理

同源策略是浏览器须要遵循的标准,而若是是服务器向服务器请求就无需遵循同源策略。代理服务器,须要作如下几个步骤:

// index.html(http://127.0.0.1:5500)
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script> $.ajax({ url: 'http://localhost:3000', type: 'post', data: { name: 'xiamen', password: '123456' }, contentType: 'application/json;charset=utf-8', success: function(result) { console.log(result) // {"title":"fontend","password":"123456"} }, error: function(msg) { console.log(msg) } }) </script>
复制代码
// server1.js 代理服务器(http://localhost:3000)
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) => {
  // 代理服务器,直接和浏览器直接交互,须要设置CORS 的首部字段
  response.writeHead(200, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': '*',
    'Access-Control-Allow-Headers': 'Content-Type'
  })
  // 第二步:将请求转发给服务器
  const proxyRequest = http
    .request(
      {
        host: '127.0.0.1',
        port: 4000,
        url: '/',
        method: request.method,
        headers: request.headers
      },
      serverResponse => {
        // 第三步:收到服务器的响应
        var body = ''
        serverResponse.on('data', chunk => {
          body += chunk
        })
        serverResponse.on('end', () => {
          console.log('The data is ' + body)
          // 第四步:将响应结果转发给浏览器
          response.end(body)
        })
      }
    )
    .end()
})
server.listen(3000, () => {
  console.log('The proxyServer is running at http://localhost:3000')
})
复制代码
// server2.js(http://localhost:4000)
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) => {
  if (request.url === '/') {
    response.end(JSON.stringify(data))
  }
})
server.listen(4000, () => {
  console.log('The server is running at http://localhost:4000')
})
复制代码

postMessage

postMessage是H5的新API。能够实现多窗口之间的信息传递,能够是页面和iframe之间、页面与新打开的窗口之间、多个窗口之间的消息传递。

<body>
  <iframe src="http://127.0.0.1:53402/b.html" frameborder="0" id="frame" onload="load()"></iframe>
  <script>
    function load() {
      let frame = document.getElementById('frame')
      frame.contentWindow.postMessage('a发过去的', 'http://127.0.0.1:53402') //发送数据

      // 监听消息传来的事件
      window.onmessage = function (e) { //接受返回数据
        console.log(e.data) //我不爱你
      }
    }
  </script>

</body>


复制代码
<body>
  <script>
    // 监听消息传来的事件
    window.onmessage = function (e) {
      console.log(e.data) // a发来的
      // 监听到a发来的时候,再去发送出去
      e.source.postMessage('b发来的', e.origin)
    }
  </script>
</body>


复制代码

nginx跨域

location / {  
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    if ($request_method = 'OPTIONS') {
        return 204;
    }
}
复制代码

总结

CORS支持全部类型的HTTP请求,是跨域HTTP请求的根本解决方案

JSONP只支持GET请求,JSONP的优点在于支持老式浏览器,以及能够向不支持CORS的网站请求数据。

无论是Node中间件代理仍是nginx反向代理,主要是经过同源策略对服务器不加限制。

平常工做中,用得比较多的跨域方案是cors和nginx反向代理.

6、函数柯里化

一、什么是函数柯里化

柯里化,是函数式编程的一个重要概念。它既能减小代码冗余,也能增长可读性.

以下案例:

// 写一个 sum 方法,当使用下面的语法调用时,能正常工做
console.log(sum(2, 3)); // Outputs 5
console.log(sum(2)(3)); // Outputs 5
复制代码

实现能够是:

function sum(x) {
    if(arguments.length == 2) {
        return arguments[0] + arguments[1];
    }
    
    return function(y) {
        return x + y;
    }
}
复制代码

二、函数柯里化的实现

若是是这样:

function sum (a, b, c) {
    console.log(a + b + c);
}
sum(1, 2, 3); // 6
复制代码

调用的写法能够是这样: sum(1, 2)(3); 或这样 sum(1, 2)(10); 。就是,先把前2个参数的运算结果拿到后,再与第3个参数相加。

好比:sum(1, 2)(3),sum(1,2)执行以后应该仍是一个函数。

实现一个通用的函数柯里化封装:

function curry (fn, currArgs) {
    return function() {
        let args = [].slice.call(arguments);

        // 首次调用时,若未提供最后一个参数currArgs,则不用进行args的拼接
        if (currArgs !== undefined) {
            args = args.concat(currArgs);
        }

        // 递归调用
        if (args.length < fn.length) {
            return curry(fn, args);// curry执行后仍是会返回一个新的函数f
        }

        // 递归出口
        return fn.apply(null, args);
    }
}


function sum(a, b, c) {
    console.log(a + b + c);
}

const fn = curry(sum);

// fn(1, 2, 3); // 6
fn(1, 2)(3); // 6
// fn(1)(2, 3); // 6
// fn(1)(2)(3); // 6
复制代码
相关文章
相关标签/搜索