• home > webfront > ECMAS > javascript >

    读李老课程引发的思考之JS设计思想篇

    Author:zhoulujun Date:

    V8是如何执⾏⼀段JavaScript代码的?函数即对象,函数的特点?快属性和慢属性:V8采⽤了哪些策略提升了对象属性的访问速度?函数表达式:涉及⼤量概念,函数表达式到底该怎么学?作⽤域链:V8是如何查找变量的?

    目录如下

    • 一、V8是如何执⾏⼀段JavaScript代码的

      1. V8是怎么执⾏JavaScript代码的呢

      2. 什么是解释执行

      3. 什么是编译执行

      4. V8作为JavaScript的虚拟机的⼀种,?是解释执⾏,还是编译执⾏呢?

    • 二、函数即对象,函数的特点

      1. js是一门面向对象的语言吗?

      2. js与面向对象语言在继承上有什么区别?

      3. V8内部是怎么实现函数可调⽤特性的呢?

      4. function在JavaScript中是一等公民 ,何为一等公民?

      5. 什么是闭包?

    • 三、快属性和慢属性:V8采⽤了哪些策略提升了对象属性的访问速度?

      1. 什么是对象中的 常规属性和 排序属性

      2. 什么是对象内属性

      3. 什么是快属性和慢属性

    • 四、函数表达式:涉及⼤量概念,函数表达式到底该怎么学

      1. 函数声明与函数表达式的差异

      2. V8是怎么处理函数声明的?

      3. 什么是变量提升?

      4. 表达式和语句的区别是什么

      5. 函数声明是表达式还是语句呢?

      6. 为什么⽴即调⽤的函数表达式(IIFE)可以拥有私有作用域

      7. 变量a的值是什么

    • 五、作⽤域链:V8是如何查找变量的?

      1. 全局作⽤域和函数作⽤域

      2. 什么是词法作⽤域

      3. 什么是动态作用域和静态作用域

    • 六.类型转换:V8是怎么实现1-“2”的?

      1. V8是怎么实现1-“2”的

    一、V8是如何执⾏⼀段JavaScript代码的?

    1.V8是怎么执⾏JavaScript代码的呢

    其主要核⼼流程分为编译和执⾏两步。⾸先需要将JavaScript代码转换为低级中间代码或者机器能够理解的 机器代码,然后再执⾏转换后的代码并输出执⾏结果。

    读李老课程引发的思考之JS设计思想篇

    2.什么是解释执行

    解释执⾏,需要先将输⼊的源代码通过解析器编译成中间代码,之后直接使⽤解释器解释执⾏中间 代码,然后直接输出结果。具体流程如下图所⽰:

    读李老课程引发的思考之JS设计思想篇

    3.什么是编译执行

    编译执⾏。采⽤这种⽅式时,也需要先将源代码转换为中间代码,然后我们的编译器再将中间代码 编译成机器代码。通常编译成的机器代码是以⼆进制⽂件形式存储的,需要执⾏这段程序的时候直接执⾏⼆    进制⽂件就可以了。还可以使⽤虚拟机将编译后的机器代码保存在内存中,然后直接执⾏内存中的⼆进制代 码。

    什么是编译执行

    4.V8作为JavaScript的虚拟机的⼀种,是解释执⾏,还是编译执⾏呢?

    实际上,V8并没有采⽤某种单⼀的技术,⽽是混合编译执⾏和解释执⾏这两种⼿段,我们把这种混合使⽤    编译器和解释器的技术称为JIT(Just In Time)技术。这是⼀种权衡策略,因为这两种⽅法都各⾃有⾃的优缺点,解释执⾏的启动速度快,但是执⾏时的速度慢,⽽编译执⾏的启动速度慢,但是执⾏时的速度快。你可以参看下⾯完整的V8执⾏JavaScript的流程图:

    V8执行JavaScript代码解析图相信你注意到了,我们在解释器附近画了个监控机器⼈,这是⼀个监控解释器执⾏状态的模块,在解释执⾏    字节码的过程中,如果发现了某⼀段代码会被重复多次执⾏,那么监控机器⼈就会将这段代码标记为热点代 码。当某段代码被标记为热点代码后,V8就会将这段字节码丢给优化编译器,优化编译器会在后台将字节码编    译为⼆进制代码,然后再对编译后的⼆进制代码执⾏优化操作,优化后的⼆进制机器代码的执⾏效率会得到 ⼤幅提升。如果下⾯再执⾏到这段代码时,那么V8会优先选择优化之后的⼆进制代码,这样代码的执⾏速 度就会⼤幅提升。

    理解了这⼀点,我们就可以来深⼊分析V8执⾏⼀段JavaScript代码所经历的主要流程了,这包括了:

    1. 初始化基础环境;

    2. 解析源码⽣成AST和作⽤域;

    3. 依据AST和作⽤域⽣成字节码;

    4. 解释执⾏字节码;

    5. 监听热点代码;

    6. 优化热点代码为⼆进制的机器代码;

    7. 反优化⽣成的⼆进制机器代码

    这方安利下本站的文章:

    二、函数即对象,函数的特点

    1.js是一门面向对象的语言吗?

    不是的,js是基于对象设计的,但不是面向对象的语言

    2.js与面向对象语言在继承上有什么区别?

    ⾯向对象语⾔是由语⾔本⾝对继承做了充分的⽀持,并提供了⼤量的关键字,如public、protected、friend、interface等,众多的关键字使得⾯向对象语⾔的继承变得异常繁琐和复杂,「⽽JavaScript中实现继承的⽅式却⾮常简单清爽,只是在对象中添加了⼀个称为原型的属性,把继承的对象通过原型链接起来,就实现了继承,我们把这种继承⽅式称为基于原型链继承」

    3.V8内部是怎么实现函数可调⽤特性的呢?

    在V8内部,我们会为函数对象添加了两个隐藏属性,具体属性如下图所⽰:

    也就是说,函数除了可以拥有常⽤类型的属性值之外,还拥有两个隐藏属性,分别是name属性和code属    性。隐藏name属性的值就是函数名称,如果某个函数没有设置函数名,如下⾯这段函数:


    该函数对象的默认的name属性值就是anonymous,表⽰该函数对象没有被设置名称。另外⼀个隐藏属性是 code属性,其值表⽰函数代码,以字符串的形式存储在内存中。当执⾏到⼀个函数调⽤语句时,V8便会从    函数对象中取出code属性值,也就是函数代码,然后再解释执⾏这段函数代码。

    这部分安利本站文章:

    4.function在JavaScript中是一等公民 ,何为一等公民?

    一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量

    5.什么是闭包?

    将外部变量和和函数绑定起来的技术称为闭包

    function foo() {
      let number = 1;
      function bar() {
        number++;
        console.log(number);
      }
      return bar;
    }
    const mybar = foo();
    mybar();

    观察上段代码可以看到,我们在foo函数中定义了⼀个新的bar函数,并且bar函数引⽤了foo函数中的变量 number,当调⽤foo函数的时候,它会返回bar函数。

    本站解析:

    三、快属性和慢属性:V8采⽤了哪些策略提升了对象属性的访问速度?

    1.什么是对象中的 常规属性和 排序属性

    function Foo() {
      this[100] = 'test-100';
      this[1] = 'test-1';
      this.B = 'bar-B';
      this[50] = 'test-50';
      this[9] = 'test-9';
      this[8] = 'test-8';
      this[3] = 'test-3';
      this[5] = 'test-5';
      this.A = 'bar-A';
      this.C = 'bar-C';
    }
    const bar = new Foo();
    for (const key in bar) {
      console.log(`index:${key} value:${bar[key]}`);
    }

    在上⾯这段代码中,我们利⽤构造函数Foo创建了⼀个bar对象,在构造函数中,我们给bar对象设置了很多 属性,包括了数字属性和字符串属性,然后我们枚举出来了bar对象中所有的属性,并将其⼀⼀打印出来,    下⾯就是执⾏这段代码所打印出来的结果

    index:1 value:test-1
    index:3 value:test-3
    index:5 value:test-5
    index:8 value:test-8
    index:9 value:test-9
    index:50 value:test-50
    index:100 value:test-100
    index:B value:bar-B
    index:A value:bar-A
    index:C value:bar-C

    观察这段打印出来的数据,我们发现打印出来的属性顺序并不是我们设置的顺序,我们设置属性的时候是乱 序设置的,⽐如开始先设置100,然后有设置了1,但是输出的内容却⾮常规律,总的来说体现在以下两 点:

    • 设置的数字属性被最先打印出来了,并且按照数字⼤⼩的顺序打印的;

    • 设置的字符串属性依然是按照之前的设置顺序打印的,⽐如我们是按照B、A、C的顺序设置的,打印出来,依然是这个顺序。

    之所以出现这样的结果,是因为在ECMAScript规范中定义了 「数字属性应该按照索引值⼤⼩升序排列,字符 串属性根据创建时的顺序升序排列。」在这⾥我们把对象中的数字属性称为 「排序属性」,在V8中被称为    elements,字符串属性就被称为 「常规属性」, 在V8中被称为 properties。在V8内部,为了有效地提升存储和访问这两种属性的性能,分别使⽤了两个 线性数据结构来分别保存排序    属性和常规属性,具体结构如下图所⽰:

    在elements对象中,会按照顺序存放排序属性,properties属性则指向了properties对    象,在properties对象中,会按照创建时的顺序保存了常规属性。

    这里可以看一下:《JS遍历循环方法性能对比:for/while/for in/for of/map/foreach/every

    2.什么是对象内属性

    将不同的属性分别保存到elements属性和properties属性中,⽆疑简化了程序的复杂度,但是在查找元素 时,却多了⼀步操作,⽐如执⾏ bar.B这个语句来查找B的属性值,那么在V8会先查找出properties属性所    指向的对象properties,然后再在properties对象中查找B属性,这种⽅式在查找过程中增加了⼀步操作,因此会影响到元素的查找效率。基于这个原因,V8采取了⼀个权衡的策略以加快查找属性的效率,这个策略是将部分常规属性直接存储到 对象本⾝,我们把这称为    「对象内属性(in-object properties)」。对象在内存中的展现形式你可以参看下图:

    采⽤对象内属性之后,常规属性就被保存到bar对象本⾝了,这样当再次使⽤bar.B来查找B的属性值时,V8就可以直接从bar对象本⾝去获取该值就可以了,这种⽅式减少查找属性值的步骤,增加了查找效率。不过对象内属性的数量是固定的,默认是10个,如果添加的属性超出了对象分配的空间,则它们将被保存在常规属性存储中。虽然属性存储多了⼀层间接层,但可以⾃由地扩容。

    3.什么是快属性和慢属性

    通常,我们将保存在线性数据结构中的属性称之为“快属性”,因为线性数据结构中只需要通过索引即可以 访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除⼤量的属性时,则执⾏效率会⾮常低,这主要因为会产⽣⼤量时间和内存开销。因此,如果⼀个对象的属性过多时,V8为就会采取另外⼀种存储策略,那就是“慢属性”策略,但慢属性的对象内部会有独⽴的⾮线性数据结构(词典)作为属性存储容器。所有的属性元信息不再是线性存储的,⽽ 是直接保存在属性字典中。

    四、函数表达式:涉及⼤量概念,函数表达式到底该怎么学?

    1.函数声明与函数表达式的差异

    同样是在定义的函数之前调⽤函数,第⼀段代码就可以正确执⾏,⽽第⼆段代码却报错,这是为什么呢?其主要原因是这两种定义函数的⽅式具有不同语义,不同的语义触发了不同的⾏为。

    因为语义不同,所以我们给这两种定义函数的⽅式使⽤了不同的名称,第⼀种称之为 「函数声明」,第⼆种称之 为「函数表达式」

    2.V8是怎么处理函数声明的?

    V8在执⾏JavaScript的过程中,会先对其进⾏编译,然后再执⾏,⽐如下⾯这段代码:

    const x = 5;
    function foo() {
      console.log('Foo');
    }

    V8执⾏这段代码的流程⼤致如下图所⽰:

    V8是怎么处理函数声明的

    在编译阶段,如果解析到函数声明,那么V8会将这个函数声明转换为内存中的函数对象(「函数名放在栈,函数体放在堆」),并将其放到作⽤    域中。同样,如果解析到了某个变量声明,也会将其放到作⽤域中,但是会将其值设置为undefined,表⽰ 该变量还未被使⽤。

    然后在V8执⾏阶段,如果使⽤了某个变量,或者调⽤了某个函数,那么V8便会去作⽤域查找相关内容。

    3.什么是变量提升?

    因为在执⾏之前,这些变量都被提升到作⽤域中了,所以在执⾏阶段,V8当然就能获取到所有的定义变量 了。我们把这种在编译阶段,将所有的变量提升到作⽤域的过程称为「变量提升」

    4.表达式和语句的区别是什么

    简单地理解,表达式就是表⽰值的式⼦,⽽语句是操作值的式⼦。

    ⽐如:

    x = 5

    就是表达式,因为执⾏这段代码,它会返回⼀个值。同样,6 === 5也是⼀个表达式,因为它会返回 False。

    ⽽语句则不同了,⽐如你定义了⼀个变量:

    var x;

    这就是⼀个语句,执⾏该语句时,V8并不会返回任何值给你。同样,当我声明了⼀个函数时,这个函数声明也是⼀个语句,⽐如下⾯这段函数声明:

    function foo(){  return 1}

    当执⾏到这段代码时,V8并没有返回任何的值,它只是解析foo函数,并将函数对象存储到内存中。

    「这么一来就说明了,语句的执行是 编译阶段,把变量放到作用域,导致变量提升,表达式的执行是在执行阶段,导致作用域中的变量的值的改变。」

    5.函数声明是表达式还是语句呢?

    function foo(){  console.log('Foo')}

    执⾏上⾯这段代码,它并没有输出任何内容,所以可以肯定,函数声明并不是⼀个表达式,⽽是⼀个语句。

    总的来说,在V8解析JavaScript源码的过程中,如果遇到普通的变量声明,那么便会将其提升到作⽤域中, 并给该变量赋值为undefined,如果遇到的是函数声明,那么V8会在内存中为声明⽣成函数对象,并将该对    象提升到作⽤域中。

    函数声明是表达式

    我觉得最有意思的是下面这道题

    6.为什么⽴即调⽤的函数表达式(IIFE)可以拥有私有作用域

    JavaScript中有⼀个圆括号运算符,圆括号⾥⾯可以放⼀个表达式,⽐如下⾯的代码:

    (a=3)

    括号⾥⾯是⼀个表达式,整个语句也是⼀个表达式,最终输出3。如果在⼩括号⾥⾯放上⼀段函数的定义,如下所⽰:

    (function () {//statements})

    因为⼩括号之间存放的必须是表达式,所以如果在⼩阔号⾥⾯定义⼀个函数,那么V8就会把这个函数看成 是函数表达式,执⾏时它会返回⼀个函数对象

    存放在括号⾥⾯的函数便是⼀个函数表达式,它会返回⼀个函数对象,如果我直接在表达式后⾯加上调⽤的 括号,这就称 ⽴即调⽤函数表达式(IIFE),⽐如下⾯代码:

    (function () {  //statements})()

    因为函数⽴即表达式也是⼀个表达式,所以V8在编译阶段,并不会为该表达式创建函数对象。这样的⼀个 好处就是不会污染环境,函数和函数内部的变量都不会被其他部分的代码访问到。

    7. 变量a的值是什么

    var a = (function () {  return 1})()

    因为函数⽴即表达式是⽴即执⾏的,所以将⼀个函数⽴即表达式赋给⼀个变量时,不是存储?IIFE?本 ⾝,⽽是存储?IIFE?执⾏后返回的结果,所以a=1。

    五、作⽤域链:V8是如何查找变量的?

    1.全局作⽤域和函数作⽤域

    全局作⽤域和函数作⽤域类似,也是存放变量和函数的地⽅,但是它们还是有点不⼀样:

    • 全局作⽤域是在 V8启动过程中就创建了,且⼀直保存在内存中不会被销毁的,直⾄V8退出。 

    • 函数作⽤域是在执⾏该函数    时创建的,当函数执⾏结束之后,函数作⽤域就随之被销毁掉了。

    2.什么是词法作⽤域

    因为JavaScript是基于词法作⽤域的,词法作⽤域就是指,查找作⽤域的顺序是按照函数定义时的位置来决定的

    bar和foo函数的外部代码都是全局代码,所以⽆论你是在bar函数中查找变量,还是在foo函数中查找变量,其查找顺序都是按照当前函数作⽤域‒>全局作⽤域 这个路径来的。

    由于我们代码中的foo函数和bar函数都是在全局下⾯定义的,所以在foo函数中使⽤了type,最终打印出来 的值就是全局作⽤域中的type。

    词法作⽤域

    3.什么是动态作用域和静态作用域

    因为词法作⽤域是根据函数在代码中的位置来确定的,作⽤域是在声明函数时就确定好的了,所以我们也将词法作⽤域称为静态作⽤域。和静态作⽤域相对的是动态作⽤域,动态作⽤域并不关⼼函数和作⽤域是如何声明以及在何处声明的,只关 ⼼它们从何处调⽤。换句话说,作⽤域链是基于调⽤栈的,⽽不是基于函数定义的位置的

    六.类型转换:V8是怎么实现1-“2”的?

    1.V8是怎么实现1-“2”的

    V8会提供了⼀个ToPrimitve⽅法,其作⽤是将a和b转换为原⽣数据类型,其转换流程如下:

    • 先检测该对象中是否存在valueOf⽅法,如果有并返回了原始类型,那么就使⽤该值进⾏强制类型转换;

    • 如果valueOf没有返回原始类型,那么就使⽤toString⽅法的返回值;

    • 如果vauleOf和toString两个⽅法都不返回基本类型值,便会触发⼀个TypeError的错误。

    将对象转换为原⽣类型的流程图如下所⽰:

    读李老课程引发的思考之JS设计思想篇

    当V8执⾏1+“2”时,因为这是两个原始值相加,原始值相加的时候,如果其中⼀项是字符串,那么V8会默    认将另外⼀个值也转换为字符串,相当于执⾏了下⾯的操作:

    Number(1).toString() + "2"

    「注意」:上面valueOf和toString的调用顺序仅适用于 运算。其他情况可以参考:8. {} 和 [] 的 valueOf 和 toString 的结果是什么?.


    转载本站文章《读李老课程引发的思考之JS设计思想篇》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js/2021_0823_8671.html