《浏览器工作原理与实践》是极客时间上的一个浏览器学习系列,在学习之后特在此做记录和总结。

一、执行流程

  实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中。

  一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进入执行阶段。

  下图把 JavaScript 的执行流程细化。

  

  从上图可以看出,输入一段代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。

  执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

  JavaScript 引擎开始执行“可执行代码”,按照顺序一行一行地执行。

二、调用栈

  调用栈就是用来管理函数调用关系的一种数据结构。

1)函数调用

  函数调用就是运行一个函数,具体使用方式是使用函数名称跟着一对小括号。

var a = 2
function add(){
var b = 10
return a + b
}
add()

  在执行到函数 add() 之前,JavaScript 引擎会为上面这段代码创建全局执行上下文,包含了声明的函数和变量,可以参考下图:

  

  执行上下文准备好之后,便开始执行全局代码,当执行到 add 这儿时,JavaScript 判断这是一个函数调用,那么将执行以下操作:

  (1)首先,从全局执行上下文中,取出 add 函数代码。

  (2)其次,对 add 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码。

  (3)最后,执行代码,输出结果。

  

2)JavaScript的调用栈

  在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。

var a = 2
function add(b, c){
return b + c
}
function addAll(b, c){
var d = 10
result = add(b, c)
return a + result + d
}
addAll(3,6)

  下面就一步步地分析在代码的执行过程中,调用栈的状态变化情况。

  (1)第一步,创建全局上下文,并将其压入栈底。

  (2)第二步,调用 addAll 函数。

  (3)第三步,当执行到 add 函数调用语句时,同样会为其创建执行上下文,并压入调用栈。

  

  (4)当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9。

  (5)紧接着 addAll 执行最后一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。

  当执行一段复杂的代码时,可能很难从代码文件中分析其调用关系,这时候可以在想要查看的函数中加入断点,然后当执行到该函数时,就可以查看该函数的调用栈了。

  还有一点你要注意,调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,这种错误叫做栈溢出(Stack Overflow)。

三、块级作用域

  作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

  块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

  ES6 引入了 let 和 const 关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域。

function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()

  接下来就来一步步分析上面这段代码的执行流程。

  (1)第一步,编译并创建执行上下文。

  函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。

  通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。

  在函数的作用域块内部,通过 let 声明的变量并没有被存放到词法环境中。

  

  (2)第二步,继续执行代码,当执行到代码块里面时,变量环境中 a 的值已经被设置成了 1,词法环境中 b 的值已经被设置成了 2。

  

  从图中可以看出,当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量 b,在该作用域块内部也声明了变量 b,当执行到作用域内部时,它们都是独立的存在。

  其实,在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。需要注意下,这里所讲的变量是指通过 let 或者 const 声明的变量。

四、作用域链和闭包

1)作用域链

  在每个执行上下文的变量环境中,都包含了一个称为 outer的外部引用,用来指向外部的执行上下文。

  当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量。如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。

  

2)词法作用域

  在 JavaScript 执行过程中,其作用域链是由词法作用域决定的。

  词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。

  

  从图中可以看出,词法作用域就是根据代码的位置来决定的,其中 main 函数包含了 bar 函数,bar 函数中包含了 foo 函数,因为 JavaScript 作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:foo 函数作用域—>bar 函数作用域—>main 函数作用域—> 全局作用域。

  词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

3)块级作用域中的变量查找

  分析下这段代码的执行流程。

function bar() {
var myName = "极客世界"
let test1 = 100
if (1) {
let myName = "Chrome浏览器"
console.log(test)
}
}
function foo() {
var myName = "极客邦"
let test = 2
{
let test = 3
bar()
}
}
var myName = "极客时间"
let myAge = 10
let test = 1
foo()

  对于上面这段代码,当执行到 bar 函数内部的 if 语句块时,其调用栈的情况如下图所示:

  

  查找 test 变量的值,其过程已经在上图中使用序号 1、2、3、4、5 标出。

  首先是在 bar 函数的执行上下文中查找,但因为 bar 函数的执行上下文中没有定义 test 变量,所以根据词法作用域的规则,下一步就在 bar 函数的外部作用域中查找,也就是全局作用域。

