新手入门带你学习JavaScript引擎运行原理

一些名词

JS引擎 — 一个读取代码并运行的引擎,没有单一的“JS引擎”;,每个浏览器都有自己的引擎,如谷歌有V。

作用域 — 可以从中访问变量的“区域”。

词法作用域— 在词法阶段的作用域,换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。

块作用域 — 由花括号{}创建的范围

作用域链 — 函数可以上升到它的外部环境(词法上)来搜索一个变量,它可以一直向上查找,直到它到达全局作用域。

同步 — 一次执行一件事, “同步”引擎一次只执行一行,JavaScript是同步的。

异步 — 同时做多个事,JS通过浏览器API模拟异步行为

事件循环(Event Loop) - 浏览器API完成函数调用的过程,将回调函数推送到回调队列(callback queue),然后当堆栈为空时,它将回调函数推送到调用堆栈。

堆栈 —一种数据结构,只能将元素推入并弹出顶部元素。 想想堆叠一个字形的塔楼; 你不能删除中间块,后进先出。

— 变量存储在内存中。

调用堆栈 — 函数调用的队列,它实现了堆栈数据类型,这意味着一次可以运行一个函数。 调用函数将其推入堆栈并从函数返回将其弹出堆栈。

执行上下文 — 当函数放入到调用堆栈时由JS创建的环境。

闭包 — 当在另一个函数内创建一个函数时,它“记住”它在以后调用时创建的环境。

垃圾收集 — 当内存中的变量被自动删除时,因为它不再使用,引擎要处理掉它。

变量的提升— 当变量内存没有赋值时会被提升到全局的顶部并设置为undefined。

this —由JavaScript为每个新的执行上下文自动创建的变量/关键字。

调用堆栈(Call Stack)

看看下面的代码:

var myOtherVar = 10
function a() {
console.log('myVar', myVar)
b()
}
function b() {
console.log('myOtherVar', myOtherVar)
c()
}
function c() {
console.log('Hello world!')
}
a()
var myVar = 5

有几个点需要注意:

  • 变量声明的位置(一个在上,一个在下)
  • 函数a调用下面定义的函数b, 函数b调用函数c

当它被执行时你期望发生什么? 是否发生错误,因为b在a之后声明或者一切正常? console.log 打印的变量又是怎么样?
以下是打印结果:

"myVar" undefined
"myOtherVar" 10
"Hello world!"

来分解一下上述的执行步骤。

1. 变量和函数声明(创建阶段)

第一步是在内存中为所有变量和函数分配空间。 但请注意,除了undefined之外,尚未为变量分配值。 因此,myVar在被打印时的值是undefined,因为JS引擎从顶部开始逐行执行代码。

函数与变量不一样,函数可以一次声明和初始化,这意味着它们可以在任何地方被调用。

所以以上代码看起来像这样子:

var myOtherVar = undefined
var myVar = undefined
function a() {...}
function b() {...}
function c() {...}

这些都存在于JS创建的全局上下文中,因为它位于全局空间中。

在全局上下文中,JS还添加了:

  1. 全局对象(浏览器中是 window 对象,NodeJs 中是 global 对象)
  2. this 指向全局对象

2. 执行

接下来,JS 引擎会逐行执行代码。

myOtherVar = 10在全局上下文中,myOtherVar被赋值为10

已经创建了所有函数,下一步是执行函数 a()

每次调用函数时,都会为该函数创建一个新的上下文(重复步骤1),并将其放入调用堆栈。

function a() {
console.log('myVar', myVar)
b()
}

如下步骤:

  1. 创建新的函数上下文
  2. a 函数里面没有声明变量和函数
  3. 函数内部创建了 this 并指向全局对象(window)
  4. 接着引用了外部变量 myVar,myVar 属于全局作用域的。
  5. 接着调用函数 b ,函数b的过程跟 a一样,这里不做分析。

下面调用堆栈的执行示意图:

  1. 创建全局上下文,全局变量和函数。
  2. 每个函数的调用,会创建一个上下文,外部环境的引用及 this。
  3. 函数执行结束后会从堆栈中弹出,并且它的执行上下文被垃圾收集回收(闭包除外)。
  4. 当调用堆栈为空时,它将从事件队列中获取事件。

作用域及作用域链

