jchappytime / front-end-knowledge-share Goto Github PK
View Code? Open in Web Editor NEW前端知识,面试题,一些好用的工具分享。欢迎✨
前端知识,面试题,一些好用的工具分享。欢迎✨
1. 【前端体系】从一道面试题谈谈对EventLoop的理解
2. JavaScript 运行机制详解:再谈Event Loop
3. 详解JavaScript中的Event Loop(事件循环)机制
4. 再话js的事件循环机制
5. JavaScript中的Event Loop(事件循环)机制
JavaScript的单线程与它的用途紧密相关。作为浏览器的脚本语言,JavaScript 的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。如果说JavaScript同时又2个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这个时候浏览器该如何处理呢?
所以,为了避免复杂性,从一开始,JavaScript就是单线程,这已经成为了这门语言的核心特点,未来也不会改变。
为了利用多核CPU的计算能力,HTML5提出了WEB Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,并且不允许操作DOM。所以这个标准并没有改变JavaScript单线程的本质。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。但是很多时候CPU是处于空闲状态的,因为IO设备很慢,比如说Ajax获取数据,这个时候就不得不等到结果出来之后,再往下继续执行。
这个时候JavaScript的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到设备返回了结果,再回过头,把挂起的任务继续执行下去。
因此,所有的任务分成了2种,一种是同步任务(synchronous),一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务是指,不进入主线程、而进入“任务队列”(task queue)的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体的运行机制如下:
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。如下图所示:
上图,在主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码会调用各种外部的API,它们在“任务队列”中加入各种事件(click, done, load)。一旦栈中的代码执行完成,主线程就会去读取“任务队列”,一次执行其中的事件对应的回调函数。执行栈中的代码(同步任务),总是在读取“任务队列”(异步任务)之前执行。
总结起来:Event Loop会循环不断的去拿宏任务队列中最老的一个任务,推入队列中执行,并在当次循环里一次执行并清空microtask队列里的任务,执行完microtask队列里的任务之后,更新渲染。也就是:
这两种任务都是在任务队列的异步事件中注册的回调函数。那为什么要引入宏任务与微任务,只有一种类型的任务可不可以呢?
页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。不同的异步任务被分为:宏任务和微任务。
宏任务:
微任务:
异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务与微任务队列中去。在当前执行栈为空时,主线程会查看微任务队列是否有事件存在:
为了方便理解,我们可以认为在任务队列里面有宏任务和微任务;宏任务有多个,微任务只有一个。执行顺序应该是:
所以总体是:宏任务优先,在宏任务执行完毕之后才会来一次性清空任务队列中的所有微任务。
// 代码一执行就开始执行了一个宏任务-宏0
console.log('script start');
setTimeout(() => { // 宏1
console.log('setTimeout');
}, 1* 2000)
Promise.resolve().then(function() {
console.log(promise1); // 微1-1
})
.then(function() {
console.log(promise2); // 微1-4,这个then中的会等待上一个then执行完毕之后得到其他状态才会Queue注册状态对应的回调,假设上一个then中主动抛出错误且没有被捕获,那就注册的是这个then中的第二个回调了。
})
async function foo() {
await bar(); // await是promise的语法糖,会异步等待获取其返回值
// 后面的代码可以理解为放到异步队列微任务中。
console.log('async1 end');
}
foo();
function bar() {
console.log('async2 end');
}
async function errorFunc() {
try {
await Promise.reject('error!');
}.catch(e) {
// 从这后面开始,所有的代码可以理解为放到微任务队列中去
console.log(e); // 微1-3
}
console.log('async1');
return Promise.resolve('async1 success');
}
errorFunc().then(res => console.log(res)); // 微1-5
console.log('script end');
PS:注意一点就是Promise.then().then(),在注册异步任务的时候,第二个then中的回调是依赖第一个then中回调的结果的,如果执行没有异常才会在该异步任务执行完成之后注册状态对应的回调。
全局一个宏任务执行,输出同步代码。挂载宏1,微1-1, 微1-2,微1-3, 微1-4。1-表示属于第一次轮询。输出的结果是:
script start->async2 end->script end
同步代码执行完毕,开始执行异步任务中的微任务队列中的代码。
微任务队列:只有一个队列且会在当前轮询一次性清空。
执行微1-1:promise1
执行微1-2:async1 end
执行微1-3:error!, async1。当前异步回调执行完毕才Promise.resolve('async1 success'),然后注册.then()中的成功的回调:微1-5
执行微1-4:promise2
执行刚注册的微1-5:async1 success
到这里,第一波轮询结束。
开启第二波轮询:执行宏1
run: setTimeout
到此为止,整个轮询结束。其实相对难以理解的也就是微任务,对于微任务也就是上面说的只有一个队列会在此次轮询中一次清空(包括此次执行过程中所产生的微任务)。
整个打印的顺序为:script start -> async2 end ->script end -> promise1 -> async1 end -> error! -> async1 -> promise2 -> async1 success -> setTimeout
通过上面的分析,再来看一些类似的题目。
console.log(1)
setTimeout(function() {
//settimeout1
console.log(2)
}, 0);
const intervalId = setInterval(function() {
//setinterval1
console.log(3)
}, 0)
setTimeout(function() {
//settimeout2
console.log(10)
new Promise(function(resolve) {
//promise1
console.log(11)
resolve()
})
.then(function() {
console.log(12)
})
.then(function() {
console.log(13)
clearInterval(intervalId)
})
}, 0);
//promise2
Promise.resolve()
.then(function() {
console.log(7)
})
.then(function() {
console.log(8)
})
console.log(9)
打印顺序是:1 -> 9 -> 7 -> 8 -> 2 -> 3 ->10 -> 11 -> 12 -> 13
解析:
注意:由于在执行microtask任务的时候,只有当microtask队列为空的时候,它才会进入下一个事件循环,因此,如果它源源不断地产生新的microtask任务,就会导致主线程一直在执行microtask任务,而没有办法执行macrotask任务,这样我们就无法进行UI渲染/IO操作/ajax请求了,因此,我们应该避免这种情况发生。在nodejs里的process.nexttick里,就可以设置最大的调用次数,以此来防止阻塞主线程。
为什么在第一次Event Loop时,不先执行macrotask,因为按照流程的话,不应该是先检查macrotask队列是否为空,再检查microtask队列吗?
因为一开始Js主线程中跑的任务就是macrotask任务,而根据事件循环的流程,一次事件循环只会执行一个macrotask任务,所以执行完主线程的代码之后,它就去从microtask队列里面取队首的任务来执行。
定时器是否是真实可靠的呢?比如我执行一个命令:setTimeout(task, 1000),他是否就能准确的在1000毫秒后执行呢?
实根据以上的讨论,我们就可以得知,这是不可能的。
因为你执行setTimeout(task,100)后,其实只是确保这个任务,会在100毫秒后进入macrotask队列,但并不意味着他能立刻运行,可能当前主线程正在进行一个耗时的操作,也可能目前microtask队列有很多个任务,所以这也可能是大家一直诟病setTimeout的原因吧。
在我们日常编写CSS的时候,都会有一个疑惑就是:CSS选择器的优先级问题。我们先来看一小段代码:
.text-size {
font-size: 15px !important;
}
这个时候,!important是具有最高优先级的,而且相比较内联的样式而言,它的优先级会更高。那么除了!important之外的其他选择器呢?
我们把选择器的名称以及权重列出来看一下。
选择器名称 | 权重 |
---|---|
内联样式 | 1000 |
ID选择器 | 100 |
类选择器(包括属性选择器和伪类) | 10 |
标签和伪元素选择器 | 1 |
组合符和通配符 | 0 |
根据表中给出来的权重值,对于我们在编写CSS的时候就会有一个比较明确的顺序了:
.text-box > span {
height: 100px; // 权重值=10+1=11(类选择器+标签)
}
.text-box span:last-child {
font-size: 18px; // 权重值=10+10+1=21(类选择器+伪类+伪元素选择器)
}
.text-box div>span:first-child {
color: red; // 权重值=10+10+1+1=22(类选择器+标签+标签+伪元素选择器)
}
要想对前端性能进行实质上的优化,就需要清楚的知道:从输入URL到页面展示出来这个过程发生了什么?从这个过程中的某些环节来具体的了解一下性能优化。
这是一道经典的前端面试题目,重要性不必多说,它的大致流程如下:
构建DOM树的大致流程如下:
与HTML的转换类似,浏览器会去识别CSS正确的令牌,然后将这些令牌转化为CSS节点;
PS:子节点会继承父节点的样式规则,这里对应的就是层叠规则和层叠样式表。
有了DOM和CSSOM,接下来就可以合成布局树(Render Tree)了。
等 DOM 和 CSSOM 都构建好之后,渲染引擎就会构造布局树。布局树的结构基本上就是复制 DOM 树的结构,不同之处在于 DOM 树中那些不需要显示的元素会被过滤掉,如 display:none 属性的元素、head 标签、script 标签等。
复制好基本的布局树结构之后,渲染引擎会为对应的 DOM 元素选择对应的样式信息,这个过程就是样式计算。
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成。
现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。
通过样式计算和计算布局就完成了最终布局树的构建。再之后,就该进行后续的绘制操作了。
到这里,浏览器的渲染过程就基本结束了。
通常一个页面有三个阶段:加载阶段、交互阶段和关闭阶段。
1. 从ES6重新认识JavaScript设计模式(一): 单例模式
单例模式是一种非常常用但却相对而言比较简单的设计模式。它是指在一个类中只能有一个实例,即使多次实例化该类,也只能返回第一次实例化后的对象。单例模式不仅能减少不必要的内存开销,并且在减少全局的函数和变量冲突也具有重要的意义。
单例模式能保证一个类只有一个实例,并且提供一个访问它的全局访问点。也就是说,在整个生命周期中,该对象的生产都始终是一个,不曾变化。
它的特点是:
即使目前我们对单例模式还比较模糊,但在我们日常开发中也早就已经使用过单例模式了。看下面的例子:
let timeTool = {
name: '时间处理工具',
getISODate: function() {},
getUTCDate: function() {},
};
以对象字面量创建对象的方式在js开发中很常见。上面的例子就是以对象字面量的方式来封装了一些方法处理时间格式。全局只暴露了一个timeTool
对象,在需要使用时,只需要采用timeTool.getISODate()
调用即可。timeTool
对象就是单例模式的体现。在JavaScript创建对象的方式十分灵活,可以直接通过对象字面量的方式实例化一个对象,而其他面向对象的语言必须使用类进行实例化。所以这里的 timeTool
已经是一个实例,且ES6中 let
和 const
不允许重复声明的特性,确保了timeTool
不能被重新覆盖。
采用对象字面量创建单例只能适用于简单的应用场景,一旦该对象十分复杂,那么创建对象本身就需要一定的耗时,且该对象可能需要一些私有变量和私有方法。此时使用对象字面量创建单例就不再行得通了,我们还是需要采用构造函数的方式实例化对象。下面就是使用立即执行函数和构造函数的方式改造上面的timeTool
工具。
let timeTool = (function() {
let _instance = null;
function init() {
let now = new Date(); // 私有变量
// 公用属性和方法
this.name = '时间处理工具';
this.getISODate = function() {
return now.toISOString();
}
this.getUTCDate = function() {
return now.toUTCString();
}
}
return function() {
if (!_instance) {
_instance = new init();
}
return _instance;
}
})()
这时的timeTool
实际上是一个函数,_instance作为实例对象最开始赋值为null
,init
函数是其构造函数,用于实例化对象,立即执行函数返回的是匿名函数,用于判断实例是否创建,只有当调用timeTool()
时进行实例化,这就是惰性单例的应用,不在js加载时就进行实例化创建,而是在需要的时候再进行单例的创建。如果再次调用,那么返回的永远是第一次实例化后的实例对象。
let ins1 = timeTool();
let ins2 = timeTool();
console.log(ins1 === ins2); // true
一个项目往往都不止一个程序员来开发和维护,一个程序员很难去弄清楚另外一个程序员暴露在项目中的全局变量和方法。如果将变量和方法都暴露在全局中,变量冲突是难以避免的。就像下面的事故一样:
// 开发者A写了一大段js代码
function addNum() {}
// 另一个开发者B开始写js代码
var addNum = '';
// A重新维护该js代码
addNum(); // 这个时候抛出错误:Uncaught TypeError: addNum is not a function
命名空间就是用来解决全局变量冲突的问题,我们完全可以只暴露一个对象名,将变量作为该对象的属性,将方法作为该对象的方法,这样就能大大减少全局变量的个数。
// 开发者A写了一大段js代码
let devA = {
addNum() {};
};
// 另一个开发者B开始写js代码
let devB = {
addNum: '';
}
// A重新维护该js代码
devA.addNum();
上面的代码中,devA
和devB
就是两个命名空间,采用命名空间可以有效减少全局变量的数量,以此解决变量冲突的发生。
上面讲到的timeTool
对象时一个只用来处理时间的工具库,但是实际开发过程中的库可能会有多种多样的功能,例如处理ajax请求,操作dom或者处理事件。这个时候单例模式还可以用来管理代码库中的各个模块,如下所示:
var devA = (function() {
// ajax模块
var ajax = {
get: function(api, obj) { console.log('ajax get调用'); },
post: function(api, obj) {}
};
// dom模块
var dom = {
get: function() {},
create: function() {},
};
// event模块
var event = {
add: function() {},
remove: function() {},
};
return {
ajax: ajax,
dom: dom,
event: event,
};
})()
上面的代码库中有ajax
, dom
和event
三个模块,用同一个命名空间devA
来管理。在进行相应的操作的时候,只需要devA.ajax.get()
进行调用即可。这样可以让库的功能更加清晰。
ES6创建对象时引入了class
和constructor
来创建。下面我们来看看如何使用ES6语法实例化苹果公司。
class Apple {
constructor(name, creator, products) {
this.name = name;
this.creator = creator;
this.products = products;
}
}
let appleCompany = new Apple('苹果公司', ‘乔布斯’, ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = new Apple('苹果公司', ‘李四’, ['iPhone', 'iMac', 'iPad', 'iPod']);
显然,在全世界范围内,苹果公司有且只有一个。所以appleCompany
应该是一个单例,现在我们使用ES6语法将constructor
改写为单例模式的构造器。
class SingletonApple {
constructor(name, creator, products) {
if(!SingletonApple .instance) {
this.name = name;
this.creator = creator;
this.products = products;
// 将this挂载到SingletonApple这个类的instance属性上
SingletonApple.instance = this;
}
return SingletonApple.instance;
}
}
let appleCompany = new SingletonApple('苹果公司', ‘乔布斯’, ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = new SingletonApple('苹果公司', ‘李四’, ['iPhone', 'iMac', 'iPad', 'iPod']);
console.log(appleCompany === copyApple); // true
ES6为class
提供了static
关键字定义的静态方法,我们可以将constructor
中判断是否实例化的逻辑放入一个静态方法getInstance
中,调用该静态方法获取实例,constructor
中只包含需要实例化的代码,这样可以增强代码的可读性、结构更加优化。
class SingletonApple {
constructor(name, creator, products) {
this.name = name;
this.creator = creator;
this.products = products;
}
// 静态方法
static getInstance(name, creator, products) {
if(!this.instance) {
this.instance = new SingletonApple(name, creator, products);
}
return this.instance;
}
}
let appleCompany = new SingletonApple('苹果公司', ‘乔布斯’, ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = new SingletonApple('苹果公司', ‘李四’, ['iPhone', 'iMac', 'iPad', 'iPod']);
console.log(appleCompany === copyApple); // true
var Singleton = function(name) {
this.name = name;
}
Singleton.prototype.getName = function() {
alert(this.name);
}
// 利用闭包创建符合惰性的单例
Singleton.getInstance = (function(name)) {
var instance;
return function(name) {
if (!instance) {
instance = new Singleton(name);
}
}
})();
var a = Singleton.getInstance('test1');
var b = Singleton.getInstance('test2');
console.log(a === b); // true
// 反面的单例模式例子
var CreateDiv = (function() {
var instance;
var CreateDiv = function(html) {
if (instance) {
return instance;
}
this.html = html;
this.init();
return instance = this;
};
CreateDiv.prototype.init = function() {
var div = document.createElement('div');
div.innerHTML = this.html;
document.body.appendChild(div);
}
return CreateDiv;
})();
var a = new CreateDiv('test1');
var b = new CreateDiv('test2');
这种形式的单例模式特点:
为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,当然这增加了程序的复杂性。
CreateDiv构造函数做了两件事。
登陆弹框在项目中是一个比较经典的单例模式,因为对于大部分网站不需要用户必须登陆才能浏览,所以登陆操作的弹框可以在用户点击登陆按钮后再进行创建。而且登陆框永远只有一个,不会出现多个登陆框的情况,也就意味着再次点击登录按钮后返回的永远是一个登录框的实例。
我们梳理一下登录框的流程,然后再实现。
因为5和6是登录框的实际项目逻辑,与单例模式关系不大,所以这里主要实现1-4步。
<nav class="top-bar">
<div class="top-bar_left">
TEST
</div>
<div class="top-bar_right">
<div class="login-btn">登录</div>
<div class="signin-btn">注册</div>
</div>
</nav>
class Login {
//构造器
constructor() {
this.init();
}
//初始化方法
init() {
//新建div
let mask = document.createElement('div');
//添加样式
mask.classList.add('mask-layer');
//添加模板字符串
mask.innerHTML =
`
<div class="login-wrapper">
<div class="login-title">
<div class="title-text">登录框</div>
<div class="close-btn">×</div>
</div>
<div class="username-input user-input">
<span class="login-text">用户名:</span>
<input type="text">
</div>
<div class="pwd-input user-input">
<span class="login-text">密码:</span>
<input type="password">
</div>
<div class="btn-wrapper">
<button class="confrim-btn">确定</button>
<button class="clear-btn">清空</button>
</div>
</div>
`;
//插入元素
document.body.insertBefore(mask, document.body.childNodes[0]);
//添加关闭登录框事件
Login.addCloseLoginEvent();
//静态方法: 获取元素
static getLoginDom(cls) {
return document.querySelector(cls);
}
//静态方法: 注册关闭登录框事件
static addCloseLoginEvent() {
this.getLoginDom('.close-btn').addEventListener('click', () => {
//给遮罩层添加style, 用于隐藏遮罩层
this.getLoginDom('.mask-layer').style = "display: none";
})
}
//静态方法: 获取实例(单例)
static getInstance() {
if(!this.instance) {
this.instance = new Login();
} else {
//移除遮罩层style, 用于显示遮罩层
this.getLoginDom('.mask-layer').removeAttribute('style');
}
return this.instance;
}
}
// 注册点击事件
Login.getLoginDom('.login-btn').addEventListener('click', () => {
Login.getInstance();
})
分析:
在上面的登录框中,实现了只创建一个Login类,但是却实现了一个并不简单的登录功能。在第一次点击登录按钮的时候,我们调用Login.getInstance()
实例化了一个登录框,且在之后的点击中并没有重新创建新的登录框,只是移除掉了display:none
这个样式来显示登录框,节省了内存开销。
单例模式虽然简单,但是在项目中的应用场景却是相当多的,单例模式的核心是确保只有一个实例,并提供全局访问。就像我们只需要一个浏览器的window
对象,jQuery的$
对象也不再需要第二个。由于JavaScript代码书写方式十分灵活,这也导致了如果没有严格的规范的情况下,大型的项目中JavaScript不利于多人协同开发,使用单例模式进行命名空间来管理模块是一个很好的开发习惯,能够有效的解决协同开发变量冲突的问题,灵活使用单例模式,也能够减少不必要的内存开销,提高使用体验。
hash是跟整个项目构建相关,只要项目里面有文件修改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值。
使用webpack编译代码时,我们可以在js文件里面引用css文件。所以这2个文件应该共用相同的chunkhash值,但是有个问题:如果js更改了代码,css文件就算内容没有任何改变,由于该模块发生了改变,这导致CSS会重复构建,这个时候,我们可以使用 extra-text-webpack-plugin 里面的contenthash值,保证即使CSS文件所处的模块里其他文件内容改变,只要CSS文件内容不变,那么就不会重复构建。
采用hash计算的话,每一次构建后生成的哈希值都不一样,即使文件内容压根没有改变。这样子时没办法实现缓存效果,我们需要换另一种hash值计算方法,也就是chunkhash。它会根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值,那么只要我们不改动公共库代码,就可以保证其哈希值不会受影响。
随着项目体量的增加,如果前端的构建+部署全部由人工去处理,这无疑会增加人力成本,同时随着人员增多,各种各样的问题也会暴露出来。所以对于实现持续集成,自动化部署的要求会越来越高。
1. windows下利用Docker+Jenkins+GitHub搭建可持续化构建环境
2. 实战笔记:Jenkins打造强大的前端自动化工作流
3. Github配合Jenkins,实现vue等前端项目的自动构建与发布
1. JavaScript 垃圾回收机制
2. 深入理解 V8 的垃圾回收原理
3. V8 之旅: 垃圾回收器
垃圾回收又称为 GC 。在 JavaScript 编码过程中,开发者不需要手动跟踪内存的使用情况,只需要按照要求的标准写 JavaScript 代码,程序运行所需内存的分配以及无用内存的回收完全是自动管理的。其原理是:
局部变量只在函数执行的过程中存在。
在函数执行的过程中,会为局部变量在栈内存(或者堆内存)上分配相应的空间来存储它们的值。在函数中使用这些变量,直到函数执行结束,此时可以释放局部变量的内存供将来需要时使用。
以上情况可以比较容易地判断变量是否有存在的必要,更复杂的情况需要更精细的变量追踪策略。
JavaScript 中的垃圾回收器必须跟踪每个变量是否有用,需要为不再用的变量打上编辑,用于将来回收其占用的内存。标识无用变量的策略通常有2个:标记清除和引用计数。
mark-and-sweep即是标记清除,也是JS中最为常用的垃圾回收方式,其执行机制如下:
reference counting 即是引用计数,它是另外一种垃圾回收策略。引用计数的本质是跟踪记录每个值被引用的次数,其执行机制如下:
function cycleReference() {
let objA = new Object();
let objB = new Object();
objA .someOtherObject = objB;
objB .someOtherObject = objA;
}
上述例子中objA和objB通过各自属性相互引用。按照引用计数的策略,两个对象的引用次数均为2.若采用标记清除策略,函数执行完毕,对象离开作用域就不存在相互引用。但采用引用计数后,函数执行完,两个对象的引用次数永不为0,会一直存在内存中,如果多次调用,导致大量内存得不到释放。
IE8浏览器 之前中有一部分对象并不是原生的 JavaScript 对象,可能是使用 C++ 以 COM 对象的形式实现的(BOM, DOM)。而 COM 对象的垃圾收集机制采用的是 引用计数策略。使 IE 的 JavaScript 引擎是使用标记清除策略实现的,但 JavaScript 访问 COM 对象仍然是基于 引用计数策略的。在这种情况下,只要在 IE 中涉及 COM 对象,就可能存在循环引用的问题。
那么该如何避免这种情况呢?
为了避免循环引用,最好在不使用这些对象时,手动断开原生的JavaScript对象与DOM元素之间的连接。IE中的循环引用与手动断开的操作如下所示:
// 存在循环引用的情况
let element = document.getElementById('some_element');
let myObj = new Object();
myObj.element = element;
element.someObj = myObj;
// 手动断开循环引用
myObj.element = null;
element.someObj = null;
PS:将变量设为null即可切断变量与它之前引用的值之间的连接。下次垃圾收集器运行时,会删除这些值并回收它们占用的内存。为了解决上述问题,IE9及以上版本把BOM和DOM对象都转换成了真正的 JavaScript 对象,避免了两种垃圾回收算法并存引起的问题。
垃圾收集器是周期运行的,确定垃圾收集的时间间隔是个非常重要的问题。IE7之前的垃圾收集器是根据内存分配量运行的,即256个变量、4096个对象(数组)字面量或64KB的字符串。达到这些临界值的任何一个,垃圾收集器就会运行。所以就导致如果一个脚本含有很多变量,在整个生命周期中一直保有前面临界值大小的变量,就会频繁触发垃圾回收,会存在严重的性能问题。
IE7重写了垃圾收集历程。新的工作方式为:触发垃圾收集的变量分配、字面量和数组元素的临界值被调整为动态修正。初始值与之前版本相同,如果垃圾收集例程回收的内存低于15%,则临界值加倍。若回收内存分配量超过85%,则临界值重置为默认值。
在JavaScript脚本中,绝大多数对象的生存期很短,只有部分对象的生存期较长,所以V8中的垃圾回收主要使用的是 分代回收(Generational collection)机制。
V8引擎将保存对象的堆(heap)进行了分代:
由于垃圾清理发生的比较频繁,清理的过程必须很快。V8中的清理过程使用的是 Scavenge 算法,按照比较经典的 Cheney算法(https://m.tqwba.com/x_d/jishu/428898.html)来实现的。Scavenge 算法的主要过程是:
如果新生区有某个对象,只有一个指向它的指针,恰好该指针在老生区的对象中,在垃圾回收之前我们如何得知新生区的该对象是否活跃呢?
为解决此问题,V8在写缓冲区有一个列表,其中记录了所有老生区对象指向新生区的情况。新生区对象诞生时不会有指向它的指针,当老生区的对象出现指向新生区对象的指针时,便记录跨区指向,记录行为总是发生在写操作中。
因为新生区的内存一般都不大,所以使用 Scavenge 算法进行垃圾回收效果比较好。老生区一般占用内存较大,因此采用的是 标记-清除(Mark-Sweep)算法 与 标记-紧缩(Mark-Compact)算法。两种算法都包括两个阶段:标记阶段,清除或紧缩阶段。
在标记阶段,堆上所有的活跃对象都会被发现并且标记。
在使用CSS隐藏一些元素的时候,我们经常用到display: none和visibility: hidden。那么2者之间有什么不一样呢。
本文主要介绍几个在前端工程构建过程中非常显著的一些插件,帮助提升webpack的构建速度以及深入分析影响速度的原因。但在继续下面的内容之前,也需要充分的了解一下webpack的实现原理,这篇文章已经叙述得非常仔细,我也先去膜拜一下再来继续了。
webpack有时候打包很慢,因为我们在项目中可能使用了非常多的plugin和loader,想知道具体是哪个环节慢,下面这个插件就可以计算plugin和loader的耗时。
yarn add -D speed-measure-webpack-plugin
配置也很简单,将webpack配置对象包裹起来即可:
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins: [
new plugin1(),
new plugin2()
]
});
在项目中华引入speed-measure-webpack-plugin后,它完成的主要工作是:
打包后的体积优化是一个可以着重优化的方向,比如引入的一些第三方组件库体积过大,这个时候就要考虑是否寻求替代品了。
这里采用的是webpack-bundle-analyzer,它可以用交互式可缩放树形图来显示webpack输出文件的大小,用起来非常方便。
安装插件:
yarn add -D webpack-bundle-analyzer
安装完成之后再在webpack.config.js中简单的配置一下:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
// 可以是server, static或者disabled
// 在server模式下:分析器会启动HTTP服务器来显示软件报告;
// 在static模式下:会生成带有报告的单个HTML文件;
// 在disabled模式下:可以使用这个插件将generateStatsFile设置为true来生成Webpack Stats JSON文件;
analyaerMode: 'server',
// 将在“服务器”模式下使用的主机启动HTTP服务器。
analyzerHost: "127.0.0.1",
// 将在“服务器”模式下使用的端口启动HTTP服务器。
analyzerPort: 8866,
// 路径捆绑,将在`static`模式下生成的报告文件。
// 相对于捆绑输出目录。
reportFilename: "report.html",
// 模块大小默认显示在报告中。
// 应该是`stat`,`parsed`或者`gzip`中的一个。
// 有关更多信息,请参见“定义”一节。
defaultSizes: "parsed",
// 在默认浏览器中自动打开报告
openAnalyzer: true,
// 如果为true,则Webpack Stats JSON文件将在bundle输出目录中生成
generateStatsFile: false,
// 如果`generateStatsFile`为`true`,将会生成Webpack Stats JSON文件的名字。
// 相对于捆绑输出目录。
statsFilename: "stats.json",
// stats.toJson()方法的选项。
// 例如,您可以使用`source:false`选项排除统计文件中模块的来源。
statsOptions: null,
logLevel: "info"
],
};
如果想要查看更多选项,请参照官方文档
然后,我们在控制台中输入: npm run dev / yarn dev,它就会默认起一个端口号为8888的本地服务器,图中的每一块都清晰的展示了组件、第三方库的代码体积。有了它之后,我们就能针对体积偏大的模块进行相关的优化了。
安装:
yarn add -D hapyppack
HappyPack可以让webpack同一时间处理多个任务,发挥多核CPU的能力,将任务分解给多个子进程去并发的执行,子进程处理完后,再把结果发送给主进程。通过多进程模型,来达到加速构建代码的目的。
示例:
// webpack.config.js
const HappyPack = require('happypack');
exports.module = {
rules: [
{
test: /.js$/,
// 1) replace your original list of loaders with "happypack/loader":
// loaders: [ 'babel-loader?presets[]=es2015' ],
use: 'happypack/loader',
include: [ /* ... */ ],
exclude: [ /* ... */ ]
}
]
};
exports.plugins = [
// 2) create the plugin:
new HappyPack({
// 3) re-add the loaders you replaced above in #1:
loaders: [ 'babel-loader?presets[]=es2015' ]
})
];
遗憾的是,HappyPack作者表示不再维护此项目了,同时他也推荐使用webpack官方提供的thread-loader,但thread-loader 和 happypack 对于小型项目来说打包速度几乎没有影响,甚至可能会增加开销,所以建议尽量在大项目中采用。
一般而言,在我们的开发环境中,代码构建时间比较快,而构建用于发布到线上的代码时会添加压缩这个流程,则会导致计算量大、耗时多。
webpack提供了UglifyJS插件来压缩JS代码,但是它使用的是单线程压缩代码,也就是说多个js文件需要被压缩,它需要一个个文件进行压缩。所以说在正式环境打包压缩代码的速度非常慢(因为压缩JS代码需要先把代码解析成用Object抽象表示的AST语法树,在应用各种规则分析和处理AST,导致这个过程耗时非常大。)
所以要对压缩代码这一块做优化,常用的方法就是多进程并行压缩。目前有三种压缩方案:
上面介绍的HappyPack的**是使用多个子进程去解析和编译JS,CSS等,这样就可以并行处理多个子任务,多个子任务完成后,再将结果发到主进程中,有了这个**后,ParallelUglifyPlugin 插件就产生了。
当webpack有多个JS文件需要输出和压缩时,原来会使用UglifyJS去一个个压缩并且输出,而ParallelUglifyPlugin插件则会开启多个子进程,把对多个文件压缩的工作分给多个子进程去完成,但是每个子进程还是通过UglifyJS去压缩代码。并行压缩可以显著的提升效率。
安装:
yarn add -D webpack-parallel-uglify-plugin
使用示例:
import ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin';
module.exports = {
plugins: [
new ParallelUglifyPlugin({
// Optional regex, or array of regex to match file against. Only matching files get minified.
// Defaults to /.js$/, any file ending in .js.
test,
include, // Optional regex, or array of regex to include in minification. Only matching files get minified.
exclude, // Optional regex, or array of regex to exclude from minification. Matching files are not minified.
cacheDir, // Optional absolute path to use as a cache. If not provided, caching will not be used.
workerCount, // Optional int. Number of workers to run uglify. Defaults to num of cpus - 1 or asset count (whichever is smaller)
sourceMap, // Optional Boolean. This slows down the compilation. Defaults to false.
uglifyJS: {
// These pass straight through to uglify-js@3.
// Cannot be used with uglifyES.
// Defaults to {} if not neither uglifyJS or uglifyES are provided.
// You should use this option if you need to ensure es5 support. uglify-js will produce an error message
// if it comes across any es6 code that it can't parse.
},
uglifyES: {
// These pass straight through to uglify-es.
// Cannot be used with uglifyJS.
// uglify-es is a version of uglify that understands newer es6 syntax. You should use this option if the
// files that you're minifying do not need to run in older browsers/versions of node.
}
}),
],
};
注意: webpack-parallel-uglify-plugin已不再维护,不推荐再继续使用。
安装:
yarn add -D uglifyjs-webpack-plugin
示例:
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
plugins: [
new UglifyJsPlugin({
uglifyOptions: {
warnings: false,
parse: {},
compress: {},
ie8: false
},
parallel: true
})
]
};
其实它和上面的parallel-uglify-plugin类似,也可通过设置parallel: true开启多进程压缩。
其实webpack4已经默认支持es6语法的压缩,这离不开terser-webpack-plugin。
安装:
yarn add -D terser-webpack-plugin
示例:
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: 4,
}),
],
},
};
想象一个场景,两个前端项目组 A 和 B 同时上线2个项目,比如 A 上线的项目叫做”牛云聊“,B 上线的项目叫做”牛外卖“。结果上线当天,A 只用了1个小时实现打包上线,而 B 却用了一天时间打包上线。这时候我们可以想象一下B项目经理脸上的表情。
实际上,在上线前我们是需要对webpack打包进行一定优化的,那么我们该从哪些方向来进行优化呢?请继续往下看。
如果你们项目组正在使用 webpack4.x 的版本,那你应该或多或少了解到 4.x 版本的webpack在生产环境下会对代码做自动的tree shaking。但是,可能当你读完这篇文章你的Tree-Shaking并没什么卵用之后,你会有不一样的看法,你的Tree-Shaking不一定真的有用!
总结一下tree-shaking的原理就是:
我们都知道,函数式编程的副作用就是,一个函数可能会对函数外部的变量产生影响,而这个影响就是函数的副作用。举个例子吧:
function goBack() {
window.location.href = '/home';
}
可以看到,goBack()修改了全局变量,结果时为了让浏览器进行跳转,而修改全局变量的这个行为就可能会引发一些副作用。
所以,针对这个问题,国内外各路大神提出了很多解决办法,比如说Webpack 中的 sideEffects 到底该怎么用?;"sideEffects": false没有使打包后的bundle减少
比如说,一个项目中有个lib目录下放着自己编写的函数,分析之后发现它被重复打包到了业务代码的js文件中。这种情况该如何优化呢?
splitChunks: {
chunks: 'all',
automaticNameDelimiter: '.',
name: undefined,
cacheGroups: {
default: false,
vendors: false,
common: {
test: function (module, chunks) {
// 这里通过配置规则只将 common lib moment公共依赖打包进入common中
if (/src\/common\//.test(module.context) ||
/src\/lib/.test(module.context) ||
/cube-ui/.test(module.context) ||
/better-scroll/.test(module.context)) {
return true;
}
},
chunks: 'all',
name: 'common',
// 这里的minchunks 非常重要,控制moment使用的组件被超过几个chunk引用之后才打包进入common中,否则不打包进去
minChunks: 2,
priority: 20
},
vendor: {
chunks: 'all',
test: (module, chunks) => {
// 将node_modules 目录下的依赖统一打包进入vendor中
if (/node_modules/.test(module.context)) {
return true;
}
},
name: 'vendor',
minChunks: 2,
// 配置chunk的打包优先级,这里的数值决定了node_modules下的 moment不会被打包到 vendor 中
priority: 10,
enforce: true
}
}
}
为了避免影响打包速度,在项目代码中就是不要将使用频率低,体积大的组件引入到这个文件中。类似于datepicker这种大型的组件,就在对应需要使用的页面中引入即可。然后再webpack配置中,通过设置minChunk来指定当这些较大的组件引用超过多少次之后才能打包到common中,否则就单独打包到对应页面的js中。
因此,优化对于第三方依赖组件的加载方式,以减少不必要的加载和执行时间的损耗。
webpack4.x会根据以下条件自动分割代码块:
splitChunks: {
// 默认:用于异步chunk,值为all
// initial模式下会分开优化打包异步和非异步模块。all会把异步和非异步模块同时进行优化,也就是意味着module1在index1中
// 异步引入,index2中同步引入,initial下module1会出现在两个打包块中,而all只会出现一个。
// all所有chunk代码(同步加载和异步加载模块都可以使用)的公共部分分离出来成为一个单独的文件
// async将异步模块代码公共部分抽离出来成为一个单独的文件
chunks: async,
minSize: 30000, // 默认值是30kb,当文件体积>=minSize时将会被拆分为2个文件,否则不生成新的chunk
minChunks: 1, // 共享该module的最小chunk数(当>=minChunks时才会被拆分为新的chunk)
maxAsyncRequests: 5, // 最多有5个异步加载请求该module
maxInitialRequests: 3, // 初始会话时最多有3个请求该module
automaticNameDelimiter: '~', // 名字中间的间隔符
name: true, // 打包后的名称,如果设置为true默认时chunk的名字通过分隔符(默认时~)分隔开,如vendor~也可以自己手动
// 指定。
cacheGroups: { // 设置缓存组用来抽取满足不同规则的chunk,切割成的每一个新的chunk就是一个cache group
common: {
name: 'common', // 抽取的chunk名字
chunks: 'all', // 同外层的参数配置,覆盖外层的chunks,以chunk为维度进行抽取
// 1. 可以为字符串,正则表达式,函数,以module为维度进行抽取;
// 2. 只要是满足条件的module都会被抽取到该common的chunk下,为函数的第一个参数;
// 3. 遍历到的每一个模块,第二个参数是每一个引用到该模块的chunks数组
test(module, chunks) {
// module.context: 当前文件模块所属的目录,该目录下包含多个文件
// module.resource: 当前模块文件的绝对路径
if (/datepicker/.test(module.context)) {
let chunkName = ''; // 引用该chunk的模块名字
chunks.forEach(item => {
chunkName += item.name + ',';
});
console.log(`module-datePicker`, module.context, chunkName, chunks.length);
}
},
// 优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,数值高的优先被选择
priority: 10,
minChunks: 2, // 最少被几个chunk引用
reuseExistingChunk: true, // 如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码(当module未
// 发生变化时是否使用之前的mudule)
enforce: true, // 如果cacheGroup中没有设置minSize,据此判断是否使用上层的minSize,当为true时则使用0;为false时则使
// 用上层的minSize
},
},
},
这种情况一般会因为检查遗漏导致引入。举个例子:最开始写代码的时候,由于业务上的需要,导致要引入datepicker,但是设计突然更改了原型图,不需要选择日期了,这时候你注释掉了js里面对日期的各种操作,但是却遗漏了在import部分的引入。在webpack打包的时候,仍然会将datepicker打进去。
好的一点是,现在大部分的编辑器都会将这种不需要的引入进行提示,一般是颜色变灰。
lodash这个js库是我们日常开发中非常依赖的一个前端库,非常好用,但是好用的同时也存在一定的缺陷,就是全量引入后打包的体积较大。那我们能不能按需引入lodash呢?
答案是肯定的。我们可以在npm库上搜索lodash-es这个模块,通过阅读它的文档,我们可以将lodash导出为es6 modules,然后就可以通过import的方式单独导入某个函数来使用。
到底有没有必要优化lodash,其实这个存在一定的争议,可以参考lodash在webpack中的各项优化的尝试。其实优化就是根据自身的业务需求做出各种权衡后的妥协。
可以试想一下:在Vue开发中,我们的key是拿来做什么的?为什么不推荐用index作为key呢?接下来就将为你揭秘key的作用。
有这样一段HTML代码:
<ul>
<li>1</li>
<li>2</li>
</ul>
我们知道在Vue中,都是先将节点转换为虚拟DOM,等到真正渲染的时候通过diff算法进行比对之后,才会成功渲染成我们需要的页面,而这个虚拟DOM是什么呢?其实就是JavaScript的一个对象。
那么上面示例的vdom节点大概就是下面这种形式:
{
tag: 'ul',
children: [
{ tag: 'li', children: [ { vnode: { text: '1' } } ] },
{ tag: 'li', children: [ { vnode: { text: '2' } } ] },
],
}
var func = function(str) {
// 统计每个字母出现的次数
var obj = {};
for (let i = 0; i < str.length; i++) {
if (!obj[str.charAt(i)]) {
obj[str.charAt(i)] = 1
} else {
obj[str.charAt(i)] ++;
}
// obj 对象的key值是字符名,value是出现次数
// 找出value最大的key并输出就可以了
var max = 0, thisChar;
for (keyValue in obj) {
if (obj[keyValue] > max) {
max = obj[keyValue]
thisChar= keyValue
}
}
return thisChar;
}
}
这是几乎每场面试时面试官都会问到的问题,所以重要性可以毫无疑问的排第一。同时也是需要牢记在心的一个问题,它是我们在前端开发过程中几乎每天都会用到的基础点。
js可以分为两种类型的值,一种是基本数据类型,一种复杂数据类型。
两种类型的主要区别是它们的存储位置不同,基本数据类型的值直接保存在栈中,复杂数据类型的值保存在堆中,
通过使用在栈中保存对应的指针来获取堆中的值。
主要有以下四种方法:
typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。
typeof '';// string 有效
typeof 1;// number 有效
typeof Symbol();// symbol 有效
typeof true;//boolean 有效
typeof undefined;//undefined 有效
typeof null;//object 无效
typeof [] ;//object 无效
有的时候,typeof会返回一些令人不解但技术上却正确的结果:
instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型,我们用一段伪代码来模拟其内部执行过程:
instanceof (A, B) = {
var L = A.__proto__;
var R = B.prototype;
if(L === R) {
// A的内部属性 __proto__ 指向 B 的原型对象
return true;
}
return false;
}
从上述过程可以看出,当 A 的 proto 指向 B 的 prototype 时,就认为 A 就是 B 的实例,我们再来看几个例子:
[ ] instanceof Array; // true
{1: 'test', 2: 'test'} instanceof Object; // true
new Date() instanceof Date; // true
function Person(){};
new Person() instanceof Person; // true
[ ] instanceof Object;// true
new Date() instanceof Object; // true
new Person instanceof Object; // true
可以看出,instanceof可以判断出[ ]是Array的实例,但它也认为[ ]是Object的实例,这是为什呢? [ ]、Array、Object 三者之间存在怎样的关系呢?
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[0].Array;
var arr =new xArray(1,2,3); // [1,2,3]
arr instanceof Array; // false
针对数组的这个问题,ES5 提供了 Array.isArray() 方法 。该方法用以确认某个对象本身是否为 Array 类型,而不区分该对象在哪个环境中创建。
Array.isArray() 本质上检测的是对象的 [[Class]] 值,[[Class]] 是对象的一个内部属性,里面包含了对象的类型信息,其格式为 [object Xxx] ,Xxx 就是对应的具体类型 。对于数组而言,[[Class]] 的值就是 [object Array] 。
当一个函数 F被定义时,JS引擎会为F添加 prototype 原型,然后再在 prototype上添加一个 constructor 属性,并让其指向 F 的引用。当执行 var f = new F() 时,F 被当成了构造函数,f 是F的实例对象,此时 F 原型上的 constructor 传递到了 f 上,因此 f.constructor == F
可以看出,F 利用原型对象上的 constructor 引用了自身,当 F 作为构造函数来创建对象时,原型上的 constructor 就被遗传到了新创建的对象上, 从原型链角度讲,构造函数 F 就是新对象的类型。这样做的意义是,让新对象在诞生以后,就具有可追溯的数据类型。
''.constructor == String; // true
new Number(1).constructor == Number; // true
new Error().constructor == Error; // true
细节点:
toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。
Object.prototype.toString.call('') ; // [object String]
Object.prototype.toString.call(1) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
当问到这个问题的时候,你不应该仅仅知道在项目中使用了npm这一个东西,整个前端工程化的过程都用到了node,所以你首先应该了解node是如何支撑前端项目运行起来的。
资源太大,所以分享网盘链接给大家(不用担心,这是永久有效的)
链接: https://pan.baidu.com/s/10zLJ-DE04Z-5Ha4vJq04JA 提取码: 1ttj
对父容器使用display: table-cell + vertical-align: middle,使其内部的子元素实现垂直居中。
原理:父元素设置为表格的单元格元素,而在表格单元格中的元素设置:vertical-align: middle会使其以中间对齐的方式来显示。
.parent {
width: 200px;
height: 200px;
border: 1px solid;
display: table-cell;
vertical-align: middle;
}
.child{
width: 50px;
height: 50px;
background: blue;
}
利用给父元素设置相对定位,子元素设置绝对定位,margin: 0 auto 和 top: 0; bottom: 0;从而实现垂直居中。
原理:因为auto默认分配剩余空间,宽度相对window是固定的,所以margin: 0 auto;可以有水平居中的效果,而高度相对window并不是固定的,所以margin: auto 0;不能垂直居中,所以让子元素上下margin值不相对于window进行计算,改为相对父元素进行计算即可。如下:
.parent {
width: 200px;
height: 200px;
border: 1px solid;
position: relative;
}
child{
width: 200px;
height: 200px;
margin: auto 0;
position: absolute;
top: 0;
bottom: 0;
}
flex布局可以很方便的实现垂直与水平居中,好处很多,在移动端使用比较广泛,不好的地方就是浏览器兼容性不好。代码如下:
//html
<div class="main">
<div class="middle"></div>
</div>
//css
.main {
width: 60px;
height: 10%;
background: #dddddd;
display: flex;
justify-content: center;
align-items: center;
}
.middle{
width: 30%;
height: 50%;
background: red;
}
绝对定位/相对定位,在不知道自己高度和父容器高度的情况下,利用绝对定位.
.parent {
position: relative;
}
.children {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
//最简单的一种形式:一个父函数
function superFunc(){
// 局部变量
var _super_a = 1;
var subFunc = function(){
// _super_a++;
alert('_super_a: ' + _super_a);
}
return subFunc;
}
// superFunc() 得到的是subFunc,superFunc()()等于subFunc()
superFunc()();
首先,闭包是一个函数(上述示例代码中的subFunc),该函数必须有父函数(上述示例代码中的superFunc),然后调用了父函数的变量。
function outputNumbers(count){
(function(){
for(var i = 0; i < count; i++){
alert(i);
}
})();
alert(i); //导致一个错误!就是永远打印出的都是count的值
}
function MyObject(){
// 私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
// 特权方法,调用私有方法、函数
this.publicMethod = function(){
privateVariable++;
return privateFunction();
}
}
所有typeof返回值为Object的对象都包含一个内部属性。这个属性无法直接访问,一般通过Object.prototype.toString(..)
来查看。如:
Object.prototype.toString.call(['a', 'b', 'c'])
=>"[object Array]"
Object.prototype.toString.call(/regex-literal/i/);
=>"[object RegExp]"
但是我们自己创建的类就不会有这样的待遇,因为toString()找不到toStringTag属性,只能返回默认的Object标签。
(PS:有时候也可以用来判断数据的类型)
默认情况下,类的[[Class]]返回[object Object],如:
class Person {}
Object.prototype.toString.call(new Person());
=>"[object Object]"
**这个时候需要定制我们自己的[[Class]]
class Person1 {
get Symbol.toStringTag {
return 'Person1';
}
}
Object.prototype.toString.call(new Person1());
=> "[object Person1]"
深浅拷贝实际上针对的是复杂数据类型而言的,浅拷贝只复制对象的一层属性,深拷贝则复制了所有层级的属性。因为Js存储对象是存地址的,所以浅拷贝会导致obj1和obj2指向同一块内存地址,而深拷贝则是为对象开辟一块新的内存,复制对象进来,因此对于深拷贝而言,obj1和obj2的操作互不影响。
指的是将一个对象的属性值复制到另外一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用Object.assign()和展开运算符来实现(如:const {a, b, c} = {1, 2, {3, 4}}; 所以c = {3, 4})。
如何实现一个对象或者数组的浅拷贝。
想一想,好像很简单,遍历对象,然后把属性和属性值都放在一个新的对象就好了~
function shallowCopy(object) {
// 只拷贝对象
if (!object || typeof object !== "object") {
return;
}
// 根据object的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {};
// 遍历object,并且判断是object的属性才拷贝
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key];
}
}
return newObject;
}
相对于浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用JSON的两个函数来实现,但是由于JSON的对象格式化比js的对象格式化更加严格,所以如果属性值里面出现函数或者Symbol类型的值时,会转换失败。
那如何实现一个深拷贝呢?说起来也简单,我们在拷贝的时候判断一下属性值的类型,如果是对象,我们递归调用深拷贝函数不就好了~
function deepCopy(object) {
if (!object || typeof object !== "object") return;
let newObject = Array.isArray(object) ? [] : {};
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
}
}
return newObject;
}
//例1
var source = { name:"source", child:{ name:"child" } }
var target = JSON.parse(JSON.stringify(source));
target.name = "target"; //改变target的name属性
console.log(source.name); //source
console.log(target.name); //target
target.child.name = "target child"; //改变target的child
console.log(source.child.name); //child
console.log(target.child.name); //target child
//例2
var source = { name:function(){console.log(1);}, child:{ name:"child" } }
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
//例3
var source = { name:function(){console.log(1);}, child:new RegExp("e") }
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
console.log(target.child); //Object {}
这种方法使用较为简单,可以满足基本的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深拷贝(而且会直接丢失相应的值)。
还有一点不好的地方是它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。同时如果对象中存在循环引用的情况也无法正确处理。
es6推荐使用let作为局部变量的声明,相比之前的var(因为有变量提升,所以无论声明在何处,都会被视为声明在函数的最顶部),let和var声明的区别:
var x = '全局变量';
{
let x = '局部变量';
console.log(x); // 局部变量
}
console.log(x); // 全局变量
let表示声明变量,const表示声明常量(意味着它的值在设置完成之后就不能再修改了),两者都是块级作用域;如果const是一个对象,对象所包含的值是可以被修改的。也就是说,对象所指向的地址没有改变。
const person = { name: 'Jessica' };
person.name = 'Nick'; // 不会报错
person = { name: 'Nick' }; // 会报错,地址修改了
因此,需要我们注意:
在es6之前我们是怎样处理模板字符串的呢?通过''和'+'来构建。
'I am a string, you are a number, so we are different, right? \
He is a boolean, he is different with us. His value is\
' + value_boolean + ', my value is ' + value_string + '.';
对于es6而言:
`'I am a string, you are a number, so we are different, right?He is a boolean, he is different with us. His value is${value_boolean }, my value is ${value_string} .`
箭头函数实际上就是函数的一种简写形式,使用括号包裹参数,再使用箭头(=>),再接上函数体。它最直观的三个特点是:
// es5
var func = function(a, b) {
return a + b;
}
[1, 2, 3].map((function(item) {
return item++;
}).bind(this));
// 箭头函数
var func = (a, b) => a + b;
[1, 2, 3].map(item => item++);
注意:
// 没有参数
let func = () => {
console.log(123);
}
// 只有一个参数
let func = name => {
console.log(name);
}
// 多个参数
let func = (name, age, height) => {
return [name, age, height];
}
var id = 'outter';
function funA() {
setTimeout(function() {
console.log(this.id);
}, 1000);
}
function funB() {
setTimeout(() => {
console.log(this.id);
}, 1000);
}
funA.call({ id: 'inner' }); // outter
funB.call({ id: 'inner' }); // inner
分析:
再来看另外一个例子:
var id = 'outter_obj';
var obj = {
id: 'inner_obj',
funA: function() {
console.log(this.id);
},
funB: () => {
console.log(this.id);
}
};
obj.funA(); // inner_obj
obj.funB(); // outter_obj
分析:
call() apply()和bind()方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以这些方法永远改变不了箭头函数的this指向,虽然即使这样做了代码也不会报错。
var id = 'outter';
// 箭头函数定义在全局作用域
let fun = () => {
console.log(this.id)
};
fun(); // 'outter'
// this的指向不会改变,永远指向Window对象
fun.call({id: 'Obj'}); // 'outter'
fun.apply({id: 'Obj'}); // 'outter'
fun.bind({id: 'Obj'})(); // 'outter'
首先,先看看构造函数的new都做了什么?主要有如下几个步骤:
let fun = (name, age) => {
this.name = name;
this.age = age;
};
// 下面的代码会报错
let p = new fun('Jessica', 18);
// 例1
let fun = (val) => {
console.log(val); // test
// 但下面会报错:Uncaught ReferenceError: arguments is not defined,因为外层全局环境没有arguments对象
console.log(arguments);
};
fun('test');
// 例2
function fun(name, age) {
let args = arguments;
console.log(args);
let funIn = () => {
let argsIn = arguments;
console.log(argsIn);
console.log(args === argsIn);
};
funIn();
}
fun('Jessica', 18);
从上图可以看出,fun函数内部的箭头函数funIn中的arguments对象,其实是沿作用域链向上访问的外层fun函数的arguments对象。但是,我们仍然可以使用rest参数代替arguments对象,来访问箭头函数的参数列表。
let sayName =() => {
console.log('Hello Jessica');
};
console.log(sayName.prototype); // undefined
ES5的写法:
ES6写法:
总结:for...in更适合遍历对象,for...of更适合遍历数组。
ES6中支持class语法,不过,ES6的class不是新的对象继承模型,它只是原型链的语法糖表现形式。函数中使用static关键词定义构造函数的方法和属性。
class Student {
constructor() {
console.log("I'm a student.");
}
study() {
console.log('study!');
}
static read() {
console.log("Reading Now.");
}
}
console.log(typeof Student); // function
let stu = new Student(); // "I'm a student."
stu.study(); // "study!"
stu.read(); // "Reading Now."
类中的继承和超集:
class Phone {
constructor() {
console.log("I'm a phone.");
}
}
class MI extends Phone {
constructor() {
super();
console.log("I'm a phone designed by xiaomi");
}
}
let mi8 = new MI();
分析:
ES6可以在定义函数的时候设置参数的默认值。
function sayName(name='Jessica') {
// 如果调用函数的时候没有传参,这个时候才会使用默认值。
console.log(`My name is ${name}`);
}
sayName(); // =>My name is Jessica
sayName('Nick'); // =>My name is Nick
Cypress.io,使用Cypress的好处是:所有的测试用例都采用Javascript编写,类似于Jquery的方式,对前端开发人员来说是非常友好的了。同时Cypress运行非常快,几乎只要当我们的页面渲染出来时就已经加载出来了,因为Cypress都是基于真实的DOM进行的测试;可视化的debug。
注意:自己测试的时候最好不要以“Cypress/cypress”来命名,否则install的时候会报错。
npm install cypress --save-dev
yarn add cypress --save
然后再通过离线安装的方式 npm install cypress-6.6.0.tgz
,注意:此时的cypress-6.6.0.tgz必须在相应的文件夹中。
CYPRESS_INSTALL_BINARY=D:\\Install-Packages\\Cypress\\cypress.zip npm install cypress
安装完成之后,我们就可以顺利使用Cypress了,但在使用之前,为了启动更方便需要在packages.json
中配置
"scripts": {
"cy:open": "cypress open"
}
然后再运行:yarn run cy:open
或者npm run cy:open
,成功之后展示如下:
如果我们的项目中配置了.babelrc
,并且在运行之后报错Webpack Compilation Error
则需要在启动Cypress之后的文件夹下创建一个空的.babelrc
为{}
,随后再次运行将不会在报错。该问题可以参考Webpack Compilation Error: Cannot find module
接下来就可以编写自己的测试用例了。可以先参考这里测试用例。这里举一个例子:
describe('First Cypress Test', function () {
it('Does not do much!', function () {
cy.visit('https://www.naver.com/'); // 访问Naver搜索引擎
cy.get('#query').type('Cypress Test'); // 获取是否存在id="query"的元素,存在则输入“Cypress Test”
cy.wait(120);
cy.get('#search_btn').click(); // 获取是否存在id="search_btn"的元素,存在则点击
cy.contains('JavaScript End to End Testing').click(); // 内容包含JavaScript End to End Testing的,则点击跳转
});
});
什么是describe
,it
和expect
?
yarn add @types/mocha
;/// <reference types="cypress" />
;function Person(nick, age) { // 构造函数Person
this.nick = nick;
this.age = age;
}
Person.prototype.sayName = function() { // 在构造函数的原型上定义一个sayName()的方法
console.log('我的名字是:' + this.nick);
}
var p1 = new Person('Jessica', 18);
var p2 = new Person('Jay', 20);
p1.sayName(); // 我的名字是:Jessica
p1.sayName(); // 我的名字是:Jay
p1.__proto__ === Person.prototype; // true
p2.__proto__ === Person.prototype; // true
Person.prototype.constructor === Person //true
需要注意的是:
请看下面的代码:
var arr = [1, 2, 3];
arr.valueOf(); // [1, 2, 3]
按照之前的理论,如果自身没有该方法,我们应该无Array.prototype对象里面去找,但是你会发现 arr.__proto__上根本就没有 valueOf 方法,那它是从哪里来的呢?
我们来看一下Array.prototype中__proto__的构成:
这里却有一个valueOf方法,为什么呢?
当试图访问一个对象属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,一次层层向上搜索,直到找到一个名字匹配的属性或者达到原型链的末尾。
查找 valueOf 的大致过程如下:
原型链的终点:
Object.prototype.proto === null; // true
所以对于这种情况,整个原型链的查找过程是:
arr ----> Array.prototype ----> Object.prototype ----> null
这就是我们常说的原型链,层层向上查找,最后还没有找到就返回undefined。
继承指的是一个对象直接使用另外一个对象的属性和方法。
由此可见,只要实现属性和方法的继承,就能达到继承的效果:
我们先创建一个Person类
function Person(name, age) {
this.name = name;
this.age = age;
}
// 方法定义在构造函数的原型上
Person.prototype.getName = function() { console.log(this.name); }
这个时候,我想创建一个Student类,我们希望它可以继承Person的所有属性,并且能够额外添加自己的特定属性;
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
属性的继承是通过在一个类内执行另外一个类的构造函数,通过call指定this为当前执行环境,这样就可以得到另外一个类的所有属性。
我们实例化这个类来看一下:
var student1 = new Student('Jessica', '29', '100');
student1.name; // Jessica
student1.age; // 29
student1.grade; // 100
显然,student1成功的继承了Person的属性(name和age)。
我们需要让Student从Person的原型对象里继承方法。我们要怎么做呢?
我们都知道类的方法都定义在prototype里,那其实我们只需要把Person.prototype的备份赋值给Student.prototype即可。
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student;
我们整理一下,主要是prototype和constructor的问题。
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor
f Person(name, age) {
this.name = name;
this.age = age;
}
===================================
Student.prototype.constructor = Student;
f Student(name, age, grade) {
Person.call(this, name, age) {
this.grade = grade;
}
}
所以,继承的最终方案是:
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
在原型链上查询属性比较耗时,对性能也存在一定的影响,试图访问不存在的属性时会遍历整个原型链。遍历对象时,每个可枚举的属性都会被枚举出来。要检查是否具有自己定义的属性,而不是原型链上的属性,就必须使用 hasOwnProperty 方法。该方法时JavaScript中唯一处理属性并且不会遍历原型链的方法。
每个对象都有一个 proto,它指向它的 prototype 原型对象,而 prototype 原型对象又具有一个自己的 prototype 原型对象,这样层层往上直到一个对象的原型 prototype 为 null,这个查询的路径就是原型链。
function Person (name, age) {
this.name = name
this.age = age
}
// 方法定义在构造函数的原型上
Person.prototype.getName = function () { console.log(this.name)}
function Student(name, age, grade) {
Person.call(this, name, age)
this.grade= grade
}
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student
解法一:
function fn(n) {
let num1 = 1,
num2 = 1,
num3 = 0;
for (let i = 0; i < n - 2; i++) {
num3 = num1 + num2;
num1 = num2;
num2 = num3;
}
return num3;
}
// fn(7); // 13
解法二:
递归:这种计算方式简洁并且直观,但是由于存在大量的重复计算,实际运行效率很低,并且会占用较多的内存。
function fn(n) {
if (n <= 2) {
return 1;
}
return fn(n - 1) + fn(n - 2);
}
// fn(7); // 13
解法三:
memoization方案在《JavaScript模式》和《JavaScript设计模式》中都有提到过。memoization是一种将函数执行结果用变量缓存起来的方法。当函数进行计算之前,先看缓存对象是否有次计算结果,如果有,就直接从缓存对象中获取结果;如果没有,就进行计算,并将结果保存到缓存对象中。(闭包方式实现)
let fibonacci = (function() {
let memory = []
return function(n) {
if(memory[n] !== undefined) {
return memory[n]
}
return memory[n] = (n === 0 || n === 1) ? n : fibonacci(n-1) + fibonacci(n-2)
}
})()
// fn(7); // 13
这个时候有一个问题就是,我们使用的数组来存储,如果把数组换成对象呢?答案是速度会快很多。是因为比如说我们调用fibonacci(100)的时候,fibonacci
函数在第一次计算的时候会设置memory[100]=xxx
,此时数组的长度为101,而前面100项都会初始化为undefined。所以memory类型为数组时比类型为对象时要慢。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.