4)闭包

  这里可以结合下面这段代码来理解什么是闭包:

function foo() {
var myName = "极客时间"
let test1 = 1
const test2 = 2
var innerBar = {
getName: function(){
console.log(test1)
return myName
},
setName: function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

  首先看看当执行到 foo 函数内部的return innerBar这行代码时调用栈的情况。

  

  根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量。所以当 innerBar 对象返回给全局变量 bar 时,虽然 foo 函数已经执行结束,但是 getName 和 setName 函数依然可以使用 foo 函数中的变量 myName 和 test1。所以当 foo 函数执行完成之后,其整个调用栈的状态如下图所示:

  

  foo 函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的 setName 和 getName 方法中使用了 foo 函数内部的变量 myName 和 test1,所以这两个变量依然保存在内存中。这像极了 setName 和 getName 方法背的一个专属背包,之所以是专属背包,是因为除了 setName 和 getName 函数之外,其他任何地方都是无法访问该背包的,可以把这个背包称为 foo 函数的闭包。

  在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。

  当执行到 bar.setName 方法中的myName = "极客邦"这句代码时,JavaScript 引擎会沿着“当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量。

  可以通过“开发者工具”来看看闭包的情况,打开 Chrome 的“开发者工具”,在 bar 函数任意地方打上断点,然后刷新页面,可以看到如下内容:

  

  从图中可以看出来,当调用 bar.getName 的时候,右边 Scope 项就体现出了作用域链的情况:Local 就是当前的 getName 函数的作用域,Closure(foo) 是指 foo 函数的闭包,最下面的 Global 就是指全局作用域,从“Local–>Closure(foo)–>Global”就是一个完整的作用域链。

5)闭包回收

  如果引用闭包的函数是个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。

  如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。

  尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

五、this

  this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。

  执行上下文主要分为三种——全局执行上下文、函数执行上下文和 eval 执行上下文,所以对应的 this 也只有这三种——全局执行上下文中的 this、函数中的 this 和 eval 中的 this。

1)全局

  全局执行上下文中的 this 是指向 window 对象的。这也是 this 和作用域链的唯一交点,作用域链的最底端包含了 window 对象。

2)函数

  默认情况下调用一个函数,其执行上下文中的 this 也是指向 window 对象的。

  通常情况下,有下面三种方式来设置函数执行上下文中的 this 值。

  (1)通过函数的 call 方法设置。

  (2)通过对象调用方法设置。使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的。

  (3)通过构造函数中设置。当执行 new CreateObj() 的时候,JavaScript 引擎做了如下四件事:

    a、首先创建了一个空对象 tempObj;

    b、接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;

    c、然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;

    d、最后返回 tempObj 对象。

3)设计缺陷

  (1)嵌套函数中的 this 不会从外层函数中继承。

var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
function bar(){console.log(this)}
bar()
}
}
myObj.showThis()

  函数 bar 中的 this 指向的是全局 window 对象,而函数 showThis 中的 this 指向的是 myObj 对象。

  要解决这个问题,你可以有两种思路:

    a、第一种是把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数。

    b、第二种是继续使用 this,但是要把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的 this。

  (2)普通函数中的 this 默认指向全局对象 window。

  这个问题可以通过设置 JavaScript 的“严格模式”来解决。在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined。

六、数据存储

1)内存空间

  在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间。

  栈空间就是之前反复提及的调用栈,是用来存储执行上下文的。

  

  JavaScript 引擎并不是直接将该对象存放到变量环境中,而是将它分配到堆空间里面,分配后该对象会有一个在“堆”中的地址,然后再将该数据的地址写进 c 的变量值,最终分配好内存的示意图如下所示:

  

  原始类型的数据值都是直接保存在“栈”中的,引用类型的值是存放在“堆”中的。

  为什么一定要分“堆”和“栈”两个存储空间呢?

  因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。

  所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。

  在 JavaScript 中,赋值操作和其他语言有很大的不同,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。