在前面的示例中,所有内容都是全局作用域的,这意味着我们可以从代码中的任何位置访问它。 现在,介绍下私有作用域以及如何定义作用域。

函数/词法作用域

考虑如下代码:

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()

需要注意以下几点:

  1. 全局作用域和函数内部都声明了变量
  2. 函数c现在在函数b中声明

打印结果如下:

myOtherVar: "global otherVar"
myVar: "inside B"

执行步骤:

  1. 全局创建和声明 - 创建内存中的所有函数和变量以及全局对象和 this
  2. 执行 - 它逐行读取代码,给变量赋值,并执行函数a
  3. 函数a创建一个新的上下文并被放入堆栈,在上下文中创建变量myOtherVar,然后调用函数b
  4. 函数b 也会创建一个新的上下文,同样也被放入堆栈中
  5. 函数b的上下文中创建了 myVar 变量,并声明函数c

上面提到每个新上下文会创建的外部引用,外部引用取决于函数在代码中声明的位置。

  1. 函数b试图打印myOtherVar,但这个变量并不存在于函数b中,函数b 就会使用它的外部引用上作用域链向上找。由于函数b是全局声明的,而不是在函数a内部声明的,所以它使用全局变量myOtherVar。
  2. 函数c执行步骤一样。由于函数c本身没有变量myVar,所以它它通过作用域链向上找,也就是函数b,因为myVar是函数b内部声明过。

下面是执行示意图:

请记住,外部引用是单向的,它不是双向关系。例如,函数b不能直接跳到函数c的上下文中并从那里获取变量。
最好将它看作一个只能在一个方向上运行的链(范围链)。

  • a -> global
  • c -> b -> global

在上面的图中,你可能注意到,函数是创建新作用域的一种方式。(除了全局作用域)然而,还有另一种方法可以创建新的作用域,就是块作用域。

块作用域

下面代码中,我们有两个变量和两个循环,在循环重新声明相同的变量,会打印什么(反正我是做错了)?

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
  1. a是块作用域,但它在函数中,而不是嵌套的,本例中使用var是一样的。
  2. 对于块作用域的变量,它的行为类似于函数,注意var b可以在外部访问,但是const blockedVar不能。
  3. 在块内部,从作用域链向上找到 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')

上述代码主要是将一些 message 打印到控制台。 利用setTimeout函数来延迟一条消息。 我们知道js是同步,来看看输出结果

Message 1
Message 3
Message 2
  1. 打印 Message 1
  2. 调用 setTimeout
  3. 打印 Message 3
  4. 打印 Message 2

它记录消息3

稍后,它会记录消息2

setTimeout是一个 API,和大多数浏览器 API一样,当它被调用时,它会向浏览器发送一些数据和回调。我们这边是延迟一秒打印 Message 2。

