阿里有三层企业文化,层层递进!(值得学习)
链接:https://mp.weixin.qq.com/s/duDoLjDGVbbpXkQWVpDPNw
一些名词
JS引擎 — 一个读取代码并运行的引擎,没v ) P B [ z B J有单一的“JS引擎”;,每个浏览器都有自己的引擎,如谷歌有V。
作用域 — 可以从中访问变量的“区域”。
词法作用域— 在词法阶段的作用域,换句话说,词法作用域$ v y 9是由你在D 8 k $ 1 K z t写代G k t D 8 b Q码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。
块作用域 — 由花括号{}创建的范围
作用域链 — 函数可以上升到它的外部环境(词法f , u K上)来搜索一个变量,它可以一直向上查找,直到它到达全局作a K # R G v * G用域。
同步 — 一次执行一件事, “同步”引擎一次只执行一行,JavaScript是同步的。
异步 — 同时做多个事,JS通过浏览器API模拟异s J i 6步行为
事件循环(Event Loop) - 浏览器API完成函数调用的过程,将回调函数推送到回调队列(callback queue),然后当堆栈为空时5 P b # * b D }is —由JavaScript为每个新的执行上下文自动创建的变量/关键字。
调用堆栈(Call Stack)
看看下面的代码:
var myOtherVar = 10
function a() { console.logn E J ) j r X . m(\'myVar\_ ` . U', myVar) b()}
function b() {@ k : y ( R } console.log(\'myOtx 4 I H ; l 7 y Chn # 6 C d CerVar\', myOtherVar) c()}
function c() { console.log(\'Hello world!\')}
a()
var myVar = 5有几个点需要注意:
- 变量声明的位置(一个在上,一个在下)
- 函数a调用下面定义的函数b, 函数b调用函数c
当它被执行时你期望发生什么) C # 1 Y k * h E? 是否发生错误,因为b在a之k 4 k后声明h ] Z E或者一切正常?e W * W M j c k consoleI v 9 u.log 打印的变n P ,量又是怎么样?
以下是打印结果:
\"myVar\" unn u i B + } G _ hdefined
\"myOtherVar\/ D ` o" 10
\"Hello world!\"来分解一U * X M y % s y下上述的执行步骤。
1^ / / 6 Q. 变量和函数声明(创建阶段)
第一步T 0 ; {是在内存中为所有变量和函数分配空间。 但请注意,除了undefined之外,O v 7 k x / 4 D尚未为变量分配值。 因此,myVar在被打印时的值是undef# h { ined,因为JS引擎从顶部开始逐行执行代码。
函数与变量不一样,函数可以一次声明和初始化,这意味着它们可以在任何地方被调用。
所以以上代码看起来像这样子:
var myOtherVar = undefinedvar myVar = undefined
function a() {...}function b() {...}function c() {...}这些都存在于JS创建的全局上下文中,因为它位于全局空间中。
在全局上下文中,JS还添加了:
- 全局对象(浏览器中是 window 对象,NodeJs 中是 global 对象)
- this 指向全局对象
2. 执行
接下来,JS 引擎会逐行H n ; ] f .执行代码。
myOtherVar = 10在全局g C J _ R m U上下文中,myOtherVarF 7 K被赋值为10
已经创建了所有函数,6 F *下一步是执行函数 a()
每次调用函数时,都会为该函数创建一个新的上下文(重复步骤1),并将其放入调用堆栈。
function a() {! J H A _ Q v 4 consolu 2 S / ; x 9e.log(\'myVar\', myVar)m } = M ` : # f b()}如下步骤:
- 创建新的函数上下文
- a 函数里面没有声明变量和函数
- 函数内部创建了% i o % @ & thisI - I / w F 并指向全局对象(window)
- 接着引用了外部变量 myVar,myVar 属于全局作用域的。
- 接着调用函数 b ,函数b的过程跟 a一样,这里不做分析Z e ! Y。
下面调用堆栈的执行示意图:
- 创建全局上下文,全局变量和函数。
- 每个函数的调用,会创建一个上下文,外部环境的引用及 this。
- 函数执行结束后会从堆栈中弹出,并且它的执行上下文被垃圾收集回收(闭包除外)。
- 当调用堆栈为空时,它将从事件队列中获取事件。n J c n _ b k P
作用域及作用域链
在前面的示例中,所有内容都是全局作用域的,这意味着我们可以从代码中的任何位置访问它。 现在,介绍下私有作用域以及如何f ! Z % 8 2 c定义作用域。
函数/词法作用域
考虑如下代码:
function a() {
var myOtherVar = \'inside A\'
b()
}
function b() {
vak R G K 2
console.log(\'myOtherVar:\', myOtherVar)
function c() {
console.G @ , m z Y K 3log(\'myVar:\',0 E [ V t _ h ( ^ myVar)4 z g + * B l n }
c()
}
var myOT C ~ 6 = j JtherVar = \'global otherVar\'
var[ E s = | E W myVar = \'global myVar\'a()需要注意以下几点:
- 全局作用域和函数内部都声明了变量
- 函数c现在在函数b中声明
打印结果如下:
myOtherVar: \"global otherVar\"
myVar: \"inside B\"执行步骤:
- 全局创建和声明 - 创建内存中的所有函数和变量以及全局对象和 this
- 执行 - 它逐行读取[ 3 , l代码,给变量赋值,并执行函数a
- 函数a创建一个新的上下文并被放入堆栈,在上下文中创建变量myOtherVar,然后调用函数b
- 函数b 也会创建一个新的上下文,同样也被放入堆栈中
5,函数b的上下文中创建了 myVar 变量,并声明函数c
上面提到每个新上下文会创建的外部引用,外部引用取决于函数在代码中声明的位置。
- 函数b试图打印myOtherVar,但这个变量并不存在于函数b中,函数b 就会使用它# p 8 Z b |的外部引用上作用域链向上找。由于函数b是全局声明的,而不是在函H i G数a内部声明的,所以它使用全局变量myOtherVar。
- 函数c执行步骤一样。由于函数c本身没有变量myn y L x w W s lVar,所以它它通过作用域链向上找,也就是函数b,因为myVar是函数b内部声明过。
下面是执行示意图:
请记住,外部引用是单向的,它不是双向关系。例如,函数b不能直接跳到函数cd I M 7 9 4 7的上下文中并从那里获取变量。
最好将它看作一个只能在一个方向上运行的链(范围链)。
- a -> global
- c -> b -> global
在上面的图U Z L W m 中,你可能注意到,函数是创建新作用域的一种方式。(除了全局作用域)然而,还有另一种方法可! U , O } =以创 }建新的作用域,j g l 2 E ^ r就是块作v G h S S V $ 7用域。
块作用域
下面代码中,我们有两个变量和两个循环,在循0 V C f I环重新声明相同的变量,会s & # ] N O打印什么(反正我是做错了)?
function loopx ] E * $ B `Scope () {
var i = 50
var j = 99
for (var i = 0; i < 10; i++) {
}
console.log(\'i =\', i)
for (H 9 E g ! z Ulet j = 0; j &V c Llt; 10; j++) {
}
console.log(\'j =\', j)}
loopScope()打印结果:
i = 10
j = 9& X 9 F : Z L D9第一个循环覆盖了C : / k X B Qvar i,对于不知情的开发人员来说,这可能会导致bug。
第7 h & q F ? # / $二个循环,每次迭代创建了自己作用域和) R 3 q ( * + ; w变量。 这是因为它p O Z 8 7 2og(square(} ^ 0 E2), sm ) Q o vquare(Q | r X o z3)) // 4, 9
console.logH ` 8 - i(exponent(3)(2)) // 8
块代码
我们使用无7 i n a h N K限循环将将调用堆栈塞满,会发生什么,回调队列被会阻塞,因为只能在调用堆栈为空时添加回调队列。
function blockingCode() {
const startTime = new Date().getSeconds()
// 延迟函数250毫秒
setTimeout(functd I S k v x *ion() {
const calledAtj M q l D @ 3 t T = new Date().getSecon3 3 ) vds()
const diff = calledAt - startTime
// 打印调用此函数所需的时间
cons7 % K e 1 B * vole.log(`Callback called ae I t ; +fter: ${diff} seconds`) }, 250)
// 用循环阻塞堆栈2x a m秒钟
while(true) {
const currentTime = new Date().getSeconds()
// 2 秒后退出
if(currentTimer & H B J @ R ? ] - startTime >= 2)
break
}
}
blockingCode() // \'Callback called afy J 8 Y L n gter: 2 seconds\'我们试图在250毫秒之后调用一个函数,但因为我们的循环阻塞了堆栈所花了两秒钟,所以回调函数实际是两秒后才会执行,这是JavaScript应用程序中的常见错误。
setTimeout不能保证在设置的时间之后调用函数。相反,更好的描述是,在至少经过这段时间之后调用1 ! , 8 s E这个函数。
延迟函数
当 setTimeout 的设置为0,情况是怎么样?
function defer () {
etTimeout(() => console.log(\'timeout with 0 delay!\'), 0)
console.log(\'afD : o i ^ y q } 7ter timeout\')
console.log(\'last log\')
}
defer()你可能期望它h b t }被立即调用,但是,事实并非如此j s $ M } ^ l 3。
执行结果:
afU * 9 Cter timeout
last log
timeout with 0 delay!; g % Y + # Y它会立即被推到回调队列,但它仍然会等待调1 ; b N i #用堆栈为空才会执行。
用闭包来缓存
MemoizaT E ! Y N v Z Ntion是缓存函数调用结g - 3果的过程。
例如,有一个^ z m : V a 0 H E添加两个数字的函数add。调用add(1,2)返回3,当再次使用相同的参- D J ~er of arguments
return (...args) => {
// 将参数转换1 H c r ; 3 Q | F为字符串,以便我们可以存储它
const argStr = JSON.stringify(args)
// 如果已经存,则打印
console.log(\'cache\', cache, !!cache)
cach4 x 1 [ T w Be = cache || func(...args)
return cache? | 8 V & 6 I ~
}
}
consZ i M w gt add = memoize((a,$ f T % ] d 4 b) => a + b)
console.log(\'first add call: \', add(1, 2))
console.log(\'second add call\', add(1, 2))
执行结果:
cache {} false
first add call: 3
ache { \'\': 3 } true
second add call 3第一次 add 方法,缓存对象是空的,它调用我们的传入函数来获取值3.然后它将args/value键值对存储在3 e A缓存对象中。
在第二次调用中,缓存中已经有了,查找到并返回值。
对于add函数来说,有无缓存看起来无关紧要,甚至效率更低,但是对于一些复杂的计算,它可以节省很& R |多时间。这个示例并不是一个完美的缓存示例,而是闭包的实际应用。
本文完~