2)再谈闭包

  探讨下闭包的内存模型,仍然采用之前的示例。

  画出执行到 foo 函数中“return innerBar”语句时的调用栈状态,如下图所示:

  

  当执行到 foo 函数时,闭包就产生了;当 foo 函数执行结束之后,返回的 getName 和 setName 方法都引用“clourse(foo)”对象,所以即使 foo 函数退出了,“clourse(foo)”依然被其内部的 getName 和 setName 方法引用。所以在下次调用bar.setName或者bar.getName时,创建的执行上下文中就包含了“clourse(foo)”。

  总的来说,产生闭包的核心有两步:第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中。

七、垃圾回收

1)栈中的数据回收

  通过一段示例代码的执行流程来分析其回收机制。

function foo(){
var a = 1
var b = {name:"极客邦"}
function showName(){
var c = 2
var d = {name:"极客时间"}
}
showName()
}
foo()

  有一个记录当前执行状态的指针(称为 ESP),指向调用栈中 showName 函数的执行上下文,表示当前正在执行 showName 函数。

  接着,当 showName 函数执行完成之后,函数执行流程就进入了 foo 函数,那这时就需要销毁 showName 函数的执行上下文了。

  JavaScript 会将 ESP 下移到 foo 函数的执行上下文,这个下移操作就是销毁 showName 函数执行上下文的过程。看下面这张移动 ESP 前后的对比图:

  

2)堆中的数据回收

  当上面那段代码的 foo 函数执行结束之后,ESP 应该是指向全局执行上下文的,那这样的话,showName 函数和 foo 函数的执行上下文就处于无效状态了,不过保存在堆中的两个对象依然占用着空间。

  要回收堆中的垃圾数据,就需要用到 JavaScript 中的垃圾回收器了。

  代际假说(The Generational Hypothesis)是垃圾回收领域中一个重要的术语,有以下两个特点:

  (1)第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;

  (2)第二个是不死的对象,会活得更久。

  在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。

  新生区通常只支持 1~8M 的容量,而老生区支持的容量就大很多了。对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。

  (1)副垃圾回收器,主要负责新生代的垃圾回收。

  (2)主垃圾回收器,主要负责老生代的垃圾回收。

  垃圾回收器的工作流程:

  (1)第一步是标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。

  (2)第二步是回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。

  (3)第三步是做内存整理,频繁回收对象后,内存中就会存在大量不连续空间,这些不连续的内存空间称为内存碎片。

  副垃圾回收器主要负责新生区的垃圾回收,新生代中用 Scavenge 算法来处理。所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。

  (1)首先要对对象区域中的垃圾做标记;

  (2)副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。

  (3)完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。

  主垃圾回收器主要负责老生区中的垃圾回收,采用标记 - 清除(Mark-Sweep)的算法来处理。

  (1)首先是标记过程阶段。标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。

  (2)接下来就是垃圾的清除过程。它和副垃圾回收器的垃圾清除过程完全不同,你可以理解这个过程是清除掉红色标记数据的过程。

  

  而碎片过多会导致大对象无法分配到足够的连续内存,于是又产生了另外一种算法——标记 - 整理(Mark-Compact),这个标记过程仍然与标记 - 清除算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  

3)全停顿

  由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行,这种行为叫做全停顿(Stop-The-World)。

  

  为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,把这个算法称为增量标记(Incremental Marking)算法。

  

  使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行上述动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。

八、编译器和解释器

  之所以存在编译器和解释器,是因为机器不能直接理解我们所写的代码,所以在执行程序之前,需要将我们所写的代码“翻译”成机器能读懂的机器语言。按语言的执行流程,可以把语言划分为编译型语言和解释型语言。

  (1)编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,就可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。

  (2)而由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。

  V8 在执行一段JavaScript代码的过程中既有解释器 Ignition(点火器),又有编译器 TurboFan(涡轮增压)。

  

  接下来就按照上图来一一分解其执行流程。

1)生成抽象语法树(AST)和执行上下文

  执行上下文主要是代码在执行过程中的环境信息。

  无论使用的是解释型语言还是编译型语言,在编译过程中,它们都会生成一个 AST。这和渲染引擎将 HTML 格式文件转换为计算机可以理解的 DOM 树的情况类似。