调用完setTimeout 后,我们的代码继续运行,没有暂停,打印 Message 3 并执行一些必须先执行的操作。
浏览器等待一秒钟,它就会将数据传递给我们的回调函数并将其添加到事件/回调队列中( event/callback 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毫秒之后调用一个函数,但因为我们的循环阻塞了堆栈所花了两秒钟,所以回调函数实际是两秒后才会执行,这是JavaScript应用程序中的常见错误。

setTimeout不能保证在设置的时间之后调用函数。相反,更好的描述是,在至少经过这段时间之后调用这个函数。

延迟函数

当 setTimeout 的设置为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!

它会立即被推到回调队列,但它仍然会等待调用堆栈为空才会执行。

用闭包来缓存

Memoization是缓存函数调用结果的过程。

例如,有一个添加两个数字的函数add。调用add(1,2)返回3,当再次使用相同的参数add(1,2)调用它,这次不是重新计算,而是记住1 + 2是3的结果并直接返回对应的结果。 Memoization可以提高代码运行速度,是一个很好的工具。
我们可以使用闭包实现一个简单的memoize函数。

// 缓存函数,接收一个函数
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函数来说,有无缓存看起来无关紧要,甚至效率更低,但是对于一些复杂的计算,它可以节省很多时间。这个示例并不是一个完美的缓存示例,而是闭包的实际应用。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,

时间: 2019-06-24

使用Node.js实现一个多人游戏服务器引擎

摘要 听说过文字冒险游戏吗? 如果你的年龄足够大的话(就像我一样),那么你可能听说过.甚至玩过"back in the day".在本文中,我将向你展示编写的整个过程.这不仅仅是一个文本冒险游戏,而是一个能让你和你的朋友们一起玩的,可以进行任何剧情的文本冒险游戏引擎. 没错,我们将通过在添加多人游戏功能来增加它的趣味性. 文字冒险是最早的 RPG 形式的游戏之一,回到还没有图形画面的时代,你只能通过阅读 CRT 显示器上黑色背景下的描述,并且依赖自己的想象力来推动游戏剧情的发展. 如果

koa2使用ejs和nunjucks作为模板引擎的使用

一.使用 ejs 作为模板引擎 koa2 如果使用 ejs.jade 这种作为模板引擎的话,直接使用 koa-views 进行模板加载即可. 比如使用 ejs : 安装: yarn add koa-views ejs 使用: 在使用 render 的时候,需要进行异步文件模板读取,因此 ctx.render 需要使用 await const app= require('koa')(); const koaViews= require('koa-views'); const path = requ

JavaScript模板引擎实现原理实例详解

本文实例讲述了JavaScript模板引擎实现原理.分享给大家供大家参考,具体如下: 1.入门实例 首先我们来看一个简单模板: <script type="template" id="template"> <h2> <a href="{{href}}" rel="external nofollow" > {{title}} </a> </h2> <img src

JavaScript模板引擎原理与用法详解

本文实例讲述了JavaScript模板引擎原理与用法.分享给大家供大家参考,具体如下: 一.前言 什么是模板引擎,说的简单点,就是一个字符串中有几个变量待定.比如: var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.'; 通过模板引擎函数把数据塞进去, var data = { "name": "Barret Lee", "age": "

JavaScript模板引擎应用场景及实现原理详解

本文实例讲述了JavaScript模板引擎应用场景及实现原理.分享给大家供大家参考,具体如下: 一.应用场景 以下应用场景可以使用模板引擎: 1.如果你有动态ajax请求数据并需要封装成视图展现给用户,想要提高自己的工作效率. 2.如果你是拼串族或者数组push族,迫切的希望改变现有的书写方式. 3.如果你在页面布局中,存在共性模块和布局,你可以提取出公共模板,减少维护的数量. 二.实现原理 不同模板间实现原理大同小异,各有优缺,请按需选择,以下示例以artTemplate模板引擎来分析. 2.

JavaScript构建自己的模板小引擎示例

本文实例讲述了JavaScript构建自己的模板小引擎.分享给大家供大家参考,具体如下: 有时候,我们不需要太牛逼太强大的JavaScript模板引擎(比如jQuery tmpl或者handlebarsjs),我们只是需要在简单的模板里绑定一些非常简单的字段,本文将使用非常简单的技巧来帮你实现这个小功能. 首先我们先来定义我们需要的模板,在id为template的script块里: HTML部分: <!doctype html> <html> <head> <me

详解Node.js模板引擎Jade入门

Jade是Node.js的一个模板引擎,它借鉴了Haml的很多地方,所以语法上和Haml比较相近.并且,Jade也支持空格. 1.标签 在Jade里,一行开头的任何文本都被默认解释成HTML标签.并且你只需要你写开始标签--注意:不需要加"<>".因为Jade会帮我们渲染闭合和开始标签.例如: body div h1 Jade是Node.js的一个模板引擎 p 它借鉴了Haml的很多地方,所以语法上和Haml比较相近. div footer © Pandora 上面的Jad

Node.js 使用jade模板引擎的示例

在"Node.js开发入门--Express安装与使用"里,我们曾经使用express generator创建了一个HelloExpress网站,express工具为我们生成了基本的目录结构.模板.stylesheet.routers等.虽然那只是一个简单的HelloWorld类的小东西,可里面包含的内容还是有些多了,为了更好的理解Express所支持的jade模板引擎的用法,我们这次提供一个手动创建的小网站,可以显示来访者的IP,并对访问进行计数. 安装jade npm instal

Node.js的Web模板引擎ejs的入门使用教程

Node 开源模板的选择很多,但推荐像我这样的老人去用 EJS,有 Classic ASP/PHP/JSP 的经验用起 EJS 来的确可以很自然,也就是说,你能够在 <%...%> 块中安排 JavaScript 代码,利用最传统的方式 <%=输出变量%>(另外 <%-输出变量是不会对 & 等符号进行转义的).安装 EJS 命令如下: npm install ejs JS 调用 JS 调用的方法主要有两个: ejs.compile(str, options); //

node.js 使用ejs模板引擎时后缀换成.html

这是一个小技巧,看着.ejs的后缀总觉得不爽,使用如下方法,可以将模板文件的后缀换成我们习惯的.html. 1.在app.js的头上定义ejs: 复制代码 代码如下: var ejs = require('ejs'); 2.注册html模板引擎: 复制代码 代码如下: app.engine('html',ejs.__express); 3.将模板引擎换成html: 复制代码 代码如下: app.set('view engine', 'html'); 4.修改模板文件的后缀为.html. 好了,任

node.js 用socket实现聊天的示例代码

本文介绍了node.js 用socket实现聊天的示例代码,分享给大家,也给自己留个笔记,具体如下: 服务器搭建 app.js const http = require("http"); const express = require("./express"); //创建一个服务 const server = http.createServer(express); //监听服务端口 server.listen(8001,()=>{ console.log(&q

node.js中EJS 模板快速入门教程

Node 开源模板的选择很多,但推荐像我这样的老人去用 EJS,有 Classic ASP/PHP/JSP 的经验用起 EJS 来的确可以很自然,也就是说,你能够在 <%...%> 块中安排 JavaScript 代码,利用最传统的方式 <%=输出变量%>(另外 <%-输出变量是不会对 & 等符号进行转义的).安装 EJS 命令如下: npm install ejs JS 调用 JS 调用的方法主要有两个: ejs.compile(str, options); //

Node.js创建HTTP文件服务器的使用示例

HelloWorld示例只有演示意义,这次我们来搞一个实际的例子:文件服务器.我们使用Node.js创建一个HTTP协议的文件服务器,你可以使用浏览器或其它下载工具到文件服务器上下载文件. 为了读取文件,我们会用到File System模块(名字是"fs"),Stream,我们还要分析URL,区别HTTP方法,还会用到EventEmitter. 文件服务器FileServer的代码 先上代码吧,依然是简单的: // 引入http模块 var http = require("h

node express使用HTML模板的方法示例

前言 一般我们在做node web项目的时候,想使用我们平时常用的html模板,express默认使用jade模板,本身是没有HTML的,那么如何实现呢? ejs模板配置 使用 var app = express(); app.set('views', path.join(__dirname, 'views')); //设置视图引擎 app.set('view engine', 'ejs'); html模板配置 安装ejs npm install ejs -D 引入 var ejs = requ

node.js+captchapng+jsonwebtoken实现登录验证示例

提到登录验证,大家肯定能想到的就是12306的验证码了吧.12306为了防止刷票,可以说是煞费苦心,验证码也越来越难识别,最终即使是人也可能识别不了了. 今天,小编就给大家说一下node如何实现图片验证码,以及使用token验证登录.学习本文你将学到: 1.使用captchapng生成图片验证码 2.使用jsonwebtoken实现登录验证 一.图片验证码生成(最后有全部代码) 首先,我们理一下流程,第一步服务器要随机生成一组四位数. 第二步,将这四位数用canvas绘图生成图片. 第三步,我们

Node.js中的http请求客户端示例(request client)

Node.JS有一个request模块,可以很方便的抓取网页内容.最简单的一个示例: var request = require('request'); request('http://www.google.com', function (error, response, body) { if (!error && response.statusCode == 200) { console.log(body); } }) 由上例可以看出用request发起一个http请求确实非常简单,不过

node.js入门教程迷你书、node.js入门web应用开发完全示例

本书状态 你正在阅读的已经是本书的最终版.因此,只有当进行错误更正以及针对新版本Node.js的改动进行对应的修正时,才会进行更新. 本书中的代码案例都在Node.js 0.6.11版本中测试过,可以正确工作. 读者对象 本书最适合与我有相似技术背景的读者: 至少对一门诸如Ruby.Python.PHP或者Java这样面向对象的语言有一定的经验:对JavaScript处于初学阶段,并且完全是一个Node.js的新手. 这里指的适合对其他编程语言有一定经验的开发者,意思是说,本书不会对诸如数据类型