继承
原型继承
核心思想:子类的原型成为父类的实例
实现:
代码语言:javascript复制function SuperType() {
this.colors = ['red', 'green'];
}
function SubType() {}
// 原型继承关键: 子类的原型成为父类的实例
SubType.prototype = new SuperType();
// 测试
let instance1 = new SubType();
instance1.colors.push('blue');
let instance2 = new SubType();
console.log(instance2.colors); // ['red', 'green', 'blue']
原型继承存在的问题:
- 原型中包含的引用类型属性将被所有实例对象共享
- 子类在实例化时不能给父类构造函数传参
构造函数继承
核心思想:在子类构造函数中调用父类构造函数
实现:
代码语言:javascript复制function SuperType(name) {
this.name = name;
this.colors = ['red', 'green'];
this.getName = function() {
return this.name;
}
}
function SubType(name) {
// 继承 SuperType 并传参
SuperType.call(this, name);
}
// 测试
let instance1 = new SubType('instance1');
instance1.colors.push('blue');
console.log(instance1.colors); // ['red','green','blue']
let instance2 = new SubType('instance2');
console.log(instance2.colors); // ['red', 'green']
构造函数继承的出现是为了解决了原型继承的引用值共享问题。优点是可以在子类构造函数中向父类构造函数传参。它存在的问题是:1)由于方法必须在构造函数中定义,因此方法不能重用。2)子类也不能访问父类原型上定义的方法。
组合继承
核心思想:综合了原型链和构造函数,即,使用原型链继承原型上的方法,而通过构造函数继承实例属性。
实现:
代码语言:javascript复制function SuperType(name) {
this.name = name;
this.colors = ['red', 'green'];
}
Super.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name);
// 实例属性
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
// 测试
let instance1 = new SubType('instance1', 1);
instance1.sayName(); // "instance1"
instance1.colors.push("blue");
console.log(instance1.colors); // ['red','green','blue']
let instance2 = new SubType('instance2', 2);
instance2.sayName(); // "instance2"
console.log(instance2.colors); // ['red','green']
组合继承存在的问题是:父类构造函数始终会被调用两次:一次是在创建子类原型时new SuperType()
调用,另一次是在子类构造函数中SuperType.call()
调用。
寄生式组合继承(最佳)
核心思想:通过构造函数继承属性,但使用混合式原型继承方法,即,不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。
实现:
代码语言:javascript复制function SuperType(name) {
this.name = name;
this.colors = ['red', 'green'];
}
Super.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = Object.create(SuperType.prototype);
// 重写原型导致默认 constructor 丢失,手动将 constructor 指回 SubType
SubType.prototype.constructor = SubType;
class 实现继承(ES6)
核心思想:通过 extends
来实现类的继承(相当于 ES5
的原型继承)。通过 super
调用父类的构造方法 (相当于 ES5
的构造函数继承)。
实现:
代码语言:javascript复制class SuperType {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class SubType extends SuperType {
constructor(name, age) {
super(name); // 继承属性
this.age = age;
}
}
// 测试
let instance = new SubType('instance', 0);
instance.sayName(); // "instance"
虽然类继承使用的是新语法,但背后依旧使用的是原型链。
OPTIONS请求方法及使用场景
OPTIONS是除了GET和POST之外的其中一种 HTTP请求方法。
OPTIONS方法是用于请求获得由Request-URI
标识的资源在请求/响应的通信过程中可以使用的功能选项。通过这个方法,客户端可以在采取具体资源请求之前,决定对该资源采取何种必要措施,或者了解服务器的性能。该请求方法的响应不能缓存。
OPTIONS请求方法的主要用途有两个:
- 获取服务器支持的所有HTTP请求方法;
- 用来检查访问权限。例如:在进行 CORS 跨域资源共享时,对于复杂请求,就是使用 OPTIONS 方法发送嗅探请求,以判断是否有对指定资源的访问权限。
浏览器渲染进程的线程有哪些
浏览器的渲染进程的线程总共有五种: (1)GUI渲染线程 负责渲染浏览器页面,解析HTML、CSS,构建DOM树、构建CSSOM树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。
注意:GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
(2)JS引擎线程 JS引擎线程也称为JS内核,负责处理Javascript脚本程序,解析Javascript脚本,运行代码;JS引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个Tab页中无论什么时候都只有一个JS引擎线程在运行JS程序;
注意:GUI渲染线程与JS引擎线程的互斥关系,所以如果JS执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。
(3)时间触发线程 时间触发线程属于浏览器而不是JS引擎,用来控制事件循环;当JS引擎执行代码块如setTimeOut时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理;
注意:由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行);
(4)定时器触发进程 定时器触发进程即setInterval与setTimeout所在线程;浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中;
注意:W3C在HTML标准中规定,定时器的定时时间不能小于4ms,如果是小于4ms,则默认为4ms。
(5)异步http请求线程
- XMLHttpRequest连接后通过浏览器新开一个线程请求;
- 检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待JS引擎空闲后执行;
虚拟DOM转换成真实DOM
描述:将如下 JSON格式的虚拟DOM结构转换成真实DOM结构。
代码语言:JSON复制// vnode 结构
{
tag: 'DIV',
attrs: {
id: "app"
},
children: [
{
tag: 'SPAN',
children: [
{
tag: 'A',
children: []
}
]
}
]
}
// 真实DOM 结构
<div id="app">
<span>
<a></a>
</span>
</div>
实现:
代码语言:javascript复制function _render(vnode) {
// 如果是数字类型转化为字符串;
if(typeof vnode === "number") {
vnode = String(vnode);
}
// 字符串类型直接就是文本节点
if(typeof vnode === "string") {
return document.createTextNode(vnode);
}
// 普通 DOM
const dom = document.createElement(vnode.tag);
if(vnode.attrs) {
// 遍历属性
Object.keys(vnode.attrs).forEach((key) => {
dom.setAttribute(key, vnode.attrs[key]);
});
}
// 子数组进行递归操作
vnode.children.forEach((child) => dom.appendChild(_render(child)));
return dom;
}
// 测试
let vnode = {
tag: "DIV",
attrs: {
id: "app",
},
children: [
{
tag: "SPAN",
children: [
{
tag: "A",
children: [],
},
],
},
],
};
console.log(_render(vnode)); // <div id="app"><span><a></a></span></div>
如何判断一个对象是不是空对象?
Object.keys(obj).length === 0
手写题:在线编程,getUrlParams(url,key); 就是很简单的获取url的某个参数的问题,但要考虑边界情况,多个返回值等等
代码输出结果
代码语言:javascript复制function a() {
console.log(this);
}
a.call(null);
打印结果:window对象
根据ECMAScript262规范规定:如果第一个参数传入的对象调用者是null或者undefined,call方法将把全局对象(浏览器上是window对象)作为this的值。所以,不管传入null 还是 undefined,其this都是全局对象window。所以,在浏览器上答案是输出 window 对象。
要注意的是,在严格模式中,null 就是 null,undefined 就是 undefined:
代码语言:javascript复制'use strict';
function a() {
console.log(this);
}
a.call(null); // null
a.call(undefined); // undefined
代码输出结果
代码语言:javascript复制Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
输出结果如下:
代码语言:javascript复制1
看到这个题目,好多的then,实际上只需要记住一个原则:.then
或.catch
的参数期望是函数,传入非函数则会发生值透传。
第一个then和第二个then中传入的都不是函数,一个是数字,一个是对象,因此发生了透传,将resolve(1)
的值直接传到最后一个then里,直接打印出1。
参考:前端进阶面试题详细解答
JS闭包,你了解多少?
应该有面试官问过你:
- 什么是闭包?
- 闭包有哪些实际运用场景?
- 闭包是如何产生的?
- 闭包产生的变量如何被回收?
这些问题其实都可以被看作是同一个问题,那就是面试官在问你:你对JS闭包了解多少?
来总结一下我听到过的答案,尽量完全复原候选人面试的时候说的原话。
答案1:
就是一个function
里面return
了一个子函数,子函数访问了外面那个函数的变量。
答案2:
for循环里面可以用闭包来解决问题。
for(var i = 0; i < 10; i ){
setTimeout(()=>console.log(i),0)
}
// 控制台输出10遍10.
for(var i = 0; i < 10; i ){
(function(a){
setTimeout(()=>console.log(a),0)
})(i)
}
// 控制台输出0-9
答案3:
当前作用域产产生了对父作用域的引用。
答案4:
不知道。是跟浏览器的垃圾回收机制有关吗?
开杠了。请问,小伙伴的答案和以上的内容有多少相似程度?
其实,拿着这些问题好好想想,你就会发现这些问题都只是为了最终那一个问题。
闭包的底层实现原理
1. JS执行上下文
我们都知道,我们手写的js代码是要经过浏览器V8进行预编译后才能真正的被执行。例如变量提升、函数提升。举个栗子。
代码语言:javascript复制// 栗子:
var d = 'abc';
function a(){
console.log("函数a");
};
console.log(a); // ƒ a(){ console.log("函数a"); }
a(); // '函数a'
var a = "变量a";
console.log(a); // '变量a'
a(); // a is not a function
var c = 123;
// 输出结果及顺序:
// ƒ a(){ console.log("函数a"); }
// '函数a'
// '变量a'
// a is not a function
// 栗子预编后相当于:
function a(){
console.log("函数a");
};
var d;
console.log(a); // ƒ a(){ console.log("函数a"); }
a(); // '函数a'
a = "变量a"; // 此时变量a赋值,函数声明被覆盖
console.log(a); // "变量a"
a(); // a is not a function
那么问题来了。 请问是谁来执行预编译操作的?那这个谁又是在哪里进行预编译的?
是的,你的疑惑没有错。js代码运行需要一个运行环境,那这个环境就是执行上下文。 是的,js运行前的预编译也是在这个环境中进行。
js执行上下文分三种:
全局执行上下文
: 代码开始执行时首先进入的环境。函数执行上下文
:函数调用时,会开始执行函数中的代码。eval执行上下文
:不建议使用,可忽略。
那么,执行上下文的周期,分为两个阶段:
创建阶段
- 创建词法环境
- 生成变量对象(
VO
),建立作用域链、作用域链、作用域链(重要的事说三遍) - 确认
this
指向,并绑定this
执行阶段
。这个阶段进行变量赋值,函数引用及执行代码。
你现在猜猜看,预编译是发生在什么时候?
噢,我忘记说了,其实与编译还有另一个称呼:执行期上下文
。
预编译发生在函数执行之前。预编译四部曲为:
- 创建
AO
对象 - 找形参和变量声明,将变量和形参作为AO属性名,值为
undefined
- 将实参和形参相统一
- 在函数体里找到函数声明,值赋予函数体。最后程序输出变量值的时候,就是从
AO
对象中拿。
所以,预编译真正的结果是:
代码语言:javascript复制var AO = {
a = function a(){console.log("函数a");};
d = 'abc'
}
我们重新来。
1. 什么叫变量对象?
变量对象是 js
代码在进入执行上下文时,js
引擎在内存中建立的一个对象,用来存放当前执行环境中的变量。
2. 变量对象(VO)的创建过程
变量对象的创建,是在执行上下文创建阶段,依次经过以下三个过程:
- 创建
arguments
对象。
对于函数执行环境,首先查询是否有传入的实参,如果有,则会将参数名是实参值组成的键值对放入arguments
对象中。否则,将参数名和 undefined
组成的键值对放入 arguments
对象中。
//举个栗子
function bar(a, b, c) {
console.log(arguments); // [1, 2]
console.log(arguments[2]); // undefined
}
bar(1,2)
- 当遇到同名的函数时,后面的会覆盖前面的。
console.log(a); // function a() {console.log('Is a ?') }
function a() {
console.log('Is a');
}
function a() {
console.log('Is a ?')
}
/**ps: 在执行第一行代码之前,函数声明已经创建完成.后面的对之前的声明进行了覆盖。**/
- 检查当前环境中的变量声明并赋值为
undefined
。当遇到同名的函数声明,为了避免函数被赋值为undefined
,会忽略此声明
console.log(a); // function a() {console.log('Is a ?') }
console.log(b); // undefined
function a() {
console.log('Is a ');
}
function a() {
console.log('Is a ?');
}
var b = 'Is b';
var a = 10086;
/**这段代码执行一下,你会发现 a 打印结果仍旧是一个函数,而 b 则是 undefined。**/
根据以上三个步骤,对于变量提升也就知道是怎么回事了。
3. 变量对象变为活动对象
执行上下文的第二个阶段,称为执行阶段,在此时,会进行变量赋值,函数引用并执行其他代码,此时,变量对象变为活动对象。
我们还是举上面的例子:
代码语言:javascript复制console.log(a); // function a() {console.log('fjdsfs') }
console.log(b); // undefined
function a() {
console.log('Is a');
}
function a() {
console.log('Is a?');
}
var b = 'Is b';
console.log(b); // 'Is b'
var a = 10086;
console.log(a); // 10086
var b = 'Is b?';
console.log(b); // 'Is b?'
在上面的代码中,代码真正开始执行是从第一行 console.log() 开始的,自这之前,执行上下文是这样的:
代码语言:javascript复制// 创建过程
EC= {
VO: {}; // 创建变量对象
scopeChain: {}; // 作用域链
}
VO = {
argument: {...}; // 当前为全局上下文,所以这个属性值是空的
a: <a reference> // 函数 a 的引用地址 b: undefiend // 见上文创建变量对象的第三步}
词法作用域(Lexical scope
)
这里想说明,我们在函数执行上下文中有变量,在全局执行上下文中有变量。JavaScript
的一个复杂之处在于它如何查找变量,如果在函数执行上下文中找不到变量,它将在调用上下文中寻找它,如果在它的调用上下文中没有找到,就一直往上一级,直到它在全局执行上下文中查找为止。(如果最后找不到,它就是 undefined
)。
再来举个栗子:
代码语言:javascript复制 1: let top = 0; //
2: function createWarp() {
3: function add(a, b) {
4: let ret = a b
5: return ret
6: }
7: return add
8: }
9: let sum = createWarp()
10: let result = sum(top, 8)
11: console.log('result:',result)
分析过程如下:
- 在全局上下文中声明变量
top
并赋值为0. - 2 - 8行。在全局执行上下文中声明了一个名为
createWarp
的变量,并为其分配了一个函数定义。其中第3-7行描述了其函数定义,并将函数定义存储到那个变量(createWarp
)中。 - 第9行。我们在全局执行上下文中声明了一个名为
sum
的新变量,暂时,值为undefined
。 - 第9行。遇到
()
,表明需要执行或调用一个函数。那么查找全局执行上下文的内存并查找名为createWarp
的变量。 明显,已经在步骤2中创建完毕。接着,调用它。 - 调用函数时,回到第2行。创建一个新的
createWarp
执行上下文。我们可以在createWarp
的执行上下文中创建自有变量。js
引擎createWarp
的上下文添加到调用堆栈(call stack
)。因为这个函数没有参数,直接跳到它的主体部分. - 3 - 6 行。我们有一个新的函数声明,在
createWarp
执行上下文中创建一个变量add
。add
只存在于createWarp
执行上下文中, 其函数定义存储在名为add
的自有变量中。 - 第7行,我们返回变量
add
的内容。js引擎查找一个名为add
的变量并找到它. 第4行和第5行括号之间的内容构成该函数定义。 createWarp
调用完毕,createWarp
执行上下文将被销毁。add 变量也跟着被销毁。 但add
函数定义仍然存在,因为它返回并赋值给了sum
变量。 (ps:这才是闭包产生的变量存于内存当中的真相
)- 接下来就是简单的执行过程,不再赘述。。
- ……
- 代码执行完毕,全局执行上下文被销毁。sum 和 result 也跟着被销毁。
小结一下
现在,如果再让你回答什么是闭包,你能答出多少?
其实,大家说的都对。不管是函数返回一个函数,还是产生了外部作用域的引用,都是有道理的。
所以,什么是闭包?
- 解释一下作用域链是如何产生的。
- 解释一下js执行上下文的创建、执行过程。
- 解释一下闭包所产生的变量放在哪了。
- 最后请把以上3点结合起来说给面试官听。
图片懒加载
与普通的图片懒加载不同,如下这个多做了 2 个精心处理:
- 图片全部加载完成后移除事件监听;
- 加载完的图片,从 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length
// 修正错误,需要加上自执行
- const imgLazyLoad = function() {
const imgLazyLoad = (function() {
let count = 0
return function() {
let deleteIndexList = []
imgList.forEach((img, index) => {
let rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataset.src
deleteIndexList.push(index)
count
if (count === length) {
document.removeEventListener('scroll', imgLazyLoad)
}
}
})
imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
}
- }
})()
// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad)
手写题:数组扁平化
代码语言:javascript复制function flatten(arr) {
let result = [];
for (let i = 0; i < arr.length; i ) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));
} else {
result = result.concat(arr[i]);
}
}
return result;
}
const a = [1, [2, [3, 4]]];
console.log(flatten(a));
说一说SessionStorage和localStorage还有cookie
代码语言:javascript复制共同点:都是保存在浏览器端、且同源的
不同点:
1.cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。
cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下
sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。
2.存储大小限制也不同,cookie数据不能超过4K,sessionStorage和localStorage可以达到5M
3.sessionStorage:仅在当前浏览器窗口关闭之前有效;
localStorage:始终有效,窗口或浏览器关闭也一直保存,本地存储,因此用作持久数据;
cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭
4.作用域不同
sessionStorage:不在不同的浏览器窗口中共享,即使是同一个页面;
localstorage:在所有同源窗口中都是共享的;也就是说只要浏览器不关闭,数据仍然存在
cookie: 也是在所有同源窗口中都是共享的.也就是说只要浏览器不关闭,数据仍然存在
vuex
代码语言:text复制vuex是一个专为vue.js应用程序开发的状态管理器,它采用集中式存储管理应用的所有组件的状态,并且以相
应的规则保证状态以一种可以预测的方式发生变化。
state: vuex使用单一状态树,用一个对象就包含来全部的应用层级状态
mutation: 更改vuex中state的状态的唯一方法就是提交mutation
action: action提交的是mutation,而不是直接变更状态,action可以包含任意异步操作
getter: 相当于vue中的computed计算属性
说说Vue原理
代码语言:css复制Vue是采用数据劫持配合发布者-订阅者模式,通过Object.defineProperty来()来劫持各个属性的getter和setter
在数据发生变化的时候,发布消息给依赖收集器,去通知观察者,做出对应的回调函数去更新视图。
具体就是:
MVVM作为绑定的入口,整合Observe,Compil和Watcher三者,通过Observe来监听model的变化
通过Compil来解析编译模版指令,最终利用Watcher搭起Observe和Compil之前的通信桥梁
从而达到数据变化 => 更新视图,视图交互变化(input) => 数据model变更的双向绑定效果。
说一说前端性能优化方案
代码语言:text复制三个方面来说明前端性能优化
一: webapck优化与开启gzip压缩
1.babel-loader用 include 或 exclude 来帮我们避免不必要的转译,不转译node_moudules中的js文件
其次在缓存当前转译的js文件,设置loader: 'babel-loader?cacheDirectory=true' 2.文件采用按需加载等等 3.具体的做法非常简单,只需要你在你的 request headers 中加上这么一句: accept-encoding:gzip 4.图片优化,采用svg图片或者字体图标 5.浏览器缓存机制,它又分为强缓存和协商缓存二:本地存储——从 Cookie 到 Web Storage、IndexedDB 说明一下SessionStorage和localStorage还有cookie的区别和优缺点三:代码优化 1.事件代理 2.事件的节流和防抖 3.页面的回流和重绘 4.EventLoop事件循环机制 5.代码优化等等
什么是同源策略
跨域问题其实就是浏览器的同源策略造成的。
同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议、端口号、域名必须一致。
同源策略:protocol(协议)、domain(域名)、port(端口)三者必须一致。
同源政策主要限制了三个方面:
- 当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage 和 indexDB。
- 当前域下的 js 脚本不能够操作访问操作其他域下的 DOM。
- 当前域下 ajax 无法发送跨域请求。
同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。
网络劫持有哪几种,如何防范?
⽹络劫持分为两种:
(1)DNS劫持: (输⼊京东被强制跳转到淘宝这就属于dns劫持)
- DNS强制解析: 通过修改运营商的本地DNS记录,来引导⽤户流量到缓存服务器
- 302跳转的⽅式: 通过监控⽹络出⼝的流量,分析判断哪些内容是可以进⾏劫持处理的,再对劫持的内存发起302跳转的回复,引导⽤户获取内容
(2)HTTP劫持: (访问⾕歌但是⼀直有贪玩蓝⽉的⼴告),由于http明⽂传输,运营商会修改你的http响应内容(即加⼴告)
DNS劫持由于涉嫌违法,已经被监管起来,现在很少会有DNS劫持,⽽http劫持依然⾮常盛⾏,最有效的办法就是全站HTTPS,将HTTP加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容。
Node 中的 Event Loop 和浏览器中的有什么区别?process.nextTick 执行顺序?
Node 中的 Event Loop 和浏览器中的是完全不相同的东西。
Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
(1)Timers(计时器阶段):初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Pending callbacks 阶段。
(2)Pending callbacks:执行推迟到下一个循环迭代的I / O回调(系统调用相关的回调)。
(3)Idle/Prepare:仅供内部使用。
(4)Poll(轮询阶段):
- 当回调队列不为空时:会执行回调,若回调中触发了相应的微任务,这里的微任务执行时机和其他地方有所不同,不会等到所有回调执行完毕后才执行,而是针对每一个回调执行完毕后,就执行相应微任务。执行完所有的回调后,变为下面的情况。
- 当回调队列为空时(没有回调或所有回调执行完毕):但如果存在有计时器(setTimeout、setInterval和setImmediate)没有执行,会结束轮询阶段,进入 Check 阶段。否则会阻塞并等待任何正在执行的I/O操作完成,并马上执行相应的回调,直到所有回调执行完毕。
(5)Check(查询阶段):会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。
(6)Close callbacks:执行一些关闭回调,比如socket.on('close', ...)等。
下面来看一个例子,首先在有些情况下,定时器的执行顺序其实是随机的
代码语言:javascript复制setTimeout(() => { console.log('setTimeout')}, 0)setImmediate(() => { console.log('setImmediate')})
对于以上代码来说,setTimeout
可能执行在前,也可能执行在后
- 首先
setTimeout(fn, 0) === setTimeout(fn, 1)
,这是由源码决定的 - 进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行
setTimeout
回调 - 那么如果准备时间花费小于 1ms,那么就是
setImmediate
回调先执行了
当然在某些情况下,他们的执行顺序一定是固定的,比如以下代码:
代码语言:javascript复制const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
在上述代码中,setImmediate
永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate
回调,所以就直接跳转到 check 阶段去执行回调了。
上面都是 macrotask 的执行情况,对于 microtask 来说,它会在以上每个阶段完成前清空 microtask 队列,
代码语言:javascript复制setTimeout(() => {
console.log('timer21')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
})
对于以上代码来说,其实和浏览器中的输出是一样的,microtask 永远执行在 macrotask 前面。
最后来看 Node 中的 process.nextTick
,这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
对于以上代码,永远都是先把 nextTick 全部打印出来。
call apply bind
题目描述:手写 call apply bind 实现
实现代码如下:
代码语言:javascript复制Function.prototype.myCall = function (context, ...args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this; //this指向调用call的函数
// 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了
return context[fn](...args);
};
// apply原理一致 只是第二个参数是传入的数组
Function.prototype.myApply = function (context, args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this;
// 执行函数并返回结果
return context[fn](...args);
};
//bind实现要复杂一点 因为他考虑的情况比较多 还要涉及到参数合并(类似函数柯里化)
Function.prototype.myBind = function (context, ...args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this;
let _this = this;
// bind情况要复杂一点
const result = function (...innerArgs) {
// 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象
// 此时由于new操作符作用 this指向result实例对象 而result又继承自传入的_this 根据原型链知识可得出以下结论
// this.__proto__ === result.prototype //this instanceof result =>true
// this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true
if (this instanceof _this === true) {
// 此时this指向指向result的实例 这时候不需要改变this指向
this[fn] = _this;
this[fn](...[...args, ...innerArgs]); //这里使用es6的方法让bind支持参数合并
} else {
// 如果只是作为普通函数调用 那就很简单了 直接改变this指向为传入的context
context[fn](...[...args, ...innerArgs]);
}
};
// 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
// 实现继承的方式: 使用Object.create
result.prototype = Object.create(this.prototype);
return result;
};
//用法如下
// function Person(name, age) {
// console.log(name); //'我是参数传进来的name'
// console.log(age); //'我是参数传进来的age'
// console.log(this); //构造函数this指向实例对象
// }
// // 构造函数原型的方法
// Person.prototype.say = function() {
// console.log(123);
// }
// let obj = {
// objName: '我是obj传进来的name',
// objAge: '我是obj传进来的age'
// }
// // 普通函数
// function normalFun(name, age) {
// console.log(name); //'我是参数传进来的name'
// console.log(age); //'我是参数传进来的age'
// console.log(this); //普通函数this指向绑定bind的第一个参数 也就是例子中的obj
// console.log(this.objName); //'我是obj传进来的name'
// console.log(this.objAge); //'我是obj传进来的age'
// }
// 先测试作为构造函数调用
// let bindFun = Person.myBind(obj, '我是参数传进来的name')
// let a = new bindFun('我是参数传进来的age')
// a.say() //123
// 再测试作为普通函数调用
// let bindFun = normalFun.myBind(obj, '我是参数传进来的name')
// bindFun('我是参数传进来的age')
什么情况会阻塞渲染?
首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。
当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染。
NaN 是什么,用 typeof 会输出什么?
Not a Number,表示非数字,typeof NaN === 'number'