var myName = "极客时间"
function foo(){
return 23;
}
myName = "geektime"
foo()

  这段代码经过javascript-ast站点处理后,生成的 AST 结构如下:

  

  AST 的结构和代码的结构非常相似,其实也可以把 AST 看成代码的结构化的表示,编译器或者解释器后续的工作都需要依赖于 AST,而不是源代码。

  通常,生成 AST 需要经过两个阶段。

  (1)第一阶段是分词(tokenize),又称为词法分析,其作用是将一行行的源码拆解成一个个 token。所谓 token,指的是语法上不可能再分的、最小的单个字符或字符串。

  

  (2)第二阶段是解析(parse),又称为语法分析,其作用是将上一步生成的 token 数据,根据语法规则转为 AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。

2)生成字节码

  有了 AST 和执行上下文后,那接下来的第二步,解释器 Ignition 就登场了,它会根据 AST 生成字节码,并解释执行字节码。

  字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

  

  从图中可以看出,机器码所占用的空间远远超过了字节码,所以使用字节码可以减少系统的内存使用。

3)执行代码

  如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。

  解释器 Ignition(点火器) 除了负责生成字节码之外,它还有另外一个作用,就是解释执行字节码。

  在 Ignition 执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan(涡轮增压)就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了。

  其实字节码配合解释器和编译器是最近一段时间很火的技术,比如 Java 和 Python 的虚拟机也都是基于这种技术实现的,这种技术称为即时编译(JIT)。结合下图看看 JIT 的工作过程:

  

4)性能优化

  对于优化 JavaScript 执行效率,应该将优化的中心聚焦在单次脚本的执行时间和脚本的网络下载上,主要关注以下三点内容:

  (1)提升单次脚本的执行速度,避免 JavaScript 的长任务霸占主线程,这样可以使得页面快速响应交互;

  (2)避免大的内联脚本,因为在解析 HTML 的过程中,解析和编译也会占用主线程;

  (3)减少 JavaScript 文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存。

