TDZ(暂时性死区)
学习暂时性死区之前需要先了解一下var,let和const的区别(变量提升可分为创建提升和初始化提升):
- var同时进行了创建提升和初始化提升,可重复声明同一变量,声明的变量均可改;
- let只进行了创建提升,而没有进行初始化提升,不可重复声明同一变量,声明的变量均可改;
- const只进行了创建提升,而没有进行初始化提升,不可重复声明同一变量,声明的基本数据类型不可改,引用类型可改属性,由于其常量的特征,创建时就需要初始化。
由于let/const只进行了创建提升,而没有进行初始化提升,所以形成了TDZ,我们称之为暂时性死区。
var a = 100;
if(1){
a = 10;
//在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
// 而这时,还未到声明时候,所以控制台Error:a is not defined
let a = 1;
}
防抖(debounce)和节流(throttling)
在前端开发的过程中,我们经常会需要绑定一些持续触发的事件( resize、scroll、mousemove 等等),但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。这时候就需要用到防抖和节流来解决。
- 防抖:触发事件后在一个时间周期内函数只能执行一次(可分为立即执行和延迟执行,即在时间周期开始时执行和在时间周期结束时执行),有事件触发就会再延迟一个周期,直到所有延迟时间过完再执行触发函数;
/**
* @desc 函数防抖
* @param func 函数
* @param wait 延迟执行毫秒数
* @param immediate true 表立即执行,false 表非立即执行
*/
const debounce = (func: any, wait: number, immediate?: boolean) => {
let timeout: any;
const debounced = () => {
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
const callNow = !timeout;
timeout = setTimeout(function () {
// wait时间后把timeout设置为null
timeout = null;
}, wait);
if (callNow) func();
} else {
timeout = setTimeout(() => {
func();
}, wait);
}
};
debounced.cancel = () => {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};
- 节流:每隔一个时间周期就会执行一次触发函数(节流会稀释函数的执行频率),再当前时间周期没结束前,无论触发多少次都不执行。节流有两种实现方式:时间戳版和定时器版(时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候)。
/**
* @desc 函数节流
* @param func 函数
* @param wait 延迟执行毫秒数
* @param type 1 表时间戳版,2 表定时器版
*/
const throttle = (func: any, wait: number, type: number) => {
let previous = 0;
let timeout: any;
return () => {
if (type === 1) {
const now = Date.now();
if (now - previous > wait) {
func();
previous = now;
}
} else if (type === 2) {
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply();
}, wait);
}
}
};
};
如果事件触发是高频但是有停顿时,可以选择debounce; 在事件连续不断高频触发时,只能选择throttling,因为debounce可能会导致动作只被执行一次,界面出现跳跃。
JavaScript事件执行机制
因为javascript是一门单线程语言,所以我们可以得出结论:javascript是按照语句出现的顺序执行的。javascript每执行一个语句,我们称之为一次任务,因此可以将任务分为两类:同步任务和异步任务,先来看下面这张图:
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue(事件队列)。
- 主线程内的任务执行完毕为空,会去读取Event Queue(事件队列)中对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
只看导图可能有些笼统,请结合下面这段代码:
console.log(1) // 同步
setTimeout(() => {
console.log(2) // 异步,注册回调函数fn1
}, 2000);
console.log(3) // 同步
setTimeout(() => {
console.log(4) // 异步,注册回调函数fn2
}, 0);
console.log(5) // 同步
-
console.log(1)
是一条同步任务,进入主线程;setTimeout(() => {console.log(2)}, 2000);
是一条异步任务,进入Event Table,注册回调函数fn1;console.log(3)
是一条同步任务,进入主线程;setTimeout(() => {console.log(4)}, 0);
是一条异步任务,进入Event Table,注册回调函数fn2;console.log(5)
是一条同步任务,进入主线程。 - 由于setTimeout把回调函数fn2的延时设置为0,所以注册完回调函数后,Event Table会将立即将fn2移入Event Queue(事件队列);同理2s后Event Table将fn1移入Event Queue。这些操作进入队列的顺序,由设定的延迟时间来决定。
- 所以主线程第一次执行完成后输出结果1、3、5,此时主线程内的任务为空,会去顺序读取Event Queue(事件队列)中的函数,所以主线程第二次执行完成后输出结果1、3、5、4。
- 只要主线程的任务变为空,就回去Event Queue(事件队列)询问是否有可自行的回调函数,这时发现Event Queue(事件队列)中还有回调函数fn1,所以fn1会进入主线程执行,所以主线程第三次执行完成后输出结果1、3、5、4、2。
- 到此,整个js事件执行结束。
前面说了,等所有同步代码都执行完,再从Event Queue(事件队列)里依次执行所有异步回调函数,实际上Event Queue(事件队列)也有自己的规则:Event Queue(事件队列)用来存异步回调,而异步任务分为宏任务和微任务,并且微任务执行时机先于宏任务
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
整体代码是一个宏任务,进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务,如下图所示:
setTimeout(function() {
console.log('setTimeout'); // 异步:宏任务
})
new Promise(function(resolve) {
console.log('promise'); // 同步
resolve();
}).then(function() {
console.log('then'); // 异步:微任务
})
console.log('console'); // 同步
- 这段代码作为第一个宏任务,进入主线程,先遇到setTimeout,setTimeout异步执行,将其回调函数注册后分发到宏任务Event Queue。
- 接下来遇到了Promise,new Promise立即执行,输出
promise
,then函数分发到微任务Event Queue。 - 在往后遇到console.log(),立即执行,输出
console
。 - 整体代码script作为第一个宏任务执行结束,输出结果
promise console
,Event Queue里面有微任务then在和宏任务setTimeout。 - 执行微任务then,第一轮事件循环正式结束,这一轮的结果是输出
promise console then
。那么第二轮时间循环从setTimeout1宏任务开始。 - 立即执行宏任务Event Queue中setTimeout对应的回调函数,回调函数中只有一个console.log(),执行完第二轮事件循环结束,输出
promise console then setTimeout
。
总结以上,有两个基础点需要牢记:
- javascript是一门单线程语言
- 事件轮询(Event Loop)是js实现异步的一种方法,也是js的执行机制
参考链接:这一次,彻底弄懂 JavaScript 执行机制
setTimeout+Promise+Async输出顺序?很简单呀!
作用域、作用域链、闭包
作用域可以分为函数作用域和全局作用域:
- 全局作用域:代码在程序任何地方都能访问,window对象的内置属性都属于全局作用域
- 函数作用域:在固定的代码片段才能被访问
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
一般情况下,变量会到创建这个变量的函数的作用域中取值,如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。
var x = 10;
function fn(){
console.log(x);
}
function show(f){
var x = 20;
f();
}
show(fn);
上面的代码中,x
现在fn作用域中查找,没有找到值,所以向上级作用域(全局作用域)中查找,找到x=10
,所以这里会输出10
。
闭包是一种特殊的对象。它由两部分组成。执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)。当B执行时,如果访问了A中变量对象中的值,那么闭包就会产生。因此,闭包就是指函数作用域中的内部变量被另一个函数访问。我们通过一个例子来详细了解:
var fn = null;
function foo() {
var a = 2;
function innnerFoo() {
console.log(a);
}
fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}
function bar() {
fn(); // 此处保留innerFoo的引用
}
foo();
bar(); // 2
在上面的例子中,foo()执行完毕之后,按照常理,其执行环境生命周期会结束,JavaScript拥有自动的垃圾回收机制,它所占内存被垃圾收集器释放。但是通过fn = innerFoo,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,导致了foo的变量对象,也被保留了下来。于是,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象,所以此刻仍然能够访问到变量a的值。这样,我们就可以称foo形成了一个闭包。
虽然例子中的闭包被保存在了全局变量中,但是闭包的作用域链并不会发生任何改变。在闭包中,能访问到的变量,仍然是作用域链上能够查询到的变量。我们将上面的例子稍作调整,如果我们在函数bar中声明一个变量c,并在fn中试图访问该变量,运行结果会抛出错误,因为变量c没有在innnerFoo的作用域链上。
var fn = null;
function foo() {
var a = 2;
function innnerFoo() {
console.log(c); // 在这里,试图访问函数bar中的c变量,会抛出错误
console.log(a);
}
fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}
function bar() {
const c = 100;
fn(); // 此处保留innerFoo的引用
}
foo();
bar(); // 2
闭包有如下使用场景:
- 循环中使用异步事件,在事件执行机制中我们说过,异步事件会进入Event Table并注册函数,等主线程执行完后才会调用此事件队列,所以需要通过闭包保存循环中的某些变量以供异步事件使用。
- setTimeout,原生的setTimeout传递的第一个函数不能带参数,通过闭包可以实现传参效果。
这里我们通过一个循环闭包的经典题目来讲解以上两种场景:
// 利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
我们知道在函数中闭包判定的准则,即执行时是否在内部定义的函数中访问了上层作用域的变量,因此我们只需要2个操作就可以完成题目需求,一是使用自执行函数提供闭包条件,二是传入i值并保存在闭包中。
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i)
}
前面我们说到原生的setTimeout传递的第一个函数不能带参数的问题,所以这里我们也可以在setTimeout的第一个参数处利用闭包。
for (var i = 1; i <= 5; i++) {
setTimeout((function (i) {
return function () {
console.log(i);
}
})(i), i * 1000);
}
- 函数防抖,在上面章节中有介绍。
此外,闭包还可运用于封装私有变量、模块化与柯里化。
npm、npx、yarn 的区别
yarn和npm的区别
yarn 和 npm 都是 node 软件包管理器,用于 Node.js 包的发布、传播、依赖控制,npm 提供了命令行工具,使你可以方便地下载、安装、升级、删除包,也可以让你作为开发者发布并维护包。yarn 是为了弥补 npm 的一些缺陷而出现的。
- npm的特点和缺陷:
- 是按照队列执行每一个package,每一次都是从网络上下载,也就是说必须要等到当前 package 安装完成之后,才能继续后面的安装。因此npm install的时候巨慢;
- 同一个项目,安装的时候无法保持一致性。由于package.json文件中版本号的特点,下面三个版本号在安装的时候代表不同的含义。
-"5.0.3" : 表示安装指定的5.0.3版本;
-"~5.0.3" :表示安装5.0.x中最新的版本;
-"^5.0.3" :表示安装5.X.X中最新的版本,
- yarn的优势:
- 并行安装:区别去npm按队列执行,yarn是同步执行所有任务,因此提高了性能;
- 离线模式:区别去npm每一次都是从网络上下载,如果你上一次安装过软件包,yarn再次安装会从缓存中获取;
- 安装版本统一:为了防止拉取到不同的版本,Yarn 有一个锁定文件 (lock file) 记录了被确切安装上的模块的版本号。
yarn 和 npm 命令也有一些区别:
NPM Yarn
npm install == yarn
npm install vue -g == yarn global add vue
npm install vue --save == yarn add vue
npm install vue --save-dev == yarn add vue --dev
npm uninstall vue --save(-dev) == yarn remove vue
npm unpdate vue --save == yarn upgrade vue
npm 和 npx 的区别
如果说npm 是 node 软件包的管理器,那么npx 是 node 软件包的执行工具。
官方文档,从npm@5.2.0 版本开始,npx就和npm捆绑在了一起,可以认为npx是npm 的高级版本,npx 具有更强大的功能。
npx是一个可执行的二进制文件,原理很简单,运行npx的时候,默认会到 node_modules/.bin 路径和环境变量$PATH里面,检查命令是否存在,例如:
项目中想运行一个脚本命令有两种方式:
1. package.json中配置script脚本
{
"scripts": {
"mocha": xxxx
}
}
2. 项目根目录路径下面:
node-modules/.bin/mocha --version
现在有了npx可以直接执行:
npx mocka --version
npx 当执行一个包的时候,会自动检查本地是否存在,如果没有会为你从 npm 上下载,临时安装这个包,并且执行它。当做完这些事情后,已安装的包不会出现在你的全局安装中,所以不用担心长期使用所带来的全局污染。常用的命令:npx create-react-app my-app
,npx安装一个临时create-react-app并调用,而不用污染全局安装。所以执行npx命令步骤如下:
- 首先会检查本地项目路径中是否存在要执行的包;
- 如果存在,执行;
- 如果不存在,意味着尚未安装该软件包,npx将临时安装其最新版本,然后执行它。
总结
- 因为有了yarn的出现,npm5.0版本之后,npm也做了改进,引入了package-lock.json,速度和性能上也大大提升,个人感觉还没有超过yarn;
- 如何选择?
个人建议大多数项目依然可以采取用npm,兼容性好,成熟,稳定,种类多,有趣,它和node一起提供,管理包安全放心;
新的项目可以使用yarn,yarn是一个更安全的选择,随着时代的发展,我发现其实无关选择,都是为了工作需求,完成日常工作,选择哪一种看个人爱好。