中华视窗是诚信为本,市场在变,我们的诚信永远不变...
第一步是在内存中为所有变量和函数分配空间。 但请注意,除了之外,尚未为变量分配值。 因此,myVar在被打印时的值是,因为 JS 引擎从顶部开始逐行执行代码。
函数与变量不一样,函数可以一次声明和初始化,这意味着它们可以在任何地方被调用。
所以以上代码看起来像这样子:
var myOtherVar = undefined
var myVar = undefined
function a() {...}
function b() {...}
function c() {...}
这些都存在于 JS 创建的全局上下文中,因为它位于全局空间中。
在全局上下文中,JS 还添加了:
全局对象(浏览器中是 对象, 中是 对象)this 指向全局对象2. 执行
接下来,JS 引擎会逐行执行代码。
myOtherVar = 10`在全局上下文中,`myOtherVar`被赋值为`10
已经创建了所有函数,下一步是执行函数 a()
每次调用函数时,都会为该函数创建一个新的上下文(重复步骤 1),并将其放入调用堆栈。
function a() {
console.log("myVar", myVar);
b();
}
如下步骤:
创建新的函数上下文a 函数里面没有声明变量和函数函数内部创建了 this 并指向全局对象()接着引用了外部变量 myVar,myVar 属于全局作用域的。接着调用函数 b ,函数b的过程跟 a一样,这里不做分析。
下面调用堆栈的执行示意图:
创建全局上下文,全局变量和函数。每个函数的调用,会创建一个上下文,外部环境的引用及 this。函数执行结束后会从堆栈中弹出,并且它的执行上下文被垃圾收集回收(闭包除外)。当调用堆栈为空时,它将从事件队列中获取事件。作用域及作用域链
在前面的示例中,所有内容都是全局作用域的,这意味着我们可以从代码中的任何位置访问它。 现在,介绍下私有作用域以及如何定义作用域。
函数/词法作用域
考虑如下代码:
function a() {
var myOtherVar = "inside A";
b();
}
function b() {
var myVar = "inside B";
console.log("myOtherVar:", myOtherVar);
function c() {
console.log("myVar:", myVar);
}
c();
}
var myOtherVar = "global otherVar";
var myVar = "global myVar";
a();
需要注意以下几点:
全局作用域和函数内部都声明了变量函数c现在在函数b中声明
打印结果如下:
myOtherVar: "global otherVar";
myVar: "inside B";
执行步骤:
全局创建和声明 - 创建内存中的所有函数和变量以及全局对象和 this执行 - 它逐行读取代码,给变量赋值,并执行函数 a函数 a创建一个新的上下文并被放入堆栈,在上下文中创建变量,然后调用函数 b函数 b 也会创建一个新的上下文,同样也被放入堆栈中
5,函数b 的上下文中创建了 myVar 变量,并声明函数 c
上面提到每个新上下文会创建的外部引用,外部引用取决于函数在代码中声明的位置。
函数 b试图打印,但这个变量并不存在于函数 b中,函数 b 就会使用它的外部引用上作用域链向上找。由于函数 b是全局声明的,而不是在函数 a内部声明的,所以它使用全局变量 。函数 c执行步骤一样。由于函数 c本身没有变量myVar,所以它它通过作用域链向上找,也就是函数 b,因为myVar是函数 b内部声明过。
下面是执行示意图:
请记住,外部引用是单向的,它不是双向关系。例如,函数 b不能直接跳到函数 c的上下文中并从那里获取变量。
最好将它看作一个只能在一个方向上运行的链(范围链)。
在上面的图中,你可能注意到,函数是创建新作用域的一种方式。(除了全局作用域)然而,还有另一种方法可以创建新的作用域,就是块作用域。
块作用域
下面代码中,我们有两个变量和两个循环,在循环重新声明相同的变量,会打印什么(反正我是做错了)?
function loopScope() {
var i = 50;
var j = 99;
for (var i = 0; i < 10; i++) {}
console.log("i =", i);
for (let j = 0; j < 10; j++) {}
console.log("j =", j);
}
loopScope();
打印结果:
i = 10;
j = 99;
第一个循环覆盖了var i,对于不知情的开发人员来说,这可能会导致 bug。
第二个循环,每次迭代创建了自己作用域和变量。 这是因为它使用let关键字,它与var相同,只是let有自己的块作用域。 另一个关键字是const,它与let相同,但const常量且无法更改(指内存地址)。
块作用域由大括号 {} 创建的作用域
再看一个例子:
function blockScope() {
let a = 5;
{
const blockedVar = "blocked";
var b = 11;
a = 9000;
}
console.log("a =", a);
console.log("b =", b);
console.log("blockedVar =", blockedVar);
}
blockScope();
打印结果:
a = 9000
b = 11
ReferenceError: blockedVar is not defined
a是块作用域,但它在函数中,而不是嵌套的,本例中使用var是一样的。对于块作用域的变量,它的行为类似于函数,注意var b可以在外部访问,但是const 不能。在块内部,从作用域链向上找到 a 并将let a更改为9000。
使用块作用域可以使代码更清晰,更安全,应该尽可能地使用它。
事件循环(Event Loop)
接下来看看事件循环。 这是回调,事件和浏览器 API 工作的地方
我们没有过多讨论的事情是堆,也叫全局内存。它是变量存储的地方。由于了解 JS 引擎是如何实现其数据存储的实际用途并不多,所以我们不在这里讨论它。
来个异步代码:
function logMessage2() {
console.log("Message 2");
}
console.log("Message 1");
setTimeout(logMessage2, 1000);
console.log("Message 3");
上述代码主要是将一些 打印到控制台。 利用函数来延迟一条消息。 我们知道 js 是同步,来看看输出结果
Message 1
Message 3
Message 2
打印 1调用 打印 3打印 2
它记录消息 3
稍后,它会记录消息 2
是一个 API,和大多数浏览器 API 一样,当它被调用时,它会向浏览器发送一些数据和回调。我们这边是延迟一秒打印 2。
调用完 后,我们的代码继续运行,没有暂停,打印 3 并执行一些必须先执行的操作。
浏览器等待一秒钟,它就会将数据传递给我们的回调函数并将其添加到事件/回调队列中( event/ queue)。 然后停留在队列中,只有当调用堆栈(call stack)为空时才会被压入堆栈。
代码示例
要熟悉 JS 引擎,最好的方法就是使用它,再来些有意义的例子。
简单的闭包
这个例子中 有一个返回函数的函数,并在返回的函数中使用外部的变量, 这称为闭包。
function exponent(x) {
return function(y) {
//和math.pow() 或者x的y次方是一样的
return y ** x;
};
}
const square = exponent(2);
console.log(square(2), square(3)); // 4, 9
console.log(exponent(3)(2)); // 8
块代码
我们使用无限循环将将调用堆栈塞满,会发生什么,回调队列被会阻塞,因为只能在调用堆栈为空时添加回调队列。
function blockingCode() {
const startTime = new Date().getSeconds();
// 延迟函数250毫秒
setTimeout(function() {
const calledAt = new Date().getSeconds();
const diff = calledAt - startTime;
// 打印调用此函数所需的时间
console.log(`Callback called after: ${diff} seconds`);
}, 250);
// 用循环阻塞堆栈2秒钟
while (true) {
const currentTime = new Date().getSeconds();
// 2 秒后退出
if (currentTime - startTime >= 2) break;
}
}
blockingCode(); // 'Callback called after: 2 seconds'
我们试图在250毫秒之后调用一个函数,但因为我们的循环阻塞了堆栈所花了两秒钟,所以回调函数实际是两秒后才会执行,这是 应用程序中的常见错误。
不能保证在设置的时间之后调用函数。相反,更好的描述是,在至少经过这段时间之后调用这个函数。
延迟函数
当 的设置为 0,情况是怎么样?
function defer() {
setTimeout(() => console.log("timeout with 0 delay!"), 0);
console.log("after timeout");
console.log("last log");
}
defer();
你可能期望它被立即调用,但是,事实并非如此。
执行结果:
after timeout
last log
timeout with 0 delay!
它会立即被推到回调队列,但它仍然会等待调用堆栈为空才会执行。
用闭包来缓存
是缓存函数调用结果的过程。
例如,有一个添加两个数字的函数add。调用add(1,2)返回3,当再次使用相同的参数add(1,2)调用它,这次不是重新计算,而是记住 1 + 2是3的结果并直接返回对应的结果。 可以提高代码运行速度,是一个很好的工具。
我们可以使用闭包实现一个简单的函数。
// 缓存函数,接收一个函数
const memoize = func => {
// 缓存对象
// keys 是 arguments, values are results
const cache = {};
// 返回一个新的函数
// it remembers the cache object & func (closure)
// ...args is any number of arguments
return (...args) => {
// 将参数转换为字符串,以便我们可以存储它
const argStr = JSON.stringify(args);
// 如果已经存,则打印
console.log("cache", cache, !!cache[argStr]);
cache[argStr] = cache[argStr] || func(...args);
return cache[argStr];
};
};
const add = memoize((a, 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
cache { '[1,2]': 3 } true
second add call 3
第一次 add 方法,缓存对象是空的,它调用我们的传入函数来获取值3.然后它将args/value键值对存储在缓存对象中。
在第二次调用中,缓存中已经有了,查找到并返回值。
对于add函数来说,有无缓存看起来无关紧要,甚至效率更低,但是对于一些复杂的计算,它可以节省很多时间。这个示例并不是一个完美的缓存示例,而是闭包的实际应用。
关于
专注于、微信小程序、微信小游戏、支付宝小程序、React 、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,累计处理了10亿+错误事件,付费客户有阳光保险、核桃编程、荔枝FM、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家免费试用!