浏览器工作原理和实践(二)——JavaScript的更多相关文章

  1. 《浏览器工作原理与实践》<06>渲染流程(下):HTML、CSS和JavaScript,是如何变成页面的?

    在上篇文章中,我们介绍了渲染流水线中的 DOM 生成.样式计算和布局三个阶段,那今天我们接着讲解渲染流水线后面的阶段. 这里还是先简单回顾下上节前三个阶段的主要内容:在 HTML 页面内容被提交给渲染 ...

  2. 《浏览器工作原理与实践》<11>this:从JavaScript执行上下文的视角讲清楚this

    在上篇文章中,我们讲了词法作用域.作用域链以及闭包,接下来我们分析一下这段代码: var bar = { myName:"time.geekbang.com", printName ...

  3. 《浏览器工作原理与实践》<10>作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

    在上一篇文章中我们讲到了什么是作用域,以及 ES6 是如何通过变量环境和词法环境来同时支持变量提升和块级作用域,在最后我们也提到了如何通过词法环境和变量环境来查找变量,这其中就涉及到作用域链的概念. ...

  4. 《浏览器工作原理与实践》<08>调用栈:为什么JavaScript代码会出现栈溢出?

    在上篇文章中,我们讲到了,当一段代码被执行时,JavaScript 引擎先会对其进行编译,并创建执行上下文.但是并没有明确说明到底什么样的代码才算符合规范. 那么接下来我们就来明确下,哪些情况下代码才 ...

  5. 《浏览器工作原理与实践》<07>变量提升:JavaScript代码是按顺序执行的吗?

    讲解完宏观视角下的浏览器后,从这篇文章开始,我们就进入下一个新的模块了,这里我会对 JavaScript 执行原理做深入介绍. 今天在该模块的第一篇文章,我们主要讲解执行上下文相关的内容.那为什么先讲 ...

  6. 《浏览器工作原理与实践》<05>渲染流程(上):HTML、CSS和JavaScript,是如何变成页面的?

    在上一篇文章中我们介绍了导航相关的流程,那导航被提交后又会怎么样呢?就进入了渲染阶段.这个阶段很重要,了解其相关流程能让你“看透”页面是如何工作的,有了这些知识,你可以解决一系列相关的问题,比如能熟练 ...

  7. 《浏览器工作原理与实践》<01>Chrome架构:仅仅打开了1个页面,为什么有4个进程?

    无论你是想要设计高性能 Web 应用,还是要优化现有的 Web 应用,你都需要了解浏览器中的网络流程.页面渲染过程,JavaScript 执行流程,以及 Web 安全理论,而这些功能是分散在浏览器的各 ...

  8. 《浏览器工作原理与实践》 <12>栈空间和堆空间:数据是如何存储的?

    对于前端开发者来说,JavaScript 的内存机制是一个不被经常提及的概念 ,因此很容易被忽视.特别是一些非计算机专业的同学,对内存机制可能没有非常清晰的认识,甚至有些同学根本就不知道 JavaSc ...

  9. 《浏览器工作原理与实践》<09>块级作用域:var缺陷以及为什么要引入let和const?

    在前面我们已经讲解了 JavaScript 中变量提升的相关内容,正是由于 JavaScript 存在变量提升这种特性,从而导致了很多与直觉不符的代码,这也是 JavaScript 的一个重要设计缺陷 ...

  10. 《浏览器工作原理与实践》<04>从输入URL到页面展示,这中间发生了什么?

    “在浏览器里,从输入 URL 到页面展示,这中间发生了什么? ”这是一道经典的面试题,能比较全面地考察应聘者知识的掌握程度,其中涉及到了网络.操作系统.Web 等一系列的知识. 在面试应聘者时也必问这 ...

随机推荐

  1. 第五周单元测验题英语教学与互联网 mooc

    第五周单元测验题 返回 本次得分为:16.00/20.00, 本次测试的提交时间为:2020-08-30, 如果你认为本次测试成绩不理想,你可以选择 再做一次 . 1 单选(2分) 从评价的主体来看, ...

  2. React跨路由组件动画

    我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值. 本文作者:佳岚 回顾传统React动画 对于普通的 React 动画 ...

  3. 探索Redis与MySQL的双写问题

    本文已收录至GitHub,推荐阅读 Java随想录 微信公众号:Java随想录 原创不易,注重版权.转载请注明原作者和原文链接 目录 双写一致问题 缓存读写策略 Cache-Aside Pattern ...

  4. 手把手教你写一个JSON在线解析的前端网站1

    前言 作为一名Android开发,经常要跟后端同事联调接口,那么总避免不了要格式化接口返回值,将其转换为清晰直观高亮的UI样式以及折叠部分内容,方便我们查看定位关键的信息. 一直以来都是打开Googl ...

  5. ELK+ filebeat

    ELK 企业级日志分析系统 ELK 概述 1.ELK 简介 ELK平台是一套完整的日志集中处理解决方案,将 ElasticSearch.Logstash 和 Kiabana 三个开源工具配合使用, 完 ...

  6. Dynamics CRM中自定义页面实现附件管理包含下载模板、上传、下载、删除

    前言 附件使用的Dynamics CRM平台本身的注释表annotation存储,将附件转换成二进制字节流保存到数据库中,因自带的注释在页面中显示附件不够直观,特做了一个单独的附件管理自定义页面,通过 ...

  7. TS实现汉诺塔算法,以及图灵完备讨论

    之前在网上看到徐大佬更新的一篇文章: 用 TypeScript 类型运算实现一个中国象棋程序 在线预览地址:https://tsplay.dev/Nd4n0N 把鼠标放在最后几行的走棋结果上,惊喜的一 ...

  8. 聊聊BIO、NIO与AIO的区别(转)

    转自:https://www.cnblogs.com/blackjoyful/p/11534985.html 题目:说一下BIO/AIO/NIO 有什么区别?及异步模式的用途和意义? BIO:Apac ...

  9. SVN分支与合并透析

    做法: 1.拉分支开发代码 2.开发完成后要和到主干去发布,这时候先把主干合并到分支,解决冲突 3.运行分支系统,看是否正确 4.再把该分支合并到主干 要知道分支分出去时的版本号,cmd打开命令行,使 ...

  10. 「译文」Google SRE 二十年的经验教训

    ️URL: https://sre.google/resources/practices-and-processes/twenty-years-of-sre-lessons-learned/ ✍️Au ...